Skip to content

Make Live Debugger function IDs stable and unique - #625

Draft
watson wants to merge 2 commits into
watson/DEBUG-6327/generate-symbol-tablefrom
watson/DEBUG-6332/stable-unique-function-ids
Draft

watson wants to merge 2 commits into
watson/DEBUG-6327/generate-symbol-tablefrom
watson/DEBUG-6332/stable-unique-function-ids

Conversation

@watson

@watson watson commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

DEBUG-6332, stacked on #582: function IDs no longer contain line numbers, every function of a module gets a different ID, and the path is relative to the repository root.

Function Before After
add in app/src/math.js, build root app src/math.js;add app/src/math.js;add
double inside add src/math.js;double app/src/math.js;add/double
class Cart { total() {} } …;total …;Cart.total
get size() and set size(v) in Cart …;size, twice …;Cart.size:get, …;Cart.size:set
First and second callback in doubleAll …;<anonymous>@12:20:0, …;<anonymous>@13:20:0 …;doubleAll/<anonymous>, …;doubleAll/<anonymous>#1
open in function make() { class Box { open() {} } } …;open …;make/Box.open
Peer package through a Yarn virtual path ../.yarn/__virtual__/peer-virtual-…/1/packages/peer/index.js;peerHello packages/peer/index.js;peerHello
Vue script module src/Cart.vue?vue&type=script;total app/src/Cart.vue;total
transformCode                       # same single parse and traversal
  getFunctionIdPath                 # once per module: strip ?query, resolve Yarn virtual path,
                                    #   relative to the repo root (memoized per build, build root
                                    #   outside git); realpath only if that's outside the root
  traverse
    Class enter/exit                # container stack: nearest named function or class
    Function enter
      enterFunction                 # own name (+ :get/:set), qualified with "." for direct class
                                    #   members, "/" otherwise; anonymous containers are transparent;
                                    #   next free #<n> for a used name, before any skip check
      skip checks, collect target   # unchanged
    Function exit

The path helpers move from src/symbol-table/paths.ts to src/paths.ts. The symbol table now trusts the ID: sourceFile is the ID's path, and paths that are absolute or start with ../ are still left out. parseFunctionId returns the last / segment without its #<n> suffix as the display name, such as Cart.size:get, double or <anonymous>.

Left out on purpose:

  • Display names from the original source. The input source map is only available on webpack and Rspack, and decoding it for every module would cost build time. Names come from the code the plugin transforms, after earlier loaders.
  • Suffixes that survive inserting a sibling. A function's #<n> shifts when a function with the same qualified name is added before it in the same named scope. That's treated like a move, and a test documents it.

Known limitations:

  • IDs are unique within a module. Modules of the same file with different queries, such as vue-loader's script and template modules, are numbered separately and can share an ID. The symbol table keeps them apart by location, but a probe would match both. Numbering across modules would make IDs depend on the order bundlers transform modules in.
  • A string key with a /, such as { '/users': () => {} }, gets only the part after its last / as display name. The ID stays unique.

The transform/index.ts diff is mostly re-indentation, because the Function visitor became { enter, exit }. Hide whitespace to review it.

Evidence

  • Before: a getter and a setter shared an ID, and IDs changed whenever a line was added above.
    After: functionId.test.ts covers qualified names, accessors, class fields and expressions, transparent anonymous containers, duplicates including a key literally named 'f#1', a module of awkward shapes where every function gets a different ID, a before/after table of edits that keep IDs, and the documented suffix shift.

  • transform/index.test.ts: skip comments, functionTypes and namedOnly don't change the IDs of other functions.

  • symbolTable.integration.test.ts, on esbuild, Rollup, Rspack, Vite and webpack: IDs are relative to the repository root, without __virtual__ or ?, the table's sourceFile is the ID's path, and each function_id has a single location.

  • Size, on the @datadog/browser-core, browser-rum-core and browser-rum 6.26.0 ESM files (279 files, 1,643 instrumented functions), transformed by both versions with the same path part:

    Before After Change
    Distinct IDs 1,610 of 1,643 1,643 of 1,643
    ID strings 112,575 B 128,372 B +15,797 B (+14%)
    Transformed code, minified with esbuild 793,378 B 809,175 B +2.0%
    The same, gzipped 154,674 B 153,560 B −1,114 B (−0.7%)
    Transform time, median of 10 runs 452–463 ms 458–470 ms +1–2%

    Gzipped output gets smaller because repeated qualified prefixes compress better than unique line and column numbers. When the build root is a subdirectory of the repository, each ID also grows by that subdirectory, such as app/, while workspace packages outside the build root lose their ../.

  • Live Debugger suite: 516/516. Live Debugger typecheck, yarn lint and yarn cli integrity pass.

Merge Danger

Door: two-way until released

Once released, probes are created against these IDs, so changing the format again would orphan them.

Blast Radius: every Live Debugger build

  • Every function ID changes. Don't release before DEBUG-6326 is deployed, so that the Browser Debugger SDK matches probes on where.functionId instead of deriving anything from the ID.
  • The internal TransformOptions.buildRoot is renamed to repositoryRoot. transform/corpus.test.ts on master still passes buildRoot, so it needs the rename when master is merged into this stack.
  • One .git lookup per build, and one realpath per module outside the repository root.

🤖 Generated with Claude Code

Function IDs no longer contain line numbers. The name part is the
function's qualified name: its own name, prefixed by the nearest named
function or class around it (`Cart.total`, `Cart.size:get`,
`doubleAll/<anonymous>`, `make/Box.open`). Functions whose qualified
name is already used in the module get the next free `#<n>` suffix, in
traversal order and before any skip check, so `namedOnly`,
`functionTypes` and skip comments don't shift other IDs.

The path part is now relative to the git repository root (or the build
root outside one), without query strings or Yarn PnP virtual folders.
Symlinks are only resolved for files outside the root. The path helpers
move from the symbol table to `src/paths.ts`, and the symbol table now
takes each function's source file straight from its ID.

Environment: Datadog workspace

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 8454844 | Docs | View more details | Give us feedback!

Master's corpus test now passes the transform's repositoryRoot option.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant