fix(live-debugger): emit correct sourcemaps for instrumented functions#337
Draft
watson wants to merge 1 commit intowatson/DEBUG-5465/do-not-treat-args-as-localsfrom
Draft
Conversation
Contributor
Author
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
2c189b9 to
c59aefd
Compare
Live Debugger probes capture stack traces from instrumented functions.
For those traces to point at the right place in the user's source, the
source map for each transformed file must (a) carry segments for the
purely-injected probe code and (b) report positions in the original
source rather than in whatever intermediate buffer the transform was
handed.
For (a), wrap each function body's boundary characters (the opening
`{`, the closing `}`, and the trailing terminator of any leading
directive) via `MagicString#update` rather than `appendLeft`. `update`
routes content through `Mappings.addEdit`, which emits one source-map
segment per line of the new content anchored at the boundary
character's original location; `appendLeft` content goes through
`Mappings.advance` and produces no segments at all. Combined with
`hires: true`, every injected line resolves back to the function it
wraps, and the preamble and postamble can stay multi-line and readable
in the bundled output.
For (b), compose the magic-string delta map with the input source map
produced by the previous loader (e.g. `builtin:swc-loader` stripping
TypeScript types). With `enforce: 'post'` the transform sees the
post-loader buffer, so magic-string anchors to positions in *that*
buffer and labels them with the original file path. Without
composition the bundler reports those post-loader positions as if they
were original-source positions — captured probe stack frames in
production resolved to import statements near the top of the file
rather than the function body. Compose via `@jridgewell/remapping`
(already in the lockfile via `unplugin`) using the `inputSourceMap`
exposed by unplugin's native build context on webpack/rspack;
rollup/vite/esbuild compose maps through their own pipelines and don't
need this. Fall back to the un-composed map and log at error level —
matching the `Instrumentation Error` precedent in the same handler —
when composition throws, so a single malformed input map doesn't kill
instrumentation for every other file.
The integration test that exercises the full rspack pipeline uses a
non-identity input map (a banner-shift loader that shifts source line
numbers) so composition is validated end-to-end; an identity shim is
indistinguishable from no composition.
c59aefd to
cbb54ad
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.

What and why?
Make the Live Debugger transform produce correct source maps for the code it injects.
When a Live Debugger probe is hit at runtime, the captured stack frame is reported back to the backend with positions in the bundled JavaScript. The deobfuscation service uses the source maps emitted by this build pipeline to translate those positions into the file path, line number and column the user sees in the UI. For those user-facing positions to actually point at the function the probe wraps, the source map for each transformed file has to satisfy two properties:
Carry source-map segments for the purely-injected probe code. The transform inserts a preamble (probe declaration,
tryopener, entry call) and a postamble (returnhelper,catchblock) into the function body — those lines exist only in the bundled output, not in the original source. If they have no segments, captured stack frames hitting them resolve tonull.Report positions in the original source, not in the intermediate buffer the transform was handed. With
enforce: 'post'the transform sees the post-loader JavaScript (e.g. post-SWC, with TypeScript type imports stripped). Magic-string therefore anchors injected lines to positions in that buffer while labelling them with the original file path. Without composing back to the original source, downstream consumers report the post-loader line numbers as if they were original-source line numbers — captured probe stack frames in production were resolving to import statements near the top of the file rather than the function body.After this PR, captured probe stack frames resolve to the exact original line and column of the function being instrumented, regardless of where in the preamble/postamble they land and regardless of how heavily upstream loaders shifted line numbers.
How?
Three pieces, all in
packages/plugins/live-debugger/src/:1. Wrap the function body's boundary characters via
MagicString#updaterather thanappendLeft. For each instrumented function we edit:{(or, when present, the trailing terminator of the last leading directive) with<char><preamble>,}with<postamble><char>.update()flows throughMappings.addEdit, which emits one source-map segment per line of the new content, all anchored at the original chunk being edited. So every line of the injected preamble and postamble has a mapping that points back to the function's own source location.appendLeftcontent goes throughMappings.advanceand produces no segments at all.Two corner cases:
() => 1), the two boundary ranges collide, so we fall back to a singleupdate()covering the whole body.update's defaultoverwrite: falsepreserves anyintro/outrothat surroundingappendLeftcalls placed on neighbouring chunks, so theappendLeft-based wrapping ofreturnstatements composes correctly with the boundary updates.2.
hires: trueons.generateMap(), so every original character emits its own source-map segment. Without it, mappings are emitted at line boundaries only and column information for the function signature, parameters,returnbody, etc. is lost in the round-trip.3. Compose the magic-string delta map with the input source map produced by the previous loader, using
@jridgewell/remapping(already in the lockfile viaunplugin). The input map is exposed by unplugin's native build context (getNativeBuildContext().inputSourceMap) on webpack/rspack; rollup/vite/esbuild compose maps through their own pipelines and don't need this. Whenremappingthrows (malformed input map, very rare), we fall back to the un-composed map and log at error level — matching theInstrumentation Errorprecedent in the same handler — so a single bad upstream map doesn't kill instrumentation for every other file in the build.Test plan
src/index.test.tscover the four handler code paths: compose-with-input-map, no-input-map passthrough, no-instrumentation early-return, and the error-with-fallback path (which asserts the composition failure is logged at error level withforward: true).src/transform/index.test.tsasserts the magic-string segments resolve back to the function's declaration line, regardless of where the entry-call lands in the generated output.src/sourcemap.integration.test.tsexercises the full rspack pipeline with two shim loaders: