Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createDebugIdUploadFunction } from "./debug-id-upload";
import { Logger } from "./logger";
import { Options, SentrySDKBuildFlags } from "./types";
import {
containsOnlyImports,
generateGlobalInjectorCode,
generateModuleMetadataInjectorCode,
replaceBooleanFlagsInCode,
Expand Down Expand Up @@ -229,6 +230,9 @@ function isJsFile(fileName: string): boolean {
* HTML entry points create "facade" chunks that should not contain injected code.
* See: https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829
*
* However, in SPA mode, the main bundle also has an HTML facade but contains
* substantial application code. We should NOT skip injection for these bundles.
*
* @param code - The chunk's code content
* @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks
* @returns true if the chunk should be skipped
Expand All @@ -239,10 +243,9 @@ function shouldSkipCodeInjection(code: string, facadeModuleId: string | null | u
return true;
}

// Skip HTML facade chunks
// They only contain import statements and should not have Sentry code injected
// For HTML facade chunks, only skip if they contain only import statements
if (facadeModuleId && stripQueryAndHashFromPath(facadeModuleId).endsWith(".html")) {
return true;
return containsOnlyImports(code);
}

return false;
Expand Down
31 changes: 31 additions & 0 deletions packages/bundler-plugin-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,34 @@ export function serializeIgnoreOptions(ignoreValue: string | string[] | undefine
[] as string[]
);
}

/**
* Checks if a chunk contains only import/export statements and no substantial code.
*
* In Vite MPA (multi-page application) mode, HTML entry points create "facade" chunks
* that only contain import statements to load shared modules. These should not have
* Sentry code injected. However, in SPA mode, the main bundle also has an HTML facade
* but contains substantial application code that SHOULD have debug IDs injected.
*
* @ref https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/829
* @ref https://github.com/getsentry/sentry-javascript-bundler-plugins/issues/839
*/
export function containsOnlyImports(code: string): boolean {
const codeWithoutImports = code
// Remove side effect imports: import '/path'; or import "./path";
// Using explicit negated character classes to avoid polynomial backtracking
.replace(/^\s*import\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "")
// Remove named/default imports: import x from '/path'; import { x } from '/path';
.replace(/^\s*import\b[^'"`\n]*\bfrom\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "")
// Remove re-exports: export * from '/path'; export { x } from '/path';
.replace(/^\s*export\b[^'"`\n]*\bfrom\s+(?:'[^'\n]*'|"[^"\n]*"|`[^`\n]*`)[\s;]*$/gm, "")
// Remove block comments
.replace(/\/\*[\s\S]*?\*\//g, "")
// Remove line comments
.replace(/\/\/.*$/gm, "")
// Remove "use strict" directives
.replace(/["']use strict["']\s*;?/g, "")
.trim();

return codeWithoutImports.length === 0;
}
233 changes: 233 additions & 0 deletions packages/bundler-plugin-core/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
sentryUnpluginFactory,
createRollupDebugIdInjectionHooks,
} from "../src";
import { containsOnlyImports } from "../src/utils";

describe("getDebugIdSnippet", () => {
it("returns the debugId injection snippet for a passed debugId", () => {
Expand All @@ -14,6 +15,137 @@ describe("getDebugIdSnippet", () => {
});
});

describe("containsOnlyImports", () => {
describe("should return true (import-only code)", () => {
it.each([
["empty string", ""],
["whitespace only", " \n\t "],
["side effect import with single quotes", "import './module.js';"],
["side effect import with double quotes", 'import "./module.js";'],
["side effect import with backticks", "import `./module.js`;"],
["side effect import without semicolon", "import './module.js'"],
["default import", "import foo from './module.js';"],
["named import", "import { foo } from './module.js';"],
["named import with alias", "import { foo as bar } from './module.js';"],
["multiple named imports", "import { foo, bar, baz } from './module.js';"],
["namespace import", "import * as utils from './utils.js';"],
["default and named imports", "import React, { useState } from 'react';"],
["re-export all", "export * from './module.js';"],
["re-export named", "export { foo, bar } from './module.js';"],
["re-export with alias", "export { foo as default } from './module.js';"],
])("%s", (_, code) => {
expect(containsOnlyImports(code)).toBe(true);
});

it.each([
[
"multiple imports",
`
import './polyfill.js';
import { helper } from './utils.js';
import config from './config.js';
`,
],
[
"imports with line comments",
`
// This is a comment
import './module.js';
// Another comment
`,
],
[
"imports with block comments",
`
/* Block comment */
import './module.js';
/* Multi
line
comment */
`,
],
["'use strict' with imports", `"use strict";\nimport './module.js';`],
["'use strict' with single quotes", `'use strict';\nimport './module.js';`],
[
"mixed imports, re-exports, and comments",
`
"use strict";
// Entry point facade
import './polyfills.js';
import { init } from './app.js';
/* Re-export for external use */
export * from './types.js';
export { config } from './config.js';
`,
],
])("%s", (_, code) => {
expect(containsOnlyImports(code)).toBe(true);
});
});

describe("should return false (contains substantial code)", () => {
it.each([
["variable declaration", "const x = 1;"],
["let declaration", "let y = 2;"],
["var declaration", "var z = 3;"],
["function declaration", "function foo() {}"],
["arrow function", "const fn = () => {};"],
["class declaration", "class MyClass {}"],
["function call", "console.log('hello');"],
["IIFE", "(function() {})();"],
["expression statement", "1 + 1;"],
["object literal", "({ foo: 'bar' });"],
["export declaration (not re-export)", "export const foo = 1;"],
["export default expression", "export default {};"],
["export function", "export function foo() {}"],
["minified bundle code", `import{a as e}from"./chunk.js";var t=function(){return e()};t();`],
])("%s", (_, code) => {
expect(containsOnlyImports(code)).toBe(false);
});

// Multi-line code snippets
it.each([
[
"import followed by code",
`
import { init } from './app.js';
init();
`,
],
[
"import with variable declaration",
`
import './module.js';
const config = { debug: true };
`,
],
[
"import with function declaration",
`
import { helper } from './utils.js';
function main() {
helper();
}
`,
],
[
"real-world SPA bundle snippet",
`
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(router);
app.mount('#app');
`,
],
])("%s", (_, code) => {
expect(containsOnlyImports(code)).toBe(false);
});
});
});

describe("createRollupDebugIdInjectionHooks", () => {
const hooks = createRollupDebugIdInjectionHooks();

Expand Down Expand Up @@ -99,6 +231,107 @@ describe("createRollupDebugIdInjectionHooks", () => {
hooks.renderChunk(codeWithCommentBeyond500B, { fileName: "bundle.js" })
).not.toBeNull();
});

describe("HTML facade chunks (MPA vs SPA)", () => {
// Issue #829: MPA facades should be skipped
// Regression fix: SPA main bundles with HTML facades should NOT be skipped

it.each([
["empty", ""],
["only side-effect imports", `import './shared-module.js';`],
["only named imports", `import { foo, bar } from './shared-module.js';`],
["only re-exports", `export * from './shared-module.js';`],
[
"multiple imports and comments",
`// This is a facade module
import './moduleA.js';
import { x } from './moduleB.js';
/* block comment */
export * from './moduleC.js';`,
],
["'use strict' and imports only", `"use strict";\nimport './shared-module.js';`],
["query string in facadeModuleId", `import './shared.js';`, "?query=param"],
["hash in facadeModuleId", `import './shared.js';`, "#hash"],
])("should SKIP HTML facade chunks: %s", (_, code, suffix = "") => {
const result = hooks.renderChunk(code, {
fileName: "page1.js",
facadeModuleId: `/path/to/page1.html${suffix}`,
});
expect(result).toBeNull();
});

it("should inject into HTML facade with function declarations", () => {
const result = hooks.renderChunk(`function main() { console.log("hello"); }`, {
fileName: "index.js",
facadeModuleId: "/path/to/index.html",
});
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]=\\"c4c89e04-3658-4874-b25b-07e638185091\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-c4c89e04-3658-4874-b25b-07e638185091\\");})();}catch(e){}};function main() { console.log(\\"hello\\"); }"`
);
});

it("should inject into HTML facade with variable declarations", () => {
const result = hooks.renderChunk(`const x = 42;`, {
fileName: "index.js",
facadeModuleId: "/path/to/index.html",
});
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]=\\"43e69766-1963-49f2-a291-ff8de60cc652\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-43e69766-1963-49f2-a291-ff8de60cc652\\");})();}catch(e){}};const x = 42;"`
);
});

