Skip to content

Add live-debugger plugin#296

Merged
yoannmoinet merged 7 commits intomasterfrom
watson/DEBUG-5291/add-live-debugger-plugin
Apr 20, 2026
Merged

Add live-debugger plugin#296
yoannmoinet merged 7 commits intomasterfrom
watson/DEBUG-5291/add-live-debugger-plugin

Conversation

@watson
Copy link
Copy Markdown
Contributor

@watson watson commented Apr 1, 2026

Supersedes #253

What and why?

Adds a new Live Debugger build plugin (@dd/live-debugger-plugin) that automatically instruments JavaScript/TypeScript functions at build time to enable Live Debugger without requiring code rebuilds.

When enabled, every matching function in the application is wrapped with lightweight probes that can be activated at runtime via $dd_probes(functionId). When no probes are active the check returns undefined and all instrumentation is skipped.

This originated as an Innovation Week POC and has been hardened with bug fixes, performance optimizations (down from ~17x to ~1.2x build overhead), comprehensive test coverage, and filtering options.

Notes:

  • This PR is not intended to be production-ready. The goal is to land the plugin in a state that is ready for dogfooding. In particular, the function ID algorithm (<relative-file-path>;<function-name>) is a placeholder — the final stable ID scheme will be implemented in a follow-up PR before this is ready for end user consumption.
  • There is also a known escaping limitation in the current POC: function IDs are still embedded directly into generated code, so quoted method names containing a single quote can produce invalid output. This will be fixed before the plugin is considered production-ready.

Overhead stats

  • Build overhead: ~1.2x (for the files where instrumentation code is added).
  • Runtime overhead of no-op instrumented functions is negligible (near zero) for all browsers except Firefox, which isn't able to fully optimize away the no-op instrumentations.

Installation

Babel (@babel/parser, @babel/traverse, @babel/types) and magic-string are declared as optional peer dependencies on the 5 published bundler packages. Users who don't enable Live Debugger don't pay the install-size cost (~12 MB of Babel + magic-string). When the plugin is enabled (by providing a liveDebugger configuration), projects install the peer deps alongside the Datadog plugin:

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

If any of them is missing at build time, the plugin throws with an actionable install hint instead of a raw Node MODULE_NOT_FOUND.

How?

New plugin (packages/plugins/live-debugger/)

  • Transform pipeline (src/transform/): Uses Babel to parse the AST in read-only mode, collects instrumentation targets, then applies injections via MagicString (no AST mutation). Processes inner functions before outer ones so that appendLeft calls at shared positions stack correctly.
  • Lazy loader (src/transform/loader.ts): Gates the entire transform module behind getTransformCode(), so Babel and magic-string are not loaded at all when the liveDebugger configuration is omitted. When the plugin is enabled, peer deps are loaded on the first file that reaches the transform hook, and a missing module is rewrapped with a clear install hint.
  • Function ID generation (src/transform/functionId.ts): Produces human-readable IDs in <relative-file-path>;<function-name> format. Anonymous functions use <anonymous>@line:col:siblingIndex for disambiguation. This algorithm is temporary and will be replaced with a stable, production-grade scheme in a follow-up.
  • Scope tracking (src/transform/scopeTracker.ts): Extracts variable names (params + locals, capped at 25) for entry and exit snapshots so probes can capture local state.
  • Instrumentation guards (src/transform/instrumentation.ts): Skips functions that can't be safely instrumented (generators, async generators, class constructors) and respects // @dd-no-instrumentation skip comments.
  • CJS interop (src/transform/cjs-interop.ts): Handles @babel/traverse CJS default-export resolution in bundled environments where the module wrapper shape can vary.
  • Option validation (src/validate.ts): Validates all user-supplied config with descriptive error messages.

Configuration options (providing a liveDebugger object — even an empty {} — enables the plugin, matching the convention used by the other product plugins):

  • include / exclude — file patterns to control scope (defaults include .js/.jsx/.ts/.tsx, exclude node_modules, minified files, virtual modules, Datadog SDK packages, etc.)
  • honorSkipComments — respect // @dd-no-instrumentation comments (default: true)
  • functionTypes — restrict to specific function kinds (e.g., functionDeclaration, arrowFunction, classMethod)
  • namedOnly — skip anonymous functions (default: false)

The env variable DD_LD_LIMIT can cap the number of files with functions that are processed, as a safety valve during dogfooding.

Factory integration:

  • Registered in packages/factory/src/index.ts and packages/core/src/types.ts via the standard plugin injection markers.
  • Exposed through all published bundler packages (webpack, esbuild, rollup, rspack, vite), with Babel and magic-string declared as optional peerDependencies so they're only installed by consumers who enable Live Debugger.
  • Hidden from the root README via hideFromRootReadme: true until the plugin is production-ready.

