Skip to content

Commit 83a82fb

Browse files
committed
fix(injection): keep rollup debug IDs in upload prefix
1 parent 3eb123e commit 83a82fb

4 files changed

Lines changed: 109 additions & 41 deletions

File tree

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

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
// This product includes software developed at Datadog (https://www.datadoghq.com/).
33
// Copyright 2019-Present Datadog, Inc.
44

5-
import { outputFileSync, rmSync } from '@dd/core/helpers/fs';
5+
import { datadogRollupPlugin } from '@datadog/rollup-plugin';
6+
import { outputFileSync, readFile, rmSync } from '@dd/core/helpers/fs';
7+
import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks';
68
import os from 'os';
79
import path from 'path';
10+
import { rollup, type Plugin } from 'rollup';
811

9-
import { extractDebugId } from './debugId';
12+
import { DEBUG_ID_SEARCH_PREFIX_BYTES, extractDebugId } from './debugId';
1013

1114
describe('extractDebugId', () => {
1215
const debugId = '93fd4850-7b77-4f2e-9aa2-ba013e1a5027';
@@ -51,4 +54,48 @@ describe('extractDebugId', () => {
5154

5255
await expect(extractDebugId(filePath)).resolves.toBeUndefined();
5356
});
57+
58+
test('Should keep a Rollup debug ID in the search prefix after later chunk transforms', async () => {
59+
const inputPath = path.join(tempDir, 'input.js');
60+
const outputDir = path.join(tempDir, 'dist');
61+
const outputPath = path.join(outputDir, 'main.js');
62+
outputFileSync(inputPath, 'console.log("hello");');
63+
64+
const datadogPlugin = datadogRollupPlugin({
65+
...defaultPluginOptions,
66+
enableGit: false,
67+
logLevel: 'none',
68+
rum: {
69+
sourceCodeContext: {
70+
debugId: true,
71+
service: 'test-service',
72+
version: '1.0.0',
73+
},
74+
},
75+
});
76+
const lateChunkTransform: Plugin = {
77+
name: 'late-chunk-transform',
78+
renderChunk(code) {
79+
const padding = `/*${'x'.repeat(DEBUG_ID_SEARCH_PREFIX_BYTES)}*/`;
80+
return `${padding}\n${code}`;
81+
},
82+
};
83+
const bundle = await rollup({
84+
input: inputPath,
85+
plugins: [datadogPlugin, lateChunkTransform],
86+
});
87+
88+
await bundle.write({
89+
dir: outputDir,
90+
entryFileNames: 'main.js',
91+
format: 'es',
92+
});
93+
await bundle.close();
94+
95+
const content = await readFile(outputPath);
96+
expect(content.indexOf('ddDebugId')).toBeLessThan(DEBUG_ID_SEARCH_PREFIX_BYTES);
97+
await expect(extractDebugId(outputPath)).resolves.toMatch(
98+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
99+
);
100+
});
54101
});

