Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
54b7edf
fix(crashlytics, iOS): reorder error reason logging to match Android …
SelaseKay Sep 11, 2025
9f4ef22
feat(crashlytics): add test event channel and CI detection for error …
SelaseKay Sep 22, 2025
3348923
fix(tests): update imports and correct test formatting for consistency
SelaseKay Sep 22, 2025
dd5ed15
fix(tests): correct import path and streamline event stream listener …
SelaseKay Sep 22, 2025
210ed6a
fix(tests): improve event channel declaration and enhance event strea…
SelaseKay Sep 22, 2025
e7a95e5
feat(crashlytics): add test event channel and modify CI detection logic
SelaseKay Sep 22, 2025
fd1b46f
fix(crashlytics): ensure main thread execution for test event success…
SelaseKay Sep 22, 2025
ba8f793
fix(crashlytics): correct formatting of crashlytics error reason for …
SelaseKay Sep 22, 2025
c2e9139
refactor(crashlytics): remove CI check from error reporting in Androi…
SelaseKay Sep 22, 2025
c0ba352
refactor(tests): comment out delay in crashlytics e2e test for faster…
SelaseKay Sep 22, 2025
2312d96
fix(tests): update event handling in crashlytics e2e test for accurat…
SelaseKay Sep 23, 2025
5ce041d
fix(tests): add missing import for async functionality in crashlytics…
SelaseKay Sep 23, 2025
4f36df7
fix(tests): add logging for received events in crashlytics e2e test
SelaseKay Sep 23, 2025
272d567
Merge branch 'main' into crashlytics_17711
SelaseKay Oct 23, 2025
eb2da85
chore: ensure testEventSink is not null before posting error reason
SelaseKay Oct 23, 2025
32020a2
chore(tests): remove unused import from firebase_crashlytics_e2e_test…
SelaseKay Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand All @@ -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
Expand All @@ -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<Map<String, Object>> checkForUnsentReports() {
Expand Down Expand Up @@ -134,6 +144,7 @@ private Task<Map<String, Object>> didCrashOnPreviousExecution() {

private Task<Void> recordError(final Map<String, Object> arguments) {
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
Handler mainHandler = new Handler(Looper.getMainLooper());

cachedThreadPool.execute(
() -> {
Expand All @@ -160,8 +171,12 @@ private Task<Void> recordError(final Map<String, Object> arguments) {

Exception exception;
if (reason != null) {
final String crashlyticsErrorReason = "thrown " + reason;
if (testEventSink != null) {
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 {
Expand Down Expand Up @@ -466,4 +481,14 @@ public Task<Void> didReinitializeFirebaseCore() {

return taskCompletionSource.getTask();
}

@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
testEventSink = events;
}

@Override
public void onCancel(Object arguments) {
testEventSink = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -34,6 +36,11 @@
NSString *const kCrashlyticsArgumentUnsentReports = @"unsentReports";
NSString *const kCrashlyticsArgumentDidCrashOnPreviousExecution = @"didCrashOnPreviousExecution";

@interface FLTFirebaseCrashlyticsPlugin () <FlutterStreamHandler>
@property(nonatomic, strong) FlutterEventChannel *testEventChannel;
@property(nonatomic, strong) FlutterEventSink testEventSink;
@end

@implementation FLTFirebaseCrashlyticsPlugin

#pragma mark - FlutterPlugin
Expand Down Expand Up @@ -61,6 +68,10 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)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 {
Expand Down Expand Up @@ -126,10 +137,15 @@ - (void)recordError:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallRes
}

if (![reason isEqual:[NSNull null]]) {
reason = [NSString stringWithFormat:@"%@. Error thrown %@.", dartExceptionMessage, reason];
NSString *crashlyticsErrorReason = [NSString stringWithFormat:@"thrown %@", reason];

if (self.testEventSink) {
self.testEventSink(crashlyticsErrorReason);
}
// Log additional custom value to match Android.
[[FIRCrashlytics crashlytics] setCustomValue:[NSString stringWithFormat:@"thrown %@", reason]
[[FIRCrashlytics crashlytics] setCustomValue:crashlyticsErrorReason
forKey:@"flutter_error_reason"];
reason = [NSString stringWithFormat:@"%@. Error thrown %@.", dartExceptionMessage, reason];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test to test the output? and make sure it matches between the platforms?

} else {
reason = dartExceptionMessage;
}
Expand Down Expand Up @@ -247,4 +263,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
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
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 'dart:async';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -98,6 +100,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<String>();

final subscription = eventStream.listen((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 foo reason');
await subscription.cancel();
},
skip: kIsWeb || defaultTargetPlatform == TargetPlatform.macOS,
);
});

group('log', () {
Expand Down
Loading