From d8e4b0db0d3299ec1c8ac230c679f9c2d653cbe7 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud <68241710+a7medev@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:31:52 +0200 Subject: [PATCH] feat(ios): add native-side init API for startup crashes (#1056) Jira ID: MOB-13246 --- CHANGELOG.md | 4 + .../InstabugExample.xcodeproj/project.pbxproj | 4 + .../ios/InstabugTests/InstabugSampleTests.m | 14 +-- .../ios/InstabugTests/RNInstabugTests.m | 71 +++++++++++++++ ios/RNInstabug/InstabugReactBridge.m | 66 +------------- ios/RNInstabug/RNInstabug.h | 13 +++ ios/RNInstabug/RNInstabug.m | 90 +++++++++++++++++++ ios/RNInstabug/Util/Instabug+CP.h | 12 +++ 8 files changed, 201 insertions(+), 73 deletions(-) create mode 100644 examples/default/ios/InstabugTests/RNInstabugTests.m create mode 100644 ios/RNInstabug/RNInstabug.h create mode 100644 ios/RNInstabug/RNInstabug.m create mode 100644 ios/RNInstabug/Util/Instabug+CP.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 53522dfd6..d950e5149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased](https://github.com/Instabug/Instabug-React-Native/compare/v12.1.0...dev) +### Added + +- Add an iOS-side init API which allows capturing crashes that happen early in the app lifecycle and before the JavaScript code has started ([#1056](https://github.com/Instabug/Instabug-React-Native/pull/1056)). + ### Changed - Bump Instabug iOS SDK to v12.2.0 ([#1053](https://github.com/Instabug/Instabug-React-Native/pull/1053)). [See release notes](https://github.com/instabug/instabug-ios/releases/tag/12.2.0). diff --git a/examples/default/ios/InstabugExample.xcodeproj/project.pbxproj b/examples/default/ios/InstabugExample.xcodeproj/project.pbxproj index 68d57d960..f7632345b 100644 --- a/examples/default/ios/InstabugExample.xcodeproj/project.pbxproj +++ b/examples/default/ios/InstabugExample.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ CC3DF8932A1DFC9A003E9914 /* InstabugSurveysTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3DF88B2A1DFC99003E9914 /* InstabugSurveysTests.m */; }; CC3DF8942A1DFC9A003E9914 /* InstabugAPMTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3DF88C2A1DFC99003E9914 /* InstabugAPMTests.m */; }; CC3DF8952A1DFC9A003E9914 /* IBGConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3DF88D2A1DFC9A003E9914 /* IBGConstants.m */; }; + CCF1E4092B022CF20024802D /* RNInstabugTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCF1E4082B022CF20024802D /* RNInstabugTests.m */; }; CD36F4707EA1F435D2CC7A15 /* libPods-InstabugExample-InstabugTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF7A6E02D40E0CEEA833CC4 /* libPods-InstabugExample-InstabugTests.a */; }; F7BF47401EF3A435254C97BB /* libPods-InstabugExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BAED0D0441A708AE2390E153 /* libPods-InstabugExample.a */; }; /* End PBXBuildFile section */ @@ -59,6 +60,7 @@ CC3DF88B2A1DFC99003E9914 /* InstabugSurveysTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstabugSurveysTests.m; sourceTree = ""; }; CC3DF88C2A1DFC99003E9914 /* InstabugAPMTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InstabugAPMTests.m; sourceTree = ""; }; CC3DF88D2A1DFC9A003E9914 /* IBGConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IBGConstants.m; sourceTree = ""; }; + CCF1E4082B022CF20024802D /* RNInstabugTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNInstabugTests.m; sourceTree = ""; }; DBCB1B1D023646D84146C91E /* Pods-InstabugExample-InstabugTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugExample-InstabugTests.release.xcconfig"; path = "Target Support Files/Pods-InstabugExample-InstabugTests/Pods-InstabugExample-InstabugTests.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -97,6 +99,7 @@ CC3DF8872A1DFC99003E9914 /* InstabugSampleTests.m */, CC3DF88B2A1DFC99003E9914 /* InstabugSurveysTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, + CCF1E4082B022CF20024802D /* RNInstabugTests.m */, ); path = InstabugTests; sourceTree = ""; @@ -447,6 +450,7 @@ CC3DF8932A1DFC9A003E9914 /* InstabugSurveysTests.m in Sources */, 20E556262AC55766007416B1 /* InstabugSessionReplayTests.m in Sources */, CC3DF88F2A1DFC9A003E9914 /* InstabugBugReportingTests.m in Sources */, + CCF1E4092B022CF20024802D /* RNInstabugTests.m in Sources */, CC3DF88E2A1DFC9A003E9914 /* InstabugCrashReportingTests.m in Sources */, CC3DF8942A1DFC9A003E9914 /* InstabugAPMTests.m in Sources */, CC3DF8922A1DFC9A003E9914 /* InstabugRepliesTests.m in Sources */, diff --git a/examples/default/ios/InstabugTests/InstabugSampleTests.m b/examples/default/ios/InstabugTests/InstabugSampleTests.m index ad24ee21e..2e6445b5b 100644 --- a/examples/default/ios/InstabugTests/InstabugSampleTests.m +++ b/examples/default/ios/InstabugTests/InstabugSampleTests.m @@ -12,6 +12,7 @@ #import "InstabugReactBridge.h" #import #import "IBGConstants.h" +#import "RNInstabug.h" @protocol InstabugCPTestProtocol /** @@ -36,6 +37,7 @@ - (void)setEnabled:(BOOL)isEnabled; @interface InstabugSampleTests : XCTestCase @property (nonatomic, retain) InstabugReactBridge *instabugBridge; +@property (nonatomic, retain) id mRNInstabug; @end @implementation InstabugSampleTests @@ -44,6 +46,7 @@ @implementation InstabugSampleTests - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. self.instabugBridge = [[InstabugReactBridge alloc] init]; + self.mRNInstabug = OCMClassMock([RNInstabug class]); } /* @@ -62,23 +65,14 @@ - (void)testSetEnabled { } - (void)testInit { - id mock = OCMClassMock([Instabug class]); IBGInvocationEvent floatingButtonInvocationEvent = IBGInvocationEventFloatingButton; NSString *appToken = @"app_token"; NSArray *invocationEvents = [NSArray arrayWithObjects:[NSNumber numberWithInteger:floatingButtonInvocationEvent], nil]; IBGSDKDebugLogsLevel sdkDebugLogsLevel = IBGSDKDebugLogsLevelDebug; - XCTestExpectation *expectation = [self expectationWithDescription:@"Testing [Instabug init]"]; - - OCMStub([mock startWithToken:appToken invocationEvents:floatingButtonInvocationEvent]); [self.instabugBridge init:appToken invocationEvents:invocationEvents debugLogsLevel:sdkDebugLogsLevel]; - [[NSRunLoop mainRunLoop] performBlock:^{ - OCMVerify([mock startWithToken:appToken invocationEvents:floatingButtonInvocationEvent]); - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:EXPECTATION_TIMEOUT handler:nil]; + OCMVerify([self.mRNInstabug initWithToken:appToken invocationEvents:floatingButtonInvocationEvent debugLogsLevel:sdkDebugLogsLevel]); } - (void)testSetUserData { diff --git a/examples/default/ios/InstabugTests/RNInstabugTests.m b/examples/default/ios/InstabugTests/RNInstabugTests.m new file mode 100644 index 000000000..9ceed3662 --- /dev/null +++ b/examples/default/ios/InstabugTests/RNInstabugTests.m @@ -0,0 +1,71 @@ +#import +#import "OCMock/OCMock.h" +#import "Instabug/Instabug.h" +#import "Instabug/IBGSurvey.h" +#import +#import "RNInstabug.h" +#import "RNInstabug/Instabug+CP.h" +#import "RNInstabug/IBGNetworkLogger+CP.h" +#import "IBGConstants.h" + +@interface RNInstabugTests : XCTestCase + +@property (nonatomic, strong) id mInstabug; +@property (nonatomic, strong) id mIBGNetworkLogger; + +@end + +// Expose the `reset` API on RNInstabug to allow multiple calls to `initWithToken`. +@interface RNInstabug (Test) ++ (void)reset; +@end + +@implementation RNInstabugTests + +- (void)setUp { + self.mInstabug = OCMClassMock([Instabug class]); + self.mIBGNetworkLogger = OCMClassMock([IBGNetworkLogger class]); + + [RNInstabug reset]; +} + +- (void)testInitWithoutLogsLevel { + NSString *token = @"app-token"; + IBGInvocationEvent invocationEvents = IBGInvocationEventFloatingButton | IBGInvocationEventShake; + + [RNInstabug initWithToken:token invocationEvents:invocationEvents]; + + OCMVerify([self.mInstabug startWithToken:token invocationEvents:invocationEvents]); + OCMVerify([self.mInstabug setCurrentPlatform:IBGPlatformReactNative]); + OCMVerify([self.mIBGNetworkLogger disableAutomaticCapturingOfNetworkLogs]); + OCMVerify([self.mIBGNetworkLogger setEnabled:YES]); +} + +- (void)testInitStartsOnce { + NSString *token = @"app-token"; + IBGInvocationEvent invocationEvents = IBGInvocationEventFloatingButton | IBGInvocationEventShake; + + // Call init twice to check that only 1 call to native methods happens. + [RNInstabug initWithToken:token invocationEvents:invocationEvents]; + [RNInstabug initWithToken:token invocationEvents:invocationEvents]; + + OCMVerify(times(1), [self.mInstabug startWithToken:token invocationEvents:invocationEvents]); + OCMVerify(times(1), [self.mInstabug setCurrentPlatform:IBGPlatformReactNative]); + OCMVerify(times(1), [self.mIBGNetworkLogger disableAutomaticCapturingOfNetworkLogs]); + OCMVerify(times(1), [self.mIBGNetworkLogger setEnabled:YES]); +} + +- (void)testInitWithLogsLevel { + NSString *token = @"app-token"; + IBGInvocationEvent invocationEvents = IBGInvocationEventFloatingButton | IBGInvocationEventShake; + IBGSDKDebugLogsLevel debugLogsLevel = IBGSDKDebugLogsLevelDebug; + + [RNInstabug initWithToken:token invocationEvents:invocationEvents debugLogsLevel:debugLogsLevel]; + + OCMVerify([self.mInstabug startWithToken:token invocationEvents:invocationEvents]); + OCMVerify([self.mInstabug setCurrentPlatform:IBGPlatformReactNative]); + OCMVerify([self.mIBGNetworkLogger disableAutomaticCapturingOfNetworkLogs]); + OCMVerify([self.mIBGNetworkLogger setEnabled:YES]); +} + +@end diff --git a/ios/RNInstabug/InstabugReactBridge.m b/ios/RNInstabug/InstabugReactBridge.m index a7d7e45ba..bf97d0ff9 100644 --- a/ios/RNInstabug/InstabugReactBridge.m +++ b/ios/RNInstabug/InstabugReactBridge.m @@ -12,11 +12,9 @@ #import #import #import -#import #import -#import #import -#import "Util/IBGNetworkLogger+CP.h" +#import "RNInstabug.h" @interface Instabug (PrivateWillSendAPI) + (void)setWillSendReportHandler_private:(void(^)(IBGReport *report, void(^reportCompletionHandler)(IBGReport *)))willSendReportHandler_private; @@ -40,31 +38,13 @@ - (dispatch_queue_t)methodQueue { } RCT_EXPORT_METHOD(init:(NSString *)token invocationEvents:(NSArray*)invocationEventsArray debugLogsLevel:(IBGSDKDebugLogsLevel)sdkDebugLogsLevel) { - SEL setPrivateApiSEL = NSSelectorFromString(@"setCurrentPlatform:"); - if ([[Instabug class] respondsToSelector:setPrivateApiSEL]) { - NSInteger *platform = IBGPlatformReactNative; - NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[Instabug class] methodSignatureForSelector:setPrivateApiSEL]]; - [inv setSelector:setPrivateApiSEL]; - [inv setTarget:[Instabug class]]; - [inv setArgument:&(platform) atIndex:2]; - [inv invoke]; - } IBGInvocationEvent invocationEvents = 0; - NSLog(@"invocation events: %ld",(long)invocationEvents); + for (NSNumber *boxedValue in invocationEventsArray) { invocationEvents |= [boxedValue intValue]; } - [IBGNetworkLogger disableAutomaticCapturingOfNetworkLogs]; - [Instabug startWithToken:token invocationEvents:invocationEvents]; - [Instabug setSdkDebugLogsLevel:sdkDebugLogsLevel]; - RCTAddLogFunction(InstabugReactLogFunction); - RCTSetLogThreshold(RCTLogLevelInfo); - - IBGNetworkLogger.enabled = YES; - - // Temporarily disabling APM hot launches - IBGAPM.hotAppLaunchEnabled = NO; + [RNInstabug initWithToken:token invocationEvents:invocationEvents debugLogsLevel:sdkDebugLogsLevel]; } RCT_EXPORT_METHOD(setReproStepsConfig:(IBGUserStepsMode)bugMode :(IBGUserStepsMode)crashMode:(IBGUserStepsMode)sessionReplayMode) { @@ -415,44 +395,4 @@ + (BOOL)iOSVersionIsLessThan:(NSString *)iOSVersion { return [iOSVersion compare:[UIDevice currentDevice].systemVersion options:NSNumericSearch] == NSOrderedDescending; }; -// Note: This function is used to bridge IBGNSLog with RCTLogFunction. -// This log function should not be used externally and is only an implementation detail. -void RNIBGLog(IBGLogLevel logLevel, NSString *format, ...) { - va_list arg_list; - va_start(arg_list, format); - IBGNSLogWithLevel(format, arg_list, logLevel); - va_end(arg_list); -} - -RCTLogFunction InstabugReactLogFunction = ^( - RCTLogLevel level, - __unused RCTLogSource source, - NSString *fileName, - NSNumber *lineNumber, - NSString *message - ) -{ - NSString *formatString = @"Instabug - REACT LOG: %@"; - NSString *log = RCTFormatLog([NSDate date], level, fileName, lineNumber, message); - - switch(level) { - case RCTLogLevelTrace: - RNIBGLog(IBGLogLevelVerbose, formatString, log); - break; - case RCTLogLevelInfo: - RNIBGLog(IBGLogLevelInfo, formatString, log); - break; - case RCTLogLevelWarning: - RNIBGLog(IBGLogLevelWarning, formatString, log); - break; - case RCTLogLevelError: - RNIBGLog(IBGLogLevelError, formatString, log); - break; - case RCTLogLevelFatal: - RNIBGLog(IBGLogLevelError, formatString, log); - break; - } -}; - - @end diff --git a/ios/RNInstabug/RNInstabug.h b/ios/RNInstabug/RNInstabug.h new file mode 100644 index 000000000..b18b4d41f --- /dev/null +++ b/ios/RNInstabug/RNInstabug.h @@ -0,0 +1,13 @@ +#ifndef RNInstabug_h +#define RNInstabug_h + +#import + +@interface RNInstabug : NSObject + ++ (void)initWithToken:(NSString *)token invocationEvents:(IBGInvocationEvent)invocationEvents debugLogsLevel:(IBGSDKDebugLogsLevel)debugLogsLevel; ++ (void)initWithToken:(NSString *)token invocationEvents:(IBGInvocationEvent)invocationEvents; + +@end + +#endif /* RNInstabug_h */ diff --git a/ios/RNInstabug/RNInstabug.m b/ios/RNInstabug/RNInstabug.m new file mode 100644 index 000000000..209670a5e --- /dev/null +++ b/ios/RNInstabug/RNInstabug.m @@ -0,0 +1,90 @@ +#import +#import +#import "RNInstabug.h" +#import "Util/IBGNetworkLogger+CP.h" +#import "Util/Instabug+CP.h" + +@implementation RNInstabug + +static BOOL didInit = NO; + +/// Resets `didInit` allowing re-initialization, it should not be added to the header file and is there for testing purposes. ++ (void)reset { + didInit = NO; +} + ++ (void)initWithToken:(NSString *)token invocationEvents:(IBGInvocationEvent)invocationEvents { + // Initialization is performed only once to avoid unexpected behavior. + if (didInit) { + NSLog(@"IBG-RN: Skipped iOS SDK re-initialization"); + return; + } + + didInit = YES; + + [Instabug setCurrentPlatform:IBGPlatformReactNative]; + + // Disable automatic network logging in the iOS SDK to avoid duplicate network logs coming + // from both the iOS and React Native SDKs + [IBGNetworkLogger disableAutomaticCapturingOfNetworkLogs]; + + [Instabug startWithToken:token invocationEvents:invocationEvents]; + + // Setup automatic capturing of JavaScript console logs + RCTAddLogFunction(InstabugReactLogFunction); + RCTSetLogThreshold(RCTLogLevelInfo); + + // Even though automatic network logging is disabled in the iOS SDK, the network logger itself + // is still needed since network logs captured by the React Native SDK need to be logged through it + IBGNetworkLogger.enabled = YES; + + // Temporarily disabling APM hot launches + IBGAPM.hotAppLaunchEnabled = NO; +} + ++ (void)initWithToken:(NSString *)token + invocationEvents:(IBGInvocationEvent)invocationEvents + debugLogsLevel:(IBGSDKDebugLogsLevel)debugLogsLevel { + [Instabug setSdkDebugLogsLevel:debugLogsLevel]; + [self initWithToken:token invocationEvents:invocationEvents]; +} + +// Note: This function is used to bridge IBGNSLog with RCTLogFunction. +// This log function should not be used externally and is only an implementation detail. +void RNIBGLog(IBGLogLevel logLevel, NSString *format, ...) { + va_list arg_list; + va_start(arg_list, format); + IBGNSLogWithLevel(format, arg_list, logLevel); + va_end(arg_list); +} + +RCTLogFunction InstabugReactLogFunction = ^(RCTLogLevel level, + __unused RCTLogSource source, + NSString *fileName, + NSNumber *lineNumber, + NSString *message) +{ + NSString *formatString = @"Instabug - REACT LOG: %@"; + NSString *log = RCTFormatLog([NSDate date], level, fileName, lineNumber, message); + + switch(level) { + case RCTLogLevelTrace: + RNIBGLog(IBGLogLevelVerbose, formatString, log); + break; + case RCTLogLevelInfo: + RNIBGLog(IBGLogLevelInfo, formatString, log); + break; + case RCTLogLevelWarning: + RNIBGLog(IBGLogLevelWarning, formatString, log); + break; + case RCTLogLevelError: + RNIBGLog(IBGLogLevelError, formatString, log); + break; + case RCTLogLevelFatal: + RNIBGLog(IBGLogLevelError, formatString, log); + break; + } +}; + +@end + diff --git a/ios/RNInstabug/Util/Instabug+CP.h b/ios/RNInstabug/Util/Instabug+CP.h new file mode 100644 index 000000000..8666413f0 --- /dev/null +++ b/ios/RNInstabug/Util/Instabug+CP.h @@ -0,0 +1,12 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface Instabug (CP) + ++ (void)setCurrentPlatform:(IBGPlatform)platform; + +@end + +NS_ASSUME_NONNULL_END