From 53b65e8dbedd5416c996ba15992163cb2a6d39a3 Mon Sep 17 00:00:00 2001 From: Hugo Silva Date: Fri, 14 Aug 2026 13:52:39 +0200 Subject: [PATCH 1/3] fix(injection): keep rollup debug IDs in upload prefix --- .../src/sourcemaps/debugId.test.ts | 51 ++++++++++++++- packages/plugins/injection/src/rollup.ts | 65 ++++++++++--------- .../rum/src/getSourceCodeContextSnippet.ts | 20 +++--- packages/plugins/rum/src/index.test.ts | 14 ++++ 4 files changed, 109 insertions(+), 41 deletions(-) diff --git a/packages/plugins/error-tracking/src/sourcemaps/debugId.test.ts b/packages/plugins/error-tracking/src/sourcemaps/debugId.test.ts index 7f5c810c4..9b316f692 100644 --- a/packages/plugins/error-tracking/src/sourcemaps/debugId.test.ts +++ b/packages/plugins/error-tracking/src/sourcemaps/debugId.test.ts @@ -2,11 +2,14 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { outputFileSync, rmSync } from '@dd/core/helpers/fs'; +import { datadogRollupPlugin } from '@datadog/rollup-plugin'; +import { outputFileSync, readFile, rmSync } from '@dd/core/helpers/fs'; +import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; import os from 'os'; import path from 'path'; +import { rollup, type Plugin } from 'rollup'; -import { extractDebugId } from './debugId'; +import { DEBUG_ID_SEARCH_PREFIX_BYTES, extractDebugId } from './debugId'; describe('extractDebugId', () => { const debugId = '93fd4850-7b77-4f2e-9aa2-ba013e1a5027'; @@ -51,4 +54,48 @@ describe('extractDebugId', () => { await expect(extractDebugId(filePath)).resolves.toBeUndefined(); }); + + test('Should keep a Rollup debug ID in the search prefix after later chunk transforms', async () => { + const inputPath = path.join(tempDir, 'input.js'); + const outputDir = path.join(tempDir, 'dist'); + const outputPath = path.join(outputDir, 'main.js'); + outputFileSync(inputPath, 'console.log("hello");'); + + const datadogPlugin = datadogRollupPlugin({ + ...defaultPluginOptions, + enableGit: false, + logLevel: 'none', + rum: { + sourceCodeContext: { + debugId: true, + service: 'test-service', + version: '1.0.0', + }, + }, + }); + const lateChunkTransform: Plugin = { + name: 'late-chunk-transform', + renderChunk(code) { + const padding = `/*${'x'.repeat(DEBUG_ID_SEARCH_PREFIX_BYTES)}*/`; + return `${padding}\n${code}`; + }, + }; + const bundle = await rollup({ + input: inputPath, + plugins: [datadogPlugin, lateChunkTransform], + }); + + await bundle.write({ + dir: outputDir, + entryFileNames: 'main.js', + format: 'es', + }); + await bundle.close(); + + const content = await readFile(outputPath); + expect(content.indexOf('ddDebugId')).toBeLessThan(DEBUG_ID_SEARCH_PREFIX_BYTES); + await expect(extractDebugId(outputPath)).resolves.toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); + }); }); diff --git a/packages/plugins/injection/src/rollup.ts b/packages/plugins/injection/src/rollup.ts index d2ed26444..f4715742f 100644 --- a/packages/plugins/injection/src/rollup.ts +++ b/packages/plugins/injection/src/rollup.ts @@ -22,41 +22,46 @@ export const getRollupPlugin = ( contentsToInject: ContentsToInject, ): PluginOptions['rollup'] => { return { - renderChunk(code, chunk: RenderedChunk) { - const { base, ext } = path.parse(chunk.fileName); - if (!isFileSupported(ext)) { - warnUnsupportedFile(log, ext, base); - return null; - } + renderChunk: { + // Keep BEFORE and AFTER injections in their requested positions even when another + // plugin, such as Terser, transforms or reorders the chunk. + order: 'post', + handler(code, chunk: RenderedChunk) { + const { base, ext } = path.parse(chunk.fileName); + if (!isFileSupported(ext)) { + warnUnsupportedFile(log, ext, base); + return null; + } - const banner = getContentToInject(contentsToInject, InjectPosition.BEFORE, { - sourceOrHash: code, - fileName: chunk.fileName, - isEntry: chunk.isEntry, - }); - const footer = getContentToInject(contentsToInject, InjectPosition.AFTER, { - sourceOrHash: code, - fileName: chunk.fileName, - isEntry: chunk.isEntry, - }); + const banner = getContentToInject(contentsToInject, InjectPosition.BEFORE, { + sourceOrHash: code, + fileName: chunk.fileName, + isEntry: chunk.isEntry, + }); + const footer = getContentToInject(contentsToInject, InjectPosition.AFTER, { + sourceOrHash: code, + fileName: chunk.fileName, + isEntry: chunk.isEntry, + }); - if (!banner && !footer) { - return null; - } + if (!banner && !footer) { + return null; + } - const s = new MagicString(code); + const s = new MagicString(code); - if (banner) { - s.prepend(`${banner}\n`); - } - if (footer) { - s.append(`\n${footer}`); - } + if (banner) { + s.prepend(`${banner}\n`); + } + if (footer) { + s.append(`\n${footer}`); + } - return { - code: s.toString(), - map: s.generateMap({ file: chunk.fileName, hires: 'boundary' }), - }; + return { + code: s.toString(), + map: s.generateMap({ file: chunk.fileName, hires: 'boundary' }), + }; + }, }, async resolveId(source, importer, options) { if (isInjectionFile(source)) { diff --git a/packages/plugins/rum/src/getSourceCodeContextSnippet.ts b/packages/plugins/rum/src/getSourceCodeContextSnippet.ts index ee196a85b..ed5fb1d26 100644 --- a/packages/plugins/rum/src/getSourceCodeContextSnippet.ts +++ b/packages/plugins/rum/src/getSourceCodeContextSnippet.ts @@ -39,20 +39,22 @@ export const getSourceCodeContextSnippet = ( contextOptions: SourceCodeContextOptions, chunk?: ChunkInfo, ): SourceCodeContextSnippet => { + let debugId: string | undefined; + if (contextOptions.debugId) { + // Compute deterministic debug IDs whenever possible to prevent the backend from storing + // duplicate source maps for identical builds. The `dd` prefix in `ddDebugId` allows + // upload tools to locate the value and send it as sourcemap metadata. + debugId = chunk ? stringToUUID(chunk.sourceOrHash) : randomUUID(); + } + const context: SourceCodeContext = { + // Keep the debug ID first so upload tools can find it with a bounded prefix read. + ddDebugId: debugId, service: contextOptions.service, version: contextOptions.version, }; - if (contextOptions.debugId) { - // Compute deterministic debug IDs whenever possible preventing the backend from storing duplicate source maps for identical build - // - // The `dd` prefix in `ddDebugId` allows upload tools (for example, datadog-ci) to reliably locate the - // debug ID with a regex and send it as upload metadata alongside the source map. - context.ddDebugId = chunk ? stringToUUID(chunk.sourceOrHash) : randomUUID(); - } - 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)});`; - return { code, debugId: context.ddDebugId }; + return { code, debugId }; }; diff --git a/packages/plugins/rum/src/index.test.ts b/packages/plugins/rum/src/index.test.ts index 2ba67a511..01ead556d 100644 --- a/packages/plugins/rum/src/index.test.ts +++ b/packages/plugins/rum/src/index.test.ts @@ -54,4 +54,18 @@ describe('RUM Plugin', () => { const value = run({ sourceCodeContext: { debugId: true } })[0] as () => string; expect(value()).toMatch(/(?=.*DD_SOURCE_CODE_CONTEXT)(?=.*"ddDebugId":"[0-9a-f-]+")/); }); + + test('Should serialize the debug ID before source code context metadata', () => { + const value = run({ + sourceCodeContext: { + debugId: true, + service: 'checkout', + version: '1.2.3', + }, + })[0] as () => string; + const code = value(); + + expect(code.indexOf('"ddDebugId"')).toBeLessThan(code.indexOf('"service"')); + expect(code.indexOf('"ddDebugId"')).toBeLessThan(code.indexOf('"version"')); + }); }); From 5e14ee4bb74dd2d0ebf8a03844749bd9785c8763 Mon Sep 17 00:00:00 2001 From: Hugo Silva Date: Mon, 24 Aug 2026 16:16:56 +0200 Subject: [PATCH 2/3] test(error-tracking): reuse bundler test helpers --- .../plugins/error-tracking/src/index.test.ts | 42 +++++++++++++++- .../src/sourcemaps/debugId.test.ts | 49 +------------------ 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/packages/plugins/error-tracking/src/index.test.ts b/packages/plugins/error-tracking/src/index.test.ts index d1976c4e9..97a29b164 100644 --- a/packages/plugins/error-tracking/src/index.test.ts +++ b/packages/plugins/error-tracking/src/index.test.ts @@ -2,7 +2,11 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { extractDebugId } from '@dd/error-tracking-plugin/sourcemaps/debugId'; +import { readFile } from '@dd/core/helpers/fs'; +import { + DEBUG_ID_SEARCH_CHUNK_BYTES, + extractDebugId, +} from '@dd/error-tracking-plugin/sourcemaps/debugId'; import { uploadSourcemaps } from '@dd/error-tracking-plugin/sourcemaps/index'; import { getPlugins } from '@dd/error-tracking-plugin'; import { @@ -12,6 +16,7 @@ import { hardProjectEntries, } from '@dd/tests/_jest/helpers/mocks'; import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers'; +import type { Plugin } from 'rollup'; jest.mock('@dd/error-tracking-plugin/sourcemaps/index', () => { return { @@ -125,4 +130,39 @@ describe('Error Tracking Plugin', () => { expect(debugIdsAtUpload.length).toBeGreaterThan(2); expect(debugIdsAtUpload).not.toContain(undefined); }); + + test('Should keep Rollup debug IDs in the search prefix after later chunk transforms.', async () => { + let debugIdOffsetAtUpload: number | undefined; + uploadSourcemapsMock.mockImplementationOnce(async (_options, context) => { + const javascriptOutput = (context.outputs || []).find(({ filepath }) => + filepath.endsWith('.js'), + ); + if (!javascriptOutput) { + return; + } + const content = await readFile(javascriptOutput.filepath); + debugIdOffsetAtUpload = content.indexOf('ddDebugId'); + }); + + const lateChunkTransform: Plugin = { + name: 'late-chunk-transform', + renderChunk(code) { + const padding = `/*${'x'.repeat(DEBUG_ID_SEARCH_CHUNK_BYTES)}*/`; + return `${padding}\n${code}`; + }, + }; + const { errors } = await runBundlers( + { + enableGit: false, + errorTracking: { sourcemaps: getSourcemapsConfiguration() }, + rum: { sourceCodeContext: { debugId: true } }, + }, + { plugins: [lateChunkTransform] }, + ['rollup'], + ); + + expect(errors).toHaveLength(0); + expect(debugIdOffsetAtUpload).toBeGreaterThanOrEqual(0); + expect(debugIdOffsetAtUpload).toBeLessThan(DEBUG_ID_SEARCH_CHUNK_BYTES); + }); }); diff --git a/packages/plugins/error-tracking/src/sourcemaps/debugId.test.ts b/packages/plugins/error-tracking/src/sourcemaps/debugId.test.ts index 14a25914f..73c3b6a62 100644 --- a/packages/plugins/error-tracking/src/sourcemaps/debugId.test.ts +++ b/packages/plugins/error-tracking/src/sourcemaps/debugId.test.ts @@ -2,13 +2,10 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { datadogRollupPlugin } from '@datadog/rollup-plugin'; -import { outputFileSync, readFile, rmSync } from '@dd/core/helpers/fs'; -import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; +import { outputFileSync, rmSync } from '@dd/core/helpers/fs'; import fsp from 'fs/promises'; import os from 'os'; import path from 'path'; -import { rollup, type Plugin } from 'rollup'; import { DEBUG_ID_SEARCH_CHUNK_BYTES, extractDebugId } from './debugId'; @@ -100,48 +97,4 @@ describe('extractDebugId', () => { await expect(extractDebugId(filePath)).resolves.toBeUndefined(); }); - - test('Should keep a Rollup debug ID in the search prefix after later chunk transforms', async () => { - const inputPath = path.join(tempDir, 'input.js'); - const outputDir = path.join(tempDir, 'dist'); - const outputPath = path.join(outputDir, 'main.js'); - outputFileSync(inputPath, 'console.log("hello");'); - - const datadogPlugin = datadogRollupPlugin({ - ...defaultPluginOptions, - enableGit: false, - logLevel: 'none', - rum: { - sourceCodeContext: { - debugId: true, - service: 'test-service', - version: '1.0.0', - }, - }, - }); - const lateChunkTransform: Plugin = { - name: 'late-chunk-transform', - renderChunk(code) { - const padding = `/*${'x'.repeat(DEBUG_ID_SEARCH_CHUNK_BYTES)}*/`; - return `${padding}\n${code}`; - }, - }; - const bundle = await rollup({ - input: inputPath, - plugins: [datadogPlugin, lateChunkTransform], - }); - - await bundle.write({ - dir: outputDir, - entryFileNames: 'main.js', - format: 'es', - }); - await bundle.close(); - - const content = await readFile(outputPath); - expect(content.indexOf('ddDebugId')).toBeLessThan(DEBUG_ID_SEARCH_CHUNK_BYTES); - await expect(extractDebugId(outputPath)).resolves.toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, - ); - }); }); From ba149eb02c7cf38dc3a276169a42056f5f598e32 Mon Sep 17 00:00:00 2001 From: Hugo Silva Date: Tue, 25 Aug 2026 10:09:05 +0200 Subject: [PATCH 3/3] test(rum): tighten debug ID assertions --- packages/plugins/rum/src/getSourceCodeContextSnippet.ts | 4 ++-- packages/plugins/rum/src/index.test.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/plugins/rum/src/getSourceCodeContextSnippet.ts b/packages/plugins/rum/src/getSourceCodeContextSnippet.ts index ed5fb1d26..312b03b0c 100644 --- a/packages/plugins/rum/src/getSourceCodeContextSnippet.ts +++ b/packages/plugins/rum/src/getSourceCodeContextSnippet.ts @@ -42,12 +42,12 @@ export const getSourceCodeContextSnippet = ( let debugId: string | undefined; if (contextOptions.debugId) { // Compute deterministic debug IDs whenever possible to prevent the backend from storing - // duplicate source maps for identical builds. The `dd` prefix in `ddDebugId` allows - // upload tools to locate the value and send it as sourcemap metadata. + // duplicate source maps for identical builds. debugId = chunk ? stringToUUID(chunk.sourceOrHash) : randomUUID(); } const context: SourceCodeContext = { + // The `dd` prefix lets upload tools locate the value and send it as sourcemap metadata. // Keep the debug ID first so upload tools can find it with a bounded prefix read. ddDebugId: debugId, service: contextOptions.service, diff --git a/packages/plugins/rum/src/index.test.ts b/packages/plugins/rum/src/index.test.ts index 01ead556d..80d034236 100644 --- a/packages/plugins/rum/src/index.test.ts +++ b/packages/plugins/rum/src/index.test.ts @@ -64,8 +64,10 @@ describe('RUM Plugin', () => { }, })[0] as () => string; const code = value(); + const debugIdIndex = code.indexOf('"ddDebugId"'); - expect(code.indexOf('"ddDebugId"')).toBeLessThan(code.indexOf('"service"')); - expect(code.indexOf('"ddDebugId"')).toBeLessThan(code.indexOf('"version"')); + expect(debugIdIndex).toBeGreaterThanOrEqual(0); + expect(debugIdIndex).toBeLessThan(code.indexOf('"service"')); + expect(debugIdIndex).toBeLessThan(code.indexOf('"version"')); }); });