From 1e19350e2c37fccdb11b4320bbb93ffeb900ee8c Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 13 Jun 2023 10:21:31 +0200 Subject: [PATCH 1/2] feat(expo): Add support for Expo hash named bundles in RewriteFrames --- CHANGELOG.md | 5 ++ src/js/integrations/rewriteframes.ts | 40 ++++++++---- test/integrations/rewriteframes.test.ts | 82 +++++++++++++++++++++++-- 3 files changed, 109 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b43a7a65a..ec8326d13 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 - Dynamically resolve `collectModulesScript` path to support monorepos ([#3092])(https://github.com/getsentry/sentry-react-native/pull/3092) diff --git a/src/js/integrations/rewriteframes.ts b/src/js/integrations/rewriteframes.ts index 4b2984c30..eb78fd801 100644 --- a/src/js/integrations/rewriteframes.ts +++ b/src/js/integrations/rewriteframes.ts @@ -1,28 +1,42 @@ import { RewriteFrames } from '@sentry/integrations'; import type { StackFrame } from '@sentry/types'; +import { Platform } from 'react-native'; + +import { isExpo } from '../utils/environment'; /** * 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) { + 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') { - 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 === '[native code]' || frame.filename === 'native') { + return frame; } + + // Expo adds hash to the end of bundle names + if (isExpo()) { + frame.filename = Platform.OS === 'android' ? 'app:///index.android.bundle' : 'app:///main.jsbundle'; + 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, From 93f8b9f0787bc14c73d3f1963363a33179e9d8df Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 13 Jun 2023 13:37:41 +0200 Subject: [PATCH 2/2] Only update bundle filename for android and ios --- src/js/integrations/rewriteframes.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/js/integrations/rewriteframes.ts b/src/js/integrations/rewriteframes.ts index eb78fd801..7fbf250e9 100644 --- a/src/js/integrations/rewriteframes.ts +++ b/src/js/integrations/rewriteframes.ts @@ -4,6 +4,9 @@ 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 @@ -28,8 +31,13 @@ export function createReactNativeRewriteFrames(): RewriteFrames { } // Expo adds hash to the end of bundle names - if (isExpo()) { - frame.filename = Platform.OS === 'android' ? 'app:///index.android.bundle' : 'app:///main.jsbundle'; + 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; }