diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f58cefd..03f4ff922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased](https://github.com/Instabug/Instabug-React-Native/compare/v12.7.1...dev) + +### Added + +- Support enabling NDK crash capturing on Android ([#1132](https://github.com/Instabug/Instabug-React-Native/pull/1132)). + ## [12.7.1](https://github.com/Instabug/Instabug-React-Native/compare/v12.7.0...v12.7.1) (February 15, 2024) ### Changed diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugCrashReportingModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugCrashReportingModule.java index 8ac7d038b..4bd84c0bd 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugCrashReportingModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugCrashReportingModule.java @@ -113,6 +113,28 @@ public void run() { } } }); + } + /** + * Enables and disables capturing native C++ NDK crash reporting. + * + * @param isEnabled boolean indicating enabled or disabled. + */ + @ReactMethod + public void setNDKCrashesEnabled(final boolean isEnabled) { + MainThreadHandler.runOnMainThread(new Runnable() { + @Override + public void run() { + try { + if (isEnabled) { + CrashReporting.setNDKCrashesState(Feature.State.ENABLED); + } else { + CrashReporting.setNDKCrashesState(Feature.State.DISABLED); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); } } diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugCrashReportingModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugCrashReportingModuleTest.java index 2d3ac28e9..fd2a9b58e 100644 --- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugCrashReportingModuleTest.java +++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugCrashReportingModuleTest.java @@ -1,9 +1,85 @@ package com.instabug.reactlibrary; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import android.os.Looper; + +import com.instabug.crash.CrashReporting; +import com.instabug.library.Feature; +import com.instabug.reactlibrary.util.GlobalMocks; +import com.instabug.reactlibrary.utils.MainThreadHandler; + +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; public class RNInstabugCrashReportingModuleTest { + private final RNInstabugCrashReportingModule rnModule = new RNInstabugCrashReportingModule(null); + + // Mock Objects + private MockedStatic mockLooper; + private MockedStatic mockMainThreadHandler; + private MockedStatic mockCrashReporting; + + + @Before + public void mockMainThreadHandler() throws Exception { + // Mock static functions + mockLooper = mockStatic(Looper.class); + mockMainThreadHandler = mockStatic(MainThreadHandler.class); + mockCrashReporting = mockStatic(CrashReporting.class); + // Mock Looper class + Looper mockMainThreadLooper = mock(Looper.class); + Mockito.when(Looper.getMainLooper()).thenReturn(mockMainThreadLooper); + + + // Override runOnMainThread + Answer handlerPostAnswer = new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + invocation.getArgument(0, Runnable.class).run(); + return true; + } + }; + Mockito.doAnswer(handlerPostAnswer).when(MainThreadHandler.class); + MainThreadHandler.runOnMainThread(any(Runnable.class)); + } + + @After + public void tearDown() { + // Remove static mocks + mockLooper.close(); + mockMainThreadHandler.close(); + mockCrashReporting.close(); + } + + /********Crashes*********/ + + @Test + public void testSetNDKCrashesEnabledGivenTrue() { + // when + rnModule.setNDKCrashesEnabled(true); + +//then + mockCrashReporting.verify(() -> CrashReporting.setNDKCrashesState(Feature.State.ENABLED)); + } + + @Test + public void testSetNDKCrashesEnabledGivenFalse() { + // when + rnModule.setNDKCrashesEnabled(false); + + //then + mockCrashReporting.verify(() -> CrashReporting.setNDKCrashesState(Feature.State.DISABLED)); + } + @Test public void givenString$sendHandledJSCrash_whenQuery_thenShouldCallNativeApiWithArgs() throws Exception { // JSONObject json = mock(JSONObject.class); diff --git a/android/src/test/java/com/instabug/reactlibrary/RNInstabugRepliesModuleTest.java b/android/src/test/java/com/instabug/reactlibrary/RNInstabugRepliesModuleTest.java index bc7e95f9d..bd3ecf60e 100644 --- a/android/src/test/java/com/instabug/reactlibrary/RNInstabugRepliesModuleTest.java +++ b/android/src/test/java/com/instabug/reactlibrary/RNInstabugRepliesModuleTest.java @@ -10,6 +10,7 @@ import com.facebook.react.bridge.WritableMap; import com.instabug.chat.Replies; import com.instabug.library.Feature; +import com.instabug.reactlibrary.utils.InstabugUtil; import com.instabug.reactlibrary.utils.MapUtil; import com.instabug.reactlibrary.utils.MainThreadHandler; @@ -42,6 +43,7 @@ public class RNInstabugRepliesModuleTest { private MockedStatic mockLooper; private MockedStatic mockMainThreadHandler; private MockedStatic mockReplies; + private MockedStatic mockInstabugUtil; @Before public void mockMainThreadHandler() throws Exception { @@ -49,6 +51,7 @@ public void mockMainThreadHandler() throws Exception { mockReplies = mockStatic(Replies.class); mockLooper = mockStatic(Looper.class); mockMainThreadHandler = mockStatic(MainThreadHandler.class); + mockInstabugUtil = mockStatic(InstabugUtil.class); // Mock Looper class Looper mockMainThreadLooper = mock(Looper.class); @@ -71,6 +74,7 @@ public void tearDown() { mockLooper.close(); mockMainThreadHandler.close(); mockReplies.close(); + mockInstabugUtil.close(); } /********Replies*********/ diff --git a/examples/default/android/app/src/main/java/com/instabug/react/example/MainApplication.java b/examples/default/android/app/src/main/java/com/instabug/react/example/MainApplication.java index 285c0dc84..a988f0a84 100644 --- a/examples/default/android/app/src/main/java/com/instabug/react/example/MainApplication.java +++ b/examples/default/android/app/src/main/java/com/instabug/react/example/MainApplication.java @@ -21,10 +21,9 @@ public boolean getUseDeveloperSupport() { @Override protected List getPackages() { - @SuppressWarnings("UnnecessaryLocalVariable") List packages = new PackageList(this).getPackages(); // Packages that cannot be autolinked yet can be added manually here, for example: - // packages.add(new MyReactNativePackage()); + packages.add(new RNInstabugExampleReactnativePackage()); return packages; } diff --git a/examples/default/android/app/src/main/java/com/instabug/react/example/RNInstabugExampleCrashReportingModule.java b/examples/default/android/app/src/main/java/com/instabug/react/example/RNInstabugExampleCrashReportingModule.java new file mode 100644 index 000000000..7f748ad9f --- /dev/null +++ b/examples/default/android/app/src/main/java/com/instabug/react/example/RNInstabugExampleCrashReportingModule.java @@ -0,0 +1,105 @@ +package com.instabug.react.example; + +import static com.instabug.reactlibrary.utils.InstabugUtil.getMethod; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.instabug.crash.CrashReporting; +import com.instabug.crash.models.IBGNonFatalException; +import com.instabug.library.Feature; +import com.instabug.reactlibrary.RNInstabugReactnativeModule; +import com.instabug.reactlibrary.utils.MainThreadHandler; + +import org.json.JSONObject; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class RNInstabugExampleCrashReportingModule extends ReactContextBaseJavaModule { + + public RNInstabugExampleCrashReportingModule(ReactApplicationContext reactApplicationContext) { + super(reactApplicationContext); + } + + @Nonnull + @Override + public String getName() { + return "CrashReportingExampleModule"; + } + + @ReactMethod + public void sendNativeNonFatal(final String exceptionObject) { + final IBGNonFatalException exception = new IBGNonFatalException.Builder(new IllegalStateException("Test exception")) + .build(); + CrashReporting.report(exception); + + } + + @ReactMethod + public void sendNativeFatalCrash() { + throw new IllegalStateException("Unhandled IllegalStateException from Instabug Test App"); + } + + @ReactMethod + public void sendANR() { + try { + Thread.sleep(20000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @ReactMethod + public void sendFatalHang() { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @ReactMethod + public void sendOOM() { + oomCrash(); + } + + private void oomCrash() { + new Thread(() -> { + List stringList = new ArrayList<>(); + for (int i = 0; i < 1_000_000; i++) { + stringList.add(getRandomString(10_000)); + } + }).start(); + } + + private String getRandomString(int length) { + List charset = new ArrayList<>(); + for (char ch = 'a'; ch <= 'z'; ch++) { + charset.add(ch); + } + for (char ch = 'A'; ch <= 'Z'; ch++) { + charset.add(ch); + } + for (char ch = '0'; ch <= '9'; ch++) { + charset.add(ch); + } + + StringBuilder randomString = new StringBuilder(); + Random random = new Random(); + for (int i = 0; i < length; i++) { + char randomChar = charset.get(random.nextInt(charset.size())); + randomString.append(randomChar); + } + + return randomString.toString(); + } + +} diff --git a/examples/default/android/app/src/main/java/com/instabug/react/example/RNInstabugExampleReactnativePackage.java b/examples/default/android/app/src/main/java/com/instabug/react/example/RNInstabugExampleReactnativePackage.java new file mode 100644 index 000000000..304ab6870 --- /dev/null +++ b/examples/default/android/app/src/main/java/com/instabug/react/example/RNInstabugExampleReactnativePackage.java @@ -0,0 +1,41 @@ +package com.instabug.react.example; + +import androidx.annotation.NonNull; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.instabug.reactlibrary.RNInstabugAPMModule; +import com.instabug.reactlibrary.RNInstabugBugReportingModule; +import com.instabug.reactlibrary.RNInstabugCrashReportingModule; +import com.instabug.reactlibrary.RNInstabugFeatureRequestsModule; +import com.instabug.reactlibrary.RNInstabugReactnativeModule; +import com.instabug.reactlibrary.RNInstabugRepliesModule; +import com.instabug.reactlibrary.RNInstabugSessionReplayModule; +import com.instabug.reactlibrary.RNInstabugSurveysModule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RNInstabugExampleReactnativePackage implements ReactPackage { + + private static final String TAG = RNInstabugExampleReactnativePackage.class.getSimpleName(); + + public RNInstabugExampleReactnativePackage() {} + + @NonNull + @Override + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new RNInstabugExampleCrashReportingModule(reactContext)); + return modules; + } + + @NonNull + @Override + public List createViewManagers(@NonNull ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/examples/default/ios/InstabugExample.xcodeproj/project.pbxproj b/examples/default/ios/InstabugExample.xcodeproj/project.pbxproj index f7632345b..32dd9b905 100644 --- a/examples/default/ios/InstabugExample.xcodeproj/project.pbxproj +++ b/examples/default/ios/InstabugExample.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 20E556262AC55766007416B1 /* InstabugSessionReplayTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 20E556252AC55766007416B1 /* InstabugSessionReplayTests.m */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + C3C8CCF379347A4DF9D2A39D /* CrashReportingExampleModule.m in Sources */ = {isa = PBXBuildFile; fileRef = C3C8C24386310A3120006604 /* CrashReportingExampleModule.m */; }; CC3DF88E2A1DFC9A003E9914 /* InstabugCrashReportingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3DF8852A1DFC99003E9914 /* InstabugCrashReportingTests.m */; }; CC3DF88F2A1DFC9A003E9914 /* InstabugBugReportingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3DF8862A1DFC99003E9914 /* InstabugBugReportingTests.m */; }; CC3DF8902A1DFC9A003E9914 /* InstabugSampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3DF8872A1DFC99003E9914 /* InstabugSampleTests.m */; }; @@ -51,6 +52,8 @@ 9A3D962AB03F97E25566779F /* Pods-InstabugExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugExample.debug.xcconfig"; path = "Target Support Files/Pods-InstabugExample/Pods-InstabugExample.debug.xcconfig"; sourceTree = ""; }; BAED0D0441A708AE2390E153 /* libPods-InstabugExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-InstabugExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BD54B44E2DF85672BB2D4DEE /* Pods-InstabugExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugExample.release.xcconfig"; path = "Target Support Files/Pods-InstabugExample/Pods-InstabugExample.release.xcconfig"; sourceTree = ""; }; + C3C8C24386310A3120006604 /* CrashReportingExampleModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CrashReportingExampleModule.m; sourceTree = ""; }; + C3C8C784EADC037C5A752B94 /* CrashReportingExampleModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CrashReportingExampleModule.h; sourceTree = ""; }; CC3DF8852A1DFC99003E9914 /* InstabugCrashReportingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstabugCrashReportingTests.m; sourceTree = ""; }; CC3DF8862A1DFC99003E9914 /* InstabugBugReportingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstabugBugReportingTests.m; sourceTree = ""; }; CC3DF8872A1DFC99003E9914 /* InstabugSampleTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstabugSampleTests.m; sourceTree = ""; }; @@ -121,6 +124,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB71A68108700A75B9A /* main.m */, + C3C8C1DDCEA91410F27A3683 /* native */, ); name = InstabugExample; sourceTree = ""; @@ -177,6 +181,15 @@ path = Pods; sourceTree = ""; }; + C3C8C1DDCEA91410F27A3683 /* native */ = { + isa = PBXGroup; + children = ( + C3C8C784EADC037C5A752B94 /* CrashReportingExampleModule.h */, + C3C8C24386310A3120006604 /* CrashReportingExampleModule.m */, + ); + path = native; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -463,6 +476,7 @@ files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, + C3C8CCF379347A4DF9D2A39D /* CrashReportingExampleModule.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/examples/default/ios/native/CrashReportingExampleModule.h b/examples/default/ios/native/CrashReportingExampleModule.h new file mode 100644 index 000000000..d9b35437c --- /dev/null +++ b/examples/default/ios/native/CrashReportingExampleModule.h @@ -0,0 +1,12 @@ +#import +#import +#import + +@interface CrashReportingExampleModule : RCTEventEmitter + +- (void)sendNativeNonFatal; +- (void)sendNativeFatalCrash; +- (void)sendFatalHang; +- (void)sendOOM; + +@end diff --git a/examples/default/ios/native/CrashReportingExampleModule.m b/examples/default/ios/native/CrashReportingExampleModule.m new file mode 100644 index 000000000..b337da215 --- /dev/null +++ b/examples/default/ios/native/CrashReportingExampleModule.m @@ -0,0 +1,77 @@ +#import "CrashReportingExampleModule.h" +#import +#import + +@interface CrashReportingExampleModule() +@property (nonatomic, strong) NSMutableArray *oomBelly; +@property (nonatomic, strong) dispatch_queue_t serialQueue; +@end + +@implementation CrashReportingExampleModule + +- (instancetype)init { + self = [super init]; + if (self) { + self.serialQueue = dispatch_queue_create("QUEUE>SERIAL", DISPATCH_QUEUE_SERIAL); + } + return self; +} + +- (dispatch_queue_t)methodQueue { + return dispatch_get_main_queue(); +} + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + + +- (void)oomCrash { + dispatch_async(self.serialQueue, ^{ + self.oomBelly = [NSMutableArray array]; + [UIApplication.sharedApplication beginBackgroundTaskWithName:@"OOM Crash" expirationHandler:nil]; + while (true) { + unsigned long dinnerLength = 1024 * 1024 * 10; + char *dinner = malloc(sizeof(char) * dinnerLength); + for (int i=0; i < dinnerLength; i++) + { + //write to each byte ensure that the memory pages are actually allocated + dinner[i] = '0'; + } + NSData *plate = [NSData dataWithBytesNoCopy:dinner length:dinnerLength freeWhenDone:YES]; + [self.oomBelly addObject:plate]; + } + }); +} + +RCT_EXPORT_MODULE(CrashReportingExampleModule) + + +RCT_EXPORT_METHOD(sendNativeNonFatal) { + IBGNonFatalException *nonFatalException = [IBGCrashReporting exception:[NSException exceptionWithName:@"native Handled NS Exception" reason:@"Test iOS Handled Crash" userInfo:@{@"Key": @"Value"}]]; + + [nonFatalException report]; +} + +RCT_EXPORT_METHOD(sendNativeFatalCrash) { + NSException *exception = [NSException exceptionWithName:@"native Unhandled NS Exception" reason:@"Test iOS Unhandled Crash" userInfo:nil]; + @throw exception; +} +RCT_EXPORT_METHOD(sendFatalHang) { + [NSThread sleepForTimeInterval:3.0f]; +} + +RCT_EXPORT_METHOD(sendOOM) { + [self oomCrash]; +} + +@synthesize description; + +@synthesize hash; + +@synthesize superclass; + +@end + + diff --git a/examples/default/src/components/PlatformListTile.tsx b/examples/default/src/components/PlatformListTile.tsx new file mode 100644 index 000000000..1bf1fe6e7 --- /dev/null +++ b/examples/default/src/components/PlatformListTile.tsx @@ -0,0 +1,37 @@ +import React, { PropsWithChildren } from 'react'; + +import { Box, HStack, Pressable, Text } from 'native-base'; +import { Platform } from 'react-native'; + +interface PlatformListTileProps extends PropsWithChildren { + title: string; + onPress?: () => void; + platform?: 'ios' | 'android'; +} + +export const PlatformListTile: React.FC = ({ + title, + onPress, + platform, + children, +}) => { + if (Platform.OS === platform || !platform) { + return ( + + + {title} + {children} + + + ); + } + return null; +}; diff --git a/examples/default/src/native/NativeCrashReporting.ts b/examples/default/src/native/NativeCrashReporting.ts new file mode 100644 index 000000000..a64f4526d --- /dev/null +++ b/examples/default/src/native/NativeCrashReporting.ts @@ -0,0 +1,13 @@ +import type { NativeModule } from 'react-native'; + +import { NativeExampleModules } from './NativePackage'; + +export interface CrashReportingExampleNativeModule extends NativeModule { + sendNativeNonFatal(): Promise; + sendNativeFatalCrash(): Promise; + sendFatalHang(): Promise; + sendANR(): Promise; + sendOOM(): Promise; +} + +export const NativeExampleCrashReporting = NativeExampleModules.CrashReportingExampleModule; diff --git a/examples/default/src/native/NativePackage.ts b/examples/default/src/native/NativePackage.ts new file mode 100644 index 000000000..16b79b494 --- /dev/null +++ b/examples/default/src/native/NativePackage.ts @@ -0,0 +1,9 @@ +import { NativeModules as ReactNativeModules } from 'react-native'; + +import type { CrashReportingExampleNativeModule } from './NativeCrashReporting'; + +export interface InstabugExampleNativePackage { + CrashReportingExampleModule: CrashReportingExampleNativeModule; +} + +export const NativeExampleModules = ReactNativeModules as InstabugExampleNativePackage; diff --git a/examples/default/src/screens/CrashReportingScreen.tsx b/examples/default/src/screens/CrashReportingScreen.tsx index 3d805185e..eeb1b6f18 100644 --- a/examples/default/src/screens/CrashReportingScreen.tsx +++ b/examples/default/src/screens/CrashReportingScreen.tsx @@ -1,34 +1,126 @@ import React from 'react'; -import { Alert } from 'react-native'; +import { Alert, ScrollView, Text } from 'react-native'; import { CrashReporting } from 'instabug-reactnative'; import { ListTile } from '../components/ListTile'; import { Screen } from '../components/Screen'; +import { Section } from '../components/Section'; +import { PlatformListTile } from '../components/PlatformListTile'; +import { NativeExampleCrashReporting } from '../native/NativeCrashReporting'; export const CrashReportingScreen: React.FC = () => { + function throwHandledException(error: Error) { + try { + if (!error.message) { + const appName = 'Instabug Test App'; + error.message = `Handled ${error.name} From ${appName}`; + } + throw error; + } catch (err) { + if (err instanceof Error) { + CrashReporting.reportError(err).then(() => + Alert.alert(`Crash report for ${error.name} is Sent!`), + ); + } + } + } + + function throwUnhandledException(error: Error, isPromise: boolean = false) { + const appName = 'Instabug Test App'; + const rejectionType = isPromise ? 'Promise Rejection ' : ''; + const errorMessage = `Unhandled ${rejectionType}${error.name} from ${appName}`; + + if (!error.message) { + console.log(`IBG-CRSH | Error message: ${error.message}`); + error.message = errorMessage; + } + + if (isPromise) { + console.log('IBG-CRSH | Promise'); + Promise.reject(error).then(() => + Alert.alert(`Promise Rejection Crash report for ${error.name} is Sent!`), + ); + } else { + throw error; + } + } + return ( - { - try { - throw new Error('Handled Exception From Instabug Test App'); - } catch (err) { - if (err instanceof Error) { - CrashReporting.reportError(err); - Alert.alert('Crash report Sent!'); - } - } - }} - /> - { - Promise.reject(new Error('Unhandled Promise Rejection from Instabug Test App')); - Alert.alert('Crash report sent!'); - }} - /> + +
+ throwHandledException(new Error())} + /> + throwHandledException(new SyntaxError())} + /> + throwHandledException(new RangeError())} + /> + throwHandledException(new ReferenceError())} + /> + throwHandledException(new URIError())} + /> + NativeExampleCrashReporting.sendNativeNonFatal()} + /> +
+
+ Fatal Crashes can only be tested in release mode + These buttons will crash the application. + throwUnhandledException(Error(), true)} + /> + throwUnhandledException(Error())} + /> + throwUnhandledException(new SyntaxError())} + /> + throwUnhandledException(new RangeError())} + /> + throwUnhandledException(new ReferenceError())} + /> + throwUnhandledException(new URIError())} + /> + NativeExampleCrashReporting.sendNativeFatalCrash()} + /> + NativeExampleCrashReporting.sendFatalHang()} + /> + NativeExampleCrashReporting.sendANR()} + platform={'android'} + /> + NativeExampleCrashReporting.sendOOM()} + /> +
+
); }; diff --git a/src/modules/CrashReporting.ts b/src/modules/CrashReporting.ts index c03a1c339..86af24ca6 100644 --- a/src/modules/CrashReporting.ts +++ b/src/modules/CrashReporting.ts @@ -2,6 +2,7 @@ import type { ExtendedError } from 'react-native/Libraries/Core/Devtools/parseEr import { NativeCrashReporting } from '../native/NativeCrashReporting'; import InstabugUtils from '../utils/InstabugUtils'; +import { Platform } from 'react-native'; /** * Enables and disables everything related to crash reporting including intercepting @@ -19,3 +20,13 @@ export const setEnabled = (isEnabled: boolean) => { export const reportError = (error: ExtendedError) => { return InstabugUtils.sendCrashReport(error, NativeCrashReporting.sendHandledJSCrash); }; + +/** + * Enables and disables capturing native C++ NDK crashes. + * @param isEnabled + */ +export const setNDKCrashesEnabled = (isEnabled: boolean) => { + if (Platform.OS === 'android') { + NativeCrashReporting.setNDKCrashesEnabled(isEnabled); + } +}; diff --git a/src/native/NativeCrashReporting.ts b/src/native/NativeCrashReporting.ts index f1a8b85f4..f460ce6d6 100644 --- a/src/native/NativeCrashReporting.ts +++ b/src/native/NativeCrashReporting.ts @@ -16,6 +16,7 @@ export interface CrashReportingNativeModule extends NativeModule { setEnabled(isEnabled: boolean): void; sendJSCrash(data: CrashData | string): Promise; sendHandledJSCrash(data: CrashData | string): Promise; + setNDKCrashesEnabled(isEnabled: boolean): Promise; } export const NativeCrashReporting = NativeModules.IBGCrashReporting; diff --git a/test/mocks/mockCrashReporting.ts b/test/mocks/mockCrashReporting.ts index 0d2685156..fee9bdc2a 100644 --- a/test/mocks/mockCrashReporting.ts +++ b/test/mocks/mockCrashReporting.ts @@ -6,6 +6,7 @@ const mockCrashReporting: CrashReportingNativeModule = { setEnabled: jest.fn(), sendHandledJSCrash: jest.fn(), sendJSCrash: jest.fn(), + setNDKCrashesEnabled: jest.fn(), }; export default mockCrashReporting; diff --git a/test/modules/CrashReporting.spec.ts b/test/modules/CrashReporting.spec.ts index 65b9db69e..5b09d28ca 100644 --- a/test/modules/CrashReporting.spec.ts +++ b/test/modules/CrashReporting.spec.ts @@ -3,6 +3,7 @@ import '../mocks/mockInstabugUtils'; import * as CrashReporting from '../../src/modules/CrashReporting'; import { NativeCrashReporting } from '../../src/native/NativeCrashReporting'; import InstabugUtils from '../../src/utils/InstabugUtils'; +import { Platform } from 'react-native'; describe('CrashReporting Module', () => { it('should call the native method setEnabled', () => { @@ -22,4 +23,19 @@ describe('CrashReporting Module', () => { NativeCrashReporting.sendHandledJSCrash, ); }); + + it('should call the native method setNDKCrashesEnabled for Android platform', () => { + Platform.OS = 'android'; + CrashReporting.setNDKCrashesEnabled(true); + + expect(NativeCrashReporting.setNDKCrashesEnabled).toBeCalledTimes(1); + expect(NativeCrashReporting.setNDKCrashesEnabled).toBeCalledWith(true); + }); + + it('should not call the native method setNDKCrashesEnabled for ios platform', () => { + Platform.OS = 'ios'; + CrashReporting.setNDKCrashesEnabled(true); + + expect(NativeCrashReporting.setNDKCrashesEnabled).not.toBeCalled(); + }); });