diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1c89518..7e1fce55c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Features + +- Overwrite Expo bundle names in stack frames ([#3115])(https://github.com/getsentry/sentry-react-native/pull/3115) + - This enables source maps to resolve correctly without using `sentry-expo` package + ### Fixes - Disable `enableNative` if Native SDK is not available ([#3099](https://github.com/getsentry/sentry-react-native/pull/3099)) diff --git a/src/js/integrations/rewriteframes.ts b/src/js/integrations/rewriteframes.ts index 4b2984c30..7fbf250e9 100644 --- a/src/js/integrations/rewriteframes.ts +++ b/src/js/integrations/rewriteframes.ts @@ -1,28 +1,50 @@ import { RewriteFrames } from '@sentry/integrations'; import type { StackFrame } from '@sentry/types'; +import { Platform } from 'react-native'; + +import { isExpo } from '../utils/environment'; + +const ANDROID_DEFAULT_BUNDLE_NAME = 'app:///index.android.bundle'; +const IOS_DEFAULT_BUNDLE_NAME = 'app:///main.jsbundle'; /** * Creates React Native default rewrite frames integration * which appends app:// to the beginning of the filename - * and removes file://, 'address at' prefixes and CodePush postfix. + * and removes file://, 'address at' prefixes, CodePush postfix, + * and Expo bundle postfix. */ export function createReactNativeRewriteFrames(): RewriteFrames { return new RewriteFrames({ iteratee: (frame: StackFrame) => { - if (frame.filename) { - frame.filename = frame.filename - .replace(/^file:\/\//, '') - .replace(/^address at /, '') - .replace(/^.*\/[^.]+(\.app|CodePush|.*(?=\/))/, ''); - - if (frame.filename !== '[native code]' && frame.filename !== 'native') { - const appPrefix = 'app://'; - // We always want to have a triple slash - frame.filename = - frame.filename.indexOf('/') === 0 ? `${appPrefix}${frame.filename}` : `${appPrefix}/${frame.filename}`; - } - delete frame.abs_path; + if (!frame.filename) { + return frame; + } + delete frame.abs_path; + + frame.filename = frame.filename + .replace(/^file:\/\//, '') + .replace(/^address at /, '') + .replace(/^.*\/[^.]+(\.app|CodePush|.*(?=\/))/, ''); + + if (frame.filename === '[native code]' || frame.filename === 'native') { + return frame; } + + // Expo adds hash to the end of bundle names + if (isExpo() && Platform.OS === 'android') { + frame.filename = ANDROID_DEFAULT_BUNDLE_NAME; + return frame; + } + + if (isExpo() && Platform.OS === 'ios') { + frame.filename = IOS_DEFAULT_BUNDLE_NAME; + return frame; + } + + const appPrefix = 'app://'; + // We always want to have a triple slash + frame.filename = + frame.filename.indexOf('/') === 0 ? `${appPrefix}${frame.filename}` : `${appPrefix}/${frame.filename}`; return frame; }, }); diff --git a/test/integrations/rewriteframes.test.ts b/test/integrations/rewriteframes.test.ts index 4224fed08..45f7ddace 100644 --- a/test/integrations/rewriteframes.test.ts +++ b/test/integrations/rewriteframes.test.ts @@ -1,7 +1,13 @@ import type { Exception } from '@sentry/browser'; import { defaultStackParser, eventFromException } from '@sentry/browser'; +import { Platform } from 'react-native'; import { createReactNativeRewriteFrames } from '../../src/js/integrations/rewriteframes'; +import { isExpo } from '../../src/js/utils/environment'; +import { mockFunction } from '../testutils'; + +jest.mock('../../src/js/utils/environment'); +jest.mock('react-native', () => ({ Platform: { OS: 'ios' } })); describe('RewriteFrames', () => { const HINT = {}; @@ -20,6 +26,11 @@ describe('RewriteFrames', () => { return exception; }; + beforeEach(() => { + mockFunction(isExpo).mockReturnValue(false); + jest.resetAllMocks(); + }); + it('should parse exceptions for react-native-v8', async () => { const REACT_NATIVE_V8_EXCEPTION = { message: 'Manually triggered crash to test Sentry reporting', @@ -98,7 +109,10 @@ describe('RewriteFrames', () => { }); }); - it('should parse exceptions for react-native Expo bundles', async () => { + it('should parse exceptions for react-native Expo bundles on ios', async () => { + mockFunction(isExpo).mockReturnValue(true); + Platform.OS = 'ios'; + const REACT_NATIVE_EXPO_EXCEPTION = { message: 'Test Error Expo', name: 'Error', @@ -121,28 +135,86 @@ describe('RewriteFrames', () => { frames: [ { filename: '[native code]', function: 'forEach', in_app: true }, { - filename: 'app:///bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', + filename: 'app:///main.jsbundle', function: 'p', lineno: 96, colno: 385, in_app: true, }, { - filename: 'app:///bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', + filename: 'app:///main.jsbundle', function: 'onResponderRelease', lineno: 221, colno: 5666, in_app: true, }, { - filename: 'app:///bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', + filename: 'app:///main.jsbundle', function: 'value', lineno: 221, colno: 7656, in_app: true, }, { - filename: 'app:///bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3', + filename: 'app:///main.jsbundle', + function: 'onPress', + lineno: 595, + colno: 658, + in_app: true, + }, + ], + }, + }); + }); + + it('should parse exceptions for react-native Expo bundles on android', async () => { + mockFunction(isExpo).mockReturnValue(true); + Platform.OS = 'android'; + + const REACT_NATIVE_EXPO_EXCEPTION = { + message: 'Test Error Expo', + name: 'Error', + stack: `onPress@/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3:595:658 + value@/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3:221:7656 + onResponderRelease@/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3:221:5666 + p@/data/user/0/com.sentrytest/files/.expo-internal/bundle-613EDD44F3305B9D75D4679663900F2BCDDDC326F247CA3202A3A4219FD412D3:96:385 + forEach@[native code]`, + }; + const exception = await exceptionFromError(REACT_NATIVE_EXPO_EXCEPTION); + + expect(exception).toEqual({ + value: 'Test Error Expo', + type: 'Error', + mechanism: { + handled: true, + type: 'generic', + }, + stacktrace: { + frames: [ + { filename: '[native code]', function: 'forEach', in_app: true }, + { + filename: 'app:///index.android.bundle', + function: 'p', + lineno: 96, + colno: 385, + in_app: true, + }, + { + filename: 'app:///index.android.bundle', + function: 'onResponderRelease', + lineno: 221, + colno: 5666, + in_app: true, + }, + { + filename: 'app:///index.android.bundle', + function: 'value', + lineno: 221, + colno: 7656, + in_app: true, + }, + { + filename: 'app:///index.android.bundle', function: 'onPress', lineno: 595, colno: 658,