diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index 2c38cac7..be2d24f9 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -269,6 +269,17 @@ export function createRollupDebugIdInjectionHooks(): { stripQueryAndHashFromPath(chunk.fileName).endsWith(ending) ) ) { + // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI) + const chunkStartSnippet = code.slice(0, 2000); + const chunkEndSnippet = code.slice(-500); + + if ( + chunkStartSnippet.includes("_sentryDebugIdIdentifier") || + chunkEndSnippet.includes("//# debugId=") + ) { + return null; // Debug ID already present, skip injection + } + const debugId = stringToUUID(code); // generate a deterministic debug ID const codeToInject = getDebugIdSnippet(debugId); diff --git a/packages/bundler-plugin-core/test/index.test.ts b/packages/bundler-plugin-core/test/index.test.ts index f6c60aa0..c3d5f5bb 100644 --- a/packages/bundler-plugin-core/test/index.test.ts +++ b/packages/bundler-plugin-core/test/index.test.ts @@ -1,5 +1,9 @@ import { Compiler } from "webpack"; -import { getDebugIdSnippet, sentryUnpluginFactory } from "../src"; +import { + getDebugIdSnippet, + sentryUnpluginFactory, + createRollupDebugIdInjectionHooks, +} from "../src"; describe("getDebugIdSnippet", () => { it("returns the debugId injection snippet for a passed debugId", () => { @@ -10,6 +14,88 @@ describe("getDebugIdSnippet", () => { }); }); +describe("createRollupDebugIdInjectionHooks", () => { + const hooks = createRollupDebugIdInjectionHooks(); + + describe("renderChunk", () => { + it("should inject debug ID into clean JavaScript files", () => { + const code = 'console.log("Hello world");'; + const result = hooks.renderChunk(code, { fileName: "bundle.js" }); + + expect(result).not.toBeNull(); + expect(result?.code).toContain("_sentryDebugIdIdentifier"); + expect(result?.code).toContain('console.log("Hello world");'); + }); + + it("should inject debug ID after 'use strict'", () => { + const code = '"use strict";\nconsole.log("Hello world");'; + const result = hooks.renderChunk(code, { fileName: "bundle.js" }); + + expect(result).not.toBeNull(); + expect(result?.code).toMatch(/^"use strict";.*;{try/); + }); + + it.each([ + ["bundle.js"], + ["bundle.mjs"], + ["bundle.cjs"], + ["bundle.js?foo=bar"], + ["bundle.js#hash"], + ])("should process file '%s': %s", (fileName) => { + const code = 'console.log("test");'; + const result = hooks.renderChunk(code, { fileName }); + + expect(result).not.toBeNull(); + expect(result?.code).toContain("_sentryDebugIdIdentifier"); + }); + + it.each([["index.html"], ["styles.css"]])("should NOT process file '%s': %s", (fileName) => { + const code = 'console.log("test");'; + const result = hooks.renderChunk(code, { fileName }); + + expect(result).toBeNull(); + }); + + it.each([ + [ + "inline format at start", + ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};console.log("test");', + ], + [ + "comment format at end", + 'console.log("test");\n//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n//# sourceMappingURL=bundle.js.map', + ], + [ + "inline format with large file", + '"use strict";\n' + + "// comment\n".repeat(10) + + ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};' + + '\nconsole.log("line");\n'.repeat(100), + ], + ])("should NOT inject when debug ID already exists (%s)", (_description, code) => { + const result = hooks.renderChunk(code, { fileName: "bundle.js" }); + expect(result).toBeNull(); + }); + + it("should only check boundaries for performance (not entire file)", () => { + // Inline format beyond first 2KB boundary + const codeWithInlineBeyond2KB = + "a".repeat(2100) + + ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};'; + + expect(hooks.renderChunk(codeWithInlineBeyond2KB, { fileName: "bundle.js" })).not.toBeNull(); + + // Comment format beyond last 500 bytes boundary + const codeWithCommentBeyond500B = + "//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n" + "a".repeat(600); + + expect( + hooks.renderChunk(codeWithCommentBeyond500B, { fileName: "bundle.js" }) + ).not.toBeNull(); + }); + }); +}); + describe("sentryUnpluginFactory sourcemaps.disable behavior", () => { const mockReleaseInjectionPlugin = jest.fn((_injectionCode: string) => ({ name: "mock-release-injection-plugin",