From 1ae2b467296c6f96c4a875a69631d53add242fea Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 10 Aug 2026 11:22:37 +0200 Subject: [PATCH 1/2] feat(core): Add Unhandled session state and pending-unhandled marker Adds Session.State.Unhandled from the session protocol, plus a pending-unhandled marker that survives serialization. A session carrying the marker finalizes as Unhandled instead of Exited on end(), while Crashed and Abnormal keep taking precedence. Co-authored-by: Cursor --- sentry/api/sentry.api | 5 + sentry/src/main/java/io/sentry/Session.java | 97 +++++++++--- .../io/sentry/PreviousSessionFinalizerTest.kt | 41 +++++ sentry/src/test/java/io/sentry/SessionTest.kt | 144 ++++++++++++++++++ .../protocol/SessionSerializationTest.kt | 13 ++ 5 files changed, 283 insertions(+), 17 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/SessionTest.kt diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 43de4164a4f..941463a66f8 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4286,9 +4286,12 @@ public final class io/sentry/Session : io/sentry/JsonSerializable, io/sentry/Jso public fun getTimestamp ()Ljava/util/Date; public fun getUnknown ()Ljava/util/Map; public fun getUserAgent ()Ljava/lang/String; + public fun isPendingUnhandled ()Z public fun isTerminated ()Z + public fun markPendingUnhandled ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setInitAsTrue ()V + public fun setPendingUnhandled (Z)V public fun setUnknown (Ljava/util/Map;)V public fun update (Lio/sentry/Session$State;Ljava/lang/String;Z)Z public fun update (Lio/sentry/Session$State;Ljava/lang/String;ZLjava/lang/String;)Z @@ -4309,6 +4312,7 @@ public final class io/sentry/Session$JsonKeys { public static final field ERRORS Ljava/lang/String; public static final field INIT Ljava/lang/String; public static final field IP_ADDRESS Ljava/lang/String; + public static final field PENDING_UNHANDLED Ljava/lang/String; public static final field RELEASE Ljava/lang/String; public static final field SEQ Ljava/lang/String; public static final field SID Ljava/lang/String; @@ -4324,6 +4328,7 @@ public final class io/sentry/Session$State : java/lang/Enum { public static final field Crashed Lio/sentry/Session$State; public static final field Exited Lio/sentry/Session$State; public static final field Ok Lio/sentry/Session$State; + public static final field Unhandled Lio/sentry/Session$State; public static fun valueOf (Ljava/lang/String;)Lio/sentry/Session$State; public static fun values ()[Lio/sentry/Session$State; } diff --git a/sentry/src/main/java/io/sentry/Session.java b/sentry/src/main/java/io/sentry/Session.java index 2fdfffb35d9..a1334ea45f5 100644 --- a/sentry/src/main/java/io/sentry/Session.java +++ b/sentry/src/main/java/io/sentry/Session.java @@ -21,7 +21,8 @@ public enum State { Ok, Exited, Crashed, - Abnormal + Abnormal, + Unhandled } /** started timestamp */ @@ -66,6 +67,14 @@ public enum State { /** the Abnormal mechanism, e.g. what was the reason for session to become abnormal (ANR) */ private @Nullable String abnormalMechanism; + /** + * Whether the session experienced an unhandled (but non-terminal) exception. Kept locally and + * persisted with the session, but never sent as a status while the session is alive. On end() the + * session is finalized as {@link State#Unhandled} instead of {@link State#Exited} unless a crash + * escalated it to {@link State#Crashed}. + */ + private boolean pendingUnhandled; + /** The session lock, ops should be atomic */ private final @NotNull AutoClosableReentrantLock sessionLock = new AutoClosableReentrantLock(); @@ -188,6 +197,41 @@ public int errorCount() { return abnormalMechanism; } + /** + * Whether the session has a pending unhandled (non-terminal) exception that hasn't been finalized + * yet. + */ + @ApiStatus.Internal + public boolean isPendingUnhandled() { + return pendingUnhandled; + } + + /** + * Marks the session as having experienced an unhandled (non-terminal) exception without ending + * it. On {@link #end()} the session will be finalized as {@link State#Unhandled} unless a crash + * escalated it to {@link State#Crashed} first. + */ + @ApiStatus.Internal + public void setPendingUnhandled(final boolean pendingUnhandled) { + this.pendingUnhandled = pendingUnhandled; + } + + /** Marks an active session as having experienced an unhandled non-terminal exception. */ + @ApiStatus.Internal + public boolean markPendingUnhandled() { + try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { + if (status != State.Ok) { + return false; + } + pendingUnhandled = true; + errorCount.incrementAndGet(); + init = null; + timestamp = DateUtils.getCurrentDateTime(); + sequence = getSequenceTimestamp(timestamp); + return true; + } + } + @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) public @Nullable Date getTimestamp() { return timestamp; @@ -209,7 +253,9 @@ public void end(final @Nullable Date timestamp) { // at this state it might be Crashed already, so we don't check for it. if (status == State.Ok) { - status = State.Exited; + // a session that experienced an unhandled (but non-terminal) exception is finalized as + // Unhandled rather than Exited. + status = pendingUnhandled ? State.Unhandled : State.Exited; } if (timestamp != null) { @@ -262,6 +308,10 @@ public boolean update( boolean sessionHasBeenUpdated = false; if (status != null) { this.status = status; + // a real crash takes precedence over a pending unhandled (non-terminal) exception. + if (status == State.Crashed) { + pendingUnhandled = false; + } sessionHasBeenUpdated = true; } @@ -318,21 +368,24 @@ private long getSequenceTimestamp(final @NotNull Date timestamp) { */ @SuppressWarnings("MissingOverride") public @NotNull Session clone() { - return new Session( - status, - started, - timestamp, - errorCount.get(), - distinctId, - sessionId, - init, - sequence, - duration, - ipAddress, - userAgent, - environment, - release, - abnormalMechanism); + final Session session = + new Session( + status, + started, + timestamp, + errorCount.get(), + distinctId, + sessionId, + init, + sequence, + duration, + ipAddress, + userAgent, + environment, + release, + abnormalMechanism); + session.setPendingUnhandled(pendingUnhandled); + return session; } // JsonSerializable @@ -354,6 +407,7 @@ public static final class JsonKeys { public static final String IP_ADDRESS = "ip_address"; public static final String USER_AGENT = "user_agent"; public static final String ABNORMAL_MECHANISM = "abnormal_mechanism"; + public static final String PENDING_UNHANDLED = "pending_unhandled"; } @Override @@ -384,6 +438,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (abnormalMechanism != null) { writer.name(JsonKeys.ABNORMAL_MECHANISM).value(logger, abnormalMechanism); } + if (pendingUnhandled) { + writer.name(JsonKeys.PENDING_UNHANDLED).value(pendingUnhandled); + } writer.name(JsonKeys.ATTRS); writer.beginObject(); writer.name(JsonKeys.RELEASE).value(logger, release); @@ -440,6 +497,7 @@ public static final class Deserializer implements JsonDeserializer { String environment = null; String release = null; // @NotNull String abnormalMechanism = null; + boolean pendingUnhandled = false; Map unknown = null; while (reader.peek() == JsonToken.NAME) { @@ -483,6 +541,10 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.ABNORMAL_MECHANISM: abnormalMechanism = reader.nextStringOrNull(); break; + case JsonKeys.PENDING_UNHANDLED: + final Boolean pendingUnhandledValue = reader.nextBooleanOrNull(); + pendingUnhandled = pendingUnhandledValue != null && pendingUnhandledValue; + break; case JsonKeys.ATTRS: reader.beginObject(); while (reader.peek() == JsonToken.NAME) { @@ -542,6 +604,7 @@ public static final class Deserializer implements JsonDeserializer { environment, release, abnormalMechanism); + session.setPendingUnhandled(pendingUnhandled); session.setUnknown(unknown); reader.endObject(); return session; diff --git a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt index 4b433ffb3e1..36b934b4906 100644 --- a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt +++ b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt @@ -200,6 +200,47 @@ class PreviousSessionFinalizerTest { ) } + @Test + fun `if previous session has pending unhandled and no crash marker, finalizes as unhandled`() { + val finalizer = + fixture.getSut( + tmpDir, + session = + Session(null, null, null, "io.sentry.sample@1.0").apply { setPendingUnhandled(true) }, + ) + finalizer.run() + + verify(fixture.scopes) + .captureEnvelope( + argThat { + val session = fixture.sessionFromEnvelope(this) + session.release == "io.sentry.sample@1.0" && + session.status == Session.State.Unhandled && + session.isPendingUnhandled + } + ) + } + + @Test + fun `if previous session has pending unhandled but a native crash marker exists, finalizes as crashed`() { + val finalizer = + fixture.getSut( + tmpDir, + session = + Session(null, null, null, "io.sentry.sample@1.0").apply { setPendingUnhandled(true) }, + nativeCrashTimestamp = DateUtils.getDateTime("2023-10-01T00:00:00.000Z"), + ) + finalizer.run() + + verify(fixture.scopes) + .captureEnvelope( + argThat { + val session = fixture.sessionFromEnvelope(this) + session.release == "io.sentry.sample@1.0" && session.status == Crashed + } + ) + } + @Test fun `if previous session file exists, deletes previous session file`() { val finalizer = fixture.getSut(tmpDir, sessionFileExists = true) diff --git a/sentry/src/test/java/io/sentry/SessionTest.kt b/sentry/src/test/java/io/sentry/SessionTest.kt new file mode 100644 index 00000000000..77a16b8625a --- /dev/null +++ b/sentry/src/test/java/io/sentry/SessionTest.kt @@ -0,0 +1,144 @@ +package io.sentry + +import com.google.common.truth.Truth.assertThat +import java.io.StringReader +import java.io.StringWriter +import kotlin.test.Test +import org.mockito.kotlin.mock + +class SessionTest { + + private fun okSession(): Session = Session(null, null, "environment", "release") + + @Test + fun `markPendingUnhandled atomically updates an Ok session`() { + val session = okSession() + val initialTimestamp = session.timestamp + + val updated = session.markPendingUnhandled() + + assertThat(updated).isTrue() + assertThat(session.status).isEqualTo(Session.State.Ok) + assertThat(session.isPendingUnhandled).isTrue() + assertThat(session.errorCount()).isEqualTo(1) + assertThat(session.init).isNull() + assertThat(session.timestamp).isNotNull() + assertThat(session.timestamp!!.time).isAtLeast(initialTimestamp!!.time) + assertThat(session.sequence).isEqualTo(session.timestamp!!.time) + } + + @Test + fun `markPendingUnhandled does not change terminal sessions`() { + for (state in Session.State.entries.filter { it != Session.State.Ok }) { + val session = okSession() + session.update(state, null, false) + val before = session.clone() + + val updated = session.markPendingUnhandled() + + assertThat(updated).isFalse() + assertThat(session.status).isEqualTo(before.status) + assertThat(session.isPendingUnhandled).isEqualTo(before.isPendingUnhandled) + assertThat(session.errorCount()).isEqualTo(before.errorCount()) + assertThat(session.init).isEqualTo(before.init) + assertThat(session.timestamp).isEqualTo(before.timestamp) + assertThat(session.sequence).isEqualTo(before.sequence) + } + } + + @Test + fun `end without pending unhandled finalizes as Exited`() { + val session = okSession() + + session.end() + + assertThat(session.status).isEqualTo(Session.State.Exited) + } + + @Test + fun `end with pending unhandled finalizes as Unhandled`() { + val session = okSession() + assertThat(session.isPendingUnhandled).isFalse() + + session.setPendingUnhandled(true) + session.end() + + assertThat(session.status).isEqualTo(Session.State.Unhandled) + assertThat(session.isPendingUnhandled).isTrue() + } + + @Test + fun `end with pending unhandled keeps Abnormal as Abnormal`() { + val session = okSession() + session.setPendingUnhandled(true) + session.update(Session.State.Abnormal, null, false, "anr") + + session.end() + + assertThat(session.status).isEqualTo(Session.State.Abnormal) + assertThat(session.isPendingUnhandled).isTrue() + } + + @Test + fun `end with pending unhandled keeps Crashed as Crashed`() { + val session = okSession() + session.setPendingUnhandled(true) + session.update(Session.State.Crashed, null, false) + + session.end() + + assertThat(session.status).isEqualTo(Session.State.Crashed) + assertThat(session.isPendingUnhandled).isFalse() + } + + @Test + fun `updating to Crashed clears pending unhandled and end stays Crashed`() { + val session = okSession() + session.setPendingUnhandled(true) + + session.update(Session.State.Crashed, null, true) + session.end() + + assertThat(session.status).isEqualTo(Session.State.Crashed) + assertThat(session.isPendingUnhandled).isFalse() + } + + @Test + fun `clone preserves pending unhandled`() { + val session = okSession() + session.setPendingUnhandled(true) + + val clone = session.clone() + + assertThat(clone.isPendingUnhandled).isTrue() + } + + @Test + fun `serialization round-trips pending unhandled and Unhandled status`() { + val logger = mock() + val session = okSession() + session.setPendingUnhandled(true) + session.end() + assertThat(session.status).isEqualTo(Session.State.Unhandled) + + val writer = StringWriter() + session.serialize(JsonObjectWriter(writer, 100), logger) + + val deserialized = + Session.Deserializer().deserialize(JsonObjectReader(StringReader(writer.toString())), logger) + + assertThat(deserialized.status).isEqualTo(Session.State.Unhandled) + assertThat(deserialized.isPendingUnhandled).isTrue() + } + + @Test + fun `pending unhandled defaults to false and is not serialized when unset`() { + val logger = mock() + val session = okSession() + + val writer = StringWriter() + session.serialize(JsonObjectWriter(writer, 100), logger) + + assertThat(writer.toString()).doesNotContain("pending_unhandled") + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt index 644f57a0232..1280680e7f8 100644 --- a/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt @@ -53,6 +53,19 @@ class SessionSerializationTest { assertEquals(expectedJson, actualJson) } + @Test + fun `serialize and deserialize round-trips Unhandled status and pending unhandled flag`() { + val session = Session(null, null, "environment", "release") + session.setPendingUnhandled(true) + session.end() + assertEquals(Session.State.Unhandled, session.status) + + val deserialized = deserialize(serialize(session)) + + assertEquals(Session.State.Unhandled, deserialized.status) + assertEquals(true, deserialized.isPendingUnhandled) + } + // Helper private fun sanitizedFile(path: String): String = From e62793f8693905c33e9fed08e8858136fb3acd7e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 10 Aug 2026 11:37:48 +0200 Subject: [PATCH 2/2] ref: rename pendingUnhandled to nonTerminatingUnhandledError "Unhandled" alone is ambiguous: a native crash is also an unhandled error, it just terminates the process and so ends the session as crashed rather than unhandled. Name the flag after the property that actually distinguishes the two and match the vocabulary of captureEnvelopeNonTerminating. Also clarify that the setter only restores the flag when rebuilding a session and must not be used to mutate a live one. Co-authored-by: Cursor --- sentry/api/sentry.api | 8 +-- sentry/src/main/java/io/sentry/Session.java | 71 +++++++++++-------- .../io/sentry/PreviousSessionFinalizerTest.kt | 14 ++-- sentry/src/test/java/io/sentry/SessionTest.kt | 57 +++++++-------- .../protocol/SessionSerializationTest.kt | 6 +- 5 files changed, 87 insertions(+), 69 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 941463a66f8..8a810918c03 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4286,12 +4286,12 @@ public final class io/sentry/Session : io/sentry/JsonSerializable, io/sentry/Jso public fun getTimestamp ()Ljava/util/Date; public fun getUnknown ()Ljava/util/Map; public fun getUserAgent ()Ljava/lang/String; - public fun isPendingUnhandled ()Z + public fun hasNonTerminatingUnhandledError ()Z public fun isTerminated ()Z - public fun markPendingUnhandled ()Z + public fun recordNonTerminatingUnhandledError ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setInitAsTrue ()V - public fun setPendingUnhandled (Z)V + public fun setNonTerminatingUnhandledError (Z)V public fun setUnknown (Ljava/util/Map;)V public fun update (Lio/sentry/Session$State;Ljava/lang/String;Z)Z public fun update (Lio/sentry/Session$State;Ljava/lang/String;ZLjava/lang/String;)Z @@ -4312,7 +4312,7 @@ public final class io/sentry/Session$JsonKeys { public static final field ERRORS Ljava/lang/String; public static final field INIT Ljava/lang/String; public static final field IP_ADDRESS Ljava/lang/String; - public static final field PENDING_UNHANDLED Ljava/lang/String; + public static final field NON_TERMINATING_UNHANDLED_ERROR Ljava/lang/String; public static final field RELEASE Ljava/lang/String; public static final field SEQ Ljava/lang/String; public static final field SID Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/Session.java b/sentry/src/main/java/io/sentry/Session.java index a1334ea45f5..33e9de42894 100644 --- a/sentry/src/main/java/io/sentry/Session.java +++ b/sentry/src/main/java/io/sentry/Session.java @@ -68,12 +68,15 @@ public enum State { private @Nullable String abnormalMechanism; /** - * Whether the session experienced an unhandled (but non-terminal) exception. Kept locally and - * persisted with the session, but never sent as a status while the session is alive. On end() the - * session is finalized as {@link State#Unhandled} instead of {@link State#Exited} unless a crash - * escalated it to {@link State#Crashed}. + * Whether the session experienced an unhandled error that did not terminate the process, + * e.g. an unhandled Flutter exception. A native crash is also unhandled, but it kills the process + * and therefore ends the session as {@link State#Crashed} instead. + * + *

Kept locally and persisted with the session, but never sent as a status while the session is + * alive. On end() the session is finalized as {@link State#Unhandled} instead of {@link + * State#Exited}, unless a crash escalated it to {@link State#Crashed}. */ - private boolean pendingUnhandled; + private boolean nonTerminatingUnhandledError; /** The session lock, ops should be atomic */ private final @NotNull AutoClosableReentrantLock sessionLock = new AutoClosableReentrantLock(); @@ -198,32 +201,41 @@ public int errorCount() { } /** - * Whether the session has a pending unhandled (non-terminal) exception that hasn't been finalized - * yet. + * Whether the session experienced an unhandled error that did not terminate the process, and so + * finalizes as {@link State#Unhandled} rather than {@link State#Exited}. */ @ApiStatus.Internal - public boolean isPendingUnhandled() { - return pendingUnhandled; + public boolean hasNonTerminatingUnhandledError() { + return nonTerminatingUnhandledError; } /** - * Marks the session as having experienced an unhandled (non-terminal) exception without ending - * it. On {@link #end()} the session will be finalized as {@link State#Unhandled} unless a crash - * escalated it to {@link State#Crashed} first. + * Restores the flag when rebuilding a session, i.e. from {@link #clone()} or the deserializer. + * + *

Not for use on a live session: unlike {@link #recordNonTerminatingUnhandledError()} this + * neither counts the error nor advances the session's sequence, so a session mutated through this + * setter would be sent as an out-of-date update. */ @ApiStatus.Internal - public void setPendingUnhandled(final boolean pendingUnhandled) { - this.pendingUnhandled = pendingUnhandled; + public void setNonTerminatingUnhandledError(final boolean nonTerminatingUnhandledError) { + this.nonTerminatingUnhandledError = nonTerminatingUnhandledError; } - /** Marks an active session as having experienced an unhandled non-terminal exception. */ + /** + * Records that an active session experienced an unhandled error which did not terminate the + * process, counting the error and advancing the session's sequence without ending it. On {@link + * #end()} the session is finalized as {@link State#Unhandled} unless a crash escalated it to + * {@link State#Crashed} first. + * + * @return whether the session was updated, i.e. false if it had already reached a terminal state + */ @ApiStatus.Internal - public boolean markPendingUnhandled() { + public boolean recordNonTerminatingUnhandledError() { try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { if (status != State.Ok) { return false; } - pendingUnhandled = true; + nonTerminatingUnhandledError = true; errorCount.incrementAndGet(); init = null; timestamp = DateUtils.getCurrentDateTime(); @@ -255,7 +267,7 @@ public void end(final @Nullable Date timestamp) { if (status == State.Ok) { // a session that experienced an unhandled (but non-terminal) exception is finalized as // Unhandled rather than Exited. - status = pendingUnhandled ? State.Unhandled : State.Exited; + status = nonTerminatingUnhandledError ? State.Unhandled : State.Exited; } if (timestamp != null) { @@ -308,9 +320,9 @@ public boolean update( boolean sessionHasBeenUpdated = false; if (status != null) { this.status = status; - // a real crash takes precedence over a pending unhandled (non-terminal) exception. + // a crash terminates the process, so it takes precedence over a non-terminating one. if (status == State.Crashed) { - pendingUnhandled = false; + nonTerminatingUnhandledError = false; } sessionHasBeenUpdated = true; } @@ -384,7 +396,7 @@ private long getSequenceTimestamp(final @NotNull Date timestamp) { environment, release, abnormalMechanism); - session.setPendingUnhandled(pendingUnhandled); + session.setNonTerminatingUnhandledError(nonTerminatingUnhandledError); return session; } @@ -407,7 +419,7 @@ public static final class JsonKeys { public static final String IP_ADDRESS = "ip_address"; public static final String USER_AGENT = "user_agent"; public static final String ABNORMAL_MECHANISM = "abnormal_mechanism"; - public static final String PENDING_UNHANDLED = "pending_unhandled"; + public static final String NON_TERMINATING_UNHANDLED_ERROR = "non_terminating_unhandled_error"; } @Override @@ -438,8 +450,8 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (abnormalMechanism != null) { writer.name(JsonKeys.ABNORMAL_MECHANISM).value(logger, abnormalMechanism); } - if (pendingUnhandled) { - writer.name(JsonKeys.PENDING_UNHANDLED).value(pendingUnhandled); + if (nonTerminatingUnhandledError) { + writer.name(JsonKeys.NON_TERMINATING_UNHANDLED_ERROR).value(nonTerminatingUnhandledError); } writer.name(JsonKeys.ATTRS); writer.beginObject(); @@ -497,7 +509,7 @@ public static final class Deserializer implements JsonDeserializer { String environment = null; String release = null; // @NotNull String abnormalMechanism = null; - boolean pendingUnhandled = false; + boolean nonTerminatingUnhandledError = false; Map unknown = null; while (reader.peek() == JsonToken.NAME) { @@ -541,9 +553,10 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.ABNORMAL_MECHANISM: abnormalMechanism = reader.nextStringOrNull(); break; - case JsonKeys.PENDING_UNHANDLED: - final Boolean pendingUnhandledValue = reader.nextBooleanOrNull(); - pendingUnhandled = pendingUnhandledValue != null && pendingUnhandledValue; + case JsonKeys.NON_TERMINATING_UNHANDLED_ERROR: + final Boolean nonTerminatingUnhandledErrorValue = reader.nextBooleanOrNull(); + nonTerminatingUnhandledError = + nonTerminatingUnhandledErrorValue != null && nonTerminatingUnhandledErrorValue; break; case JsonKeys.ATTRS: reader.beginObject(); @@ -604,7 +617,7 @@ public static final class Deserializer implements JsonDeserializer { environment, release, abnormalMechanism); - session.setPendingUnhandled(pendingUnhandled); + session.setNonTerminatingUnhandledError(nonTerminatingUnhandledError); session.setUnknown(unknown); reader.endObject(); return session; diff --git a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt index 36b934b4906..af2e67b19d9 100644 --- a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt +++ b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt @@ -201,12 +201,14 @@ class PreviousSessionFinalizerTest { } @Test - fun `if previous session has pending unhandled and no crash marker, finalizes as unhandled`() { + fun `if previous session has a non-terminating unhandled error and no crash marker, finalizes as unhandled`() { val finalizer = fixture.getSut( tmpDir, session = - Session(null, null, null, "io.sentry.sample@1.0").apply { setPendingUnhandled(true) }, + Session(null, null, null, "io.sentry.sample@1.0").apply { + setNonTerminatingUnhandledError(true) + }, ) finalizer.run() @@ -216,18 +218,20 @@ class PreviousSessionFinalizerTest { val session = fixture.sessionFromEnvelope(this) session.release == "io.sentry.sample@1.0" && session.status == Session.State.Unhandled && - session.isPendingUnhandled + session.hasNonTerminatingUnhandledError() } ) } @Test - fun `if previous session has pending unhandled but a native crash marker exists, finalizes as crashed`() { + fun `if previous session has a non-terminating unhandled error but a native crash marker exists, finalizes as crashed`() { val finalizer = fixture.getSut( tmpDir, session = - Session(null, null, null, "io.sentry.sample@1.0").apply { setPendingUnhandled(true) }, + Session(null, null, null, "io.sentry.sample@1.0").apply { + setNonTerminatingUnhandledError(true) + }, nativeCrashTimestamp = DateUtils.getDateTime("2023-10-01T00:00:00.000Z"), ) finalizer.run() diff --git a/sentry/src/test/java/io/sentry/SessionTest.kt b/sentry/src/test/java/io/sentry/SessionTest.kt index 77a16b8625a..6326179a9be 100644 --- a/sentry/src/test/java/io/sentry/SessionTest.kt +++ b/sentry/src/test/java/io/sentry/SessionTest.kt @@ -11,15 +11,15 @@ class SessionTest { private fun okSession(): Session = Session(null, null, "environment", "release") @Test - fun `markPendingUnhandled atomically updates an Ok session`() { + fun `recordNonTerminatingUnhandledError atomically updates an Ok session`() { val session = okSession() val initialTimestamp = session.timestamp - val updated = session.markPendingUnhandled() + val updated = session.recordNonTerminatingUnhandledError() assertThat(updated).isTrue() assertThat(session.status).isEqualTo(Session.State.Ok) - assertThat(session.isPendingUnhandled).isTrue() + assertThat(session.hasNonTerminatingUnhandledError()).isTrue() assertThat(session.errorCount()).isEqualTo(1) assertThat(session.init).isNull() assertThat(session.timestamp).isNotNull() @@ -28,17 +28,18 @@ class SessionTest { } @Test - fun `markPendingUnhandled does not change terminal sessions`() { + fun `recordNonTerminatingUnhandledError does not change terminal sessions`() { for (state in Session.State.entries.filter { it != Session.State.Ok }) { val session = okSession() session.update(state, null, false) val before = session.clone() - val updated = session.markPendingUnhandled() + val updated = session.recordNonTerminatingUnhandledError() assertThat(updated).isFalse() assertThat(session.status).isEqualTo(before.status) - assertThat(session.isPendingUnhandled).isEqualTo(before.isPendingUnhandled) + assertThat(session.hasNonTerminatingUnhandledError()) + .isEqualTo(before.hasNonTerminatingUnhandledError()) assertThat(session.errorCount()).isEqualTo(before.errorCount()) assertThat(session.init).isEqualTo(before.init) assertThat(session.timestamp).isEqualTo(before.timestamp) @@ -47,7 +48,7 @@ class SessionTest { } @Test - fun `end without pending unhandled finalizes as Exited`() { + fun `end without a non-terminating unhandled error finalizes as Exited`() { val session = okSession() session.end() @@ -56,68 +57,68 @@ class SessionTest { } @Test - fun `end with pending unhandled finalizes as Unhandled`() { + fun `end with a non-terminating unhandled error finalizes as Unhandled`() { val session = okSession() - assertThat(session.isPendingUnhandled).isFalse() + assertThat(session.hasNonTerminatingUnhandledError()).isFalse() - session.setPendingUnhandled(true) + session.setNonTerminatingUnhandledError(true) session.end() assertThat(session.status).isEqualTo(Session.State.Unhandled) - assertThat(session.isPendingUnhandled).isTrue() + assertThat(session.hasNonTerminatingUnhandledError()).isTrue() } @Test - fun `end with pending unhandled keeps Abnormal as Abnormal`() { + fun `end with a non-terminating unhandled error keeps Abnormal as Abnormal`() { val session = okSession() - session.setPendingUnhandled(true) + session.setNonTerminatingUnhandledError(true) session.update(Session.State.Abnormal, null, false, "anr") session.end() assertThat(session.status).isEqualTo(Session.State.Abnormal) - assertThat(session.isPendingUnhandled).isTrue() + assertThat(session.hasNonTerminatingUnhandledError()).isTrue() } @Test - fun `end with pending unhandled keeps Crashed as Crashed`() { + fun `end with a non-terminating unhandled error keeps Crashed as Crashed`() { val session = okSession() - session.setPendingUnhandled(true) + session.setNonTerminatingUnhandledError(true) session.update(Session.State.Crashed, null, false) session.end() assertThat(session.status).isEqualTo(Session.State.Crashed) - assertThat(session.isPendingUnhandled).isFalse() + assertThat(session.hasNonTerminatingUnhandledError()).isFalse() } @Test - fun `updating to Crashed clears pending unhandled and end stays Crashed`() { + fun `updating to Crashed clears a non-terminating unhandled error and end stays Crashed`() { val session = okSession() - session.setPendingUnhandled(true) + session.setNonTerminatingUnhandledError(true) session.update(Session.State.Crashed, null, true) session.end() assertThat(session.status).isEqualTo(Session.State.Crashed) - assertThat(session.isPendingUnhandled).isFalse() + assertThat(session.hasNonTerminatingUnhandledError()).isFalse() } @Test - fun `clone preserves pending unhandled`() { + fun `clone preserves a non-terminating unhandled error`() { val session = okSession() - session.setPendingUnhandled(true) + session.setNonTerminatingUnhandledError(true) val clone = session.clone() - assertThat(clone.isPendingUnhandled).isTrue() + assertThat(clone.hasNonTerminatingUnhandledError()).isTrue() } @Test - fun `serialization round-trips pending unhandled and Unhandled status`() { + fun `serialization round-trips a non-terminating unhandled error and Unhandled status`() { val logger = mock() val session = okSession() - session.setPendingUnhandled(true) + session.setNonTerminatingUnhandledError(true) session.end() assertThat(session.status).isEqualTo(Session.State.Unhandled) @@ -128,17 +129,17 @@ class SessionTest { Session.Deserializer().deserialize(JsonObjectReader(StringReader(writer.toString())), logger) assertThat(deserialized.status).isEqualTo(Session.State.Unhandled) - assertThat(deserialized.isPendingUnhandled).isTrue() + assertThat(deserialized.hasNonTerminatingUnhandledError()).isTrue() } @Test - fun `pending unhandled defaults to false and is not serialized when unset`() { + fun `a non-terminating unhandled error defaults to false and is not serialized when unset`() { val logger = mock() val session = okSession() val writer = StringWriter() session.serialize(JsonObjectWriter(writer, 100), logger) - assertThat(writer.toString()).doesNotContain("pending_unhandled") + assertThat(writer.toString()).doesNotContain("non_terminating_unhandled_error") } } diff --git a/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt index 1280680e7f8..ed85351f83e 100644 --- a/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt @@ -54,16 +54,16 @@ class SessionSerializationTest { } @Test - fun `serialize and deserialize round-trips Unhandled status and pending unhandled flag`() { + fun `serialize and deserialize round-trips Unhandled status and non-terminating flag`() { val session = Session(null, null, "environment", "release") - session.setPendingUnhandled(true) + session.setNonTerminatingUnhandledError(true) session.end() assertEquals(Session.State.Unhandled, session.status) val deserialized = deserialize(serialize(session)) assertEquals(Session.State.Unhandled, deserialized.status) - assertEquals(true, deserialized.isPendingUnhandled) + assertEquals(true, deserialized.hasNonTerminatingUnhandledError()) } // Helper