From d031a9c5908218971cbbb4577a1230c1c8b32064 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Wed, 13 Feb 2019 11:59:19 +0000 Subject: [PATCH 01/18] feat: add stopSession() and resumeSession() to Client Adds the ability to stop and resume sessions. This prevents the handled/unhandled error count from incrementing when a session is in the stopped state, which may be desirable if a user manually tracks sessions and does not care about crashes that occur in the background. --- .../android/SessionTrackerStopResumeTest.kt | 123 ++++++++++++++++++ .../main/java/com/bugsnag/android/Client.java | 10 +- .../java/com/bugsnag/android/Session.java | 1 + .../com/bugsnag/android/SessionTracker.java | 39 +++++- 4 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 sdk/src/androidTest/java/com/bugsnag/android/SessionTrackerStopResumeTest.kt diff --git a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackerStopResumeTest.kt b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackerStopResumeTest.kt new file mode 100644 index 0000000000..858358a98a --- /dev/null +++ b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackerStopResumeTest.kt @@ -0,0 +1,123 @@ +package com.bugsnag.android + +import com.bugsnag.android.BugsnagTestUtils.generateClient +import com.bugsnag.android.BugsnagTestUtils.generateConfiguration +import com.bugsnag.android.BugsnagTestUtils.generateSessionStore +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class SessionTrackerStopResumeTest { + + private val configuration = generateConfiguration().also { + it.autoCaptureSessions = false + } + private val sessionStore = generateSessionStore() + private lateinit var tracker: SessionTracker + + @Before + fun setUp() { + tracker = SessionTracker(configuration, generateClient(), sessionStore) + } + + /** + * Verifies that a session can be resumed after it is stopped + */ + @Test + fun resumeFromStoppedSession() { + tracker.startSession(false) + val originalSession = tracker.currentSession + assertNotNull(originalSession) + + tracker.stopSession() + assertNull(tracker.currentSession) + + assertTrue(tracker.resumeSession()) + assertEquals(originalSession, tracker.currentSession) + } + + /** + * Verifies that a new session is started when calling [SessionTracker.resumeSession], + * if there is no stopped session + */ + @Test + fun resumeWithNoStoppedSession() { + assertNull(tracker.currentSession) + assertFalse(tracker.resumeSession()) + assertNotNull(tracker.currentSession) + } + + /** + * Verifies that a new session can be created after the previous one is stopped + */ + @Test + fun startNewAfterStoppedSession() { + tracker.startSession(false) + val originalSession = tracker.currentSession + + tracker.stopSession() + tracker.startSession(false) + assertNotEquals(originalSession, tracker.currentSession) + } + + /** + * Verifies that calling [SessionTracker.resumeSession] multiple times only starts one session + */ + @Test + fun multipleResumesHaveNoEffect() { + tracker.startSession(false) + val original = tracker.currentSession + tracker.stopSession() + + assertTrue(tracker.resumeSession()) + assertEquals(original, tracker.currentSession) + + assertFalse(tracker.resumeSession()) + assertEquals(original, tracker.currentSession) + } + + /** + * Verifies that calling [SessionTracker.stopSession] multiple times only stops one session + */ + @Test + fun multipleStopsHaveNoEffect() { + tracker.startSession(false) + assertNotNull(tracker.currentSession) + + tracker.stopSession() + assertNull(tracker.currentSession) + + tracker.stopSession() + assertNull(tracker.currentSession) + } + + /** + * Verifies that if a handled or unhandled error occurs when a session is stopped, the + * error count is not updated + */ + @Test + fun stoppedSessionDoesNotIncrement() { + tracker.startSession(false) + tracker.incrementHandledError() + tracker.incrementUnhandledError() + assertEquals(1, tracker.currentSession?.handledCount) + assertEquals(1, tracker.currentSession?.unhandledCount) + + tracker.stopSession() + tracker.incrementHandledError() + tracker.incrementUnhandledError() + tracker.resumeSession() + assertEquals(1, tracker.currentSession?.handledCount) + assertEquals(1, tracker.currentSession?.unhandledCount) + + tracker.incrementHandledError() + tracker.incrementUnhandledError() + assertEquals(2, tracker.currentSession?.handledCount) + assertEquals(2, tracker.currentSession?.unhandledCount) + } +} diff --git a/sdk/src/main/java/com/bugsnag/android/Client.java b/sdk/src/main/java/com/bugsnag/android/Client.java index d8fbdc8839..992f08b367 100644 --- a/sdk/src/main/java/com/bugsnag/android/Client.java +++ b/sdk/src/main/java/com/bugsnag/android/Client.java @@ -289,7 +289,15 @@ public void update(@NonNull Observable observable, @NonNull Object arg) { * session everytime the app enters the foreground. */ public void startSession() { - sessionTracker.startNewSession(new Date(), user, false); + sessionTracker.startSession(false); + } + + public final void stopSession() { + sessionTracker.stopSession(); + } + + public final boolean resumeSession() { + return sessionTracker.resumeSession(); } /** diff --git a/sdk/src/main/java/com/bugsnag/android/Session.java b/sdk/src/main/java/com/bugsnag/android/Session.java index 642b1f2c9a..0e70111488 100644 --- a/sdk/src/main/java/com/bugsnag/android/Session.java +++ b/sdk/src/main/java/com/bugsnag/android/Session.java @@ -34,6 +34,7 @@ public Session(String id, Date startedAt, User user, boolean autoCaptured) { private AtomicInteger unhandledCount = new AtomicInteger(); private AtomicInteger handledCount = new AtomicInteger(); private AtomicBoolean tracked = new AtomicBoolean(false); + final AtomicBoolean isStopped = new AtomicBoolean(false); String getId() { return id; diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java index 07d72dabd3..e9dc4275f8 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java @@ -7,6 +7,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import java.io.File; import java.util.Arrays; @@ -65,7 +66,9 @@ class SessionTracker extends Observable implements Application.ActivityLifecycle * @param date the session start date * @param user the session user (if any) */ - @Nullable Session startNewSession(@NonNull Date date, @Nullable User user, + @Nullable + @VisibleForTesting + Session startNewSession(@NonNull Date date, @Nullable User user, boolean autoCaptured) { if (configuration.getSessionEndpoint() == null) { Logger.warn("The session tracking endpoint has not been set. " @@ -78,6 +81,29 @@ class SessionTracker extends Observable implements Application.ActivityLifecycle return session; } + void startSession(boolean autoCaptured) { + startNewSession(new Date(), client.getUser(), autoCaptured); + } + + void stopSession() { + Session session = currentSession.get(); + + if (session != null) { + session.isStopped.set(true); + } + } + + boolean resumeSession() { + Session session = currentSession.get(); + + if (session == null) { + startSession(false); + return false; + } else { + return session.isStopped.compareAndSet(true, false); + } + } + /** * Determines whether or not a session should be tracked. If this is true, the session will be * stored and sent to the Bugsnag API, otherwise no action will occur in this method. @@ -141,18 +167,23 @@ private String getReleaseStage() { @Nullable Session getCurrentSession() { - return currentSession.get(); + Session session = currentSession.get(); + + if (session != null && !session.isStopped.get()) { + return session; + } + return null; } void incrementUnhandledError() { - Session session = currentSession.get(); + Session session = getCurrentSession(); if (session != null) { session.incrementUnhandledErrCount(); } } void incrementHandledError() { - Session session = currentSession.get(); + Session session = getCurrentSession(); if (session != null) { session.incrementHandledErrCount(); } From 19a2ef97957b9fa822959e9a520e91dab4bcb66b Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Wed, 13 Feb 2019 14:39:07 +0000 Subject: [PATCH 02/18] test: add mazerunner scenarios for stopping/resuming a session when a session is stopped, no session information should be present in error payloads. --- .../scenarios/ResumedSessionScenario.kt | 35 +++++++++++++++++++ .../scenarios/StoppedSessionScenario.kt | 34 ++++++++++++++++++ features/session_stopping.feature | 19 ++++++++++ 3 files changed, 88 insertions(+) create mode 100644 features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/ResumedSessionScenario.kt create mode 100644 features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/StoppedSessionScenario.kt create mode 100644 features/session_stopping.feature diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/ResumedSessionScenario.kt b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/ResumedSessionScenario.kt new file mode 100644 index 0000000000..8ce6a97bc6 --- /dev/null +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/ResumedSessionScenario.kt @@ -0,0 +1,35 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import android.os.Handler +import android.os.HandlerThread + +/** + * Sends 2 exceptions, 1 before resuming a session, and 1 after resuming a session. + */ +internal class ResumedSessionScenario(config: Configuration, + context: Context) : Scenario(config, context) { + init { + config.setAutoCaptureSessions(false) + } + + override fun run() { + super.run() + val client = Bugsnag.getClient() + val thread = HandlerThread("HandlerThread") + thread.start() + + Handler(thread.looper).post { + // send 1st exception + client.startSession() + client.notifyBlocking(generateException()) + + // send 2nd exception after resuming a session + client.stopSession() + client.resumeSession() + client.notifyBlocking(generateException()) + } + } +} diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/StoppedSessionScenario.kt b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/StoppedSessionScenario.kt new file mode 100644 index 0000000000..c3450a0ba9 --- /dev/null +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/StoppedSessionScenario.kt @@ -0,0 +1,34 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import android.os.Handler +import android.os.HandlerThread + +/** + * Sends an exception after stopping the session + */ +internal class StoppedSessionScenario(config: Configuration, + context: Context) : Scenario(config, context) { + init { + config.setAutoCaptureSessions(false) + } + + override fun run() { + super.run() + val client = Bugsnag.getClient() + val thread = HandlerThread("HandlerThread") + thread.start() + + Handler(thread.looper).post { + // send 1st exception which should include session info + client.startSession() + client.notifyBlocking(generateException()) + + // send 2nd exception which should not include session info + client.stopSession() + client.notifyBlocking(generateException()) + } + } +} diff --git a/features/session_stopping.feature b/features/session_stopping.feature new file mode 100644 index 0000000000..3ae0d2f83e --- /dev/null +++ b/features/session_stopping.feature @@ -0,0 +1,19 @@ +Feature: Stopping and resuming sessions + +Scenario: When a session is stopped the error has no session information + When I run "StoppedSessionScenario" + Then I should receive 3 requests + And the request 0 is valid for the session tracking API + And the request 1 is valid for the error reporting API + And the request 2 is valid for the error reporting API + And the payload field "events.0.session" is not null for request 1 + And the payload field "events.0.session" is null for request 2 + +Scenario: When a session is resumed the error uses the previous session information + When I run "ResumedSessionScenario" + Then I should receive 3 requests + And the request 0 is valid for the session tracking API + And the request 1 is valid for the error reporting API + And the request 2 is valid for the error reporting API + And the payload field "events.0.session.events.handled" equals 1 for request 1 + And the payload field "events.0.session.events.handled" equals 2 for request 2 From 0d07dfecb8627f36b68d10a26075f7d34efebbae Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Thu, 14 Feb 2019 10:55:42 +0000 Subject: [PATCH 03/18] test: add mazerunner scenarios to verify that fatal errors contain session information in their payl --- .../mazerunner/src/main/cpp/bugsnags.cpp | 20 +++++++++++ .../scenarios/CXXStartSessionScenario.java | 34 ++++++++++++++++++ .../scenarios/CXXStopSessionScenario.java | 35 +++++++++++++++++++ features/native_session_tracking.feature | 19 ++++++++++ 4 files changed, 108 insertions(+) create mode 100644 features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStartSessionScenario.java create mode 100644 features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStopSessionScenario.java create mode 100644 features/native_session_tracking.feature diff --git a/features/fixtures/mazerunner/src/main/cpp/bugsnags.cpp b/features/fixtures/mazerunner/src/main/cpp/bugsnags.cpp index 9e5e7e4d8e..47c7df9081 100644 --- a/features/fixtures/mazerunner/src/main/cpp/bugsnags.cpp +++ b/features/fixtures/mazerunner/src/main/cpp/bugsnags.cpp @@ -17,6 +17,26 @@ Java_com_bugsnag_android_mazerunner_scenarios_CXXAutoContextScenario_activate(JN (char *)"This is a new world", BSG_SEVERITY_INFO); } +JNIEXPORT int JNICALL +Java_com_bugsnag_android_mazerunner_scenarios_CXXStartSessionScenario_crash(JNIEnv *env, + jobject instance, + jint value) { + int x = 22; + if (x > 0) + __builtin_trap(); + return 338; +} + +JNIEXPORT int JNICALL +Java_com_bugsnag_android_mazerunner_scenarios_CXXStopSessionScenario_crash(JNIEnv *env, + jobject instance, + jint value) { + int x = 22552; + if (x > 0) + __builtin_trap(); + return 555; +} + JNIEXPORT int JNICALL Java_com_bugsnag_android_mazerunner_scenarios_CXXUpdateContextCrashScenario_crash(JNIEnv *env, jobject instance, diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStartSessionScenario.java b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStartSessionScenario.java new file mode 100644 index 0000000000..f02e265a53 --- /dev/null +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStartSessionScenario.java @@ -0,0 +1,34 @@ +package com.bugsnag.android.mazerunner.scenarios; + +import android.content.Context; + +import com.bugsnag.android.Bugsnag; +import com.bugsnag.android.Configuration; + +import android.support.annotation.NonNull; + +public class CXXStartSessionScenario extends Scenario { + static { + System.loadLibrary("bugsnag-ndk"); + System.loadLibrary("monochrome"); + System.loadLibrary("entrypoint"); + } + + public native int crash(int counter); + + public CXXStartSessionScenario(@NonNull Configuration config, @NonNull Context context) { + super(config, context); + config.setAutoCaptureSessions(false); + } + + @Override + public void run() { + super.run(); + String metadata = getEventMetaData(); + + if (metadata == null || !metadata.equals("non-crashy")) { + Bugsnag.getClient().startSession(); + crash(0); + } + } +} diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStopSessionScenario.java b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStopSessionScenario.java new file mode 100644 index 0000000000..6563c5147e --- /dev/null +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStopSessionScenario.java @@ -0,0 +1,35 @@ +package com.bugsnag.android.mazerunner.scenarios; + +import android.content.Context; + +import com.bugsnag.android.Bugsnag; +import com.bugsnag.android.Configuration; + +import android.support.annotation.NonNull; + +public class CXXStopSessionScenario extends Scenario { + static { + System.loadLibrary("bugsnag-ndk"); + System.loadLibrary("monochrome"); + System.loadLibrary("entrypoint"); + } + + public native int crash(int counter); + + public CXXStopSessionScenario(@NonNull Configuration config, @NonNull Context context) { + super(config, context); + config.setAutoCaptureSessions(false); + } + + @Override + public void run() { + super.run(); + String metadata = getEventMetaData(); + + if (metadata == null || !metadata.equals("non-crashy")) { + Bugsnag.getClient().startSession(); + Bugsnag.getClient().stopSession(); + crash(0); + } + } +} diff --git a/features/native_session_tracking.feature b/features/native_session_tracking.feature new file mode 100644 index 0000000000..11f75ec9d4 --- /dev/null +++ b/features/native_session_tracking.feature @@ -0,0 +1,19 @@ +Feature: NDK Session Tracking + +Scenario: Started session is in payload of unhandled NDK error + When I run "CXXStartSessionScenario" + And I configure the app to run in the "non-crashy" state + And I relaunch the app + Then I should receive 2 requests + And the request 0 is a valid for the session tracking API + And the request 1 is a valid for the error reporting API + And the payload field "events.0.session.events.unhandled" equals 1 for request 1 + +Scenario: Stopped session is not in payload of unhandled NDK error + When I run "CXXStopSessionScenario" + And I configure the app to run in the "non-crashy" state + And I relaunch the app + Then I should receive 2 requests + And the request 0 is a valid for the session tracking API + And the request 1 is a valid for the error reporting API + And the payload field "events.0.session" is null for request 1 From 47898c67e58a69b20ffdda8e1b46a4fbb48307e8 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Thu, 14 Feb 2019 16:48:03 +0000 Subject: [PATCH 04/18] feat: implement stopping sessions for NDK Allow sessions to be stopped on the NDK. when a session is stopped from the java layer, the session information will no longer be serialised in the error payload of fatal NDK errors. --- .../com/bugsnag/android/ndk/NativeBridge.java | 9 ++++++ ndk/src/main/jni/bugsnag_ndk.c | 17 +++++++++- ndk/src/main/jni/metadata.c | 1 + ndk/src/main/jni/report.c | 1 + ndk/src/main/jni/report.h | 1 + ndk/src/main/jni/utils/serializer.c | 17 +++++----- .../com/bugsnag/android/NativeInterface.java | 6 ++++ .../com/bugsnag/android/SessionTracker.java | 31 +++++++++++++------ 8 files changed, 65 insertions(+), 18 deletions(-) diff --git a/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java b/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java index a073fea2d4..1196b6d6d5 100644 --- a/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java +++ b/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java @@ -56,6 +56,8 @@ public static native void addBreadcrumb(String name, String type, String timesta public static native void startedSession(String sessionID, String key); + public static native void stoppedSession(); + public static native void updateAppVersion(String appVersion); public static native void updateBuildUUID(String uuid); @@ -131,6 +133,9 @@ public void update(Observable observable, Object rawMessage) { case START_SESSION: handleStartSession(arg); break; + case STOP_SESSION: + handleStopSession(); + break; case UPDATE_APP_VERSION: handleAppVersionChange(arg); break; @@ -326,6 +331,10 @@ private void handleStartSession(Object arg) { warn("START_SESSION object is invalid: " + arg); } + private void handleStopSession() { + stoppedSession(); + } + private void handleReleaseStageChange(Object arg) { if (arg instanceof String) { updateReleaseStage((String)arg); diff --git a/ndk/src/main/jni/bugsnag_ndk.c b/ndk/src/main/jni/bugsnag_ndk.c index bf7d28145e..b66ec73f22 100644 --- a/ndk/src/main/jni/bugsnag_ndk.c +++ b/ndk/src/main/jni/bugsnag_ndk.c @@ -121,7 +121,11 @@ Java_com_bugsnag_android_ndk_NativeBridge_addHandledEvent(JNIEnv *env, if (bsg_global_env == NULL) return; bsg_request_env_write_lock(); - bsg_global_env->next_report.handled_events++; + bugsnag_report *report = &bsg_global_env->next_report; + + if (report->stoppedSession) { + report->handled_events++; + } bsg_release_env_write_lock(); } @@ -139,6 +143,17 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_startedSession( (*env)->ReleaseStringUTFChars(env, start_date_, started_at); } +JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_stoppedSession( + JNIEnv *env, jobject _this) { + if (bsg_global_env == NULL) { + return; + } + bsg_request_env_write_lock(); + bugsnag_report *report = &bsg_global_env->next_report; + report->stoppedSession = true; + bsg_release_env_write_lock(); +} + JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_clearBreadcrumbs(JNIEnv *env, jobject _this) { diff --git a/ndk/src/main/jni/metadata.c b/ndk/src/main/jni/metadata.c index 7f4a1f80ef..d093b22c69 100644 --- a/ndk/src/main/jni/metadata.c +++ b/ndk/src/main/jni/metadata.c @@ -332,6 +332,7 @@ void bsg_populate_report(JNIEnv *env, bugsnag_report *report) { bsg_populate_app_data(env, jni_cache, report); bsg_populate_device_data(env, jni_cache, report); bsg_populate_user_data(env, jni_cache, report); + report->stoppedSession = false; } void bsg_populate_metadata(JNIEnv *env, bugsnag_report *report, jobject metadata) { diff --git a/ndk/src/main/jni/report.c b/ndk/src/main/jni/report.c index 4962e25697..3e909633a2 100644 --- a/ndk/src/main/jni/report.c +++ b/ndk/src/main/jni/report.c @@ -88,6 +88,7 @@ void bugsnag_report_start_session(bugsnag_report *report, char *session_id, bsg_strncpy_safe(report->session_start, started_at, sizeof(report->session_start)); report->handled_events = 0; + report->stoppedSession = false; } void bugsnag_report_set_context(bugsnag_report *report, char *value) { diff --git a/ndk/src/main/jni/report.h b/ndk/src/main/jni/report.h index 879e9f3b0a..56656c5049 100644 --- a/ndk/src/main/jni/report.h +++ b/ndk/src/main/jni/report.h @@ -292,6 +292,7 @@ typedef struct { char session_id[33]; char session_start[33]; int handled_events; + bool stoppedSession; } bugsnag_report; void bugsnag_report_add_metadata_double(bugsnag_report *report, char *section, diff --git a/ndk/src/main/jni/utils/serializer.c b/ndk/src/main/jni/utils/serializer.c index f2a7bb48e7..202f619514 100644 --- a/ndk/src/main/jni/utils/serializer.c +++ b/ndk/src/main/jni/utils/serializer.c @@ -255,13 +255,16 @@ char *bsg_serialize_report_to_json_string(bugsnag_report *report) { json_object_dotset_string(event, "user.email", report->user.email); if (strlen(report->user.id) > 0) json_object_dotset_string(event, "user.id", report->user.id); - if (strlen(report->session_id) > 0) { - json_object_dotset_string(event, "session.startedAt", - report->session_start); - json_object_dotset_string(event, "session.id", report->session_id); - json_object_dotset_number(event, "session.events.handled", - report->handled_events); - json_object_dotset_number(event, "session.events.unhandled", 1); + + if (!report->stoppedSession) { + if (strlen(report->session_id) > 0) { + json_object_dotset_string(event, "session.startedAt", + report->session_start); + json_object_dotset_string(event, "session.id", report->session_id); + json_object_dotset_number(event, "session.events.handled", + report->handled_events); + json_object_dotset_number(event, "session.events.unhandled", 1); + } } json_object_set_string(exception, "errorClass", report->exception.name); diff --git a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java index fd4eed4bf2..3b6026caf3 100644 --- a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java @@ -59,6 +59,12 @@ public enum MessageType { * containing [id, startDateIsoString] */ START_SESSION, + + /** + * A session was stopped. + */ + STOP_SESSION, + /** * Set a new app version. The Message object should be the new app * version diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java index e9dc4275f8..5138c17112 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java @@ -81,8 +81,8 @@ Session startNewSession(@NonNull Date date, @Nullable User user, return session; } - void startSession(boolean autoCaptured) { - startNewSession(new Date(), client.getUser(), autoCaptured); + Session startSession(boolean autoCaptured) { + return startNewSession(new Date(), client.getUser(), autoCaptured); } void stopSession() { @@ -90,18 +90,33 @@ void stopSession() { if (session != null) { session.isStopped.set(true); + setChanged(); + notifyObservers(new NativeInterface.Message( + NativeInterface.MessageType.STOP_SESSION, null)); } } boolean resumeSession() { Session session = currentSession.get(); + boolean resumed; if (session == null) { - startSession(false); - return false; + session = startSession(false); + resumed = false; } else { - return session.isStopped.compareAndSet(true, false); + resumed = session.isStopped.compareAndSet(true, false); } + + notifySessionStartObserver(session); + return resumed; + } + + private void notifySessionStartObserver(Session session) { + setChanged(); + String startedAt = DateUtils.toIso8601(session.getStartedAt()); + notifyObservers(new NativeInterface.Message( + NativeInterface.MessageType.START_SESSION, + Arrays.asList(session.getId(), startedAt))); } /** @@ -142,11 +157,7 @@ public void run() { // This is on the current thread but there isn't much else we can do sessionStore.write(session); } - setChanged(); - String startedAt = DateUtils.toIso8601(session.getStartedAt()); - notifyObservers(new NativeInterface.Message( - NativeInterface.MessageType.START_SESSION, - Arrays.asList(session.getId(), startedAt))); + notifySessionStartObserver(session); } } From 3fd8417fe2df4ce857552a751c666a170f8bb9c8 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Fri, 15 Feb 2019 11:20:11 +0000 Subject: [PATCH 05/18] feat: update START_SESSION ndk message to include handled count the START_SESSION message is emitted when a session is started or resumed, and allows the NDK layer to update its cached value for session information which is included in the error payload. It was previously assumed that the handled count would always be 0 when starting a session - this is no longer the case as sessions can be resumed, therefore the handled count is passed as part of the message. --- .../java/com/bugsnag/android/ndk/NativeBridge.java | 11 +++++++---- ndk/src/main/jni/bugsnag_ndk.c | 7 ++++--- ndk/src/main/jni/report.c | 4 ++-- ndk/src/main/jni/report.h | 2 +- .../com/bugsnag/android/ObserverInterfaceTest.java | 3 ++- .../main/java/com/bugsnag/android/SessionTracker.java | 5 +++-- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java b/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java index 1196b6d6d5..be888e06a1 100644 --- a/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java +++ b/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java @@ -54,7 +54,7 @@ public static native void addBreadcrumb(String name, String type, String timesta public static native void removeMetadata(String tab, String key); - public static native void startedSession(String sessionID, String key); + public static native void startedSession(String sessionID, String key, int handledCount); public static native void stoppedSession(); @@ -318,11 +318,14 @@ private void handleStartSession(Object arg) { if (arg instanceof List) { @SuppressWarnings("unchecked") List metadata = (List)arg; - if (metadata.size() == 2) { + if (metadata.size() == 3) { Object id = metadata.get(0); Object startTime = metadata.get(1); - if (id instanceof String && startTime instanceof String) { - startedSession((String)id, (String)startTime); + Object handledCount = metadata.get(2); + + if (id instanceof String && startTime instanceof String + && handledCount instanceof Integer) { + startedSession((String)id, (String)startTime, (Integer) handledCount); return; } } diff --git a/ndk/src/main/jni/bugsnag_ndk.c b/ndk/src/main/jni/bugsnag_ndk.c index b66ec73f22..3e046dad92 100644 --- a/ndk/src/main/jni/bugsnag_ndk.c +++ b/ndk/src/main/jni/bugsnag_ndk.c @@ -123,21 +123,22 @@ Java_com_bugsnag_android_ndk_NativeBridge_addHandledEvent(JNIEnv *env, bsg_request_env_write_lock(); bugsnag_report *report = &bsg_global_env->next_report; - if (report->stoppedSession) { + if (!report->stoppedSession) { report->handled_events++; } bsg_release_env_write_lock(); } JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_startedSession( - JNIEnv *env, jobject _this, jstring session_id_, jstring start_date_) { + JNIEnv *env, jobject _this, jstring session_id_, jstring start_date_, jint handled_count) { if (bsg_global_env == NULL || session_id_ == NULL) return; char *session_id = (char *)(*env)->GetStringUTFChars(env, session_id_, 0); char *started_at = (char *)(*env)->GetStringUTFChars(env, start_date_, 0); bsg_request_env_write_lock(); bugsnag_report_start_session(&bsg_global_env->next_report, session_id, - started_at); + started_at, handled_count); + bsg_release_env_write_lock(); (*env)->ReleaseStringUTFChars(env, session_id_, session_id); (*env)->ReleaseStringUTFChars(env, start_date_, started_at); diff --git a/ndk/src/main/jni/report.c b/ndk/src/main/jni/report.c index 3e909633a2..41903b9f29 100644 --- a/ndk/src/main/jni/report.c +++ b/ndk/src/main/jni/report.c @@ -83,11 +83,11 @@ void bugsnag_report_remove_metadata_tab(bugsnag_report *report, char *section) { } void bugsnag_report_start_session(bugsnag_report *report, char *session_id, - char *started_at) { + char *started_at, int handled_count) { bsg_strncpy_safe(report->session_id, session_id, sizeof(report->session_id)); bsg_strncpy_safe(report->session_start, started_at, sizeof(report->session_start)); - report->handled_events = 0; + report->handled_events = handled_count; report->stoppedSession = false; } diff --git a/ndk/src/main/jni/report.h b/ndk/src/main/jni/report.h index 56656c5049..4bbee20e06 100644 --- a/ndk/src/main/jni/report.h +++ b/ndk/src/main/jni/report.h @@ -316,7 +316,7 @@ void bugsnag_report_set_user_email(bugsnag_report *report, char *value); void bugsnag_report_set_user_id(bugsnag_report *report, char *value); void bugsnag_report_set_user_name(bugsnag_report *report, char *value); void bugsnag_report_start_session(bugsnag_report *report, char *session_id, - char *started_at); + char *started_at, int handled_count); #ifdef __cplusplus } #endif diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java index b1539f4423..82a9272a4d 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java @@ -152,9 +152,10 @@ public void testStartSessionSendsMessage() throws InterruptedException { client.startSession(); List sessionInfo = (List)findMessageInQueue( NativeInterface.MessageType.START_SESSION, List.class); - assertEquals(2, sessionInfo.size()); + assertEquals(3, sessionInfo.size()); assertTrue(sessionInfo.get(0) instanceof String); assertTrue(sessionInfo.get(1) instanceof String); + assertTrue(sessionInfo.get(2) instanceof Integer); } @Test diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java index 5138c17112..3823e70fd9 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java @@ -116,7 +116,7 @@ private void notifySessionStartObserver(Session session) { String startedAt = DateUtils.toIso8601(session.getStartedAt()); notifyObservers(new NativeInterface.Message( NativeInterface.MessageType.START_SESSION, - Arrays.asList(session.getId(), startedAt))); + Arrays.asList(session.getId(), startedAt, session.getHandledCount()))); } /** @@ -131,6 +131,8 @@ private void trackSessionIfNeeded(final Session session) { if (notifyForRelease && (configuration.getAutoCaptureSessions() || !session.isAutoCaptured()) && session.isTracked().compareAndSet(false, true)) { + notifySessionStartObserver(session); + try { final String endpoint = configuration.getSessionEndpoint(); Async.run(new Runnable() { @@ -157,7 +159,6 @@ public void run() { // This is on the current thread but there isn't much else we can do sessionStore.write(session); } - notifySessionStartObserver(session); } } From 9e86464abab80dc41bf9ea5711881f667ce90d48 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 18 Feb 2019 10:20:51 +0000 Subject: [PATCH 06/18] feat: add stopSession and resumeSession to bugsnag interface --- sdk/src/main/java/com/bugsnag/android/Bugsnag.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sdk/src/main/java/com/bugsnag/android/Bugsnag.java b/sdk/src/main/java/com/bugsnag/android/Bugsnag.java index 97aa905ee1..77190df487 100644 --- a/sdk/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/sdk/src/main/java/com/bugsnag/android/Bugsnag.java @@ -678,6 +678,14 @@ public static void startSession() { getClient().startSession(); } + public static boolean resumeSession() { + return getClient().resumeSession(); + } + + public static void stopSession() { + getClient().stopSession(); + } + /** * Get the current Bugsnag Client instance. */ From 7d5253e51da4095bafb70f9e061e0eec59357c33 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 18 Feb 2019 11:25:16 +0000 Subject: [PATCH 07/18] refactor: avoid adding additional field to bugsnag_report when stopping sessions --- ndk/src/main/jni/bugsnag_ndk.c | 6 ++++-- ndk/src/main/jni/metadata.c | 1 - ndk/src/main/jni/report.c | 5 ++++- ndk/src/main/jni/report.h | 3 ++- ndk/src/main/jni/utils/serializer.c | 16 +++++++--------- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/ndk/src/main/jni/bugsnag_ndk.c b/ndk/src/main/jni/bugsnag_ndk.c index 3e046dad92..ddd414aba4 100644 --- a/ndk/src/main/jni/bugsnag_ndk.c +++ b/ndk/src/main/jni/bugsnag_ndk.c @@ -123,7 +123,7 @@ Java_com_bugsnag_android_ndk_NativeBridge_addHandledEvent(JNIEnv *env, bsg_request_env_write_lock(); bugsnag_report *report = &bsg_global_env->next_report; - if (!report->stoppedSession) { + if (bugsnag_report_has_session(report)) { report->handled_events++; } bsg_release_env_write_lock(); @@ -151,7 +151,9 @@ JNIEXPORT void JNICALL Java_com_bugsnag_android_ndk_NativeBridge_stoppedSession( } bsg_request_env_write_lock(); bugsnag_report *report = &bsg_global_env->next_report; - report->stoppedSession = true; + memset(report->session_id, 0, strlen(report->session_id)); + memset(report->session_start, 0, strlen(report->session_start)); + report->handled_events = 0; bsg_release_env_write_lock(); } diff --git a/ndk/src/main/jni/metadata.c b/ndk/src/main/jni/metadata.c index d093b22c69..7f4a1f80ef 100644 --- a/ndk/src/main/jni/metadata.c +++ b/ndk/src/main/jni/metadata.c @@ -332,7 +332,6 @@ void bsg_populate_report(JNIEnv *env, bugsnag_report *report) { bsg_populate_app_data(env, jni_cache, report); bsg_populate_device_data(env, jni_cache, report); bsg_populate_user_data(env, jni_cache, report); - report->stoppedSession = false; } void bsg_populate_metadata(JNIEnv *env, bugsnag_report *report, jobject metadata) { diff --git a/ndk/src/main/jni/report.c b/ndk/src/main/jni/report.c index 41903b9f29..97e49e37e9 100644 --- a/ndk/src/main/jni/report.c +++ b/ndk/src/main/jni/report.c @@ -88,7 +88,6 @@ void bugsnag_report_start_session(bugsnag_report *report, char *session_id, bsg_strncpy_safe(report->session_start, started_at, sizeof(report->session_start)); report->handled_events = handled_count; - report->stoppedSession = false; } void bugsnag_report_set_context(bugsnag_report *report, char *value) { @@ -159,3 +158,7 @@ void bugsnag_report_clear_breadcrumbs(bugsnag_report *report) { report->crumb_count = 0; report->crumb_first_index = 0; } + +bool bugsnag_report_has_session(bugsnag_report *report) { + return strlen(report->session_id) > 0; +} diff --git a/ndk/src/main/jni/report.h b/ndk/src/main/jni/report.h index 4bbee20e06..1c126e7ef6 100644 --- a/ndk/src/main/jni/report.h +++ b/ndk/src/main/jni/report.h @@ -292,7 +292,6 @@ typedef struct { char session_id[33]; char session_start[33]; int handled_events; - bool stoppedSession; } bugsnag_report; void bugsnag_report_add_metadata_double(bugsnag_report *report, char *section, @@ -317,6 +316,8 @@ void bugsnag_report_set_user_id(bugsnag_report *report, char *value); void bugsnag_report_set_user_name(bugsnag_report *report, char *value); void bugsnag_report_start_session(bugsnag_report *report, char *session_id, char *started_at, int handled_count); +bool bugsnag_report_has_session(bugsnag_report *report); + #ifdef __cplusplus } #endif diff --git a/ndk/src/main/jni/utils/serializer.c b/ndk/src/main/jni/utils/serializer.c index 202f619514..3361b3671c 100644 --- a/ndk/src/main/jni/utils/serializer.c +++ b/ndk/src/main/jni/utils/serializer.c @@ -256,15 +256,13 @@ char *bsg_serialize_report_to_json_string(bugsnag_report *report) { if (strlen(report->user.id) > 0) json_object_dotset_string(event, "user.id", report->user.id); - if (!report->stoppedSession) { - if (strlen(report->session_id) > 0) { - json_object_dotset_string(event, "session.startedAt", - report->session_start); - json_object_dotset_string(event, "session.id", report->session_id); - json_object_dotset_number(event, "session.events.handled", - report->handled_events); - json_object_dotset_number(event, "session.events.unhandled", 1); - } + if (bugsnag_report_has_session(report)) { + json_object_dotset_string(event, "session.startedAt", + report->session_start); + json_object_dotset_string(event, "session.id", report->session_id); + json_object_dotset_number(event, "session.events.handled", + report->handled_events); + json_object_dotset_number(event, "session.events.unhandled", 1); } json_object_set_string(exception, "errorClass", report->exception.name); From 2fd81c8c786e7b3b415dd96806d590d530f442a5 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 18 Feb 2019 13:25:10 +0000 Subject: [PATCH 08/18] refactor: address review feedback by increasing test coverage and inlining method --- .../main/java/com/bugsnag/android/ndk/NativeBridge.java | 2 +- .../java/com/bugsnag/android/ObserverInterfaceTest.java | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java b/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java index be888e06a1..73d467c492 100644 --- a/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java +++ b/ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.java @@ -134,7 +134,7 @@ public void update(Observable observable, Object rawMessage) { handleStartSession(arg); break; case STOP_SESSION: - handleStopSession(); + stoppedSession(); break; case UPDATE_APP_VERSION: handleAppVersionChange(arg); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java index 82a9272a4d..ec4e52d33c 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.support.test.InstrumentationRegistry; @@ -158,6 +159,14 @@ public void testStartSessionSendsMessage() throws InterruptedException { assertTrue(sessionInfo.get(2) instanceof Integer); } + @Test + public void testStopSessionSendsmessage() { + client.startSession(); + client.stopSession(); + Object msg = findMessageInQueue(NativeInterface.MessageType.STOP_SESSION, null); + assertNull(msg); + } + @Test public void testClientSetBuildUUIDSendsMessage() { client.setBuildUUID("234423-a"); From c4f52befb25159b881c6dd43e56b9e787179af01 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 18 Feb 2019 13:52:43 +0000 Subject: [PATCH 09/18] docs: update javadoc for session API to match docs repo --- .../java/com/bugsnag/android/Bugsnag.java | 56 +++++++++++++++++-- .../main/java/com/bugsnag/android/Client.java | 54 ++++++++++++++++-- 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/sdk/src/main/java/com/bugsnag/android/Bugsnag.java b/sdk/src/main/java/com/bugsnag/android/Bugsnag.java index 77190df487..e6baa08a22 100644 --- a/sdk/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/sdk/src/main/java/com/bugsnag/android/Bugsnag.java @@ -668,20 +668,66 @@ public static void setLoggingEnabled(boolean enabled) { } /** - * Manually starts tracking a new session. - * - * Automatic session tracking can be enabled via - * {@link Configuration#setAutoCaptureSessions(boolean)}, which will automatically create a new - * session everytime the app enters the foreground. + * Starts tracking a new session. You should disable automatic session tracking via + * {@link #setAutoCaptureSessions(boolean)} if you call this method. + *

+ * You should call this at the appropriate time in your application when you wish to start a + * session. Any subsequent errors which occur in your application will still be reported to + * Bugsnag but will not count towards your application's + * + * stability score. This will start a new session even if there is already an existing + * session; you should call {@link #resumeSession()} if you only want to start a session + * when one doesn't already exist. + * + * @see #resumeSession() + * @see #stopSession() + * @see Configuration#setAutoCaptureSessions(boolean) */ public static void startSession() { getClient().startSession(); } + /** + * Resumes a session which has previously been stopped, or starts a new session if none exists. + * If a session has already been resumed or started and has not been stopped, calling this + * method will have no effect. You should disable automatic session tracking via + * {@link #setAutoCaptureSessions(boolean)} if you call this method. + *

+ * It's important to note that sessions are stored in memory for the lifetime of the + * application process and are not persisted on disk. Therefore calling this method on app + * startup would start a new session, rather than continuing any previous session. + *

+ * You should call this at the appropriate time in your application when you wish to resume + * a previously started session. Any subsequent errors which occur in your application will + * still be reported to Bugsnag but will not count towards your application's + * + * stability score. + * + * @see #startSession() + * @see #stopSession() + * @see Configuration#setAutoCaptureSessions(boolean) + * + * @return true if a previous session was resumed, false if a new session was started. + */ public static boolean resumeSession() { return getClient().resumeSession(); } + /** + * Stops tracking a session. You should disable automatic session tracking via + * {@link #setAutoCaptureSessions(boolean)} if you call this method. + *

+ * You should call this at the appropriate time in your application when you wish to stop a + * session. Any subsequent errors which occur in your application will still be reported to + * Bugsnag but will not count towards your application's + * + * stability score. This can be advantageous if, for example, you do not wish the + * stability score to include crashes in a background service. + * + * @see #startSession() + * @see #resumeSession() + * @see Configuration#setAutoCaptureSessions(boolean) + */ public static void stopSession() { getClient().stopSession(); } diff --git a/sdk/src/main/java/com/bugsnag/android/Client.java b/sdk/src/main/java/com/bugsnag/android/Client.java index 992f08b367..8456e78791 100644 --- a/sdk/src/main/java/com/bugsnag/android/Client.java +++ b/sdk/src/main/java/com/bugsnag/android/Client.java @@ -282,20 +282,66 @@ public void update(@NonNull Observable observable, @NonNull Object arg) { } /** - * Manually starts tracking a new session. + * Starts tracking a new session. You should disable automatic session tracking via + * {@link #setAutoCaptureSessions(boolean)} if you call this method. + *

+ * You should call this at the appropriate time in your application when you wish to start a + * session. Any subsequent errors which occur in your application will still be reported to + * Bugsnag but will not count towards your application's + * + * stability score. This will start a new session even if there is already an existing + * session; you should call {@link #resumeSession()} if you only want to start a session + * when one doesn't already exist. * - * Automatic session tracking can be enabled via - * {@link Configuration#setAutoCaptureSessions(boolean)}, which will automatically create a new - * session everytime the app enters the foreground. + * @see #resumeSession() + * @see #stopSession() + * @see Configuration#setAutoCaptureSessions(boolean) */ public void startSession() { sessionTracker.startSession(false); } + /** + * Stops tracking a session. You should disable automatic session tracking via + * {@link #setAutoCaptureSessions(boolean)} if you call this method. + *

+ * You should call this at the appropriate time in your application when you wish to stop a + * session. Any subsequent errors which occur in your application will still be reported to + * Bugsnag but will not count towards your application's + * + * stability score. This can be advantageous if, for example, you do not wish the + * stability score to include crashes in a background service. + * + * @see #startSession() + * @see #resumeSession() + * @see Configuration#setAutoCaptureSessions(boolean) + */ public final void stopSession() { sessionTracker.stopSession(); } + /** + * Resumes a session which has previously been stopped, or starts a new session if none exists. + * If a session has already been resumed or started and has not been stopped, calling this + * method will have no effect. You should disable automatic session tracking via + * {@link #setAutoCaptureSessions(boolean)} if you call this method. + *

+ * It's important to note that sessions are stored in memory for the lifetime of the + * application process and are not persisted on disk. Therefore calling this method on app + * startup would start a new session, rather than continuing any previous session. + *

+ * You should call this at the appropriate time in your application when you wish to resume + * a previously started session. Any subsequent errors which occur in your application will + * still be reported to Bugsnag but will not count towards your application's + * + * stability score. + * + * @see #startSession() + * @see #stopSession() + * @see Configuration#setAutoCaptureSessions(boolean) + * + * @return true if a previous session was resumed, false if a new session was started. + */ public final boolean resumeSession() { return sessionTracker.resumeSession(); } From 62954c7834df6c052ba9f27537835a66d748fbb4 Mon Sep 17 00:00:00 2001 From: Alex Moinet Date: Tue, 19 Feb 2019 09:12:45 +0000 Subject: [PATCH 10/18] tests: Update locked version of maze-runner --- Gemfile.lock | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cb9b0d9c4f..833070354e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,12 @@ GIT remote: https://github.com/bugsnag/maze-runner - revision: 24f53756011b1247a166b70fe65ddf1c40b4fc89 + revision: 6f5af2c98c6fa1cfb07d6466a5cf3eaca5dcbc90 specs: bugsnag-maze-runner (1.0.0) cucumber (~> 3.1.0) cucumber-expressions (= 5.0.15) minitest (~> 5.0) + os (~> 1.0.0) rack (~> 2.0.0) rake (~> 12.3.0) test-unit (~> 3.2.0) @@ -34,17 +35,18 @@ GEM cucumber-wire (0.0.1) diff-lcs (1.3) gherkin (5.1.0) - method_source (0.9.0) + method_source (0.9.2) minitest (5.11.3) multi_json (1.13.1) multi_test (0.1.2) + os (1.0.0) power_assert (1.1.3) - pry (0.11.3) + pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) - rack (2.0.5) - rake (12.3.1) - test-unit (3.2.8) + rack (2.0.6) + rake (12.3.2) + test-unit (3.2.9) power_assert PLATFORMS @@ -55,4 +57,4 @@ DEPENDENCIES pry BUNDLED WITH - 1.16.2 + 1.16.1 From f3ec09a047d05ccdaae3c08ad544df627b5a47b8 Mon Sep 17 00:00:00 2001 From: Alex Moinet Date: Tue, 19 Feb 2019 09:13:08 +0000 Subject: [PATCH 11/18] tests: Add session id & time comparison to stoppedSessions --- features/session_stopping.feature | 2 ++ 1 file changed, 2 insertions(+) diff --git a/features/session_stopping.feature b/features/session_stopping.feature index 3ae0d2f83e..68a787d5bc 100644 --- a/features/session_stopping.feature +++ b/features/session_stopping.feature @@ -17,3 +17,5 @@ Scenario: When a session is resumed the error uses the previous session informat And the request 2 is valid for the error reporting API And the payload field "events.0.session.events.handled" equals 1 for request 1 And the payload field "events.0.session.events.handled" equals 2 for request 2 + And the payload field "events.0.session.id" of request 1 equals the payload field "events.0.session.id" of request 2 + And the payload field "events.0.session.startedAt" of request 1 equals the payload field "events.0.session.startedAt" of request 2 From c3be0e46a1611a3b8742c18182804016b360bd95 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Tue, 19 Feb 2019 10:38:00 +0000 Subject: [PATCH 12/18] test: reorganise test order --- features/native_session_tracking.feature | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/features/native_session_tracking.feature b/features/native_session_tracking.feature index 11f75ec9d4..7330404f72 100644 --- a/features/native_session_tracking.feature +++ b/features/native_session_tracking.feature @@ -1,19 +1,19 @@ Feature: NDK Session Tracking -Scenario: Started session is in payload of unhandled NDK error - When I run "CXXStartSessionScenario" +Scenario: Stopped session is not in payload of unhandled NDK error + When I run "CXXStopSessionScenario" And I configure the app to run in the "non-crashy" state And I relaunch the app Then I should receive 2 requests And the request 0 is a valid for the session tracking API And the request 1 is a valid for the error reporting API - And the payload field "events.0.session.events.unhandled" equals 1 for request 1 + And the payload field "events.0.session" is null for request 1 -Scenario: Stopped session is not in payload of unhandled NDK error - When I run "CXXStopSessionScenario" +Scenario: Started session is in payload of unhandled NDK error + When I run "CXXStartSessionScenario" And I configure the app to run in the "non-crashy" state And I relaunch the app Then I should receive 2 requests And the request 0 is a valid for the session tracking API And the request 1 is a valid for the error reporting API - And the payload field "events.0.session" is null for request 1 + And the payload field "events.0.session.events.unhandled" equals 1 for request 1 From c1b89bcf7a00e56aadeb5780756f6af150dcc801 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Tue, 19 Feb 2019 10:42:01 +0000 Subject: [PATCH 13/18] docs: add changelog entry --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b0579610..a0e5b4bbed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 4.X.X (TBD) + +### Enhancements + +* Add stopSession() and resumeSession() to Client +[#429](https://github.com/bugsnag/bugsnag-android/pull/429) + ## 4.11.0 (2019-01-22) ### Enhancements From 9988dc5cead80861ec640be758f66ac5311cc27e Mon Sep 17 00:00:00 2001 From: Alex Moinet Date: Tue, 19 Feb 2019 10:44:30 +0000 Subject: [PATCH 14/18] tests: Add new session scenario to session_stopping feature --- .../scenarios/NewSessionScenario.kt | 37 +++++++++++++++++++ features/session_stopping.feature | 11 ++++++ 2 files changed, 48 insertions(+) create mode 100644 features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/NewSessionScenario.kt diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/NewSessionScenario.kt b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/NewSessionScenario.kt new file mode 100644 index 0000000000..e8969b8d65 --- /dev/null +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/NewSessionScenario.kt @@ -0,0 +1,37 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import android.os.Handler +import android.os.HandlerThread + +/** + * Sends an exception after stopping the session + */ +internal class NewSessionScenario(config: Configuration, + context: Context) : Scenario(config, context) { + init { + config.setAutoCaptureSessions(false) + } + + override fun run() { + super.run() + val client = Bugsnag.getClient() + val thread = HandlerThread("HandlerThread") + thread.start() + + Handler(thread.looper).post { + // send 1st exception which should include session info + client.startSession() + client.notifyBlocking(generateException()) + + // send 2nd exception which should not include session info + client.stopSession() + + // send 3rd exception which should contain new session info + client.startSession() + client.notifyBlocking(generateException()) + } + } +} diff --git a/features/session_stopping.feature b/features/session_stopping.feature index 68a787d5bc..35778329c7 100644 --- a/features/session_stopping.feature +++ b/features/session_stopping.feature @@ -19,3 +19,14 @@ Scenario: When a session is resumed the error uses the previous session informat And the payload field "events.0.session.events.handled" equals 2 for request 2 And the payload field "events.0.session.id" of request 1 equals the payload field "events.0.session.id" of request 2 And the payload field "events.0.session.startedAt" of request 1 equals the payload field "events.0.session.startedAt" of request 2 + +Scenario: When a new session is started the error uses different session information + When I run "NewSessionScenario" + Then I should receive 4 requests + And the request 0 is valid for the session tracking API + And the request 1 is valid for the error reporting API + And the request 2 is valid for the session tracking API + And the request 3 is valid for the error reporting API + And the payload field "events.0.session.events.handled" equals 1 for request 1 + And the payload field "events.0.session.events.handled" equals 1 for request 3 + And the payload field "events.0.session.id" of request 1 does not equal the payload field "events.0.session.id" of request 3 From 8c90a00ab11a953e4328737b7d1f355187327197 Mon Sep 17 00:00:00 2001 From: Alex Moinet Date: Tue, 19 Feb 2019 11:45:21 +0000 Subject: [PATCH 15/18] tests: Fix tests comment --- .../android/mazerunner/scenarios/NewSessionScenario.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/NewSessionScenario.kt b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/NewSessionScenario.kt index e8969b8d65..4f519bd3d2 100644 --- a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/NewSessionScenario.kt +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/NewSessionScenario.kt @@ -26,10 +26,10 @@ internal class NewSessionScenario(config: Configuration, client.startSession() client.notifyBlocking(generateException()) - // send 2nd exception which should not include session info + // stop tracking the existing session client.stopSession() - // send 3rd exception which should contain new session info + // send 2nd exception which should contain new session info client.startSession() client.notifyBlocking(generateException()) } From c1cd1827d44045a65807e02afb891243e90be5ee Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 25 Feb 2019 15:00:48 +0000 Subject: [PATCH 16/18] test: wait for session scenarios to send session before crashing --- .../mazerunner/scenarios/CXXStartSessionScenario.java | 11 ++++++++++- .../mazerunner/scenarios/CXXStopSessionScenario.java | 11 ++++++++++- features/native_session_tracking.feature | 4 ++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStartSessionScenario.java b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStartSessionScenario.java index f02e265a53..b53e885303 100644 --- a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStartSessionScenario.java +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStartSessionScenario.java @@ -1,6 +1,7 @@ package com.bugsnag.android.mazerunner.scenarios; import android.content.Context; +import android.os.Handler; import com.bugsnag.android.Bugsnag; import com.bugsnag.android.Configuration; @@ -14,6 +15,8 @@ public class CXXStartSessionScenario extends Scenario { System.loadLibrary("entrypoint"); } + private Handler handler = new Handler(); + public native int crash(int counter); public CXXStartSessionScenario(@NonNull Configuration config, @NonNull Context context) { @@ -28,7 +31,13 @@ public void run() { if (metadata == null || !metadata.equals("non-crashy")) { Bugsnag.getClient().startSession(); - crash(0); + + handler.postDelayed(new Runnable() { + @Override + public void run() { + crash(0); + } + }, 8000); } } } diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStopSessionScenario.java b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStopSessionScenario.java index 6563c5147e..f1089c74b3 100644 --- a/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStopSessionScenario.java +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/android/mazerunner/scenarios/CXXStopSessionScenario.java @@ -1,6 +1,7 @@ package com.bugsnag.android.mazerunner.scenarios; import android.content.Context; +import android.os.Handler; import com.bugsnag.android.Bugsnag; import com.bugsnag.android.Configuration; @@ -14,6 +15,8 @@ public class CXXStopSessionScenario extends Scenario { System.loadLibrary("entrypoint"); } + private Handler handler = new Handler(); + public native int crash(int counter); public CXXStopSessionScenario(@NonNull Configuration config, @NonNull Context context) { @@ -29,7 +32,13 @@ public void run() { if (metadata == null || !metadata.equals("non-crashy")) { Bugsnag.getClient().startSession(); Bugsnag.getClient().stopSession(); - crash(0); + + handler.postDelayed(new Runnable() { + @Override + public void run() { + crash(0); + } + }, 8000); } } } diff --git a/features/native_session_tracking.feature b/features/native_session_tracking.feature index 7330404f72..a3b0872f3b 100644 --- a/features/native_session_tracking.feature +++ b/features/native_session_tracking.feature @@ -2,6 +2,8 @@ Feature: NDK Session Tracking Scenario: Stopped session is not in payload of unhandled NDK error When I run "CXXStopSessionScenario" + And I wait a bit + And I wait a bit And I configure the app to run in the "non-crashy" state And I relaunch the app Then I should receive 2 requests @@ -11,6 +13,8 @@ Scenario: Stopped session is not in payload of unhandled NDK error Scenario: Started session is in payload of unhandled NDK error When I run "CXXStartSessionScenario" + And I wait a bit + And I wait a bit And I configure the app to run in the "non-crashy" state And I relaunch the app Then I should receive 2 requests From 574c1fa53631b1cdc917ca1a1e928d6a22c61054 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Wed, 27 Feb 2019 10:17:08 +0000 Subject: [PATCH 17/18] chore: bump version --- README.md | 2 +- gradle.properties | 2 +- sdk/src/main/java/com/bugsnag/android/Notifier.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 626e3b7b92..eac2d4ff32 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build status](https://travis-ci.org/bugsnag/bugsnag-android.svg?branch=master)](https://travis-ci.org/bugsnag/bugsnag-android) [![Coverage Status](https://coveralls.io/repos/github/bugsnag/bugsnag-android/badge.svg?branch=master)](https://coveralls.io/github/bugsnag/bugsnag-android?branch=master) -![Method count and size](https://img.shields.io/badge/Methods%20and%20size-86%20classes%20|%20688%20methods%20|%20351%20fields%20|%20128%20KB-e91e63.svg) +![Method count and size](https://img.shields.io/badge/Methods%20and%20size-86%20classes%20|%20698%20methods%20|%20353%20fields%20|%20132%20KB-e91e63.svg) Get comprehensive [Android crash reports](https://www.bugsnag.com/platforms/android/) to quickly debug errors. diff --git a/gradle.properties b/gradle.properties index 876778a209..44c9b7d83b 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.11.0 +VERSION_NAME=4.12.0 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/sdk/src/main/java/com/bugsnag/android/Notifier.java b/sdk/src/main/java/com/bugsnag/android/Notifier.java index 2dddd6b0b5..a2a0d98fb0 100644 --- a/sdk/src/main/java/com/bugsnag/android/Notifier.java +++ b/sdk/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.11.0"; + private static final String NOTIFIER_VERSION = "4.12.0"; private static final String NOTIFIER_URL = "https://bugsnag.com"; @NonNull From 1853c51a0ed93d564484cf6d06b6407e40d719d0 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Wed, 27 Feb 2019 10:18:17 +0000 Subject: [PATCH 18/18] alter date in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3109f86e..5efc07bca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 4.X.X (TBD) +## 4.12.0 (2019-02-27) ### Enhancements