Skip to content

Commit 3ecb9ff

Browse files
authored
feat(core): Add SENTRY_RELEASES variable during release injection (#102)
Add injection of `SENTRY_RELEASES` variable if `injectReleasesMap` option was set to `true`. - Introduce `injectReleasesMap` option - Add map map injection to release injection code generation and make it depend on `injectReleasesMap` - Add integration test to verify behaviour
1 parent 2345793 commit 3ecb9ff

File tree

8 files changed

+102
-3
lines changed

8 files changed

+102
-3
lines changed

MIGRATION.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@ In version 2 we removed this functionality because it lead to intransparent nami
4848

4949
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.
5050
The `ignore` and `ignoreFile` options will still allow globbing patterns.
51+
52+
### Injecting `SENTRY_RELEASES` Map
53+
54+
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`.
55+
56+
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.

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
185185
});
186186

187187
if (id === RELEASE_INJECTOR_ID) {
188-
return generateGlobalInjectorCode({ release: internalOptions.release });
188+
return generateGlobalInjectorCode({
189+
release: internalOptions.release,
190+
injectReleasesMap: internalOptions.injectReleasesMap,
191+
org: internalOptions.org,
192+
project: internalOptions.project,
193+
});
189194
} else {
190195
return undefined;
191196
}
@@ -320,10 +325,20 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
320325
* Generates code for the "sentry-release-injector" which is responsible for setting the global `SENTRY_RELEASE`
321326
* variable.
322327
*/
323-
function generateGlobalInjectorCode({ release }: { release: string }) {
328+
function generateGlobalInjectorCode({
329+
release,
330+
injectReleasesMap,
331+
org,
332+
project,
333+
}: {
334+
release: string;
335+
injectReleasesMap: boolean;
336+
org?: string;
337+
project?: string;
338+
}) {
324339
// The code below is mostly ternary operators because it saves bundle size.
325340
// The checks are to support as many environments as possible. (Node.js, Browser, webworkers, etc.)
326-
return `
341+
let code = `
327342
var _global =
328343
typeof window !== 'undefined' ?
329344
window :
@@ -334,6 +349,16 @@ function generateGlobalInjectorCode({ release }: { release: string }) {
334349
{};
335350
336351
_global.SENTRY_RELEASE={id:"${release}"};`;
352+
353+
if (injectReleasesMap && project) {
354+
const key = org ? `${project}@${org}` : project;
355+
code += `
356+
_global.SENTRY_RELEASES=_global.SENTRY_RELEASES || {};
357+
_global.SENTRY_RELEASES["${key}"]={id:"${release}"};
358+
`;
359+
}
360+
361+
return code;
337362
}
338363

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

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type RequiredInternalOptions = Required<
1515
| "silent"
1616
| "cleanArtifacts"
1717
| "telemetry"
18+
| "injectReleasesMap"
1819
>
1920
>;
2021

@@ -100,6 +101,7 @@ export function normalizeUserOptions(userOptions: UserOptions): InternalOptions
100101
entries,
101102
include,
102103
configFile: userOptions.configFile,
104+
injectReleasesMap: userOptions.injectReleasesMap ?? false,
103105
};
104106
}
105107

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,15 @@ export type Options = Omit<IncludeEntry, "paths"> & {
179179
* defaults from ~/.sentryclirc are always loaded
180180
*/
181181
configFile?: string;
182+
183+
/**
184+
* If set to true, the plugin will inject an additional `SENTRY_RELEASES` variable that
185+
* maps from `{org}@{project}` to the `release` value. This might be helpful for webpack
186+
* module federation or micro frontend setups.
187+
*
188+
* Defaults to `false`
189+
*/
190+
injectReleasesMap?: boolean;
182191
};
183192

184193
export type IncludeEntry = {

packages/bundler-plugin-core/test/option-mappings.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe("normalizeUserOptions()", () => {
3535
telemetry: true,
3636
url: "https://sentry.io/",
3737
vcsRemote: "origin",
38+
injectReleasesMap: false,
3839
});
3940
});
4041

@@ -78,6 +79,7 @@ describe("normalizeUserOptions()", () => {
7879
telemetry: true,
7980
url: "https://sentry.io/",
8081
vcsRemote: "origin",
82+
injectReleasesMap: false,
8183
});
8284
});
8385
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
2+
process.stdout.write(global.SENTRY_RELEASES["releasesProject@releasesOrg"].id.toString());
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import childProcess from "child_process";
2+
import path from "path";
3+
4+
/**
5+
* Runs a node file in a seprate process.
6+
*
7+
* @param bundlePath Path of node file to run
8+
* @returns Stdout of the process
9+
*/
10+
function checkBundle(bundlePath: string): void {
11+
const processOutput = childProcess.execSync(`node ${bundlePath}`, { encoding: "utf-8" });
12+
expect(processOutput).toBe("I AM A RELEASE!");
13+
}
14+
15+
test("esbuild bundle", () => {
16+
expect.assertions(1);
17+
checkBundle(path.join(__dirname, "./out/esbuild/index.js"));
18+
});
19+
20+
test("rollup bundle", () => {
21+
expect.assertions(1);
22+
checkBundle(path.join(__dirname, "./out/rollup/index.js"));
23+
});
24+
25+
test("vite bundle", () => {
26+
expect.assertions(1);
27+
checkBundle(path.join(__dirname, "./out/vite/index.js"));
28+
});
29+
30+
test("webpack 4 bundle", () => {
31+
expect.assertions(1);
32+
checkBundle(path.join(__dirname, "./out/webpack4/index.js"));
33+
});
34+
35+
test("webpack 5 bundle", () => {
36+
expect.assertions(1);
37+
checkBundle(path.join(__dirname, "./out/webpack5/index.js"));
38+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Options } from "@sentry/bundler-plugin-core";
2+
import * as path from "path";
3+
import { createCjsBundles } from "../../utils/create-cjs-bundles";
4+
5+
const entryPointPath = path.resolve(__dirname, "./input/entrypoint.js");
6+
const outputDir = path.resolve(__dirname, "./out");
7+
8+
createCjsBundles({ index: entryPointPath }, outputDir, {
9+
release: "I AM A RELEASE!",
10+
project: "releasesProject",
11+
org: "releasesOrg",
12+
include: outputDir,
13+
dryRun: true,
14+
injectReleasesMap: true,
15+
} as Options);

0 commit comments

Comments
 (0)