diff --git a/.circleci/config.yml b/.circleci/config.yml index 47df5c5d5..3624090f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,7 +47,7 @@ commands: steps: - run: name: Install XCUITest Driver - command: appium driver install xcuitest@7.14.0 + command: appium driver install xcuitest@4.35.0 - when: condition: equal: diff --git a/CHANGELOG.md b/CHANGELOG.md index a59302235..b3a80f8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased](https://github.com/Instabug/Instabug-Flutter/compare/v13.0.0...dev) +### Added +- Add support for passing a grouping fingerprint, error level, and user attributes to the `CrashReporting.reportHandledCrash` non-fatals API ([#461](https://github.com/Instabug/Instabug-Flutter/pull/461)). + ### Changed - Bump Instabug iOS SDK to v13.1.0 ([#1227](https://github.com/Instabug/Instabug-Flutter/pull/1227)). [See release notes](https://github.com/Instabug/Instabug-iOS/releases/tag/13.1.0). diff --git a/android/src/main/java/com/instabug/flutter/modules/CrashReportingApi.java b/android/src/main/java/com/instabug/flutter/modules/CrashReportingApi.java index fc9c204a9..075d0da69 100644 --- a/android/src/main/java/com/instabug/flutter/modules/CrashReportingApi.java +++ b/android/src/main/java/com/instabug/flutter/modules/CrashReportingApi.java @@ -1,17 +1,21 @@ package com.instabug.flutter.modules; -import android.util.Log; +import static com.instabug.crash.CrashReporting.getFingerprintObject; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.instabug.crash.CrashReporting; +import com.instabug.crash.models.IBGNonFatalException; import com.instabug.flutter.generated.CrashReportingPigeon; +import com.instabug.flutter.util.ArgsRegistry; import com.instabug.flutter.util.Reflection; import com.instabug.library.Feature; import org.json.JSONObject; import java.lang.reflect.Method; +import java.util.Map; import io.flutter.plugin.common.BinaryMessenger; @@ -46,4 +50,24 @@ public void send(@NonNull String jsonCrash, @NonNull Boolean isHandled) { } } + @Override + public void sendNonFatalError(@NonNull String jsonCrash, @Nullable Map userAttributes, @Nullable String fingerprint, @NonNull String nonFatalExceptionLevel) { + try { + Method method = Reflection.getMethod(Class.forName("com.instabug.crash.CrashReporting"), "reportException", JSONObject.class, boolean.class, + Map.class, JSONObject.class, IBGNonFatalException.Level.class); + final JSONObject exceptionObject = new JSONObject(jsonCrash); + + JSONObject fingerprintObj = null; + if (fingerprint != null) { + fingerprintObj = getFingerprintObject(fingerprint); + } + IBGNonFatalException.Level nonFatalExceptionLevelType = ArgsRegistry.nonFatalExceptionLevel.get(nonFatalExceptionLevel); + if (method != null) { + method.invoke(null, exceptionObject, true, userAttributes, fingerprintObj, nonFatalExceptionLevelType); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } diff --git a/android/src/main/java/com/instabug/flutter/util/ArgsRegistry.java b/android/src/main/java/com/instabug/flutter/util/ArgsRegistry.java index a94485952..222a72836 100644 --- a/android/src/main/java/com/instabug/flutter/util/ArgsRegistry.java +++ b/android/src/main/java/com/instabug/flutter/util/ArgsRegistry.java @@ -2,6 +2,7 @@ import androidx.annotation.NonNull; +import com.instabug.crash.models.IBGNonFatalException; import com.instabug.library.LogLevel; import com.instabug.bug.BugReporting; import com.instabug.bug.invocation.Option; @@ -56,7 +57,12 @@ public T get(Object key) { put("ColorTheme.light", InstabugColorTheme.InstabugColorThemeLight); put("ColorTheme.dark", InstabugColorTheme.InstabugColorThemeDark); }}; - + public static ArgsMap nonFatalExceptionLevel = new ArgsMap() {{ + put("NonFatalExceptionLevel.critical", IBGNonFatalException.Level.CRITICAL); + put("NonFatalExceptionLevel.error", IBGNonFatalException.Level.ERROR); + put("NonFatalExceptionLevel.warning", IBGNonFatalException.Level.WARNING); + put("NonFatalExceptionLevel.info", IBGNonFatalException.Level.INFO); + }}; public static final ArgsMap floatingButtonEdges = new ArgsMap() {{ put("FloatingButtonEdge.left", InstabugFloatingButtonEdge.LEFT); put("FloatingButtonEdge.right", InstabugFloatingButtonEdge.RIGHT); diff --git a/android/src/test/java/com/instabug/flutter/CrashReportingApiTest.java b/android/src/test/java/com/instabug/flutter/CrashReportingApiTest.java index c51bdd8df..a538fc8b4 100644 --- a/android/src/test/java/com/instabug/flutter/CrashReportingApiTest.java +++ b/android/src/test/java/com/instabug/flutter/CrashReportingApiTest.java @@ -1,5 +1,6 @@ package com.instabug.flutter; +import static com.instabug.crash.CrashReporting.getFingerprintObject; import static com.instabug.flutter.util.GlobalMocks.reflected; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -7,8 +8,10 @@ import static org.mockito.Mockito.mockStatic; import com.instabug.crash.CrashReporting; +import com.instabug.crash.models.IBGNonFatalException; import com.instabug.flutter.generated.CrashReportingPigeon; import com.instabug.flutter.modules.CrashReportingApi; +import com.instabug.flutter.util.ArgsRegistry; import com.instabug.flutter.util.GlobalMocks; import com.instabug.flutter.util.MockReflected; import com.instabug.library.Feature; @@ -19,6 +22,9 @@ import org.junit.Test; import org.mockito.MockedStatic; +import java.util.HashMap; +import java.util.Map; + import io.flutter.plugin.common.BinaryMessenger; @@ -77,4 +83,19 @@ public void testSend() { reflected.verify(() -> MockReflected.crashReportException(any(JSONObject.class), eq(isHandled))); } + + @Test + public void testSendNonFatalError() { + String jsonCrash = "{}"; + boolean isHandled = true; + String fingerPrint = "test"; + + Map expectedUserAttributes = new HashMap<>(); + String level = ArgsRegistry.nonFatalExceptionLevel.keySet().iterator().next(); + JSONObject expectedFingerprint = getFingerprintObject(fingerPrint); + IBGNonFatalException.Level expectedLevel = ArgsRegistry.nonFatalExceptionLevel.get(level); + api.sendNonFatalError(jsonCrash, expectedUserAttributes, fingerPrint, level); + + reflected.verify(() -> MockReflected.crashReportException(any(JSONObject.class), eq(isHandled), eq(expectedUserAttributes), eq(expectedFingerprint), eq(expectedLevel))); + } } diff --git a/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java b/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java index 8a37d3ba1..c80436d36 100644 --- a/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java +++ b/android/src/test/java/com/instabug/flutter/util/GlobalMocks.java @@ -8,12 +8,15 @@ import android.net.Uri; import android.util.Log; +import com.instabug.crash.models.IBGNonFatalException; + import org.json.JSONObject; import org.mockito.MockedStatic; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.lang.reflect.Method; +import java.util.Map; public class GlobalMocks { public static MockedStatic threadManager; @@ -75,6 +78,15 @@ public static void setUp() throws NoSuchMethodException { JSONObject.class, boolean.class)) .thenReturn(mCrashReportException); + Method mCrashReportNonFatalException = MockReflected.class.getDeclaredMethod("crashReportException", JSONObject.class, boolean.class, + Map.class, JSONObject.class, IBGNonFatalException.Level.class); + mCrashReportNonFatalException.setAccessible(true); + reflection + .when(() -> Reflection.getMethod(Class.forName("com.instabug.crash.CrashReporting"), "reportException", + JSONObject.class, boolean.class, + Map.class, JSONObject.class, IBGNonFatalException.Level.class)) + .thenReturn(mCrashReportNonFatalException); + uri = mockStatic(Uri.class); uri.when(() -> Uri.fromFile(any())).thenReturn(mock(Uri.class)); } diff --git a/android/src/test/java/com/instabug/flutter/util/MockReflected.java b/android/src/test/java/com/instabug/flutter/util/MockReflected.java index cb81c4f1c..040b0853d 100644 --- a/android/src/test/java/com/instabug/flutter/util/MockReflected.java +++ b/android/src/test/java/com/instabug/flutter/util/MockReflected.java @@ -4,8 +4,12 @@ import androidx.annotation.Nullable; +import com.instabug.crash.models.IBGNonFatalException; + import org.json.JSONObject; +import java.util.Map; + /** * Includes fake implementations of methods called by reflection. * Used to verify whether or not a private methods was called. @@ -36,4 +40,6 @@ public static void apmNetworkLog(long requestStartTime, long requestDuration, St * CrashReporting.reportException */ public static void crashReportException(JSONObject exception, boolean isHandled) {} + public static void crashReportException(JSONObject exception, boolean isHandled, Map userAttributes, JSONObject fingerPrint, IBGNonFatalException.Level level) {} + } diff --git a/e2e/BugReportingTests.cs b/e2e/BugReportingTests.cs index 103ebecc9..f73608e2e 100644 --- a/e2e/BugReportingTests.cs +++ b/e2e/BugReportingTests.cs @@ -113,7 +113,7 @@ public void ManualInvocation() [Fact] public void MultipleScreenshotsInReproSteps() { - ScrollDown(); + ScrollDownLittle(); captain.FindByText("Enter screen name").Tap(); captain.Type("My Screen"); diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 4f8d4d245..8c6e56146 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/example/ios/InstabugTests/CrashReportingApiTests.m b/example/ios/InstabugTests/CrashReportingApiTests.m index ad4c3e48c..7b067b329 100644 --- a/example/ios/InstabugTests/CrashReportingApiTests.m +++ b/example/ios/InstabugTests/CrashReportingApiTests.m @@ -4,12 +4,13 @@ #import "Instabug/IBGCrashReporting.h" #import "Instabug/Instabug.h" #import "Util/Instabug+Test.h" +#import "Util/IBGCrashReporting+CP.h" @interface CrashReportingApiTests : XCTestCase -@property (nonatomic, strong) id mCrashReporting; -@property (nonatomic, strong) id mInstabug; -@property (nonatomic, strong) CrashReportingApi *api; +@property(nonatomic, strong) id mCrashReporting; +@property(nonatomic, strong) id mInstabug; +@property(nonatomic, strong) CrashReportingApi *api; @end @@ -24,9 +25,9 @@ - (void)setUp { - (void)testSetEnabled { NSNumber *isEnabled = @1; FlutterError *error; - + [self.api setEnabledIsEnabled:isEnabled error:&error]; - + OCMVerify([self.mCrashReporting setEnabled:YES]); } @@ -34,10 +35,32 @@ - (void)testSend { NSString *jsonCrash = @"{}"; NSNumber *isHandled = @0; FlutterError *error; - + [self.api sendJsonCrash:jsonCrash isHandled:isHandled error:&error]; + + OCMVerify([self.mCrashReporting cp_reportFatalCrashWithStackTrace:@{}]); +} - OCMVerify([self.mInstabug reportCrashWithStackTrace:@{} handled:isHandled]); + +- (void)testSendNonFatalErrorJsonCrash { + NSString *jsonCrash = @"{}"; + NSString *fingerPrint = @"fingerprint"; + NSDictionary *userAttributes = @{@"key": @"value",}; + NSString *ibgNonFatalLevel = @"NonFatalExceptionLevel.error"; + + FlutterError *error; + + [self.api sendNonFatalErrorJsonCrash:jsonCrash + userAttributes:userAttributes + fingerprint:fingerPrint + nonFatalExceptionLevel:ibgNonFatalLevel + error:&error]; + + OCMVerify([self.mCrashReporting cp_reportNonFatalCrashWithStackTrace:@{} + level:IBGNonFatalLevelError + groupingString:fingerPrint + userAttributes:userAttributes + ]); } @end diff --git a/example/ios/InstabugTests/Util/IBGCrashReporting+CP.h b/example/ios/InstabugTests/Util/IBGCrashReporting+CP.h new file mode 100644 index 000000000..4229dbcea --- /dev/null +++ b/example/ios/InstabugTests/Util/IBGCrashReporting+CP.h @@ -0,0 +1,13 @@ +#import + + +@interface IBGCrashReporting (CP) + ++ (void)cp_reportFatalCrashWithStackTrace:(NSDictionary*)stackTrace; + ++ (void)cp_reportNonFatalCrashWithStackTrace:(NSDictionary*)stackTrace + level:(IBGNonFatalLevel)level + groupingString:(NSString *)groupingString + userAttributes:(NSDictionary *)userAttributes; +@end + diff --git a/example/ios/Podfile b/example/ios/Podfile index 3a216ba41..287784283 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '11.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -26,7 +26,6 @@ end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup - target 'Runner' do use_frameworks! use_modular_headers! diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 00201cfdc..8d38697ec 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -28,6 +28,6 @@ SPEC CHECKSUMS: instabug_flutter: 8b86ee14635a4b0ebfb4f760a108c7b0606c47e4 OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 -PODFILE CHECKSUM: 637e800c0a0982493b68adb612d2dd60c15c8e5c +PODFILE CHECKSUM: 85507f53c31d3e834227ea88986033537c7c78b9 COCOAPODS: 1.13.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 10bac3aa7..ebd99c58a 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -92,6 +92,7 @@ A964F0D42132F93F7E4DEB73 /* Pods-InstabugUITests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugUITests.profile.xcconfig"; path = "Target Support Files/Pods-InstabugUITests/Pods-InstabugUITests.profile.xcconfig"; sourceTree = ""; }; B03C8370EEFE061BDDDA1DA1 /* Pods-InstabugUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugUITests.debug.xcconfig"; path = "Target Support Files/Pods-InstabugUITests/Pods-InstabugUITests.debug.xcconfig"; sourceTree = ""; }; BA5633844585BB93FE7BCCE7 /* Pods-InstabugTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugTests.profile.xcconfig"; path = "Target Support Files/Pods-InstabugTests/Pods-InstabugTests.profile.xcconfig"; sourceTree = ""; }; + BE26C80C2BD55575009FECCF /* IBGCrashReporting+CP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IBGCrashReporting+CP.h"; sourceTree = ""; }; BF9025BBD0A6FD7B193E903A /* Pods-InstabugTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InstabugTests.debug.xcconfig"; path = "Target Support Files/Pods-InstabugTests/Pods-InstabugTests.debug.xcconfig"; sourceTree = ""; }; C090017925D9A030006F3DAE /* InstabugTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstabugTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C090017D25D9A031006F3DAE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -261,6 +262,7 @@ CC78720A2938D1C5008CB2A5 /* Util */ = { isa = PBXGroup; children = ( + BE26C80C2BD55575009FECCF /* IBGCrashReporting+CP.h */, CC78720E293CA8EE008CB2A5 /* Instabug+Test.h */, CC787211293CAB28008CB2A5 /* IBGNetworkLogger+Test.h */, CC198C62293E2392007077C8 /* IBGSurvey+Test.h */, @@ -350,7 +352,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -453,10 +455,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -484,6 +488,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -681,7 +686,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -767,7 +772,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -816,7 +821,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1392808b8..0b15932d1 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Instabug needs access to your photo library so you can attach images. CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/main.dart b/example/lib/main.dart index 4ee2cccfd..2889f2ebc 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -48,7 +48,8 @@ void main() { ); FlutterError.onError = (FlutterErrorDetails details) { - Zone.current.handleUncaughtError(details.exception, details.stack!); + Zone.current.handleUncaughtError( + details.exception, details.stack ?? StackTrace.current); }; runApp(const MyApp()); diff --git a/example/lib/src/components/non_fatal_crashes_content.dart b/example/lib/src/components/non_fatal_crashes_content.dart index 863a3bac6..c3d331187 100644 --- a/example/lib/src/components/non_fatal_crashes_content.dart +++ b/example/lib/src/components/non_fatal_crashes_content.dart @@ -1,8 +1,13 @@ part of '../../main.dart'; -class NonFatalCrashesContent extends StatelessWidget { +class NonFatalCrashesContent extends StatefulWidget { const NonFatalCrashesContent({Key? key}) : super(key: key); + @override + State createState() => _NonFatalCrashesContentState(); +} + +class _NonFatalCrashesContentState extends State { void throwHandledException(dynamic error) { try { if (error is! Error) { @@ -19,6 +24,18 @@ class NonFatalCrashesContent extends StatelessWidget { } } + final crashFormKey = GlobalKey(); + + final crashNameController = TextEditingController(); + + final crashfingerPrintController = TextEditingController(); + + final crashUserAttributeKeyController = TextEditingController(); + + final crashUserAttributeValueController = TextEditingController(); + + NonFatalExceptionLevel crashType = NonFatalExceptionLevel.error; + @override Widget build(BuildContext context) { return Column( @@ -60,7 +77,124 @@ class NonFatalCrashesContent extends StatelessWidget { onPressed: InstabugFlutterExampleMethodChannel.sendNativeNonFatalCrash, ), + Form( + key: crashFormKey, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: InstabugTextField( + label: "Crash title", + controller: crashNameController, + validator: (value) { + if (value?.trim().isNotEmpty == true) return null; + + return 'this field is required'; + }, + )), + ], + ), + Row( + children: [ + Expanded( + child: InstabugTextField( + label: "User Attribute key", + controller: crashUserAttributeKeyController, + validator: (value) { + if (crashUserAttributeValueController.text.isNotEmpty) { + if (value?.trim().isNotEmpty == true) return null; + + return 'this field is required'; + } + return null; + }, + )), + Expanded( + child: InstabugTextField( + label: "User Attribute Value", + controller: crashUserAttributeValueController, + validator: (value) { + if (crashUserAttributeKeyController.text.isNotEmpty) { + if (value?.trim().isNotEmpty == true) return null; + + return 'this field is required'; + } + return null; + }, + )), + ], + ), + Row( + children: [ + Expanded( + child: InstabugTextField( + label: "Fingerprint", + controller: crashfingerPrintController, + )), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + Expanded( + flex: 5, + child: DropdownButtonHideUnderline( + child: + DropdownButtonFormField( + value: crashType, + decoration: const InputDecoration( + border: OutlineInputBorder(), isDense: true), + padding: EdgeInsets.zero, + items: NonFatalExceptionLevel.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.toString()), + )) + .toList(), + onChanged: (NonFatalExceptionLevel? value) { + crashType = value!; + }, + ), + )), + ], + ), + ), + SizedBox( + height: 8, + ), + InstabugButton( + text: 'Send Non Fatal Crash', + onPressed: sendNonFatalCrash, + ) + ], + ), + ), ], ); } + + void sendNonFatalCrash() { + if (crashFormKey.currentState?.validate() == true) { + Map? userAttributes = null; + if (crashUserAttributeKeyController.text.isNotEmpty) { + userAttributes = { + crashUserAttributeKeyController.text: + crashUserAttributeValueController.text + }; + } + CrashReporting.reportHandledCrash( + new Exception(crashNameController.text), null, + userAttributes: userAttributes, + fingerprint: crashfingerPrintController.text, + level: crashType); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Crash sent"))); + crashNameController.text = ''; + crashfingerPrintController.text = ''; + crashUserAttributeValueController.text = ''; + crashUserAttributeKeyController.text = ''; + } + } } diff --git a/example/lib/src/screens/crashes_page.dart b/example/lib/src/screens/crashes_page.dart index caa6b7f48..7e0ec8e76 100644 --- a/example/lib/src/screens/crashes_page.dart +++ b/example/lib/src/screens/crashes_page.dart @@ -16,7 +16,8 @@ class CrashesPage extends StatelessWidget { Text('Fatal Crashes can only be tested in release mode'), Text('Most of these buttons will crash the application'), FatalCrashesContent(), - ], + SectionTitle('Crash section'), + ], // This trailing comma makes auto-formatting nicer for build methods. ); } } diff --git a/example/lib/src/widget/instabug_text_field.dart b/example/lib/src/widget/instabug_text_field.dart index af2ff88a3..3d01cc623 100644 --- a/example/lib/src/widget/instabug_text_field.dart +++ b/example/lib/src/widget/instabug_text_field.dart @@ -8,6 +8,7 @@ class InstabugTextField extends StatelessWidget { this.labelStyle, this.margin, this.keyboardType, + this.validator, }) : super(key: key); final String label; @@ -15,6 +16,7 @@ class InstabugTextField extends StatelessWidget { final EdgeInsetsGeometry? margin; final TextStyle? labelStyle; final TextInputType? keyboardType; + final FormFieldValidator? validator; @override Widget build(BuildContext context) { @@ -23,9 +25,10 @@ class InstabugTextField extends StatelessWidget { const EdgeInsets.symmetric( horizontal: 20.0, ), - child: TextField( + child: TextFormField( controller: controller, keyboardType: keyboardType, + validator: validator, decoration: InputDecoration( labelText: label, labelStyle: labelStyle ?? Theme.of(context).textTheme.labelLarge, diff --git a/example/pubspec.lock b/example/pubspec.lock index 916470f9e..1fc868c83 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -107,7 +107,7 @@ packages: path: ".." relative: true source: path - version: "13.0.0" + version: "12.7.0" leak_tracker: dependency: transitive description: diff --git a/ios/Classes/Modules/CrashReportingApi.m b/ios/Classes/Modules/CrashReportingApi.m index a63ab4c0b..8d4e9b313 100644 --- a/ios/Classes/Modules/CrashReportingApi.m +++ b/ios/Classes/Modules/CrashReportingApi.m @@ -1,5 +1,7 @@ #import "Instabug.h" #import "CrashReportingApi.h" +#import "../Util/IBGCrashReporting+CP.h" +#import "ArgsRegistry.h" extern void InitCrashReportingApi(id messenger) { CrashReportingApi *api = [[CrashReportingApi alloc] init]; @@ -19,10 +21,29 @@ - (void)sendJsonCrash:(NSString *)jsonCrash isHandled:(NSNumber *)isHandled erro NSDictionary *stackTrace = [NSJSONSerialization JSONObjectWithData:objectData options:NSJSONReadingMutableContainers error:&jsonError]; - SEL reportCrashWithStackTraceSEL = NSSelectorFromString(@"reportCrashWithStackTrace:handled:"); - if ([[Instabug class] respondsToSelector:reportCrashWithStackTraceSEL]) { - [[Instabug class] performSelector:reportCrashWithStackTraceSEL withObject:stackTrace withObject:isHandled]; + BOOL isNonFatal = [isHandled boolValue]; + + if (isNonFatal) { + [IBGCrashReporting cp_reportNonFatalCrashWithStackTrace:stackTrace + level:IBGNonFatalLevelError groupingString:nil userAttributes:nil + ]; + } else { + [IBGCrashReporting cp_reportFatalCrashWithStackTrace:stackTrace ]; + } } +- (void)sendNonFatalErrorJsonCrash:(nonnull NSString *)jsonCrash userAttributes:(nullable NSDictionary *)userAttributes fingerprint:(nullable NSString *)fingerprint nonFatalExceptionLevel:(nonnull NSString *)nonFatalExceptionLevel error:(FlutterError * _Nullable __autoreleasing * _Nonnull)error { + NSError *jsonError; + NSData *objectData = [jsonCrash dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *stackTrace = [NSJSONSerialization JSONObjectWithData:objectData + options:NSJSONReadingMutableContainers + error:&jsonError]; + IBGNonFatalLevel level = (ArgsRegistry.nonFatalExceptionLevel[nonFatalExceptionLevel]).integerValue; + [IBGCrashReporting cp_reportNonFatalCrashWithStackTrace:stackTrace + level: level + groupingString:fingerprint + userAttributes:userAttributes]; + +} @end diff --git a/ios/Classes/Util/ArgsRegistry.h b/ios/Classes/Util/ArgsRegistry.h index d97b3fd23..a465365df 100644 --- a/ios/Classes/Util/ArgsRegistry.h +++ b/ios/Classes/Util/ArgsRegistry.h @@ -1,5 +1,5 @@ #import -#import +#import typedef NSDictionary ArgsDictionary; @@ -17,6 +17,8 @@ typedef NSDictionary ArgsDictionary; + (ArgsDictionary *)actionTypes; + (ArgsDictionary *)extendedBugReportStates; + (ArgsDictionary *)reproModes; ++ (ArgsDictionary *)nonFatalExceptionLevel; + + (ArgsDictionary *)locales; + (NSDictionary *)placeholders; diff --git a/ios/Classes/Util/ArgsRegistry.m b/ios/Classes/Util/ArgsRegistry.m index 2181a72e6..b8cdaa21a 100644 --- a/ios/Classes/Util/ArgsRegistry.m +++ b/ios/Classes/Util/ArgsRegistry.m @@ -93,6 +93,16 @@ + (ArgsDictionary *)extendedBugReportStates { @"ExtendedBugReportMode.disabled" : @(IBGExtendedBugReportModeDisabled), }; } ++ (ArgsDictionary *)nonFatalExceptionLevel { + return @{ + @"NonFatalExceptionLevel.info" : @(IBGNonFatalLevelInfo), + @"NonFatalExceptionLevel.error" : @(IBGNonFatalLevelError), + @"NonFatalExceptionLevel.warning" : @(IBGNonFatalLevelWarning), + @"NonFatalExceptionLevel.critical" : @(IBGNonFatalLevelCritical) + + + }; +} + (ArgsDictionary *)reproModes { return @{ diff --git a/ios/Classes/Util/IBGCrashReporting+CP.h b/ios/Classes/Util/IBGCrashReporting+CP.h new file mode 100644 index 000000000..4229dbcea --- /dev/null +++ b/ios/Classes/Util/IBGCrashReporting+CP.h @@ -0,0 +1,13 @@ +#import + + +@interface IBGCrashReporting (CP) + ++ (void)cp_reportFatalCrashWithStackTrace:(NSDictionary*)stackTrace; + ++ (void)cp_reportNonFatalCrashWithStackTrace:(NSDictionary*)stackTrace + level:(IBGNonFatalLevel)level + groupingString:(NSString *)groupingString + userAttributes:(NSDictionary *)userAttributes; +@end + diff --git a/lib/src/modules/crash_reporting.dart b/lib/src/modules/crash_reporting.dart index cf5c37884..e5d0a0e3e 100644 --- a/lib/src/modules/crash_reporting.dart +++ b/lib/src/modules/crash_reporting.dart @@ -10,6 +10,8 @@ import 'package:instabug_flutter/src/models/exception_data.dart'; import 'package:instabug_flutter/src/utils/ibg_build_info.dart'; import 'package:stack_trace/stack_trace.dart'; +enum NonFatalExceptionLevel { error, critical, info, warning } + class CrashReporting { static var _host = CrashReportingHostApi(); static bool enabled = true; @@ -42,10 +44,19 @@ class CrashReporting { /// [Object] exception /// [StackTrace] stack static Future reportHandledCrash( - Object exception, [ - StackTrace? stack, - ]) async { - await _sendCrash(exception, stack ?? StackTrace.current, true); + Object exception, + StackTrace? stack, { + Map? userAttributes, + String? fingerprint, + NonFatalExceptionLevel level = NonFatalExceptionLevel.error, + }) async { + await _sendHandledCrash( + exception, + stack ?? StackTrace.current, + userAttributes, + fingerprint, + level, + ); } static Future _reportUnhandledCrash( @@ -60,6 +71,32 @@ class CrashReporting { StackTrace stack, bool handled, ) async { + final crashData = getCrashDataFromException(stack, exception); + + return _host.send(jsonEncode(crashData), handled); + } + + static Future _sendHandledCrash( + Object exception, + StackTrace stack, + Map? userAttributes, + String? fingerprint, + NonFatalExceptionLevel? nonFatalExceptionLevel, + ) async { + final crashData = getCrashDataFromException(stack, exception); + + return _host.sendNonFatalError( + jsonEncode(crashData), + userAttributes, + fingerprint, + nonFatalExceptionLevel.toString(), + ); + } + + static CrashData getCrashDataFromException( + StackTrace stack, + Object exception, + ) { final trace = Trace.from(stack); final frames = trace.frames .map( @@ -77,7 +114,6 @@ class CrashReporting { message: exception.toString(), exception: frames, ); - - return _host.send(jsonEncode(crashData), handled); + return crashData; } } diff --git a/pigeons/crash_reporting.api.dart b/pigeons/crash_reporting.api.dart index 2149d89a4..45f4a9cdb 100644 --- a/pigeons/crash_reporting.api.dart +++ b/pigeons/crash_reporting.api.dart @@ -3,5 +3,13 @@ import 'package:pigeon/pigeon.dart'; @HostApi() abstract class CrashReportingHostApi { void setEnabled(bool isEnabled); + void send(String jsonCrash, bool isHandled); + + void sendNonFatalError( + String jsonCrash, + Map? userAttributes, + String? fingerprint, + String nonFatalExceptionLevel, + ); } diff --git a/test/crash_reporting_test.dart b/test/crash_reporting_test.dart index 7e07d6f69..a4ec87528 100644 --- a/test/crash_reporting_test.dart +++ b/test/crash_reporting_test.dart @@ -61,11 +61,25 @@ void main() { message: exception.toString(), exception: frames, ); + final userAttributes = {"name": "flutter"}; + const fingerPrint = "fingerprint"; + const level = NonFatalExceptionLevel.critical; - await CrashReporting.reportHandledCrash(exception, stack); + await CrashReporting.reportHandledCrash( + exception, + stack, + userAttributes: userAttributes, + fingerprint: fingerPrint, + level: level, + ); verify( - mHost.send(jsonEncode(data), true), + mHost.sendNonFatalError( + jsonEncode(data), + userAttributes, + fingerPrint, + level.toString(), + ), ).called(1); } });