Runtime stubs:

  • The plugin injects a minimal no-op stub (~80 bytes) into all output chunks via context.inject(). This defines globalThis.$dd_probes as an empty function so instrumented code never crashes when the Datadog Browser Debugger SDK (@datadog/browser-debugger) is absent.
  • $dd_entry, $dd_return, and $dd_throw don't need stubs because they are always guarded by if (probe) checks in the injected instrumentation.
  • When the SDK loads and DD_DEBUGGER.init() is called, it overwrites $dd_probes with the real implementation and sets up the other three globals. Probes activate immediately — no rebuild required.

Testing & benchmarks:

  • Comprehensive transform unit tests covering all supported function types, instrumentation patterns, skip comments, control flow, source maps, and deterministic output
  • Plugin unit tests covering child-compilation include/exclude fallback behavior, DD_LD_LIMIT capping, error handling, and option validation
  • CJS interop tests and a dedicated peer-dependency test suite (eager-import surface, lazy-load chain, missing-dep diagnostics) that locks in the optional-install contract
  • E2E browser smoke tests running across all supported bundlers and browsers, verifying the app works both without the debugger SDK and with simulated active probes (including code-splitting with dynamic imports)
  • Benchmark script (scripts/benchmark-subset.js) for measuring build overhead on real file subsets

@watson watson changed the title Add live-debugger plugin POC for Innovation Week Add live-debugger plugin Apr 1, 2026
@watson watson force-pushed the watson/DEBUG-5291/add-live-debugger-plugin branch from 221a99f to 0704915 Compare April 1, 2026 19:32
Copy link
Copy Markdown
Contributor Author

watson commented Apr 1, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@watson watson force-pushed the watson/DEBUG-5291/add-live-debugger-plugin branch 7 times, most recently from 6614e50 to f9499d8 Compare April 2, 2026 08:05
@watson watson force-pushed the watson/DEBUG-5291/add-live-debugger-plugin branch 5 times, most recently from e9873f5 to 2f6900f Compare April 9, 2026 12:25
@watson watson marked this pull request as ready for review April 11, 2026 19:43
@watson watson requested a review from yoannmoinet as a code owner April 11, 2026 19:43
Copy link
Copy Markdown
Member

@yoannmoinet yoannmoinet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty solid PR, well done!

I have some concerns though.

A ~1.2x build overhead is not "negligible", that's 20%.
Totally not a blocker, but I don't want to misrepresent the impact of this.

I'm not a fan of adding babel for everyone, especially with Unplugin shipping with rollup's this.parse for both rollup and vite (and rolldown), which makes babel redundant in these cases.

It could also be set as peer dependencies or optional dependencies, and verified at runtime. So not everyone installing build-plugins has to pay the price of such dependencies.

ref

For bundlers other than Rollup, Rolldown, or Vite, setParseImpl must be called to manually provide a parser implementation. Parsers such as Acorn, Babel, or Oxc can be used.

I'd prefer you to follow this way of adding a parser to the ecosystem, could sit in a different, independent PR too, it would lighten this one.

Note that we have a parallel effort in that regard, led by @sdkennedy2, and they want to use oxc, which is more modern than babel imo, and a better option. For instance, oxc would prevent some workarounds you have to do with babel.

Comment thread packages/published/vite-plugin/package.json Outdated
Comment thread packages/published/vite-plugin/package.json Outdated
@watson
Copy link
Copy Markdown
Contributor Author

watson commented Apr 16, 2026

A ~1.2x build overhead is not "negligible", that's 20%.
Totally not a blocker, but I don't want to misrepresent the impact of this.

Sorry, I was referring to the runtime overhead being negligible. But I can see the PR body could easily be misunderstood. I've re-phrased it.

@watson watson requested a review from yoannmoinet April 16, 2026 16:26
Copy link
Copy Markdown
Member

@yoannmoinet yoannmoinet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit on the pattern we use for enable in other plugins.
Other than that, lgtm.

Especially if it's not "public" yet, and that it uses peer/optional dependencies for @babel/*.

I won't be as strict. Outside's impact is pretty minimal.

Comment thread packages/plugins/live-debugger/src/validate.ts Outdated
Comment thread packages/plugins/live-debugger/README.md Outdated
Comment thread packages/plugins/live-debugger/README.md Outdated
Comment thread packages/plugins/live-debugger/src/index.test.ts Outdated
@watson watson force-pushed the watson/DEBUG-5291/add-live-debugger-plugin branch from 3715b65 to 6e4d5aa Compare April 17, 2026 05:45
Copy link
Copy Markdown
Member

@yoannmoinet yoannmoinet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. :shipit:

