Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/plugins/live-debugger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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?$/]`
Expand Down
1 change: 1 addition & 0 deletions packages/plugins/live-debugger/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const makeOptions = (
overrides: Partial<LiveDebuggerOptionsWithDefaults> = {},
): LiveDebuggerOptionsWithDefaults => ({
enable: true,
version: '1.0.0',
include: [/\.[jt]sx?$/],
exclude: [/\/node_modules\//],
honorSkipComments: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/live-debugger/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +25,7 @@ export type LiveDebuggerOptions = {

export type LiveDebuggerOptionsWithDefaults = {
enable: boolean;
version: string | undefined;
include: (string | RegExp)[];
exclude: (string | RegExp)[];
honorSkipComments: boolean;
Expand Down
126 changes: 94 additions & 32 deletions packages/plugins/live-debugger/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -31,18 +32,25 @@ describe('validateOptions', () => {
input: makeConfig(undefined),
expected: {
enable: false,
version: undefined,
include: [/\.[jt]sx?$/],
exclude: expect.arrayContaining([/\/node_modules\//]),
honorSkipComments: true,
functionTypes: undefined,
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,
Expand All @@ -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,
},
];

Expand All @@ -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',
Expand All @@ -129,6 +158,7 @@ describe('validateOptions', () => {
],
}),
expected: expect.objectContaining({
version: '1.0.0',
functionTypes: [
'functionDeclaration',
'functionExpression',
Expand All @@ -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: [] }),
},
];

Expand Down Expand Up @@ -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 = [
{
Expand Down
16 changes: 16 additions & 0 deletions packages/plugins/live-debugger/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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\//,
Expand Down
Loading