Skip to content

Commit edb4ac4

Browse files
committed
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.
1 parent 795ac49 commit edb4ac4

5 files changed

Lines changed: 125 additions & 32 deletions

File tree

packages/plugins/live-debugger/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ the plugin throws an error with the exact install command above.
5050
```ts
5151
liveDebugger?: {
5252
enable?: boolean;
53+
version?: string;
5354
include?: (string | RegExp)[];
5455
exclude?: (string | RegExp)[];
5556
honorSkipComments?: boolean;
@@ -72,6 +73,8 @@ Each instrumented function gets:
7273

7374
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.
7475

76+
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`.
77+
7578
**Example transformation (block body):**
7679

7780
```javascript
@@ -122,6 +125,15 @@ const double = (x) => {
122125
123126
Enable or disable the plugin without removing its configuration.
124127

128+
### liveDebugger.version
129+
130+
Optional. When set, use an immutable deployed browser build identifier. This value should match:
131+
132+
- the `version` passed to `@datadog/browser-debugger`
133+
- `errorTracking.sourcemaps.releaseVersion` when sourcemap upload is enabled
134+
135+
If omitted, Live Debugger instrumentation still works, but browser build lookup and source-code-aware resolution will gracefully degrade.
136+
125137
### liveDebugger.include
126138

127139
> default: `[/\.[jt]sx?$/]`

packages/plugins/live-debugger/src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const makeOptions = (
1414
overrides: Partial<LiveDebuggerOptionsWithDefaults> = {},
1515
): LiveDebuggerOptionsWithDefaults => ({
1616
enable: true,
17+
version: '1.0.0',
1718
include: [/\.[jt]sx?$/],
1819
exclude: [/\/node_modules\//],
1920
honorSkipComments: false,

packages/plugins/live-debugger/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type FunctionKind = (typeof VALID_FUNCTION_KINDS)[number];
1515

1616
export type LiveDebuggerOptions = {
1717
enable?: boolean;
18+
version?: string;
1819
include?: (string | RegExp)[];
1920
exclude?: (string | RegExp)[];
2021
honorSkipComments?: boolean;
@@ -24,6 +25,7 @@ export type LiveDebuggerOptions = {
2425

2526
export type LiveDebuggerOptionsWithDefaults = {
2627
enable: boolean;
28+
version: string | undefined;
2729
include: (string | RegExp)[];
2830
exclude: (string | RegExp)[];
2931
honorSkipComments: boolean;

packages/plugins/live-debugger/src/validate.test.ts

Lines changed: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ const mockLogger: Logger = {
1717
debug: jest.fn(),
1818
};
1919

20-
const makeConfig = (liveDebugger?: unknown): Options => ({ liveDebugger }) as unknown as Options;
20+
const makeConfig = (liveDebugger?: unknown, errorTracking?: unknown): Options =>
21+
({ liveDebugger, errorTracking }) as unknown as Options;
2122

2223
beforeEach(() => {
2324
jest.clearAllMocks();
@@ -31,18 +32,25 @@ describe('validateOptions', () => {
3132
input: makeConfig(undefined),
3233
expected: {
3334
enable: false,
35+
version: undefined,
3436
include: [/\.[jt]sx?$/],
3537
exclude: expect.arrayContaining([/\/node_modules\//]),
3638
honorSkipComments: true,
3739
functionTypes: undefined,
3840
namedOnly: false,
3941
} satisfies LiveDebuggerOptionsWithDefaults,
4042
},
43+
{
44+
description: 'honor enable: false even when the config key is present',
45+
input: makeConfig({ enable: false }),
46+
expected: expect.objectContaining({ enable: false, version: undefined }),
47+
},
4148
{
4249
description: 'enable and return defaults when an empty object is provided',
4350
input: makeConfig({}),
4451
expected: {
4552
enable: true,
53+
version: undefined,
4654
include: [/\.[jt]sx?$/],
4755
exclude: expect.arrayContaining([/\/node_modules\//]),
4856
honorSkipComments: true,
@@ -51,14 +59,22 @@ describe('validateOptions', () => {
5159
} satisfies LiveDebuggerOptionsWithDefaults,
5260
},
5361
{
54-
description: 'honor enable: false even when the config key is present',
55-
input: makeConfig({ enable: false }),
56-
expected: expect.objectContaining({ enable: false }),
62+
description: 'honor enable: true when version is provided',
63+
input: makeConfig({ enable: true, version: '1.0.0' }),
64+
expected: expect.objectContaining({ enable: true, version: '1.0.0' }),
5765
},
5866
{
59-
description: 'honor enable: true (redundant but valid)',
60-
input: makeConfig({ enable: true }),
61-
expected: expect.objectContaining({ enable: true }),
67+
description: 'enable when a config object with version is provided',
68+
input: makeConfig({ version: '1.0.0' }),
69+
expected: {
70+
enable: true,
71+
version: '1.0.0',
72+
include: [/\.[jt]sx?$/],
73+
exclude: expect.arrayContaining([/\/node_modules\//]),
74+
honorSkipComments: true,
75+
functionTypes: undefined,
76+
namedOnly: false,
77+
} satisfies LiveDebuggerOptionsWithDefaults,
6278
},
6379
];
6480

@@ -74,51 +90,64 @@ describe('validateOptions', () => {
7490

7591
describe('valid options', () => {
7692
const cases = [
93+
{
94+
description: 'accept version as a string',
95+
input: makeConfig({ version: '1.0.0' }),
96+
expected: expect.objectContaining({ version: '1.0.0' }),
97+
},
7798
{
7899
description: 'accept string include patterns',
79-
input: makeConfig({ include: ['src/'] }),
80-
expected: expect.objectContaining({ include: ['src/'] }),
100+
input: makeConfig({ version: '1.0.0', include: ['src/'] }),
101+
expected: expect.objectContaining({ version: '1.0.0', include: ['src/'] }),
81102
},
82103
{
83104
description: 'accept RegExp include patterns',
84-
input: makeConfig({ include: [/\.tsx?$/] }),
85-
expected: expect.objectContaining({ include: [/\.tsx?$/] }),
105+
input: makeConfig({ version: '1.0.0', include: [/\.tsx?$/] }),
106+
expected: expect.objectContaining({ version: '1.0.0', include: [/\.tsx?$/] }),
86107
},
87108
{
88109
description: 'accept mixed include patterns',
89-
input: makeConfig({ include: ['src/', /\.tsx?$/] }),
90-
expected: expect.objectContaining({ include: ['src/', /\.tsx?$/] }),
110+
input: makeConfig({ version: '1.0.0', include: ['src/', /\.tsx?$/] }),
111+
expected: expect.objectContaining({
112+
version: '1.0.0',
113+
include: ['src/', /\.tsx?$/],
114+
}),
91115
},
92116
{
93117
description: 'accept string exclude patterns',
94-
input: makeConfig({ exclude: ['vendor/'] }),
95-
expected: expect.objectContaining({ exclude: ['vendor/'] }),
118+
input: makeConfig({ version: '1.0.0', exclude: ['vendor/'] }),
119+
expected: expect.objectContaining({ version: '1.0.0', exclude: ['vendor/'] }),
96120
},
97121
{
98122
description: 'accept RegExp exclude patterns',
99-
input: makeConfig({ exclude: [/node_modules/] }),
100-
expected: expect.objectContaining({ exclude: [/node_modules/] }),
123+
input: makeConfig({ version: '1.0.0', exclude: [/node_modules/] }),
124+
expected: expect.objectContaining({ version: '1.0.0', exclude: [/node_modules/] }),
101125
},
102126
{
103127
description: 'accept honorSkipComments as true',
104-
input: makeConfig({ honorSkipComments: true }),
105-
expected: expect.objectContaining({ honorSkipComments: true }),
128+
input: makeConfig({ version: '1.0.0', honorSkipComments: true }),
129+
expected: expect.objectContaining({ version: '1.0.0', honorSkipComments: true }),
106130
},
107131
{
108132
description: 'accept honorSkipComments as false',
109-
input: makeConfig({ honorSkipComments: false }),
110-
expected: expect.objectContaining({ honorSkipComments: false }),
133+
input: makeConfig({ version: '1.0.0', honorSkipComments: false }),
134+
expected: expect.objectContaining({ version: '1.0.0', honorSkipComments: false }),
111135
},
112136
{
113137
description: 'accept valid functionTypes',
114-
input: makeConfig({ functionTypes: ['arrowFunction', 'classMethod'] }),
138+
input: makeConfig({
139+
version: '1.0.0',
140+
functionTypes: ['arrowFunction', 'classMethod'],
141+
}),
115142
expected: expect.objectContaining({
143+
version: '1.0.0',
116144
functionTypes: ['arrowFunction', 'classMethod'],
117145
}),
118146
},
119147
{
120148
description: 'accept all valid functionTypes',
121149
input: makeConfig({
150+
version: '1.0.0',
122151
functionTypes: [
123152
'functionDeclaration',
124153
'functionExpression',
@@ -129,6 +158,7 @@ describe('validateOptions', () => {
129158
],
130159
}),
131160
expected: expect.objectContaining({
161+
version: '1.0.0',
132162
functionTypes: [
133163
'functionDeclaration',
134164
'functionExpression',
@@ -141,28 +171,28 @@ describe('validateOptions', () => {
141171
},
142172
{
143173
description: 'accept namedOnly as true',
144-
input: makeConfig({ namedOnly: true }),
145-
expected: expect.objectContaining({ namedOnly: true }),
174+
input: makeConfig({ version: '1.0.0', namedOnly: true }),
175+
expected: expect.objectContaining({ version: '1.0.0', namedOnly: true }),
146176
},
147177
{
148178
description: 'accept namedOnly as false',
149-
input: makeConfig({ namedOnly: false }),
150-
expected: expect.objectContaining({ namedOnly: false }),
179+
input: makeConfig({ version: '1.0.0', namedOnly: false }),
180+
expected: expect.objectContaining({ version: '1.0.0', namedOnly: false }),
151181
},
152182
{
153183
description: 'accept an empty include array',
154-
input: makeConfig({ include: [] }),
155-
expected: expect.objectContaining({ include: [] }),
184+
input: makeConfig({ version: '1.0.0', include: [] }),
185+
expected: expect.objectContaining({ version: '1.0.0', include: [] }),
156186
},
157187
{
158188
description: 'accept an empty exclude array',
159-
input: makeConfig({ exclude: [] }),
160-
expected: expect.objectContaining({ exclude: [] }),
189+
input: makeConfig({ version: '1.0.0', exclude: [] }),
190+
expected: expect.objectContaining({ version: '1.0.0', exclude: [] }),
161191
},
162192
{
163193
description: 'accept an empty functionTypes array',
164-
input: makeConfig({ functionTypes: [] }),
165-
expected: expect.objectContaining({ functionTypes: [] }),
194+
input: makeConfig({ version: '1.0.0', functionTypes: [] }),
195+
expected: expect.objectContaining({ version: '1.0.0', functionTypes: [] }),
166196
},
167197
];
168198

@@ -198,6 +228,38 @@ describe('validateOptions', () => {
198228
});
199229
});
200230

231+
describe('version validation', () => {
232+
it('should reject version when not a string', () => {
233+
expect(() => validateOptions(makeConfig({ version: 123 }), mockLogger)).toThrow(
234+
`Invalid configuration for ${PLUGIN_NAME}.`,
235+
);
236+
expect(mockLogger.error).toHaveBeenCalledWith(
237+
expect.stringMatching(/version.*must be a string/),
238+
);
239+
});
240+
241+
it('should reject version mismatch with sourcemap releaseVersion', () => {
242+
expect(() =>
243+
validateOptions(
244+
makeConfig(
245+
{ version: '1.0.0' },
246+
{
247+
sourcemaps: {
248+
releaseVersion: '2.0.0',
249+
},
250+
},
251+
),
252+
mockLogger,
253+
),
254+
).toThrow(`Invalid configuration for ${PLUGIN_NAME}.`);
255+
expect(mockLogger.error).toHaveBeenCalledWith(
256+
expect.stringMatching(
257+
/version.*must match.*errorTracking\.sourcemaps\.releaseVersion/,
258+
),
259+
);
260+
});
261+
});
262+
201263
describe('invalid exclude', () => {
202264
const cases = [
203265
{

packages/plugins/live-debugger/src/validate.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,27 @@ const red = chalk.bold.red;
1414
export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptionsWithDefaults => {
1515
const pluginConfig: LiveDebuggerOptions = config[CONFIG_KEY] || {};
1616
const errors: string[] = [];
17+
const sourcemapReleaseVersion = config.errorTracking?.sourcemaps?.releaseVersion;
1718

1819
// Validate enable option
1920
if (pluginConfig.enable !== undefined && typeof pluginConfig.enable !== 'boolean') {
2021
errors.push(`${red('enable')} must be a boolean`);
2122
}
2223

24+
// Validate version option
25+
if (pluginConfig.version !== undefined && typeof pluginConfig.version !== 'string') {
26+
errors.push(`${red('version')} must be a string`);
27+
}
28+
if (
29+
pluginConfig.version &&
30+
sourcemapReleaseVersion &&
31+
pluginConfig.version !== sourcemapReleaseVersion
32+
) {
33+
errors.push(
34+
`${red('version')} must match ${red('errorTracking.sourcemaps.releaseVersion')} when both Live Debugger and sourcemap upload are configured`,
35+
);
36+
}
37+
2338
// Validate include option
2439
if (pluginConfig.include !== undefined) {
2540
if (!Array.isArray(pluginConfig.include)) {
@@ -86,6 +101,7 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio
86101
// Build the final configuration with defaults
87102
return {
88103
enable: pluginConfig.enable ?? !!config[CONFIG_KEY],
104+
version: pluginConfig.version,
89105
include: pluginConfig.include || [/\.[jt]sx?$/], // .js, .jsx, .ts, .tsx
90106
exclude: pluginConfig.exclude || [
91107
/\/node_modules\//,

0 commit comments

Comments
 (0)