Skip to content

Commit b2563d3

Browse files
committed
fix(error-tracking): derive debug_id from file content, not a filename Map
The RUM plugin embeds debug_id inside each chunk's own JS content as a ddDebugId literal, but a side-channel Map keyed by filename was used to pass it to error-tracking's uploader. Bundlers like webpack/rspack can rename a chunk's file after the RUM plugin's injection (e.g. realContentHash), so the Map's key goes stale and the lookup misses for most chunks. Read the debug_id straight out of the minified file's own content at upload time instead. This removes the filename coordination between the two plugins entirely, so it stays correct regardless of any bundler renaming step.
1 parent 3043e57 commit b2563d3

11 files changed

Lines changed: 71 additions & 45 deletions

File tree

packages/core/src/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,6 @@ export type GlobalData = {
345345
};
346346

347347
export type GlobalStores = {
348-
// Keyed by chunk relative path, filled in by the RUM plugin, read by error-tracking.
349-
debugIds: Map<string, string>;
350348
errors: string[];
351349
logs: Log[];
352350
metrics: Set<Metric>;

packages/factory/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ export const buildPluginFactory = ({
117117
};
118118

119119
const stores: GlobalStores = {
120-
debugIds: new Map(),
121120
errors: [],
122121
logs: [],
123122
metrics: new Set(),

packages/plugins/error-tracking/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type types = {
1818
ErrorTrackingOptions: ErrorTrackingOptions;
1919
};
2020

21-
export const getPlugins: GetPlugins = ({ options, context, stores }) => {
21+
export const getPlugins: GetPlugins = ({ options, context }) => {
2222
const log = context.getLogger(PLUGIN_NAME);
2323
const timeOptions = log.time('validate options');
2424
const validatedOptions = validateOptions(options, log);
@@ -41,7 +41,6 @@ export const getPlugins: GetPlugins = ({ options, context, stores }) => {
4141
{
4242
apiKey: context.auth.apiKey,
4343
bundlerName: context.bundler.name,
44-
debugIds: stores.debugIds,
4544
git: gitInfo,
4645
addMetric: context.addMetric,
4746
outDir: context.bundler.outDir,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
2+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
3+
// Copyright 2019-Present Datadog, Inc.
4+
5+
import { extractDebugId } from './debugId';
6+
7+
describe('extractDebugId', () => {
8+
const debugId = '93fd4850-7b77-4f2e-9aa2-ba013e1a5027';
9+
10+
test('Should extract the debug ID when the key is quoted (unminified JSON.stringify output)', () => {
11+
const content = `!function(){}({"service":"app","version":"1.0.0","ddDebugId":"${debugId}"},"DD_SOURCE_CODE_CONTEXT");`;
12+
expect(extractDebugId(content)).toBe(debugId);
13+
});
14+
15+
test('Should extract the debug ID when the key is unquoted (minifiers strip quotes from valid identifier keys)', () => {
16+
const content = `!function(){}({service:"app",version:"1.0.0",ddDebugId:"${debugId}"},"DD_SOURCE_CODE_CONTEXT");`;
17+
expect(extractDebugId(content)).toBe(debugId);
18+
});
19+
20+
test('Should return undefined when there is no debug ID in the content', () => {
21+
const content = `!function(){}({service:"app",version:"1.0.0"},"DD_SOURCE_CODE_CONTEXT");`;
22+
expect(extractDebugId(content)).toBeUndefined();
23+
});
24+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
2+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
3+
// Copyright 2019-Present Datadog, Inc.
4+
5+
// Matches the `ddDebugId:"<uuid>"` literal the RUM plugin injects into each chunk's own content
6+
// (see packages/plugins/rum/src/getSourceCodeContextSnippet.ts). The key is quoted in source
7+
// (`JSON.stringify(context)`) but minifiers like terser strip quotes from object keys that are
8+
// valid identifiers, so the built output can have either `"ddDebugId":"..."` or `ddDebugId:"..."`.
9+
// Reading it back out of the file we're about to upload means we never have to trust a filename
10+
// as a coordination key between the RUM plugin and this one, so it stays correct across any
11+
// bundler renaming step.
12+
const DEBUG_ID_RX = /"?ddDebugId"?:"([0-9a-fA-F-]{36})"/;
13+
14+
export const extractDebugId = (fileContent: string): string | undefined => {
15+
return DEBUG_ID_RX.exec(fileContent)?.[1];
16+
};

packages/plugins/error-tracking/src/sourcemaps/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export const uploadSourcemaps = async (
3535
addMetric: context.addMetric,
3636
apiKey: context.apiKey,
3737
bundlerName: context.bundlerName,
38-
debugIds: context.debugIds,
3938
git: context.git,
4039
outDir: context.outDir,
4140
sendMetrics: context.sendMetrics,

packages/plugins/error-tracking/src/sourcemaps/sender.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jest.mock('@dd/core/helpers/fs', () => {
3030
...original,
3131
checkFile: jest.fn(),
3232
getFile: jest.fn(),
33+
readFile: jest.fn(),
3334
};
3435
});
3536

@@ -54,7 +55,6 @@ const uploadContextMock = {
5455
};
5556
const senderContextMock = {
5657
...uploadContextMock,
57-
debugIds: new Map(),
5858
git: contextMock.git,
5959
};
6060

@@ -167,34 +167,34 @@ describe('Error Tracking Plugin Sourcemaps', () => {
167167
expect(doRequestMock).not.toHaveBeenCalled();
168168
});
169169

170-
test('Should resolve the debug ID for a chunk nested in a subdirectory of the output dir', async () => {
171-
// Add some fixtures.
170+
test('Should resolve the debug ID straight from the minified file content, regardless of its filename', async () => {
171+
// The minified file's name here has nothing to do with the debug ID lookup —
172+
// it's extracted from the file's own content, so it survives any bundler
173+
// renaming step (e.g. webpack/rspack's realContentHash) that happens after
174+
// the RUM plugin injects it.
175+
const debugId = '12345678-1234-4123-8123-123456789012';
172176
addFixtureFiles({
173-
'/path/to/minified.min.js': 'Some JS File with some content.',
177+
'/path/to/minified.min.js':
178+
// Minifiers strip quotes from object keys that are valid identifiers, so
179+
// the real on-disk shape has an unquoted key, not `"ddDebugId":"..."`.
180+
`Some JS File with some content.(function(c,n){...})({ddDebugId:"${debugId}"},"DD_SOURCE_CODE_CONTEXT");`,
174181
'/path/to/sourcemap.js.map': '{"version":3,"sources":["/path/to/minified.min.js"]}',
175182
});
176183

177184
const getPayloadSpy = jest.spyOn(payloadModule, 'getPayload');
178185

179-
// `relativePath` mirrors what error-tracking's own file decomposition
180-
// produces for a chunk nested under the output dir (e.g. rspack/webpack
181-
// module federation output). The stored key must match this format,
182-
// not a bare basename, or the debug ID lookup silently misses.
183186
const sourcemap = getSourcemapMock({ relativePath: 'path/to/minified.min.js' });
184187

185188
await sendSourcemaps(
186189
[sourcemap],
187190
getSourcemapsConfiguration(),
188-
{
189-
...senderContextMock,
190-
debugIds: new Map([['path/to/minified.min.js', 'debug-id-1']]),
191-
},
191+
senderContextMock,
192192
mockLogger,
193193
);
194194

195195
expect(getPayloadSpy).toHaveBeenCalledTimes(1);
196196
const debugIdArg = getPayloadSpy.mock.calls[0][4];
197-
expect(debugIdArg).toBe('debug-id-1');
197+
expect(debugIdArg).toBe(debugId);
198198

199199
getPayloadSpy.mockRestore();
200200
});

packages/plugins/error-tracking/src/sourcemaps/sender.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// Copyright 2019-Present Datadog, Inc.
44

55
import { getDDEnvValue } from '@dd/core/helpers/env';
6-
import { getFile } from '@dd/core/helpers/fs';
6+
import { getFile, readFile } from '@dd/core/helpers/fs';
77
import {
88
createRequestData,
99
doRequest,
@@ -15,10 +15,10 @@ import { formatDuration, prettyObject } from '@dd/core/helpers/strings';
1515
import type { Logger, Metric, RepositoryData } from '@dd/core/types';
1616
import chalk from 'chalk';
1717
import PQueue from 'p-queue';
18-
import path from 'path';
1918

2019
import type { SourcemapsOptionsWithDefaults, Sourcemap } from '../types';
2120

21+
import { extractDebugId } from './debugId';
2222
import type { Metadata, MultipartFileValue, Payload } from './payload';
2323
import { getPayload } from './payload';
2424
import {
@@ -78,14 +78,6 @@ export type UploadContext = {
7878
outDir: string;
7979
};
8080

81-
export type DebugIdsContext = {
82-
// Keyed by chunk relative path (forward-slashed), filled in by the RUM plugin.
83-
debugIds: Map<string, string>;
84-
};
85-
86-
// Bundlers report chunk paths with forward slashes regardless of OS.
87-
const toPosixPath = (filePath: string) => filePath.split(path.sep).join('/');
88-
8981
export const upload = async (
9082
payloads: Payload[],
9183
options: SourcemapsOptionsWithDefaults,
@@ -184,10 +176,9 @@ export const upload = async (
184176
return { warnings, errors };
185177
};
186178

187-
export type SourcemapsSenderContext = UploadContext &
188-
DebugIdsContext & {
189-
git?: RepositoryData;
190-
};
179+
export type SourcemapsSenderContext = UploadContext & {
180+
git?: RepositoryData;
181+
};
191182

192183
export const sendSourcemaps = async (
193184
sourcemaps: Sourcemap[],
@@ -210,8 +201,13 @@ export const sendSourcemaps = async (
210201

211202
const payloadsTimer = log.time('Compute payloads');
212203
const payloads = await Promise.all(
213-
sourcemaps.map((sourcemap) => {
214-
const debugId = context.debugIds.get(toPosixPath(sourcemap.relativePath));
204+
sourcemaps.map(async (sourcemap) => {
205+
// Read the debug_id straight from the minified file's own content instead of
206+
// trusting a filename as a coordination key with the RUM plugin — the bundler may
207+
// still rename the file after injection (e.g. webpack/rspack's realContentHash),
208+
// but the content, and the debug_id embedded in it, is unaffected.
209+
const fileContent = await readFile(sourcemap.minifiedFilePath).catch(() => undefined);
210+
const debugId = fileContent ? extractDebugId(fileContent) : undefined;
215211
return getPayload(sourcemap, metadata, prefix, context.git, debugId);
216212
}),
217213
);

packages/plugins/rum/src/index.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export type types = {
2626
RumInitConfiguration: RumInitConfiguration;
2727
};
2828

29-
export const getPlugins: GetPlugins = ({ options, context, stores }) => {
29+
export const getPlugins: GetPlugins = ({ options, context }) => {
3030
const log = context.getLogger(PLUGIN_NAME);
3131
const validatedOptions = validateOptions(options, log);
3232
const plugins: PluginOptions[] = [];
@@ -38,13 +38,10 @@ export const getPlugins: GetPlugins = ({ options, context, stores }) => {
3838
position: InjectPosition.BEFORE,
3939
injectIntoAllChunks: true,
4040
value: (chunk) => {
41-
const { code, debugId } = getSourceCodeContextSnippet(sourceCodeContext, chunk);
42-
if (debugId && chunk) {
43-
// Let the error-tracking plugin pick this up when uploading its sourcemap.
44-
// Keyed by the chunk's relative path, since some bundlers can emit
45-
// sibling chunks sharing the same basename in different subdirectories.
46-
stores.debugIds.set(chunk.fileName, debugId);
47-
}
41+
// The debug_id is embedded directly in the returned code (see
42+
// getSourceCodeContextSnippet.ts); error-tracking reads it back out of the
43+
// built file's content at upload time, so it doesn't need to be tracked here.
44+
const { code } = getSourceCodeContextSnippet(sourceCodeContext, chunk);
4845
return code;
4946
},
5047
});

packages/tests/src/_jest/helpers/mocks.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export const getMockData = (overrides: Partial<GlobalData> = {}): GlobalData =>
8686
});
8787

8888
export const getMockStores = (overrides: Partial<GlobalStores> = {}): GlobalStores => ({
89-
debugIds: new Map(),
9089
logs: [],
9190
errors: [],
9291
warnings: [],

0 commit comments

Comments
 (0)