Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an overload to FirebaseCrashlytics.recordException to attach additional custom key value pairs #6528

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion firebase-crashlytics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Unreleased

* [feature] Added an overload to the `recordException` API to allow attaching additional custom
keys to the non fatal report.

# 19.2.1
* [changed] Updated protobuf dependency to `3.25.5` to fix
Expand Down
3 changes: 2 additions & 1 deletion firebase-crashlytics/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ package com.google.firebase.crashlytics {
method public void deleteUnsentReports();
method public boolean didCrashOnPreviousExecution();
method @NonNull public static com.google.firebase.crashlytics.FirebaseCrashlytics getInstance();
method public boolean isCrashlyticsCollectionEnabled();
method public void log(@NonNull String);
method public void recordException(@NonNull Throwable);
method public void recordException(@NonNull Throwable, @NonNull java.util.Map<java.lang.String,java.lang.String>);
method public void sendUnsentReports();
method public boolean isCrashlyticsCollectionEnabled();
method public void setCrashlyticsCollectionEnabled(boolean);
method public void setCrashlyticsCollectionEnabled(@Nullable Boolean);
method public void setCustomKey(@NonNull String, boolean);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.google.firebase.crashlytics.internal.NativeSessionFileProvider;
import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger;
import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers;
import com.google.firebase.crashlytics.internal.metadata.EventMetadata;
import com.google.firebase.crashlytics.internal.metadata.LogFileManager;
import com.google.firebase.crashlytics.internal.metadata.UserMetadata;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
Expand All @@ -57,6 +58,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -216,14 +218,14 @@ public void testWriteNonFatal_callsSessionReportingCoordinatorPersistNonFatal()
when(mockSessionReportingCoordinator.listSortedOpenSessionIds())
.thenReturn(new TreeSet<>(Collections.singleton(sessionId)));

controller.writeNonFatalException(thread, nonFatal);
controller.writeNonFatalException(thread, nonFatal, Map.of());
crashlyticsWorkers.common.submit(() -> controller.doCloseSessions(testSettingsProvider));

crashlyticsWorkers.common.await();
crashlyticsWorkers.diskWrite.await();

verify(mockSessionReportingCoordinator)
.persistNonFatalEvent(eq(nonFatal), eq(thread), eq(sessionId), anyLong());
.persistNonFatalEvent(eq(nonFatal), eq(thread), any(EventMetadata.class));
}

@SdkSuppress(minSdkVersion = 30) // ApplicationExitInfo
Expand Down Expand Up @@ -377,7 +379,7 @@ public void testLoggedExceptionsAfterCrashOk() {
testSettingsProvider, Thread.currentThread(), new RuntimeException());

// This should not throw.
controller.writeNonFatalException(Thread.currentThread(), new RuntimeException());
controller.writeNonFatalException(Thread.currentThread(), new RuntimeException(), Map.of());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.concurrent.TestOnlyExecutors;
import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers;
import com.google.firebase.crashlytics.internal.metadata.EventMetadata;
import com.google.firebase.crashlytics.internal.metadata.LogFileManager;
import com.google.firebase.crashlytics.internal.metadata.UserMetadata;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
Expand Down Expand Up @@ -138,7 +139,8 @@ public void testNonFatalEvent_persistsNormalPriorityEventWithoutAllThreadsForSes
mockEventInteractions();