it("should inject into HTML facade with substantial code (SPA main bundle)", () => {
const code = `import { initApp } from './app.js';

const config = { debug: true };

function bootstrap() {
initApp(config);
}

bootstrap();`;
const result = hooks.renderChunk(code, {
fileName: "index.js",
facadeModuleId: "/path/to/index.html",
});
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]=\\"d0c4524b-496e-45a4-9852-7558d043ba3c\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-d0c4524b-496e-45a4-9852-7558d043ba3c\\");})();}catch(e){}};import { initApp } from './app.js';

const config = { debug: true };

function bootstrap() {
initApp(config);
}

bootstrap();"
`);
});

it("should inject into HTML facade with mixed imports and code", () => {
const result = hooks.renderChunk(
`import './polyfills.js';\nimport { init } from './app.js';\n\ninit();`,
{ fileName: "index.js", facadeModuleId: "/path/to/index.html" }
);
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]=\\"28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175\\");})();}catch(e){}};import './polyfills.js';
import { init } from './app.js';

init();"
`);
});

it("should inject into regular JS chunks (no HTML facade)", () => {
const result = hooks.renderChunk(`console.log("Hello");`, { 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]=\\"79f18a7f-ca16-4168-9797-906c82058367\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-79f18a7f-ca16-4168-9797-906c82058367\\");})();}catch(e){}};console.log(\\"Hello\\");"`
);
});
});
});
});

Expand Down
Loading