From 83a82fb4c98bafe374f0ab68740872860ff65cf4 Mon Sep 17 00:00:00 2001 From: Hugo Silva Date: Fri, 14 Aug 2026 13:52:39 +0200 Subject: [PATCH] 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"')); + }); });