diff --git a/CHANGELOG.md b/CHANGELOG.md index e64a69dd41..fda3b0a184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Fixes +- Recover missing JS stack traces from native `JavascriptException` on Android ([#5964](https://github.com/getsentry/sentry-react-native/pull/5964)) - Lazy-load Metro internal modules to prevent Expo 55 import errors ([#5958](https://github.com/getsentry/sentry-react-native/pull/5958)) ### Dependencies diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt index bfa1647cbc..305e42459f 100644 --- a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt @@ -176,7 +176,7 @@ class RNSentrySDKTest { } private fun verifyDefaults(actualOptions: SentryAndroidOptions) { - assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + assertFalse(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) assertEquals(RNSentryVersion.ANDROID_SDK_NAME, actualOptions.sdkVersion?.name) assertEquals( io.sentry.android.core.BuildConfig.VERSION_NAME, diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt index c60776c942..aa883f2264 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -4,6 +4,7 @@ import android.app.Activity import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.common.JavascriptException import io.sentry.Breadcrumb +import io.sentry.Hint import io.sentry.ILogger import io.sentry.SentryEvent import io.sentry.android.core.CurrentActivityHolder @@ -32,6 +33,7 @@ class RNSentryStartTest { MockitoAnnotations.openMocks(this) logger = mock(ILogger::class.java) activity = mock(Activity::class.java) + RNSentryJavascriptExceptionCache.clear() } @Test @@ -196,10 +198,37 @@ class RNSentryStartTest { } @Test - fun `the JavascriptException is added to the ignoredExceptionsForType list on with react defaults`() { + fun `the JavascriptException is not added to the ignoredExceptionsForType list`() { val actualOptions = SentryAndroidOptions() RNSentryStart.updateWithReactDefaults(actualOptions, activity) - assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + assertFalse(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + } + + @Test + fun `beforeSend caches JavascriptException stack and drops the event`() { + val options = SentryAndroidOptions() + RNSentryStart.updateWithReactFinals(options) + + val jsStackTrace = "TypeError: Cannot read property 'content' of undefined\n at UserMessage (index.android.bundle:1:5274251)" + val jsException = JavascriptException(jsStackTrace) + val event = SentryEvent(jsException) + + val result = options.beforeSend?.execute(event, Hint()) + + assertNull("JavascriptException event should be dropped", result) + val cached = RNSentryJavascriptExceptionCache.getAndClear() + assertEquals(jsStackTrace, cached) + } + + @Test + fun `beforeSend does not drop non-JavascriptException events`() { + val options = SentryAndroidOptions() + val event = SentryEvent().apply { sdk = SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, "1.0") } + + RNSentryStart.updateWithReactFinals(options) + val result = options.beforeSend?.execute(event, Hint()) + + assertNotNull("Non-JavascriptException event should not be dropped", result) } @Test diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryJavascriptExceptionCache.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryJavascriptExceptionCache.java new file mode 100644 index 0000000000..7e170c5e81 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryJavascriptExceptionCache.java @@ -0,0 +1,54 @@ +package io.sentry.react; + +import java.util.concurrent.atomic.AtomicReference; +import org.jetbrains.annotations.Nullable; + +/** + * Thread-safe cache for the last JavascriptException stack trace string. + * + *

When React Native throws a JavascriptException on Android, the native Sentry SDK intercepts it + * in beforeSend and caches the stack trace here. The JS side can then retrieve it to enrich error + * events that arrive without a stack trace. + */ +final class RNSentryJavascriptExceptionCache { + + private static final long TTL_MS = 5000; + + private static final AtomicReference cache = new AtomicReference<>(null); + + private RNSentryJavascriptExceptionCache() {} + + static void cache(@Nullable String jsStackTrace) { + if (jsStackTrace == null || jsStackTrace.isEmpty()) { + return; + } + cache.set(new CachedEntry(jsStackTrace, System.currentTimeMillis())); + } + + @Nullable + static String getAndClear() { + CachedEntry entry = cache.getAndSet(null); + if (entry == null) { + return null; + } + if (System.currentTimeMillis() - entry.timestampMs > TTL_MS) { + return null; + } + return entry.jsStackTrace; + } + + /** Clears the cache. Visible for testing. */ + static void clear() { + cache.set(null); + } + + private static final class CachedEntry { + final String jsStackTrace; + final long timestampMs; + + CachedEntry(String jsStackTrace, long timestampMs) { + this.jsStackTrace = jsStackTrace; + this.timestampMs = timestampMs; + } + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index ec1e75607a..161deb5b42 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -993,6 +993,10 @@ public String fetchNativePackageName() { return packageInfo.packageName; } + public String fetchCachedJavascriptExceptionStack() { + return RNSentryJavascriptExceptionCache.getAndClear(); + } + public void getDataFromUri(String uri, Promise promise) { try { Uri contentUri = Uri.parse(uri); diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java index 77851bf83b..c91a94d47b 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -297,10 +297,6 @@ static void updateWithReactDefaults( options.setTracesSampleRate(null); options.setTracesSampler(null); - // React native internally throws a JavascriptException. - // we want to ignore it on the native side to avoid sending it twice. - options.addIgnoredExceptionForType(JavascriptException.class); - setCurrentActivity(currentActivity); } @@ -312,6 +308,15 @@ static void updateWithReactFinals(@NotNull SentryAndroidOptions options) { BeforeSendCallback userBeforeSend = options.getBeforeSend(); options.setBeforeSend( (event, hint) -> { + // React Native internally throws a JavascriptException when a JS error occurs. + // We cache its stack trace (which may contain frames missing from the JS error) + // and drop the native event to avoid sending duplicates. + Throwable throwable = event.getThrowable(); + if (throwable instanceof JavascriptException) { + RNSentryJavascriptExceptionCache.cache(throwable.getMessage()); + return null; + } + setEventOriginTag(event); // Note: In Sentry Android SDK v7, native SDK packages/integrations are already // included in the SDK version set during initialization, so no need to copy them here. diff --git a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 9215c09c36..dd912adcac 100644 --- a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -173,6 +173,11 @@ public String fetchNativePackageName() { return this.impl.fetchNativePackageName(); } + @Override + public String fetchCachedJavascriptExceptionStack() { + return this.impl.fetchCachedJavascriptExceptionStack(); + } + @Override public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) { // Not used on Android diff --git a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 85fdb97a35..9908801cba 100644 --- a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -173,6 +173,11 @@ public String fetchNativePackageName() { return this.impl.fetchNativePackageName(); } + @ReactMethod(isBlockingSynchronousMethod = true) + public String fetchCachedJavascriptExceptionStack() { + return this.impl.fetchCachedJavascriptExceptionStack(); + } + @ReactMethod(isBlockingSynchronousMethod = true) public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) { // Not used on Android diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index a43557079c..a6ba069ddb 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -371,6 +371,12 @@ - (void)handleShakeDetected return packageName; } +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, fetchCachedJavascriptExceptionStack) +{ + // Android-only feature, iOS does not need this. + return nil; +} + RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD( NSDictionary *, fetchNativeStackFramesBy : (NSArray *)instructionsAddr) { diff --git a/packages/core/src/js/NativeRNSentry.ts b/packages/core/src/js/NativeRNSentry.ts index 0a4aff54b5..ceafebeb65 100644 --- a/packages/core/src/js/NativeRNSentry.ts +++ b/packages/core/src/js/NativeRNSentry.ts @@ -48,6 +48,7 @@ export interface Spec extends TurboModule { error?: string; }; fetchNativePackageName(): string | undefined | null; + fetchCachedJavascriptExceptionStack(): string | undefined | null; fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | undefined | null; initNativeReactNavigationNewFrameTracking(): Promise; captureReplay(isHardCrash: boolean): Promise; diff --git a/packages/core/src/js/integrations/default.ts b/packages/core/src/js/integrations/default.ts index 30dd3a156e..c12354f9cb 100644 --- a/packages/core/src/js/integrations/default.ts +++ b/packages/core/src/js/integrations/default.ts @@ -33,6 +33,7 @@ import { modulesLoaderIntegration, nativeLinkedErrorsIntegration, nativeReleaseIntegration, + nativeStackRecoveryIntegration, primitiveTagIntegration, reactNativeErrorHandlersIntegration, reactNativeInfoIntegration, @@ -62,6 +63,7 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ }), ); integrations.push(nativeLinkedErrorsIntegration()); + integrations.push(nativeStackRecoveryIntegration()); } else { integrations.push(browserApiErrorsIntegration()); integrations.push(browserGlobalHandlersIntegration()); diff --git a/packages/core/src/js/integrations/exports.ts b/packages/core/src/js/integrations/exports.ts index d4e80f8ef6..b210240fbd 100644 --- a/packages/core/src/js/integrations/exports.ts +++ b/packages/core/src/js/integrations/exports.ts @@ -2,6 +2,7 @@ export { debugSymbolicatorIntegration } from './debugsymbolicator'; export { deviceContextIntegration } from './devicecontext'; export { reactNativeErrorHandlersIntegration } from './reactnativeerrorhandlers'; export { nativeLinkedErrorsIntegration } from './nativelinkederrors'; +export { nativeStackRecoveryIntegration } from './nativestackrecovery'; export { nativeReleaseIntegration } from './release'; export { eventOriginIntegration } from './eventorigin'; export { sdkInfoIntegration } from './sdkinfo'; diff --git a/packages/core/src/js/integrations/nativestackrecovery.ts b/packages/core/src/js/integrations/nativestackrecovery.ts new file mode 100644 index 0000000000..b1dc383cec --- /dev/null +++ b/packages/core/src/js/integrations/nativestackrecovery.ts @@ -0,0 +1,61 @@ +import type { Client, Event, EventHint, Integration } from '@sentry/core'; + +import { debug, parseStackFrames } from '@sentry/core'; +import { Platform } from 'react-native'; + +import { notWeb } from '../utils/environment'; +import { NATIVE } from '../wrapper'; + +const INTEGRATION_NAME = 'NativeStackRecovery'; + +/** + * Recovers missing JS stack traces from the native JavascriptException cache. + * + * On Android, when a React render error occurs with Hermes, the JS error may arrive + * at the global error handler without a stack trace. However, the same error is caught + * by React Native's native layer as a JavascriptException which contains the full + * JS stack trace. This integration fetches that cached stack and attaches it to the event. + */ +export const nativeStackRecoveryIntegration = (): Integration => { + return { + name: INTEGRATION_NAME, + setupOnce: () => { + /* noop */ + }, + preprocessEvent: (event: Event, hint: EventHint, client: Client): void => preprocessEvent(event, hint, client), + }; +}; + +function isEnabled(): boolean { + return notWeb() && NATIVE.enableNative && Platform.OS === 'android'; +} + +function preprocessEvent(event: Event, _hint: EventHint, client: Client): void { + if (!isEnabled()) { + return; + } + + const primaryException = event.exception?.values?.[0]; + if (!primaryException) { + return; + } + + if (primaryException.stacktrace?.frames && primaryException.stacktrace.frames.length > 0) { + return; + } + + const cachedStack = NATIVE.fetchCachedJavascriptExceptionStack(); + if (!cachedStack) { + return; + } + + const parser = client.getOptions().stackParser; + const syntheticError = new Error(); + syntheticError.stack = cachedStack; + + const frames = parseStackFrames(parser, syntheticError); + if (frames.length > 0) { + primaryException.stacktrace = { frames }; + debug.log(`[${INTEGRATION_NAME}] Recovered ${frames.length} frames from native JavascriptException cache`); + } +} diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index 6a71e0ff6f..f7a780823e 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -123,6 +123,12 @@ interface SentryNativeWrapper { fetchNativePackageName(): string | null; + /** + * Fetches and clears the cached JavascriptException stack trace from the native side. + * Returns null if no cached stack is available or the cache has expired. + */ + fetchCachedJavascriptExceptionStack(): string | null; + /** * Fetches native stack frames and debug images for the instructions addresses. */ @@ -756,6 +762,17 @@ export const NATIVE: SentryNativeWrapper = { return RNSentry.fetchNativePackageName() || null; }, + fetchCachedJavascriptExceptionStack(): string | null { + if (!this.enableNative) { + return null; + } + if (!this._isModuleLoaded(RNSentry)) { + return null; + } + + return RNSentry.fetchCachedJavascriptExceptionStack() || null; + }, + fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | null { if (!this.enableNative) { return null; diff --git a/packages/core/test/integrations/integrationsexecutionorder.test.ts b/packages/core/test/integrations/integrationsexecutionorder.test.ts index df04a9327c..266acf3031 100644 --- a/packages/core/test/integrations/integrationsexecutionorder.test.ts +++ b/packages/core/test/integrations/integrationsexecutionorder.test.ts @@ -43,6 +43,24 @@ describe('Integration execution order', () => { expect(nativeLinkedErrors.preprocessEvent).toHaveBeenCalledBefore(rewriteFrames.processEvent!); expect(rewriteFrames.processEvent!).toHaveBeenCalledBefore(debugSymbolicator.processEvent!); }); + + it('NativeStackRecovery is before RewriteFrames', async () => { + // NativeStackRecovery has to process event before RewriteFrames + // otherwise recovered stack trace frames won't be rewritten + + const client = createTestClient(); + const { integrations } = client.getOptions(); + + const nativeStackRecovery = spyOnIntegrationById('NativeStackRecovery', integrations); + const rewriteFrames = spyOnIntegrationById('RewriteFrames', integrations); + + client.init(); + + client.captureException(new Error('test')); + await client.flush(); + + expect(nativeStackRecovery.preprocessEvent).toHaveBeenCalledBefore(rewriteFrames.processEvent!); + }); }); describe('web', () => { diff --git a/packages/core/test/integrations/nativestackrecovery.test.ts b/packages/core/test/integrations/nativestackrecovery.test.ts new file mode 100644 index 0000000000..a16ab0ca14 --- /dev/null +++ b/packages/core/test/integrations/nativestackrecovery.test.ts @@ -0,0 +1,216 @@ +jest.mock('../../src/js/utils/environment'); + +import type { Client, Event, EventHint } from '@sentry/core'; + +import { defaultStackParser } from '@sentry/browser'; +import { Platform } from 'react-native'; + +import { nativeStackRecoveryIntegration } from '../../src/js/integrations/nativestackrecovery'; +import { notWeb } from '../../src/js/utils/environment'; +import { NATIVE } from '../../src/js/wrapper'; + +jest.mock('../../src/js/wrapper'); + +function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint): Event { + const mockedClient = { + getOptions: () => ({ stackParser: defaultStackParser }), + } as unknown as Client; + + const integration = nativeStackRecoveryIntegration(); + integration.preprocessEvent!(mockedEvent, mockedHint, mockedClient); + return mockedEvent; +} + +describe('NativeStackRecovery', () => { + let originalPlatformOS: typeof Platform.OS; + + beforeEach(() => { + jest.clearAllMocks(); + originalPlatformOS = Platform.OS; + Platform.OS = 'android'; + (notWeb as jest.Mock).mockReturnValue(true); + (NATIVE as any).enableNative = true; + }); + + afterEach(() => { + Platform.OS = originalPlatformOS; + }); + + it('does nothing when native is disabled', () => { + (NATIVE as any).enableNative = false; + (NATIVE.fetchCachedJavascriptExceptionStack as jest.Mock).mockReturnValue('Error: test\n at foo (file.js:1:1)'); + + const event = executeIntegrationFor( + { + exception: { + values: [{ type: 'Error', value: 'test' }], + }, + }, + {}, + ); + + expect(NATIVE.fetchCachedJavascriptExceptionStack).not.toHaveBeenCalled(); + expect(event.exception?.values?.[0].stacktrace).toBeUndefined(); + }); + + it('does nothing on non-android platforms', () => { + Platform.OS = 'ios'; + (NATIVE.fetchCachedJavascriptExceptionStack as jest.Mock).mockReturnValue('Error: test\n at foo (file.js:1:1)'); + + const event = executeIntegrationFor( + { + exception: { + values: [{ type: 'Error', value: 'test' }], + }, + }, + {}, + ); + + expect(NATIVE.fetchCachedJavascriptExceptionStack).not.toHaveBeenCalled(); + expect(event.exception?.values?.[0].stacktrace).toBeUndefined(); + }); + + it('does nothing when the event already has stacktrace frames', () => { + const event = executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'test', + stacktrace: { + frames: [ + { + filename: 'app.js', + function: 'myFunc', + lineno: 10, + colno: 5, + in_app: true, + }, + ], + }, + }, + ], + }, + }, + {}, + ); + + expect(NATIVE.fetchCachedJavascriptExceptionStack).not.toHaveBeenCalled(); + expect(event.exception?.values?.[0].stacktrace?.frames).toHaveLength(1); + }); + + it('does nothing when no cached stack is available', () => { + (NATIVE.fetchCachedJavascriptExceptionStack as jest.Mock).mockReturnValue(null); + + const event = executeIntegrationFor( + { + exception: { + values: [{ type: 'Error', value: 'test' }], + }, + }, + {}, + ); + + expect(NATIVE.fetchCachedJavascriptExceptionStack).toHaveBeenCalledTimes(1); + expect(event.exception?.values?.[0].stacktrace).toBeUndefined(); + }); + + it('parses and attaches frames when cached stack is available and event has no frames', () => { + const cachedStack = [ + 'Error: Value is undefined, expected an Object', + ' at UserMessage (http://localhost:8081/index.bundle:1:5274251)', + ' at renderItem (http://localhost:8081/index.bundle:1:5280705)', + ' at Container (http://localhost:8081/index.bundle:1:5288922)', + ].join('\n'); + + (NATIVE.fetchCachedJavascriptExceptionStack as jest.Mock).mockReturnValue(cachedStack); + + const event = executeIntegrationFor( + { + exception: { + values: [{ type: 'Error', value: 'Value is undefined, expected an Object' }], + }, + }, + {}, + ); + + expect(event.exception?.values?.[0].stacktrace?.frames).toBeDefined(); + expect(event.exception?.values?.[0].stacktrace!.frames!.length).toBeGreaterThan(0); + }); + + it('does nothing when cached stack cannot be parsed', () => { + (NATIVE.fetchCachedJavascriptExceptionStack as jest.Mock).mockReturnValue('not a valid stack trace'); + + const event = executeIntegrationFor( + { + exception: { + values: [{ type: 'Error', value: 'test' }], + }, + }, + {}, + ); + + expect(event.exception?.values?.[0].stacktrace).toBeUndefined(); + }); + + it('does nothing when event has no exception', () => { + (NATIVE.fetchCachedJavascriptExceptionStack as jest.Mock).mockReturnValue('Error: test\n at foo (file.js:1:1)'); + + executeIntegrationFor({}, {}); + + expect(NATIVE.fetchCachedJavascriptExceptionStack).not.toHaveBeenCalled(); + }); + + it('does nothing when exception has empty frames array', () => { + const cachedStack = ['Error: test', ' at foo (http://localhost:8081/index.bundle:1:100)'].join('\n'); + + (NATIVE.fetchCachedJavascriptExceptionStack as jest.Mock).mockReturnValue(cachedStack); + + const event = executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'test', + stacktrace: { frames: [] }, + }, + ], + }, + }, + {}, + ); + + expect(NATIVE.fetchCachedJavascriptExceptionStack).toHaveBeenCalledTimes(1); + expect(event.exception?.values?.[0].stacktrace?.frames!.length).toBeGreaterThan(0); + }); + + it('only patches the primary exception (values[0]) when multiple exception values exist', () => { + const cachedStack = ['Error: test', ' at foo (http://localhost:8081/index.bundle:1:100)'].join('\n'); + + (NATIVE.fetchCachedJavascriptExceptionStack as jest.Mock).mockReturnValue(cachedStack); + + const event = executeIntegrationFor( + { + exception: { + values: [ + { type: 'Error', value: 'primary error without stack' }, + { + type: 'Error', + value: 'linked cause', + stacktrace: { + frames: [{ filename: 'cause.js', function: 'causeFn', lineno: 5, colno: 1, in_app: true }], + }, + }, + ], + }, + }, + {}, + ); + + expect(event.exception?.values?.[0].stacktrace?.frames!.length).toBeGreaterThan(0); + expect(event.exception?.values?.[1].stacktrace?.frames).toHaveLength(1); + expect(event.exception?.values?.[1].stacktrace?.frames![0].filename).toBe('cause.js'); + }); +}); diff --git a/packages/core/test/mockWrapper.ts b/packages/core/test/mockWrapper.ts index afa8c6d1fb..1eed7300f9 100644 --- a/packages/core/test/mockWrapper.ts +++ b/packages/core/test/mockWrapper.ts @@ -55,6 +55,7 @@ const NATIVE: MockInterface = { stopProfiling: jest.fn(), fetchNativePackageName: jest.fn(), + fetchCachedJavascriptExceptionStack: jest.fn(), fetchNativeStackFramesBy: jest.fn(), initNativeReactNavigationNewFrameTracking: jest.fn(),