Skip to content

Implement on-demand fatals internally #3402

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

Merged
merged 12 commits into from
Mar 15, 2022
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,13 @@ public void onReportSend_successfulReportsAreDeleted() {
final Task<CrashlyticsReportWithSessionId> failedTask =
Tasks.forException(new Exception("fail"));

when(reportSender.sendReport(mockReport1)).thenReturn(successfulTask);
when(reportSender.sendReport(mockReport2)).thenReturn(failedTask);
when(reportSender.enqueueReport(mockReport1, false)).thenReturn(successfulTask);
when(reportSender.enqueueReport(mockReport2, false)).thenReturn(failedTask);

reportingCoordinator.sendReports(Runnable::run);

verify(reportSender).sendReport(mockReport1);
verify(reportSender).sendReport(mockReport2);
verify(reportSender).enqueueReport(mockReport1, false);
verify(reportSender).enqueueReport(mockReport2, false);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.crashlytics.internal.common.CrashlyticsReportWithSessionId;
import com.google.firebase.crashlytics.internal.common.OnDemandCounter;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
import java.io.File;
import java.util.concurrent.ExecutionException;
Expand All @@ -51,7 +52,9 @@ public class DataTransportCrashlyticsReportSenderTest {
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mockTransform.apply(any())).thenReturn(new byte[0]);
reportSender = new DataTransportCrashlyticsReportSender(mockTransport, mockTransform);
reportSender =
new DataTransportCrashlyticsReportSender(
new ReportQueue(60, 1.2, 3_000, mockTransport, new OnDemandCounter()), mockTransform);
}

