Skip to content

Commit ad9f55e

Browse files
authored
Merge branch 'main' into timfish/test/ensure-debug-ids-match
2 parents d12543c + 5f3448e commit ad9f55e

File tree

7 files changed

+323
-7
lines changed

7 files changed

+323
-7
lines changed

.github/workflows/changelog-preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Changelog Preview
22
on:
3-
pull_request:
3+
pull_request_target:
44
types:
55
- opened
66
- synchronize

packages/bundler-plugin-core/src/build-plugin-manager.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
arrayify,
2424
getProjects,
2525
getTurborepoEnvPassthroughWarning,
26+
serializeIgnoreOptions,
2627
stripQueryAndHashFromPath,
2728
} from "./utils";
2829
import { glob } from "glob";
@@ -554,7 +555,12 @@ export function createSentryBuildPluginManager(
554555
try {
555556
const cliInstance = createCliInstance(options);
556557
await cliInstance.execute(
557-
["sourcemaps", "inject", ...buildArtifactPaths],
558+
[
559+
"sourcemaps",
560+
"inject",
561+
...serializeIgnoreOptions(options.sourcemaps?.ignore),
562+
...buildArtifactPaths,
563+
],
558564
options.debug ? "rejectOnError" : false
559565
);
560566
} catch (e) {

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { createDebugIdUploadFunction } from "./debug-id-upload";
1212
import { Logger } from "./logger";
1313
import { Options, SentrySDKBuildFlags } from "./types";
1414
import {
15+
containsOnlyImports,
1516
generateGlobalInjectorCode,
1617
generateModuleMetadataInjectorCode,
1718
replaceBooleanFlagsInCode,
@@ -229,6 +230,9 @@ function isJsFile(fileName: string): boolean {
229230
* HTML entry points create "facade" chunks that should not contain injected code.
230231
* See: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829
231232
*
233+
* However, in SPA mode, the main bundle also has an HTML facade but contains
234+
* substantial application code. We should NOT skip injection for these bundles.
235+
*
232236
* @param code - The chunk's code content
233237
* @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks
234238
* @returns true if the chunk should be skipped
@@ -239,10 +243,9 @@ function shouldSkipCodeInjection(code: string, facadeModuleId: string | null | u
239243
return true;
240244
}
241245

242-
// Skip HTML facade chunks
243-
// They only contain import statements and should not have Sentry code injected
246+
// For HTML facade chunks, only skip if they contain only import statements
244247
if (facadeModuleId && stripQueryAndHashFromPath(facadeModuleId).endsWith(".html")) {
245-
return true;
248+
return containsOnlyImports(code);
246249
}
247250

248251
return false;

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,54 @@ export function getProjects(project: string | string[] | undefined): string[] |
408408

409409
return undefined;
410410
}
411+
412+
/**
413+
* Inlined functionality from @sentry/cli helper code to add `--ignore` options.
414+
*
415+
* Temporary workaround until we expose a function for injecting debug IDs. Currently, we directly call `execute` with CLI args to inject them.
416+
*/
417+
export function serializeIgnoreOptions(ignoreValue: string | string[] | undefined): string[] {
418+
const DEFAULT_IGNORE = ["node_modules"];
419+
420+
const ignoreOptions: string[] = Array.isArray(ignoreValue)
421+
? ignoreValue
422+
: typeof ignoreValue === "string"
423+
? [ignoreValue]
424+
: DEFAULT_IGNORE;
425+
426+
return ignoreOptions.reduce(
427+
(acc, value) => acc.concat(["--ignore", String(value)]),
428+
[] as string[]
429+
);
430+
}
431+
432+
/**
433+
* Checks if a chunk contains only import/export statements and no substantial code.
434+
*
435+
* In Vite MPA (multi-page application) mode, HTML entry points create "facade" chunks
436+
* that only contain import statements to load shared modules. These should not have
437+
* Sentry code injected. However, in SPA mode, the main bundle also has an HTML facade
438+
* but contains substantial application code that SHOULD have debug IDs injected.
439+
*
440+
* @ref https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829
441+
* @ref https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/839
442+
*/
443+
export function containsOnlyImports(code: string): boolean {
444+
const codeWithoutImports = code
445+
// Remove side effect imports: import '/path'; or import "./path";
446+
// Using explicit negated character classes to avoid polynomial backtracking
447+
.replace(/^\s*import\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "")
448+
// Remove named/default imports: import x from '/path'; import { x } from '/path';
449+
.replace(/^\s*import\b[^'"`\n]*\bfrom\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "")
450+
// Remove re-exports: export * from '/path'; export { x } from '/path';
451+
.replace(/^\s*export\b[^'"`\n]*\bfrom\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "")
452+
// Remove block comments
453+
.replace(/\/\*[\s\S]*?\*\//g, "")
454+
// Remove line comments
455+
.replace(/\/\/.*$/gm, "")
456+
// Remove "use strict" directives
457+
.replace(/["']use strict["']\s*;?/g, "")
458+
.trim();
459+
460+
return codeWithoutImports.length === 0;
461+
}

packages/bundler-plugin-core/test/build-plugin-manager.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ describe("createSentryBuildPluginManager", () => {
434434
await buildPluginManager.injectDebugIds(buildArtifactPaths);
435435

436436
expect(mockCliExecute).toHaveBeenCalledWith(
437-
["sourcemaps", "inject", "/path/to/1", "/path/to/2"],
437+
["sourcemaps", "inject", "--ignore", "node_modules", "/path/to/1", "/path/to/2"],
438438
false
439439
);
440440
});
@@ -459,7 +459,7 @@ describe("createSentryBuildPluginManager", () => {
459459
await buildPluginManager.injectDebugIds(buildArtifactPaths);
460460

461461
expect(mockCliExecute).toHaveBeenCalledWith(
462-
["sourcemaps", "inject", "/path/to/bundle"],
462+
["sourcemaps", "inject", "--ignore", "node_modules", "/path/to/bundle"],
463463
"rejectOnError"
464464
);
465465
});

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

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
sentryUnpluginFactory,
55
createRollupDebugIdInjectionHooks,
66
} from "../src";
7+
import { containsOnlyImports } from "../src/utils";
78

89
describe("getDebugIdSnippet", () => {
910
it("returns the debugId injection snippet for a passed debugId", () => {
@@ -14,6 +15,137 @@ describe("getDebugIdSnippet", () => {
1415
});
1516
});
1617

18+
describe("containsOnlyImports", () => {
19+
describe("should return true (import-only code)", () => {
20+
it.each([
21+
["empty string", ""],
22+
["whitespace only", " \n\t "],
23+
["side effect import with single quotes", "import './module.js';"],
24+
["side effect import with double quotes", 'import "./module.js";'],
25+
["side effect import with backticks", "import `./module.js`;"],
26+
["side effect import without semicolon", "import './module.js'"],
27+
["default import", "import foo from './module.js';"],
28+
["named import", "import { foo } from './module.js';"],
29+
["named import with alias", "import { foo as bar } from './module.js';"],
30+
["multiple named imports", "import { foo, bar, baz } from './module.js';"],
31+
["namespace import", "import * as utils from './utils.js';"],
32+
["default and named imports", "import React, { useState } from 'react';"],
33+
["re-export all", "export * from './module.js';"],
34+
["re-export named", "export { foo, bar } from './module.js';"],
35+
["re-export with alias", "export { foo as default } from './module.js';"],
36+
])("%s", (_, code) => {
37+
expect(containsOnlyImports(code)).toBe(true);
38+
});
39+
40+
it.each([
41+
[
42+
"multiple imports",
43+
`
44+
import './polyfill.js';
45+
import { helper } from './utils.js';
46+
import config from './config.js';
47+
`,
48+
],
49+
[
50+
"imports with line comments",
51+
`
52+
// This is a comment
53+
import './module.js';
54+
// Another comment
55+
`,
56+
],
57+
[
58+
"imports with block comments",
59+
`
60+
/* Block comment */
61+
import './module.js';
62+
/* Multi
63+
line
64+
comment */
65+
`,
66+
],
67+
["'use strict' with imports", `"use strict";\nimport './module.js';`],
68+
["'use strict' with single quotes", `'use strict';\nimport './module.js';`],
69+
[
70+
"mixed imports, re-exports, and comments",
71+
`
72+
"use strict";
73+
// Entry point facade
74+
import './polyfills.js';
75+
import { init } from './app.js';
76+
/* Re-export for external use */
77+
export * from './types.js';
78+
export { config } from './config.js';
79+
`,
80+
],
81+
])("%s", (_, code) => {
82+
expect(containsOnlyImports(code)).toBe(true);
83+
});
84+
});
85+
86+
describe("should return false (contains substantial code)", () => {
87+
it.each([
88+
["variable declaration", "const x = 1;"],
89+
["let declaration", "let y = 2;"],
90+
["var declaration", "var z = 3;"],
91+
["function declaration", "function foo() {}"],
92+
["arrow function", "const fn = () => {};"],
93+
["class declaration", "class MyClass {}"],
94+
["function call", "console.log('hello');"],
95+
["IIFE", "(function() {})();"],
96+
["expression statement", "1 + 1;"],
97+
["object literal", "({ foo: 'bar' });"],
98+
["export declaration (not re-export)", "export const foo = 1;"],
99+
["export default expression", "export default {};"],
100+
["export function", "export function foo() {}"],
101+
["minified bundle code", `import{a as e}from"./chunk.js";var t=function(){return e()};t();`],
102+
])("%s", (_, code) => {
103+
expect(containsOnlyImports(code)).toBe(false);
104+
});
105+
106+
// Multi-line code snippets
107+
it.each([
108+
[
109+
"import followed by code",
110+
`
111+
import { init } from './app.js';
112+
init();
113+
`,
114+
],
115+
[
116+
"import with variable declaration",
117+
`
118+
import './module.js';
119+
const config = { debug: true };
120+
`,
121+
],
122+
[
123+
"import with function declaration",
124+
`
125+
import { helper } from './utils.js';
126+
function main() {
127+
helper();
128+
}
129+
`,
130+
],
131+
[
132+
"real-world SPA bundle snippet",
133+
`
134+
import { createApp } from 'vue';
135+
import App from './App.vue';
136+
import router from './router';
137+
138+
const app = createApp(App);
139+
app.use(router);
140+
app.mount('#app');
141+
`,
142+
],
143+
])("%s", (_, code) => {
144+
expect(containsOnlyImports(code)).toBe(false);
145+
});
146+
});
147+
});
148+
17149
describe("createRollupDebugIdInjectionHooks", () => {
18150
const hooks = createRollupDebugIdInjectionHooks();
19151

@@ -99,6 +231,107 @@ describe("createRollupDebugIdInjectionHooks", () => {
99231
hooks.renderChunk(codeWithCommentBeyond500B, { fileName: "bundle.js" })
100232
).not.toBeNull();
101233
});
234+
235+
describe("HTML facade chunks (MPA vs SPA)", () => {
236+
// Issue #829: MPA facades should be skipped
237+
// Regression fix: SPA main bundles with HTML facades should NOT be skipped
238+
239+
it.each([
240+
["empty", ""],
241+
["only side-effect imports", `import './shared-module.js';`],
242+
["only named imports", `import { foo, bar } from './shared-module.js';`],
243+
["only re-exports", `export * from './shared-module.js';`],
244+
[
245+
"multiple imports and comments",
246+
`// This is a facade module
247+
import './moduleA.js';
248+
import { x } from './moduleB.js';
249+
/* block comment */
250+
export * from './moduleC.js';`,
251+
],
252+
["'use strict' and imports only", `"use strict";\nimport './shared-module.js';`],
253+
["query string in facadeModuleId", `import './shared.js';`, "?query=param"],
254+
["hash in facadeModuleId", `import './shared.js';`, "#hash"],
255+
])("should SKIP HTML facade chunks: %s", (_, code, suffix = "") => {
256+
const result = hooks.renderChunk(code, {
257+
fileName: "page1.js",
258+
facadeModuleId: `/path/to/page1.html${suffix}`,
259+
});
260+
expect(result).toBeNull();
261+
});
262+
263+
it("should inject into HTML facade with function declarations", () => {
264+
const result = hooks.renderChunk(`function main() { console.log("hello"); }`, {
265+
fileName: "index.js",
266+
facadeModuleId: "/path/to/index.html",
267+
});
268+
expect(result).not.toBeNull();
269+
expect(result?.code).toMatchInlineSnapshot(
270+
`";{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]=\\"c4c89e04-3658-4874-b25b-07e638185091\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-c4c89e04-3658-4874-b25b-07e638185091\\");})();}catch(e){}};function main() { console.log(\\"hello\\"); }"`
271+
);
272+
});
273+
274+
it("should inject into HTML facade with variable declarations", () => {
275+
const result = hooks.renderChunk(`const x = 42;`, {
276+
fileName: "index.js",
277+
facadeModuleId: "/path/to/index.html",
278+
});
279+
expect(result).not.toBeNull();
280+
expect(result?.code).toMatchInlineSnapshot(
281+
`";{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]=\\"43e69766-1963-49f2-a291-ff8de60cc652\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-43e69766-1963-49f2-a291-ff8de60cc652\\");})();}catch(e){}};const x = 42;"`
282+
);
283+
});
284+
285+
it("should inject into HTML facade with substantial code (SPA main bundle)", () => {
286+
const code = `import { initApp } from './app.js';
287+
288+
const config = { debug: true };
289+
290+
function bootstrap() {
291+
initApp(config);
292+
}
293+
294+
bootstrap();`;
295+
const result = hooks.renderChunk(code, {
296+
fileName: "index.js",
297+
facadeModuleId: "/path/to/index.html",
298+
});
299+
expect(result).not.toBeNull();
300+
expect(result?.code).toMatchInlineSnapshot(`
301+
";{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]=\\"d0c4524b-496e-45a4-9852-7558d043ba3c\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-d0c4524b-496e-45a4-9852-7558d043ba3c\\");})();}catch(e){}};import { initApp } from './app.js';
302+
303+
const config = { debug: true };
304+
305+
function bootstrap() {
306+
initApp(config);
307+
}
308+
309+
bootstrap();"
310+
`);
311+
});
312+
313+
it("should inject into HTML facade with mixed imports and code", () => {
314+
const result = hooks.renderChunk(
315+
`import './polyfills.js';\nimport { init } from './app.js';\n\ninit();`,
316+
{ fileName: "index.js", facadeModuleId: "/path/to/index.html" }
317+
);
318+
expect(result).not.toBeNull();
319+
expect(result?.code).toMatchInlineSnapshot(`
320+
";{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]=\\"28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175\\");})();}catch(e){}};import './polyfills.js';
321+
import { init } from './app.js';
322+
323+
init();"
324+
`);
325+
});
326+
327+
it("should inject into regular JS chunks (no HTML facade)", () => {
328+
const result = hooks.renderChunk(`console.log("Hello");`, { fileName: "bundle.js" });
329+
expect(result).not.toBeNull();
330+
expect(result?.code).toMatchInlineSnapshot(
331+
`";{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]=\\"79f18a7f-ca16-4168-9797-906c82058367\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-79f18a7f-ca16-4168-9797-906c82058367\\");})();}catch(e){}};console.log(\\"Hello\\");"`
332+
);
333+
});
334+
});
102335
});
103336
});
104337

0 commit comments

Comments
 (0)