packages/plugins/injection/src/rollup.ts

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,41 +22,46 @@ export const getRollupPlugin = (
2222
contentsToInject: ContentsToInject,
2323
): PluginOptions['rollup'] => {
2424
return {
25-
renderChunk(code, chunk: RenderedChunk) {
26-
const { base, ext } = path.parse(chunk.fileName);
27-
if (!isFileSupported(ext)) {
28-
warnUnsupportedFile(log, ext, base);
29-
return null;
30-
}
25+
renderChunk: {
26+
// Keep BEFORE and AFTER injections in their requested positions even when another
27+
// plugin, such as Terser, transforms or reorders the chunk.
28+
order: 'post',
29+
handler(code, chunk: RenderedChunk) {
30+
const { base, ext } = path.parse(chunk.fileName);
31+
if (!isFileSupported(ext)) {
32+
warnUnsupportedFile(log, ext, base);
33+
return null;
34+
}
3135

32-
const banner = getContentToInject(contentsToInject, InjectPosition.BEFORE, {
33-
sourceOrHash: code,
34-
fileName: chunk.fileName,
35-
isEntry: chunk.isEntry,
36-
});
37-
const footer = getContentToInject(contentsToInject, InjectPosition.AFTER, {
38-
sourceOrHash: code,
39-
fileName: chunk.fileName,
40-
isEntry: chunk.isEntry,
41-
});
36+
const banner = getContentToInject(contentsToInject, InjectPosition.BEFORE, {
37+
sourceOrHash: code,
38+
fileName: chunk.fileName,
39+
isEntry: chunk.isEntry,
40+
});
41+
const footer = getContentToInject(contentsToInject, InjectPosition.AFTER, {
42+
sourceOrHash: code,
43+
fileName: chunk.fileName,
44+
isEntry: chunk.isEntry,
45+
});
4246

43-
if (!banner && !footer) {
44-
return null;
45-
}
47+
if (!banner && !footer) {
48+
return null;
49+
}
4650

47-
const s = new MagicString(code);
51+
const s = new MagicString(code);
4852

49-
if (banner) {
50-
s.prepend(`${banner}\n`);
51-
}
52-
if (footer) {
53-
s.append(`\n${footer}`);
54-
}
53+
if (banner) {
54+
s.prepend(`${banner}\n`);
55+
}
56+
if (footer) {
57+
s.append(`\n${footer}`);
58+
}
5559

56-
return {
57-
code: s.toString(),
58-
map: s.generateMap({ file: chunk.fileName, hires: 'boundary' }),
59-
};
60+
return {
61+
code: s.toString(),
62+
map: s.generateMap({ file: chunk.fileName, hires: 'boundary' }),
63+
};
64+
},
6065
},
6166
async resolveId(source, importer, options) {
6267
if (isInjectionFile(source)) {

packages/plugins/rum/src/getSourceCodeContextSnippet.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,22 @@ export const getSourceCodeContextSnippet = (
3939
contextOptions: SourceCodeContextOptions,
4040
chunk?: ChunkInfo,
4141
): SourceCodeContextSnippet => {
42+
let debugId: string | undefined;
43+
if (contextOptions.debugId) {
44+
// Compute deterministic debug IDs whenever possible to prevent the backend from storing
45+
// duplicate source maps for identical builds. The `dd` prefix in `ddDebugId` allows
46+
// upload tools to locate the value and send it as sourcemap metadata.
47+
debugId = chunk ? stringToUUID(chunk.sourceOrHash) : randomUUID();
48+
}
49+
4250
const context: SourceCodeContext = {
51+
// Keep the debug ID first so upload tools can find it with a bounded prefix read.
52+
ddDebugId: debugId,
4353
service: contextOptions.service,
4454
version: contextOptions.version,
4555
};
4656

47-
if (contextOptions.debugId) {
48-
// Compute deterministic debug IDs whenever possible preventing the backend from storing duplicate source maps for identical build
49-
//
50-
// The `dd` prefix in `ddDebugId` allows upload tools (for example, datadog-ci) to reliably locate the
51-
// debug ID with a regex and send it as upload metadata alongside the source map.
52-
context.ddDebugId = chunk ? stringToUUID(chunk.sourceOrHash) : randomUUID();
53-
}
54-
5557
const code = `(function(c,n){try{if(typeof window==='undefined')return;var w=window,m=w[n]=w[n]||{},s=new Error().stack;s&&(m[s]=c)}catch(e){}})(${JSON.stringify(context)},${JSON.stringify(DEFAULT_SOURCE_CODE_CONTEXT_VARIABLE)});`;
5658

57-
return { code, debugId: context.ddDebugId };
59+
return { code, debugId };
5860
};

packages/plugins/rum/src/index.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,18 @@ describe('RUM Plugin', () => {
5454
const value = run({ sourceCodeContext: { debugId: true } })[0] as () => string;
5555
expect(value()).toMatch(/(?=.*DD_SOURCE_CODE_CONTEXT)(?=.*"ddDebugId":"[0-9a-f-]+")/);
5656
});
57+
58+
test('Should serialize the debug ID before source code context metadata', () => {
59+
const value = run({
60+
sourceCodeContext: {
61+
debugId: true,
62+
service: 'checkout',
63+
version: '1.2.3',
64+
},
65+
})[0] as () => string;
66+
const code = value();
67+
68+
expect(code.indexOf('"ddDebugId"')).toBeLessThan(code.indexOf('"service"'));
69+
expect(code.indexOf('"ddDebugId"')).toBeLessThan(code.indexOf('"version"'));
70+
});
5771
});

0 commit comments

Comments
 (0)