Skip to content
Open
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
152 changes: 55 additions & 97 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,20 @@ import {
stripQueryAndHashFromPath,
} from "./utils";

interface SentryUnpluginFactoryOptions {
type InjectionPlugin = (
injectionCode: string,
debugIds: boolean,
logger: Logger
) => UnpluginOptions;
type LegacyPlugins = {
releaseInjectionPlugin: (injectionCode: string) => UnpluginOptions;
componentNameAnnotatePlugin?: (ignoredComponents?: string[]) => UnpluginOptions;
moduleMetadataInjectionPlugin: (injectionCode: string) => UnpluginOptions;
debugIdInjectionPlugin: (logger: Logger) => UnpluginOptions;
};

interface SentryUnpluginFactoryOptions {
injectionPlugin: InjectionPlugin | LegacyPlugins;
componentNameAnnotatePlugin?: (ignoredComponents?: string[]) => UnpluginOptions;
debugIdUploadPlugin: (
upload: (buildArtifacts: string[]) => Promise<void>,
logger: Logger,
Expand All @@ -38,10 +47,8 @@ interface SentryUnpluginFactoryOptions {
* Creates an unplugin instance used to create Sentry plugins for Vite, Rollup, esbuild, and Webpack.
*/
export function sentryUnpluginFactory({
releaseInjectionPlugin,
injectionPlugin,
componentNameAnnotatePlugin,
moduleMetadataInjectionPlugin,
debugIdInjectionPlugin,
debugIdUploadPlugin,
bundleSizeOptimizationsPlugin,
}: SentryUnpluginFactoryOptions): UnpluginInstance<Options | undefined, true> {
Expand Down Expand Up @@ -94,6 +101,8 @@ export function sentryUnpluginFactory({
plugins.push(bundleSizeOptimizationsPlugin(bundleSizeOptimizationReplacementValues));
}

let injectionCode = "";

if (!options.release.inject) {
logger.debug(
"Release injection disabled via `release.inject` option. Will not inject release."
Expand All @@ -103,18 +112,31 @@ export function sentryUnpluginFactory({
"No release name provided. Will not inject release. Please set the `release.name` option to identify your release."
);
} else {
const injectionCode = generateGlobalInjectorCode({
const code = generateGlobalInjectorCode({
release: options.release.name,
injectBuildInformation: options._experiments.injectBuildInformation || false,
});
plugins.push(releaseInjectionPlugin(injectionCode));
if (typeof injectionPlugin !== "function") {
plugins.push(injectionPlugin.releaseInjectionPlugin(code));
} else {
injectionCode += code;
}
}

if (Object.keys(sentryBuildPluginManager.bundleMetadata).length > 0) {
const injectionCode = generateModuleMetadataInjectorCode(
sentryBuildPluginManager.bundleMetadata
);
plugins.push(moduleMetadataInjectionPlugin(injectionCode));
const code = generateModuleMetadataInjectorCode(sentryBuildPluginManager.bundleMetadata);
if (typeof injectionPlugin !== "function") {
plugins.push(injectionPlugin.moduleMetadataInjectionPlugin(code));
} else {
injectionCode += code;
}
}

if (
typeof injectionPlugin === "function" &&
(injectionCode !== "" || options.sourcemaps?.disable !== true)
) {
plugins.push(injectionPlugin(injectionCode, options.sourcemaps?.disable !== true, logger));
}

// Add plugin to create and finalize releases, and also take care of adding commits and legacy sourcemaps
Expand All @@ -132,7 +154,9 @@ export function sentryUnpluginFactory({
});

if (options.sourcemaps?.disable !== true) {
plugins.push(debugIdInjectionPlugin(logger));
if (typeof injectionPlugin !== "function") {
plugins.push(injectionPlugin.debugIdInjectionPlugin(logger));
}

if (options.sourcemaps?.disable !== "disable-upload") {
// This option is only strongly typed for the webpack plugin, where it is used. It has no effect on other plugins
Expand Down Expand Up @@ -251,42 +275,6 @@ function shouldSkipCodeInjection(code: string, facadeModuleId: string | null | u
return false;
}

export function createRollupReleaseInjectionHooks(injectionCode: string): {
renderChunk: RenderChunkHook;
} {
return {
renderChunk(code: string, chunk: { fileName: string; facadeModuleId?: string | null }) {
if (!isJsFile(chunk.fileName)) {
return null; // returning null means not modifying the chunk at all
}

// Skip empty chunks and HTML facade chunks (Vite MPA)
if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) {
return null;
}

const ms = new MagicString(code, { filename: chunk.fileName });

const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0];

if (match) {
// Add injected code after any comments or "use strict" at the beginning of the bundle.
ms.appendLeft(match.length, injectionCode);
} else {
// ms.replace() doesn't work when there is an empty string match (which happens if
// there is neither, a comment, nor a "use strict" at the top of the chunk) so we
// need this special case here.
ms.prepend(injectionCode);
}

return {
code: ms.toString(),
map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }),
};
},
};
}

export function createRollupBundleSizeOptimizationHooks(replacementValues: SentrySDKBuildFlags): {
transform: UnpluginOptions["transform"];
} {
Expand All @@ -297,7 +285,10 @@ export function createRollupBundleSizeOptimizationHooks(replacementValues: Sentr
};
}

export function createRollupDebugIdInjectionHooks(): {
export function createRollupInjectionHooks(
injectionCode: string,
debugIds: boolean
): {
renderChunk: RenderChunkHook;
} {
return {
Expand All @@ -311,22 +302,25 @@ export function createRollupDebugIdInjectionHooks(): {
return null;
}

// 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);
let codeToInject = injectionCode;

if (
chunkStartSnippet.includes("_sentryDebugIdIdentifier") ||
chunkEndSnippet.includes("//# debugId=")
) {
return null; // Debug ID already present, skip injection
}
if (debugIds) {
// 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);

const debugId = stringToUUID(code); // generate a deterministic debug ID
const codeToInject = getDebugIdSnippet(debugId);
if (
!(
chunkStartSnippet.includes("_sentryDebugIdIdentifier") ||
chunkEndSnippet.includes("//# debugId=")
)
) {
const debugId = stringToUUID(code); // generate a deterministic debug ID
codeToInject += getDebugIdSnippet(debugId);
}
}

const ms = new MagicString(code, { filename: chunk.fileName });

const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0];

if (match) {
Expand All @@ -347,42 +341,6 @@ export function createRollupDebugIdInjectionHooks(): {
};
}

export function createRollupModuleMetadataInjectionHooks(injectionCode: string): {
renderChunk: RenderChunkHook;
} {
return {
renderChunk(code: string, chunk: { fileName: string; facadeModuleId?: string | null }) {
if (!isJsFile(chunk.fileName)) {
return null; // returning null means not modifying the chunk at all
}

// Skip empty chunks and HTML facade chunks (Vite MPA)
if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) {
return null;
}

const ms = new MagicString(code, { filename: chunk.fileName });

const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0];

if (match) {
// Add injected code after any comments or "use strict" at the beginning of the bundle.
ms.appendLeft(match.length, injectionCode);
} else {
// ms.replace() doesn't work when there is an empty string match (which happens if
// there is neither, a comment, nor a "use strict" at the top of the chunk) so we
// need this special case here.
ms.prepend(injectionCode);
}

return {
code: ms.toString(),
map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }),
};
},
};
}

export function createRollupDebugIdUploadHooks(
upload: (buildArtifacts: string[]) => Promise<void>,
_logger: Logger,
Expand Down
36 changes: 11 additions & 25 deletions packages/bundler-plugin-core/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { Compiler } from "webpack";
import {
getDebugIdSnippet,
sentryUnpluginFactory,
createRollupDebugIdInjectionHooks,
} from "../src";
import { getDebugIdSnippet, sentryUnpluginFactory, createRollupInjectionHooks } from "../src";
import { containsOnlyImports } from "../src/utils";

describe("getDebugIdSnippet", () => {
Expand Down Expand Up @@ -146,8 +142,8 @@ app.mount('#app');
});
});

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

describe("renderChunk", () => {
it("should inject debug ID into clean JavaScript files", () => {
Expand Down Expand Up @@ -212,7 +208,7 @@ describe("createRollupDebugIdInjectionHooks", () => {
],
])("should NOT inject when debug ID already exists (%s)", (_description, code) => {
const result = hooks.renderChunk(code, { fileName: "bundle.js" });
expect(result).toBeNull();
expect(result?.code).not.toContain("_sentryDebugIds");
});

it("should only check boundaries for performance (not entire file)", () => {
Expand Down Expand Up @@ -336,20 +332,12 @@ bootstrap();`;
});

describe("sentryUnpluginFactory sourcemaps.disable behavior", () => {
const mockReleaseInjectionPlugin = jest.fn((_injectionCode: string) => ({
name: "mock-release-injection-plugin",
}));

const mockComponentNameAnnotatePlugin = jest.fn(() => ({
name: "mock-component-name-annotate-plugin",
}));

const mockModuleMetadataInjectionPlugin = jest.fn((_injectionCode: string) => ({
name: "mock-module-metadata-injection-plugin",
}));

const mockDebugIdInjectionPlugin = jest.fn(() => ({
name: "mock-debug-id-injection-plugin",
const mockInjectionPlugin = jest.fn(() => ({
name: "mock-injection-plugin",
}));

const mockDebugIdUploadPlugin = jest.fn(() => ({
Expand All @@ -362,10 +350,8 @@ describe("sentryUnpluginFactory sourcemaps.disable behavior", () => {

const createUnpluginInstance = (): ReturnType<typeof sentryUnpluginFactory> => {
return sentryUnpluginFactory({
releaseInjectionPlugin: mockReleaseInjectionPlugin,
injectionPlugin: mockInjectionPlugin,
componentNameAnnotatePlugin: mockComponentNameAnnotatePlugin,
moduleMetadataInjectionPlugin: mockModuleMetadataInjectionPlugin,
debugIdInjectionPlugin: mockDebugIdInjectionPlugin,
debugIdUploadPlugin: mockDebugIdUploadPlugin,
bundleSizeOptimizationsPlugin: mockBundleSizeOptimizationsPlugin,
});
Expand Down Expand Up @@ -423,7 +409,7 @@ describe("sentryUnpluginFactory sourcemaps.disable behavior", () => {
const pluginNames = plugins.map((plugin) => plugin.name);

// Should include debug ID injection but not upload
expect(pluginNames).toContain("mock-debug-id-injection-plugin");
expect(pluginNames).toContain("mock-injection-plugin");
expect(pluginNames).not.toContain("mock-debug-id-upload-plugin");

// Should still include other core plugins
Expand Down Expand Up @@ -452,7 +438,7 @@ describe("sentryUnpluginFactory sourcemaps.disable behavior", () => {
const pluginNames = plugins.map((plugin) => plugin.name);

// Should include both debug ID related plugins
expect(pluginNames).toContain("mock-debug-id-injection-plugin");
expect(pluginNames).toContain("mock-injection-plugin");
expect(pluginNames).toContain("mock-debug-id-upload-plugin");

// Should include other core plugins
Expand All @@ -479,7 +465,7 @@ describe("sentryUnpluginFactory sourcemaps.disable behavior", () => {
const pluginNames = plugins.map((plugin) => plugin.name);

// Should include both debug ID related plugins by default
expect(pluginNames).toContain("mock-debug-id-injection-plugin");
expect(pluginNames).toContain("mock-injection-plugin");
expect(pluginNames).toContain("mock-debug-id-upload-plugin");

// Should include other core plugins
Expand All @@ -506,7 +492,7 @@ describe("sentryUnpluginFactory sourcemaps.disable behavior", () => {
const pluginNames = plugins.map((plugin) => plugin.name);

// Should include both debug ID related plugins by default
expect(pluginNames).toContain("mock-debug-id-injection-plugin");
expect(pluginNames).toContain("mock-injection-plugin");
expect(pluginNames).toContain("mock-debug-id-upload-plugin");

// Should include other core plugins
Expand Down
8 changes: 5 additions & 3 deletions packages/esbuild-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,11 @@ function esbuildBundleSizeOptimizationsPlugin(
}

const sentryUnplugin = sentryUnpluginFactory({
releaseInjectionPlugin: esbuildReleaseInjectionPlugin,
debugIdInjectionPlugin: esbuildDebugIdInjectionPlugin,
moduleMetadataInjectionPlugin: esbuildModuleMetadataInjectionPlugin,
injectionPlugin: {
releaseInjectionPlugin: esbuildReleaseInjectionPlugin,
debugIdInjectionPlugin: esbuildDebugIdInjectionPlugin,
moduleMetadataInjectionPlugin: esbuildModuleMetadataInjectionPlugin,
},
debugIdUploadPlugin: esbuildDebugIdUploadPlugin,
bundleSizeOptimizationsPlugin: esbuildBundleSizeOptimizationsPlugin,
});
Expand Down
Loading
Loading