reportingCoordinator.onBeginSession(sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(
mockException, mockThread, new EventMetadata(sessionId, timestamp));

crashlyticsWorkers.diskWrite.await();

Expand All @@ -163,7 +165,8 @@ public void testNonFatalEvent_addsLogsToEvent() throws Exception {
when(logFileManager.getLogString()).thenReturn(testLog);

reportingCoordinator.onBeginSession(sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(
mockException, mockThread, new EventMetadata(sessionId, timestamp));

crashlyticsWorkers.diskWrite.await();

Expand All @@ -184,7 +187,8 @@ public void testNonFatalEvent_addsNoLogsToEventWhenNoneAvailable() throws Except
when(logFileManager.getLogString()).thenReturn(null);

reportingCoordinator.onBeginSession(sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(
mockException, mockThread, new EventMetadata(sessionId, timestamp));

crashlyticsWorkers.diskWrite.await();

Expand Down Expand Up @@ -261,7 +265,8 @@ public void testNonFatalEvent_addsSortedKeysToEvent() throws Exception {
when(reportMetadata.getInternalKeys()).thenReturn(attributes);

reportingCoordinator.onBeginSession(sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(
mockException, mockThread, new EventMetadata(sessionId, timestamp));

crashlyticsWorkers.diskWrite.await();

Expand All @@ -286,7 +291,8 @@ public void testNonFatalEvent_addsNoKeysToEventWhenNoneAvailable() throws Except
when(reportMetadata.getCustomKeys()).thenReturn(attributes);

reportingCoordinator.onBeginSession(sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(
mockException, mockThread, new EventMetadata(sessionId, timestamp));

crashlyticsWorkers.diskWrite.await();

Expand All @@ -297,6 +303,95 @@ public void testNonFatalEvent_addsNoKeysToEventWhenNoneAvailable() throws Except
verify(logFileManager, never()).clearLog();
}

@Test
public void testNonFatalEvent_addsUserInfoKeysToEventWhenAvailable() throws Exception {
final long timestamp = System.currentTimeMillis();

mockEventInteractions();

final String sessionId = "testSessionId";

final Map<String, String> attributes = Collections.emptyMap();
when(reportMetadata.getCustomKeys()).thenReturn(attributes);

final String testKey1 = "testKey1";
final String testValue1 = "testValue1";

final Map<String, String> userInfo = new HashMap<>();
userInfo.put(testKey1, testValue1);

final CustomAttribute customAttribute1 =
CustomAttribute.builder().setKey(testKey1).setValue(testValue1).build();

final List<CustomAttribute> expectedCustomAttributes = new ArrayList<>();
expectedCustomAttributes.add(customAttribute1);

reportingCoordinator.onBeginSession(sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(
mockException, mockThread, new EventMetadata(sessionId, timestamp, userInfo));

crashlyticsWorkers.diskWrite.await();

verify(mockEventAppBuilder).setCustomAttributes(expectedCustomAttributes);
verify(mockEventAppBuilder).build();
verify(mockEventBuilder).setApp(mockEventApp);
verify(mockEventBuilder).build();
verify(logFileManager, never()).clearLog();
}

@Test
public void testNonFatalEvent_mergesUserInfoKeysWithCustomKeys() throws Exception {
final long timestamp = System.currentTimeMillis();

mockEventInteractions();

final String sessionId = "testSessionId";

final String testKey1 = "testKey1";
final String testValue1 = "testValue1";

final String testKey2 = "testKey2";
final String testValue2 = "testValue2";

final Map<String, String> attributes = new HashMap<>();
attributes.put(testKey1, testValue1);
attributes.put(testKey2, testValue2);

when(reportMetadata.getCustomKeys()).thenReturn(attributes);

final String testValue1UserInfo = "testValue1";
final String testKey3 = "testKey3";
final String testValue3 = "testValue3";

final Map<String, String> userInfo = new HashMap<>();
userInfo.put(testKey1, testValue1UserInfo);
userInfo.put(testKey3, testValue3);

final CustomAttribute customAttribute1 =
CustomAttribute.builder().setKey(testKey1).setValue(testValue1UserInfo).build();
final CustomAttribute customAttribute2 =
CustomAttribute.builder().setKey(testKey2).setValue(testValue2).build();
final CustomAttribute customAttribute3 =
CustomAttribute.builder().setKey(testKey3).setValue(testValue3).build();

final List<CustomAttribute> expectedCustomAttributes = new ArrayList<>();
expectedCustomAttributes.add(customAttribute1);
expectedCustomAttributes.add(customAttribute2);
expectedCustomAttributes.add(customAttribute3);

reportingCoordinator.onBeginSession(sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(
mockException, mockThread, new EventMetadata(sessionId, timestamp, userInfo));

crashlyticsWorkers.diskWrite.await();

verify(mockEventAppBuilder).setCustomAttributes(expectedCustomAttributes);
verify(mockEventAppBuilder).build();
verify(mockEventBuilder).setApp(mockEventApp);
verify(mockEventBuilder).build();
verify(logFileManager, never()).clearLog();
}

@Test
public void testNonFatalEvent_addRolloutsEvent() throws Exception {
long timestamp = System.currentTimeMillis();
Expand All @@ -309,7 +404,8 @@ public void testNonFatalEvent_addRolloutsEvent() throws Exception {
when(reportMetadata.getRolloutsState()).thenReturn(rolloutsState);

reportingCoordinator.onBeginSession(sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp);
reportingCoordinator.persistNonFatalEvent(
mockException, mockThread, new EventMetadata(sessionId, timestamp));

crashlyticsWorkers.diskWrite.await();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import com.google.firebase.remoteconfig.interop.FirebaseRemoteConfigInterop;
import com.google.firebase.sessions.api.FirebaseSessionsDependencies;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;

/**
Expand Down Expand Up @@ -202,11 +203,26 @@ public static FirebaseCrashlytics getInstance() {
* @param throwable a {@link Throwable} to be recorded as a non-fatal event.
*/
public void recordException(@NonNull Throwable throwable) {
recordException(throwable, Map.of());
}

/**
* Records a non-fatal report to send to Crashlytics.
*
* @param throwable a {@link Throwable} to be recorded as a non-fatal event.
* @param userInfo a {@link Map} to add key value pairs to be recorded with the non fatal exception.
*/
public void recordException(@NonNull Throwable throwable, @NonNull Map<String, String> userInfo) {
if (throwable == null) { // Users could call this with null despite the annotation.
Logger.getLogger().w("A null value was passed to recordException. Ignoring.");
return;
}
core.logException(throwable);

if (userInfo == null) { // It's possible to set this to null even with the annotation.
userInfo = Map.of();
}

core.logException(throwable, userInfo);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger;
import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsTasks;
import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers;
import com.google.firebase.crashlytics.internal.metadata.EventMetadata;
import com.google.firebase.crashlytics.internal.metadata.LogFileManager;
import com.google.firebase.crashlytics.internal.metadata.UserMetadata;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
Expand Down Expand Up @@ -405,7 +406,10 @@ void writeToLog(final long timestamp, final String msg) {
}

/** Log a caught exception - write out Throwable as event section of protobuf */
void writeNonFatalException(@NonNull final Thread thread, @NonNull final Throwable ex) {
void writeNonFatalException(
@NonNull final Thread thread,
@NonNull final Throwable ex,
@NonNull Map<String, String> userInfo) {
// Capture and close over the current time, so that we get the exact call time,
// rather than the time at which the task executes.
final long timestampMillis = System.currentTimeMillis();
Expand All @@ -417,7 +421,8 @@ void writeNonFatalException(@NonNull final Thread thread, @NonNull final Throwab
Logger.getLogger().w("Tried to write a non-fatal exception while no session was open.");
return;
}
reportingCoordinator.persistNonFatalEvent(ex, thread, currentSessionId, timestampSeconds);
EventMetadata eventMetadata = new EventMetadata(currentSessionId, timestampSeconds, userInfo);
reportingCoordinator.persistNonFatalEvent(ex, thread, eventMetadata);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,9 @@ public static String getVersion() {
* Throwable was thrown. The Throwable will always be processed on a background thread, so it is
* safe to invoke this method from the main thread.
*/
public void logException(@NonNull Throwable throwable) {
public void logException(@NonNull Throwable throwable, @NonNull Map<String, String> userInfo) {
crashlyticsWorkers.common.submit(
() -> controller.writeNonFatalException(Thread.currentThread(), throwable));
() -> controller.writeNonFatalException(Thread.currentThread(), throwable, userInfo));
}

/**
Expand Down
Loading
Loading