diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index 2c38cac7..a59eeca0 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, 6000); + 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..9c69694e 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,94 @@ 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).toMatchInlineSnapshot( + `";{try{(function(){var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"d4309f93-5358-4ae1-bcf0-3813aa590eb5\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-d4309f93-5358-4ae1-bcf0-3813aa590eb5\\");})();}catch(e){}};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).toMatchInlineSnapshot(` + "\\"use strict\\";;{try{(function(){var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"79a86c07-8ecc-4367-82b0-88cf822f2d41\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-79a86c07-8ecc-4367-82b0-88cf822f2d41\\");})();}catch(e){}}; + console.log(\\"Hello world\\");" + `); + }); + + 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).toMatchInlineSnapshot( + `";{try{(function(){var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"b80112c0-6818-486d-96f0-185c023439b4\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-b80112c0-6818-486d-96f0-185c023439b4\\");})();}catch(e){}};console.log(\\"test\\");"` + ); + }); + + 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 6KB boundary + const codeWithInlineBeyond6KB = + "a".repeat(6100) + + ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};'; + + expect(hooks.renderChunk(codeWithInlineBeyond6KB, { 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",