Draft
Conversation
This POC adds a live-debugger plugin using Babel transforms to instrument code. Babel was chosen over WASM as it was more straightforward to implement within the time constraints of Innovation Week.
Enhance function ID generation to include source location for anonymous functions (`file;<anonymous>@line:col:index`) and resolve names for class private methods, member expression assignments (`obj.foo = () => {}`), and literal property keys. Extract variables from destructured parameters (object/array patterns, rest elements, assignment patterns, TS parameter properties) so captured scope snapshots are more complete. Add a trailing `$dd_return` call for functions with implicit undefined returns, and remove the single-try-catch heuristic that could falsely skip user code resembling instrumented output. Skip files containing unsupported import patterns (CSS modules, workers, sprites, dynamic imports) before parsing, and switch the parser to `sourceType: 'unambiguous'` for broader compatibility. Track granular instrumentation metrics (failed, skipped-by-comment, skipped-file, skipped-unsupported) and surface them in the build-end log to aid debugging and monitoring of the transform pipeline.
Replace Babel codegen with MagicString string injection, eliminating @babel/generator (the most expensive operation) entirely. The AST traverse is now read-only — it collects injection positions, then MagicString applies all insertions in a single pass. Additional optimizations: - Add regex pre-filter to skip files with no function syntax before parsing - Skip code generation entirely when no functions were instrumented - Cap captured variables at 25 to fix RangeError crash on large scopes - Replace O(n²) countPreviousAnonymousSiblings with pre-indexed Map - Hoist variable capture into shared helpers instead of duplicating at each return - Replace Babel scope.bindings with direct param/declaration walk
…nd new defaults - Enforce exclude patterns at runtime to cover bundler child compilations where unplugin's native filter is not applied (e.g. worker bundles) - Handle parenthesized arrow expression bodies when wrapping in blocks - Filter TypeScript `this` parameter from captured variable names - Process inner functions before outer ones to fix MagicString ordering - Deduplicate entry/exit snapshot helpers when variables are identical - Switch source maps to hires: false for faster generation - Add DD_LD_LIMIT env var to cap number of instrumented files - Exclude /pyodide-lib/ from instrumentation by default - Export validateSyntax utility for syntax checking - Tighten type annotations and add null-safety assertions
- Refactor pre-filter tests to assert unchanged code and undefined map - Add pre-filter case for classes with no instrumentable methods - Add full-output smoke tests verifying exact instrumented code for block-body functions and arrow expressions
Allow users to restrict which function kinds are instrumented via a new `functionTypes` option (e.g. only arrow functions or class methods), and to skip anonymous functions entirely with `namedOnly`. - Define FunctionKind type and VALID_FUNCTION_KINDS constant - Add filtering logic in transformCode based on AST node type and name - Wire options through from plugin config to the transform layer - Validate both options in validateOptions with descriptive errors - Add comprehensive test coverage for both options and their combination
Add a benchmark tool that measures transform performance and size overhead on a configurable set of web-ui source files, with support for repeated runs and per-file + aggregate reporting. - Add scripts/benchmark-subset.js with yarn benchmark:subset command - Exclude plugin scripts directories from ESLint overrides
The previous name implied the option was about "hot functions" when it actually controls whether @dd-no-instrumentation magic comments are respected. The new name better describes the behavior.
The old live-debugger-helpers.ts was from an earlier iteration and defined globals ($dd_start, $dd_instrumentation, etc.) that no longer match what the transform actually produces ($dd_probes, $dd_entry, $dd_return, $dd_throw). Replace it with a minimal stub that only defines a no-op $dd_probes on globalThis. This is the only global that needs a fallback because it's called unconditionally by every instrumented function — the other three are guarded by `if (probe)` checks and only need to exist once the SDK activates probes. The live-debugger plugin now injects this stub as a banner into all output chunks via context.inject(), using the same injection system as the RUM plugin. This ensures instrumented code never crashes when the Datadog Browser Debugger SDK (@datadog/browser-debugger) is absent, while adding ~80 bytes to each chunk.
221a99f to
0704915
Compare
c08af42 to
b98932a
Compare
…eference Bring documentation up to date with the current plugin implementation: - Document functionTypes and namedOnly configuration options - Update exclude defaults with pyodide-lib, browser SDK patterns - Rewrite transformation examples to reflect deferred helper functions ($dd_e/$dd_l) and $dd_rv comma-expression return wrapping - Add arrow expression body transformation example - Add "Skipped function types" section (generators, constructors) - Update table of contents with new sections - Fix stale config snippet in root README (replace removed skipHotFunctions with honorSkipComments, functionTypes, namedOnly) Rename "Dynamic Instrumentation" to "Live Debugger" in the transformCode JSDoc comment.
Replace @babel/generator with magic-string in all published plugin package.json files and update yarn.lock. Also fix license headers and import ordering in live-debugger transform.
b98932a to
419e8d5
Compare
When a block-body function ends with a semicolon-free `return` (e.g.
`function add(a,b){return a+b}`), the return-wrapping suffix and the
postamble can target the same MagicString position. Reorder the
appendLeft calls so return wrapping runs before the postamble insertion,
ensuring the suffix stacks correctly at shared positions.
The HAS_FUNCTION_SYNTAX regex only matched `function`, `=>`, and
`class`, so files containing only object-literal method shorthand
(e.g. `{ method() {} }`) were silently skipped and never instrumented.
Add `\)\s*\{` to the regex to catch method shorthand definitions.
False positives (e.g. `if () {`) are harmless — they just trigger a
Babel parse that finds zero functions.
The manual fallback that re-checks filters for bundler child compilations (e.g. web worker bundles in rspack/webpack) only enforced exclude patterns, allowing files outside the intended include scope to be instrumented. Add a matching runtime include check so both filters are applied consistently.
…p comments The @dd-no-instrumentation comment was not recognized on common declaration forms like `const fn = () => 1` or `export const fn = () => 1` because Babel attaches leading comments to the VariableDeclaration or ExportNamedDeclaration, not to the function node or its immediate parent. Replace the two-level parent check with an upward walk that checks leadingComments at every ancestor until it reaches a statement boundary, plus one additional level for wrapping export declarations. Add seven tests covering positive cases (const, export const, function declaration) and false-positive guards (consecutive declarations, nested functions, multi-declarator const, unrelated prior statement).
Move probeVarCounter from module scope into transformCode so it resets to 0 on each invocation. The previous global counter caused the same file to produce different output across rebuilds, breaking chunk hash stability in watch mode and incremental builds.
Root README.md will be automatically updated
…pe safety Remove @ts-nocheck from all transform files by introducing a local BabelPath interface that avoids the cross-package type conflict between @types/babel__traverse's bundled @babel/types and the directly imported @babel/types. Replace all `any` types and `as` casts with proper type narrowing. Convert require() calls to ESM imports. Use shared getContextMock test helper instead of hand-rolled mock. Delete orphaned rum/src/built/live-debugger-helpers.ts (stubs are already injected via context.inject in the plugin entry point).
0d13d3b to
afec47e
Compare
…onments
The conversion from require() to ESM imports broke the @babel/traverse
import in bundled environments. The old code used
`require('@babel/traverse').default` to access the traverse function,
but `import traverse from '@babel/traverse'` relies on the bundler's
CJS-ESM interop to unwrap .default automatically. rspack/webpack
double-wrap the namespace, resulting in traverse being { default: fn }
instead of fn — causing "y is not a function" errors at build time.
Extract the interop resolution into a testable resolveCjsDefaultExport
helper that normalizes both shapes, and add tests covering the direct,
double-wrapped, and actual @babel/traverse module cases.
cf34001 to
f53e9fc
Compare
7a4a2c6 to
a3e15c2
Compare
…n runs The live-debugger transform pulled in @babel/parser, @babel/traverse, @babel/types, and magic-string during plugin initialization. Because the factory imports all plugins eagerly, every build paid that startup cost even when live-debugger was disabled, which pushed the E2E suite past Playwright's global timeout. Lazy-load the transform runtime the first time instrumentation actually runs so disabled builds avoid the Babel startup overhead while keeping the published plugin bundle layout intact.
Add regression coverage for the live-debugger transform runtime and a browser smoke test for an instrumented app running without the debugger SDK. Keep the runtime smoke test scoped to the esbuild + Chromium slice so it protects the behavior without pushing the E2E suite over CI's time budget.
6614e50 to
f9499d8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 returnsundefinedand all instrumentation is skipped, so the steady-state overhead is near-zero (~1.2x build overhead, negligible, near zero runtime cost when inactive).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:
<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.How?
New plugin (
packages/plugins/live-debugger/)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 thatappendLeftcalls at shared positions stack correctly.src/transform/functionId.ts): Produces human-readable IDs in<relative-file-path>;<function-name>format. Anonymous functions get a parent-scoped sibling index. This algorithm is temporary and will be replaced with a stable, production-grade scheme in a follow-up.src/transform/scopeTracker.ts): Extracts variable names (params + locals) for entry and exit snapshots so probes can capture local state.src/transform/instrumentation.ts): Skips functions that can't be safely instrumented (e.g., generators, async generators) and respects// @dd-no-instrumentationskip comments.src/validate.ts): Validates all user-supplied config with descriptive error messages.Configuration options:
enable— toggle the plugin (default:false)include/exclude— file patterns to control scope (defaults include.js/.jsx/.ts/.tsx, excludenode_modules, minified files, virtual modules, Datadog SDK packages, etc.)honorSkipComments— respect// @dd-no-instrumentationcomments (default:true)functionTypes— restrict to specific function kinds (e.g.,functionDeclaration,arrowFunction,classMethod)namedOnly— skip anonymous functions (default:false)Factory integration:
packages/factory/src/index.tsandpackages/core/src/types.tsvia the standard plugin injection markers.Runtime stubs:
context.inject(). This definesglobalThis.$dd_probesas 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_throwdon't need stubs because they are always guarded byif (probe)checks in the injected instrumentation.DD_DEBUGGER.init()is called, it overwrites$dd_probeswith the real implementation and sets up the other three globals. Probes activate immediately — no rebuild required.Testing & benchmarks:
scripts/benchmark-subset.js) for measuring build overhead on real file subsets