From 8c1e33b350e2c57753c1d21ea338250ed48b1883 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Tue, 9 Mar 2021 13:03:40 +0000 Subject: [PATCH] feat: propagate app.isLaunching to NDK layer --- CHANGELOG.md | 1 + .../android/ObserverInterfaceTest.java | 8 ++ .../java/com/bugsnag/android/AppWithState.kt | 2 +- .../main/java/com/bugsnag/android/Client.java | 1 + .../com/bugsnag/android/LaunchCrashTracker.kt | 4 +- .../java/com/bugsnag/android/StateEvent.kt | 1 + .../resources/app_serialization_0.json | 2 +- .../src/main/assets/include/event.h | 3 + .../com/bugsnag/android/ndk/NativeBridge.kt | 2 + .../src/main/jni/bugsnag_ndk.c | 11 +++ .../src/main/jni/event.c | 10 ++ .../src/main/jni/event.h | 3 +- .../src/main/jni/metadata.c | 1 + .../src/main/jni/utils/migrate.h | 45 ++++++++- .../src/main/jni/utils/serializer.c | 99 +++++++++++++++++-- .../src/test/cpp/test_bsg_event.c | 10 ++ .../src/test/cpp/test_serializer.c | 1 + .../src/test/cpp/test_utils_serialize.c | 67 +++++++++++-- .../src/main/cpp/cxx-scenarios.cpp | 12 +++ .../scenarios/CXXDelayedErrorScenario.kt | 42 ++++++++ .../CXXMarkLaunchCompletedScenario.kt | 30 ++++++ .../scenarios/JvmDelayedErrorScenario.kt | 40 ++++++++ .../JvmMarkLaunchCompletedScenario.kt | 27 +++++ .../identify_crashes_on_launch.feature | 46 +++++++++ features/smoke_tests/unhandled.feature | 4 +- 25 files changed, 454 insertions(+), 18 deletions(-) create mode 100644 features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXDelayedErrorScenario.kt create mode 100644 features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXMarkLaunchCompletedScenario.kt create mode 100644 features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmDelayedErrorScenario.kt create mode 100644 features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmMarkLaunchCompletedScenario.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index cf96481c42..e0f56a10d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ [#1184](https://github.com/bugsnag/bugsnag-android/pull/1184) [#1185](https://github.com/bugsnag/bugsnag-android/pull/1185) [#1186](https://github.com/bugsnag/bugsnag-android/pull/1186) + [#1180](https://github.com/bugsnag/bugsnag-android/pull/1180) ## 5.7.0 (2021-02-18) diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java index dc98a69cb1..4b1315f565 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java @@ -2,6 +2,7 @@ import static com.bugsnag.android.BugsnagTestUtils.generateConfiguration; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -151,6 +152,13 @@ public void testClientSetContextSendsMessage() { assertEquals("Pod Bay", msg.getContext()); } + @Test + public void testClientMarkLaunchCompletedSendsMessage() { + client.markLaunchCompleted(); + StateEvent.UpdateIsLaunching msg = findMessageInQueue(StateEvent.UpdateIsLaunching.class); + assertFalse(msg.isLaunching()); + } + @Test public void testClientSetUserId() { client.setUser("personX", "bip@example.com", "Loblaw"); diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppWithState.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppWithState.kt index 05b06a70ea..173ce4e192 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppWithState.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppWithState.kt @@ -67,6 +67,6 @@ class AppWithState( writer.name("duration").value(duration) writer.name("durationInForeground").value(durationInForeground) writer.name("inForeground").value(inForeground) - writer.name("isLaunching").value(inForeground) + writer.name("isLaunching").value(isLaunching) } } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 41b945519b..ab2c615451 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -365,6 +365,7 @@ void registerObserver(Observer observer) { userState.addObserver(observer); contextState.addObserver(observer); deliveryDelegate.addObserver(observer); + launchCrashTracker.addObserver(observer); } /** diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt index 95d98c006f..2b497ec326 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt @@ -10,7 +10,7 @@ import java.util.concurrent.atomic.AtomicBoolean * configuration.launchDurationMillis, after which which the launch period is considered * complete. If this value is zero, then the user must manually call markLaunchCompleted(). */ -internal class LaunchCrashTracker(config: ImmutableConfig) { +internal class LaunchCrashTracker(config: ImmutableConfig) : BaseObservable() { private val launching = AtomicBoolean(true) private val executor = ScheduledThreadPoolExecutor(1) @@ -32,6 +32,8 @@ internal class LaunchCrashTracker(config: ImmutableConfig) { fun markLaunchCompleted() { executor.shutdown() launching.set(false) + notifyObservers(StateEvent.UpdateIsLaunching(false)) + logger.d("App launch period marked as complete") } fun isLaunching() = launching.get() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt index 7f6bcf5ae4..091da1f2d8 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt @@ -35,6 +35,7 @@ sealed class StateEvent { class UpdateContext(val context: String?) : StateEvent() class UpdateInForeground(val inForeground: Boolean, val contextActivity: String?) : StateEvent() + class UpdateIsLaunching(val isLaunching: Boolean) : StateEvent() class UpdateOrientation(val orientation: String?) : StateEvent() class UpdateUser(val user: User) : StateEvent() diff --git a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization_0.json b/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization_0.json index 846afa0c59..e7a69b0d5d 100644 --- a/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization_0.json +++ b/bugsnag-plugin-android-ndk/src/androidTest/resources/app_serialization_0.json @@ -1 +1 @@ -{"app":{"version":"22","id":"com.bugsnag.example","type":"android","releaseStage":"prod","versionCode":55,"buildUUID":"1234-uuid","binaryArch":"x86","duration":6502,"durationInForeground":6502,"inForeground":true}} \ No newline at end of file +{"app":{"version":"22","id":"com.bugsnag.example","type":"android","releaseStage":"prod","versionCode":55,"buildUUID":"1234-uuid","binaryArch":"x86","duration":6502,"durationInForeground":6502,"inForeground":true,"isLaunching":true}} \ No newline at end of file diff --git a/bugsnag-plugin-android-ndk/src/main/assets/include/event.h b/bugsnag-plugin-android-ndk/src/main/assets/include/event.h index 8e86657817..daf69f4b20 100644 --- a/bugsnag-plugin-android-ndk/src/main/assets/include/event.h +++ b/bugsnag-plugin-android-ndk/src/main/assets/include/event.h @@ -152,6 +152,9 @@ void bugsnag_app_set_id(void *event_ptr, char *value); bool bugsnag_app_get_in_foreground(void *event_ptr); void bugsnag_app_set_in_foreground(void *event_ptr, bool value); +bool bugsnag_app_get_is_launching(void *event_ptr); +void bugsnag_app_set_is_launching(void *event_ptr, bool value); + char *bugsnag_app_get_release_stage(void *event_ptr); void bugsnag_app_set_release_stage(void *event_ptr, char *value); diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt index 9a5044c5ff..a693ee8607 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt @@ -71,6 +71,7 @@ class NativeBridge : Observer { external fun pausedSession() external fun updateContext(context: String) external fun updateInForeground(inForeground: Boolean, activityName: String) + external fun updateIsLaunching(isLaunching: Boolean) external fun updateOrientation(orientation: String) external fun updateUserId(newValue: String) external fun updateUserEmail(newValue: String) @@ -122,6 +123,7 @@ class NativeBridge : Observer { msg.inForeground, makeSafe(msg.contextActivity ?: "") ) + is StateEvent.UpdateIsLaunching -> updateIsLaunching(msg.isLaunching) is UpdateOrientation -> updateOrientation(msg.orientation ?: "") is UpdateUser -> { updateUserId(makeSafe(msg.user.id ?: "")) diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c index 68a54f7505..918efb54f5 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag_ndk.c @@ -405,6 +405,17 @@ Java_com_bugsnag_android_ndk_NativeBridge_updateInForeground( } } +JNIEXPORT void JNICALL +Java_com_bugsnag_android_ndk_NativeBridge_updateIsLaunching( + JNIEnv *env, jobject _this, jboolean new_value) { + if (bsg_global_env == NULL) { + return; + } + bsg_request_env_write_lock(); + bugsnag_app_set_is_launching(&bsg_global_env->next_event, new_value); + bsg_release_env_write_lock(); +} + JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_updateLowMemory(JNIEnv *env, jobject _this, diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.c b/bugsnag-plugin-android-ndk/src/main/jni/event.c index 456f1c3aea..ac04b40788 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.c @@ -324,6 +324,16 @@ void bugsnag_app_set_in_foreground(void *event_ptr, bool value) { event->app.in_foreground = value; } +bool bugsnag_app_get_is_launching(void *event_ptr) { + bugsnag_event *event = (bugsnag_event *)event_ptr; + return event->app.is_launching; +} + +void bugsnag_app_set_is_launching(void *event_ptr, bool value) { + bugsnag_event *event = (bugsnag_event *)event_ptr; + event->app.is_launching = value; +} + /* Accessors for event.device */ bool bugsnag_device_get_jailbroken(void *event_ptr) { diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.h b/bugsnag-plugin-android-ndk/src/main/jni/event.h index 3fa9e26b8c..e32d8aee37 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.h @@ -27,7 +27,7 @@ /** * Version of the bugsnag_event struct. Serialized to report header. */ -#define BUGSNAG_EVENT_VERSION 4 +#define BUGSNAG_EVENT_VERSION 5 #ifdef __cplusplus extern "C" { @@ -60,6 +60,7 @@ typedef struct { */ time_t duration_in_foreground_ms_offset; bool in_foreground; + bool is_launching; char binary_arch[32]; } bsg_app_info; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c index 120c96a4ff..f4be7f833e 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/metadata.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/metadata.c @@ -442,6 +442,7 @@ void bsg_populate_app_data(JNIEnv *env, bsg_jni_cache *jni_cache, sizeof(event->app.id)); event->app.in_foreground = bsg_get_map_value_bool(env, jni_cache, data, "inForeground"); + event->app.is_launching = true; char name[64]; bsg_copy_map_value_string(env, jni_cache, data, "name", name, sizeof(name)); diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h index c2f6a8d90d..e7704ee927 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/migrate.h @@ -87,6 +87,23 @@ typedef struct { char binaryArch[32]; } bsg_app_info_v1; +typedef struct { + char id[64]; + char release_stage[64]; + char type[32]; + char version[32]; + char active_screen[64]; + int version_code; + char build_uuid[64]; + time_t duration; + time_t duration_in_foreground; + time_t duration_ms_offset; + time_t duration_in_foreground_ms_offset; + bool in_foreground; + bool is_launching; + char binary_arch[32]; +} bsg_app_info_v2; + typedef struct { int api_level; double battery_level; @@ -158,7 +175,7 @@ typedef struct { typedef struct { bsg_notifier notifier; - bsg_app_info app; + bsg_app_info_v2 app; bsg_device_info device; bugsnag_user user; bsg_error error; @@ -181,6 +198,32 @@ typedef struct { bool unhandled; } bugsnag_report_v3; +typedef struct { + bsg_notifier notifier; + bsg_app_info_v2 app; + bsg_device_info device; + bugsnag_user user; + bsg_error error; + bugsnag_metadata metadata; + + int crumb_count; + // Breadcrumbs are a ring; the first index moves as the + // structure is filled and replaced. + int crumb_first_index; + bugsnag_breadcrumb breadcrumbs[BUGSNAG_CRUMBS_MAX]; + + char context[64]; + bugsnag_severity severity; + + char session_id[33]; + char session_start[33]; + int handled_events; + int unhandled_events; + char grouping_hash[64]; + bool unhandled; + char api_key[64]; +} bugsnag_report_v4; + #ifdef __cplusplus } #endif diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c index 4b386b1522..774539a70b 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/serializer.c @@ -18,13 +18,14 @@ extern "C" { bool bsg_event_write(bsg_report_header *header, bugsnag_event *event, int fd); bugsnag_event *bsg_event_read(int fd); -bugsnag_event *bsg_report_v4_read(int fd); bsg_report_header *bsg_report_header_read(int fd); +bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4); bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3); bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2); bugsnag_event *bsg_map_v1_to_report(bugsnag_report_v1 *report_v1); void migrate_app_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); +void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event); void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); void migrate_breadcrumb_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event); @@ -87,7 +88,19 @@ bugsnag_report_v3 *bsg_report_v3_read(int fd) { return event; } -bugsnag_event *bsg_report_v4_read(int fd) { +bugsnag_report_v4 *bsg_report_v4_read(int fd) { + size_t event_size = sizeof(bugsnag_report_v4); + bugsnag_report_v4 *event = malloc(event_size); + + ssize_t len = read(fd, event, event_size); + if (len != event_size) { + free(event); + return NULL; + } + return event; +} + +bugsnag_event *bsg_report_v5_read(int fd) { size_t event_size = sizeof(bugsnag_event); bugsnag_event *event = malloc(event_size); @@ -99,6 +112,16 @@ bugsnag_event *bsg_report_v4_read(int fd) { return event; } +/** + * Reads persisted structs into memory from disk. The report version is + * serialized in the file header, and old structs are maintained in migrate.h + * for backwards compatibility. These are then migrated to the current + * bugsnag_event struct. + * + * Note that calling the individual bsg_map_v functions will free the parameter + * - this is to conserve memory when migrating particularly old payload + * versions. + */ bugsnag_event *bsg_event_read(int fd) { bsg_report_header *header = bsg_report_header_read(fd); if (header == NULL) { @@ -118,8 +141,45 @@ bugsnag_event *bsg_event_read(int fd) { } else if (event_version == 3) { bugsnag_report_v3 *report_v3 = bsg_report_v3_read(fd); event = bsg_map_v3_to_report(report_v3); - } else { - event = bsg_report_v4_read(fd); + } else if (event_version == 4) { + bugsnag_report_v4 *report_v4 = bsg_report_v4_read(fd); + event = bsg_map_v4_to_report(report_v4); + } else if (event_version == 5) { + event = bsg_report_v5_read(fd); + } + return event; +} + +bugsnag_event *bsg_map_v4_to_report(bugsnag_report_v4 *report_v4) { + if (report_v4 == NULL) { + return NULL; + } + bugsnag_event *event = malloc(sizeof(bugsnag_event)); + + if (event != NULL) { + event->notifier = report_v4->notifier; + event->device = report_v4->device; + event->user = report_v4->user; + event->error = report_v4->error; + event->metadata = report_v4->metadata; + event->crumb_count = report_v4->crumb_count; + event->crumb_first_index = report_v4->crumb_first_index; + memcpy(event->breadcrumbs, report_v4->breadcrumbs, + sizeof(event->breadcrumbs)); + event->severity = report_v4->severity; + bsg_strncpy_safe(event->session_id, report_v4->session_id, + sizeof(event->session_id)); + bsg_strncpy_safe(event->session_start, report_v4->session_start, + sizeof(event->session_id)); + event->handled_events = report_v4->handled_events; + event->unhandled_events = report_v4->unhandled_events; + bsg_strncpy_safe(event->grouping_hash, report_v4->grouping_hash, + sizeof(event->session_id)); + event->unhandled = report_v4->unhandled; + bsg_strncpy_safe(event->api_key, report_v4->api_key, + sizeof(event->api_key)); + migrate_app_v2(report_v4, event); + free(report_v4); } return event; } @@ -128,7 +188,7 @@ bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3) { if (report_v3 == NULL) { return NULL; } - bugsnag_event *event = malloc(sizeof(bugsnag_event)); + bugsnag_report_v4 *event = malloc(sizeof(bugsnag_event)); if (event != NULL) { event->notifier = report_v3->notifier; @@ -153,7 +213,7 @@ bugsnag_event *bsg_map_v3_to_report(bugsnag_report_v3 *report_v3) { strcpy(event->api_key, ""); free(report_v3); } - return event; + return bsg_map_v4_to_report(event); } bugsnag_event *bsg_map_v2_to_report(bugsnag_report_v2 *report_v2) { @@ -294,6 +354,32 @@ void migrate_app_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { bugsnag_event_add_metadata_string(event, "app", "name", report_v2->app.name); } +void migrate_app_v2(bugsnag_report_v4 *report_v4, bugsnag_event *event) { + bsg_strncpy_safe(event->app.id, report_v4->app.id, sizeof(event->app.id)); + bsg_strncpy_safe(event->app.release_stage, report_v4->app.release_stage, + sizeof(event->app.release_stage)); + bsg_strncpy_safe(event->app.type, report_v4->app.type, + sizeof(event->app.type)); + bsg_strncpy_safe(event->app.version, report_v4->app.version, + sizeof(event->app.version)); + bsg_strncpy_safe(event->app.active_screen, report_v4->app.active_screen, + sizeof(event->app.active_screen)); + bsg_strncpy_safe(event->app.build_uuid, report_v4->app.build_uuid, + sizeof(event->app.build_uuid)); + bsg_strncpy_safe(event->app.binary_arch, report_v4->app.binary_arch, + sizeof(event->app.binary_arch)); + event->app.version_code = report_v4->app.version_code; + event->app.duration = report_v4->app.duration; + event->app.duration_in_foreground = report_v4->app.duration_in_foreground; + event->app.duration_ms_offset = report_v4->app.duration_ms_offset; + event->app.duration_in_foreground_ms_offset = + report_v4->app.duration_in_foreground_ms_offset; + event->app.in_foreground = report_v4->app.in_foreground; + + // no info available, set to sensible default + event->app.is_launching = false; +} + void migrate_device_v1(bugsnag_report_v2 *report_v2, bugsnag_report_v3 *event) { bsg_strncpy_safe( event->device.os_name, bsg_os_name(), @@ -487,6 +573,7 @@ void bsg_serialize_app(const bsg_app_info app, JSON_Object *event_obj) { json_object_dotset_number(event_obj, "app.durationInForeground", app.duration_in_foreground); json_object_dotset_boolean(event_obj, "app.inForeground", app.in_foreground); + json_object_dotset_boolean(event_obj, "app.isLaunching", app.is_launching); } void bsg_serialize_app_metadata(const bsg_app_info app, diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c index 79e28412cb..a6d17b4e02 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_bsg_event.c @@ -27,6 +27,7 @@ bugsnag_event *init_event() { event->app.duration = 9019; event->app.duration_in_foreground = 7017; event->app.in_foreground = true; + event->app.is_launching = true; bsg_strncpy_safe(event->grouping_hash, "Bar", sizeof(event->grouping_hash)); event->device.jailbroken = true; @@ -188,6 +189,14 @@ TEST test_app_in_foreground(void) { PASS(); } +TEST test_app_is_launching(void) { + bugsnag_event *event = init_event(); + ASSERT(bugsnag_app_get_is_launching(event)); + bugsnag_app_set_is_launching(event, false); + ASSERT_FALSE(bugsnag_app_get_is_launching(event)); + free(event); + PASS(); +} TEST test_device_jailbroken(void) { bugsnag_event *event = init_event(); @@ -394,6 +403,7 @@ SUITE(event_mutators) { RUN_TEST(test_app_duration); RUN_TEST(test_app_duration_in_foreground); RUN_TEST(test_app_in_foreground); + RUN_TEST(test_app_is_launching); RUN_TEST(test_device_jailbroken); RUN_TEST(test_device_id); RUN_TEST(test_device_locale); diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.c index 0198f06f20..951968bc27 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_serializer.c @@ -44,6 +44,7 @@ bsg_app_info * loadAppTestCase(jint num) { app->duration_ms_offset = 0; app->duration_in_foreground_ms_offset = 0; app->in_foreground = true; + app->is_launching = true; strcpy(app->binary_arch, "x86"); return app; } diff --git a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c index 83835855c2..5ec0a4036b 100644 --- a/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c +++ b/bugsnag-plugin-android-ndk/src/test/cpp/test_utils_serialize.c @@ -36,6 +36,15 @@ bool bsg_report_v3_write(bsg_report_header *header, bugsnag_report_v3 *report, return len == sizeof(bugsnag_report_v3); } +bool bsg_report_v4_write(bsg_report_header *header, bugsnag_report_v4 *report, + int fd) { + if (!bsg_report_header_write(header, fd)) { + return false; + } + ssize_t len = write(fd, report, sizeof(bugsnag_report_v4)); + return len == sizeof(bugsnag_report_v4); +} + bool bsg_serialize_report_v1_to_file(bsg_environment *env, bugsnag_report_v1 *report) { int fd = open(env->next_event_path, O_WRONLY | O_CREAT, 0644); if (fd == -1) { @@ -60,6 +69,14 @@ bool bsg_serialize_report_v3_to_file(bsg_environment *env, bugsnag_report_v3 *re return bsg_report_v3_write(&env->report_header, report, fd); } +bool bsg_serialize_report_v4_to_file(bsg_environment *env, bugsnag_report_v4 *report) { + int fd = open(env->next_event_path, O_WRONLY | O_CREAT, 0644); + if (fd == -1) { + return false; + } + return bsg_report_v4_write(&env->report_header, report, fd); +} + void generate_basic_report(bugsnag_event *event) { strcpy(event->grouping_hash, "foo-hash"); @@ -85,7 +102,6 @@ void generate_basic_report(bugsnag_event *event) { strcpy(event->user.id, "fex"); event->device.total_memory = 234678100; event->app.duration = 6502; - event->app.in_foreground = true; bugsnag_event_add_metadata_bool(event, "metrics", "experimentX", false); bugsnag_event_add_metadata_string(event, "metrics", "subject", "percy"); bugsnag_event_add_metadata_string(event, "app", "weather", "rain"); @@ -109,13 +125,18 @@ void generate_basic_report(bugsnag_event *event) { strcpy(event->notifier.name, "Test Notifier"); } +bugsnag_report_v4 *bsg_generate_report_v4(void) { + bugsnag_report_v4 *report = calloc(1, sizeof(bugsnag_report_v4)); + generate_basic_report((bugsnag_event *) report); + return report; +} + bugsnag_report_v3 *bsg_generate_report_v3(void) { bugsnag_report_v3 *report = calloc(1, sizeof(bugsnag_report_v3)); generate_basic_report((bugsnag_event *) report); return report; } - bugsnag_report_v2 *bsg_generate_report_v2(void) { bugsnag_report_v2 *report = calloc(1, sizeof(bugsnag_report_v2)); generate_basic_report((bugsnag_event *) report); @@ -181,7 +202,7 @@ TEST test_report_to_file(void) { TEST test_file_to_report(void) { bsg_environment *env = malloc(sizeof(bsg_environment)); - env->report_header.version = 7; + env->report_header.version = 5; env->report_header.big_endian = 1; strcpy(env->report_header.os_build, "macOS Sierra"); bugsnag_event *generated_report = bsg_generate_event(); @@ -215,6 +236,7 @@ TEST test_report_v1_migration(void) { ASSERT(strcmp("2019-03-19T12:58:19+00:00", event->session_start) == 0); ASSERT_EQ(1, event->handled_events); ASSERT_EQ(1, event->unhandled_events); + ASSERT_FALSE(event->app.is_launching); free(generated_report); free(env); @@ -224,13 +246,14 @@ TEST test_report_v1_migration(void) { TEST test_report_v2_migration(void) { bsg_environment *env = malloc(sizeof(bsg_environment)); - env->report_header.version = 2; - env->report_header.big_endian = 1; - strcpy(env->report_header.os_build, "macOS Sierra"); bugsnag_report_v2 *generated_report = bsg_generate_report_v2(); memcpy(&env->next_event, generated_report, sizeof(bugsnag_report_v2)); strcpy(env->next_event_path, SERIALIZE_TEST_FILE); + + env->report_header.version = 2; + env->report_header.big_endian = 1; + strcpy(env->report_header.os_build, "macOS Sierra"); bsg_serialize_report_v2_to_file(env, generated_report); bugsnag_event *event = bsg_deserialize_event_from_file(SERIALIZE_TEST_FILE); @@ -267,6 +290,7 @@ TEST test_report_v2_migration(void) { ASSERT_EQ(BSG_CRUMB_STATE, crumb->type); ASSERT_STR_EQ("message", val->key); ASSERT_STR_EQ("Moving laterally 26º", val->value); + ASSERT_FALSE(event->app.is_launching); free(generated_report); free(env); @@ -298,6 +322,36 @@ TEST test_report_v3_migration(void) { ASSERT_STR_EQ("SIGBUS", event->error.errorClass); ASSERT_STR_EQ("POSIX is serious about oncoming traffic", event->error.errorMessage); ASSERT_STR_EQ("C", event->error.type); + ASSERT_FALSE(event->app.is_launching); + + free(generated_report); + free(env); + free(event); + PASS(); +} + +TEST test_report_v4_migration(void) { + bsg_environment *env = malloc(sizeof(bsg_environment)); + env->report_header.version = 4; + env->report_header.big_endian = 1; + strcpy(env->report_header.os_build, "macOS Sierra"); + + bugsnag_report_v4 *generated_report = bsg_generate_report_v4(); + memcpy(&env->next_event, generated_report, sizeof(bugsnag_report_v4)); + strcpy(env->next_event_path, SERIALIZE_TEST_FILE); + bsg_serialize_report_v4_to_file(env, generated_report); + + bugsnag_event *event = bsg_deserialize_event_from_file(SERIALIZE_TEST_FILE); + ASSERT(event != NULL); + + // values are migrated correctly + ASSERT_STR_EQ("com.example.PhotoSnapPlus", event->app.id); + ASSERT_STR_EQ("リリース", event->app.release_stage); + ASSERT_STR_EQ("2.0.52", event->app.version); + ASSERT_EQ(57, event->app.version_code); + ASSERT_STR_EQ("1234-9876-adfe", event->app.build_uuid); + ASSERT_FALSE(event->app.in_foreground); + ASSERT_FALSE(event->app.is_launching); free(generated_report); free(env); @@ -436,6 +490,7 @@ SUITE(serialize_utils) { RUN_TEST(test_report_v1_migration); RUN_TEST(test_report_v2_migration); RUN_TEST(test_report_v3_migration); + RUN_TEST(test_report_v4_migration); RUN_TEST(test_session_handled_counts); RUN_TEST(test_context_to_json); RUN_TEST(test_grouping_hash_to_json); diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp index a5d4dcbe5f..d2f8574060 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp @@ -341,4 +341,16 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_mazerunner_scenarios_CXXNaughtyStringsScenario_crash(JNIEnv *env, jobject instance) { abort(); } + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_mazerunner_scenarios_CXXDelayedErrorScenario_crash(JNIEnv *env, + jobject thiz) { + abort(); +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_mazerunner_scenarios_CXXMarkLaunchCompletedScenario_crash(JNIEnv *env, + jobject thiz) { + abort(); +} } diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXDelayedErrorScenario.kt b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXDelayedErrorScenario.kt new file mode 100644 index 0000000000..5a5b9e3919 --- /dev/null +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXDelayedErrorScenario.kt @@ -0,0 +1,42 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import android.os.Handler +import android.os.Looper +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration + +/** + * Sends an NDK error to Bugsnag shortly after the launchDurationMillis has past. + */ +internal class CXXDelayedErrorScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + companion object { + private const val CRASH_DELAY_MS = 250L + } + + external fun crash() + + init { + config.autoTrackSessions = false + config.launchDurationMillis = CRASH_DELAY_MS + System.loadLibrary("cxx-scenarios") + } + + override fun startScenario() { + super.startScenario() + Bugsnag.notify(RuntimeException("first error")) + val handler = Handler(Looper.getMainLooper()) + + handler.postDelayed( + { + crash() + }, + CRASH_DELAY_MS * 2 + ) + } +} diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXMarkLaunchCompletedScenario.kt b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXMarkLaunchCompletedScenario.kt new file mode 100644 index 0000000000..19f3faa4f7 --- /dev/null +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXMarkLaunchCompletedScenario.kt @@ -0,0 +1,30 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration + +/** + * Sends an NDK error to Bugsnag after markLaunchCompleted() is invoked. + */ +internal class CXXMarkLaunchCompletedScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + external fun crash() + + init { + config.autoTrackSessions = false + config.launchDurationMillis = 0 + System.loadLibrary("cxx-scenarios") + } + + override fun startScenario() { + super.startScenario() + Bugsnag.notify(RuntimeException("first error")) + Bugsnag.markLaunchCompleted() + crash() + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmDelayedErrorScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmDelayedErrorScenario.kt new file mode 100644 index 0000000000..6c894b0723 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmDelayedErrorScenario.kt @@ -0,0 +1,40 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import android.os.Handler +import android.os.Looper +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration + +/** + * Sends a JVM error to Bugsnag shortly after the launchDurationMillis has past. + */ +internal class JvmDelayedErrorScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + companion object { + private const val CRASH_DELAY_MS = 250L + } + + init { + config.autoTrackSessions = false + config.launchDurationMillis = CRASH_DELAY_MS + } + + override fun startScenario() { + super.startScenario() + Bugsnag.notify(RuntimeException("first error")) + + val handler = Handler(Looper.getMainLooper()) + + handler.postDelayed( + { + Bugsnag.notify(generateException()) + }, + CRASH_DELAY_MS * 2 + ) + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmMarkLaunchCompletedScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmMarkLaunchCompletedScenario.kt new file mode 100644 index 0000000000..46dab2b253 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/JvmMarkLaunchCompletedScenario.kt @@ -0,0 +1,27 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration + +/** + * Sends a JVM error to Bugsnag after markLaunchCompleted() is invoked. + */ +internal class JvmMarkLaunchCompletedScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + init { + config.autoTrackSessions = false + config.launchDurationMillis = 0 + } + + override fun startScenario() { + super.startScenario() + Bugsnag.notify(RuntimeException("first error")) + Bugsnag.markLaunchCompleted() + Bugsnag.notify(generateException()) + } +} diff --git a/features/full_tests/batch_1/identify_crashes_on_launch.feature b/features/full_tests/batch_1/identify_crashes_on_launch.feature index dbca29e87c..9f0484425a 100644 --- a/features/full_tests/batch_1/identify_crashes_on_launch.feature +++ b/features/full_tests/batch_1/identify_crashes_on_launch.feature @@ -1,5 +1,51 @@ Feature: Identifying crashes on launch + Scenario: A JVM error captured after the launch period is false for app.isLaunching + When I run "JvmDelayedErrorScenario" + Then I wait to receive 2 errors + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "first error" + And the event "app.isLaunching" is true + Then I discard the oldest error + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "JvmDelayedErrorScenario" + And the event "app.isLaunching" is false + + Scenario: An NDK error captured after the launch period is false for app.isLaunching + When I run "CXXDelayedErrorScenario" and relaunch the app + And I configure Bugsnag for "CXXDelayedErrorScenario" + Then I wait to receive 2 errors + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "first error" + And the event "app.isLaunching" is true + Then I discard the oldest error + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "Abort program" + And the event "app.isLaunching" is false + + Scenario: A JVM error captured after markLaunchComplete() is false for app.isLaunching + When I run "JvmMarkLaunchCompletedScenario" + Then I wait to receive 2 errors + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "first error" + And the event "app.isLaunching" is true + Then I discard the oldest error + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "JvmMarkLaunchCompletedScenario" + And the event "app.isLaunching" is false + + Scenario: An NDK error captured after markLaunchComplete() is false for app.isLaunching + When I run "CXXMarkLaunchCompletedScenario" and relaunch the app + And I configure Bugsnag for "CXXMarkLaunchCompletedScenario" + Then I wait to receive 2 errors + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "first error" + And the event "app.isLaunching" is true + Then I discard the oldest error + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "Abort program" + And the event "app.isLaunching" is false + Scenario: Escaping from a crash loop by reading LastRunInfo in a JVM error When I run "JvmCrashLoopScenario" and relaunch the app When I run "JvmCrashLoopScenario" diff --git a/features/smoke_tests/unhandled.feature b/features/smoke_tests/unhandled.feature index 206d06c3ec..ba4350b51f 100644 --- a/features/smoke_tests/unhandled.feature +++ b/features/smoke_tests/unhandled.feature @@ -126,6 +126,7 @@ Scenario: Signal raised with overwritten config And the error payload field "events.0.app.duration" is an integer And the error payload field "events.0.app.durationInForeground" is an integer And the event "app.inForeground" is true + And the event "app.isLaunching" is true And the event "metaData.app.name" equals "MazeRunner" # Device data @@ -205,6 +206,7 @@ Scenario: C++ exception thrown with overwritten config And the error payload field "events.0.app.duration" is an integer And the error payload field "events.0.app.durationInForeground" is an integer And the event "app.inForeground" is true + And the event "app.isLaunching" is true And the event "metaData.app.name" equals "MazeRunner" # Device data @@ -280,7 +282,7 @@ Scenario: ANR detection And the error payload field "events.0.app.duration" is an integer And the error payload field "events.0.app.durationInForeground" is an integer And the event "app.inForeground" is true - And the event "app.isLaunching" is true + And the event "app.isLaunching" is false And the event "metaData.app.name" equals "MazeRunner" # Device data