Skip to content

Commit 2b75688

Browse files
authored
feat: Allow to configure bundleSizeOptimizations (#428)
1 parent eb3407b commit 2b75688

File tree

15 files changed

+487
-7
lines changed

15 files changed

+487
-7
lines changed

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { releaseManagementPlugin } from "./plugins/release-management";
99
import { telemetryPlugin } from "./plugins/telemetry";
1010
import { createLogger } from "./sentry/logger";
1111
import { allowedToSendTelemetry, createSentryInstance } from "./sentry/telemetry";
12-
import { Options } from "./types";
12+
import { Options, SentrySDKBuildFlags } from "./types";
1313
import {
1414
generateGlobalInjectorCode,
1515
generateModuleMetadataInjectorCode,
1616
getDependencies,
1717
getPackageJson,
1818
parseMajorVersion,
19+
replaceBooleanFlagsInCode,
1920
stringToUUID,
2021
stripQueryAndHashFromPath,
2122
} from "./utils";
@@ -27,6 +28,7 @@ interface SentryUnpluginFactoryOptions {
2728
moduleMetadataInjectionPlugin?: (injectionCode: string) => UnpluginOptions;
2829
debugIdInjectionPlugin: () => UnpluginOptions;
2930
debugIdUploadPlugin: (upload: (buildArtifacts: string[]) => Promise<void>) => UnpluginOptions;
31+
bundleSizeOptimizationsPlugin: (buildFlags: SentrySDKBuildFlags) => UnpluginOptions;
3032
}
3133

3234
/**
@@ -61,6 +63,7 @@ export function sentryUnpluginFactory({
6163
moduleMetadataInjectionPlugin,
6264
debugIdInjectionPlugin,
6365
debugIdUploadPlugin,
66+
bundleSizeOptimizationsPlugin,
6467
}: SentryUnpluginFactoryOptions) {
6568
return createUnplugin<Options, true>((userOptions, unpluginMetaContext) => {
6669
const logger = createLogger({
@@ -161,6 +164,31 @@ export function sentryUnpluginFactory({
161164
})
162165
);
163166

167+
if (options.bundleSizeOptimizations) {
168+
const { bundleSizeOptimizations } = options;
169+
const replacementValues: SentrySDKBuildFlags = {};
170+
171+
if (bundleSizeOptimizations.excludeDebugStatements) {
172+
replacementValues["__SENTRY_DEBUG__"] = false;
173+
}
174+
if (bundleSizeOptimizations.excludePerformanceMonitoring) {
175+
replacementValues["__SENTRY_TRACE__"] = false;
176+
}
177+
if (bundleSizeOptimizations.excludeReplayCanvas) {
178+
replacementValues["__RRWEB_EXCLUDE_CANVAS__"] = true;
179+
}
180+
if (bundleSizeOptimizations.excludeReplayIframe) {
181+
replacementValues["__RRWEB_EXCLUDE_IFRAME__"] = true;
182+
}
183+
if (bundleSizeOptimizations.excludeReplayShadowDom) {
184+
replacementValues["__RRWEB_EXCLUDE_SHADOW_DOM__"] = true;
185+
}
186+
187+
if (Object.keys(replacementValues).length > 0) {
188+
plugins.push(bundleSizeOptimizationsPlugin(replacementValues));
189+
}
190+
}
191+
164192
if (!options.release.inject) {
165193
logger.debug(
166194
"Release injection disabled via `release.inject` option. Will not inject release."
@@ -371,6 +399,14 @@ export function createRollupReleaseInjectionHooks(injectionCode: string) {
371399
};
372400
}
373401

402+
export function createRollupBundleSizeOptimizationHooks(replacementValues: SentrySDKBuildFlags) {
403+
return {
404+
transform(code: string) {
405+
return replaceBooleanFlagsInCode(code, replacementValues);
406+
},
407+
};
408+
}
409+
374410
// We need to be careful not to inject the snippet before any `"use strict";`s.
375411
// As an additional complication `"use strict";`s may come after any number of comments.
376412
const COMMENT_USE_STRICT_REGEX =
@@ -475,6 +511,6 @@ export function getDebugIdSnippet(debugId: string): string {
475511
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){}}();`;
476512
}
477513

478-
export { stringToUUID } from "./utils";
514+
export { stringToUUID, replaceBooleanFlagsInCode } from "./utils";
479515

480-
export type { Options } from "./types";
516+
export type { Options, SentrySDKBuildFlags } from "./types";

packages/bundler-plugin-core/src/options-mapping.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function normalizeUserOptions(userOptions: UserOptions) {
2828
vcsRemote: userOptions.release?.vcsRemote ?? process.env["SENTRY_VSC_REMOTE"] ?? "origin",
2929
cleanArtifacts: userOptions.release?.cleanArtifacts ?? false,
3030
},
31+
bundleSizeOptimizations: userOptions.bundleSizeOptimizations,
3132
_experiments: userOptions._experiments ?? {},
3233
};
3334

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,47 @@ export interface Options {
228228
uploadLegacySourcemaps?: string | IncludeEntry | Array<string | IncludeEntry>;
229229
};
230230

231+
/**
232+
* Options related to bundle size optimizations.
233+
*/
234+
bundleSizeOptimizations?: {
235+
/**
236+
* If set to true, the plugin will try to tree-shake debug statements out.
237+
* Note that the success of this depends on tree shaking generally being enabled in your build.
238+
*/
239+
excludeDebugStatements?: boolean;
240+
241+
/**
242+
* If set to true, the plugin will try to tree-shake performance monitoring statements out.
243+
* Note that the success of this depends on tree shaking generally being enabled in your build.
244+
* Attention: DO NOT enable this when you're using any performance monitoring-related SDK features (e.g. Sentry.startTransaction()).
245+
* This flag is intended to be used in combination with packages like @sentry/next or @sentry/sveltekit,
246+
* which automatically include performance monitoring functionality.
247+
*/
248+
excludePerformanceMonitoring?: boolean;
249+
250+
/**
251+
* If set to true, the plugin will try to tree-shake Session Replay's Canvas recording functionality out.
252+
* You can safely do this when you do not want to capture any Canvas activity via Replay.
253+
* Note that the success of this depends on tree shaking generally being enabled in your build.
254+
*/
255+
excludeReplayCanvas?: boolean;
256+
257+
/**
258+
* If set to true, the plugin will try to tree-shake Session Replay's Shadow DOM recording functionality out.
259+
* You can safely do this when you do not want to capture any Shadow DOM activity via Replay.
260+
* Note that the success of this depends on tree shaking generally being enabled in your build.
261+
*/
262+
excludeReplayShadowDom?: boolean;
263+
264+
/**
265+
* If set to true, the plugin will try to tree-shake Session Replay's IFrame recording functionality out.
266+
* You can safely do this when you do not want to capture any IFrame activity via Replay.
267+
* Note that the success of this depends on tree shaking generally being enabled in your build.
268+
*/
269+
excludeReplayIframe?: boolean;
270+
};
271+
231272
/**
232273
* Options that are considered experimental and subject to change.
233274
*
@@ -348,6 +389,14 @@ export type IncludeEntry = {
348389
validate?: boolean;
349390
};
350391

392+
export interface SentrySDKBuildFlags extends Record<string, boolean | undefined> {
393+
__SENTRY_DEBUG__?: boolean;
394+
__SENTRY_TRACE__?: boolean;
395+
__RRWEB_EXCLUDE_CANVAS__?: boolean;
396+
__RRWEB_EXCLUDE_IFRAME__?: boolean;
397+
__RRWEB_EXCLUDE_SHADOW_DOM__?: boolean;
398+
}
399+
351400
type SetCommitsOptions = (AutoSetCommitsOptions | ManualSetCommitsOptions) & {
352401
/**
353402
* The commit before the beginning of this release (in other words,

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from "fs";
44
import os from "os";
55
import crypto from "crypto";
66
import childProcess from "child_process";
7+
import MagicString, { SourceMap } from "magic-string";
78

89
/**
910
* Checks whether the given input is already an array, and if it isn't, wraps it in one.
@@ -307,3 +308,27 @@ export function stripQueryAndHashFromPath(path: string): string {
307308
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
308309
return path.split("?")[0]!.split("#")[0]!;
309310
}
311+
312+
export function replaceBooleanFlagsInCode(
313+
code: string,
314+
replacementValues: Record<string, boolean | undefined>
315+
): { code: string; map: SourceMap } | null {
316+
const ms = new MagicString(code);
317+
318+
Object.keys(replacementValues).forEach((key) => {
319+
const value = replacementValues[key];
320+
321+
if (typeof value === "boolean") {
322+
ms.replaceAll(key, JSON.stringify(value));
323+
}
324+
});
325+
326+
if (ms.hasChanged()) {
327+
return {
328+
code: ms.toString(),
329+
map: ms.generateMap({ hires: true }),
330+
};
331+
}
332+
333+
return null;
334+
}

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { getDependencies, getPackageJson, parseMajorVersion, stringToUUID } from "../src/utils";
1+
import {
2+
getDependencies,
3+
getPackageJson,
4+
parseMajorVersion,
5+
replaceBooleanFlagsInCode,
6+
stringToUUID,
7+
} from "../src/utils";
28
import path from "node:path";
39

410
type PackageJson = Record<string, unknown>;
@@ -175,3 +181,36 @@ describe("stringToUUID", () => {
175181
expect(stringToUUID("Nothing personnel kid")).toBe("52c7a762-5ddf-49a7-af16-6874a8cb2512");
176182
});
177183
});
184+
185+
describe("replaceBooleanFlagsInCode", () => {
186+
test("it works without a match", () => {
187+
const code = "const a = 1;";
188+
const result = replaceBooleanFlagsInCode(code, { __DEBUG_BUILD__: false });
189+
expect(result).toBeNull();
190+
});
191+
192+
test("it works with matches", () => {
193+
const code = `const a = 1;
194+
if (__DEBUG_BUILD__ && checkMe()) {
195+
// do something
196+
}
197+
if (__DEBUG_BUILD__ && __RRWEB_EXCLUDE_CANVAS__) {
198+
const a = __RRWEB_EXCLUDE_CANVAS__ ? 1 : 2;
199+
}`;
200+
const result = replaceBooleanFlagsInCode(code, {
201+
__DEBUG_BUILD__: false,
202+
__RRWEB_EXCLUDE_CANVAS__: true,
203+
});
204+
expect(result).toEqual({
205+
code: `const a = 1;
206+
if (false && checkMe()) {
207+
// do something
208+
}
209+
if (false && true) {
210+
const a = true ? 1 : 2;
211+
}`,
212+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
213+
map: expect.anything(),
214+
});
215+
});
216+
});

packages/esbuild-plugin/src/index.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { sentryUnpluginFactory, Options, getDebugIdSnippet } from "@sentry/bundler-plugin-core";
1+
import {
2+
sentryUnpluginFactory,
3+
Options,
4+
getDebugIdSnippet,
5+
SentrySDKBuildFlags,
6+
} from "@sentry/bundler-plugin-core";
27
import type { UnpluginOptions } from "unplugin";
38
import * as path from "path";
49

@@ -226,11 +231,30 @@ function esbuildDebugIdUploadPlugin(
226231
};
227232
}
228233

234+
function esbuildBundleSizeOptimizationsPlugin(
235+
replacementValues: SentrySDKBuildFlags
236+
): UnpluginOptions {
237+
return {
238+
name: "sentry-esbuild-bundle-size-optimizations-plugin",
239+
esbuild: {
240+
setup({ initialOptions }) {
241+
const replacementStringValues: Record<string, string> = {};
242+
Object.entries(replacementValues).forEach(([key, value]) => {
243+
replacementStringValues[key] = JSON.stringify(value);
244+
});
245+
246+
initialOptions.define = { ...initialOptions.define, ...replacementStringValues };
247+
},
248+
},
249+
};
250+
}
251+
229252
const sentryUnplugin = sentryUnpluginFactory({
230253
releaseInjectionPlugin: esbuildReleaseInjectionPlugin,
231254
debugIdInjectionPlugin: esbuildDebugIdInjectionPlugin,
232255
moduleMetadataInjectionPlugin: esbuildModuleMetadataInjectionPlugin,
233256
debugIdUploadPlugin: esbuildDebugIdUploadPlugin,
257+
bundleSizeOptimizationsPlugin: esbuildBundleSizeOptimizationsPlugin,
234258
});
235259

236260
// eslint-disable-next-line @typescript-eslint/no-explicit-any

packages/integration-tests/.eslintrc.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ const jestPackageJson = require("jest/package.json");
44
module.exports = {
55
root: true,
66
extends: ["@sentry-internal/eslint-config/jest", "@sentry-internal/eslint-config/base"],
7-
ignorePatterns: [".eslintrc.js", "fixtures/*/out", "jest.config.js"],
7+
ignorePatterns: [
8+
".eslintrc.js",
9+
"fixtures/*/out",
10+
"jest.config.js",
11+
"fixtures/bundle-size-optimizations/*",
12+
],
813
parserOptions: {
914
tsconfigRootDir: __dirname,
1015
project: ["./tsconfig.json"],
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* eslint-disable jest/no-standalone-expect */
2+
/* eslint-disable jest/expect-expect */
3+
import path from "path";
4+
import fs from "fs";
5+
import { testIfNodeMajorVersionIsLessThan18 } from "../../utils/testIf";
6+
7+
const expectedOutputs: Record<string, Record<string, string>> = {
8+
esbuild: {
9+
"bundle1.js": `console.log(1)`,
10+
"bundle2.js": `console.log({debug:"b",trace:"b",replayCanvas:"a",replayIframe:"a",replayShadowDom:"a"})`,
11+
},
12+
rollup: {
13+
"bundle1.js": `console.log(1 );`,
14+
"bundle2.js": `console.log({
15+
debug: "b",
16+
trace: "b",
17+
replayCanvas: "a" ,
18+
replayIframe: "a" ,
19+
replayShadowDom: "a" ,
20+
});`,
21+
},
22+
vite: {
23+
"bundle1.js": `console.log(1);`,
24+
"bundle2.js": `console.log({debug:"b",trace:"b",replayCanvas:"a",replayIframe:"a",replayShadowDom:"a"});`,
25+
},
26+
webpack4: {
27+
"bundle1.js": `console.log(1)`,
28+
"bundle2.js": `console.log({debug:"b",trace:"b",replayCanvas:"a",replayIframe:"a",replayShadowDom:"a"})`,
29+
},
30+
webpack5: {
31+
"bundle1.js": `console.log(1)`,
32+
"bundle2.js": `console.log({debug:"b",trace:"b",replayCanvas:"a",replayIframe:"a",replayShadowDom:"a"});`,
33+
},
34+
};
35+
36+
function checkBundle(bundler: string, bundlePath: string): void {
37+
const actualPath = path.join(__dirname, "out", bundler, bundlePath);
38+
39+
// We replace multiple whitespaces with a single space for consistency
40+
const actual = fs.readFileSync(actualPath, "utf-8").replace(/\s+/gim, " ");
41+
const expected = expectedOutputs[bundler]?.[bundlePath]?.replace(/\s+/gim, " ");
42+
43+
expect(actual).toContain(expected);
44+
}
45+
46+
test("esbuild bundle", () => {
47+
checkBundle("esbuild", "bundle1.js");
48+
checkBundle("esbuild", "bundle2.js");
49+
});
50+
51+
test("rollup bundle", () => {
52+
checkBundle("rollup", "bundle1.js");
53+
checkBundle("rollup", "bundle2.js");
54+
});
55+
56+
test("vite bundle", () => {
57+
checkBundle("vite", "bundle1.js");
58+
checkBundle("vite", "bundle2.js");
59+
});
60+
61+
testIfNodeMajorVersionIsLessThan18("webpack 4 bundle", () => {
62+
checkBundle("webpack4", "bundle1.js");
63+
checkBundle("webpack4", "bundle2.js");
64+
});
65+
66+
test("webpack 5 bundle", () => {
67+
checkBundle("webpack5", "bundle1.js");
68+
checkBundle("webpack5", "bundle2.js");
69+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
if (__SENTRY_DEBUG__ && Math.random() > 0.5) {
2+
console.log("was > 0.5");
3+
}
4+
5+
if (__SENTRY_DEBUG__ && __RRWEB_EXCLUDE_CANVAS__) {
6+
console.log("debug & exclude");
7+
}
8+
9+
console.log(__RRWEB_EXCLUDE_CANVAS__ ? 1 : 2);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
console.log({
2+
debug: __SENTRY_DEBUG__ ? "a" : "b",
3+
trace: __SENTRY_TRACE__ ? "a" : "b",
4+
replayCanvas: __RRWEB_EXCLUDE_CANVAS__ ? "a" : "b",
5+
replayIframe: __RRWEB_EXCLUDE_IFRAME__ ? "a" : "b",
6+
replayShadowDom: __RRWEB_EXCLUDE_SHADOW_DOM__ ? "a" : "b",
7+
});

0 commit comments

Comments
 (0)