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

Report internal SDK errors to bugsnag #570

Merged
merged 12 commits into from
Aug 27, 2019
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## TBD

* Report internal SDK errors to bugsnag
[#570](https://github.com/bugsnag/bugsnag-android/pull/570)

## 4.18.0 (2019-08-15)

* Migrate dependencies to androidx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import androidx.test.core.app.ApplicationProvider
import com.bugsnag.android.ErrorStore.ERROR_REPORT_COMPARATOR
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -53,48 +51,6 @@ class ErrorFilenameTest {
assertEquals("i-h-com.bugsnag.android.SuperCaliFragilistic", filename)
}

@Test
fun testErrorFromInvalidFilename() {
val invalids = arrayOf(
null, "", "test.txt", "i-h.foo",
"1504255147933_683c6b92-b325-4987-80ad-77086509ca1e.json"
)
invalids.forEach { assertNull(errorStore.generateErrorFromFilename(it)) }
}

@Test
fun testUnhandledErrorFromFilename() {
val filename = "1504255147933_e-u-java.lang.RuntimeException_" +
"683c6b92-b325-4987-80ad-77086509ca1e.json"
val err = errorStore.generateErrorFromFilename(filename)
assertNotNull(err)
assertTrue(err.handledState.isUnhandled)
assertEquals(Severity.ERROR, err.severity)
assertEquals("java.lang.RuntimeException", err.exceptionName)
}

@Test
fun testHandledErrorFromFilename() {
val filename = "1504500000000_i-h-java.lang.IllegalStateException_" +
"683c6b92-b325-4987-80ad-77086509ca1e_startupcrash.json"
val err = errorStore.generateErrorFromFilename(filename)
assertNotNull(err)
assertFalse(err.handledState.isUnhandled)
assertEquals(Severity.INFO, err.severity)
assertEquals("java.lang.IllegalStateException", err.exceptionName)
}

@Test
fun testErrorWithoutClassFromFilename() {
val filename = "1504500000000_i-h-_" +
"683c6b92-b325-4987-80ad-77086509ca1e_startupcrash.json"
val err = errorStore.generateErrorFromFilename(filename)
assertNotNull(err)
assertFalse(err.handledState.isUnhandled)
assertEquals(Severity.INFO, err.severity)
assertEquals("", err.exceptionName)
}

@Test
fun testIsLaunchCrashReport() {
val valid =
Expand Down
59 changes: 57 additions & 2 deletions bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import kotlin.Unit;
import kotlin.jvm.functions.Function1;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -48,6 +49,8 @@ public class Client extends Observable implements Observer {
private static final String USER_NAME_KEY = "user.name";
private static final String USER_EMAIL_KEY = "user.email";

static final String INTERNAL_DIAGNOSTICS_TAB = "BugsnagDiagnostics";

@NonNull
protected final Configuration config;
final Context appContext;
Expand Down Expand Up @@ -195,9 +198,16 @@ public Unit invoke(Boolean connected) {
// Create the error store that is used in the exception handler
errorStore = new ErrorStore(config, appContext, new ErrorStore.Delegate() {
@Override
public void onErrorReadFailure(Error error) {
public void onErrorReadFailure(Exception exc, File errorFile) {
// send a minimal error to bugsnag with no cache
fractalwrench marked this conversation as resolved.
Show resolved Hide resolved
Client.this.notify(error, DeliveryStyle.NO_CACHE, null);
Thread thread = Thread.currentThread();
Error err = new Error.Builder(config, exc, null, thread, true).build();
fractalwrench marked this conversation as resolved.
Show resolved Hide resolved
err.setContext("Crash Report Deserialization");

MetaData metaData = err.getMetaData();
metaData.addToTab(INTERNAL_DIAGNOSTICS_TAB, "filename", errorFile.getName());
metaData.addToTab(INTERNAL_DIAGNOSTICS_TAB, "fileLength", errorFile.length());
Client.this.reportInternalBugsnagError(err);
}
});

Expand Down Expand Up @@ -972,6 +982,51 @@ void notify(@NonNull Error error,
}
}

/**
* Reports an error that occurred within the notifier to bugsnag. A lean error report will be
* generated and sent asynchronously with no callbacks, retry attempts, or writing to disk.
* This is intended for internal use only, and reports will not be visible to end-users.
*/
void reportInternalBugsnagError(@NonNull Error error) {
error.setAppData(appData.getAppDataSummary());
error.setDeviceData(deviceData.getDeviceDataSummary());

MetaData metaData = error.getMetaData();
Notifier notifier = Notifier.getInstance();
metaData.addToTab(INTERNAL_DIAGNOSTICS_TAB, "notifierName", notifier.getName());
metaData.addToTab(INTERNAL_DIAGNOSTICS_TAB, "notifierVersion", notifier.getVersion());
metaData.addToTab(INTERNAL_DIAGNOSTICS_TAB, "apiKey", config.getApiKey());

Object packageName = appData.getAppData().get("packageName");
metaData.addToTab(INTERNAL_DIAGNOSTICS_TAB, "packageName", packageName);

final Report report = new Report(null, error);
try {
Async.run(new Runnable() {
@Override
public void run() {
try {
Delivery delivery = config.getDelivery();

// can only modify headers if DefaultDelivery is in use
if (delivery instanceof DefaultDelivery) {
Map<String, String> headers = config.getErrorApiHeaders();
headers.put("Bugsnag-Internal-Error", "true");
headers.remove(Configuration.HEADER_API_KEY);
kattrali marked this conversation as resolved.
Show resolved Hide resolved
DefaultDelivery defaultDelivery = (DefaultDelivery) delivery;
defaultDelivery.deliver(config.getEndpoint(), report, headers);
}

} catch (Exception exception) {
Logger.warn("Failed to report minimal error to Bugsnag", exception);
fractalwrench marked this conversation as resolved.
Show resolved Hide resolved
}
}
});
} catch (RejectedExecutionException ignored) {
// drop internal report
}
}

private void deliverReportAsync(@NonNull Error error, Report report) {
final Report finalReport = report;
final Error finalError = error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
public class Configuration extends Observable implements Observer {

private static final String HEADER_API_PAYLOAD_VERSION = "Bugsnag-Payload-Version";
private static final String HEADER_API_KEY = "Bugsnag-Api-Key";
static final String HEADER_API_KEY = "Bugsnag-Api-Key";
private static final String HEADER_BUGSNAG_SENT_AT = "Bugsnag-Sent-At";
private static final int DEFAULT_MAX_SIZE = 32;
static final String DEFAULT_EXCEPTION_TYPE = "android";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ class ErrorStore extends FileStore<Error> {
interface Delegate {

/**
* Invoked when a cached error report cannot be read, and a minimal error is
* read from the information encoded in the filename instead.
* Invoked when a cached error report cannot be read.
*
* @param minimalError the minimal error, if encoded in the filename
* @param exception the error encountered reading/delivering the file
* @param errorFile file which could not be read
*/
void onErrorReadFailure(Error minimalError);
void onErrorReadFailure(Exception exception, File errorFile);
}

private static final String STARTUP_CRASH = "_startupcrash";
Expand Down Expand Up @@ -171,11 +171,7 @@ private void flushErrorReport(File errorFile) {
+ " to Bugsnag, will try again later", exception);
} catch (Exception exception) {
if (delegate != null) {
Error minimalError = generateErrorFromFilename(errorFile.getName());

if (minimalError != null) {
delegate.onErrorReadFailure(minimalError);
}
delegate.onErrorReadFailure(exception, errorFile);
}
deleteStoredFiles(Collections.singleton(errorFile));
}
Expand Down Expand Up @@ -207,51 +203,6 @@ String calculateFilenameForError(Error error) {
return String.format("%s-%s-%s", severity, handled, errClass);
}

/**
* Generates minimal error information from a filename, if the report was incomplete/corrupted.
* This allows bugsnag to send the severity, handled state, and error class as a minimal
* report.
*
* Error information is encoded in the filename for recent notifier versions
* as "$severity-$handled-$errorClass", and is not present in legacy versions
*
* @param filename the filename
* @return the minimal error, or null if the filename does not match the expected pattern.
*/
Error generateErrorFromFilename(String filename) {
if (filename == null) {
return null;
}

try {
int errorInfoStart = filename.indexOf('_') + 1;
int errorInfoEnd = filename.indexOf('_', errorInfoStart);
String encodedErr = filename.substring(errorInfoStart, errorInfoEnd);

char sevChar = encodedErr.charAt(0);
Severity severity = Severity.fromChar(sevChar);
severity = severity == null ? Severity.ERROR : severity;

boolean unhandled = encodedErr.charAt(2) == 'u';
HandledState handledState = HandledState.newInstance(unhandled
? HandledState.REASON_UNHANDLED_EXCEPTION : HandledState.REASON_HANDLED_EXCEPTION);

// default if error has no name
String errClass = "";

if (encodedErr.length() >= 4) {
errClass = encodedErr.substring(4);
}
BugsnagException exc = new BugsnagException(errClass, "", new StackTraceElement[]{});
Error error = new Error(config, exc, handledState, severity, null, null);
error.setIncomplete(true);
return error;
} catch (IndexOutOfBoundsException exc) {
// simplifies above implementation by avoiding need for several length checks.
return null;
}
}

@NonNull
@Override
String getFilename(Object object) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,10 @@ internal fun writeErrorToStore(client: Client) {
Thread.currentThread(), false).build()
client.errorStore.write(error)
}

internal fun sendInternalReport(exc: Throwable, config: Configuration, client: Client) {
val thread = Thread.currentThread()
val err = Error.Builder(config, exc, null, thread, true).build()
err.getMetaData().addToTab("BugsnagDiagnostics", "custom-data", "FooBar")
client.reportInternalBugsnagError(err)
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.bugsnag.android.mazerunner.scenarios

import android.content.Context
import com.bugsnag.android.*

internal class InternalReportScenario(config: Configuration,
context: Context) : Scenario(config, context) {
init {
config.setAutoCaptureSessions(false)
}

override fun run() {
super.run()
sendInternalReport(RuntimeException("Whoops"), config, Bugsnag.getClient())
}

}

This file was deleted.

This file was deleted.

Loading