From 2e34ff64c4e531a1e06aa2dddb7c654fc71f9be0 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 4 May 2026 18:32:29 +0200 Subject: [PATCH] fix(live-debugger): emit correct sourcemaps for instrumented functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...pping-npm-0.3.31-1ae81d75ac-da0283270e.zip | Bin 0 -> 53443 bytes LICENSES-3rdparty.csv | 2 +- packages/plugins/live-debugger/package.json | 2 + .../plugins/live-debugger/src/index.test.ts | 113 +++++++++ packages/plugins/live-debugger/src/index.ts | 40 +++- .../src/sourcemap.integration.test.ts | 225 ++++++++++++++++++ .../live-debugger/src/transform/index.test.ts | 36 +++ .../live-debugger/src/transform/index.ts | 69 +++--- .../published/esbuild-plugin/package.json | 1 + packages/published/rollup-plugin/package.json | 1 + packages/published/rspack-plugin/package.json | 1 + packages/published/vite-plugin/package.json | 1 + .../published/webpack-plugin/package.json | 1 + yarn.lock | 19 +- 14 files changed, 481 insertions(+), 30 deletions(-) create mode 100644 .yarn/cache/@jridgewell-trace-mapping-npm-0.3.31-1ae81d75ac-da0283270e.zip create mode 100644 packages/plugins/live-debugger/src/sourcemap.integration.test.ts diff --git a/.yarn/cache/@jridgewell-trace-mapping-npm-0.3.31-1ae81d75ac-da0283270e.zip b/.yarn/cache/@jridgewell-trace-mapping-npm-0.3.31-1ae81d75ac-da0283270e.zip new file mode 100644 index 0000000000000000000000000000000000000000..d61ababcdd3b0aef72d98bd2eab6040444ae1531 GIT binary patch literal 53443 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 JPHVou`X4ZAQX~KX literal 0 HcmV?d00001 diff --git a/LICENSES-3rdparty.csv b/LICENSES-3rdparty.csv index 4c0ef827d..bf8d6bdf0 100644 --- a/LICENSES-3rdparty.csv +++ b/LICENSES-3rdparty.csv @@ -165,7 +165,7 @@ Component,Origin,Licence,Copyright @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,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) +@jridgewell/trace-mapping,npm,MIT,Justin Ridgewell (https://github.com/jridgewell/sourcemaps/tree/main/packages/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) @module-federation/error-codes,npm,MIT,zhanghang (https://www.npmjs.com/package/@module-federation/error-codes) diff --git a/packages/plugins/live-debugger/package.json b/packages/plugins/live-debugger/package.json index 89344efb0..b1946aebe 100644 --- a/packages/plugins/live-debugger/package.json +++ b/packages/plugins/live-debugger/package.json @@ -24,12 +24,14 @@ }, "dependencies": { "@dd/core": "workspace:*", + "@jridgewell/remapping": "2.3.5", "chalk": "2.3.1" }, "devDependencies": { "@babel/parser": "7.24.5", "@babel/traverse": "7.24.5", "@babel/types": "7.24.5", + "@jridgewell/trace-mapping": "0.3.31", "magic-string": "0.30.21", "typescript": "5.4.3" }, diff --git a/packages/plugins/live-debugger/src/index.test.ts b/packages/plugins/live-debugger/src/index.test.ts index 331427cc4..6c42934db 100644 --- a/packages/plugins/live-debugger/src/index.test.ts +++ b/packages/plugins/live-debugger/src/index.test.ts @@ -212,6 +212,119 @@ describe('getLiveDebuggerPlugin', () => { }); }); + describe('source-map composition', () => { + const LINES_SHIFTED = 4; + + const buildShiftedInputMap = (sourcePath: string, source: string): string => + JSON.stringify({ + version: 3, + sources: [sourcePath], + sourcesContent: [source], + names: [], + mappings: + ';'.repeat(LINES_SHIFTED) + + source + .split('\n') + .map((_, idx) => (idx === 0 ? 'AAAA' : 'AACA')) + .join(';'), + }); + + const makeBuildContext = ( + inputSourceMap?: string | null, + ): UnpluginBuildContext & UnpluginContext => ({ + ...mockBuildContext, + getNativeBuildContext: () => ({ + framework: 'rspack', + compiler: {} as never, + compilation: {} as never, + inputSourceMap, + }), + }); + + const callHandler = ( + ctx: UnpluginBuildContext & UnpluginContext, + code: string, + id: string, + ) => { + const pluginContext = getContextMock({ + buildRoot: '/', + getLogger: jest.fn(() => mockLog), + }); + const plugin = getLiveDebuggerPlugin( + makeOptions({ include: [], exclude: [] }), + pluginContext, + ); + const { handler } = getTransformHook(plugin); + const result = handler.call(ctx, code, id); + if (typeof result !== 'object' || result === null || !('code' in result)) { + throw new Error('Unexpected handler result'); + } + return result; + }; + + it('composes its delta map with the previous loader so positions resolve to original-source lines', async () => { + const original = 'function getDebuggerServicesStatus() { return 0; }'; + const id = '/src/use-debugger-services.hook.ts'; + const postLoader = `// banner\n// banner\n// banner\n// banner\n${original}`; + const inputMap = buildShiftedInputMap(id, original); + + const ctx = makeBuildContext(inputMap); + const result = callHandler(ctx, postLoader, id); + + expect(result.map).toBeDefined(); + + const lines = result.code.split('\n'); + const entryLineIndex = lines.findIndex((line) => line.includes('$dd_entry($dd_p')); + expect(entryLineIndex).toBeGreaterThan(-1); + const entryColumn = lines[entryLineIndex].indexOf('$dd_entry'); + + const { originalPositionFor, TraceMap } = await import('@jridgewell/trace-mapping'); + const traceMap = new TraceMap( + typeof result.map === 'string' ? result.map : JSON.parse(String(result.map)), + ); + const original_pos = originalPositionFor(traceMap, { + line: entryLineIndex + 1, + column: entryColumn, + }); + + expect(original_pos.line).toBe(1); + expect(original_pos.source).toBe(id); + }); + + it('returns the magic-string map verbatim when the previous loader did not provide one', async () => { + const id = '/src/utils.ts'; + const code = 'function f() { return 1; }'; + + // No inputSourceMap, no getNativeBuildContext at all. + const result = callHandler(mockBuildContext, code, id); + expect(result.map).toBeDefined(); + + const map = JSON.parse(String(result.map)); + expect(map.sources).toContain(id); + }); + + it('returns no map when the file has no instrumentable functions', () => { + const result = callHandler(mockBuildContext, 'const x = 42;', '/src/utils.ts'); + expect(result.map).toBeUndefined(); + }); + + it('falls back to the un-composed map and logs an error when composition throws', () => { + const id = '/src/utils.ts'; + const code = 'function f() { return 1; }'; + + const ctx = makeBuildContext('not a valid sourcemap, this should throw'); + const result = callHandler(ctx, code, id); + + expect(result.map).toBeDefined(); + expect(() => JSON.parse(String(result.map))).not.toThrow(); + + expect(mockLog.error).toHaveBeenCalledWith( + expect.stringContaining('Failed to compose source map'), + expect.objectContaining({ forward: true }), + ); + }); + }); + describe('error handling', () => { it('should return original code when transformCode throws', () => { jest.isolateModules(() => { diff --git a/packages/plugins/live-debugger/src/index.ts b/packages/plugins/live-debugger/src/index.ts index c88e5b9da..fd93e5f6b 100644 --- a/packages/plugins/live-debugger/src/index.ts +++ b/packages/plugins/live-debugger/src/index.ts @@ -4,6 +4,10 @@ import type { GetPlugins, GlobalContext, PluginOptions } from '@dd/core/types'; import { InjectPosition } from '@dd/core/types'; +import remapping from '@jridgewell/remapping'; +import type { SourceMapInput } from '@jridgewell/remapping'; +import type { SourceMap } from 'magic-string'; +import type { SourceMapCompact, UnpluginBuildContext } from 'unplugin'; import { CONFIG_KEY, PLUGIN_NAME } from './constants'; import { getRuntimeBootstrap } from './runtime-bootstrap'; @@ -103,9 +107,15 @@ export const getLiveDebuggerPlugin = ( transformedFileCount++; + const inputMap = getInputSourceMap(this); + const composedMap = + result.map && inputMap + ? composeWithInputMap(result.map, inputMap, id, log) + : result.map; + return { code: result.code, - map: result.map, + map: composedMap, }; } catch (e) { log.error(`Instrumentation Error in ${id}: ${e}`, { forward: true }); @@ -158,3 +168,31 @@ export const getPlugins: GetPlugins = ({ options, context }) => { return [getLiveDebuggerPlugin(validatedOptions, context)]; }; + +/** + * Return the source map produced by the previous loader, if any. + */ +function getInputSourceMap(ctx: UnpluginBuildContext): SourceMapInput | undefined { + const native = ctx.getNativeBuildContext?.(); + return (native as { inputSourceMap?: SourceMapInput })?.inputSourceMap; +} + +/** + * Compose a local source map with the previous loader's source map. The result maps instrumented + * output directly back to original source coordinates. + */ +function composeWithInputMap( + instrumentMap: SourceMap, + inputMap: SourceMapInput, + id: string, + log: ReturnType, +): SourceMapCompact | SourceMap { + try { + return remapping(instrumentMap as unknown as SourceMapInput, (_file, ctx) => + ctx.depth === 1 ? inputMap : null, + ) as unknown as SourceMapCompact; + } catch (e) { + log.error(`Failed to compose source map for ${id}: ${e}`, { forward: true }); + return instrumentMap; + } +} diff --git a/packages/plugins/live-debugger/src/sourcemap.integration.test.ts b/packages/plugins/live-debugger/src/sourcemap.integration.test.ts new file mode 100644 index 000000000..111f72638 --- /dev/null +++ b/packages/plugins/live-debugger/src/sourcemap.integration.test.ts @@ -0,0 +1,225 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +import { datadogRspackPlugin } from '@datadog/rspack-plugin'; +import { outputFileSync, readFileSync, rm } from '@dd/core/helpers/fs'; +import { getUniqueId } from '@dd/core/helpers/strings'; +import { prepareWorkingDir } from '@dd/tests/_jest/helpers/env'; +import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; +import { buildWithRspack } from '@dd/tools/bundlers'; +import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'; +import path from 'path'; + +// Tiny rspack/webpack loader that publishes an identity source map for the +// modules it touches. It is required because unplugin's rspack/webpack +// transform loader silently drops `res.map` when the *input* source map is +// null. +const IDENTITY_SHIM_LOADER_SOURCE = ` +module.exports = function (source) { + this.cacheable && this.cacheable(); + const callback = this.async(); + const lines = source.split('\\n'); + const mappings = lines + .map((_, idx) => (idx === 0 ? 'AAAA' : 'AACA')) + .join(';'); + callback(null, source, { + version: 3, + sources: [this.resourcePath], + sourcesContent: [source], + names: [], + mappings: mappings, + }); +}; +`; + +const BANNER_LINES = 4; +const BANNER_SHIFT_SHIM_LOADER_SOURCE = ` +const BANNER = ${JSON.stringify('// banner\n'.repeat(BANNER_LINES))}; +module.exports = function (source) { + this.cacheable && this.cacheable(); + const callback = this.async(); + const lines = source.split('\\n'); + // Identity-by-line mappings for the original source, prefixed with + // ${BANNER_LINES} empty entries for the banner lines (which have no + // original source position). + const sourceMappings = lines + .map((_, idx) => (idx === 0 ? 'AAAA' : 'AACA')) + .join(';'); + const mappings = ';'.repeat(${BANNER_LINES}) + sourceMappings; + callback(null, BANNER + source, { + version: 3, + sources: [this.resourcePath], + sourcesContent: [source], + names: [], + mappings: mappings, + }); +}; +`; + +describe('Live Debugger sourcemaps', () => { + const seed = `${Math.abs(jest.getSeed())}.${getUniqueId()}`; + let workingDir: string; + + beforeAll(async () => { + workingDir = await prepareWorkingDir(seed); + }); + + afterAll(async () => { + if (!process.env.NO_CLEANUP) { + await rm(workingDir); + } + }); + + const ENTRY_SOURCE = [ + 'function helper() {', + " return 'helper';", + '}', + '', + 'function getDebuggerServicesStatus(isLoadingCritical) {', + " return isLoadingCritical ? 'loading' : 'completed';", + '}', + '', + 'getDebuggerServicesStatus(false);', + ].join('\n'); + + /** + * Build the entry through rspack with the live-debugger plugin enabled and + * the given upstream loader, then return the bundle, its source map, and + * a few key positions used by every assertion below. Centralizing this + * keeps the actual test bodies focused on the source-map invariants they + * are checking and avoids re-stating the bundler config in every case. + */ + const buildAndLocate = async (upstreamShimLoaderSource: string, outputSubdir: string) => { + const entry = path.resolve(workingDir, 'live-debugger-entry.js'); + const outDir = path.resolve(workingDir, outputSubdir); + const shimLoader = path.resolve(workingDir, `${outputSubdir}-loader.cjs`); + + outputFileSync(shimLoader, upstreamShimLoaderSource); + outputFileSync(entry, ENTRY_SOURCE); + + const { errors } = await buildWithRspack({ + context: workingDir, + mode: 'none', + devtool: 'source-map', + entry: { main: entry }, + output: { + path: outDir, + filename: '[name].js', + }, + resolve: { + extensions: ['.js'], + }, + module: { + // Stand-in for the source-map-producing loader (swc/babel/ts/...) + // that would normally precede the Datadog plugin in a real build. + rules: [{ test: /\.js$/, use: [{ loader: shimLoader }] }], + }, + plugins: [ + datadogRspackPlugin({ + ...defaultPluginOptions, + liveDebugger: { + enable: true, + }, + metadata: { + version: 'test-version', + }, + }), + ], + }); + + expect(errors).toEqual([]); + + const bundle = readFileSync(path.resolve(outDir, 'main.js')); + const sourceMap = JSON.parse(readFileSync(path.resolve(outDir, 'main.js.map'))); + const lines = bundle.split('\n'); + + const probeDeclLine = lines.findIndex((line) => + line.includes('live-debugger-entry.js;getDebuggerServicesStatus'), + ); + const entryCallLine = lines.findIndex( + (line, idx) => idx > probeDeclLine && line.includes('$dd_entry($dd_p'), + ); + const functionDeclLine = lines.findIndex((line) => + line.includes('function getDebuggerServicesStatus(isLoadingCritical)'), + ); + + expect(probeDeclLine).toBeGreaterThan(-1); + expect(entryCallLine).toBeGreaterThan(-1); + expect(functionDeclLine).toBeGreaterThan(-1); + + return { + traceMap: new TraceMap(sourceMap), + probeDeclLine, + probeDeclColumn: lines[probeDeclLine].indexOf( + 'live-debugger-entry.js;getDebuggerServicesStatus', + ), + entryCallLine, + entryCallColumn: lines[entryCallLine].indexOf('$dd_entry'), + functionDeclLine, + functionDeclColumn: lines[functionDeclLine].indexOf('function '), + }; + }; + + it('produces line- and column-accurate sourcemaps after rspack', async () => { + const located = await buildAndLocate( + IDENTITY_SHIM_LOADER_SOURCE, + 'dist-live-debugger-rspack-identity', + ); + + expect( + originalPositionFor(located.traceMap, { + line: located.probeDeclLine + 1, + column: located.probeDeclColumn, + }), + ).toEqual(expect.objectContaining({ line: 5 })); + + expect( + originalPositionFor(located.traceMap, { + line: located.entryCallLine + 1, + column: located.entryCallColumn, + }), + ).toEqual(expect.objectContaining({ line: 5 })); + + expect( + originalPositionFor(located.traceMap, { + line: located.functionDeclLine + 1, + column: located.functionDeclColumn, + }), + ).toEqual(expect.objectContaining({ line: 5, column: 0 })); + }); + + it('composes its source map with the previous loader so injected positions are reported in original-source coordinates', async () => { + const located = await buildAndLocate( + BANNER_SHIFT_SHIM_LOADER_SOURCE, + 'dist-live-debugger-rspack-shifted', + ); + + const probeDeclResolved = originalPositionFor(located.traceMap, { + line: located.probeDeclLine + 1, + column: located.probeDeclColumn, + }); + const entryCallResolved = originalPositionFor(located.traceMap, { + line: located.entryCallLine + 1, + column: located.entryCallColumn, + }); + const functionDeclResolved = originalPositionFor(located.traceMap, { + line: located.functionDeclLine + 1, + column: located.functionDeclColumn, + }); + + // Every position we care about must point at line 5 of the original + // source, NOT line 5 + BANNER_LINES of the post-loader buffer. + expect(probeDeclResolved).toEqual(expect.objectContaining({ line: 5 })); + expect(entryCallResolved).toEqual(expect.objectContaining({ line: 5 })); + expect(functionDeclResolved).toEqual(expect.objectContaining({ line: 5, column: 0 })); + + // Defensive: explicitly assert that none of the resolved positions + // are pointing at the banner area. Without composition the injected + // probe positions would land on lines 5..(5 + BANNER_LINES) of what + // the bundler thinks is the original source. + for (const resolved of [probeDeclResolved, entryCallResolved, functionDeclResolved]) { + expect(resolved.line).not.toBeGreaterThan(5); + } + }); +}); diff --git a/packages/plugins/live-debugger/src/transform/index.test.ts b/packages/plugins/live-debugger/src/transform/index.test.ts index d2fa18b3d..0e0d757fb 100644 --- a/packages/plugins/live-debugger/src/transform/index.test.ts +++ b/packages/plugins/live-debugger/src/transform/index.test.ts @@ -2,6 +2,8 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'; + import { transformCode, validateSyntax } from './index'; const BASE_OPTIONS = { @@ -344,6 +346,40 @@ describe('transformCode', () => { expect(result.map).toBeDefined(); expect(result.map?.sources).toContain('/src/utils.ts'); }); + + it('should map the injected entry call back to the original function line', () => { + // Even though the preamble lands on its own generated lines (not on + // the function declaration line) the source map must resolve every + // injected line back to the function it wraps. Magic-string + // populates each injected line with a segment via `s.update()`; + // before that, purely-injected lines had no segments at all and + // resolved to `null`. + const code = [ + "import { isDefined } from '@lib/type-guards';", + '', + 'function getDebuggerServicesStatus(isLoadingCritical) {', + " return isLoadingCritical ? 'loading' : 'completed';", + '}', + ].join('\n'); + const result = transformCode({ ...BASE_OPTIONS, code }); + + expect(result.map).toBeDefined(); + const lines = result.code.split('\n'); + const entryLineIndex = lines.findIndex((line) => line.includes('$dd_entry($dd_p0')); + expect(entryLineIndex).toBeGreaterThan(-1); + + const traceMap = new TraceMap(JSON.parse(result.map!.toString())); + const entryColumn = lines[entryLineIndex].indexOf('$dd_entry'); + const original = originalPositionFor(traceMap, { + line: entryLineIndex + 1, + column: entryColumn, + }); + + // Original function declaration is on line 3 (1-indexed) of the + // source. Mapping any column on the entry-call line back through + // the source map must land on that line, regardless of the column. + expect(original.line).toBe(3); + }); }); describe('functionTypes filtering', () => { diff --git a/packages/plugins/live-debugger/src/transform/index.ts b/packages/plugins/live-debugger/src/transform/index.ts index 82dd11596..c62c7c45e 100644 --- a/packages/plugins/live-debugger/src/transform/index.ts +++ b/packages/plugins/live-debugger/src/transform/index.ts @@ -356,11 +356,7 @@ export function transformCode(options: TransformOptions): TransformResult { return { code: s.toString(), - // Known limitation: hires: false gives line-level source map granularity only. - // Column-level accuracy (hires: true or 'boundary') would be needed for - // minified code or precise debugger positioning, but is not required for - // RUM Error Tracking stack traces which reference lines. - map: s.generateMap({ source: filePath, hires: false }), + map: s.generateMap({ source: filePath, hires: true }), failedCount, instrumentedCount, skippedByCommentCount, @@ -371,10 +367,7 @@ export function transformCode(options: TransformOptions): TransformResult { } /** - * Inject instrumentation for a single function using MagicString. - * - * Uses appendLeft exclusively (no overwrite) to avoid conflicts - * with nested function instrumentation in the same source range. + * Inject instrumentation for a single function. */ function injectInstrumentation(s: MagicStringType, code: string, target: FunctionTarget): void { const { @@ -462,12 +455,20 @@ function injectInstrumentation(s: MagicStringType, code: string, target: Functio '}', ].join('\n'); - s.appendLeft(bodyStart, prefix); - s.appendLeft(bodyEnd, suffix); + // Anchor each injected line to the original expression's location by + // editing the boundary chars of the body. Two updates avoid losing + // per-character mappings of the original expression in between. + // For a single-char body (e.g. `() => 1`) the two ranges would + // collide, so we fall back to one update covering the whole body. + if (bodyEnd - bodyStart >= 2) { + s.update(bodyStart, bodyStart + 1, prefix + code[bodyStart]); + s.update(bodyEnd - 1, bodyEnd, code[bodyEnd - 1] + suffix); + } else { + s.update(bodyStart, bodyEnd, prefix + code.slice(bodyStart, bodyEnd) + suffix); + } } else { // Block body function const preamble = [ - '', probeDecl, entryHelperDecl, 'try {', @@ -478,21 +479,10 @@ function injectInstrumentation(s: MagicStringType, code: string, target: Functio .filter(Boolean) .join('\n'); - const postambleParts = ['']; - if (target.needsTrailingReturn) { - postambleParts.push( - `if (${probeVarName}) $dd_return(${probeVarName}, undefined, this${returnArgsAndLocals});`, - ); - } - postambleParts.push(`} ${catchBlock}`, ''); - const postamble = postambleParts.join('\n'); - - const preambleInsertPos = directivesEnd ?? bodyStart + 1; - s.appendLeft(preambleInsertPos, directivesEnd != null ? `\n${preamble}` : preamble); - - // Wrap return statements BEFORE inserting the postamble so that when + // Wrap return statements BEFORE the boundary updates so that when // a semicolon-free final return shares its argEnd position with - // bodyEnd - 1, appendLeft stacks the return suffix before the postamble. + // bodyEnd - 1, the return suffix is appended to the preceding chunk + // (its outro) and ends up before the postamble in the generated code. for (const ret of returns) { if (ret.argStart != null && ret.argEnd != null) { // return EXPR; → return ($dd_rvN = EXPR, probe ? $dd_return(...) : $dd_rvN); @@ -510,7 +500,32 @@ function injectInstrumentation(s: MagicStringType, code: string, target: Functio } } - s.appendLeft(bodyEnd - 1, postamble); + // Wrap the boundary character (last directive char or the body's + // opening `{`) with ``. Editing through `update()` + // makes magic-string emit a source-map segment for every line of + // the new content, all anchored at that boundary char's location — + // so injected preamble lines map to the function's own line instead + // of to nothing. + // + // For directives, an additional leading newline keeps the preamble + // on its own line (the directive's trailing `;` already ends a + // statement, but its terminator stays on the directive's line). + if (directivesEnd != null) { + const anchor = directivesEnd - 1; + s.update(anchor, directivesEnd, `${code[anchor]}\n${preamble}`); + } else { + s.update(bodyStart, bodyStart + 1, `${code[bodyStart]}${preamble}`); + } + + // Build the postamble. The optional trailing-return helper is + // included here (rather than as a separate appendLeft) so that the + // single boundary update for `}` covers it; this keeps the trailing + // helper's source-map segment anchored to the closing brace too. + const trailingReturn = target.needsTrailingReturn + ? `if (${probeVarName}) $dd_return(${probeVarName}, undefined, this${returnArgsAndLocals});\n` + : ''; + const postamble = `\n${trailingReturn}} ${catchBlock}\n`; + s.update(bodyEnd - 1, bodyEnd, `${postamble}${code[bodyEnd - 1]}`); } } diff --git a/packages/published/esbuild-plugin/package.json b/packages/published/esbuild-plugin/package.json index f2fd247db..f6482225f 100644 --- a/packages/published/esbuild-plugin/package.json +++ b/packages/published/esbuild-plugin/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "@datadog/js-instrumentation-wasm": "1.0.8", + "@jridgewell/remapping": "2.3.5", "async-retry": "1.3.3", "chalk": "2.3.1", "glob": "11.1.0", diff --git a/packages/published/rollup-plugin/package.json b/packages/published/rollup-plugin/package.json index 384b08767..21e3a6783 100644 --- a/packages/published/rollup-plugin/package.json +++ b/packages/published/rollup-plugin/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "@datadog/js-instrumentation-wasm": "1.0.8", + "@jridgewell/remapping": "2.3.5", "async-retry": "1.3.3", "chalk": "2.3.1", "glob": "11.1.0", diff --git a/packages/published/rspack-plugin/package.json b/packages/published/rspack-plugin/package.json index 861025ca5..c89146164 100644 --- a/packages/published/rspack-plugin/package.json +++ b/packages/published/rspack-plugin/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "@datadog/js-instrumentation-wasm": "1.0.8", + "@jridgewell/remapping": "2.3.5", "async-retry": "1.3.3", "chalk": "2.3.1", "glob": "11.1.0", diff --git a/packages/published/vite-plugin/package.json b/packages/published/vite-plugin/package.json index 5c67e58ee..b772a5e96 100644 --- a/packages/published/vite-plugin/package.json +++ b/packages/published/vite-plugin/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "@datadog/js-instrumentation-wasm": "1.0.8", + "@jridgewell/remapping": "2.3.5", "async-retry": "1.3.3", "chalk": "2.3.1", "glob": "11.1.0", diff --git a/packages/published/webpack-plugin/package.json b/packages/published/webpack-plugin/package.json index 6fed55ed7..3df8130e4 100644 --- a/packages/published/webpack-plugin/package.json +++ b/packages/published/webpack-plugin/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "@datadog/js-instrumentation-wasm": "1.0.8", + "@jridgewell/remapping": "2.3.5", "async-retry": "1.3.3", "chalk": "2.3.1", "glob": "11.1.0", diff --git a/yarn.lock b/yarn.lock index 329b10d2e..09d6da4b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1688,6 +1688,7 @@ __metadata: "@datadog/js-instrumentation-wasm": "npm:1.0.8" "@dd/factory": "workspace:*" "@dd/tools": "workspace:*" + "@jridgewell/remapping": "npm:2.3.5" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" "@rollup/plugin-esm-shim": "npm:0.1.7" @@ -1746,6 +1747,7 @@ __metadata: "@datadog/js-instrumentation-wasm": "npm:1.0.8" "@dd/factory": "workspace:*" "@dd/tools": "workspace:*" + "@jridgewell/remapping": "npm:2.3.5" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" "@rollup/plugin-esm-shim": "npm:0.1.7" @@ -1797,6 +1799,7 @@ __metadata: "@datadog/js-instrumentation-wasm": "npm:1.0.8" "@dd/factory": "workspace:*" "@dd/tools": "workspace:*" + "@jridgewell/remapping": "npm:2.3.5" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" "@rollup/plugin-esm-shim": "npm:0.1.7" @@ -1848,6 +1851,7 @@ __metadata: "@datadog/js-instrumentation-wasm": "npm:1.0.8" "@dd/factory": "workspace:*" "@dd/tools": "workspace:*" + "@jridgewell/remapping": "npm:2.3.5" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" "@rollup/plugin-esm-shim": "npm:0.1.7" @@ -1899,6 +1903,7 @@ __metadata: "@datadog/js-instrumentation-wasm": "npm:1.0.8" "@dd/factory": "workspace:*" "@dd/tools": "workspace:*" + "@jridgewell/remapping": "npm:2.3.5" "@rollup/plugin-babel": "npm:6.0.4" "@rollup/plugin-commonjs": "npm:28.0.1" "@rollup/plugin-esm-shim": "npm:0.1.7" @@ -2099,6 +2104,8 @@ __metadata: "@babel/traverse": "npm:7.24.5" "@babel/types": "npm:7.24.5" "@dd/core": "workspace:*" + "@jridgewell/remapping": "npm:2.3.5" + "@jridgewell/trace-mapping": "npm:0.3.31" chalk: "npm:2.3.1" magic-string: "npm:0.30.21" typescript: "npm:5.4.3" @@ -3155,7 +3162,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/remapping@npm:^2.3.5": +"@jridgewell/remapping@npm:2.3.5, @jridgewell/remapping@npm:^2.3.5": version: 2.3.5 resolution: "@jridgewell/remapping@npm:2.3.5" dependencies: @@ -3196,6 +3203,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:0.3.31": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10/da0283270e691bdb5543806077548532791608e52386cfbbf3b9e8fb00457859d1bd01d512851161c886eb3a2f3ce6fd9bcf25db8edf3bddedd275bd4a88d606 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9"