Skip to content
Open
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
Binary file not shown.
1 change: 1 addition & 0 deletions LICENSES-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ Component,Origin,Licence,Copyright
@types/mute-stream,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mute-stream)
@types/node,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node)
@types/parse-json,npm,MIT,(https://www.npmjs.com/package/@types/parse-json)
@types/picomatch,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/picomatch)
@types/resolve,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/resolve)
@types/retry,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/retry)
@types/semver,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/semver)
Expand Down
25 changes: 23 additions & 2 deletions packages/plugins/live-debugger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Automatically instrument JavaScript functions at build time to enable Live Debug
- [metadata.version](#metadataversion)
- [liveDebugger.include](#livedebuggerinclude)
- [liveDebugger.exclude](#livedebuggerexclude)
- [liveDebugger.fileExtensions](#livedebuggerfileextensions)
- [liveDebugger.honorSkipComments](#livedebuggerhonorskipcomments)
- [liveDebugger.functionTypes](#livedebuggerfunctiontypes)
- [liveDebugger.namedOnly](#livedebuggernamedonly)
Expand Down Expand Up @@ -53,6 +54,7 @@ liveDebugger?: {
enable?: boolean;
include?: (string | RegExp)[];
exclude?: (string | RegExp)[];
fileExtensions?: string[] | 'all';
honorSkipComments?: boolean;
functionTypes?: FunctionKind[];
namedOnly?: boolean;
Expand Down Expand Up @@ -127,9 +129,11 @@ If omitted, Live Debugger instrumentation still works, but browser build lookup

### liveDebugger.include

> default: `[/\.[jt]sx?$/]`
> default: `[]` (all paths)

Array of file patterns (strings or RegExp) to include for instrumentation. By default, all JavaScript and TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`) are included.
Array of file patterns to include for instrumentation. Strings are glob patterns resolved relative to the current working directory; regular expressions are matched directly against the file ID. Multiple patterns are alternatives, so a file is included when any pattern matches.

The [`fileExtensions`](#livedebuggerfileextensions) filter is applied independently. For example, `include: ['src/**']` limits instrumentation to the `src` directory without accidentally including CSS or other non-JavaScript assets.

### liveDebugger.exclude

Expand All @@ -146,6 +150,23 @@ Array of file patterns (strings or RegExp) to exclude from instrumentation. By d
- Datadog browser SDK packages (`@datadog/browser-*`, when npm linked)
- Datadog browser SDK source files (`browser-sdk/packages/`)

### liveDebugger.fileExtensions

> default: `['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts']`

Non-empty array of file extensions eligible for instrumentation. Extension matching is case-insensitive and is applied in addition to `include` and `exclude`.

Use custom extensions for file IDs containing JavaScript produced by another transform:

```ts
liveDebugger: {
include: ['src/**'],
fileExtensions: ['.js', '.ts', '.vue'],
}
```

Set `fileExtensions: 'all'` to disable the extension guard. This is intended for advanced configurations where `include` fully identifies instrumentable files.

### liveDebugger.honorSkipComments

> default: `true`
Expand Down
4 changes: 3 additions & 1 deletion packages/plugins/live-debugger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
"dependencies": {
"@dd/core": "workspace:*",
"@jridgewell/remapping": "2.3.5",
"chalk": "2.3.1"
"chalk": "2.3.1",
"picomatch": "4.0.3"
},
"devDependencies": {
"@babel/parser": "7.24.5",
"@babel/traverse": "7.24.5",
"@babel/types": "7.24.5",
"@jridgewell/trace-mapping": "0.3.31",
"@types/picomatch": "4.0.2",
"magic-string": "0.30.21",
"typescript": "5.4.3"
},
Expand Down
11 changes: 11 additions & 0 deletions packages/plugins/live-debugger/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@ export const PLUGIN_NAME: PluginName = 'datadog-live-debugger-plugin' as const;

// Skip instrumentation comment
export const SKIP_INSTRUMENTATION_COMMENT = '@dd-no-instrumentation';

export const DEFAULT_FILE_EXTENSIONS = [
'.js',
'.jsx',
'.ts',
'.tsx',
'.mjs',
'.cjs',
'.mts',
'.cts',
] as const;
94 changes: 94 additions & 0 deletions packages/plugins/live-debugger/src/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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 path from 'path';
import picomatch from 'picomatch';

import type { FileExtensions, LiveDebuggerOptionsWithDefaults } from './types';

type Pattern = string | RegExp;
type Matcher = (id: string) => boolean;

const BACKSLASH_PATTERN = /\\/g;
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^(?:[A-Z]:[\\/]|\\\\)/i;
const ID_SUFFIX_PATTERN = /[?#]/;

const normalizePath = (filePath: string): string => filePath.replace(BACKSLASH_PATTERN, '/');

const resolveGlob = (glob: string): string => {
if (
glob.startsWith('**') ||
path.isAbsolute(glob) ||
WINDOWS_ABSOLUTE_PATH_PATTERN.test(glob)
) {
return normalizePath(glob);
}

const absoluteGlob = path.resolve(glob);
return normalizePath(absoluteGlob);
};

const createPatternMatcher = (pattern: Pattern): Matcher => {
if (pattern instanceof RegExp) {
return (id) => {
const normalizedId = normalizePath(id);
const matches = pattern.test(normalizedId);
pattern.lastIndex = 0;
return matches;
};
}

const glob = resolveGlob(pattern);
const matchesGlob = picomatch(glob, { dot: true });
return (id) => {
const normalizedId = normalizePath(id);
return matchesGlob(normalizedId);
};
};

const createPatternFilter = (include: Pattern[], exclude: Pattern[]): Matcher => {
const includeMatchers = include.map(createPatternMatcher);
const excludeMatchers = exclude.map(createPatternMatcher);

return (id) => {
if (excludeMatchers.some((matches) => matches(id))) {
return false;
}

return includeMatchers.length === 0 || includeMatchers.some((matches) => matches(id));
};
};

const createFileExtensionFilter = (fileExtensions: FileExtensions): Matcher => {
if (fileExtensions === 'all') {
return () => true;
}

const normalizedExtensions = fileExtensions.map((extension) => extension.toLowerCase());
return (id) => {
const [filePath] = id.split(ID_SUFFIX_PATTERN);
const normalizedFilePath = filePath.toLowerCase();
return normalizedExtensions.some((extension) => normalizedFilePath.endsWith(extension));
};
};

export const createFileFilter = (
options: Pick<LiveDebuggerOptionsWithDefaults, 'include' | 'exclude' | 'fileExtensions'>,
): Matcher => {
const matchesPatterns = createPatternFilter(options.include, options.exclude);
const matchesFileExtension = createFileExtensionFilter(options.fileExtensions);

return (id) => matchesFileExtension(id) && matchesPatterns(id);
};

const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

export const createFileExtensionPattern = (fileExtensions: FileExtensions): RegExp | undefined => {
if (fileExtensions === 'all') {
return undefined;
}

const escapedExtensions = fileExtensions.map(escapeRegExp);
return new RegExp(`(?:${escapedExtensions.join('|')})(?:[?#].*)?$`, 'i');
};
82 changes: 68 additions & 14 deletions packages/plugins/live-debugger/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { InjectPosition } from '@dd/core/types';
import { getContextMock, getGetPluginsArg } from '@dd/tests/_jest/helpers/mocks';
import type { UnpluginBuildContext, UnpluginContext } from 'unplugin';

import { PLUGIN_NAME } from './constants';
import { DEFAULT_FILE_EXTENSIONS, PLUGIN_NAME } from './constants';
import { getLiveDebuggerPlugin, getPlugins } from './index';
import { getRuntimeBootstrap } from './runtime-bootstrap';
import type { LiveDebuggerOptionsWithDefaults } from './types';
Expand All @@ -15,8 +15,9 @@ const makeOptions = (
overrides: Partial<LiveDebuggerOptionsWithDefaults> = {},
): LiveDebuggerOptionsWithDefaults => ({
version: '1.0.0',
include: [/\.[jt]sx?$/],
include: [],
exclude: [/\/node_modules\//],
fileExtensions: [...DEFAULT_FILE_EXTENSIONS],
honorSkipComments: false,
functionTypes: undefined,
namedOnly: false,
Expand Down Expand Up @@ -159,7 +160,7 @@ describe('getLiveDebuggerPlugin', () => {
const handler = getHandler(makeOptions({ include: [/\.tsx?$/] }));
const code = 'function f() { return 1; }';

expect(handler(code, '/src/style.css')).toEqual({ code });
expect(handler(code, '/src/utils.js')).toEqual({ code });
});

it('should process files matching an include pattern', () => {
Expand All @@ -169,12 +170,63 @@ describe('getLiveDebuggerPlugin', () => {
expect(handler(code, '/src/utils.ts').code).toContain('$dd_probes');
});

it('should skip include filtering when include array is empty', () => {
it.each(DEFAULT_FILE_EXTENSIONS)(
'should process the default %s file extension',
(extension) => {
const handler = getHandler(makeOptions({ include: [], exclude: [] }));
const code = 'function f() { return 1; }';

expect(handler(code, `/src/utils${extension}`).code).toContain('$dd_probes');
},
);

it('should apply the extension filter independently of a broad include glob', () => {
const handler = getHandler(
makeOptions({
include: ['**/src/**'],
exclude: [],
}),
);
const code = 'function f() { return 1; }';

expect(handler(code, '/project/src/styles.css')).toEqual({ code });
expect(handler(code, '/project/src/utils.ts').code).toContain('$dd_probes');
});

it('should process a custom file extension', () => {
const handler = getHandler(
makeOptions({
include: ['**/src/**'],
exclude: [],
fileExtensions: ['.vue'],
}),
);
const code = 'function f() { return 1; }';

expect(handler(code, '/project/src/component.vue').code).toContain('$dd_probes');
expect(handler(code, '/project/src/utils.ts')).toEqual({ code });
});

it('should process any file extension when configured with "all"', () => {
const handler = getHandler(
makeOptions({
include: ['**/src/**'],
exclude: [],
fileExtensions: 'all',
}),
);
const code = 'function f() { return 1; }';

expect(handler(code, '/project/src/anything.xyz').code).toContain('$dd_probes');
});

it('should match file extensions case-insensitively before an ID query', () => {
const handler = getHandler(makeOptions({ include: [], exclude: [] }));
const code = 'function f() { return 1; }';

// With no include patterns, all file types pass through
expect(handler(code, '/src/anything.xyz').code).toContain('$dd_probes');
expect(handler(code, '/src/component.TSX?loader=transformed').code).toContain(
'$dd_probes',
);
});

it('should exclude files matching an exclude pattern even if included', () => {
Expand All @@ -189,25 +241,27 @@ describe('getLiveDebuggerPlugin', () => {
expect(handler(code, '/node_modules/dep/index.ts')).toEqual({ code });
});

it('should support string include patterns', () => {
const handler = getHandler(makeOptions({ include: ['src/'], exclude: [] }));
it('should support glob include patterns', () => {
const handler = getHandler(makeOptions({ include: ['src/**'], exclude: [] }));
const code = 'function f() { return 1; }';
const projectRoot = process.cwd();

expect(handler(code, '/project/src/utils.ts').code).toContain('$dd_probes');
expect(handler(code, '/project/vendor/lib.ts')).toEqual({ code });
expect(handler(code, `${projectRoot}/src/utils.ts`).code).toContain('$dd_probes');
expect(handler(code, `${projectRoot}/vendor/lib.ts`)).toEqual({ code });
});

it('should support string exclude patterns', () => {
it('should support glob exclude patterns', () => {
const handler = getHandler(
makeOptions({
include: [],
exclude: ['vendor/'],
exclude: ['vendor/**'],
}),
);
const code = 'function f() { return 1; }';
const projectRoot = process.cwd();

expect(handler(code, '/project/src/utils.ts').code).toContain('$dd_probes');
expect(handler(code, '/project/vendor/lib.ts')).toEqual({ code });
expect(handler(code, `${projectRoot}/src/utils.ts`).code).toContain('$dd_probes');
expect(handler(code, `${projectRoot}/vendor/lib.ts`)).toEqual({ code });
});
});

Expand Down
25 changes: 8 additions & 17 deletions packages/plugins/live-debugger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { SourceMap } from 'magic-string';
import type { SourceMapCompact, UnpluginBuildContext } from 'unplugin';

import { CONFIG_KEY, PLUGIN_NAME } from './constants';
import { createFileExtensionPattern, createFileFilter } from './filter';
import { getRuntimeBootstrap } from './runtime-bootstrap';
import { transformCode } from './transform';
import type { LiveDebuggerOptions, LiveDebuggerOptionsWithDefaults } from './types';
Expand All @@ -29,6 +30,9 @@ export const getLiveDebuggerPlugin = (
context: GlobalContext,
): PluginOptions => {
const log = context.getLogger(PLUGIN_NAME);
const shouldInstrumentFile = createFileFilter(pluginOptions);
const fileExtensionPattern = createFileExtensionPattern(pluginOptions.fileExtensions);
const nativeInclude = fileExtensionPattern ? [fileExtensionPattern] : pluginOptions.include;

let instrumentedCount = 0;
let failedCount = 0;
Expand All @@ -48,29 +52,16 @@ export const getLiveDebuggerPlugin = (
transform: {
filter: {
id: {
include: pluginOptions.include,
include: nativeInclude,
exclude: pluginOptions.exclude,
},
},
handler(code, id) {
// Enforce include/exclude patterns at runtime because unplugin's
// Enforce all file filters at runtime because unplugin's
// native filter is not applied in bundler child compilations
// (e.g., web worker bundles in rspack/webpack).
if (pluginOptions.include.length > 0) {
const included = pluginOptions.include.some((pattern) =>
typeof pattern === 'string' ? id.includes(pattern) : pattern.test(id),
);
if (!included) {
return { code };
}
}

for (const pattern of pluginOptions.exclude) {
const excluded =
typeof pattern === 'string' ? id.includes(pattern) : pattern.test(id);
if (excluded) {
return { code };
}
if (!shouldInstrumentFile(id)) {
return { code };
}

if (totalFilesWithFunctions >= DD_LD_LIMIT) {
Expand Down
3 changes: 3 additions & 0 deletions packages/plugins/live-debugger/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export const VALID_FUNCTION_KINDS = [
] as const;

export type FunctionKind = (typeof VALID_FUNCTION_KINDS)[number];
export type FileExtensions = string[] | 'all';

export type LiveDebuggerOptions = {
enable?: boolean;
include?: (string | RegExp)[];
exclude?: (string | RegExp)[];
fileExtensions?: FileExtensions;
honorSkipComments?: boolean;
functionTypes?: FunctionKind[];
namedOnly?: boolean;
Expand All @@ -26,6 +28,7 @@ export type LiveDebuggerOptionsWithDefaults = {
version: string | undefined;
include: (string | RegExp)[];
exclude: (string | RegExp)[];
fileExtensions: FileExtensions;
honorSkipComments: boolean;
functionTypes: FunctionKind[] | undefined;
namedOnly: boolean;
Expand Down
Loading
Loading