Skip to content

Commit

Permalink
v4.19.0
Browse files Browse the repository at this point in the history
  • Loading branch information
fractalwrench authored Aug 27, 2019
2 parents 03a4d44 + 6ff5d5f commit 3a3f24c
Show file tree
Hide file tree
Showing 21 changed files with 124 additions and 390 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 4.19.0 (2019-08-27)

* 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,15 +5,11 @@ 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
import java.io.File

class SuperCaliFragilisticExpiAlidociousBeanFactoryException: RuntimeException()

class ErrorFilenameTest {

private lateinit var errorStore: ErrorStore
Expand All @@ -31,74 +27,10 @@ class ErrorFilenameTest {
errorStore = ErrorStore(config, context, null)
}

@Test
fun testCalculateFilenameUnhandled() {
val err = generateError(true, Severity.ERROR, RuntimeException())
val filename = errorStore.calculateFilenameForError(err)
assertEquals("e-u-java.lang.RuntimeException", filename)
}

@Test
fun testCalculateFilenameHandled() {
val err = generateError(false, Severity.INFO, IllegalStateException("Whoops"))
val filename = errorStore.calculateFilenameForError(err)
assertEquals("i-h-java.lang.IllegalStateException", filename)
}

@Test
fun testCalculateTruncatedFilename() {
val err = generateError(false, Severity.INFO,
SuperCaliFragilisticExpiAlidociousBeanFactoryException())
val filename = errorStore.calculateFilenameForError(err)
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 =
arrayOf("1504255147933_e-u-java.lang.RuntimeException_30b7e350-dcd1-4032-969e-98d30be62bbc_startupcrash.json")
arrayOf("1504255147933_30b7e350-dcd1-4032-969e-98d30be62bbc_startupcrash.json")
val invalid = arrayOf(
"",
".json",
Expand All @@ -118,11 +50,9 @@ class ErrorFilenameTest {

@Test
fun testComparator() {
val first = "1504255147933_e-u-java.lang.RuntimeException_" +
"683c6b92-b325-4987-80ad-77086509ca1e.json"
val second = "1505000000000_i-h-Exception_683c6b92-b325-4987-80ad-77086509ca1e.json"
val startup = "1504500000000_w-h-java.lang.IllegalStateException_683c6b92-b325-" +
"4987-80ad-77086509ca1e_startupcrash.json"
val first = "1504255147933_683c6b92-b325-4987-80ad-77086509ca1e.json"
val second = "1505000000000_683c6b92-b325-4987-80ad-77086509ca1e.json"
val startup = "1504500000000_683c6b92-b325-4987-80ad-77086509ca1e_startupcrash.json"

// handle defaults
assertEquals(0, ERROR_REPORT_COMPARATOR.compare(null, null).toLong())
Expand All @@ -141,17 +71,4 @@ class ErrorFilenameTest {
assertTrue(ERROR_REPORT_COMPARATOR.compare(File(first), File(startup)) < 0)
assertTrue(ERROR_REPORT_COMPARATOR.compare(File(second), File(startup)) > 0)
}

private fun generateError(unhandled: Boolean, severity: Severity, exc: Throwable): Error {
val currentThread = Thread.currentThread()
val sessionTracker = BugsnagTestUtils.generateSessionTracker()

val handledState = when {
unhandled -> HandledState.REASON_UNHANDLED_EXCEPTION
else -> HandledState.REASON_HANDLED_EXCEPTION
}
return Error.Builder(config, exc, sessionTracker, currentThread, unhandled)
.severityReasonType(handledState)
.severity(severity).build()
}
}
61 changes: 58 additions & 3 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) {
// send a minimal error to bugsnag with no cache
Client.this.notify(error, DeliveryStyle.NO_CACHE, null);
public void onErrorReadFailure(Exception exc, File errorFile) {
// send an internal error to bugsnag with no cache
Thread thread = Thread.currentThread();
Error err = new Error.Builder(config, exc, null, thread, true).build();
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);
DefaultDelivery defaultDelivery = (DefaultDelivery) delivery;
defaultDelivery.deliver(config.getEndpoint(), report, headers);
}

} catch (Exception exception) {
Logger.warn("Failed to report internal error to Bugsnag", exception);
}
}
});
} 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 @@ -22,17 +22,15 @@
@ThreadSafe
class ErrorStore extends FileStore<Error> {

private static final int MAX_ERR_CLASS_LEN = 40;

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 +169,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 All @@ -196,71 +190,13 @@ private List<File> findLaunchCrashReports(Collection<File> storedFiles) {
return launchCrashes;
}

String calculateFilenameForError(Error error) {
char handled = error.getHandledState().isUnhandled() ? 'u' : 'h';
char severity = error.getSeverity().getName().charAt(0);
String errClass = error.getExceptionName();

if (errClass.length() > MAX_ERR_CLASS_LEN) {
errClass = errClass.substring(0, MAX_ERR_CLASS_LEN);
}
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) {
String suffix = "";
String encodedInfo;

if (object instanceof Error) {
Error error = (Error) object;
encodedInfo = calculateFilenameForError(error);

Map<String, Object> appData = error.getAppData();
if (appData instanceof Map) {
Expand All @@ -271,13 +207,12 @@ && isStartupCrash(((Number) appData.get("duration")).longValue())) {
}
}
} else {
encodedInfo = ""; // don't encode for NDK errors, as they are always 'e-u'
suffix = "not-jvm";
}
String uuid = UUID.randomUUID().toString();
long timestamp = System.currentTimeMillis();
return String.format(Locale.US, "%s%d_%s_%s%s.json",
storeDirectory, timestamp, encodedInfo, uuid, suffix);
return String.format(Locale.US, "%s%d_%s%s.json",
storeDirectory, timestamp, uuid, suffix);
}

boolean isStartupCrash(long durationMs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
public class Notifier implements JsonStream.Streamable {

private static final String NOTIFIER_NAME = "Android Bugsnag Notifier";
private static final String NOTIFIER_VERSION = "4.18.0";
private static final String NOTIFIER_VERSION = "4.19.0";
private static final String NOTIFIER_URL = "https://bugsnag.com";

@NonNull
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)
}
Loading

0 comments on commit 3a3f24c

Please sign in to comment.