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/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..312b03b0c 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. + 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, 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..80d034236 100644 --- a/packages/plugins/rum/src/index.test.ts +++ b/packages/plugins/rum/src/index.test.ts @@ -54,4 +54,20 @@ 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(); + const debugIdIndex = code.indexOf('"ddDebugId"'); + + expect(debugIdIndex).toBeGreaterThanOrEqual(0); + expect(debugIdIndex).toBeLessThan(code.indexOf('"service"')); + expect(debugIdIndex).toBeLessThan(code.indexOf('"version"')); + }); });