diff --git a/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/FlutterFirebaseCrashlyticsPlugin.java b/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/FlutterFirebaseCrashlyticsPlugin.java index e02fe5f6a1a3..a854cea95bf6 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/FlutterFirebaseCrashlyticsPlugin.java +++ b/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/FlutterFirebaseCrashlyticsPlugin.java @@ -21,6 +21,7 @@ import com.google.firebase.crashlytics.internal.Logger; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -34,9 +35,11 @@ /** FlutterFirebaseCrashlyticsPlugin */ public class FlutterFirebaseCrashlyticsPlugin - implements FlutterFirebasePlugin, FlutterPlugin, MethodCallHandler { + implements FlutterFirebasePlugin, FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { public static final String TAG = "FLTFirebaseCrashlytics"; private MethodChannel channel; + private EventChannel testEventChannel; + private EventChannel.EventSink testEventSink; private static final String FIREBASE_CRASHLYTICS_COLLECTION_ENABLED = "firebase_crashlytics_collection_enabled"; @@ -46,6 +49,9 @@ private void initInstance(BinaryMessenger messenger) { channel = new MethodChannel(messenger, channelName); channel.setMethodCallHandler(this); FlutterFirebasePluginRegistry.registerPlugin(channelName, this); + testEventChannel = + new EventChannel(messenger, "plugins.flutter.io/firebase_crashlytics_test_stream"); + testEventChannel.setStreamHandler(this); } @Override @@ -59,6 +65,10 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); channel = null; } + if (testEventChannel != null) { + testEventChannel.setStreamHandler(null); + testEventChannel = null; + } } private Task> checkForUnsentReports() { @@ -134,6 +144,7 @@ private Task> didCrashOnPreviousExecution() { private Task recordError(final Map arguments) { TaskCompletionSource taskCompletionSource = new TaskCompletionSource<>(); + Handler mainHandler = new Handler(Looper.getMainLooper()); cachedThreadPool.execute( () -> { @@ -160,8 +171,10 @@ private Task recordError(final Map arguments) { Exception exception; if (reason != null) { + final String crashlyticsErrorReason = "thrown " + reason; + mainHandler.post(() -> testEventSink.success(crashlyticsErrorReason)); // Set a "reason" (to match iOS) to show where the exception was thrown. - crashlytics.setCustomKey(Constants.FLUTTER_ERROR_REASON, "thrown " + reason); + crashlytics.setCustomKey(Constants.FLUTTER_ERROR_REASON, crashlyticsErrorReason); exception = new FlutterError(dartExceptionMessage + ". " + "Error thrown " + reason + "."); } else { @@ -466,4 +479,14 @@ public Task didReinitializeFirebaseCore() { return taskCompletionSource.getTask(); } + + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + testEventSink = events; + } + + @Override + public void onCancel(Object arguments) { + testEventSink = null; + } } diff --git a/packages/firebase_crashlytics/firebase_crashlytics/ios/firebase_crashlytics/Sources/firebase_crashlytics/FLTFirebaseCrashlyticsPlugin.m b/packages/firebase_crashlytics/firebase_crashlytics/ios/firebase_crashlytics/Sources/firebase_crashlytics/FLTFirebaseCrashlyticsPlugin.m index 79cf8414b416..6f67de86bc4b 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/ios/firebase_crashlytics/Sources/firebase_crashlytics/FLTFirebaseCrashlyticsPlugin.m +++ b/packages/firebase_crashlytics/firebase_crashlytics/ios/firebase_crashlytics/Sources/firebase_crashlytics/FLTFirebaseCrashlyticsPlugin.m @@ -15,6 +15,8 @@ #endif NSString *const kFLTFirebaseCrashlyticsChannelName = @"plugins.flutter.io/firebase_crashlytics"; +NSString *const kFLTFirebaseCrashlyticsTestChannelName = + @"plugins.flutter.io/firebase_crashlytics_test_stream"; // Argument Keys NSString *const kCrashlyticsArgumentException = @"exception"; @@ -34,6 +36,11 @@ NSString *const kCrashlyticsArgumentUnsentReports = @"unsentReports"; NSString *const kCrashlyticsArgumentDidCrashOnPreviousExecution = @"didCrashOnPreviousExecution"; +@interface FLTFirebaseCrashlyticsPlugin () +@property(nonatomic, strong) FlutterEventChannel *testEventChannel; +@property(nonatomic, strong) FlutterEventSink testEventSink; +@end + @implementation FLTFirebaseCrashlyticsPlugin #pragma mark - FlutterPlugin @@ -61,6 +68,10 @@ + (void)registerWithRegistrar:(NSObject *)registrar { binaryMessenger:[registrar messenger]]; FLTFirebaseCrashlyticsPlugin *instance = [FLTFirebaseCrashlyticsPlugin sharedInstance]; [registrar addMethodCallDelegate:instance channel:channel]; + instance.testEventChannel = + [FlutterEventChannel eventChannelWithName:kFLTFirebaseCrashlyticsTestChannelName + binaryMessenger:[registrar messenger]]; + [instance.testEventChannel setStreamHandler:instance]; } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutterResult { @@ -126,10 +137,13 @@ - (void)recordError:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallRes } if (![reason isEqual:[NSNull null]]) { - reason = [NSString stringWithFormat:@"%@. Error thrown %@.", dartExceptionMessage, reason]; + NSString *crashlyticsErrorReason = [NSString stringWithFormat:@"thrown %@", reason]; + + self.testEventSink(crashlyticsErrorReason); // Log additional custom value to match Android. [[FIRCrashlytics crashlytics] setCustomValue:[NSString stringWithFormat:@"thrown %@", reason] forKey:@"flutter_error_reason"]; + reason = [NSString stringWithFormat:@"%@. Error thrown %@.", dartExceptionMessage, reason]; } else { reason = dartExceptionMessage; } @@ -247,4 +261,15 @@ - (NSString *_Nonnull)flutterChannelName { return kFLTFirebaseCrashlyticsChannelName; } +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + self.testEventSink = nil; + return nil; +} + +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)events { + self.testEventSink = events; + return nil; +} + @end diff --git a/tests/integration_test/firebase_crashlytics/firebase_crashlytics_e2e_test.dart b/tests/integration_test/firebase_crashlytics/firebase_crashlytics_e2e_test.dart index 9d73803b78a9..33c64e6c8de8 100644 --- a/tests/integration_test/firebase_crashlytics/firebase_crashlytics_e2e_test.dart +++ b/tests/integration_test/firebase_crashlytics/firebase_crashlytics_e2e_test.dart @@ -5,9 +5,12 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:tests/firebase_options.dart'; +import '../e2e_test.dart'; +import 'dart:async'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -98,6 +101,31 @@ void main() { ); }, ); + + test( + 'should have consistent error reason format', + () async { + const eventChannel = EventChannel('plugins.flutter.io/firebase_crashlytics_test_stream'); + final eventStream = eventChannel.receiveBroadcastStream(); + + final completer = Completer(); + + eventStream.listen((event) { + print('Received event: $event'); + completer.complete(event.toString()); + }); + + await FirebaseCrashlytics.instance.recordError( + 'foo exception', + StackTrace.fromString('during testing'), + reason: 'foo reason', + ); + + final event = await completer.future; + expect(event, 'thrown foooo reason'); + }, + skip: kIsWeb || defaultTargetPlatform == TargetPlatform.macOS || !isCI, + ); }); group('log', () {