diff --git a/CHANGELOG.md b/CHANGELOG.md index 39e693854e..7bb59d53ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.17.1 (2019-07-24) + +### Bug fixes +* Fix NPE causing crash when reporting a minimal error + [#534](https://github.com/bugsnag/bugsnag-android/pull/534) + ## 4.17.0 (2019-07-17) This release modularizes `bugsnag-android` into 3 separate artifacts: for JVM (Core), NDK, and ANR error diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorStore.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorStore.java index 7e3d9f6574..9cbda2bbc1 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorStore.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ErrorStore.java @@ -172,7 +172,10 @@ private void flushErrorReport(File errorFile) { } catch (Exception exception) { if (delegate != null) { Error minimalError = generateErrorFromFilename(errorFile.getName()); - delegate.onErrorReadFailure(minimalError); + + if (minimalError != null) { + delegate.onErrorReadFailure(minimalError); + } } deleteStoredFiles(Collections.singleton(errorFile)); } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.java index 2e4060df9b..fa67c85be9 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.java @@ -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.17.0"; + private static final String NOTIFIER_VERSION = "4.17.1"; private static final String NOTIFIER_URL = "https://bugsnag.com"; @NonNull diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CorruptedOldReportScenario.kt b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CorruptedOldReportScenario.kt new file mode 100644 index 0000000000..6c0a4464ea --- /dev/null +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CorruptedOldReportScenario.kt @@ -0,0 +1,31 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import java.io.File + +/** + * Verifies that if a report is corrupted with an old filename, + * Bugsnag does not crash. + */ +internal class CorruptedOldReportScenario(config: Configuration, + context: Context) : Scenario(config, context) { + + init { + config.setAutoCaptureSessions(false) + val files = File(context.cacheDir, "bugsnag-errors").listFiles() + + // create an empty (invalid) file with an old name + files.forEach { + val dir = File(it.parent) + it.writeText("{\"exceptions\":[{\"stacktrace\":[") + it.renameTo(File(dir, "1504255147933_683c6b92-b325-4987-80ad-77086509ca1e.json")) + } + } + + override fun run() { + super.run() + Bugsnag.notify(generateException()) + } +} diff --git a/features/minimal_report.feature b/features/minimal_report.feature index 1116a859d2..629e542d2a 100644 --- a/features/minimal_report.feature +++ b/features/minimal_report.feature @@ -25,3 +25,12 @@ Scenario: Minimal error report for an Unhandled Exception with a corrupted file And the event "unhandled" is true And the event "incomplete" is true And the event "severityReason.type" equals "unhandledException" + +Scenario: Minimal error report with old filename + When I run "MinimalUnhandledExceptionScenario" + And I set environment variable "EVENT_TYPE" to "CorruptedOldReportScenario" + And I relaunch the app + Then I should receive 1 request + And the request is valid for the error reporting API + And the event "unhandled" is false + And the event "incomplete" is false diff --git a/gradle.properties b/gradle.properties index 40de93228d..8ced910758 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=4.17.0 +VERSION_NAME=4.17.1 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git diff --git a/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CorruptedReportScenario.kt b/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CorruptedReportScenario.kt new file mode 100644 index 0000000000..dc8a24df7b --- /dev/null +++ b/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CorruptedReportScenario.kt @@ -0,0 +1,22 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import java.io.File + +/** + * Verifies that if a report is corrupted, minimal information is still sent to bugsnag. + */ +internal class CorruptedReportScenario(config: Configuration, + context: Context) : Scenario(config, context) { + + init { + config.setAutoCaptureSessions(false) + val files = File(context.cacheDir, "bugsnag-errors").listFiles() + files.forEach { it.writeText("{\"exceptions\":[{\"stacktrace\":[") } + + val nativeFiles = File(context.cacheDir, "bugsnag-native").listFiles() + nativeFiles.forEach { it.writeText("{\"exceptions\":[{\"stacktrace\":[") } + } +} diff --git a/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/EmptyReportScenario.kt b/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/EmptyReportScenario.kt new file mode 100644 index 0000000000..a20a0a3daa --- /dev/null +++ b/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/EmptyReportScenario.kt @@ -0,0 +1,19 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import java.io.File + +/** + * Verifies that if a report is empty, minimal information is still sent to bugsnag. + */ +internal class EmptyReportScenario(config: Configuration, + context: Context) : Scenario(config, context) { + + init { + config.setAutoCaptureSessions(false) + val files = File(context.cacheDir, "bugsnag-errors").listFiles() + files.forEach { it.writeText("") } + } +} diff --git a/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/MinimalHandledExceptionScenario.kt b/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/MinimalHandledExceptionScenario.kt new file mode 100644 index 0000000000..10f0bb5cc5 --- /dev/null +++ b/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/MinimalHandledExceptionScenario.kt @@ -0,0 +1,24 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import java.io.File + +/** + * Sends a handled exception to Bugsnag, which does not include session data. + */ +internal class MinimalHandledExceptionScenario(config: Configuration, + context: Context) : Scenario(config, context) { + + init { + config.setAutoCaptureSessions(false) + disableAllDelivery(config) + } + + override fun run() { + super.run() + Bugsnag.notify(java.lang.RuntimeException("Whoops")) + } + +} diff --git a/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/MinimalUnhandledExceptionScenario.kt b/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/MinimalUnhandledExceptionScenario.kt new file mode 100644 index 0000000000..c388f7cce7 --- /dev/null +++ b/tests/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/MinimalUnhandledExceptionScenario.kt @@ -0,0 +1,22 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Configuration +import java.io.File + +/** + * Sends an unhandled exception to Bugsnag. + */ +internal class MinimalUnhandledExceptionScenario(config: Configuration, + context: Context) : Scenario(config, context) { + init { + config.setAutoCaptureSessions(false) + disableAllDelivery(config) + } + + override fun run() { + super.run() + throw java.lang.IllegalStateException("Whoops") + } + +} diff --git a/tests/features/minimal_report.feature b/tests/features/minimal_report.feature new file mode 100644 index 0000000000..00192b82c0 --- /dev/null +++ b/tests/features/minimal_report.feature @@ -0,0 +1,25 @@ +Feature: Minimal error information is reported for corrupted/empty files + +Scenario: Minimal error report for a Handled Exception with an empty file + When I run "MinimalHandledExceptionScenario" and relaunch the app + And I configure Bugsnag for "EmptyReportScenario" + And I wait to receive a request + And the request is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the payload field "events.0.exceptions.0.stacktrace" is an array with 0 elements + And the exception "errorClass" equals "java.lang.RuntimeException" + And the event "severity" equals "warning" + And the event "unhandled" is false + And the event "incomplete" is true + And the event "severityReason.type" equals "handledException" + +Scenario: Minimal error report for an Unhandled Exception with a corrupted file + When I run "MinimalUnhandledExceptionScenario" and relaunch the app + And I configure Bugsnag for "CorruptedReportScenario" + And I wait to receive a request + And the request is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the payload field "events.0.exceptions.0.stacktrace" is an array with 0 elements + And the exception "errorClass" equals "java.lang.IllegalStateException" + And the event "severity" equals "error" + And the event "unhandled" is true + And the event "incomplete" is true + And the event "severityReason.type" equals "unhandledException"