Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 hasNonTerminatingUnhandledError ()Z
public fun isTerminated ()Z
public fun recordNonTerminatingUnhandledError ()Z
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
public fun setInitAsTrue ()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
Expand All @@ -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 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;
Expand All @@ -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;
}
Expand Down
110 changes: 93 additions & 17 deletions sentry/src/main/java/io/sentry/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public enum State {
Ok,
Exited,
Crashed,
Abnormal
Abnormal,
Unhandled
}

/** started timestamp */
Expand Down Expand Up @@ -66,6 +67,17 @@ 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 error that did <em>not</em> 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.
*
* <p>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 nonTerminatingUnhandledError;

/** The session lock, ops should be atomic */
private final @NotNull AutoClosableReentrantLock sessionLock = new AutoClosableReentrantLock();

Expand Down Expand Up @@ -188,6 +200,50 @@ public int errorCount() {
return abnormalMechanism;
}

/**
* 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 hasNonTerminatingUnhandledError() {
return nonTerminatingUnhandledError;
}

/**
* Restores the flag when rebuilding a session, i.e. from {@link #clone()} or the deserializer.
*
* <p>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 setNonTerminatingUnhandledError(final boolean nonTerminatingUnhandledError) {
this.nonTerminatingUnhandledError = nonTerminatingUnhandledError;
}

/**
* 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 recordNonTerminatingUnhandledError() {
try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) {
if (status != State.Ok) {
return false;
}
nonTerminatingUnhandledError = true;
errorCount.incrementAndGet();
init = null;
timestamp = DateUtils.getCurrentDateTime();
sequence = getSequenceTimestamp(timestamp);
return true;
}
}

@SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
public @Nullable Date getTimestamp() {
return timestamp;
Expand All @@ -209,7 +265,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 = nonTerminatingUnhandledError ? State.Unhandled : State.Exited;
}

if (timestamp != null) {
Expand Down Expand Up @@ -262,6 +320,10 @@ public boolean update(
boolean sessionHasBeenUpdated = false;
if (status != null) {
this.status = status;
// a crash terminates the process, so it takes precedence over a non-terminating one.
if (status == State.Crashed) {
nonTerminatingUnhandledError = false;
}
sessionHasBeenUpdated = true;
}

Expand Down Expand Up @@ -318,21 +380,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.setNonTerminatingUnhandledError(nonTerminatingUnhandledError);
return session;
}

// JsonSerializable
Expand All @@ -354,6 +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 NON_TERMINATING_UNHANDLED_ERROR = "non_terminating_unhandled_error";
}

@Override
Expand Down Expand Up @@ -384,6 +450,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger
if (abnormalMechanism != null) {
writer.name(JsonKeys.ABNORMAL_MECHANISM).value(logger, abnormalMechanism);
}
if (nonTerminatingUnhandledError) {
writer.name(JsonKeys.NON_TERMINATING_UNHANDLED_ERROR).value(nonTerminatingUnhandledError);
}
writer.name(JsonKeys.ATTRS);
writer.beginObject();
writer.name(JsonKeys.RELEASE).value(logger, release);
Expand Down Expand Up @@ -440,6 +509,7 @@ public static final class Deserializer implements JsonDeserializer<Session> {
String environment = null;
String release = null; // @NotNull
String abnormalMechanism = null;
boolean nonTerminatingUnhandledError = false;

Map<String, Object> unknown = null;
while (reader.peek() == JsonToken.NAME) {
Expand Down Expand Up @@ -483,6 +553,11 @@ public static final class Deserializer implements JsonDeserializer<Session> {
case JsonKeys.ABNORMAL_MECHANISM:
abnormalMechanism = reader.nextStringOrNull();
break;
case JsonKeys.NON_TERMINATING_UNHANDLED_ERROR:
final Boolean nonTerminatingUnhandledErrorValue = reader.nextBooleanOrNull();
nonTerminatingUnhandledError =
nonTerminatingUnhandledErrorValue != null && nonTerminatingUnhandledErrorValue;
break;
case JsonKeys.ATTRS:
reader.beginObject();
while (reader.peek() == JsonToken.NAME) {
Expand Down Expand Up @@ -542,6 +617,7 @@ public static final class Deserializer implements JsonDeserializer<Session> {
environment,
release,
abnormalMechanism);
session.setNonTerminatingUnhandledError(nonTerminatingUnhandledError);
session.setUnknown(unknown);
reader.endObject();
return session;
Expand Down
45 changes: 45 additions & 0 deletions sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,51 @@ class PreviousSessionFinalizerTest {
)
}

@Test
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 {
setNonTerminatingUnhandledError(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.hasNonTerminatingUnhandledError()
}
)
}

@Test
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 {
setNonTerminatingUnhandledError(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)
Expand Down
Loading
Loading