Skip to content

Commit ce967a4

Browse files
committed
fix(rollup): Prevent double-injection of debug ID
1 parent 05084f2 commit ce967a4

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

packages/bundler-plugin-core/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,14 @@ export function createRollupDebugIdInjectionHooks(): {
269269
stripQueryAndHashFromPath(chunk.fileName).endsWith(ending)
270270
)
271271
) {
272+
// Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI)
273+
const chunkStartSnippet = code.slice(0, 2000);
274+
const chunkEndSnippet = code.slice(-500);
275+
276+
if (chunkStartSnippet.includes("_sentryDebugIdIdentifier") || chunkEndSnippet.includes("//# debugId=")) {
277+
return null; // Debug ID already present, skip injection
278+
}
279+
272280
const debugId = stringToUUID(code); // generate a deterministic debug ID
273281
const codeToInject = getDebugIdSnippet(debugId);
274282

packages/bundler-plugin-core/test/index.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Compiler } from "webpack";
2-
import { getDebugIdSnippet, sentryUnpluginFactory } from "../src";
2+
import { getDebugIdSnippet, sentryUnpluginFactory, createRollupDebugIdInjectionHooks } from "../src";
33

44
describe("getDebugIdSnippet", () => {
55
it("returns the debugId injection snippet for a passed debugId", () => {
@@ -10,6 +10,85 @@ describe("getDebugIdSnippet", () => {
1010
});
1111
});
1212

13+
describe("createRollupDebugIdInjectionHooks", () => {
14+
const hooks = createRollupDebugIdInjectionHooks();
15+
16+
describe("renderChunk", () => {
17+
it("should inject debug ID into clean JavaScript files", () => {
18+
const code = 'console.log("Hello world");';
19+
const result = hooks.renderChunk(code, { fileName: "bundle.js" });
20+
21+
expect(result).not.toBeNull();
22+
expect(result?.code).toContain("_sentryDebugIdIdentifier");
23+
expect(result?.code).toContain('console.log("Hello world");');
24+
});
25+
26+
it("should inject debug ID after 'use strict'", () => {
27+
const code = '"use strict";\nconsole.log("Hello world");';
28+
const result = hooks.renderChunk(code, { fileName: "bundle.js" });
29+
30+
expect(result).not.toBeNull();
31+
expect(result?.code).toMatch(/^"use strict";.*;{try/);
32+
});
33+
34+
it.each([
35+
["bundle.js", true],
36+
["bundle.mjs", true],
37+
["bundle.cjs", true],
38+
["bundle.js?foo=bar", true],
39+
["bundle.js#hash", true],
40+
["index.html", false],
41+
["styles.css", false],
42+
])("should process file '%s': %s", (fileName, shouldProcess) => {
43+
const code = 'console.log("test");';
44+
const result = hooks.renderChunk(code, { fileName });
45+
46+
if (shouldProcess) {
47+
expect(result).not.toBeNull();
48+
expect(result?.code).toContain("_sentryDebugIdIdentifier");
49+
} else {
50+
expect(result).toBeNull();
51+
}
52+
});
53+
54+
it.each([
55+
[
56+
"inline format at start",
57+
';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};console.log("test");',
58+
],
59+
[
60+
"comment format at end",
61+
'console.log("test");\n//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n//# sourceMappingURL=bundle.js.map',
62+
],
63+
[
64+
"inline format with large file",
65+
'"use strict";\n' +
66+
"// comment\n".repeat(10) +
67+
';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};' +
68+
'\nconsole.log("line");\n'.repeat(100),
69+
],
70+
])("should NOT inject when debug ID already exists (%s)", (_description, code) => {
71+
const result = hooks.renderChunk(code, { fileName: "bundle.js" });
72+
expect(result).toBeNull();
73+
});
74+
75+
it("should only check boundaries for performance (not entire file)", () => {
76+
// Inline format beyond first 2KB boundary
77+
const codeWithInlineBeyond2KB =
78+
"a".repeat(2100) +
79+
';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};';
80+
81+
expect(hooks.renderChunk(codeWithInlineBeyond2KB, { fileName: "bundle.js" })).not.toBeNull();
82+
83+
// Comment format beyond last 500 bytes boundary
84+
const codeWithCommentBeyond500B =
85+
"//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n" + "a".repeat(600);
86+
87+
expect(hooks.renderChunk(codeWithCommentBeyond500B, { fileName: "bundle.js" })).not.toBeNull();
88+
});
89+
});
90+
});
91+
1392
describe("sentryUnpluginFactory sourcemaps.disable behavior", () => {
1493
const mockReleaseInjectionPlugin = jest.fn((_injectionCode: string) => ({
1594
name: "mock-release-injection-plugin",

0 commit comments

Comments
 (0)