Skip to content

Commit 1b5c903

Browse files
authored
Don't send fatal uncaught exception if an NDK crash exists (#3020)
Our backend expects at most one fatal event per session. In some cases, such as Unity, the native signal handler may cause an uncaught exception to be thrown, after we have recorded the NDK crash. If this happens, Crashlytics should ignore the uncaught exception and send the NDK crash information as the fatal event.
1 parent 1e58174 commit 1b5c903

File tree

5 files changed

+53
-7
lines changed

5 files changed

+53
-7
lines changed

firebase-crashlytics-ndk/src/main/java/com/google/firebase/crashlytics/ndk/FirebaseCrashlyticsNdk.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ private interface SignalHandlerInstaller {
4949
}
5050

5151
private boolean installHandlerDuringPrepareSession;
52+
private String currentSessionId;
5253
private SignalHandlerInstaller signalHandlerInstaller;
5354

5455
FirebaseCrashlyticsNdk(
@@ -57,6 +58,11 @@ private interface SignalHandlerInstaller {
5758
this.installHandlerDuringPrepareSession = installHandlerDuringPrepareSession;
5859
}
5960

61+
@Override
62+
public boolean hasCrashDataForCurrentSession() {
63+
return currentSessionId != null && hasCrashDataForSession(currentSessionId);
64+
}
65+
6066
@Override
6167
public boolean hasCrashDataForSession(@NonNull String sessionId) {
6268
return controller.hasCrashDataForSession(sessionId);
@@ -74,6 +80,7 @@ public synchronized void prepareNativeSession(
7480
long startedAtSeconds,
7581
@NonNull StaticSessionData sessionData) {
7682

83+
currentSessionId = sessionId;
7784
signalHandlerInstaller =
7885
() -> {
7986
Logger.getLogger().d("Initializing native session: " + sessionId);

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/CrashlyticsNativeComponent.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
public interface CrashlyticsNativeComponent {
2121

22+
boolean hasCrashDataForCurrentSession();
23+
2224
boolean hasCrashDataForSession(@NonNull String sessionId);
2325

2426
/**

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/CrashlyticsNativeComponentDeferredProxy.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ public CrashlyticsNativeComponentDeferredProxy(
4040
});
4141
}
4242

43+
@Override
44+
public boolean hasCrashDataForCurrentSession() {
45+
CrashlyticsNativeComponent component = availableNativeComponent.get();
46+
return component != null && component.hasCrashDataForCurrentSession();
47+
}
48+
4349
@Override
4450
public boolean hasCrashDataForSession(@NonNull String sessionId) {
4551
CrashlyticsNativeComponent component = availableNativeComponent.get();

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ public void onUncaughtException(
156156
}
157157
};
158158
crashHandler =
159-
new CrashlyticsUncaughtExceptionHandler(crashListener, settingsProvider, defaultHandler);
159+
new CrashlyticsUncaughtExceptionHandler(
160+
crashListener, settingsProvider, defaultHandler, nativeComponent);
160161
Thread.setDefaultUncaughtExceptionHandler(crashHandler);
161162
}
162163

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsUncaughtExceptionHandler.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,47 @@
1414

1515
package com.google.firebase.crashlytics.internal.common;
1616

17+
import com.google.firebase.crashlytics.internal.CrashlyticsNativeComponent;
1718
import com.google.firebase.crashlytics.internal.Logger;
1819
import com.google.firebase.crashlytics.internal.settings.SettingsDataProvider;
1920
import java.util.concurrent.atomic.AtomicBoolean;
2021

2122
class CrashlyticsUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
2223

2324
interface CrashListener {
25+
2426
void onUncaughtException(
2527
SettingsDataProvider settingsDataProvider, Thread thread, Throwable ex);
2628
}
2729

2830
private final CrashListener crashListener;
2931
private final SettingsDataProvider settingsDataProvider;
3032
private final Thread.UncaughtExceptionHandler defaultHandler;
33+
private final CrashlyticsNativeComponent nativeComponent;
3134

3235
// use AtomicBoolean because value is accessible from other threads.
3336
private final AtomicBoolean isHandlingException;
3437

3538
public CrashlyticsUncaughtExceptionHandler(
3639
CrashListener crashListener,
3740
SettingsDataProvider settingsProvider,
38-
Thread.UncaughtExceptionHandler defaultHandler) {
41+
Thread.UncaughtExceptionHandler defaultHandler,
42+
CrashlyticsNativeComponent nativeComponent) {
3943
this.crashListener = crashListener;
4044
this.settingsDataProvider = settingsProvider;
4145
this.defaultHandler = defaultHandler;
4246
this.isHandlingException = new AtomicBoolean(false);
47+
this.nativeComponent = nativeComponent;
4348
}
4449

4550
@Override
4651
public void uncaughtException(Thread thread, Throwable ex) {
4752
isHandlingException.set(true);
4853
try {
49-
if (thread == null) {
50-
Logger.getLogger().e("Could not handle uncaught exception; null thread");
51-
} else if (ex == null) {
52-
Logger.getLogger().e("Could not handle uncaught exception; null throwable");
53-
} else {
54+
if (shouldRecordUncaughtException(thread, ex)) {
5455
crashListener.onUncaughtException(settingsDataProvider, thread, ex);
56+
} else {
57+
Logger.getLogger().d("Uncaught exception will not be recorded by Crashlytics.");
5558
}
5659
} catch (Exception e) {
5760
Logger.getLogger().e("An error occurred in the uncaught exception handler", e);
@@ -65,4 +68,31 @@ public void uncaughtException(Thread thread, Throwable ex) {
6568
boolean isHandlingException() {
6669
return isHandlingException.get();
6770
}
71+
72+
/**
73+
* Returns true if Crashlytics should record this exception. The decision to record is different
74+
* than the decision to report the exception to Crashlytics servers, which is handled by the
75+
* {@link DataCollectionArbiter}
76+
*
77+
* @return false if the thread or exception is null, or if a native crash already exists for this
78+
* session.
79+
*/
80+
private boolean shouldRecordUncaughtException(Thread thread, Throwable ex) {
81+
if (thread == null) {
82+
Logger.getLogger().e("Crashlytics will not record uncaught exception; null thread");
83+
return false;
84+
}
85+
if (ex == null) {
86+
Logger.getLogger().e("Crashlytics will not record uncaught exception; null throwable");
87+
return false;
88+
}
89+
// We should only report at most one fatal event for a session. If a native fatal already exists
90+
// for this session, ignore the uncaught exception
91+
if (nativeComponent.hasCrashDataForCurrentSession()) {
92+
Logger.getLogger()
93+
.d("Crashlytics will not record uncaught exception; native crash exists for session.");
94+
return false;
95+
}
96+
return true;
97+
}
6898
}

0 commit comments

Comments
 (0)