From e53db48a73bf2d049a932385181bace9f802ae66 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Fri, 9 Oct 2026 11:36:24 +0200 Subject: [PATCH 1/2] feat(core): Support applicationKey and moduleMetadata in Metro config Inject the module metadata of @sentry/bundler-plugins as a virtual premodule, so thirdPartyErrorFilterIntegration can find the frames of the application. Export thirdPartyErrorFilterIntegration. Fixes #6858 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 ++ packages/core/etc/sentry-react-native.api.md | 3 + packages/core/src/js/integrations/exports.ts | 1 + packages/core/src/js/tools/metroconfig.ts | 36 ++++++++-- .../src/js/tools/sentryMetroSerializer.ts | 38 +++++++++- packages/core/src/js/tools/utils.ts | 29 ++++++++ packages/core/test/tools/metroconfig.test.ts | 46 ++++++++++++ .../test/tools/sentryMetroSerializer.test.ts | 70 ++++++++++++++++++- packages/core/test/tools/utils.test.ts | 20 +++++- 9 files changed, 239 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b79ca1c8..cbe368dbef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Features + +- Add the `applicationKey` and `moduleMetadata` options to `withSentryConfig` and `getSentryExpoConfig`, and export `thirdPartyErrorFilterIntegration` ([#6863](https://github.com/getsentry/sentry-react-native/pull/6863)) + - Set the same key in `applicationKey` and in the `filterKeys` option of `thirdPartyErrorFilterIntegration` to filter errors from third-party code, for example browser extensions on Expo web. + ## 8.30.0 ### Features diff --git a/packages/core/etc/sentry-react-native.api.md b/packages/core/etc/sentry-react-native.api.md index 39d8eaee6e..2d2506ce89 100644 --- a/packages/core/etc/sentry-react-native.api.md +++ b/packages/core/etc/sentry-react-native.api.md @@ -106,6 +106,7 @@ import { startSpanManual } from '@sentry/core'; import type { StartSpanOptions } from '@sentry/core'; import { suppressTracing } from '@sentry/core'; import type { TextStyle } from 'react-native'; +import { thirdPartyErrorFilterIntegration } from '@sentry/react'; import { Thread } from '@sentry/core'; import { TransactionEvent } from '@sentry/core'; import type { TransportMakeRequestResponse } from '@sentry/core'; @@ -1175,6 +1176,8 @@ export type SupabaseReactNativeIntegrationOptions = { export { suppressTracing } +export { thirdPartyErrorFilterIntegration } + export { Thread } // @public (undocumented) diff --git a/packages/core/src/js/integrations/exports.ts b/packages/core/src/js/integrations/exports.ts index de88f1e7bb..0d6b223cc5 100644 --- a/packages/core/src/js/integrations/exports.ts +++ b/packages/core/src/js/integrations/exports.ts @@ -58,4 +58,5 @@ export { linkedErrorsIntegration as browserLinkedErrorsIntegration, rewriteFramesIntegration, extraErrorDataIntegration, + thirdPartyErrorFilterIntegration, } from '@sentry/react'; diff --git a/packages/core/src/js/tools/metroconfig.ts b/packages/core/src/js/tools/metroconfig.ts index 34464203fc..aab44dbf08 100644 --- a/packages/core/src/js/tools/metroconfig.ts +++ b/packages/core/src/js/tools/metroconfig.ts @@ -16,9 +16,14 @@ import { setSentryDefaultBabelTransformerPathEnv, } from './sentryBabelTransformerUtils'; import { checkSentryExpoNativeProject } from './sentryExpoNativeCheck'; -import { createSentryMetroSerializer, unstableBeforeAssetSerializationDebugIdPlugin } from './sentryMetroSerializer'; +import { + createSentryMetroSerializer, + unstableBeforeAssetSerializationDebugIdPlugin, + unstableBeforeAssetSerializationModuleMetadataPlugin, +} from './sentryMetroSerializer'; import { withSentryOptionsFromFile } from './sentryOptionsSerializer'; import { unstableReleaseConstantsPlugin } from './sentryReleaseInjector'; +import { buildModuleMetadata } from './utils'; export * from './sentryMetroSerializer'; @@ -88,6 +93,21 @@ export interface SentryMetroConfigOptions { * @default false */ autoWrapExpoRouterErrorBoundary?: boolean; + /** + * A key that identifies the code of your application. + * Use the same key in the `filterKeys` option of `thirdPartyErrorFilterIntegration` + * to filter errors that come from third-party code, for example browser extensions. + * + * This is the same option as `applicationKey` of the Sentry bundler plugins. + */ + applicationKey?: string; + /** + * Metadata to attach to the stack frames of your application code. + * The SDK adds the metadata to the `module_metadata` field of each stack frame from your bundle. + * + * This is the same option as the object form of `moduleMetadata` of the Sentry bundler plugins. + */ + moduleMetadata?: Record; } export interface SentryExpoConfigOptions { @@ -119,13 +139,15 @@ export function withSentryConfig( enableSourceContextInDevelopment = true, optionsFile = true, autoWrapExpoRouterErrorBoundary = false, + applicationKey, + moduleMetadata, }: SentryMetroConfigOptions = {}, ): MetroConfig { setSentryMetroDevServerEnvFlag(); let newConfig = config; - newConfig = withSentryDebugId(newConfig); + newConfig = withSentryDebugId(newConfig, buildModuleMetadata({ applicationKey, moduleMetadata })); newConfig = withSentryFramesCollapsed(newConfig); if (annotateReactComponents || autoWrapExpoRouterErrorBoundary) { newConfig = withSentryBabelTransformer(newConfig, annotateReactComponents, autoWrapExpoRouterErrorBoundary); @@ -159,11 +181,13 @@ export function getSentryExpoConfig( checkSentryExpoNativeProject(projectRoot); const getDefaultConfig = options.getDefaultConfig || loadExpoMetroConfigModule().getDefaultConfig; + const moduleMetadata = buildModuleMetadata(options); const config = getDefaultConfig(projectRoot, { ...options, unstable_beforeAssetSerializationPlugins: [ ...(options.unstable_beforeAssetSerializationPlugins || []), ...((options.injectReleaseForWeb ?? true) ? [unstableReleaseConstantsPlugin(projectRoot)] : []), + ...(moduleMetadata ? [unstableBeforeAssetSerializationModuleMetadataPlugin(moduleMetadata)] : []), unstableBeforeAssetSerializationDebugIdPlugin, ], }); @@ -258,10 +282,10 @@ export function withSentryBabelTransformer( }; } -function withSentryDebugId(config: MetroConfig): MetroConfig { - const customSerializer = createSentryMetroSerializer( - config.serializer?.customSerializer || undefined, - ) as MetroCustomSerializer; +function withSentryDebugId(config: MetroConfig, moduleMetadata?: Record): MetroConfig { + const customSerializer = createSentryMetroSerializer(config.serializer?.customSerializer || undefined, { + moduleMetadata, + }) as MetroCustomSerializer; // MetroConfig types customSerializers as async only, but sync returns are also supported // The default serializer is sync diff --git a/packages/core/src/js/tools/sentryMetroSerializer.ts b/packages/core/src/js/tools/sentryMetroSerializer.ts index 424ff2d8de..bba8f003f3 100644 --- a/packages/core/src/js/tools/sentryMetroSerializer.ts +++ b/packages/core/src/js/tools/sentryMetroSerializer.ts @@ -6,6 +6,7 @@ import type { Bundle, MetroSerializer, SerializedBundle, VirtualJSOutput } from import { createDebugIdSnippet, + createModuleMetadataSnippet, createVirtualJSModule, determineDebugIdFromBundleSource, prependModule, @@ -17,6 +18,7 @@ type SourceMap = Record; const DEBUG_ID_PLACE_HOLDER = '__debug_id_place_holder__'; const DEBUG_ID_MODULE_PATH = '__debugid__'; +const MODULE_METADATA_MODULE_PATH = '__sentry_module_metadata__'; const SOURCE_MAP_COMMENT = '//# sourceMappingURL='; const DEBUG_ID_COMMENT = '//# debugId='; @@ -47,15 +49,34 @@ export function unstableBeforeAssetSerializationDebugIdPlugin({ return prependModule(premodules, debugIdModule); } +/** + * Creates a plugin that adds the Sentry module metadata module to each bundle chunk. + * `thirdPartyErrorFilterIntegration` uses this metadata to find the frames of the application. + */ +export function unstableBeforeAssetSerializationModuleMetadataPlugin( + moduleMetadata: Record, +): (serializationInput: { graph: ReadOnlyGraph; premodules: Module[]; debugId?: string }) => Module[] { + return ({ premodules }) => addModuleMetadataModule(premodules, moduleMetadata); +} + /** * Creates a Metro serializer that adds Debug ID module to the plain bundle. * The Debug ID module is a virtual module that provides a debug ID in runtime. * + * If `moduleMetadata` is set, it also adds the Sentry module metadata module, also to development bundles. + * * RAM Bundles do not support custom serializers. */ -export const createSentryMetroSerializer = (customSerializer?: MetroSerializer): MetroSerializer => { +export const createSentryMetroSerializer = ( + customSerializer?: MetroSerializer, + { moduleMetadata }: { moduleMetadata?: Record } = {}, +): MetroSerializer => { const serializer = customSerializer || createDefaultMetroSerializer(); - return async function (entryPoint, preModules, graph, options) { + return async function (entryPoint, originalPreModules, graph, options) { + const preModules = moduleMetadata + ? addModuleMetadataModule(originalPreModules, moduleMetadata) + : originalPreModules; + if ('hot' in graph.transformOptions ? graph.transformOptions.hot : graph.transformOptions.dev) { return serializer(entryPoint, preModules, graph, options); } @@ -169,6 +190,19 @@ function extractSerializerResult(serializerResult: unknown): SerializedBundle | return null; } +function addModuleMetadataModule( + premodules: readonly Module[], + moduleMetadata: Record, +): Module[] { + if (premodules.some(module => module.path === MODULE_METADATA_MODULE_PATH)) { + return [...premodules]; + } + return prependModule( + premodules, + createVirtualJSModule(MODULE_METADATA_MODULE_PATH, createModuleMetadataSnippet(moduleMetadata)), + ); +} + function createDebugIdModule(debugId: string): Module & { setSource: (code: string) => void } { return createVirtualJSModule(DEBUG_ID_MODULE_PATH, createDebugIdSnippet(debugId)); } diff --git a/packages/core/src/js/tools/utils.ts b/packages/core/src/js/tools/utils.ts index fe41ccd10f..8e9ff78400 100644 --- a/packages/core/src/js/tools/utils.ts +++ b/packages/core/src/js/tools/utils.ts @@ -46,6 +46,35 @@ export function createDebugIdSnippet(debugId: string): string { return `var _sentryDebugIds,_sentryDebugIdIdentifier;void 0===_sentryDebugIds&&(_sentryDebugIds={});try{var stack=(new Error).stack;stack&&(_sentryDebugIds[stack]="${debugId}",_sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}`; } +/** + * Returns the module metadata code snippet of `@sentry/bundler-plugins`. + * + * https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/packages/bundler-plugin-core/src/utils.ts + */ +export function createModuleMetadataSnippet(metadata: Record): string { + return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n; +}): Record | undefined { + const metadata: Record = { + ...(applicationKey ? { [`_sentryBundlerPluginAppKey:${applicationKey}`]: true } : {}), + ...moduleMetadata, + }; + return Object.keys(metadata).length > 0 ? metadata : undefined; +} + /** * Deterministically hashes a string and turns the hash into a uuid. * diff --git a/packages/core/test/tools/metroconfig.test.ts b/packages/core/test/tools/metroconfig.test.ts index bb6ebf6dd9..94b97f02a3 100644 --- a/packages/core/test/tools/metroconfig.test.ts +++ b/packages/core/test/tools/metroconfig.test.ts @@ -11,6 +11,7 @@ import { withSentryExcludeServerOnlyResolver, withSentryFeedbackResolver, withSentryFramesCollapsed, + withSentryConfig, withSentryResolver, } from '../../src/js/tools/metroconfig'; import { @@ -76,6 +77,51 @@ describe('metroconfig', () => { }); }); + describe('module metadata', () => { + const getPlugins = (options: Parameters[1]) => { + const getDefaultConfig = jest.fn().mockReturnValue({}); + getSentryExpoConfig('/project/root', { ...options, injectReleaseForWeb: false, getDefaultConfig }); + return getDefaultConfig.mock.calls[0][1].unstable_beforeAssetSerializationPlugins as ((input: { + graph: unknown; + premodules: Array<{ path: string; getSource: () => Buffer }>; + }) => Array<{ path: string; getSource: () => Buffer }>)[]; + }; + + test('getSentryExpoConfig adds module metadata plugin with application key', () => { + const plugins = getPlugins({ applicationKey: 'my-app', moduleMetadata: { team: 'mobile' } }); + + expect(plugins).toHaveLength(2); + const premodules = plugins[0]?.({ graph: {}, premodules: [] }) ?? []; + expect(premodules.map(module => module.getSource().toString())).toEqual([ + expect.stringContaining('{"_sentryBundlerPluginAppKey:my-app":true,"team":"mobile"}'), + ]); + }); + + test('getSentryExpoConfig does not add module metadata plugin without application key and metadata', () => { + expect(getPlugins({})).toHaveLength(1); + }); + + test('withSentryConfig adds module metadata module to the serializer premodules', async () => { + const customSerializer = jest.fn().mockReturnValue(''); + const config = withSentryConfig( + { serializer: { customSerializer } }, + { applicationKey: 'my-app', enableSourceContextInDevelopment: false, optionsFile: false }, + ); + + await config.serializer?.customSerializer?.( + 'index.js', + [], + { transformOptions: { hot: true } } as never, + {} as never, + ); + + const premodules = customSerializer.mock.calls[0][1] as Array<{ getSource: () => Buffer }>; + expect(premodules.map(module => module.getSource().toString())).toEqual([ + expect.stringContaining('{"_sentryBundlerPluginAppKey:my-app":true}'), + ]); + }); + }); + describe('withSentryFramesCollapsed', () => { test('adds customizeFrames if undefined ', () => { const config = withSentryFramesCollapsed({}); diff --git a/packages/core/test/tools/sentryMetroSerializer.test.ts b/packages/core/test/tools/sentryMetroSerializer.test.ts index 69a74bf3df..38971c309c 100644 --- a/packages/core/test/tools/sentryMetroSerializer.test.ts +++ b/packages/core/test/tools/sentryMetroSerializer.test.ts @@ -1,5 +1,6 @@ import type { MixedOutput, Module } from 'metro'; +import { generateModuleMetadataInjectorCode } from '@sentry/bundler-plugins/core'; import * as fs from 'fs'; import CountingSet from 'metro/private/lib/CountingSet'; import countLines from 'metro/private/lib/countLines'; @@ -7,8 +8,16 @@ import * as os from 'os'; import * as path from 'path'; import { minify } from 'uglify-js'; -import { createSentryMetroSerializer } from '../../src/js/tools/sentryMetroSerializer'; -import { createDebugIdSnippet, type MetroSerializer, type VirtualJSOutput } from '../../src/js/tools/utils'; +import { + createSentryMetroSerializer, + unstableBeforeAssetSerializationModuleMetadataPlugin, +} from '../../src/js/tools/sentryMetroSerializer'; +import { + createDebugIdSnippet, + createModuleMetadataSnippet, + type MetroSerializer, + type VirtualJSOutput, +} from '../../src/js/tools/utils'; describe('Sentry Metro Serializer', () => { test('debug id minified code snippet is the same as in the original implementation', () => { @@ -137,6 +146,63 @@ describe('Sentry Metro Serializer', () => { expect(bundle).toBe(serialAssets); }); + describe('module metadata', () => { + const moduleMetadata = { '_sentryBundlerPluginAppKey:my-app': true, team: 'mobile' }; + + test('module metadata code snippet is the same as in @sentry/bundler-plugins', () => { + expect(createModuleMetadataSnippet(moduleMetadata)).toEqual( + generateModuleMetadataInjectorCode(moduleMetadata).code(), + ); + }); + + test('adds module metadata module to the production bundle', async () => { + const serializer = createSentryMetroSerializer(undefined, { moduleMetadata }); + + const bundle = await serializer(...mockMinSerializerArgs()); + if (typeof bundle === 'string') { + fail('Expected bundle to be an object with a "code" property'); + } + + expect(bundle.code).toContain(createModuleMetadataSnippet(moduleMetadata)); + expect(determineDebugIdFromBundleSource(bundle.code)).toBeDefined(); + }); + + test('adds module metadata module to the development bundle', async () => { + const wrappedSerializer = jest.fn, Parameters>(() => ''); + const serializer = createSentryMetroSerializer(wrappedSerializer, { moduleMetadata }); + const args = mockMinSerializerArgs(); + args[2] = { ...args[2], transformOptions: { ...args[2].transformOptions, hot: true } }; + + await serializer(...args); + + const preModules = wrappedSerializer.mock.calls[0]?.[1]; + expect(preModules?.map(module => module.getSource().toString())).toEqual([ + createModuleMetadataSnippet(moduleMetadata), + ]); + }); + + test('does not add module metadata module without metadata', async () => { + const serializer = createSentryMetroSerializer(); + + const bundle = await serializer(...mockMinSerializerArgs()); + + expect(typeof bundle === 'string' ? bundle : bundle.code).not.toContain('_sentryModuleMetadata'); + }); + + test('before asset serialization plugin adds module metadata module once', () => { + const plugin = unstableBeforeAssetSerializationModuleMetadataPlugin(moduleMetadata); + const graph = mockMinSerializerArgs()[2]; + + const premodules = plugin({ graph, premodules: [] }); + const secondRunPremodules = plugin({ graph, premodules }); + + expect(premodules.map(module => module.getSource().toString())).toEqual([ + createModuleMetadataSnippet(moduleMetadata), + ]); + expect(secondRunPremodules).toHaveLength(1); + }); + }); + describe('calculateDebugId', () => { // We need to access the private function for testing const crypto = require('crypto'); diff --git a/packages/core/test/tools/utils.test.ts b/packages/core/test/tools/utils.test.ts index 3c78a1014d..b329fd25b5 100644 --- a/packages/core/test/tools/utils.test.ts +++ b/packages/core/test/tools/utils.test.ts @@ -4,7 +4,7 @@ import countLines from 'metro/private/lib/countLines'; import type { VirtualJSOutput } from '../../src/js/tools/utils'; -import { createSet, getExpoConfig, prependModule } from '../../src/js/tools/utils'; +import { buildModuleMetadata, createSet, getExpoConfig, prependModule } from '../../src/js/tools/utils'; const mockedExpoConfigRequire = jest.fn(); @@ -37,6 +37,24 @@ describe('Sentry Metro Tools Utils', () => { }); }); + describe('buildModuleMetadata', () => { + test('returns undefined without application key and module metadata', () => { + expect(buildModuleMetadata({})).toBeUndefined(); + expect(buildModuleMetadata({ moduleMetadata: {} })).toBeUndefined(); + }); + + test('adds the bundler plugin application key', () => { + expect(buildModuleMetadata({ applicationKey: 'my-app' })).toEqual({ '_sentryBundlerPluginAppKey:my-app': true }); + }); + + test('merges module metadata with the application key', () => { + expect(buildModuleMetadata({ applicationKey: 'my-app', moduleMetadata: { team: 'mobile' } })).toEqual({ + '_sentryBundlerPluginAppKey:my-app': true, + team: 'mobile', + }); + }); + }); + describe('getExpoConfig', () => { test('returns empty object if @expo/config is not available', () => { mockedExpoConfigRequire.mockImplementation(() => { From f0426cf400b1fb353bebe41cd7072fcc950b7fe8 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Fri, 9 Oct 2026 14:36:23 +0200 Subject: [PATCH 2/2] fix(core): Escape unsafe characters in the module metadata snippet Add a test that shows that thirdPartyErrorFilterIntegration applies the injected module metadata on ReactNativeClient. Co-Authored-By: Claude --- packages/core/src/js/tools/utils.ts | 21 ++++++- .../thirdPartyErrorFilter.test.ts | 56 +++++++++++++++++++ .../test/tools/sentryMetroSerializer.test.ts | 13 +++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 packages/core/test/integrations/thirdPartyErrorFilter.test.ts diff --git a/packages/core/src/js/tools/utils.ts b/packages/core/src/js/tools/utils.ts index 8e9ff78400..65751b7faf 100644 --- a/packages/core/src/js/tools/utils.ts +++ b/packages/core/src/js/tools/utils.ts @@ -52,11 +52,28 @@ export function createDebugIdSnippet(debugId: string): string { * https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/packages/bundler-plugin-core/src/utils.ts */ export function createModuleMetadataSnippet(metadata: Record): string { - return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n = { + '<': '\\u003C', + '>': '\\u003E', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\0': '\\0', + '\u2028': '\\u2028', + '\u2029': '\\u2029', +}; + +function escapeUnsafeChars(json: string): string { + return json.replace(/[<>\b\f\n\r\t\0\u2028\u2029]/g, char => UNSAFE_CHARS[char] ?? char); +} + /** * Builds the module metadata in the same way as `@sentry/bundler-plugins`. * Returns `undefined` if there is no metadata to inject. diff --git a/packages/core/test/integrations/thirdPartyErrorFilter.test.ts b/packages/core/test/integrations/thirdPartyErrorFilter.test.ts new file mode 100644 index 0000000000..c5fe6ce2cc --- /dev/null +++ b/packages/core/test/integrations/thirdPartyErrorFilter.test.ts @@ -0,0 +1,56 @@ +import type { Event } from '@sentry/core'; + +import { defaultStackParser } from '@sentry/browser'; +import { GLOBAL_OBJ, Scope } from '@sentry/core'; + +import { ReactNativeClient } from '../../src/js/client'; +import { thirdPartyErrorFilterIntegration } from '../../src/js/index'; + +jest.mock('../../src/js/wrapper', () => jest.requireActual('../mockWrapper')); + +describe('thirdPartyErrorFilterIntegration', () => { + beforeEach(() => { + (GLOBAL_OBJ as { _sentryModuleMetadata?: Record })._sentryModuleMetadata = { + [new Error().stack as string]: { '_sentryBundlerPluginAppKey:my-app': true }, + }; + }); + + afterEach(() => { + delete (GLOBAL_OBJ as { _sentryModuleMetadata?: unknown })._sentryModuleMetadata; + }); + + it('keeps errors from application code with the injected module metadata', async () => { + const beforeSend = await captureError(['my-app']); + + expect(beforeSend).toHaveBeenCalledTimes(1); + }); + + it('drops errors without frames from application code', async () => { + const beforeSend = await captureError(['other-app']); + + expect(beforeSend).not.toHaveBeenCalled(); + }); +}); + +async function captureError(filterKeys: string[]): Promise { + const beforeSend = jest.fn((event: Event) => event); + const client = new ReactNativeClient({ + dsn: 'https://public@sentry.example.com/1', + enableNative: false, + stackParser: defaultStackParser, + integrations: [ + thirdPartyErrorFilterIntegration({ + filterKeys, + behaviour: 'drop-error-if-exclusively-contains-third-party-frames', + }), + ], + transport: () => ({ send: jest.fn().mockResolvedValue({}), flush: jest.fn().mockResolvedValue(true) }), + beforeSend, + }); + client.init(); + + client.captureException(new Error('test'), undefined, new Scope()); + await client.flush(); + + return beforeSend; +} diff --git a/packages/core/test/tools/sentryMetroSerializer.test.ts b/packages/core/test/tools/sentryMetroSerializer.test.ts index 38971c309c..4f37e07236 100644 --- a/packages/core/test/tools/sentryMetroSerializer.test.ts +++ b/packages/core/test/tools/sentryMetroSerializer.test.ts @@ -7,6 +7,7 @@ import countLines from 'metro/private/lib/countLines'; import * as os from 'os'; import * as path from 'path'; import { minify } from 'uglify-js'; +import * as vm from 'vm'; import { createSentryMetroSerializer, @@ -155,6 +156,18 @@ describe('Sentry Metro Serializer', () => { ); }); + test('module metadata code snippet escapes characters that can break the bundle', () => { + const unsafeMetadata = { value: '\u2028\u2029' }; + const snippet = createModuleMetadataSnippet(unsafeMetadata); + const globalObject: { Error: ErrorConstructor; _sentryModuleMetadata?: Record } = { Error }; + + vm.runInNewContext(snippet, { window: globalObject }); + + expect(snippet).not.toContain(''); + expect(snippet).not.toMatch(/[\u2028\u2029]/); + expect(Object.values(globalObject._sentryModuleMetadata ?? {})).toEqual([unsafeMetadata]); + }); + test('adds module metadata module to the production bundle', async () => { const serializer = createSentryMetroSerializer(undefined, { moduleMetadata });