From d5778f89700a910f4b96b834975f7e21e43080fc Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 2 Dec 2024 10:22:54 -0500 Subject: [PATCH] feat(crashlytics, android): Support deferred component crash stack trace (#16789) --- .../FlutterFirebaseCrashlyticsInternal.java | 15 +++++++++++++-- .../plugins/firebase/crashlytics/Constants.java | 1 + .../FlutterFirebaseCrashlyticsPlugin.java | 4 ++++ .../lib/src/firebase_crashlytics.dart | 2 ++ .../firebase_crashlytics/lib/src/utils.dart | 9 +++++++++ .../test/firebase_crashlytics_test.dart | 2 ++ .../method_channel_crashlytics.dart | 2 ++ .../platform_interface_crashlytics.dart | 1 + .../method_channel_crashlytics_test.dart | 2 ++ 9 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/com/google/firebase/crashlytics/FlutterFirebaseCrashlyticsInternal.java b/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/com/google/firebase/crashlytics/FlutterFirebaseCrashlyticsInternal.java index f6b337a7fa61..5649c1936e45 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/com/google/firebase/crashlytics/FlutterFirebaseCrashlyticsInternal.java +++ b/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/com/google/firebase/crashlytics/FlutterFirebaseCrashlyticsInternal.java @@ -8,10 +8,12 @@ import android.annotation.SuppressLint; import com.google.firebase.crashlytics.internal.Logger; +import java.util.List; /** @hide */ public final class FlutterFirebaseCrashlyticsInternal { - private static final String FLUTTER_BUILD_ID_KEY = "com.crashlytics.flutter.build-id.0"; + private static final String LOADING_UNIT_KEY = "com.crashlytics.flutter.build-id."; + private static final String FLUTTER_BUILD_ID_DEFAULT_KEY = LOADING_UNIT_KEY + 0; @SuppressLint("VisibleForTests") public static void recordFatalException(Throwable throwable) { @@ -24,7 +26,16 @@ public static void recordFatalException(Throwable throwable) { @SuppressLint("VisibleForTests") public static void setFlutterBuildId(String buildId) { - FirebaseCrashlytics.getInstance().core.setInternalKey(FLUTTER_BUILD_ID_KEY, buildId); + FirebaseCrashlytics.getInstance().core.setInternalKey(FLUTTER_BUILD_ID_DEFAULT_KEY, buildId); + } + + @SuppressLint("VisibleForTests") + public static void setLoadingUnits(List loadingUnits) { + int unit = 0; + for (String loadingUnit : loadingUnits) { + unit++; + FirebaseCrashlytics.getInstance().core.setInternalKey(LOADING_UNIT_KEY + unit, loadingUnit); + } } private FlutterFirebaseCrashlyticsInternal() {} diff --git a/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/Constants.java b/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/Constants.java index 01b3f50c2be9..8cdba63ee63a 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/Constants.java +++ b/packages/firebase_crashlytics/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/Constants.java @@ -27,6 +27,7 @@ public class Constants { public static final String IS_CRASHLYTICS_COLLECTION_ENABLED = "isCrashlyticsCollectionEnabled"; public static final String FATAL = "fatal"; public static final String BUILD_ID = "buildId"; + public static final String LOADING_UNITS = "loadingUnits"; public static final String TIMESTAMP = "timestamp"; public static final String FIREBASE_APPLICATION_EXCEPTION = "_ae"; public static final String CRASH_EVENT_KEY = "com.firebase.crashlytics.flutter.fatal"; 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 b015efaf764b..20364a1b1ed9 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 @@ -148,11 +148,15 @@ private Task recordError(final Map arguments) { final boolean fatal = (boolean) Objects.requireNonNull(arguments.get(Constants.FATAL)); final String buildId = (String) Objects.requireNonNull(arguments.get(Constants.BUILD_ID)); + final List loadingUnits = + (List) Objects.requireNonNull(arguments.get(Constants.LOADING_UNITS)); if (buildId.length() > 0) { FlutterFirebaseCrashlyticsInternal.setFlutterBuildId(buildId); } + FlutterFirebaseCrashlyticsInternal.setLoadingUnits(loadingUnits); + Exception exception; if (reason != null) { // Set a "reason" (to match iOS) to show where the exception was thrown. diff --git a/packages/firebase_crashlytics/firebase_crashlytics/lib/src/firebase_crashlytics.dart b/packages/firebase_crashlytics/firebase_crashlytics/lib/src/firebase_crashlytics.dart index 19965029f9f7..a43208f8563b 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/lib/src/firebase_crashlytics.dart +++ b/packages/firebase_crashlytics/firebase_crashlytics/lib/src/firebase_crashlytics.dart @@ -123,6 +123,7 @@ class FirebaseCrashlytics extends FirebasePluginPlatform { final List> stackTraceElements = getStackTraceElements(stackTrace); final String? buildId = getBuildId(stackTrace); + final List loadingUnits = getLoadingUnits(stackTrace); return _delegate.recordError( exception: exception.toString(), @@ -130,6 +131,7 @@ class FirebaseCrashlytics extends FirebasePluginPlatform { information: _information, stackTraceElements: stackTraceElements, buildId: buildId, + loadingUnits: loadingUnits, fatal: fatal, ); } diff --git a/packages/firebase_crashlytics/firebase_crashlytics/lib/src/utils.dart b/packages/firebase_crashlytics/firebase_crashlytics/lib/src/utils.dart index 030eb855f593..74def5bc4d28 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/lib/src/utils.dart +++ b/packages/firebase_crashlytics/firebase_crashlytics/lib/src/utils.dart @@ -64,3 +64,12 @@ String? getBuildId(StackTrace stackTrace) { return null; } + +List getLoadingUnits(StackTrace stackTrace) => + Trace.parseVM(stackTrace.toString()) + .terse + .frames + .whereType() + .map((frame) => frame.member) + .where((member) => member.startsWith('loading_unit: ')) + .toList(); diff --git a/packages/firebase_crashlytics/firebase_crashlytics/test/firebase_crashlytics_test.dart b/packages/firebase_crashlytics/firebase_crashlytics/test/firebase_crashlytics_test.dart index 3cac290b46c5..77c870c6ec7b 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics/test/firebase_crashlytics_test.dart +++ b/packages/firebase_crashlytics/firebase_crashlytics/test/firebase_crashlytics_test.dart @@ -79,6 +79,7 @@ void main() { 'fatal': false, 'stackTraceElements': getStackTraceElements(stack), 'buildId': '', + 'loadingUnits': [], }) ]); // Confirm that the stack trace contains current stack. @@ -141,6 +142,7 @@ void main() { 'information': '$exceptionFirstMessage\n$exceptionSecondMessage', 'stackTraceElements': getStackTraceElements(stack), 'buildId': '', + 'loadingUnits': [], }) ]); } finally { diff --git a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/lib/src/method_channel/method_channel_crashlytics.dart b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/lib/src/method_channel/method_channel_crashlytics.dart index 09f4f1cdf356..42a135d45302 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/lib/src/method_channel/method_channel_crashlytics.dart +++ b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/lib/src/method_channel/method_channel_crashlytics.dart @@ -95,6 +95,7 @@ class MethodChannelFirebaseCrashlytics extends FirebaseCrashlyticsPlatform { required String? reason, bool fatal = false, String? buildId, + List loadingUnits = const [], List>? stackTraceElements, }) async { try { @@ -105,6 +106,7 @@ class MethodChannelFirebaseCrashlytics extends FirebaseCrashlyticsPlatform { 'reason': reason, 'fatal': fatal, 'buildId': buildId ?? '', + 'loadingUnits': loadingUnits, 'stackTraceElements': stackTraceElements ?? [], }); } on PlatformException catch (e, s) { diff --git a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/lib/src/platform_interface/platform_interface_crashlytics.dart b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/lib/src/platform_interface/platform_interface_crashlytics.dart index 249ed127db12..ef2c2d97f9ba 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/lib/src/platform_interface/platform_interface_crashlytics.dart +++ b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/lib/src/platform_interface/platform_interface_crashlytics.dart @@ -114,6 +114,7 @@ abstract class FirebaseCrashlyticsPlatform extends PlatformInterface { required String? reason, bool fatal = false, String? buildId, + List loadingUnits = const [], List>? stackTraceElements, }) { throw UnimplementedError('recordError() is not implemented'); diff --git a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/test/method_channel_tests/method_channel_crashlytics_test.dart b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/test/method_channel_tests/method_channel_crashlytics_test.dart index 2fdd447d2384..81a27fd5b00c 100644 --- a/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/test/method_channel_tests/method_channel_crashlytics_test.dart +++ b/packages/firebase_crashlytics/firebase_crashlytics_platform_interface/test/method_channel_tests/method_channel_crashlytics_test.dart @@ -30,6 +30,7 @@ void main() { 'fatal': false, 'information': 'This is a test exception', 'buildId': '', + 'loadingUnits': [], 'stackTraceElements': >[ { 'declaringClass': 'MethodChannelCrashlyticsTest', @@ -219,6 +220,7 @@ void main() { 'information': kMockError['information'], 'stackTraceElements': kMockError['stackTraceElements'], 'buildId': '', + 'loadingUnits': [], }, ), ]);