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
42 changes: 39 additions & 3 deletions packages/bundler-plugin-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import { releaseManagementPlugin } from "./plugins/release-management";
import { telemetryPlugin } from "./plugins/telemetry";
import { createLogger } from "./sentry/logger";
import { allowedToSendTelemetry, createSentryInstance } from "./sentry/telemetry";
import { Options } from "./types";
import { Options, SentrySDKBuildFlags } from "./types";
import {
generateGlobalInjectorCode,
generateModuleMetadataInjectorCode,
getDependencies,
getPackageJson,
parseMajorVersion,
replaceBooleanFlagsInCode,
stringToUUID,
stripQueryAndHashFromPath,
} from "./utils";
Expand All @@ -27,6 +28,7 @@ interface SentryUnpluginFactoryOptions {
moduleMetadataInjectionPlugin?: (injectionCode: string) => UnpluginOptions;
debugIdInjectionPlugin: () => UnpluginOptions;
debugIdUploadPlugin: (upload: (buildArtifacts: string[]) => Promise<void>) => UnpluginOptions;
bundleSizeOptimizationsPlugin: (buildFlags: SentrySDKBuildFlags) => UnpluginOptions;
}

/**
Expand Down Expand Up @@ -61,6 +63,7 @@ export function sentryUnpluginFactory({
moduleMetadataInjectionPlugin,
debugIdInjectionPlugin,
debugIdUploadPlugin,
bundleSizeOptimizationsPlugin,
}: SentryUnpluginFactoryOptions) {
return createUnplugin<Options, true>((userOptions, unpluginMetaContext) => {
const logger = createLogger({
Expand Down Expand Up @@ -161,6 +164,31 @@ export function sentryUnpluginFactory({
})
);

if (options.bundleSizeOptimizations) {
const { bundleSizeOptimizations } = options;
const replacementValues: SentrySDKBuildFlags = {};

if (bundleSizeOptimizations.excludeDebugStatements) {
replacementValues["__SENTRY_DEBUG__"] = false;
}
if (bundleSizeOptimizations.excludePerformanceMonitoring) {
replacementValues["__SENTRY_TRACE__"] = false;
}
if (bundleSizeOptimizations.excludeReplayCanvas) {
replacementValues["__RRWEB_EXCLUDE_CANVAS__"] = true;
}
if (bundleSizeOptimizations.excludeReplayIframe) {
replacementValues["__RRWEB_EXCLUDE_IFRAME__"] = true;
}
if (bundleSizeOptimizations.excludeReplayShadowDom) {
replacementValues["__RRWEB_EXCLUDE_SHADOW_DOM__"] = true;
}

if (Object.keys(replacementValues).length > 0) {
plugins.push(bundleSizeOptimizationsPlugin(replacementValues));
}
}

if (!options.release.inject) {
logger.debug(
"Release injection disabled via `release.inject` option. Will not inject release."
Expand Down Expand Up @@ -371,6 +399,14 @@ export function createRollupReleaseInjectionHooks(injectionCode: string) {
};
}

export function createRollupBundleSizeOptimizationHooks(replacementValues: SentrySDKBuildFlags) {
return {
transform(code: string) {
return replaceBooleanFlagsInCode(code, replacementValues);
},
};
}

// We need to be careful not to inject the snippet before any `"use strict";`s.
// As an additional complication `"use strict";`s may come after any number of comments.
const COMMENT_USE_STRICT_REGEX =
Expand Down Expand Up @@ -475,6 +511,6 @@ export function getDebugIdSnippet(debugId: string): string {
return `;!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=(new Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}}();`;
}

export { stringToUUID } from "./utils";
export { stringToUUID, replaceBooleanFlagsInCode } from "./utils";

export type { Options } from "./types";
export type { Options, SentrySDKBuildFlags } from "./types";
1 change: 1 addition & 0 deletions packages/bundler-plugin-core/src/options-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function normalizeUserOptions(userOptions: UserOptions) {
vcsRemote: userOptions.release?.vcsRemote ?? process.env["SENTRY_VSC_REMOTE"] ?? "origin",
cleanArtifacts: userOptions.release?.cleanArtifacts ?? false,
},
bundleSizeOptimizations: userOptions.bundleSizeOptimizations,
_experiments: userOptions._experiments ?? {},
};

Expand Down
49 changes: 49 additions & 0 deletions packages/bundler-plugin-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,47 @@ export interface Options {
uploadLegacySourcemaps?: string | IncludeEntry | Array<string | IncludeEntry>;
};

/**
* Options related to bundle size optimizations.
*/
bundleSizeOptimizations?: {
/**
* If set to true, the plugin will try to tree-shake debug statements out.
* Note that the success of this depends on tree shaking generally being enabled in your build.
*/
excludeDebugStatements?: boolean;

/**
* If set to true, the plugin will try to tree-shake performance monitoring statements out.
* Note that the success of this depends on tree shaking generally being enabled in your build.
* Attention: DO NOT enable this when you're using any performance monitoring-related SDK features (e.g. Sentry.startTransaction()).
* This flag is intended to be used in combination with packages like @sentry/next or @sentry/sveltekit,
* which automatically include performance monitoring functionality.
*/
excludePerformanceMonitoring?: boolean;

/**
* If set to true, the plugin will try to tree-shake Session Replay's Canvas recording functionality out.
* You can safely do this when you do not want to capture any Canvas activity via Replay.
* Note that the success of this depends on tree shaking generally being enabled in your build.
*/
excludeReplayCanvas?: boolean;

/**
* If set to true, the plugin will try to tree-shake Session Replay's Shadow DOM recording functionality out.
* You can safely do this when you do not want to capture any Shadow DOM activity via Replay.
* Note that the success of this depends on tree shaking generally being enabled in your build.
*/
excludeReplayShadowDom?: boolean;

/**
* If set to true, the plugin will try to tree-shake Session Replay's IFrame recording functionality out.
* You can safely do this when you do not want to capture any IFrame activity via Replay.
* Note that the success of this depends on tree shaking generally being enabled in your build.
*/
excludeReplayIframe?: boolean;
};