@Test
Expand All @@ -61,10 +64,13 @@ public void testSendReportsSuccessful() throws Exception {
final CrashlyticsReportWithSessionId report1 = mockReportWithSessionId();
final CrashlyticsReportWithSessionId report2 = mockReportWithSessionId();

final Task<CrashlyticsReportWithSessionId> send1 = reportSender.sendReport(report1);
final Task<CrashlyticsReportWithSessionId> send2 = reportSender.sendReport(report2);
final Task<CrashlyticsReportWithSessionId> send1 =
reportSender.enqueueReport(report1, /*isOnDemand=*/ true);
final Task<CrashlyticsReportWithSessionId> send2 =
reportSender.enqueueReport(report2, /*isOnDemand=*/ true);

try {
Thread.sleep(2_000); // give time to process queue
Tasks.await(send1);
Tasks.await(send2);
} catch (ExecutionException e) {
Expand All @@ -85,10 +91,11 @@ public void testSendReportsFailure() throws Exception {
final CrashlyticsReportWithSessionId report1 = mockReportWithSessionId();
final CrashlyticsReportWithSessionId report2 = mockReportWithSessionId();

final Task<CrashlyticsReportWithSessionId> send1 = reportSender.sendReport(report1);
final Task<CrashlyticsReportWithSessionId> send2 = reportSender.sendReport(report2);
final Task<CrashlyticsReportWithSessionId> send1 = reportSender.enqueueReport(report1, false);
final Task<CrashlyticsReportWithSessionId> send2 = reportSender.enqueueReport(report2, false);

try {
Thread.sleep(2_000); // give time to process queue
Tasks.await(send1);
Tasks.await(send2);
} catch (ExecutionException e) {
Expand All @@ -112,10 +119,11 @@ public void testSendReports_oneSuccessOneFail() throws Exception {
final CrashlyticsReportWithSessionId report1 = mockReportWithSessionId();
final CrashlyticsReportWithSessionId report2 = mockReportWithSessionId();

final Task<CrashlyticsReportWithSessionId> send1 = reportSender.sendReport(report1);
final Task<CrashlyticsReportWithSessionId> send2 = reportSender.sendReport(report2);
final Task<CrashlyticsReportWithSessionId> send1 = reportSender.enqueueReport(report1, false);
final Task<CrashlyticsReportWithSessionId> send2 = reportSender.enqueueReport(report2, false);

try {
Thread.sleep(2_000); // give time to process queue
Tasks.await(send1);
Tasks.await(send2);
} catch (ExecutionException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ public TestSettingsData(
buildSettingsData(),
buildFeaturesData(),
settingsVersion,
3600);
3600,
10,
1.2,
60);
}

private static FeaturesSettingsData buildFeaturesData() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class CrashlyticsController {
private final SessionReportingCoordinator reportingCoordinator;

private CrashlyticsUncaughtExceptionHandler crashHandler;
private SettingsDataProvider settingsProvider = null;

// A promise that will be resolved when unsent reports are found on the device, and
// send/deleteUnsentReports can be called to decide how to deal with them.
Expand Down Expand Up @@ -140,6 +141,7 @@ void enableExceptionHandling(
String sessionIdentifier,
Thread.UncaughtExceptionHandler defaultHandler,
SettingsDataProvider settingsProvider) {
this.settingsProvider = settingsProvider;
// This must be called before installing the controller with
// Thread.setDefaultUncaughtExceptionHandler to ensure that we are ready to handle
// any crashes we catch.
Expand All @@ -160,10 +162,18 @@ public void onUncaughtException(
Thread.setDefaultUncaughtExceptionHandler(crashHandler);
}

synchronized void handleUncaughtException(
void handleUncaughtException(
@NonNull SettingsDataProvider settingsDataProvider,
@NonNull final Thread thread,
@NonNull final Throwable ex) {
handleUncaughtException(settingsDataProvider, thread, ex, /*isOnDemand=*/ false);
}

synchronized void handleUncaughtException(
@NonNull SettingsDataProvider settingsDataProvider,
@NonNull final Thread thread,
@NonNull final Throwable ex,
boolean isOnDemand) {

Logger.getLogger()
.d("Handling uncaught " + "exception \"" + ex + "\" from thread " + thread.getName());
Expand Down Expand Up @@ -222,13 +232,15 @@ public Task<Void> then(@Nullable AppSettingsData appSettingsData)
// Data collection is enabled, so it's safe to send the report.
return Tasks.whenAll(
logAnalyticsAppExceptionEvents(),
reportingCoordinator.sendReports(executor));
reportingCoordinator.sendReports(
executor, isOnDemand ? currentSessionId : null));
}
});
}
});

try {
// TODO(mrober): Don't block the main thread ever for on-demand fatals.
Utils.awaitEvenIfOnMainThread(handleUncaughtExceptionTask);
} catch (Exception e) {
Logger.getLogger().e("Error handling uncaught exception", e);
Expand Down Expand Up @@ -419,6 +431,14 @@ public void run() {
});
}

void logFatalException(Thread thread, Throwable ex) {
if (settingsProvider == null) {
Logger.getLogger().w("settingsProvider not set");
return;
}
handleUncaughtException(settingsProvider, thread, ex, /*isOnDemand=*/ true);
}

