diff --git a/.circleci/config.yml b/.circleci/config.yml index 38f161d29..353ea207a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -104,6 +104,9 @@ jobs: - run: name: Flutter build command: cd ..; flutter build aot + - run: + name: Update CocoaPods + command: pod update - run: name: Install CocoaPods command: pod install --repo-update diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de9a5d56..d2954d296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -## Master +## v9.1.6 (2020-07-13) +* Adds CrashReporting * Adds setShakingThresholdForiPhone, setShakingThresholdForiPad and setShakingThresholdForAndroid APIs * Added Proguard rules to protect Flutter bridge class and method names from getting obfuscated when the minifyEnabled flag is set to true. - ## v9.1.0 (2020-03-19) * Bump Native SDKs to v9.1 diff --git a/README.md b/README.md index fc49185df..a0b5ee4f2 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,13 @@ A Flutter plugin for [Instabug](https://instabug.com/). | Feature | Status | |:---------------------------------------------------------:|:-------:| | [Bug Reporting](https://instabug.com/bug-reporting) | ✅ | -| [Crash Reporting](https://instabug.com/crash-reporting) | ⚠ | +| [Crash Reporting](https://instabug.com/crash-reporting) | ✅ | | [In-App Chat](https://instabug.com/in-app-chat) | ✅ | | [In-App Surveys](https://instabug.com/in-app-surveys) | ✅ | | [Feature Requests](https://instabug.com/feature-requests) | ✅ | * ✅ Stable * ⚙️ Under active development -* ⚠ Not available yet ## Integration @@ -27,7 +26,7 @@ A Flutter plugin for [Instabug](https://instabug.com/). ```yaml dependencies: - instabug_flutter: + instabug_flutter: ``` 2. Install the package by running the following command. @@ -81,21 +80,43 @@ invocationEvents.add(InstabugFlutterPlugin.INVOCATION_EVENT_SHAKE); new InstabugFlutterPlugin().start(CustomFlutterApplication.this, "APP_TOKEN", invocationEvents); ``` -## Microphone and Photo Library Usage Description (iOS Only) +## Crash reporting -Instabug needs access to the microphone and photo library to be able to let users add audio and video attachments. Starting from iOS 10, apps that don’t provide a usage description for those 2 permissions would be rejected when submitted to the App Store. +Instabug automatically captures every crash of your app and sends relevant details to the crashes page of your dashboard. Crashes are reported only when the app is running in release mode -For your app not to be rejected, you’ll need to add the following 2 keys to your app’s info.plist file with text explaining to the user why those permissions are needed: +1. To start using Crash reporting, import the following into your `main.dart`. -* `NSMicrophoneUsageDescription` -* `NSPhotoLibraryUsageDescription` - -If your app doesn’t already access the microphone or photo library, we recommend using a usage description like: +```dart +import 'package:instabug_flutter/CrashReporting.dart'; +``` -* "`` needs access to the microphone to be able to attach voice notes." -* "`` needs access to your photo library for you to be able to attach images." +2. Replace `void main() => runApp(MyApp());` with the following snippet: +```dart +void main() async { + FlutterError.onError = (FlutterErrorDetails details) { + Zone.current.handleUncaughtError(details.exception, details.stack); + }; + runZoned>(() async { + runApp(MyApp()); + }, onError: (dynamic error, StackTrace stackTrace) { + CrashReporting.reportCrash(error, stackTrace); + }); +} +``` -**The permission alert for accessing the microphone/photo library will NOT appear unless users attempt to attach a voice note/photo while using Instabug.** +With Flutter 1.17 use this snipped instead: +```dart +void main() async { + FlutterError.onError = (FlutterErrorDetails details) { + Zone.current.handleUncaughtError(details.exception, details.stack); + }; + runZonedGuarded>(() async { + runApp(CrashyApp()); + }, (Object error, StackTrace stackTrace) { + CrashReporting.reportCrash(error, stackTrace); + }); +} +``` ## Network Logging You can choose to attach all your network requests to the reports being sent to the dashboard. To enable the feature when using the `dart:io` package `HttpClient`, use the custom Instabug client: @@ -111,4 +132,20 @@ client.getUrl(Uri.parse(URL)).then((request) async { }); ``` -We also support the packages `http` and `dio`. For details on how to enable network logging for these external packages, refer to the [Instabug Dart Http Adapter](https://github.com/Instabug/Instabug-Dart-http-Adapter) and the [Instabug Dio Interceptor](https://github.com/Instabug/Instabug-Dio-Interceptor) repositories. \ No newline at end of file +We also support the packages `http` and `dio`. For details on how to enable network logging for these external packages, refer to the [Instabug Dart Http Adapter](https://github.com/Instabug/Instabug-Dart-http-Adapter) and the [Instabug Dio Interceptor](https://github.com/Instabug/Instabug-Dio-Interceptor) repositories. + +## Microphone and Photo Library Usage Description (iOS Only) + +Instabug needs access to the microphone and photo library to be able to let users add audio and video attachments. Starting from iOS 10, apps that don’t provide a usage description for those 2 permissions would be rejected when submitted to the App Store. + +For your app not to be rejected, you’ll need to add the following 2 keys to your app’s info.plist file with text explaining to the user why those permissions are needed: + +* `NSMicrophoneUsageDescription` +* `NSPhotoLibraryUsageDescription` + +If your app doesn’t already access the microphone or photo library, we recommend using a usage description like: + +* "`` needs access to the microphone to be able to attach voice notes." +* "`` needs access to your photo library for you to be able to attach images." + +**The permission alert for accessing the microphone/photo library will NOT appear unless users attempt to attach a voice note/photo while using Instabug.** diff --git a/android/src/main/java/com/instabug/instabugflutter/InstabugFlutterPlugin.java b/android/src/main/java/com/instabug/instabugflutter/InstabugFlutterPlugin.java index a5b862da7..58cb1b922 100644 --- a/android/src/main/java/com/instabug/instabugflutter/InstabugFlutterPlugin.java +++ b/android/src/main/java/com/instabug/instabugflutter/InstabugFlutterPlugin.java @@ -11,6 +11,7 @@ import com.instabug.bug.invocation.Option; import com.instabug.chat.Chats; import com.instabug.chat.Replies; +import com.instabug.crash.CrashReporting; import com.instabug.featuresrequest.FeatureRequests; import com.instabug.library.Feature; import com.instabug.library.Instabug; @@ -38,6 +39,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -897,12 +899,53 @@ public void networkLog(HashMap jsonObject) throws JSONException } /** - * Reports that the screen has been changed (Repro Steps) the screen sent to - * this method will be the 'current view' on the dashboard + * Enables and disables automatic crash reporting. + * + * @param {boolean} isEnabled + */ + public void setCrashReportingEnabled(final boolean isEnabled) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (isEnabled) { + CrashReporting.setState(Feature.State.ENABLED); + } else { + CrashReporting.setState(Feature.State.DISABLED); + } + } + }); + } + + public void sendJSCrashByReflection(final String map, final boolean isHandled) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + try { + final JSONObject exceptionObject = new JSONObject(map); + Method method = getMethod(Class.forName("com.instabug.crash.CrashReporting"), "reportException", + JSONObject.class, boolean.class); + if (method != null) { + method.invoke(null, exceptionObject, isHandled); + Log.e("IBG-Flutter", exceptionObject.toString()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + /* + * + * Reports that the screen has been + * + * changed (Repro Steps) the screen sent to this method will be the 'current + * view' on the dashboard * * @param screenName string containing the screen name * */ + public void reportScreenChange(String screenName) { try { Method method = getMethod(Class.forName("com.instabug.library.Instabug"), "reportScreenChange", @@ -930,10 +973,10 @@ public void setReproStepsMode(String reproStepsMode) { } /** - * Sets the threshold value of the shake gesture for android devices. - * Default for android is an integer value equals 350. - * you could increase the shaking difficulty level by - * increasing the `350` value and vice versa + * Sets the threshold value of the shake gesture for android devices. Default + * for android is an integer value equals 350. you could increase the shaking + * difficulty level by increasing the `350` value and vice versa + * * @param androidThreshold Threshold for android devices. */ public void setShakingThresholdForAndroid(int androidThreshold) { diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 7dd0077f2..a7ce1b1b4 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -39,6 +39,7 @@ android { versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true } buildTypes { @@ -59,6 +60,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' + implementation 'com.android.support:multidex:1.0.3' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:rules:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..4887603cd --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/java/com/instabug/instabugflutterexample/CustomFlutterApplication.java b/example/android/app/src/main/java/com/instabug/instabugflutterexample/CustomFlutterApplication.java index 405dad8e9..62e9feb2c 100644 --- a/example/android/app/src/main/java/com/instabug/instabugflutterexample/CustomFlutterApplication.java +++ b/example/android/app/src/main/java/com/instabug/instabugflutterexample/CustomFlutterApplication.java @@ -12,7 +12,7 @@ public void onCreate() { ArrayList invocation_events = new ArrayList<>(); invocation_events.add(InstabugFlutterPlugin.INVOCATION_EVENT_FLOATING_BUTTON); InstabugFlutterPlugin instabug = new InstabugFlutterPlugin(); - instabug.start(CustomFlutterApplication.this, "efa41f402620b5654f2af2b86e387029", invocation_events); + instabug.start(CustomFlutterApplication.this, "2d355f559ea67051a56fce82603f8e41", invocation_events); instabug.setWelcomeMessageMode("WelcomeMessageMode.disabled"); } } \ No newline at end of file diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..4887603cd --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/ios/Flutter/Flutter.podspec b/example/ios/Flutter/Flutter.podspec new file mode 100644 index 000000000..5ca30416b --- /dev/null +++ b/example/ios/Flutter/Flutter.podspec @@ -0,0 +1,18 @@ +# +# NOTE: This podspec is NOT to be published. It is only used as a local source! +# + +Pod::Spec.new do |s| + s.name = 'Flutter' + s.version = '1.0.0' + s.summary = 'High-performance, high-fidelity mobile apps.' + s.description = <<-DESC +Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. + DESC + s.homepage = 'https://flutter.io' + s.license = { :type => 'MIT' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } + s.ios.deployment_target = '8.0' + s.vendored_frameworks = 'Flutter.framework' +end diff --git a/example/ios/Podfile b/example/ios/Podfile old mode 100755 new mode 100644 index e3781f9b1..d2efb35ea --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -17,16 +17,16 @@ def parse_KV_file(file, separator='=') pods_ary = [] skip_line_start_symbols = ["#", "/"] File.foreach(file_abs_path) { |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - pods_ary.push({:name => podname, :path => podpath}); + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + pods_ary.push({:name => podname, :path => podpath}); else - puts "Invalid plugin specification: #{line}" - end + puts "Invalid plugin specification: #{line}" + end } return pods_ary end @@ -36,7 +36,7 @@ target 'Runner' do # referring to absolute paths on developers' machines. system('rm -rf .symlinks') system('mkdir -p .symlinks/plugins') - + # Flutter Pods generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') if generated_xcode_build_settings.empty? @@ -49,7 +49,7 @@ target 'Runner' do pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) end } - + # Plugin Pods plugin_pods = parse_KV_file('../.flutter-plugins') plugin_pods.map { |p| @@ -95,4 +95,4 @@ post_install do |installer| end end - pod 'OCMock', '~> 3.4' +pod 'OCMock', '~> 3.4' \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 000000000..e59481a4d --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,33 @@ +PODS: + - Flutter (1.0.0) + - Instabug (9.1.4) + - instabug_flutter (0.0.1): + - Flutter + - Instabug (= 9.1.4) + - OCMock (3.6) + +DEPENDENCIES: + - Flutter (from `.symlinks/flutter/ios`) + - instabug_flutter (from `.symlinks/plugins/instabug_flutter/ios`) + - OCMock (~> 3.4) + +SPEC REPOS: + trunk: + - Instabug + - OCMock + +EXTERNAL SOURCES: + Flutter: + :path: ".symlinks/flutter/ios" + instabug_flutter: + :path: ".symlinks/plugins/instabug_flutter/ios" + +SPEC CHECKSUMS: + Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + Instabug: 6d8d38c329b8ae0a88bba4d148280e24bc0cf000 + instabug_flutter: 904f8ae866249364e68e2d0ca05fc903f2fb7573 + OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 + +PODFILE CHECKSUM: 8a47551d4cb50d1be73b4b5bed8629d9a9468757 + +COCOAPODS: 1.9.1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 857330c79..29161a006 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 8482656AE596FFDBB9DF71A5 /* libPods-instabug_flutter_exampleUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7686AAC4E87F788EFA4B4DF7 /* libPods-instabug_flutter_exampleUITests.a */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; @@ -60,11 +59,9 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3A508F7C2EFD090E37443073 /* Pods-instabug_flutter_exampleUITests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-instabug_flutter_exampleUITests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-instabug_flutter_exampleUITests/Pods-instabug_flutter_exampleUITests.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 44074344FC9C61EF69573882 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7686AAC4E87F788EFA4B4DF7 /* libPods-instabug_flutter_exampleUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-instabug_flutter_exampleUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -86,8 +83,6 @@ A0CE77B5227B03D600DE990D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A9EBB397F99FB54015C87211 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BAC4880E1C40979FFB98D02A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - C4EFA9F93AD902C2F06C4D8F /* Pods-instabug_flutter_exampleUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-instabug_flutter_exampleUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-instabug_flutter_exampleUITests/Pods-instabug_flutter_exampleUITests.debug.xcconfig"; sourceTree = ""; }; - C8EC0BACB54915010643CEB0 /* Pods-instabug_flutter_exampleUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-instabug_flutter_exampleUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-instabug_flutter_exampleUITests/Pods-instabug_flutter_exampleUITests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -105,7 +100,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8482656AE596FFDBB9DF71A5 /* libPods-instabug_flutter_exampleUITests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,9 +119,6 @@ 44074344FC9C61EF69573882 /* Pods-Runner.debug.xcconfig */, BAC4880E1C40979FFB98D02A /* Pods-Runner.release.xcconfig */, 8A66ED0D8D8B0E4F8599CDAA /* Pods-Runner.profile.xcconfig */, - C4EFA9F93AD902C2F06C4D8F /* Pods-instabug_flutter_exampleUITests.debug.xcconfig */, - C8EC0BACB54915010643CEB0 /* Pods-instabug_flutter_exampleUITests.release.xcconfig */, - 3A508F7C2EFD090E37443073 /* Pods-instabug_flutter_exampleUITests.profile.xcconfig */, ); name = Pods; sourceTree = ""; @@ -136,7 +127,6 @@ isa = PBXGroup; children = ( A9EBB397F99FB54015C87211 /* libPods-Runner.a */, - 7686AAC4E87F788EFA4B4DF7 /* libPods-instabug_flutter_exampleUITests.a */, ); name = Frameworks; sourceTree = ""; @@ -248,11 +238,9 @@ isa = PBXNativeTarget; buildConfigurationList = A0BE54D1230AC8B80016CD95 /* Build configuration list for PBXNativeTarget "instabug_flutter_exampleUITests" */; buildPhases = ( - BFBA706319A967066C2F0173 /* [CP] Check Pods Manifest.lock */, A0BE54C3230AC8B80016CD95 /* Sources */, A0BE54C4230AC8B80016CD95 /* Frameworks */, A0BE54C5230AC8B80016CD95 /* Resources */, - 7CD369CAED44EFE9092F87AE /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -294,15 +282,19 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = XKKDNP2YZU; + ProvisioningStyle = Automatic; }; A0BE54C6230AC8B80016CD95 = { CreatedOnToolsVersion = 10.1; + DevelopmentTeam = XKKDNP2YZU; LastSwiftMigration = 1010; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; A0CE77B0227B03D600DE990D = { CreatedOnToolsVersion = 10.1; + DevelopmentTeam = XKKDNP2YZU; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -313,6 +305,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -364,16 +357,9 @@ files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", - "${PODS_ROOT}/Instabug/Instabug.framework", - "${PODS_ROOT}/Instabug/Instabug.framework.dSYM", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Instabug.framework", - "${DWARF_DSYM_FOLDER_PATH}/Instabug.framework.dSYM", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -416,28 +402,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 7CD369CAED44EFE9092F87AE /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-instabug_flutter_exampleUITests/Pods-instabug_flutter_exampleUITests-frameworks.sh", - "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", - "${PODS_ROOT}/Instabug/Instabug.framework", - "${PODS_ROOT}/Instabug/Instabug.framework.dSYM", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Instabug.framework", - "${DWARF_DSYM_FOLDER_PATH}/Instabug.framework.dSYM", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-instabug_flutter_exampleUITests/Pods-instabug_flutter_exampleUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -452,28 +416,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - BFBA706319A967066C2F0173 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-instabug_flutter_exampleUITests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -592,8 +534,10 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = S8QB4VV633; + DEVELOPMENT_TEAM = XKKDNP2YZU; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -607,6 +551,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.instabug.instabugFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -719,7 +664,10 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = XKKDNP2YZU; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -733,6 +681,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.instabug.instabugFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -743,7 +692,10 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = XKKDNP2YZU; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -757,13 +709,13 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.instabug.instabugFlutterExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; A0BE54CE230AC8B80016CD95 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C4EFA9F93AD902C2F06C4D8F /* Pods-instabug_flutter_exampleUITests.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -775,6 +727,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = XKKDNP2YZU; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = instabug_flutter_exampleUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.1; @@ -793,7 +746,6 @@ }; A0BE54CF230AC8B80016CD95 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C8EC0BACB54915010643CEB0 /* Pods-instabug_flutter_exampleUITests.release.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -805,6 +757,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = XKKDNP2YZU; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = instabug_flutter_exampleUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.1; @@ -821,7 +774,6 @@ }; A0BE54D0230AC8B80016CD95 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3A508F7C2EFD090E37443073 /* Pods-instabug_flutter_exampleUITests.profile.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -833,6 +785,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = XKKDNP2YZU; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = instabug_flutter_exampleUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.1; @@ -860,6 +813,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = XKKDNP2YZU; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = instabug_flutter_exampleTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.1; @@ -886,6 +840,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = XKKDNP2YZU; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = instabug_flutter_exampleTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.1; @@ -911,6 +866,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = XKKDNP2YZU; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = instabug_flutter_exampleTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.1; diff --git a/example/lib/main.dart b/example/lib/main.dart index 62e819dc2..8612d4ea7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,14 +1,23 @@ import 'dart:async'; import 'dart:io' show Platform; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:instabug_flutter/Instabug.dart'; import 'package:instabug_flutter/BugReporting.dart'; import 'package:instabug_flutter/Surveys.dart'; import 'package:instabug_flutter/FeatureRequests.dart'; -import 'package:instabug_flutter/Chats.dart'; +import 'package:instabug_flutter/CrashReporting.dart'; -void main() => runApp(MyApp()); +void main() async { + FlutterError.onError = (FlutterErrorDetails details) { + Zone.current.handleUncaughtError(details.exception, details.stack); + }; + + runZoned>(() async { + runApp(MyApp()); + }, onError: (dynamic error, StackTrace stackTrace) { + CrashReporting.reportCrash(error, stackTrace); + }); +} class MyApp extends StatefulWidget { @override diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index f26e091ab..747db1da3 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -8,20 +8,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:instabug_flutter_example/main.dart'; +import 'package:example/main.dart'; void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MyApp()); - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data.startsWith('Running on:'), - ), - findsOneWidget, - ); + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); }); } diff --git a/ios/Classes/InstabugFlutterPlugin.m b/ios/Classes/InstabugFlutterPlugin.m index be77f4ac3..23b17b082 100644 --- a/ios/Classes/InstabugFlutterPlugin.m +++ b/ios/Classes/InstabugFlutterPlugin.m @@ -59,13 +59,14 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + (void)startWithToken:(NSString *)token invocationEvents:(NSArray*)invocationEventsArray { SEL setPrivateApiSEL = NSSelectorFromString(@"setCurrentPlatform:"); if ([[Instabug class] respondsToSelector:setPrivateApiSEL]) { - NSInteger *platformId = IBGPlatformFlutter; + NSInteger *platformID = IBGPlatformFlutter; NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[Instabug class] methodSignatureForSelector:setPrivateApiSEL]]; [inv setSelector:setPrivateApiSEL]; [inv setTarget:[Instabug class]]; - [inv setArgument:&(platformId) atIndex:2]; + [inv setArgument:&(platformID) atIndex:2]; [inv invoke]; } + NSDictionary *constants = [self constants]; NSInteger invocationEvents = IBGInvocationEventNone; for (NSString * invocationEvent in invocationEventsArray) { @@ -772,7 +773,28 @@ + (void)networkLog:(NSDictionary *) networkData { } /** - * Reports that the screen has been changed (Repro Steps) the screen sent to this method will be the 'current view' on the dashboard + * Enables and disables automatic crash reporting. + * @param {boolean} isEnabled + */ ++ (void)setCrashReportingEnabled:(NSNumber *)isEnabled { + BOOL boolValue = [isEnabled boolValue]; + IBGCrashReporting.enabled = boolValue; +} + ++ (void)sendJSCrashByReflection:(NSString *) jsonString handled: (NSNumber *) isHandled{ + NSError *jsonError; + NSData *objectData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + 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]; + } + +} + + /** Reports that the screen has been changed (Repro Steps) the screen sent to this method will be the 'current view' on the dashboard * * @param screenName string containing the screen name * diff --git a/lib/CrashReporting.dart b/lib/CrashReporting.dart new file mode 100644 index 000000000..638f07b38 --- /dev/null +++ b/lib/CrashReporting.dart @@ -0,0 +1,67 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' show Platform, exit; +import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; +import 'package:instabug_flutter/models/crash_data.dart'; +import 'package:instabug_flutter/models/exception_data.dart'; +import 'package:stack_trace/stack_trace.dart'; + +class CrashReporting { + static const MethodChannel _channel = MethodChannel('instabug_flutter'); + static bool enabled = true; + static Future get platformVersion async { + final String version = await _channel.invokeMethod('getPlatformVersion'); + return version; + } + + ///Enables and disables Enables and disables automatic crash reporting. + /// [boolean] isEnabled + static void setEnabled(bool isEnabled) async { + enabled = isEnabled; + final List params = [isEnabled]; + await _channel.invokeMethod('setCrashReportingEnabled:', params); + } + + static void reportCrash(dynamic exception, StackTrace stack) async { + if (kReleaseMode && enabled) { + _reportUnhandledCrash(exception, stack); + } else { + FlutterError.dumpErrorToConsole( + FlutterErrorDetails(stack: stack, exception: exception)); + } + } + + /// Reports a handled crash to you dashboard + /// [dynamic] exception + /// [StackTrace] stack + static void reportHandledCrash(dynamic exception, [StackTrace stack]) async { + if (stack != null) { + _sendCrash(exception, stack, true); + } else { + _sendCrash(exception, StackTrace.current, true); + } + } + + static void _reportUnhandledCrash(dynamic exception, StackTrace stack) async { + _sendCrash(exception, stack, false); + } + + static void _sendCrash( + dynamic exception, StackTrace stack, bool handled) async { + final Trace trace = Trace.from(stack); + final List frames = []; + for (int i = 0; i < trace.frames.length; i++) { + frames.add(ExceptionData( + trace.frames[i].uri.toString(), + trace.frames[i].member, + trace.frames[i].line, + trace.frames[i].column == null ? 0 : trace.frames[i].column)); + } + final CrashData crashData = CrashData( + exception.toString(), Platform.operatingSystem.toString(), frames); + final List params = [jsonEncode(crashData), handled]; + await _channel.invokeMethod( + 'sendJSCrashByReflection:handled:', params); + } +} diff --git a/lib/models/crash_data.dart b/lib/models/crash_data.dart new file mode 100644 index 000000000..1e017b402 --- /dev/null +++ b/lib/models/crash_data.dart @@ -0,0 +1,26 @@ +import 'package:instabug_flutter/models/exception_data.dart'; + +class CrashData { + CrashData(this.message, this.os, this.exception); + + String message; + String os; + String platform = 'flutter'; + List exception; + + Map toJson() => { + 'message': message, + 'os': os, + 'platform': platform, + 'exception': exception, + }; + + Map toMap() { + final Map map = {}; + map['message'] = message; + map['os'] = os; + map['platform'] = platform; + map['exception'] = exception; + return map; + } +} diff --git a/lib/models/exception_data.dart b/lib/models/exception_data.dart new file mode 100644 index 000000000..99147c38c --- /dev/null +++ b/lib/models/exception_data.dart @@ -0,0 +1,29 @@ + +class ExceptionData { + + ExceptionData(this.file, this.methodName, this.lineNumber, this.column); + + String file; + String methodName; + int lineNumber; + int column; + + Map toJson() => + { + 'file': file, + 'methodName': methodName, + 'arguments': [], + 'lineNumber': lineNumber, + 'column': column, + }; + + Map toMap() { + final Map map = {}; + map['file'] = file; + map['methodName'] = methodName; + map['arguments'] = []; + map['lineNumber'] = lineNumber; + map['column'] = column; + return map; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 680d16e40..65e97bdac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: instabug_flutter -version: 9.1.0 +version: 9.1.6 description: >- Instabug is an in-app feedback and bug reporting tool for mobile apps. With just a simple shake, your users or beta testers can report bugs or diff --git a/test/instabug_flutter_test.dart b/test/instabug_flutter_test.dart index a41f89a64..5eb4e275b 100644 --- a/test/instabug_flutter_test.dart +++ b/test/instabug_flutter_test.dart @@ -1,8 +1,10 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/cupertino.dart'; +import 'package:instabug_flutter/CrashReporting.dart'; import 'package:instabug_flutter/Instabug.dart'; import 'package:instabug_flutter/BugReporting.dart'; import 'package:instabug_flutter/InstabugLog.dart'; @@ -12,6 +14,9 @@ import 'package:instabug_flutter/Surveys.dart'; import 'package:instabug_flutter/FeatureRequests.dart'; import 'package:instabug_flutter/Chats.dart'; import 'package:instabug_flutter/Replies.dart'; +import 'package:instabug_flutter/models/crash_data.dart'; +import 'package:instabug_flutter/models/exception_data.dart'; +import 'package:stack_trace/stack_trace.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -683,6 +688,46 @@ void main() { ]); }); + test('setCrashReportingEnabled: Test', () async { + bool isEnabled = false; + final List args = [isEnabled]; + CrashReporting.setEnabled(isEnabled); + expect(log, [ + isMethodCall( + 'setCrashReportingEnabled:', + arguments: args, + ) + ]); + }); + + test('sendJSCrashByReflection:handled: Test', () async { + try { + final List params = [1, 2]; + params[5] = 2; + } catch (exception, stack) { + const bool handled = true; + final Trace trace = Trace.from(stack); + final List frames = []; + for (int i = 0; i < trace.frames.length; i++) { + frames.add(ExceptionData( + trace.frames[i].uri.toString(), + trace.frames[i].member, + trace.frames[i].line, + trace.frames[i].column == null ? 0 : trace.frames[i].column)); + } + CrashData crashData = CrashData( + exception.toString(), Platform.operatingSystem.toString(), frames); + final List args = [jsonEncode(crashData), handled]; + CrashReporting.reportHandledCrash(exception, stack); + expect(log, [ + isMethodCall( + 'sendJSCrashByReflection:handled:', + arguments: args, + ) + ]); + } + }); + ///Since the below method only runs on android and has the [Platform.isAndroid] condition in it, it will fail when running outside android, /// therefore its commented. // test('setEnableInAppNotificationSound: Test', () async {