diff --git a/MIGRATION.md b/MIGRATION.md index 81d0ec68..5963841a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -48,3 +48,9 @@ In version 2 we removed this functionality because it lead to intransparent nami Going forward, if you need similar functionality, we recommend providing folder paths in the `include` and `include.paths` options and narrowing down the matched files with the `ignore`, `ignoreFile` or `ext` options. The `ignore` and `ignoreFile` options will still allow globbing patterns. + +### Injecting `SENTRY_RELEASES` Map + +Previously, the webpack plugin always injected a `SENTRY_RELEASES` variable into the global object which would map from `project@org` to the `release` value. In version 2, we made this behaviour opt-in by setting the `injectReleasesMap` option in the plugin options to `true`. + +The purpose of this option is to support module-federated projects or micro frontend setups where multiple projects would want to access the global release variable. However, Sentry SDKs by default never accessed this variable so it would require manual user-intervention to make use of it. Making this behaviour opt-in decreases the bundle size impact of our plugin for the majority of users. diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index 4142b0d7..a371e3d7 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -185,7 +185,12 @@ const unplugin = createUnplugin((options, unpluginMetaContext) => { }); if (id === RELEASE_INJECTOR_ID) { - return generateGlobalInjectorCode({ release: internalOptions.release }); + return generateGlobalInjectorCode({ + release: internalOptions.release, + injectReleasesMap: internalOptions.injectReleasesMap, + org: internalOptions.org, + project: internalOptions.project, + }); } else { return undefined; } @@ -320,10 +325,20 @@ const unplugin = createUnplugin((options, unpluginMetaContext) => { * Generates code for the "sentry-release-injector" which is responsible for setting the global `SENTRY_RELEASE` * variable. */ -function generateGlobalInjectorCode({ release }: { release: string }) { +function generateGlobalInjectorCode({ + release, + injectReleasesMap, + org, + project, +}: { + release: string; + injectReleasesMap: boolean; + org?: string; + project?: string; +}) { // The code below is mostly ternary operators because it saves bundle size. // The checks are to support as many environments as possible. (Node.js, Browser, webworkers, etc.) - return ` + let code = ` var _global = typeof window !== 'undefined' ? window : @@ -334,6 +349,16 @@ function generateGlobalInjectorCode({ release }: { release: string }) { {}; _global.SENTRY_RELEASE={id:"${release}"};`; + + if (injectReleasesMap && project) { + const key = org ? `${project}@${org}` : project; + code += ` + _global.SENTRY_RELEASES=_global.SENTRY_RELEASES || {}; + _global.SENTRY_RELEASES["${key}"]={id:"${release}"}; + `; + } + + return code; } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/bundler-plugin-core/src/options-mapping.ts b/packages/bundler-plugin-core/src/options-mapping.ts index 9c7daaec..8485f22e 100644 --- a/packages/bundler-plugin-core/src/options-mapping.ts +++ b/packages/bundler-plugin-core/src/options-mapping.ts @@ -15,6 +15,7 @@ type RequiredInternalOptions = Required< | "silent" | "cleanArtifacts" | "telemetry" + | "injectReleasesMap" > >; @@ -100,6 +101,7 @@ export function normalizeUserOptions(userOptions: UserOptions): InternalOptions entries, include, configFile: userOptions.configFile, + injectReleasesMap: userOptions.injectReleasesMap ?? false, }; } diff --git a/packages/bundler-plugin-core/src/types.ts b/packages/bundler-plugin-core/src/types.ts index 941ca96e..54059978 100644 --- a/packages/bundler-plugin-core/src/types.ts +++ b/packages/bundler-plugin-core/src/types.ts @@ -179,6 +179,15 @@ export type Options = Omit & { * defaults from ~/.sentryclirc are always loaded */ configFile?: string; + + /** + * If set to true, the plugin will inject an additional `SENTRY_RELEASES` variable that + * maps from `{org}@{project}` to the `release` value. This might be helpful for webpack + * module federation or micro frontend setups. + * + * Defaults to `false` + */ + injectReleasesMap?: boolean; }; export type IncludeEntry = { diff --git a/packages/bundler-plugin-core/test/option-mappings.test.ts b/packages/bundler-plugin-core/test/option-mappings.test.ts index 8267bc29..f440b9f5 100644 --- a/packages/bundler-plugin-core/test/option-mappings.test.ts +++ b/packages/bundler-plugin-core/test/option-mappings.test.ts @@ -35,6 +35,7 @@ describe("normalizeUserOptions()", () => { telemetry: true, url: "https://sentry.io/", vcsRemote: "origin", + injectReleasesMap: false, }); }); @@ -78,6 +79,7 @@ describe("normalizeUserOptions()", () => { telemetry: true, url: "https://sentry.io/", vcsRemote: "origin", + injectReleasesMap: false, }); }); }); diff --git a/packages/integration-tests/fixtures/releases-injection/input/entrypoint.js b/packages/integration-tests/fixtures/releases-injection/input/entrypoint.js new file mode 100644 index 00000000..1f0821b8 --- /dev/null +++ b/packages/integration-tests/fixtures/releases-injection/input/entrypoint.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call +process.stdout.write(global.SENTRY_RELEASES["releasesProject@releasesOrg"].id.toString()); diff --git a/packages/integration-tests/fixtures/releases-injection/releases-injection.test.ts b/packages/integration-tests/fixtures/releases-injection/releases-injection.test.ts new file mode 100644 index 00000000..7d889cec --- /dev/null +++ b/packages/integration-tests/fixtures/releases-injection/releases-injection.test.ts @@ -0,0 +1,38 @@ +import childProcess from "child_process"; +import path from "path"; + +/** + * Runs a node file in a seprate process. + * + * @param bundlePath Path of node file to run + * @returns Stdout of the process + */ +function checkBundle(bundlePath: string): void { + const processOutput = childProcess.execSync(`node ${bundlePath}`, { encoding: "utf-8" }); + expect(processOutput).toBe("I AM A RELEASE!"); +} + +test("esbuild bundle", () => { + expect.assertions(1); + checkBundle(path.join(__dirname, "./out/esbuild/index.js")); +}); + +test("rollup bundle", () => { + expect.assertions(1); + checkBundle(path.join(__dirname, "./out/rollup/index.js")); +}); + +test("vite bundle", () => { + expect.assertions(1); + checkBundle(path.join(__dirname, "./out/vite/index.js")); +}); + +test("webpack 4 bundle", () => { + expect.assertions(1); + checkBundle(path.join(__dirname, "./out/webpack4/index.js")); +}); + +test("webpack 5 bundle", () => { + expect.assertions(1); + checkBundle(path.join(__dirname, "./out/webpack5/index.js")); +}); diff --git a/packages/integration-tests/fixtures/releases-injection/setup.ts b/packages/integration-tests/fixtures/releases-injection/setup.ts new file mode 100644 index 00000000..8807e676 --- /dev/null +++ b/packages/integration-tests/fixtures/releases-injection/setup.ts @@ -0,0 +1,15 @@ +import { Options } from "@sentry/bundler-plugin-core"; +import * as path from "path"; +import { createCjsBundles } from "../../utils/create-cjs-bundles"; + +const entryPointPath = path.resolve(__dirname, "./input/entrypoint.js"); +const outputDir = path.resolve(__dirname, "./out"); + +createCjsBundles({ index: entryPointPath }, outputDir, { + release: "I AM A RELEASE!", + project: "releasesProject", + org: "releasesOrg", + include: outputDir, + dryRun: true, + injectReleasesMap: true, +} as Options);