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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ module.exports = {
'packages/core/**/*',
'packages/published/**/*',
'packages/plugins/**/built/*',
'packages/plugins/**/scripts/**/*',
'packages/tests/src/_jest/**/*',
'packages/tests/src/_playwright/**/*',
'packages/tests/src/e2e/**/*',
Expand Down
7 changes: 5 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ packages/plugins/analytics @yoannmoin
packages/plugins/custom-hooks @yoannmoinet

# True End
packages/plugins/true-end @yoannmoinet
packages/plugins/true-end @yoannmoinet

# Async Queue
packages/plugins/async-queue @yoannmoinet
Expand All @@ -41,4 +41,7 @@ packages/plugins/async-queue @yoannmoin
packages/plugins/output @yoannmoinet

# Apps
packages/plugins/apps @yoannmoinet
packages/plugins/apps @yoannmoinet

# Live Debugger
packages/plugins/live-debugger/ @DataDog/debugger @yoannmoinet
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion LICENSES-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Component,Origin,Licence,Copyright
@jridgewell/resolve-uri,npm,MIT,Justin Ridgewell (https://www.npmjs.com/package/@jridgewell/resolve-uri)
@jridgewell/set-array,npm,MIT,Justin Ridgewell (https://www.npmjs.com/package/@jridgewell/set-array)
@jridgewell/source-map,npm,MIT,Justin Ridgewell (https://www.npmjs.com/package/@jridgewell/source-map)
@jridgewell/sourcemap-codec,npm,MIT,Rich Harris (https://www.npmjs.com/package/@jridgewell/sourcemap-codec)
@jridgewell/sourcemap-codec,npm,MIT,Justin Ridgewell (https://github.com/jridgewell/sourcemaps/tree/main/packages/sourcemap-codec)
@jridgewell/trace-mapping,npm,MIT,Justin Ridgewell (https://www.npmjs.com/package/@jridgewell/trace-mapping)
@kwsites/file-exists,npm,MIT,Steve King (https://www.npmjs.com/package/@kwsites/file-exists)
@kwsites/promise-deferred,npm,MIT,Steve King (https://www.npmjs.com/package/@kwsites/promise-deferred)
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type { AppsOptions } from '@dd/apps-plugin/types';
import type * as apps from '@dd/apps-plugin';
import type { ErrorTrackingOptions } from '@dd/error-tracking-plugin/types';
import type * as errorTracking from '@dd/error-tracking-plugin';
import type { LiveDebuggerOptions } from '@dd/live-debugger-plugin/types';
import type * as liveDebugger from '@dd/live-debugger-plugin';
import type { MetricsOptions } from '@dd/metrics-plugin/types';
import type * as metrics from '@dd/metrics-plugin';
import type { OutputOptions } from '@dd/output-plugin/types';
Expand Down Expand Up @@ -261,6 +263,7 @@ export interface Options extends BaseOptions {
// #types-injection-marker
[apps.CONFIG_KEY]?: AppsOptions;
[errorTracking.CONFIG_KEY]?: ErrorTrackingOptions;
[liveDebugger.CONFIG_KEY]?: LiveDebuggerOptions;
[metrics.CONFIG_KEY]?: MetricsOptions;
[output.CONFIG_KEY]?: OutputOptions;
[rum.CONFIG_KEY]?: RumOptions;
Expand Down
1 change: 1 addition & 0 deletions packages/factory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@dd/internal-git-plugin": "workspace:*",
"@dd/internal-injection-plugin": "workspace:*",
"@dd/internal-true-end-plugin": "workspace:*",
"@dd/live-debugger-plugin": "workspace:*",
"@dd/metrics-plugin": "workspace:*",
"@dd/output-plugin": "workspace:*",
"@dd/rum-plugin": "workspace:*",
Expand Down
3 changes: 3 additions & 0 deletions packages/factory/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { notifyOnEnvOverrides } from '@dd/core/helpers/env';
// #imports-injection-marker
import * as apps from '@dd/apps-plugin';
import * as errorTracking from '@dd/error-tracking-plugin';
import * as liveDebugger from '@dd/live-debugger-plugin';
import * as metrics from '@dd/metrics-plugin';
import * as output from '@dd/output-plugin';
import * as rum from '@dd/rum-plugin';
Expand All @@ -52,6 +53,7 @@ import { getTrueEndPlugins } from '@dd/internal-true-end-plugin';
// #types-export-injection-marker
export type { types as AppsTypes } from '@dd/apps-plugin';
export type { types as ErrorTrackingTypes } from '@dd/error-tracking-plugin';
export type { types as LiveDebuggerTypes } from '@dd/live-debugger-plugin';
export type { types as MetricsTypes } from '@dd/metrics-plugin';
export type { types as OutputTypes } from '@dd/output-plugin';
export type { types as RumTypes } from '@dd/rum-plugin';
Expand Down Expand Up @@ -163,6 +165,7 @@ export const buildPluginFactory = ({
// #configs-injection-marker
['apps', apps.getPlugins],
['error-tracking', errorTracking.getPlugins],
['live-debugger', liveDebugger.getPlugins],
['metrics', metrics.getPlugins],
['output', output.getPlugins],
['rum', rum.getPlugins],
Expand Down
223 changes: 223 additions & 0 deletions packages/plugins/live-debugger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Live Debugger Plugin <!-- #omit in toc -->

Automatically instrument JavaScript functions at build time to enable Live Debugger without requiring code rebuilds.

<!-- The title and the following line will both be added to the root README.md -->

## Table of content <!-- #omit in toc -->

<!-- This is auto generated with yarn cli integrity -->

<!-- #toc -->
- [Required peer dependencies](#required-peer-dependencies)
- [Configuration](#configuration)
- [How it works](#how-it-works)
- [liveDebugger.enable](#livedebuggerenable)
- [liveDebugger.include](#livedebuggerinclude)
- [liveDebugger.exclude](#livedebuggerexclude)
- [liveDebugger.honorSkipComments](#livedebuggerhonorskipcomments)
- [liveDebugger.functionTypes](#livedebuggerfunctiontypes)
- [liveDebugger.namedOnly](#livedebuggernamedonly)
- [Skipped function types](#skipped-function-types)
- [Runtime requirements](#runtime-requirements)
- [Safe fallback when the SDK is absent](#safe-fallback-when-the-sdk-is-absent)
- [Activating probes](#activating-probes)
<!-- #toc -->

## Required peer dependencies

The Live Debugger transform relies on Babel and `magic-string`. To keep the cost of
`@datadog/*-plugin` packages small for projects that don't use Live Debugger, these
are declared as **optional peer dependencies**. When you enable the plugin by
providing a `liveDebugger` configuration, install them in your project:

```bash
# npm
npm install --save-dev @babel/parser @babel/traverse @babel/types magic-string

# yarn
yarn add --dev @babel/parser @babel/traverse @babel/types magic-string

# pnpm
pnpm add --save-dev @babel/parser @babel/traverse @babel/types magic-string
```

If any of these packages is missing when the plugin tries to instrument a file,
the plugin throws an error with the exact install command above.

## Configuration

```ts
liveDebugger?: {
enable?: boolean;
include?: (string | RegExp)[];
exclude?: (string | RegExp)[];
honorSkipComments?: boolean;
functionTypes?: FunctionKind[];
namedOnly?: boolean;
}
```

## 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.

Each instrumented function gets:
- A unique, stable function ID (format: `<file-path>;<function-name>`)
- A `$dd_probes()` call that returns active probes for that function (or `undefined` if none)
- Deferred variable-capture helpers (`$dd_e<N>` for entry variables, `$dd_l<N>` for exit variables) that are only evaluated when probes are active
- Entry point tracking with parameter capture via `$dd_entry()`
- Return value tracking with local variable capture via `$dd_return()`
- Exception tracking with variable state at throw time via `$dd_throw()`

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.

**Example transformation (block body):**

```javascript
// Before
function add(a, b) {
const sum = a + b;
return sum;
}

// After
function add(a, b) {
const $dd_p = $dd_probes('src/utils.js;add');
const $dd_e = () => ({a, b});
try {
const $dd_l = () => ({a, b, sum});
let $dd_rv;
if ($dd_p) $dd_entry($dd_p, this, $dd_e());
const sum = a + b;
return ($dd_rv = sum, $dd_p ? $dd_return($dd_p, $dd_rv, this, $dd_e(), $dd_l()) : $dd_rv);
} catch(e) { if ($dd_p) $dd_throw($dd_p, e, this, $dd_e()); throw e; }
}
```

When entry and exit variables are the same (i.e. the function has no local variable declarations), only a single helper is emitted and shared for both positions.

**Example transformation (arrow expression body):**

```javascript
// Before
const double = (x) => x * 2;

// After
const double = (x) => {
const $dd_p = $dd_probes('src/utils.js;double');
const $dd_e = () => ({x});
try {
if ($dd_p) $dd_entry($dd_p, this, $dd_e());
const $dd_rv = x * 2;
if ($dd_p) $dd_return($dd_p, $dd_rv, this, $dd_e(), $dd_e());
return $dd_rv;
} catch(e) { if ($dd_p) $dd_throw($dd_p, e, this, $dd_e()); throw e; }
};
```

### liveDebugger.enable

> default: `true` when a `liveDebugger` config block is present

Enable or disable the plugin without removing its configuration.

### liveDebugger.include

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

Array of file patterns (strings or RegExp) to include for instrumentation. By default, all JavaScript and TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`) are included.

### liveDebugger.exclude

> default: `[/\/node_modules\//, /\.min\.js$/, /\/pyodide-lib\//, /^vite\//, /\0/, /commonjsHelpers\.js$/, /__vite-browser-external/, /@datadog\/browser-/, /browser-sdk\/packages\//]`

Array of file patterns (strings or RegExp) to exclude from instrumentation. By default, the following are excluded:
- `node_modules` — Third-party dependencies
- Minified files (`.min.js`)
- Bundled third-party Pyodide library (`pyodide-lib/`)
- Vite internal modules (e.g., `vite/modulepreload-polyfill`)
- Virtual modules (Rollup/Vite convention using null byte prefix)
- Rollup commonjs helpers
- Vite browser externals
- Datadog browser SDK packages (`@datadog/browser-*`, when npm linked)
- Datadog browser SDK source files (`browser-sdk/packages/`)

### liveDebugger.honorSkipComments

> default: `true`

Skip instrumentation of functions marked with the `// @dd-no-instrumentation` comment. This is useful for performance-critical functions where even the no-op overhead should be avoided.

**Example:**

```javascript
// @dd-no-instrumentation
function hotPath() {
// This function will not be instrumented
}
```

### liveDebugger.functionTypes

> default: `undefined` (all function types)

Array of function kinds to instrument. When unset, all function types are instrumented. Valid values:

- `'functionDeclaration'` — `function foo() {}`
- `'functionExpression'` — `const foo = function() {}`
- `'arrowFunction'` — `const foo = () => {}`
- `'objectMethod'` — `{ foo() {} }`
- `'classMethod'` — `class Foo { foo() {} }`
- `'classPrivateMethod'` — `class Foo { #foo() {} }`

**Example — instrument only function declarations and arrow functions:**

```ts
liveDebugger: {
functionTypes: ['functionDeclaration', 'arrowFunction'],
}
```

### liveDebugger.namedOnly

> default: `false`

When `true`, only named functions are instrumented. Anonymous callbacks (e.g. `[].map((x) => x)`) are skipped. A function is considered "named" if it has an explicit name via declaration, variable assignment, object/class property, or assignment target.

**Example:**

```ts
liveDebugger: {
namedOnly: true,
}
```

With `namedOnly: true`:
- `const double = (x) => x * 2` — **instrumented** (named via variable assignment)
- `[1, 2].map((x) => x * 2)` — **skipped** (anonymous callback)
- `function add(a, b) { return a + b; }` — **instrumented** (named declaration)

## Skipped function types

The following function types are always skipped regardless of configuration:
- **Generators** — `function*` declarations and expressions
- **Constructors** — `constructor()` methods in classes

## Runtime requirements

The instrumented code calls four global functions at runtime: `$dd_probes`, `$dd_entry`, `$dd_return`, and `$dd_throw`. These are provided by the **Datadog Browser Debugger SDK** (`@datadog/browser-debugger`).

### Safe fallback when the SDK is absent

The plugin automatically injects a minimal no-op stub into every output chunk:

```javascript
if (typeof globalThis.$dd_probes === 'undefined') { globalThis.$dd_probes = function() {} }
```

This ensures that instrumented code never crashes, even if the SDK has not been loaded. The stub makes `$dd_probes` return `undefined`, which causes all `$dd_entry`, `$dd_return`, and `$dd_throw` calls to be skipped (they are guarded by `if (probe)` checks).

### Activating probes

When the Debugger SDK loads and `DD_DEBUGGER.init()` is called, it overwrites `$dd_probes` with the real implementation and sets up `$dd_entry`, `$dd_return`, and `$dd_throw`. Probes begin working immediately on the next function invocation — no rebuild required. The ordering of SDK initialization vs. application code execution does not matter.
56 changes: 56 additions & 0 deletions packages/plugins/live-debugger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@dd/live-debugger-plugin",
"packageManager": "yarn@4.0.2",
"license": "MIT",
"private": true,
"author": "Datadog",
"description": "Instruments JavaScript functions at build time for Live Debugger.",
"homepage": "https://github.com/DataDog/build-plugins/tree/main/packages/plugins/live-debugger#readme",
"repository": {
"type": "git",
"url": "https://github.com/DataDog/build-plugins",
"directory": "packages/plugins/live-debugger"
},
"buildPlugin": {
"hideFromRootReadme": true
},
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
},
"scripts": {
"benchmark:subset": "node ./scripts/benchmark-subset.js",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@dd/core": "workspace:*",
"chalk": "2.3.1"
},
"devDependencies": {
"@babel/parser": "7.24.5",
"@babel/traverse": "7.24.5",
"@babel/types": "7.24.5",
"magic-string": "0.30.21",
"typescript": "5.4.3"
},
"peerDependencies": {
"@babel/parser": "7.24.5",
"@babel/traverse": "7.24.5",
"@babel/types": "7.24.5",
"magic-string": "^0.30.0"
},
"peerDependenciesMeta": {
"@babel/parser": {
"optional": true
},
"@babel/traverse": {
"optional": true
},
"@babel/types": {
"optional": true
},
"magic-string": {
"optional": true
}
}
}
Loading
Loading