/**
* Options that are considered experimental and subject to change.
*
Expand Down Expand Up @@ -348,6 +389,14 @@ export type IncludeEntry = {
validate?: boolean;
};

export interface SentrySDKBuildFlags extends Record<string, boolean | undefined> {
__SENTRY_DEBUG__?: boolean;
__SENTRY_TRACE__?: boolean;
__RRWEB_EXCLUDE_CANVAS__?: boolean;
__RRWEB_EXCLUDE_IFRAME__?: boolean;
__RRWEB_EXCLUDE_SHADOW_DOM__?: boolean;
}

type SetCommitsOptions = (AutoSetCommitsOptions | ManualSetCommitsOptions) & {
/**
* The commit before the beginning of this release (in other words,
Expand Down
25 changes: 25 additions & 0 deletions packages/bundler-plugin-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fs from "fs";
import os from "os";
import crypto from "crypto";
import childProcess from "child_process";
import MagicString, { SourceMap } from "magic-string";

/**
* Checks whether the given input is already an array, and if it isn't, wraps it in one.
Expand Down Expand Up @@ -307,3 +308,27 @@ export function stripQueryAndHashFromPath(path: string): string {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return path.split("?")[0]!.split("#")[0]!;
}

export function replaceBooleanFlagsInCode(
code: string,
replacementValues: Record<string, boolean | undefined>
): { code: string; map: SourceMap } | null {
const ms = new MagicString(code);

Object.keys(replacementValues).forEach((key) => {
const value = replacementValues[key];

if (typeof value === "boolean") {
ms.replaceAll(key, JSON.stringify(value));
}
});

if (ms.hasChanged()) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is nice!

return {
code: ms.toString(),
map: ms.generateMap({ hires: true }),
};
}

return null;
}
41 changes: 40 additions & 1 deletion packages/bundler-plugin-core/test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { getDependencies, getPackageJson, parseMajorVersion, stringToUUID } from "../src/utils";
import {
getDependencies,
getPackageJson,
parseMajorVersion,
replaceBooleanFlagsInCode,
stringToUUID,
} from "../src/utils";
import path from "node:path";

type PackageJson = Record<string, unknown>;
Expand Down Expand Up @@ -175,3 +181,36 @@ describe("stringToUUID", () => {
expect(stringToUUID("Nothing personnel kid")).toBe("52c7a762-5ddf-49a7-af16-6874a8cb2512");
});
});

describe("replaceBooleanFlagsInCode", () => {
test("it works without a match", () => {
const code = "const a = 1;";
const result = replaceBooleanFlagsInCode(code, { __DEBUG_BUILD__: false });
expect(result).toBeNull();
});

test("it works with matches", () => {
const code = `const a = 1;
if (__DEBUG_BUILD__ && checkMe()) {
// do something
}
if (__DEBUG_BUILD__ && __RRWEB_EXCLUDE_CANVAS__) {
const a = __RRWEB_EXCLUDE_CANVAS__ ? 1 : 2;
}`;
const result = replaceBooleanFlagsInCode(code, {
__DEBUG_BUILD__: false,
__RRWEB_EXCLUDE_CANVAS__: true,
});
expect(result).toEqual({
code: `const a = 1;
if (false && checkMe()) {
// do something
}
if (false && true) {
const a = true ? 1 : 2;
}`,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
map: expect.anything(),
});
});
});
26 changes: 25 additions & 1 deletion packages/esbuild-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { sentryUnpluginFactory, Options, getDebugIdSnippet } from "@sentry/bundler-plugin-core";
import {
sentryUnpluginFactory,
Options,
getDebugIdSnippet,
SentrySDKBuildFlags,
} from "@sentry/bundler-plugin-core";
import type { UnpluginOptions } from "unplugin";
import * as path from "path";

Expand Down Expand Up @@ -226,11 +231,30 @@ function esbuildDebugIdUploadPlugin(
};
}

function esbuildBundleSizeOptimizationsPlugin(
replacementValues: SentrySDKBuildFlags
): UnpluginOptions {
return {
name: "sentry-esbuild-bundle-size-optimizations-plugin",
esbuild: {
setup({ initialOptions }) {
const replacementStringValues: Record<string, string> = {};
Object.entries(replacementValues).forEach(([key, value]) => {
replacementStringValues[key] = JSON.stringify(value);
});

initialOptions.define = { ...initialOptions.define, ...replacementStringValues };
},
},
};
}

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

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
7 changes: 6 additions & 1 deletion packages/integration-tests/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ const jestPackageJson = require("jest/package.json");
module.exports = {
root: true,
extends: ["@sentry-internal/eslint-config/jest", "@sentry-internal/eslint-config/base"],
ignorePatterns: [".eslintrc.js", "fixtures/*/out", "jest.config.js"],
ignorePatterns: [
".eslintrc.js",
"fixtures/*/out",
"jest.config.js",
"fixtures/bundle-size-optimizations/*",
],
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable jest/no-standalone-expect */
/* eslint-disable jest/expect-expect */
import path from "path";
import fs from "fs";
import { testIfNodeMajorVersionIsLessThan18 } from "../../utils/testIf";

