diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index 8799f1d8..a04b03fa 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -224,8 +224,19 @@ export function sentryUnpluginFactory({ plugins.push(releaseInjectionPlugin(injectionCode)); } - if (options.moduleMetadata) { - let metadata: Record; + if (options.moduleMetadata || options.applicationKey) { + let metadata: Record = {}; + + if (options.applicationKey) { + // We use different keys so that if user-code receives multiple bundling passes, we will store the application keys of all the passes. + // It is a bit unfortunate that we have to inject the metadata snippet at the top, because after multiple + // injections, the first injection will always "win" because it comes last in the code. We would generally be + // fine with making the last bundling pass win. But because it cannot win, we have to use a workaround of storing + // the app keys in different object keys. + // We can simply use the `_sentryBundlerPluginAppKey:` to filter for app keys in the SDK. + metadata[`_sentryBundlerPluginAppKey:${options.applicationKey}`] = true; + } + if (typeof options.moduleMetadata === "function") { const args = { org: options.org, @@ -233,10 +244,10 @@ export function sentryUnpluginFactory({ release: options.release.name, }; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - metadata = options.moduleMetadata(args); + metadata = { ...metadata, ...options.moduleMetadata(args) }; } else { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - metadata = options.moduleMetadata; + metadata = { ...metadata, ...options.moduleMetadata }; } const injectionCode = generateModuleMetadataInjectorCode(metadata); diff --git a/packages/bundler-plugin-core/src/options-mapping.ts b/packages/bundler-plugin-core/src/options-mapping.ts index 86f4e3c4..3a141378 100644 --- a/packages/bundler-plugin-core/src/options-mapping.ts +++ b/packages/bundler-plugin-core/src/options-mapping.ts @@ -34,6 +34,7 @@ export function normalizeUserOptions(userOptions: UserOptions) { metaFramework: userOptions._metaOptions?.telemetry?.metaFramework, }, }, + applicationKey: userOptions.applicationKey, moduleMetadata: userOptions.moduleMetadata || userOptions._experiments?.moduleMetadata, _experiments: userOptions._experiments ?? {}, }; diff --git a/packages/bundler-plugin-core/src/types.ts b/packages/bundler-plugin-core/src/types.ts index 05cc6f88..3cac126d 100644 --- a/packages/bundler-plugin-core/src/types.ts +++ b/packages/bundler-plugin-core/src/types.ts @@ -312,6 +312,12 @@ export interface Options { // eslint-disable-next-line @typescript-eslint/no-explicit-any moduleMetadata?: ModuleMetadata | ModuleMetadataCallback; + /** + * A key which will embedded in all the bundled files. The SDK will be able to use the key to apply filtering + * rules, for example using the `thirdPartyErrorFilterIntegration`. + */ + applicationKey?: string; + /** * Options that are considered experimental and subject to change. * diff --git a/packages/integration-tests/fixtures/application-key-injection/input/bundle.js b/packages/integration-tests/fixtures/application-key-injection/input/bundle.js new file mode 100644 index 00000000..bf138963 --- /dev/null +++ b/packages/integration-tests/fixtures/application-key-injection/input/bundle.js @@ -0,0 +1,3 @@ +// Simply output the metadata to the console so it can be checked in a test +// eslint-disable-next-line no-console +console.log(JSON.stringify(global._sentryModuleMetadata)); diff --git a/packages/integration-tests/fixtures/application-key-injection/metadata-injection.test.ts b/packages/integration-tests/fixtures/application-key-injection/metadata-injection.test.ts new file mode 100644 index 00000000..85fc09de --- /dev/null +++ b/packages/integration-tests/fixtures/application-key-injection/metadata-injection.test.ts @@ -0,0 +1,38 @@ +/* eslint-disable jest/no-standalone-expect */ +/* eslint-disable jest/expect-expect */ +import { execSync } from "child_process"; +import path from "path"; +import { testIfNodeMajorVersionIsLessThan18 } from "../../utils/testIf"; + +function checkBundle(bundlePath: string): void { + const output = execSync(`node ${bundlePath}`, { encoding: "utf-8" }); + + const map = JSON.parse(output) as Record; + + // There should be only one key in the map + expect(Object.values(map)).toHaveLength(1); + // The value should be the expected metadata + expect(Object.values(map)).toEqual([{ ["_sentryBundlerPluginAppKey:my-app"]: true }]); +} + +describe("appKey injection", () => { + testIfNodeMajorVersionIsLessThan18("webpack 4 bundle", () => { + checkBundle(path.join(__dirname, "out", "webpack4", "bundle.js")); + }); + + test("webpack 5 bundle", () => { + checkBundle(path.join(__dirname, "out", "webpack5", "bundle.js")); + }); + + test("esbuild bundle", () => { + checkBundle(path.join(__dirname, "out", "esbuild", "bundle.js")); + }); + + test("rollup bundle", () => { + checkBundle(path.join(__dirname, "out", "rollup", "bundle.js")); + }); + + test("vite bundle", () => { + checkBundle(path.join(__dirname, "out", "vite", "bundle.js")); + }); +}); diff --git a/packages/integration-tests/fixtures/application-key-injection/setup.ts b/packages/integration-tests/fixtures/application-key-injection/setup.ts new file mode 100644 index 00000000..a745efa2 --- /dev/null +++ b/packages/integration-tests/fixtures/application-key-injection/setup.ts @@ -0,0 +1,15 @@ +import * as path from "path"; +import { createCjsBundles } from "../../utils/create-cjs-bundles"; + +const outputDir = path.resolve(__dirname, "out"); + +createCjsBundles( + { + bundle: path.resolve(__dirname, "input", "bundle.js"), + }, + outputDir, + { + applicationKey: "my-app", + }, + ["webpack4", "webpack5", "esbuild", "rollup", "vite"] +);