void setUserId(String identifier) {
userMetadata.setUserId(identifier);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,19 @@ public class CrashlyticsCore {

static final int DEFAULT_MAIN_HANDLER_TIMEOUT_SEC = 4;

private static final String ON_DEMAND_RECORDED_KEY =
"com.crashlytics.on-demand.recorded-exceptions";
private static final String ON_DEMAND_DROPPED_KEY =
"com.crashlytics.on-demand.dropped-exceptions";

// If this marker sticks around, the app is crashing before we finished initializing
private static final String INITIALIZATION_MARKER_FILE_NAME = "initialization_marker";
static final String CRASH_MARKER_FILE_NAME = "crash_marker";

private final Context context;
private final FirebaseApp app;
private final DataCollectionArbiter dataCollectionArbiter;
private final OnDemandCounter onDemandCounter;

private final long startTime;

Expand Down Expand Up @@ -110,6 +116,7 @@ public CrashlyticsCore(
this.backgroundWorker = new CrashlyticsBackgroundWorker(crashHandlerExecutor);

startTime = System.currentTimeMillis();
onDemandCounter = new OnDemandCounter();
}

// endregion
Expand Down Expand Up @@ -150,7 +157,8 @@ public boolean onPreExecute(AppData appData, SettingsDataProvider settingsProvid
logFileManager,
userMetadata,
stackTraceTrimmingStrategy,
settingsProvider);
settingsProvider,
onDemandCounter);

controller =
new CrashlyticsController(
Expand Down Expand Up @@ -348,6 +356,10 @@ public void setCustomKeys(Map<String, String> keysAndValues) {
controller.setCustomKeys(keysAndValues);
}

// endregion

// region Internal API

/**
* Set a value to be associated with a given key for your crash data. The key/value pairs will be
* reported with any crash that occurs in this session. A maximum of 64 key/value pairs can be
Expand All @@ -364,6 +376,19 @@ public void setInternalKey(String key, String value) {
controller.setInternalKey(key, value);
}

/** Logs a fatal Throwable on the Crashlytics servers on-demand. */
public void logFatalException(Throwable throwable) {
Logger.getLogger()
.d("Recorded on-demand fatal events: " + onDemandCounter.getRecordedOnDemandExceptions());
Logger.getLogger()
.d("Dropped on-demand fatal events: " + onDemandCounter.getDroppedOnDemandExceptions());
controller.setInternalKey(
ON_DEMAND_RECORDED_KEY, Integer.toString(onDemandCounter.getRecordedOnDemandExceptions()));
controller.setInternalKey(
ON_DEMAND_DROPPED_KEY, Integer.toString(onDemandCounter.getDroppedOnDemandExceptions()));
controller.logFatalException(Thread.currentThread(), throwable);
}

// endregion

// region Package-protected getters
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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

import java.util.concurrent.atomic.AtomicInteger;

/** Simple, thread-safe, class to keep count of recorded and dropped on-demand events. */
public final class OnDemandCounter {
private final AtomicInteger recordedOnDemandExceptions;
private final AtomicInteger droppedOnDemandExceptions;

public OnDemandCounter() {
recordedOnDemandExceptions = new AtomicInteger();
droppedOnDemandExceptions = new AtomicInteger();
}

public int getRecordedOnDemandExceptions() {
return recordedOnDemandExceptions.get();
}

public void incrementRecordedOnDemandExceptions() {
recordedOnDemandExceptions.getAndIncrement();
}

public int getDroppedOnDemandExceptions() {
return droppedOnDemandExceptions.get();
}

public void incrementDroppedOnDemandExceptions() {
droppedOnDemandExceptions.getAndIncrement();
}

public void resetDroppedOnDemandExceptions() {
droppedOnDemandExceptions.set(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ public static SessionReportingCoordinator create(
LogFileManager logFileManager,
UserMetadata userMetadata,
StackTraceTrimmingStrategy stackTraceTrimmingStrategy,
SettingsDataProvider settingsProvider) {
SettingsDataProvider settingsProvider,
OnDemandCounter onDemandCounter) {
final CrashlyticsReportDataCapture dataCapture =
new CrashlyticsReportDataCapture(context, idManager, appData, stackTraceTrimmingStrategy);
final CrashlyticsReportPersistence reportPersistence =
new CrashlyticsReportPersistence(fileStore, settingsProvider);
final DataTransportCrashlyticsReportSender reportSender =
DataTransportCrashlyticsReportSender.create(context);
DataTransportCrashlyticsReportSender.create(context, settingsProvider, onDemandCounter);
return new SessionReportingCoordinator(
dataCapture, reportPersistence, reportSender, logFileManager, userMetadata);
}
Expand Down Expand Up @@ -202,14 +203,21 @@ public void removeAllReports() {
* sent.
*/
public Task<Void> sendReports(@NonNull Executor reportSendCompleteExecutor) {
return sendReports(reportSendCompleteExecutor, /*sessionId=*/ null);
}

public Task<Void> sendReports(
@NonNull Executor reportSendCompleteExecutor, @Nullable String sessionId) {
final List<CrashlyticsReportWithSessionId> reportsToSend =
reportPersistence.loadFinalizedReports();
final List<Task<Boolean>> sendTasks = new ArrayList<>();
for (CrashlyticsReportWithSessionId reportToSend : reportsToSend) {
sendTasks.add(
reportsSender
.sendReport(reportToSend)
.continueWith(reportSendCompleteExecutor, this::onReportSendComplete));
if (sessionId == null || sessionId.equals(reportToSend.getSessionId())) {
sendTasks.add(
reportsSender
.enqueueReport(reportToSend, sessionId != null)
.continueWith(reportSendCompleteExecutor, this::onReportSendComplete));
}
}
return Tasks.whenAll(sendTasks);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.android.datatransport.Encoding;
import com.google.android.datatransport.Event;
import com.google.android.datatransport.Transformer;
import com.google.android.datatransport.Transport;
import com.google.android.datatransport.cct.CCTDestination;
import com.google.android.datatransport.runtime.TransportRuntime;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.firebase.crashlytics.internal.common.CrashlyticsReportWithSessionId;
import com.google.firebase.crashlytics.internal.common.OnDemandCounter;
import com.google.firebase.crashlytics.internal.model.CrashlyticsReport;
import com.google.firebase.crashlytics.internal.model.serialization.CrashlyticsReportJsonTransform;
import com.google.firebase.crashlytics.internal.settings.SettingsDataProvider;
import java.nio.charset.Charset;

/**
Expand All @@ -45,10 +45,11 @@ public class DataTransportCrashlyticsReportSender {
private static final Transformer<CrashlyticsReport, byte[]> DEFAULT_TRANSFORM =
(r) -> TRANSFORM.reportToJson(r).getBytes(Charset.forName("UTF-8"));

private final Transport<CrashlyticsReport> transport;
private final ReportQueue reportQueue;
private final Transformer<CrashlyticsReport, byte[]> transportTransform;

public static DataTransportCrashlyticsReportSender create(Context context) {
public static DataTransportCrashlyticsReportSender create(
Context context, SettingsDataProvider settingsProvider, OnDemandCounter onDemandCounter) {
TransportRuntime.initialize(context);
final Transport<CrashlyticsReport> transport =
TransportRuntime.getInstance()
Expand All @@ -58,32 +59,21 @@ public static DataTransportCrashlyticsReportSender create(Context context) {
CrashlyticsReport.class,
Encoding.of("json"),
DEFAULT_TRANSFORM);
return new DataTransportCrashlyticsReportSender(transport, DEFAULT_TRANSFORM);
ReportQueue reportQueue =
new ReportQueue(transport, settingsProvider.getSettings(), onDemandCounter);
return new DataTransportCrashlyticsReportSender(reportQueue, DEFAULT_TRANSFORM);
}

DataTransportCrashlyticsReportSender(
Transport<CrashlyticsReport> transport,
Transformer<CrashlyticsReport, byte[]> transportTransform) {
this.transport = transport;
ReportQueue reportQueue, Transformer<CrashlyticsReport, byte[]> transportTransform) {
this.reportQueue = reportQueue;
this.transportTransform = transportTransform;
}

@NonNull
public Task<CrashlyticsReportWithSessionId> sendReport(
@NonNull CrashlyticsReportWithSessionId reportWithSessionId) {
final CrashlyticsReport report = reportWithSessionId.getReport();

TaskCompletionSource<CrashlyticsReportWithSessionId> tcs = new TaskCompletionSource<>();
transport.schedule(
Event.ofUrgent(report),
error -> {
if (error != null) {
tcs.trySetException(error);
return;
}
tcs.trySetResult(reportWithSessionId);
});
return tcs.getTask();
public Task<CrashlyticsReportWithSessionId> enqueueReport(
@NonNull CrashlyticsReportWithSessionId reportWithSessionId, boolean isOnDemand) {
return reportQueue.enqueueReport(reportWithSessionId, isOnDemand).getTask();
}

private static String mergeStrings(String part1, String part2) {
Expand Down
Loading