From af0764efac03c129ed3df4b60da723dfca4df363 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sun, 19 Apr 2026 10:46:23 +0200 Subject: [PATCH] Validate optional live-debugger version Add an optional version field to the live-debugger plugin config and use it to validate consistency with sourcemap uploads. This documents the browser build identity contract while still allowing instrumentation to work when no version is configured. --- packages/plugins/live-debugger/README.md | 13 ++ .../plugins/live-debugger/src/index.test.ts | 1 + packages/plugins/live-debugger/src/types.ts | 2 + .../live-debugger/src/validate.test.ts | 126 +++++++++++++----- .../plugins/live-debugger/src/validate.ts | 16 +++ 5 files changed, 126 insertions(+), 32 deletions(-) diff --git a/packages/plugins/live-debugger/README.md b/packages/plugins/live-debugger/README.md index 2ec19f9df..24952d8ff 100644 --- a/packages/plugins/live-debugger/README.md +++ b/packages/plugins/live-debugger/README.md @@ -13,6 +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) - [liveDebugger.include](#livedebuggerinclude) - [liveDebugger.exclude](#livedebuggerexclude) - [liveDebugger.honorSkipComments](#livedebuggerhonorskipcomments) @@ -50,6 +51,7 @@ 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; @@ -72,6 +74,8 @@ 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`. + **Example transformation (block body):** ```javascript @@ -122,6 +126,15 @@ const double = (x) => { Enable or disable the plugin without removing its configuration. +### liveDebugger.version + +Optional. When set, use an immutable deployed browser build identifier. This value should match: + +- the `version` passed to `@datadog/browser-debugger` +- `errorTracking.sourcemaps.releaseVersion` when sourcemap upload is enabled + +If omitted, Live Debugger instrumentation still works, but browser build lookup and source-code-aware resolution will gracefully degrade. + ### liveDebugger.include > default: `[/\.[jt]sx?$/]` diff --git a/packages/plugins/live-debugger/src/index.test.ts b/packages/plugins/live-debugger/src/index.test.ts index b1bf44caf..e351d4084 100644 --- a/packages/plugins/live-debugger/src/index.test.ts +++ b/packages/plugins/live-debugger/src/index.test.ts @@ -14,6 +14,7 @@ const makeOptions = ( overrides: Partial = {}, ): LiveDebuggerOptionsWithDefaults => ({ enable: true, + version: '1.0.0', include: [/\.[jt]sx?$/], exclude: [/\/node_modules\//], honorSkipComments: false, diff --git a/packages/plugins/live-debugger/src/types.ts b/packages/plugins/live-debugger/src/types.ts index 7bf2d52cf..0ac32fddd 100644 --- a/packages/plugins/live-debugger/src/types.ts +++ b/packages/plugins/live-debugger/src/types.ts @@ -15,6 +15,7 @@ export type FunctionKind = (typeof VALID_FUNCTION_KINDS)[number]; export type LiveDebuggerOptions = { enable?: boolean; + version?: string; include?: (string | RegExp)[]; exclude?: (string | RegExp)[]; honorSkipComments?: boolean; @@ -24,6 +25,7 @@ export type LiveDebuggerOptions = { export type LiveDebuggerOptionsWithDefaults = { enable: boolean; + version: string | undefined; 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 b06a40f61..a40654857 100644 --- a/packages/plugins/live-debugger/src/validate.test.ts +++ b/packages/plugins/live-debugger/src/validate.test.ts @@ -17,7 +17,8 @@ const mockLogger: Logger = { debug: jest.fn(), }; -const makeConfig = (liveDebugger?: unknown): Options => ({ liveDebugger }) as unknown as Options; +const makeConfig = (liveDebugger?: unknown, errorTracking?: unknown): Options => + ({ liveDebugger, errorTracking }) as unknown as Options; beforeEach(() => { jest.clearAllMocks(); @@ -31,6 +32,7 @@ describe('validateOptions', () => { input: makeConfig(undefined), expected: { enable: false, + version: undefined, include: [/\.[jt]sx?$/], exclude: expect.arrayContaining([/\/node_modules\//]), honorSkipComments: true, @@ -38,11 +40,17 @@ describe('validateOptions', () => { namedOnly: false, } satisfies LiveDebuggerOptionsWithDefaults, }, + { + description: 'honor enable: false even when the config key is present', + input: makeConfig({ enable: false }), + expected: expect.objectContaining({ enable: false, version: undefined }), + }, { description: 'enable and return defaults when an empty object is provided', input: makeConfig({}), expected: { enable: true, + version: undefined, include: [/\.[jt]sx?$/], exclude: expect.arrayContaining([/\/node_modules\//]), honorSkipComments: true, @@ -51,14 +59,22 @@ describe('validateOptions', () => { } satisfies LiveDebuggerOptionsWithDefaults, }, { - description: 'honor enable: false even when the config key is present', - input: makeConfig({ enable: false }), - expected: expect.objectContaining({ enable: false }), + description: 'honor enable: true when version is provided', + input: makeConfig({ enable: true, version: '1.0.0' }), + expected: expect.objectContaining({ enable: true, version: '1.0.0' }), }, { - description: 'honor enable: true (redundant but valid)', - input: makeConfig({ enable: true }), - expected: expect.objectContaining({ enable: true }), + description: 'enable when a config object with version is provided', + input: makeConfig({ version: '1.0.0' }), + expected: { + enable: true, + version: '1.0.0', + include: [/\.[jt]sx?$/], + exclude: expect.arrayContaining([/\/node_modules\//]), + honorSkipComments: true, + functionTypes: undefined, + namedOnly: false, + } satisfies LiveDebuggerOptionsWithDefaults, }, ]; @@ -74,51 +90,64 @@ describe('validateOptions', () => { describe('valid options', () => { const cases = [ + { + description: 'accept version as a string', + input: makeConfig({ version: '1.0.0' }), + expected: expect.objectContaining({ version: '1.0.0' }), + }, { description: 'accept string include patterns', - input: makeConfig({ include: ['src/'] }), - expected: expect.objectContaining({ include: ['src/'] }), + input: makeConfig({ version: '1.0.0', include: ['src/'] }), + expected: expect.objectContaining({ version: '1.0.0', include: ['src/'] }), }, { description: 'accept RegExp include patterns', - input: makeConfig({ include: [/\.tsx?$/] }), - expected: expect.objectContaining({ include: [/\.tsx?$/] }), + input: makeConfig({ version: '1.0.0', include: [/\.tsx?$/] }), + expected: expect.objectContaining({ version: '1.0.0', include: [/\.tsx?$/] }), }, { description: 'accept mixed include patterns', - input: makeConfig({ include: ['src/', /\.tsx?$/] }), - expected: expect.objectContaining({ include: ['src/', /\.tsx?$/] }), + input: makeConfig({ version: '1.0.0', include: ['src/', /\.tsx?$/] }), + expected: expect.objectContaining({ + version: '1.0.0', + include: ['src/', /\.tsx?$/], + }), }, { description: 'accept string exclude patterns', - input: makeConfig({ exclude: ['vendor/'] }), - expected: expect.objectContaining({ exclude: ['vendor/'] }), + input: makeConfig({ version: '1.0.0', exclude: ['vendor/'] }), + expected: expect.objectContaining({ version: '1.0.0', exclude: ['vendor/'] }), }, { description: 'accept RegExp exclude patterns', - input: makeConfig({ exclude: [/node_modules/] }), - expected: expect.objectContaining({ exclude: [/node_modules/] }), + input: makeConfig({ version: '1.0.0', exclude: [/node_modules/] }), + expected: expect.objectContaining({ version: '1.0.0', exclude: [/node_modules/] }), }, { description: 'accept honorSkipComments as true', - input: makeConfig({ honorSkipComments: true }), - expected: expect.objectContaining({ honorSkipComments: true }), + input: makeConfig({ version: '1.0.0', honorSkipComments: true }), + expected: expect.objectContaining({ version: '1.0.0', honorSkipComments: true }), }, { description: 'accept honorSkipComments as false', - input: makeConfig({ honorSkipComments: false }), - expected: expect.objectContaining({ honorSkipComments: false }), + input: makeConfig({ version: '1.0.0', honorSkipComments: false }), + expected: expect.objectContaining({ version: '1.0.0', honorSkipComments: false }), }, { description: 'accept valid functionTypes', - input: makeConfig({ functionTypes: ['arrowFunction', 'classMethod'] }), + input: makeConfig({ + version: '1.0.0', + 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', @@ -129,6 +158,7 @@ describe('validateOptions', () => { ], }), expected: expect.objectContaining({ + version: '1.0.0', functionTypes: [ 'functionDeclaration', 'functionExpression', @@ -141,28 +171,28 @@ describe('validateOptions', () => { }, { description: 'accept namedOnly as true', - input: makeConfig({ namedOnly: true }), - expected: expect.objectContaining({ namedOnly: true }), + input: makeConfig({ version: '1.0.0', namedOnly: true }), + expected: expect.objectContaining({ version: '1.0.0', namedOnly: true }), }, { description: 'accept namedOnly as false', - input: makeConfig({ namedOnly: false }), - expected: expect.objectContaining({ namedOnly: false }), + input: makeConfig({ version: '1.0.0', namedOnly: false }), + expected: expect.objectContaining({ version: '1.0.0', namedOnly: false }), }, { description: 'accept an empty include array', - input: makeConfig({ include: [] }), - expected: expect.objectContaining({ include: [] }), + input: makeConfig({ version: '1.0.0', include: [] }), + expected: expect.objectContaining({ version: '1.0.0', include: [] }), }, { description: 'accept an empty exclude array', - input: makeConfig({ exclude: [] }), - expected: expect.objectContaining({ exclude: [] }), + input: makeConfig({ version: '1.0.0', exclude: [] }), + expected: expect.objectContaining({ version: '1.0.0', exclude: [] }), }, { description: 'accept an empty functionTypes array', - input: makeConfig({ functionTypes: [] }), - expected: expect.objectContaining({ functionTypes: [] }), + input: makeConfig({ version: '1.0.0', functionTypes: [] }), + expected: expect.objectContaining({ version: '1.0.0', functionTypes: [] }), }, ]; @@ -198,6 +228,38 @@ 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', () => { + expect(() => + validateOptions( + makeConfig( + { version: '1.0.0' }, + { + sourcemaps: { + releaseVersion: '2.0.0', + }, + }, + ), + mockLogger, + ), + ).toThrow(`Invalid configuration for ${PLUGIN_NAME}.`); + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringMatching( + /version.*must match.*errorTracking\.sourcemaps\.releaseVersion/, + ), + ); + }); + }); + describe('invalid exclude', () => { const cases = [ { diff --git a/packages/plugins/live-debugger/src/validate.ts b/packages/plugins/live-debugger/src/validate.ts index eda17e121..292ab3c2d 100644 --- a/packages/plugins/live-debugger/src/validate.ts +++ b/packages/plugins/live-debugger/src/validate.ts @@ -14,12 +14,27 @@ const red = chalk.bold.red; export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptionsWithDefaults => { const pluginConfig: LiveDebuggerOptions = config[CONFIG_KEY] || {}; 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 option + if (pluginConfig.version !== undefined && typeof pluginConfig.version !== 'string') { + errors.push(`${red('version')} must be a string`); + } + if ( + pluginConfig.version && + sourcemapReleaseVersion && + pluginConfig.version !== sourcemapReleaseVersion + ) { + errors.push( + `${red('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)) { @@ -86,6 +101,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, include: pluginConfig.include || [/\.[jt]sx?$/], // .js, .jsx, .ts, .tsx exclude: pluginConfig.exclude || [ /\/node_modules\//,