const expectedOutputs: Record<string, Record<string, string>> = {
esbuild: {
"bundle1.js": `console.log(1)`,
"bundle2.js": `console.log({debug:"b",trace:"b",replayCanvas:"a",replayIframe:"a",replayShadowDom:"a"})`,
},
rollup: {
"bundle1.js": `console.log(1 );`,
"bundle2.js": `console.log({
debug: "b",
trace: "b",
replayCanvas: "a" ,
replayIframe: "a" ,
replayShadowDom: "a" ,
});`,
},
vite: {
"bundle1.js": `console.log(1);`,
"bundle2.js": `console.log({debug:"b",trace:"b",replayCanvas:"a",replayIframe:"a",replayShadowDom:"a"});`,
},
webpack4: {
"bundle1.js": `console.log(1)`,
"bundle2.js": `console.log({debug:"b",trace:"b",replayCanvas:"a",replayIframe:"a",replayShadowDom:"a"})`,
},
webpack5: {
"bundle1.js": `console.log(1)`,
"bundle2.js": `console.log({debug:"b",trace:"b",replayCanvas:"a",replayIframe:"a",replayShadowDom:"a"});`,
},
};

function checkBundle(bundler: string, bundlePath: string): void {
const actualPath = path.join(__dirname, "out", bundler, bundlePath);

// We replace multiple whitespaces with a single space for consistency
const actual = fs.readFileSync(actualPath, "utf-8").replace(/\s+/gim, " ");
const expected = expectedOutputs[bundler]?.[bundlePath]?.replace(/\s+/gim, " ");

expect(actual).toContain(expected);
}

test("esbuild bundle", () => {
checkBundle("esbuild", "bundle1.js");
checkBundle("esbuild", "bundle2.js");
});

test("rollup bundle", () => {
checkBundle("rollup", "bundle1.js");
checkBundle("rollup", "bundle2.js");
});

test("vite bundle", () => {
checkBundle("vite", "bundle1.js");
checkBundle("vite", "bundle2.js");
});

testIfNodeMajorVersionIsLessThan18("webpack 4 bundle", () => {
checkBundle("webpack4", "bundle1.js");
checkBundle("webpack4", "bundle2.js");
});

test("webpack 5 bundle", () => {
checkBundle("webpack5", "bundle1.js");
checkBundle("webpack5", "bundle2.js");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
if (__SENTRY_DEBUG__ && Math.random() > 0.5) {
console.log("was > 0.5");
}

if (__SENTRY_DEBUG__ && __RRWEB_EXCLUDE_CANVAS__) {
console.log("debug & exclude");
}

console.log(__RRWEB_EXCLUDE_CANVAS__ ? 1 : 2);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
console.log({
debug: __SENTRY_DEBUG__ ? "a" : "b",
trace: __SENTRY_TRACE__ ? "a" : "b",
replayCanvas: __RRWEB_EXCLUDE_CANVAS__ ? "a" : "b",
replayIframe: __RRWEB_EXCLUDE_IFRAME__ ? "a" : "b",
replayShadowDom: __RRWEB_EXCLUDE_SHADOW_DOM__ ? "a" : "b",
});
Loading