From 2e851b02e875d012c27f59388cb595569de7b5af Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 21 Apr 2026 07:26:46 +0200 Subject: [PATCH 1/6] Inject live-debugger build metadata for SDK init Expose the configured `liveDebugger.version` in the injected runtime bootstrap so the Browser Debugger SDK can default its runtime version from build metadata. Move the bootstrap logic into a dedicated helper and document the new behavior. --- packages/plugins/live-debugger/README.md | 10 +++++++++ .../plugins/live-debugger/src/constants.ts | 8 ------- .../plugins/live-debugger/src/index.test.ts | 20 ++++++++++++++++-- packages/plugins/live-debugger/src/index.ts | 5 +++-- .../live-debugger/src/runtime-bootstrap.ts | 21 +++++++++++++++++++ 5 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 packages/plugins/live-debugger/src/runtime-bootstrap.ts diff --git a/packages/plugins/live-debugger/README.md b/packages/plugins/live-debugger/README.md index 24952d8ff..597c71a15 100644 --- a/packages/plugins/live-debugger/README.md +++ b/packages/plugins/live-debugger/README.md @@ -133,8 +133,18 @@ Optional. When set, use an immutable deployed browser build identifier. This val - the `version` passed to `@datadog/browser-debugger` - `errorTracking.sourcemaps.releaseVersion` when sourcemap upload is enabled +When this option is set, the plugin also injects the same value into runtime-visible build metadata so the Browser Debugger SDK can use it as the default `version` during `init()`. + If omitted, Live Debugger instrumentation still works, but browser build lookup and source-code-aware resolution will gracefully degrade. +> **Prerequisites for source-code resolution.** Browser build lookup resolves `applicationId + version` to repository metadata via the RUM application model. This requires: +> +> 1. A [RUM application](https://docs.datadoghq.com/real_user_monitoring/browser) whose `applicationId` is passed to the Browser Debugger SDK. +> 2. The RUM application must have a `service` tag configured — this is how the backend maps the application to the service that owns the source code. +> 3. Sourcemaps must be uploaded with the same `version` (via `errorTracking.sourcemaps.releaseVersion`) so the backend can resolve the service + version to a repository URL and commit SHA. +> +> If any of these are missing, source-code resolution will gracefully degrade to the "no source code" state. + ### liveDebugger.include > default: `[/\.[jt]sx?$/]` diff --git a/packages/plugins/live-debugger/src/constants.ts b/packages/plugins/live-debugger/src/constants.ts index c36ae343a..137408d06 100644 --- a/packages/plugins/live-debugger/src/constants.ts +++ b/packages/plugins/live-debugger/src/constants.ts @@ -9,11 +9,3 @@ export const PLUGIN_NAME: PluginName = 'datadog-live-debugger-plugin' as const; // Skip instrumentation comment export const SKIP_INSTRUMENTATION_COMMENT = '@dd-no-instrumentation'; - -// Minimal no-op stub injected into all chunks as a banner. -// $dd_probes is called unconditionally by every instrumented function; -// $dd_entry, $dd_return, and $dd_throw are guarded by `if (probe)` so -// they only need to exist once the SDK activates probes. -// When the Datadog Browser Debugger SDK loads, its init() overwrites -// $dd_probes with the real implementation. -export const RUNTIME_STUBS = `if(typeof globalThis.$dd_probes==='undefined'){globalThis.$dd_probes=function(){}}`; diff --git a/packages/plugins/live-debugger/src/index.test.ts b/packages/plugins/live-debugger/src/index.test.ts index e351d4084..08d2cfd0f 100644 --- a/packages/plugins/live-debugger/src/index.test.ts +++ b/packages/plugins/live-debugger/src/index.test.ts @@ -6,8 +6,9 @@ import { InjectPosition } from '@dd/core/types'; import { getContextMock, getGetPluginsArg } from '@dd/tests/_jest/helpers/mocks'; import type { UnpluginBuildContext, UnpluginContext } from 'unplugin'; -import { PLUGIN_NAME, RUNTIME_STUBS } from './constants'; +import { PLUGIN_NAME } from './constants'; import { getLiveDebuggerPlugin, getPlugins } from './index'; +import { getRuntimeBootstrap } from './runtime-bootstrap'; import type { LiveDebuggerOptionsWithDefaults } from './types'; const makeOptions = ( @@ -324,7 +325,22 @@ describe('getPlugins', () => { type: 'code', position: InjectPosition.BEFORE, injectIntoAllChunks: true, - value: RUNTIME_STUBS, + value: getRuntimeBootstrap(), + }); + }); + + it('should inject build metadata when a version is provided', () => { + const arg = getGetPluginsArg({ liveDebugger: { version: '1.0.0' } }); + + const plugins = getPlugins(arg); + + expect(plugins).toHaveLength(1); + expect(plugins[0].name).toBe(PLUGIN_NAME); + expect(arg.context.inject).toHaveBeenCalledWith({ + type: 'code', + position: InjectPosition.BEFORE, + injectIntoAllChunks: true, + value: getRuntimeBootstrap('1.0.0'), }); }); }); diff --git a/packages/plugins/live-debugger/src/index.ts b/packages/plugins/live-debugger/src/index.ts index dc504cf65..c88e5b9da 100644 --- a/packages/plugins/live-debugger/src/index.ts +++ b/packages/plugins/live-debugger/src/index.ts @@ -5,7 +5,8 @@ import type { GetPlugins, GlobalContext, PluginOptions } from '@dd/core/types'; import { InjectPosition } from '@dd/core/types'; -import { CONFIG_KEY, PLUGIN_NAME, RUNTIME_STUBS } from './constants'; +import { CONFIG_KEY, PLUGIN_NAME } from './constants'; +import { getRuntimeBootstrap } from './runtime-bootstrap'; import { transformCode } from './transform'; import type { LiveDebuggerOptions, LiveDebuggerOptionsWithDefaults } from './types'; import { validateOptions } from './validate'; @@ -152,7 +153,7 @@ export const getPlugins: GetPlugins = ({ options, context }) => { type: 'code', position: InjectPosition.BEFORE, injectIntoAllChunks: true, - value: RUNTIME_STUBS, + value: getRuntimeBootstrap(validatedOptions.version), }); return [getLiveDebuggerPlugin(validatedOptions, context)]; diff --git a/packages/plugins/live-debugger/src/runtime-bootstrap.ts b/packages/plugins/live-debugger/src/runtime-bootstrap.ts new file mode 100644 index 000000000..7790c4ccc --- /dev/null +++ b/packages/plugins/live-debugger/src/runtime-bootstrap.ts @@ -0,0 +1,21 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +// Minimal runtime stub injected into all chunks. +// `$dd_probes` is called unconditionally by instrumented functions, so it must +// exist before the Browser Debugger SDK initializes. The SDK's `init()` later +// replaces the stubbed globals with the real implementations. +const runtimeStubs = `if(typeof globalThis.$dd_probes==='undefined'){globalThis.$dd_probes=function(){}}`; +const buildMetadataGlobal = '__DD_LIVE_DEBUGGER_BUILD__' as const; + +// Build the runtime bootstrap injected into all chunks. When +// `liveDebugger.version` is configured, also expose build metadata so the +// Browser Debugger SDK can default its runtime version from the injected value. +export const getRuntimeBootstrap = (version?: string): string => { + if (version === undefined) { + return runtimeStubs; + } + + return `${runtimeStubs};if(typeof globalThis.${buildMetadataGlobal}==='undefined'){globalThis.${buildMetadataGlobal}={version:${JSON.stringify(version)}}}`; +}; From eeba6829de02b0deed19b0384766b134d5f2b76a Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 28 Apr 2026 06:36:05 +0200 Subject: [PATCH 2/6] Source live-debugger version from shared metadata.version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deployed build identifier is a cross-plugin concern rather than a Live Debugger one. Move it from `liveDebugger.version` onto the shared `metadata.version` field, alongside the existing `metadata.name`. Type-validation of the shared `metadata` block is centralized in the factory's `validateOptions`, so consumers don't each re-implement it. Only `metadata.version` is validated today: a TODO in factory/validate.ts flags that `metadata.name` should also be tightened in the next major, since the root README has long documented its default as `null`, and adding the check now would break users who took that literally. The Live-Debugger-specific invariant — `metadata.version` must match `errorTracking.sourcemaps.releaseVersion` when both are configured — stays in the live-debugger validator, since that's the plugin enforcing it. --- packages/core/src/types.ts | 1 + packages/factory/src/validate.test.ts | 71 ++++++++++ packages/factory/src/validate.ts | 28 +++- packages/plugins/live-debugger/README.md | 26 +++- .../plugins/live-debugger/src/index.test.ts | 24 +++- .../live-debugger/src/runtime-bootstrap.ts | 2 +- packages/plugins/live-debugger/src/types.ts | 1 - .../live-debugger/src/validate.test.ts | 125 ++++++++++-------- .../plugins/live-debugger/src/validate.ts | 16 +-- 9 files changed, 216 insertions(+), 78 deletions(-) create mode 100644 packages/factory/src/validate.test.ts diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 527530fee..59bcff65f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -92,6 +92,7 @@ export type Timer = { export type BuildMetadata = { name?: string; + version?: string; }; export type BuildReport = { diff --git a/packages/factory/src/validate.test.ts b/packages/factory/src/validate.test.ts new file mode 100644 index 000000000..18da6de83 --- /dev/null +++ b/packages/factory/src/validate.test.ts @@ -0,0 +1,71 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +import type { Options } from '@dd/core/types'; + +import { validateOptions } from './validate'; + +describe('factory validateOptions', () => { + describe('defaults', () => { + it('should return defaults when no options are provided', () => { + expect(validateOptions()).toEqual( + expect.objectContaining({ + enableGit: true, + logLevel: 'warn', + metadata: {}, + }), + ); + }); + + it('should preserve user-provided metadata', () => { + const result = validateOptions({ + metadata: { name: 'my-build', version: '1.0.0' }, + }); + expect(result.metadata).toEqual({ name: 'my-build', version: '1.0.0' }); + }); + + it('should accept metadata with only name set', () => { + const result = validateOptions({ metadata: { name: 'my-build' } }); + expect(result.metadata).toEqual({ name: 'my-build' }); + }); + + it('should accept metadata with only version set', () => { + const result = validateOptions({ metadata: { version: '1.0.0' } }); + expect(result.metadata).toEqual({ version: '1.0.0' }); + }); + + it('should accept an empty metadata block', () => { + const result = validateOptions({ metadata: {} }); + expect(result.metadata).toEqual({}); + }); + }); + + describe('metadata validation', () => { + const cases = [ + { + description: 'reject metadata.version when not a string', + input: { metadata: { version: 123 } }, + errorPattern: /metadata\.version.*must be a string/, + }, + { + description: 'reject metadata.version when null', + input: { metadata: { version: null } }, + errorPattern: /metadata\.version.*must be a string/, + }, + ]; + + test.each(cases)('should $description', ({ input, errorPattern }) => { + expect(() => validateOptions(input as unknown as Options)).toThrow(errorPattern); + }); + + it('should accept non-string metadata.name for backwards compatibility', () => { + expect(() => + validateOptions({ metadata: { name: 123 } } as unknown as Options), + ).not.toThrow(); + expect(() => + validateOptions({ metadata: { name: null } } as unknown as Options), + ).not.toThrow(); + }); + }); +}); diff --git a/packages/factory/src/validate.ts b/packages/factory/src/validate.ts index 67ccb6abc..0d523f498 100644 --- a/packages/factory/src/validate.ts +++ b/packages/factory/src/validate.ts @@ -3,9 +3,35 @@ // Copyright 2019-Present Datadog, Inc. import { getDDEnvValue } from '@dd/core/helpers/env'; -import type { AuthOptionsWithDefaults, Options, OptionsWithDefaults } from '@dd/core/types'; +import type { + AuthOptionsWithDefaults, + BuildMetadata, + Options, + OptionsWithDefaults, +} from '@dd/core/types'; + +const validateMetadata = (metadata: BuildMetadata | undefined): string[] => { + const errors: string[] = []; + if (metadata === undefined) { + return errors; + } + // TODO(next-major): also reject non-string `metadata.name`. Skipped today + // because `metadata.name` has historically been unvalidated (and the root + // README documents its default as `null`), so adding a type-check here + // would be a breaking change for users who took the docs literally. + if (metadata.version !== undefined && typeof metadata.version !== 'string') { + errors.push('metadata.version must be a string'); + } + return errors; +}; export const validateOptions = (options: Options = {}): OptionsWithDefaults => { + const errors: string[] = [...validateMetadata(options.metadata)]; + + if (errors.length) { + throw new Error(`Invalid Datadog plugin configuration:\n - ${errors.join('\n - ')}`); + } + const auth: AuthOptionsWithDefaults = { // DATADOG_SITE env var takes precedence over configuration site: getDDEnvValue('SITE') || options.auth?.site || 'datadoghq.com', diff --git a/packages/plugins/live-debugger/README.md b/packages/plugins/live-debugger/README.md index 597c71a15..b51ebdc4f 100644 --- a/packages/plugins/live-debugger/README.md +++ b/packages/plugins/live-debugger/README.md @@ -13,7 +13,7 @@ Automatically instrument JavaScript functions at build time to enable Live Debug - [Configuration](#configuration) - [How it works](#how-it-works) - [liveDebugger.enable](#livedebuggerenable) - - [liveDebugger.version](#livedebuggerversion) + - [metadata.version](#metadataversion) - [liveDebugger.include](#livedebuggerinclude) - [liveDebugger.exclude](#livedebuggerexclude) - [liveDebugger.honorSkipComments](#livedebuggerhonorskipcomments) @@ -51,7 +51,6 @@ the plugin throws an error with the exact install command above. ```ts liveDebugger?: { enable?: boolean; - version?: string; include?: (string | RegExp)[]; exclude?: (string | RegExp)[]; honorSkipComments?: boolean; @@ -60,6 +59,8 @@ liveDebugger?: { } ``` +Live Debugger also reads the build version from the top-level [`metadata.version`](#metadataversion) option. + ## How it works The Live Debugger plugin automatically instruments all JavaScript functions in your application at build time. It adds lightweight checks that can be activated at runtime without rebuilding your code. @@ -74,7 +75,7 @@ Each instrumented function gets: The instrumentation checks whether probes are active by calling `$dd_probes(functionId)`. When no probes are active, the function returns `undefined` and all instrumentation is skipped — only the `$dd_probes` call and a conditional check remain on the hot path. -When `liveDebugger.version` is set, it should match the immutable deployed build identifier used by your Browser Debugger SDK initialization. If you also upload sourcemaps through the Error Tracking plugin, use the same value for `errorTracking.sourcemaps.releaseVersion`. +When `metadata.version` is set, it should match the immutable deployed build identifier used by your Browser Debugger SDK initialization. If you also upload sourcemaps through the Error Tracking plugin, use the same value for `errorTracking.sourcemaps.releaseVersion`. **Example transformation (block body):** @@ -126,14 +127,25 @@ const double = (x) => { Enable or disable the plugin without removing its configuration. -### liveDebugger.version +### metadata.version + +> default: `undefined` + +An immutable identifier for the deployed browser build. Set it at the top level of your Datadog plugin configuration: + +```ts +datadogBuildPlugins({ + metadata: { version: '1.0.0' }, + liveDebugger: {}, +}); +``` -Optional. When set, use an immutable deployed browser build identifier. This value should match: +This value should match: -- the `version` passed to `@datadog/browser-debugger` +- the `version` passed to `@datadog/browser-debugger`'s `init()` - `errorTracking.sourcemaps.releaseVersion` when sourcemap upload is enabled -When this option is set, the plugin also injects the same value into runtime-visible build metadata so the Browser Debugger SDK can use it as the default `version` during `init()`. +When set, Live Debugger also injects the value into runtime-visible build metadata so the Browser Debugger SDK can use it as the default `version` during `init()`. If omitted, Live Debugger instrumentation still works, but browser build lookup and source-code-aware resolution will gracefully degrade. diff --git a/packages/plugins/live-debugger/src/index.test.ts b/packages/plugins/live-debugger/src/index.test.ts index 08d2cfd0f..331427cc4 100644 --- a/packages/plugins/live-debugger/src/index.test.ts +++ b/packages/plugins/live-debugger/src/index.test.ts @@ -329,8 +329,11 @@ describe('getPlugins', () => { }); }); - it('should inject build metadata when a version is provided', () => { - const arg = getGetPluginsArg({ liveDebugger: { version: '1.0.0' } }); + it('should inject build metadata when metadata.version is provided', () => { + const arg = getGetPluginsArg({ + liveDebugger: {}, + metadata: { version: '1.0.0' }, + }); const plugins = getPlugins(arg); @@ -343,4 +346,21 @@ describe('getPlugins', () => { value: getRuntimeBootstrap('1.0.0'), }); }); + + it('should not inject build metadata when only metadata.name is provided', () => { + const arg = getGetPluginsArg({ + liveDebugger: {}, + metadata: { name: 'my-build' }, + }); + + const plugins = getPlugins(arg); + + expect(plugins).toHaveLength(1); + expect(arg.context.inject).toHaveBeenCalledWith({ + type: 'code', + position: InjectPosition.BEFORE, + injectIntoAllChunks: true, + value: getRuntimeBootstrap(), + }); + }); }); diff --git a/packages/plugins/live-debugger/src/runtime-bootstrap.ts b/packages/plugins/live-debugger/src/runtime-bootstrap.ts index 7790c4ccc..dde314b75 100644 --- a/packages/plugins/live-debugger/src/runtime-bootstrap.ts +++ b/packages/plugins/live-debugger/src/runtime-bootstrap.ts @@ -10,7 +10,7 @@ const runtimeStubs = `if(typeof globalThis.$dd_probes==='undefined'){globalThis. const buildMetadataGlobal = '__DD_LIVE_DEBUGGER_BUILD__' as const; // Build the runtime bootstrap injected into all chunks. When -// `liveDebugger.version` is configured, also expose build metadata so the +// `metadata.version` is configured, also expose build metadata so the // Browser Debugger SDK can default its runtime version from the injected value. export const getRuntimeBootstrap = (version?: string): string => { if (version === undefined) { diff --git a/packages/plugins/live-debugger/src/types.ts b/packages/plugins/live-debugger/src/types.ts index 0ac32fddd..98afc0bc9 100644 --- a/packages/plugins/live-debugger/src/types.ts +++ b/packages/plugins/live-debugger/src/types.ts @@ -15,7 +15,6 @@ export type FunctionKind = (typeof VALID_FUNCTION_KINDS)[number]; export type LiveDebuggerOptions = { enable?: boolean; - version?: string; include?: (string | RegExp)[]; exclude?: (string | RegExp)[]; honorSkipComments?: boolean; diff --git a/packages/plugins/live-debugger/src/validate.test.ts b/packages/plugins/live-debugger/src/validate.test.ts index a40654857..98bbf7244 100644 --- a/packages/plugins/live-debugger/src/validate.test.ts +++ b/packages/plugins/live-debugger/src/validate.test.ts @@ -17,8 +17,8 @@ const mockLogger: Logger = { debug: jest.fn(), }; -const makeConfig = (liveDebugger?: unknown, errorTracking?: unknown): Options => - ({ liveDebugger, errorTracking }) as unknown as Options; +const makeConfig = (liveDebugger?: unknown, errorTracking?: unknown, metadata?: unknown): Options => + ({ liveDebugger, errorTracking, metadata }) as unknown as Options; beforeEach(() => { jest.clearAllMocks(); @@ -45,6 +45,11 @@ describe('validateOptions', () => { input: makeConfig({ enable: false }), expected: expect.objectContaining({ enable: false, version: undefined }), }, + { + description: 'honor enable: false even when metadata.version is provided', + input: makeConfig({ enable: false }, undefined, { version: '1.0.0' }), + expected: expect.objectContaining({ enable: false, version: '1.0.0' }), + }, { description: 'enable and return defaults when an empty object is provided', input: makeConfig({}), @@ -59,13 +64,13 @@ describe('validateOptions', () => { } satisfies LiveDebuggerOptionsWithDefaults, }, { - description: 'honor enable: true when version is provided', - input: makeConfig({ enable: true, version: '1.0.0' }), + description: 'honor enable: true and forward metadata.version', + input: makeConfig({ enable: true }, undefined, { version: '1.0.0' }), expected: expect.objectContaining({ enable: true, version: '1.0.0' }), }, { - description: 'enable when a config object with version is provided', - input: makeConfig({ version: '1.0.0' }), + description: 'enable and forward metadata.version when liveDebugger is empty', + input: makeConfig({}, undefined, { version: '1.0.0' }), expected: { enable: true, version: '1.0.0', @@ -76,6 +81,16 @@ describe('validateOptions', () => { namedOnly: false, } satisfies LiveDebuggerOptionsWithDefaults, }, + { + description: 'leave version undefined when metadata is omitted', + input: makeConfig({}), + expected: expect.objectContaining({ enable: true, version: undefined }), + }, + { + description: 'leave version undefined when only metadata.name is set', + input: makeConfig({}, undefined, { name: 'my-build' }), + expected: expect.objectContaining({ enable: true, version: undefined }), + }, ]; test.each(cases)('should $description', ({ input, expected }) => { @@ -91,63 +106,55 @@ describe('validateOptions', () => { describe('valid options', () => { const cases = [ { - description: 'accept version as a string', - input: makeConfig({ version: '1.0.0' }), + description: 'forward metadata.version when present', + input: makeConfig({}, undefined, { version: '1.0.0' }), expected: expect.objectContaining({ version: '1.0.0' }), }, { description: 'accept string include patterns', - input: makeConfig({ version: '1.0.0', include: ['src/'] }), - expected: expect.objectContaining({ version: '1.0.0', include: ['src/'] }), + input: makeConfig({ include: ['src/'] }), + expected: expect.objectContaining({ include: ['src/'] }), }, { description: 'accept RegExp include patterns', - input: makeConfig({ version: '1.0.0', include: [/\.tsx?$/] }), - expected: expect.objectContaining({ version: '1.0.0', include: [/\.tsx?$/] }), + input: makeConfig({ include: [/\.tsx?$/] }), + expected: expect.objectContaining({ include: [/\.tsx?$/] }), }, { description: 'accept mixed include patterns', - input: makeConfig({ version: '1.0.0', include: ['src/', /\.tsx?$/] }), - expected: expect.objectContaining({ - version: '1.0.0', - include: ['src/', /\.tsx?$/], - }), + input: makeConfig({ include: ['src/', /\.tsx?$/] }), + expected: expect.objectContaining({ include: ['src/', /\.tsx?$/] }), }, { description: 'accept string exclude patterns', - input: makeConfig({ version: '1.0.0', exclude: ['vendor/'] }), - expected: expect.objectContaining({ version: '1.0.0', exclude: ['vendor/'] }), + input: makeConfig({ exclude: ['vendor/'] }), + expected: expect.objectContaining({ exclude: ['vendor/'] }), }, { description: 'accept RegExp exclude patterns', - input: makeConfig({ version: '1.0.0', exclude: [/node_modules/] }), - expected: expect.objectContaining({ version: '1.0.0', exclude: [/node_modules/] }), + input: makeConfig({ exclude: [/node_modules/] }), + expected: expect.objectContaining({ exclude: [/node_modules/] }), }, { description: 'accept honorSkipComments as true', - input: makeConfig({ version: '1.0.0', honorSkipComments: true }), - expected: expect.objectContaining({ version: '1.0.0', honorSkipComments: true }), + input: makeConfig({ honorSkipComments: true }), + expected: expect.objectContaining({ honorSkipComments: true }), }, { description: 'accept honorSkipComments as false', - input: makeConfig({ version: '1.0.0', honorSkipComments: false }), - expected: expect.objectContaining({ version: '1.0.0', honorSkipComments: false }), + input: makeConfig({ honorSkipComments: false }), + expected: expect.objectContaining({ honorSkipComments: false }), }, { description: 'accept valid functionTypes', - input: makeConfig({ - version: '1.0.0', - functionTypes: ['arrowFunction', 'classMethod'], - }), + input: makeConfig({ functionTypes: ['arrowFunction', 'classMethod'] }), expected: expect.objectContaining({ - version: '1.0.0', functionTypes: ['arrowFunction', 'classMethod'], }), }, { description: 'accept all valid functionTypes', input: makeConfig({ - version: '1.0.0', functionTypes: [ 'functionDeclaration', 'functionExpression', @@ -158,7 +165,6 @@ describe('validateOptions', () => { ], }), expected: expect.objectContaining({ - version: '1.0.0', functionTypes: [ 'functionDeclaration', 'functionExpression', @@ -171,28 +177,28 @@ describe('validateOptions', () => { }, { description: 'accept namedOnly as true', - input: makeConfig({ version: '1.0.0', namedOnly: true }), - expected: expect.objectContaining({ version: '1.0.0', namedOnly: true }), + input: makeConfig({ namedOnly: true }), + expected: expect.objectContaining({ namedOnly: true }), }, { description: 'accept namedOnly as false', - input: makeConfig({ version: '1.0.0', namedOnly: false }), - expected: expect.objectContaining({ version: '1.0.0', namedOnly: false }), + input: makeConfig({ namedOnly: false }), + expected: expect.objectContaining({ namedOnly: false }), }, { description: 'accept an empty include array', - input: makeConfig({ version: '1.0.0', include: [] }), - expected: expect.objectContaining({ version: '1.0.0', include: [] }), + input: makeConfig({ include: [] }), + expected: expect.objectContaining({ include: [] }), }, { description: 'accept an empty exclude array', - input: makeConfig({ version: '1.0.0', exclude: [] }), - expected: expect.objectContaining({ version: '1.0.0', exclude: [] }), + input: makeConfig({ exclude: [] }), + expected: expect.objectContaining({ exclude: [] }), }, { description: 'accept an empty functionTypes array', - input: makeConfig({ version: '1.0.0', functionTypes: [] }), - expected: expect.objectContaining({ version: '1.0.0', functionTypes: [] }), + input: makeConfig({ functionTypes: [] }), + expected: expect.objectContaining({ functionTypes: [] }), }, ]; @@ -228,36 +234,45 @@ describe('validateOptions', () => { }); }); - describe('version validation', () => { - it('should reject version when not a string', () => { - expect(() => validateOptions(makeConfig({ version: 123 }), mockLogger)).toThrow( - `Invalid configuration for ${PLUGIN_NAME}.`, - ); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringMatching(/version.*must be a string/), - ); - }); - - it('should reject version mismatch with sourcemap releaseVersion', () => { + describe('metadata.version cross-check', () => { + it('should reject metadata.version mismatch with sourcemap releaseVersion', () => { expect(() => validateOptions( makeConfig( - { version: '1.0.0' }, + {}, { sourcemaps: { releaseVersion: '2.0.0', }, }, + { version: '1.0.0' }, ), mockLogger, ), ).toThrow(`Invalid configuration for ${PLUGIN_NAME}.`); expect(mockLogger.error).toHaveBeenCalledWith( expect.stringMatching( - /version.*must match.*errorTracking\.sourcemaps\.releaseVersion/, + /metadata\.version.*must match.*errorTracking\.sourcemaps\.releaseVersion/, ), ); }); + + it('should accept metadata.version matching sourcemap releaseVersion', () => { + expect(() => + validateOptions( + makeConfig( + {}, + { + sourcemaps: { + releaseVersion: '1.0.0', + }, + }, + { version: '1.0.0' }, + ), + mockLogger, + ), + ).not.toThrow(); + }); }); describe('invalid exclude', () => { diff --git a/packages/plugins/live-debugger/src/validate.ts b/packages/plugins/live-debugger/src/validate.ts index 292ab3c2d..68186561d 100644 --- a/packages/plugins/live-debugger/src/validate.ts +++ b/packages/plugins/live-debugger/src/validate.ts @@ -13,6 +13,7 @@ const red = chalk.bold.red; export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptionsWithDefaults => { const pluginConfig: LiveDebuggerOptions = config[CONFIG_KEY] || {}; + const metadataVersion = config.metadata?.version; const errors: string[] = []; const sourcemapReleaseVersion = config.errorTracking?.sourcemaps?.releaseVersion; @@ -21,17 +22,10 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio errors.push(`${red('enable')} must be a boolean`); } - // Validate version option - if (pluginConfig.version !== undefined && typeof pluginConfig.version !== 'string') { - errors.push(`${red('version')} must be a string`); - } - if ( - pluginConfig.version && - sourcemapReleaseVersion && - pluginConfig.version !== sourcemapReleaseVersion - ) { + // Validate version + if (metadataVersion && sourcemapReleaseVersion && metadataVersion !== sourcemapReleaseVersion) { errors.push( - `${red('version')} must match ${red('errorTracking.sourcemaps.releaseVersion')} when both Live Debugger and sourcemap upload are configured`, + `${red('metadata.version')} must match ${red('errorTracking.sourcemaps.releaseVersion')} when both Live Debugger and sourcemap upload are configured`, ); } @@ -101,7 +95,7 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio // Build the final configuration with defaults return { enable: pluginConfig.enable ?? !!config[CONFIG_KEY], - version: pluginConfig.version, + version: metadataVersion, include: pluginConfig.include || [/\.[jt]sx?$/], // .js, .jsx, .ts, .tsx exclude: pluginConfig.exclude || [ /\/node_modules\//, From db28085c7f9c5e8b759fe2d927c59533dace1319 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 28 Apr 2026 07:11:12 +0200 Subject: [PATCH 3/6] Default sourcemap and source-code-context versions from metadata.version Have `errorTracking.sourcemaps.releaseVersion` and `rum.sourceCodeContext.version` fall back to the shared `metadata.version` when no plugin-specific value is configured. Users who declare the build version once at the top level no longer need to repeat it under each plugin namespace. To support the fallback, `SourcemapsOptions.releaseVersion` is loosened from required to optional. The existing "is required" check still fires when neither `sourcemaps.releaseVersion` nor `metadata.version` resolves, with an updated message that mentions both options. Document `metadata.version` as a top-level option in the root README so it sits alongside `metadata.name`, and update the auto-generated configuration block template in `@dd/tools` accordingly. --- README.md | 8 +++ packages/plugins/error-tracking/README.md | 4 +- packages/plugins/error-tracking/src/types.ts | 4 +- .../error-tracking/src/validate.test.ts | 44 ++++++++++++++- .../plugins/error-tracking/src/validate.ts | 53 ++++++++++++------- packages/plugins/live-debugger/README.md | 15 +----- packages/plugins/rum/src/validate.test.ts | 22 ++++++++ packages/plugins/rum/src/validate.ts | 8 ++- .../tools/src/commands/integrity/readme.ts | 1 + 9 files changed, 122 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index f18f1cbea..b45759973 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ To interact with Datadog directly from your builds. - [`enableGit`](#enablegit) - [`logLevel`](#loglevel) - [`metadata.name`](#metadataname) + - [`metadata.version`](#metadataversion) - [Features](#features) - [Error Tracking](#error-tracking-----) - [Metrics](#metrics-----) @@ -100,6 +101,7 @@ Follow the specific documentation for each bundler: logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'none', metadata?: { name?: string; + version?: string; }; errorTracking?: { enable?: boolean; @@ -271,6 +273,12 @@ Which level of log do you want to show. The name of the build.
This is used to identify the build in logs, metrics and spans. +### `metadata.version` +> default: `null` + +An immutable identifier for the deployed build (typically a release tag, a git commit SHA, or a CI build ID).
+This is the canonical place to declare the version once. Plugins that need a build version (for sourcemap upload, source-code resolution, runtime SDK initialization, etc.) read it from here unless they're given a more specific override. + ## Features diff --git a/packages/plugins/error-tracking/README.md b/packages/plugins/error-tracking/README.md index 91574c0c7..3b2ef7786 100644 --- a/packages/plugins/error-tracking/README.md +++ b/packages/plugins/error-tracking/README.md @@ -74,10 +74,12 @@ Example: if you're uploading `dist/file.js` to `https://example.com/static/file. ### errorTracking.sourcemaps.releaseVersion -> required +> required (or set via [`metadata.version`](/README.md#metadataversion)) Is similar and will be used to match the `version` tag set on the RUM SDK. +If omitted, the plugin falls back to the shared top-level `metadata.version`. At least one of the two must be set. + ### errorTracking.sourcemaps.service > required diff --git a/packages/plugins/error-tracking/src/types.ts b/packages/plugins/error-tracking/src/types.ts index 0d213210f..018350600 100644 --- a/packages/plugins/error-tracking/src/types.ts +++ b/packages/plugins/error-tracking/src/types.ts @@ -9,7 +9,9 @@ export type SourcemapsOptions = { dryRun?: boolean; maxConcurrency?: number; minifiedPathPrefix: MinifiedPathPrefix; - releaseVersion: string; + // Optional: when omitted, the validator falls back to the shared + // top-level `metadata.version`. At least one of the two must be set. + releaseVersion?: string; service: string; }; diff --git a/packages/plugins/error-tracking/src/validate.test.ts b/packages/plugins/error-tracking/src/validate.test.ts index 4568f0ba7..48141f823 100644 --- a/packages/plugins/error-tracking/src/validate.test.ts +++ b/packages/plugins/error-tracking/src/validate.test.ts @@ -54,7 +54,7 @@ describe('Error Tracking Plugins validate', () => { expect(errors).toHaveLength(3); const expectedErrors = [ - 'sourcemaps.releaseVersion is required.', + 'sourcemaps.releaseVersion is required (set it directly or via metadata.version).', 'sourcemaps.service is required.', 'sourcemaps.minifiedPathPrefix is required.', ]; @@ -83,6 +83,48 @@ describe('Error Tracking Plugins validate', () => { }); }); + test('Should fall back to metadata.version when sourcemaps.releaseVersion is unset', () => { + const { config, errors } = validateSourcemapsOptions({ + metadata: { version: '2.0.0' }, + errorTracking: { + sourcemaps: getMinimalSourcemapsConfiguration({ + releaseVersion: undefined, + }), + }, + }); + + expect(errors).toHaveLength(0); + expect(config).toEqual(expect.objectContaining({ releaseVersion: '2.0.0' })); + }); + + test('Should prefer an explicit sourcemaps.releaseVersion over metadata.version', () => { + const { config, errors } = validateSourcemapsOptions({ + metadata: { version: '2.0.0' }, + errorTracking: { + sourcemaps: getMinimalSourcemapsConfiguration({ + releaseVersion: '1.0.0', + }), + }, + }); + + expect(errors).toHaveLength(0); + expect(config).toEqual(expect.objectContaining({ releaseVersion: '1.0.0' })); + }); + + test('Should error when neither sourcemaps.releaseVersion nor metadata.version is set', () => { + const { errors } = validateSourcemapsOptions({ + errorTracking: { + sourcemaps: getMinimalSourcemapsConfiguration({ + releaseVersion: undefined, + }), + }, + }); + + expect(stripAnsi(errors[0])).toBe( + 'sourcemaps.releaseVersion is required (set it directly or via metadata.version).', + ); + }); + test('Should return an error with a bad minifiedPathPrefix', () => { const { errors } = validateSourcemapsOptions({ errorTracking: { diff --git a/packages/plugins/error-tracking/src/validate.ts b/packages/plugins/error-tracking/src/validate.ts index 1d314efc1..4a9fde7bd 100644 --- a/packages/plugins/error-tracking/src/validate.ts +++ b/packages/plugins/error-tracking/src/validate.ts @@ -72,36 +72,49 @@ export const validateSourcemapsOptions = ( }; if (validatedOptions.sourcemaps) { + const sourcemapsCfg = validatedOptions.sourcemaps; + + // Resolve `releaseVersion`: prefer the plugin-specific option, then + // fall back to the shared top-level `metadata.version`. Letting users + // configure one canonical build version at the top level keeps every + // consumer (live-debugger, sourcemaps, …) reading from the same place. + const releaseVersion = sourcemapsCfg.releaseVersion || config.metadata?.version; + // Validate the configuration. - if (!validatedOptions.sourcemaps.releaseVersion) { - toReturn.errors.push(`${red('sourcemaps.releaseVersion')} is required.`); + if (!releaseVersion) { + toReturn.errors.push( + `${red('sourcemaps.releaseVersion')} is required (set it directly or via ${red('metadata.version')}).`, + ); } - if (!validatedOptions.sourcemaps.service) { + if (!sourcemapsCfg.service) { toReturn.errors.push(`${red('sourcemaps.service')} is required.`); } - if (!validatedOptions.sourcemaps.minifiedPathPrefix) { + if (!sourcemapsCfg.minifiedPathPrefix) { toReturn.errors.push(`${red('sourcemaps.minifiedPathPrefix')} is required.`); } // Validate the minifiedPathPrefix. - if (validatedOptions.sourcemaps.minifiedPathPrefix) { - if (!validateMinifiedPathPrefix(validatedOptions.sourcemaps.minifiedPathPrefix)) { - toReturn.errors.push( - `${red('sourcemaps.minifiedPathPrefix')} must be a valid URL or start with '/'.`, - ); - } + if ( + sourcemapsCfg.minifiedPathPrefix && + !validateMinifiedPathPrefix(sourcemapsCfg.minifiedPathPrefix) + ) { + toReturn.errors.push( + `${red('sourcemaps.minifiedPathPrefix')} must be a valid URL or start with '/'.`, + ); } - // Add the defaults. - const sourcemapsWithDefaults: SourcemapsOptionsWithDefaults = { - bailOnError: false, - dryRun: false, - maxConcurrency: 20, - ...validatedOptions.sourcemaps, - }; - - // Save the config. - toReturn.config = sourcemapsWithDefaults; + // Build the resolved config only when `releaseVersion` actually + // resolves; otherwise an error has been recorded and the caller will + // throw before the config is read. + if (releaseVersion) { + toReturn.config = { + bailOnError: false, + dryRun: false, + maxConcurrency: 20, + ...sourcemapsCfg, + releaseVersion, + }; + } } return toReturn; diff --git a/packages/plugins/live-debugger/README.md b/packages/plugins/live-debugger/README.md index b51ebdc4f..bbcca03a7 100644 --- a/packages/plugins/live-debugger/README.md +++ b/packages/plugins/live-debugger/README.md @@ -140,23 +140,12 @@ datadogBuildPlugins({ }); ``` -This value should match: +If both `metadata.version` and an explicit `errorTracking.sourcemaps.releaseVersion` are configured and disagree, this plugin surfaces the mismatch as a build error. -- the `version` passed to `@datadog/browser-debugger`'s `init()` -- `errorTracking.sourcemaps.releaseVersion` when sourcemap upload is enabled - -When set, Live Debugger also injects the value into runtime-visible build metadata so the Browser Debugger SDK can use it as the default `version` during `init()`. +When set, Live Debugger injects the value into runtime-visible build metadata so the Browser Debugger SDK uses it as the default `version` during `init()`. If omitted, Live Debugger instrumentation still works, but browser build lookup and source-code-aware resolution will gracefully degrade. -> **Prerequisites for source-code resolution.** Browser build lookup resolves `applicationId + version` to repository metadata via the RUM application model. This requires: -> -> 1. A [RUM application](https://docs.datadoghq.com/real_user_monitoring/browser) whose `applicationId` is passed to the Browser Debugger SDK. -> 2. The RUM application must have a `service` tag configured — this is how the backend maps the application to the service that owns the source code. -> 3. Sourcemaps must be uploaded with the same `version` (via `errorTracking.sourcemaps.releaseVersion`) so the backend can resolve the service + version to a repository URL and commit SHA. -> -> If any of these are missing, source-code resolution will gracefully degrade to the "no source code" state. - ### liveDebugger.include > default: `[/\.[jt]sx?$/]` diff --git a/packages/plugins/rum/src/validate.test.ts b/packages/plugins/rum/src/validate.test.ts index 6d0a6fa7a..9da4a92c6 100644 --- a/packages/plugins/rum/src/validate.test.ts +++ b/packages/plugins/rum/src/validate.test.ts @@ -64,4 +64,26 @@ describe('sourceCodeContext validation', () => { expect.arrayContaining([expect.stringContaining('"rum.sourceCodeContext.service"')]), ); }); + + test('should fall back to metadata.version when sourceCodeContext.version is unset', () => { + const pluginOptions = { + ...defaultPluginOptions, + metadata: { version: '1.2.3' }, + rum: { sourceCodeContext: { service: 'checkout' } }, + }; + const result = validateSourceCodeContextOptions(pluginOptions); + expect(result.errors).toHaveLength(0); + expect(result.config).toEqual({ service: 'checkout', version: '1.2.3' }); + }); + + test('should prefer an explicit sourceCodeContext.version over metadata.version', () => { + const pluginOptions = { + ...defaultPluginOptions, + metadata: { version: '1.2.3' }, + rum: { sourceCodeContext: { service: 'checkout', version: '4.5.6' } }, + }; + const result = validateSourceCodeContextOptions(pluginOptions); + expect(result.errors).toHaveLength(0); + expect(result.config).toEqual({ service: 'checkout', version: '4.5.6' }); + }); }); diff --git a/packages/plugins/rum/src/validate.ts b/packages/plugins/rum/src/validate.ts index ab817284d..3fe8a8603 100644 --- a/packages/plugins/rum/src/validate.ts +++ b/packages/plugins/rum/src/validate.ts @@ -174,6 +174,12 @@ export const validateSourceCodeContextOptions = ( toReturn.errors.push(`Missing ${red('"rum.sourceCodeContext.service"')}.`); } - toReturn.config = cfg; + // Resolve `version`: prefer the plugin-specific option, then fall back to + // the shared top-level `metadata.version`. This keeps `metadata.version` + // as the single canonical place to declare the deployed build identifier. + toReturn.config = { + ...cfg, + version: cfg.version || options.metadata?.version, + }; return toReturn; }; diff --git a/packages/tools/src/commands/integrity/readme.ts b/packages/tools/src/commands/integrity/readme.ts index e25ff829a..279fc4c9b 100644 --- a/packages/tools/src/commands/integrity/readme.ts +++ b/packages/tools/src/commands/integrity/readme.ts @@ -362,6 +362,7 @@ export const updateReadmes = async (plugins: Workspace[], bundlers: Workspace[]) logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'none', metadata?: { name?: string; + version?: string; } `, ]; From fe2a52b4ebc33f3f61f7a7f236cf6db123f524d2 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 8 May 2026 06:33:28 +0200 Subject: [PATCH 4/6] fix(live-debugger): decouple sourcemap validation Move metadata.version and sourcemap release version mismatch validation into the error-tracking plugin so Live Debugger no longer depends on Error Tracking configuration details. --- .../error-tracking/src/validate.test.ts | 18 +++++- .../plugins/error-tracking/src/validate.ts | 9 +++ .../live-debugger/src/validate.test.ts | 55 +++---------------- .../plugins/live-debugger/src/validate.ts | 8 --- 4 files changed, 33 insertions(+), 57 deletions(-) diff --git a/packages/plugins/error-tracking/src/validate.test.ts b/packages/plugins/error-tracking/src/validate.test.ts index 48141f823..a657672d7 100644 --- a/packages/plugins/error-tracking/src/validate.test.ts +++ b/packages/plugins/error-tracking/src/validate.test.ts @@ -99,7 +99,7 @@ describe('Error Tracking Plugins validate', () => { test('Should prefer an explicit sourcemaps.releaseVersion over metadata.version', () => { const { config, errors } = validateSourcemapsOptions({ - metadata: { version: '2.0.0' }, + metadata: { version: '1.0.0' }, errorTracking: { sourcemaps: getMinimalSourcemapsConfiguration({ releaseVersion: '1.0.0', @@ -111,6 +111,22 @@ describe('Error Tracking Plugins validate', () => { expect(config).toEqual(expect.objectContaining({ releaseVersion: '1.0.0' })); }); + test('Should error when sourcemaps.releaseVersion and metadata.version mismatch', () => { + const { errors } = validateSourcemapsOptions({ + metadata: { version: '2.0.0' }, + errorTracking: { + sourcemaps: getMinimalSourcemapsConfiguration({ + releaseVersion: '1.0.0', + }), + }, + }); + + expect(errors).toHaveLength(1); + expect(stripAnsi(errors[0])).toBe( + 'sourcemaps.releaseVersion must match metadata.version when both are configured.', + ); + }); + test('Should error when neither sourcemaps.releaseVersion nor metadata.version is set', () => { const { errors } = validateSourcemapsOptions({ errorTracking: { diff --git a/packages/plugins/error-tracking/src/validate.ts b/packages/plugins/error-tracking/src/validate.ts index 4a9fde7bd..8afffe494 100644 --- a/packages/plugins/error-tracking/src/validate.ts +++ b/packages/plugins/error-tracking/src/validate.ts @@ -86,6 +86,15 @@ export const validateSourcemapsOptions = ( `${red('sourcemaps.releaseVersion')} is required (set it directly or via ${red('metadata.version')}).`, ); } + if ( + sourcemapsCfg.releaseVersion && + config.metadata?.version && + sourcemapsCfg.releaseVersion !== config.metadata.version + ) { + toReturn.errors.push( + `${red('sourcemaps.releaseVersion')} must match ${red('metadata.version')} when both are configured.`, + ); + } if (!sourcemapsCfg.service) { toReturn.errors.push(`${red('sourcemaps.service')} is required.`); } diff --git a/packages/plugins/live-debugger/src/validate.test.ts b/packages/plugins/live-debugger/src/validate.test.ts index 98bbf7244..781938286 100644 --- a/packages/plugins/live-debugger/src/validate.test.ts +++ b/packages/plugins/live-debugger/src/validate.test.ts @@ -17,8 +17,8 @@ const mockLogger: Logger = { debug: jest.fn(), }; -const makeConfig = (liveDebugger?: unknown, errorTracking?: unknown, metadata?: unknown): Options => - ({ liveDebugger, errorTracking, metadata }) as unknown as Options; +const makeConfig = (liveDebugger?: unknown, metadata?: unknown): Options => + ({ liveDebugger, metadata }) as unknown as Options; beforeEach(() => { jest.clearAllMocks(); @@ -47,7 +47,7 @@ describe('validateOptions', () => { }, { description: 'honor enable: false even when metadata.version is provided', - input: makeConfig({ enable: false }, undefined, { version: '1.0.0' }), + input: makeConfig({ enable: false }, { version: '1.0.0' }), expected: expect.objectContaining({ enable: false, version: '1.0.0' }), }, { @@ -65,12 +65,12 @@ describe('validateOptions', () => { }, { description: 'honor enable: true and forward metadata.version', - input: makeConfig({ enable: true }, undefined, { version: '1.0.0' }), + input: makeConfig({ enable: true }, { version: '1.0.0' }), expected: expect.objectContaining({ enable: true, version: '1.0.0' }), }, { description: 'enable and forward metadata.version when liveDebugger is empty', - input: makeConfig({}, undefined, { version: '1.0.0' }), + input: makeConfig({}, { version: '1.0.0' }), expected: { enable: true, version: '1.0.0', @@ -88,7 +88,7 @@ describe('validateOptions', () => { }, { description: 'leave version undefined when only metadata.name is set', - input: makeConfig({}, undefined, { name: 'my-build' }), + input: makeConfig({}, { name: 'my-build' }), expected: expect.objectContaining({ enable: true, version: undefined }), }, ]; @@ -107,7 +107,7 @@ describe('validateOptions', () => { const cases = [ { description: 'forward metadata.version when present', - input: makeConfig({}, undefined, { version: '1.0.0' }), + input: makeConfig({}, { version: '1.0.0' }), expected: expect.objectContaining({ version: '1.0.0' }), }, { @@ -234,47 +234,6 @@ describe('validateOptions', () => { }); }); - describe('metadata.version cross-check', () => { - it('should reject metadata.version mismatch with sourcemap releaseVersion', () => { - expect(() => - validateOptions( - makeConfig( - {}, - { - sourcemaps: { - releaseVersion: '2.0.0', - }, - }, - { version: '1.0.0' }, - ), - mockLogger, - ), - ).toThrow(`Invalid configuration for ${PLUGIN_NAME}.`); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringMatching( - /metadata\.version.*must match.*errorTracking\.sourcemaps\.releaseVersion/, - ), - ); - }); - - it('should accept metadata.version matching sourcemap releaseVersion', () => { - expect(() => - validateOptions( - makeConfig( - {}, - { - sourcemaps: { - releaseVersion: '1.0.0', - }, - }, - { version: '1.0.0' }, - ), - mockLogger, - ), - ).not.toThrow(); - }); - }); - describe('invalid exclude', () => { const cases = [ { diff --git a/packages/plugins/live-debugger/src/validate.ts b/packages/plugins/live-debugger/src/validate.ts index 68186561d..ede13beee 100644 --- a/packages/plugins/live-debugger/src/validate.ts +++ b/packages/plugins/live-debugger/src/validate.ts @@ -15,20 +15,12 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio const pluginConfig: LiveDebuggerOptions = config[CONFIG_KEY] || {}; const metadataVersion = config.metadata?.version; const errors: string[] = []; - const sourcemapReleaseVersion = config.errorTracking?.sourcemaps?.releaseVersion; // Validate enable option if (pluginConfig.enable !== undefined && typeof pluginConfig.enable !== 'boolean') { errors.push(`${red('enable')} must be a boolean`); } - // Validate version - if (metadataVersion && sourcemapReleaseVersion && metadataVersion !== sourcemapReleaseVersion) { - errors.push( - `${red('metadata.version')} must match ${red('errorTracking.sourcemaps.releaseVersion')} when both Live Debugger and sourcemap upload are configured`, - ); - } - // Validate include option if (pluginConfig.include !== undefined) { if (!Array.isArray(pluginConfig.include)) { From e8b798298ffa546b33e457f2744cfc68e9f2b13d Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 8 May 2026 07:01:39 +0200 Subject: [PATCH 5/6] docs(live-debugger): trim metadata validation note Remove the Live Debugger README note that describes cross-plugin metadata validation while keeping the source map version guidance. --- packages/plugins/live-debugger/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/plugins/live-debugger/README.md b/packages/plugins/live-debugger/README.md index bbcca03a7..e7a246bd6 100644 --- a/packages/plugins/live-debugger/README.md +++ b/packages/plugins/live-debugger/README.md @@ -140,8 +140,6 @@ datadogBuildPlugins({ }); ``` -If both `metadata.version` and an explicit `errorTracking.sourcemaps.releaseVersion` are configured and disagree, this plugin surfaces the mismatch as a build error. - When set, Live Debugger injects the value into runtime-visible build metadata so the Browser Debugger SDK uses it as the default `version` during `init()`. If omitted, Live Debugger instrumentation still works, but browser build lookup and source-code-aware resolution will gracefully degrade. From 519bb2fe5f9b0375548baeaa11d1b53dd45c6a85 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 8 May 2026 07:31:10 +0200 Subject: [PATCH 6/6] test(live-debugger): type validation config mocks Use the shared logger mock and separate valid option fixtures from intentionally invalid runtime config shapes in validation tests. --- .../live-debugger/src/validate.test.ts | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/plugins/live-debugger/src/validate.test.ts b/packages/plugins/live-debugger/src/validate.test.ts index 781938286..d08219a28 100644 --- a/packages/plugins/live-debugger/src/validate.test.ts +++ b/packages/plugins/live-debugger/src/validate.test.ts @@ -2,23 +2,25 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import type { Logger, Options } from '@dd/core/types'; +import type { BuildMetadata, Options } from '@dd/core/types'; +import { getMockLogger } from '@dd/tests/_jest/helpers/mocks'; import { PLUGIN_NAME } from './constants'; -import type { LiveDebuggerOptionsWithDefaults } from './types'; +import type { LiveDebuggerOptions, LiveDebuggerOptionsWithDefaults } from './types'; import { validateOptions } from './validate'; -const mockLogger: Logger = { - getLogger: jest.fn(() => mockLogger), - time: jest.fn() as unknown as Logger['time'], - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), -}; +const mockError = jest.fn(); +const mockLogger = getMockLogger({ error: mockError }); -const makeConfig = (liveDebugger?: unknown, metadata?: unknown): Options => - ({ liveDebugger, metadata }) as unknown as Options; +const makeConfig = (liveDebugger?: LiveDebuggerOptions, metadata?: BuildMetadata): Options => ({ + liveDebugger, + metadata, +}); + +type InvalidLiveDebuggerOptions = Partial>; + +const makeInvalidConfig = (liveDebugger: InvalidLiveDebuggerOptions): Options => + ({ liveDebugger }) as Options; beforeEach(() => { jest.clearAllMocks(); @@ -211,17 +213,17 @@ describe('validateOptions', () => { const cases = [ { description: 'reject include when not an array', - input: makeConfig({ include: 'src/' }), + input: makeInvalidConfig({ include: 'src/' }), errorPattern: /include.*must be an array/, }, { description: 'reject include with invalid pattern type', - input: makeConfig({ include: [42] }), + input: makeInvalidConfig({ include: [42] }), errorPattern: /include.*patterns must be strings or RegExp/, }, { description: 'reject include with a mix of valid and invalid patterns', - input: makeConfig({ include: [/\.ts$/, true] }), + input: makeInvalidConfig({ include: [/\.ts$/, true] }), errorPattern: /include.*patterns must be strings or RegExp/, }, ]; @@ -230,7 +232,7 @@ describe('validateOptions', () => { expect(() => validateOptions(input, mockLogger)).toThrow( `Invalid configuration for ${PLUGIN_NAME}.`, ); - expect(mockLogger.error).toHaveBeenCalledWith(expect.stringMatching(errorPattern)); + expect(mockError).toHaveBeenCalledWith(expect.stringMatching(errorPattern)); }); }); @@ -238,12 +240,12 @@ describe('validateOptions', () => { const cases = [ { description: 'reject exclude when not an array', - input: makeConfig({ exclude: /node_modules/ }), + input: makeInvalidConfig({ exclude: /node_modules/ }), errorPattern: /exclude.*must be an array/, }, { description: 'reject exclude with invalid pattern type', - input: makeConfig({ exclude: [null] }), + input: makeInvalidConfig({ exclude: [null] }), errorPattern: /exclude.*patterns must be strings or RegExp/, }, ]; @@ -252,7 +254,7 @@ describe('validateOptions', () => { expect(() => validateOptions(input, mockLogger)).toThrow( `Invalid configuration for ${PLUGIN_NAME}.`, ); - expect(mockLogger.error).toHaveBeenCalledWith(expect.stringMatching(errorPattern)); + expect(mockError).toHaveBeenCalledWith(expect.stringMatching(errorPattern)); }); }); @@ -260,11 +262,11 @@ describe('validateOptions', () => { const cases = [ { description: 'reject enable when a string', - input: makeConfig({ enable: 'yes' }), + input: makeInvalidConfig({ enable: 'yes' }), }, { description: 'reject enable when a number', - input: makeConfig({ enable: 1 }), + input: makeInvalidConfig({ enable: 1 }), }, ]; @@ -272,7 +274,7 @@ describe('validateOptions', () => { expect(() => validateOptions(input, mockLogger)).toThrow( `Invalid configuration for ${PLUGIN_NAME}.`, ); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockError).toHaveBeenCalledWith( expect.stringMatching(/enable.*must be a boolean/), ); }); @@ -282,11 +284,11 @@ describe('validateOptions', () => { const cases = [ { description: 'reject honorSkipComments when not a boolean', - input: makeConfig({ honorSkipComments: 'true' }), + input: makeInvalidConfig({ honorSkipComments: 'true' }), }, { description: 'reject honorSkipComments when a number', - input: makeConfig({ honorSkipComments: 1 }), + input: makeInvalidConfig({ honorSkipComments: 1 }), }, ]; @@ -294,7 +296,7 @@ describe('validateOptions', () => { expect(() => validateOptions(input, mockLogger)).toThrow( `Invalid configuration for ${PLUGIN_NAME}.`, ); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockError).toHaveBeenCalledWith( expect.stringMatching(/honorSkipComments.*must be a boolean/), ); }); @@ -304,12 +306,12 @@ describe('validateOptions', () => { const cases = [ { description: 'reject functionTypes when not an array', - input: makeConfig({ functionTypes: 'arrowFunction' }), + input: makeInvalidConfig({ functionTypes: 'arrowFunction' }), errorPattern: /functionTypes.*must be an array/, }, { description: 'reject functionTypes with an invalid value', - input: makeConfig({ functionTypes: ['arrowFunction', 'lambda'] }), + input: makeInvalidConfig({ functionTypes: ['arrowFunction', 'lambda'] }), errorPattern: /functionTypes.*contains invalid value "lambda"/, }, ]; @@ -318,7 +320,7 @@ describe('validateOptions', () => { expect(() => validateOptions(input, mockLogger)).toThrow( `Invalid configuration for ${PLUGIN_NAME}.`, ); - expect(mockLogger.error).toHaveBeenCalledWith(expect.stringMatching(errorPattern)); + expect(mockError).toHaveBeenCalledWith(expect.stringMatching(errorPattern)); }); }); @@ -326,11 +328,11 @@ describe('validateOptions', () => { const cases = [ { description: 'reject namedOnly when not a boolean', - input: makeConfig({ namedOnly: 'yes' }), + input: makeInvalidConfig({ namedOnly: 'yes' }), }, { description: 'reject namedOnly when a number', - input: makeConfig({ namedOnly: 0 }), + input: makeInvalidConfig({ namedOnly: 0 }), }, ]; @@ -338,7 +340,7 @@ describe('validateOptions', () => { expect(() => validateOptions(input, mockLogger)).toThrow( `Invalid configuration for ${PLUGIN_NAME}.`, ); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockError).toHaveBeenCalledWith( expect.stringMatching(/namedOnly.*must be a boolean/), ); }); @@ -346,7 +348,7 @@ describe('validateOptions', () => { describe('multiple errors', () => { it('should aggregate all validation errors before throwing', () => { - const input = makeConfig({ + const input = makeInvalidConfig({ enable: 'yes', include: 'bad', exclude: 'bad', @@ -359,7 +361,7 @@ describe('validateOptions', () => { `Invalid configuration for ${PLUGIN_NAME}.`, ); - const errorMessage = (mockLogger.error as jest.Mock).mock.calls[0][0] as string; + const errorMessage = mockError.mock.calls[0][0]; expect(errorMessage).toMatch(/enable/); expect(errorMessage).toMatch(/include/); expect(errorMessage).toMatch(/exclude/);