Comment on lines +18 to +21
// Validate enable option
if (pluginConfig.enable !== undefined && typeof pluginConfig.enable !== 'boolean') {
errors.push(`${red('enable')} must be a boolean`);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit overkill but that's fine haha.

watson added 7 commits April 19, 2026 07:57
Introduce a live-debugger plugin that instruments JavaScript functions
at build time for Datadog's Live Debugger product. The plugin uses Babel
to inject probe declarations and snapshot helpers into user code,
enabling runtime debugging without redeploying.

Key features:
- AST-based function instrumentation via Babel transform
- Scope tracking for variable capture at probe locations
- Deterministic function ID generation for probe targeting
- Configurable file include/exclude filters
- File and function count limits to bound overhead
- CJS/ESM interop for Babel dependencies in bundled environments

Includes unit tests, integration tests, E2E tests across all supported
bundlers, and a benchmark script for performance validation.
The bump commit updated yarn.lock only; `yarn cli integrity`
also needs to remove the stale 1.5.0 cache zip and refresh
LICENSES-3rdparty.csv with the new attribution for
@jridgewell/sourcemap-codec and @jridgewell/gen-mapping, and
drop @babel/helper-globals (no longer pulled in by any resolved
@babel/traverse version).
The loader.ts indirection did `require('./index')` from a file that
Rollup bundles together with `./index` itself. In the published bundle,
`./index` resolves to the bundled entry — a circular require that
returned the plugin's public exports instead of the transform module.
`getTransformCode()` then cached `undefined` for `transformCode`,
instrumentation silently failed at every transform hook, and the E2E
test checking that `$dd_entry` fires (active-probes test) saw 0.

Achieve the same lazy-loading guarantee without the bundler landmine:

- Convert `import * as t from '@babel/types'` to `import type` in
  `functionId.ts`, `scopeTracker.ts`, and `instrumentation.ts`, and
  thread a `typesModule: typeof import('@babel/types')` parameter
  through the helpers that need runtime type guards (same pattern
  `collectReturnStatements`/`alwaysReturns` already used).
- Move `requireOptionalPeerDep` + the diagnostic-wrapping helpers
  into `transform/index.ts` so they're co-located with their only
  caller and don't need a separate module.
- Delete `transform/loader.ts`; `src/index.ts` goes back to a direct
  `import { transformCode } from './transform'`.

Net effect: no peer dep is loaded until a file actually reaches the
transform hook, the four `require("@babel/*"|"magic-string")` calls
survive Rollup as externalized literal requires (verified in all 5
published bundles), and the missing-peer-dep error message is
unchanged.

Also extends `lazy-deps.test.ts` to cover every peer dep via
`it.each(PEER_DEPS)` and asserts the install-hint is present in the
rewrapped error.
Align `@dd/live-debugger-plugin` with the convention used by the `rum`,
`error-tracking`, and `output` plugins: enablement is derived from the
presence of the `liveDebugger` config key, not from a dedicated `enable`
sub-property. Passing `liveDebugger: {}` now enables the plugin with
default options; omitting the key keeps it disabled.

- validate.ts: use `enable: !!config[CONFIG_KEY]`, ignoring the `enable`
  sub-property like the other plugins do.
- README: drop the `liveDebugger.enable` section and its TOC entry, and
  remove the now-redundant `enable: true` from example snippets.
- transform/index.ts: update the missing-peer-dep error message to say
  "when the `liveDebugger` plugin is enabled" instead of referencing
  `liveDebugger.enable`.
- Tests: rewrite the defaults and `getPlugins` cases to reflect the new
  semantics (undefined -> disabled, `{}` -> enabled).
The previous commit reduced enablement to `!!config[CONFIG_KEY]`, which
meant `{ liveDebugger: { enable: false } }` silently kept the plugin
enabled. Every other plugin in this repo (`apps`, `error-tracking`,
`metrics`, `output`, `rum`) accepts an explicit `enable: false` — either
directly (`apps`) or implicitly via spread ordering (the rest).

Switch `validate.ts` to the explicit `apps`-style pattern:

    enable: pluginConfig.enable ?? !!config[CONFIG_KEY]

so that:
- omitted `liveDebugger` key            -> disabled
- `liveDebugger: {}`                    -> enabled
- `liveDebugger: { enable: true }`      -> enabled
- `liveDebugger: { enable: false }`     -> disabled

Also add a runtime `typeof enable === 'boolean'` check alongside the
existing boolean-option validations, and re-document `liveDebugger.enable`
in the README to match the `apps` README wording.

Test coverage:
- Restore the "return an empty array when enable is false" test in
  `index.test.ts`.
- Add `enable: false` / `enable: true` cases to `validateOptions` defaults
  tests and a new `invalid enable` describe block covering non-boolean
  inputs. Extend the `multiple errors` aggregate test to include `enable`.
@watson watson force-pushed the watson/DEBUG-5291/add-live-debugger-plugin branch from 81c7bfd to 795ac49 Compare April 19, 2026 06:00
@yoannmoinet yoannmoinet merged commit 7bfe1da into master Apr 20, 2026
4 checks passed
@yoannmoinet yoannmoinet deleted the watson/DEBUG-5291/add-live-debugger-plugin branch April 20, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants