Skip to content

Commit 52bf486

Browse files
committed
Merge branch 'feat/unhandled-sessions-protocol' into feat/unhandled-sessions-cache
2 parents 4c6184a + e62793f commit 52bf486

5 files changed

Lines changed: 87 additions & 69 deletions

File tree

‎sentry/api/sentry.api‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4286,12 +4286,12 @@ public final class io/sentry/Session : io/sentry/JsonSerializable, io/sentry/Jso
42864286
public fun getTimestamp ()Ljava/util/Date;
42874287
public fun getUnknown ()Ljava/util/Map;
42884288
public fun getUserAgent ()Ljava/lang/String;
4289-
public fun isPendingUnhandled ()Z
4289+
public fun hasNonTerminatingUnhandledError ()Z
42904290
public fun isTerminated ()Z
4291-
public fun markPendingUnhandled ()Z
4291+
public fun recordNonTerminatingUnhandledError ()Z
42924292
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
42934293
public fun setInitAsTrue ()V
4294-
public fun setPendingUnhandled (Z)V
4294+
public fun setNonTerminatingUnhandledError (Z)V
42954295
public fun setUnknown (Ljava/util/Map;)V
42964296
public fun update (Lio/sentry/Session$State;Ljava/lang/String;Z)Z
42974297
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 {
43124312
public static final field ERRORS Ljava/lang/String;
43134313
public static final field INIT Ljava/lang/String;
43144314
public static final field IP_ADDRESS Ljava/lang/String;
4315-
public static final field PENDING_UNHANDLED Ljava/lang/String;
4315+
public static final field NON_TERMINATING_UNHANDLED_ERROR Ljava/lang/String;
43164316
public static final field RELEASE Ljava/lang/String;
43174317
public static final field SEQ Ljava/lang/String;
43184318
public static final field SID Ljava/lang/String;

‎sentry/src/main/java/io/sentry/Session.java‎

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,15 @@ public enum State {
6868
private @Nullable String abnormalMechanism;
6969

7070
/**
71-
* Whether the session experienced an unhandled (but non-terminal) exception. Kept locally and
72-
* persisted with the session, but never sent as a status while the session is alive. On end() the
73-
* session is finalized as {@link State#Unhandled} instead of {@link State#Exited} unless a crash
74-
* escalated it to {@link State#Crashed}.
71+
* Whether the session experienced an unhandled error that did <em>not</em> terminate the process,
72+
* e.g. an unhandled Flutter exception. A native crash is also unhandled, but it kills the process
73+
* and therefore ends the session as {@link State#Crashed} instead.
74+
*
75+
* <p>Kept locally and persisted with the session, but never sent as a status while the session is
76+
* alive. On end() the session is finalized as {@link State#Unhandled} instead of {@link
77+
* State#Exited}, unless a crash escalated it to {@link State#Crashed}.
7578
*/
76-
private boolean pendingUnhandled;
79+
private boolean nonTerminatingUnhandledError;
7780

7881
/** The session lock, ops should be atomic */
7982
private final @NotNull AutoClosableReentrantLock sessionLock = new AutoClosableReentrantLock();
@@ -198,32 +201,41 @@ public int errorCount() {
198201
}
199202

200203
/**
201-
* Whether the session has a pending unhandled (non-terminal) exception that hasn't been finalized
202-
* yet.
204+
* Whether the session experienced an unhandled error that did not terminate the process, and so
205+
* finalizes as {@link State#Unhandled} rather than {@link State#Exited}.
203206
*/
204207
@ApiStatus.Internal
205-
public boolean isPendingUnhandled() {
206-
return pendingUnhandled;
208+
public boolean hasNonTerminatingUnhandledError() {
209+
return nonTerminatingUnhandledError;
207210
}
208211

209212
/**
210-
* Marks the session as having experienced an unhandled (non-terminal) exception without ending
211-
* it. On {@link #end()} the session will be finalized as {@link State#Unhandled} unless a crash
212-
* escalated it to {@link State#Crashed} first.
213+
* Restores the flag when rebuilding a session, i.e. from {@link #clone()} or the deserializer.
214+
*
215+
* <p>Not for use on a live session: unlike {@link #recordNonTerminatingUnhandledError()} this
216+
* neither counts the error nor advances the session's sequence, so a session mutated through this
217+
* setter would be sent as an out-of-date update.
213218
*/
214219
@ApiStatus.Internal
215-
public void setPendingUnhandled(final boolean pendingUnhandled) {
216-
this.pendingUnhandled = pendingUnhandled;
220+
public void setNonTerminatingUnhandledError(final boolean nonTerminatingUnhandledError) {
221+
this.nonTerminatingUnhandledError = nonTerminatingUnhandledError;
217222
}
218223

219-
/** Marks an active session as having experienced an unhandled non-terminal exception. */
224+
/**
225+
* Records that an active session experienced an unhandled error which did not terminate the
226+
* process, counting the error and advancing the session's sequence without ending it. On {@link
227+
* #end()} the session is finalized as {@link State#Unhandled} unless a crash escalated it to
228+
* {@link State#Crashed} first.
229+
*
230+
* @return whether the session was updated, i.e. false if it had already reached a terminal state
231+
*/
220232
@ApiStatus.Internal
221-
public boolean markPendingUnhandled() {
233+
public boolean recordNonTerminatingUnhandledError() {
222234
try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) {
223235
if (status != State.Ok) {
224236
return false;
225237
}
226-
pendingUnhandled = true;
238+
nonTerminatingUnhandledError = true;
227239
errorCount.incrementAndGet();
228240
init = null;
229241
timestamp = DateUtils.getCurrentDateTime();
@@ -255,7 +267,7 @@ public void end(final @Nullable Date timestamp) {
255267
if (status == State.Ok) {
256268
// a session that experienced an unhandled (but non-terminal) exception is finalized as
257269
// Unhandled rather than Exited.
258-
status = pendingUnhandled ? State.Unhandled : State.Exited;
270+
status = nonTerminatingUnhandledError ? State.Unhandled : State.Exited;
259271
}
260272

261273
if (timestamp != null) {
@@ -308,9 +320,9 @@ public boolean update(
308320
boolean sessionHasBeenUpdated = false;
309321
if (status != null) {
310322
this.status = status;
311-
// a real crash takes precedence over a pending unhandled (non-terminal) exception.
323+
// a crash terminates the process, so it takes precedence over a non-terminating one.
312324
if (status == State.Crashed) {
313-
pendingUnhandled = false;
325+
nonTerminatingUnhandledError = false;
314326
}
315327
sessionHasBeenUpdated = true;
316328
}
@@ -384,7 +396,7 @@ private long getSequenceTimestamp(final @NotNull Date timestamp) {
384396
environment,
385397
release,
386398
abnormalMechanism);
387-
session.setPendingUnhandled(pendingUnhandled);
399+
session.setNonTerminatingUnhandledError(nonTerminatingUnhandledError);
388400
return session;
389401
}
390402

@@ -407,7 +419,7 @@ public static final class JsonKeys {
407419
public static final String IP_ADDRESS = "ip_address";
408420
public static final String USER_AGENT = "user_agent";
409421
public static final String ABNORMAL_MECHANISM = "abnormal_mechanism";
410-
public static final String PENDING_UNHANDLED = "pending_unhandled";
422+
public static final String NON_TERMINATING_UNHANDLED_ERROR = "non_terminating_unhandled_error";
411423
}
412424

413425
@Override
@@ -438,8 +450,8 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger
438450
if (abnormalMechanism != null) {
439451
writer.name(JsonKeys.ABNORMAL_MECHANISM).value(logger, abnormalMechanism);
440452
}
441-
if (pendingUnhandled) {
442-
writer.name(JsonKeys.PENDING_UNHANDLED).value(pendingUnhandled);
453+
if (nonTerminatingUnhandledError) {
454+
writer.name(JsonKeys.NON_TERMINATING_UNHANDLED_ERROR).value(nonTerminatingUnhandledError);
443455
}
444456
writer.name(JsonKeys.ATTRS);
445457
writer.beginObject();
@@ -497,7 +509,7 @@ public static final class Deserializer implements JsonDeserializer<Session> {
497509
String environment = null;
498510
String release = null; // @NotNull
499511
String abnormalMechanism = null;
500-
boolean pendingUnhandled = false;
512+
boolean nonTerminatingUnhandledError = false;
501513

502514
Map<String, Object> unknown = null;
503515
while (reader.peek() == JsonToken.NAME) {
@@ -541,9 +553,10 @@ public static final class Deserializer implements JsonDeserializer<Session> {
541553
case JsonKeys.ABNORMAL_MECHANISM:
542554
abnormalMechanism = reader.nextStringOrNull();
543555
break;
544-
case JsonKeys.PENDING_UNHANDLED:
545-
final Boolean pendingUnhandledValue = reader.nextBooleanOrNull();
546-
pendingUnhandled = pendingUnhandledValue != null && pendingUnhandledValue;
556+
case JsonKeys.NON_TERMINATING_UNHANDLED_ERROR:
557+
final Boolean nonTerminatingUnhandledErrorValue = reader.nextBooleanOrNull();
558+
nonTerminatingUnhandledError =
559+
nonTerminatingUnhandledErrorValue != null && nonTerminatingUnhandledErrorValue;
547560
break;
548561
case JsonKeys.ATTRS:
549562
reader.beginObject();
@@ -604,7 +617,7 @@ public static final class Deserializer implements JsonDeserializer<Session> {
604617
environment,
605618
release,
606619
abnormalMechanism);
607-
session.setPendingUnhandled(pendingUnhandled);
620+
session.setNonTerminatingUnhandledError(nonTerminatingUnhandledError);
608621
session.setUnknown(unknown);
609622
reader.endObject();
610623
return session;

‎sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,14 @@ class PreviousSessionFinalizerTest {
201201
}
202202

203203
@Test
204-
fun `if previous session has pending unhandled and no crash marker, finalizes as unhandled`() {
204+
fun `if previous session has a non-terminating unhandled error and no crash marker, finalizes as unhandled`() {
205205
val finalizer =
206206
fixture.getSut(
207207
tmpDir,
208208
session =
209-
Session(null, null, null, "io.sentry.sample@1.0").apply { setPendingUnhandled(true) },
209+
Session(null, null, null, "io.sentry.sample@1.0").apply {
210+
setNonTerminatingUnhandledError(true)
211+
},
210212
)
211213
finalizer.run()
212214

@@ -216,18 +218,20 @@ class PreviousSessionFinalizerTest {
216218
val session = fixture.sessionFromEnvelope(this)
217219
session.release == "io.sentry.sample@1.0" &&
218220
session.status == Session.State.Unhandled &&
219-
session.isPendingUnhandled
221+
session.hasNonTerminatingUnhandledError()
220222
}
221223
)
222224
}
223225

224226
@Test
225-
fun `if previous session has pending unhandled but a native crash marker exists, finalizes as crashed`() {
227+
fun `if previous session has a non-terminating unhandled error but a native crash marker exists, finalizes as crashed`() {
226228
val finalizer =
227229
fixture.getSut(
228230
tmpDir,
229231
session =
230-
Session(null, null, null, "io.sentry.sample@1.0").apply { setPendingUnhandled(true) },
232+
Session(null, null, null, "io.sentry.sample@1.0").apply {
233+
setNonTerminatingUnhandledError(true)
234+
},
231235
nativeCrashTimestamp = DateUtils.getDateTime("2023-10-01T00:00:00.000Z"),
232236
)
233237
finalizer.run()

‎sentry/src/test/java/io/sentry/SessionTest.kt‎

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ class SessionTest {
1111
private fun okSession(): Session = Session(null, null, "environment", "release")
1212

1313
@Test
14-
fun `markPendingUnhandled atomically updates an Ok session`() {
14+
fun `recordNonTerminatingUnhandledError atomically updates an Ok session`() {
1515
val session = okSession()
1616
val initialTimestamp = session.timestamp
1717

18-
val updated = session.markPendingUnhandled()
18+
val updated = session.recordNonTerminatingUnhandledError()
1919

2020
assertThat(updated).isTrue()
2121
assertThat(session.status).isEqualTo(Session.State.Ok)
22-
assertThat(session.isPendingUnhandled).isTrue()
22+
assertThat(session.hasNonTerminatingUnhandledError()).isTrue()
2323
assertThat(session.errorCount()).isEqualTo(1)
2424
assertThat(session.init).isNull()
2525
assertThat(session.timestamp).isNotNull()
@@ -28,17 +28,18 @@ class SessionTest {
2828
}
2929

3030
@Test
31-
fun `markPendingUnhandled does not change terminal sessions`() {
31+
fun `recordNonTerminatingUnhandledError does not change terminal sessions`() {
3232
for (state in Session.State.entries.filter { it != Session.State.Ok }) {
3333
val session = okSession()
3434
session.update(state, null, false)
3535
val before = session.clone()
3636

37-
val updated = session.markPendingUnhandled()
37+
val updated = session.recordNonTerminatingUnhandledError()
3838

3939
assertThat(updated).isFalse()
4040
assertThat(session.status).isEqualTo(before.status)
41-
assertThat(session.isPendingUnhandled).isEqualTo(before.isPendingUnhandled)
41+
assertThat(session.hasNonTerminatingUnhandledError())
42+
.isEqualTo(before.hasNonTerminatingUnhandledError())
4243
assertThat(session.errorCount()).isEqualTo(before.errorCount())
4344
assertThat(session.init).isEqualTo(before.init)
4445
assertThat(session.timestamp).isEqualTo(before.timestamp)
@@ -47,7 +48,7 @@ class SessionTest {
4748
}
4849

4950
@Test
50-
fun `end without pending unhandled finalizes as Exited`() {
51+
fun `end without a non-terminating unhandled error finalizes as Exited`() {
5152
val session = okSession()
5253

5354
session.end()
@@ -56,68 +57,68 @@ class SessionTest {
5657
}
5758

5859
@Test
59-
fun `end with pending unhandled finalizes as Unhandled`() {
60+
fun `end with a non-terminating unhandled error finalizes as Unhandled`() {
6061
val session = okSession()
61-
assertThat(session.isPendingUnhandled).isFalse()
62+
assertThat(session.hasNonTerminatingUnhandledError()).isFalse()
6263

63-
session.setPendingUnhandled(true)
64+
session.setNonTerminatingUnhandledError(true)
6465
session.end()
6566

6667
assertThat(session.status).isEqualTo(Session.State.Unhandled)
67-
assertThat(session.isPendingUnhandled).isTrue()
68+
assertThat(session.hasNonTerminatingUnhandledError()).isTrue()
6869
}
6970

7071
@Test
71-
fun `end with pending unhandled keeps Abnormal as Abnormal`() {
72+
fun `end with a non-terminating unhandled error keeps Abnormal as Abnormal`() {
7273
val session = okSession()
73-
session.setPendingUnhandled(true)
74+
session.setNonTerminatingUnhandledError(true)
7475
session.update(Session.State.Abnormal, null, false, "anr")
7576

7677
session.end()
7778

7879
assertThat(session.status).isEqualTo(Session.State.Abnormal)
79-
assertThat(session.isPendingUnhandled).isTrue()
80+
assertThat(session.hasNonTerminatingUnhandledError()).isTrue()
8081
}
8182

8283
@Test
83-
fun `end with pending unhandled keeps Crashed as Crashed`() {
84+
fun `end with a non-terminating unhandled error keeps Crashed as Crashed`() {
8485
val session = okSession()
85-
session.setPendingUnhandled(true)
86+
session.setNonTerminatingUnhandledError(true)
8687
session.update(Session.State.Crashed, null, false)
8788

8889
session.end()
8990

9091
assertThat(session.status).isEqualTo(Session.State.Crashed)
91-
assertThat(session.isPendingUnhandled).isFalse()
92+
assertThat(session.hasNonTerminatingUnhandledError()).isFalse()
9293
}
9394

9495
@Test
95-
fun `updating to Crashed clears pending unhandled and end stays Crashed`() {
96+
fun `updating to Crashed clears a non-terminating unhandled error and end stays Crashed`() {
9697
val session = okSession()
97-
session.setPendingUnhandled(true)
98+
session.setNonTerminatingUnhandledError(true)
9899

99100
session.update(Session.State.Crashed, null, true)
100101
session.end()
101102

102103
assertThat(session.status).isEqualTo(Session.State.Crashed)
103-
assertThat(session.isPendingUnhandled).isFalse()
104+
assertThat(session.hasNonTerminatingUnhandledError()).isFalse()
104105
}
105106

106107
@Test
107-
fun `clone preserves pending unhandled`() {
108+
fun `clone preserves a non-terminating unhandled error`() {
108109
val session = okSession()
109-
session.setPendingUnhandled(true)
110+
session.setNonTerminatingUnhandledError(true)
110111

111112
val clone = session.clone()
112113

113-
assertThat(clone.isPendingUnhandled).isTrue()
114+
assertThat(clone.hasNonTerminatingUnhandledError()).isTrue()
114115
}
115116

116117
@Test
117-
fun `serialization round-trips pending unhandled and Unhandled status`() {
118+
fun `serialization round-trips a non-terminating unhandled error and Unhandled status`() {
118119
val logger = mock<ILogger>()
119120
val session = okSession()
120-
session.setPendingUnhandled(true)
121+
session.setNonTerminatingUnhandledError(true)
121122
session.end()
122123
assertThat(session.status).isEqualTo(Session.State.Unhandled)
123124

@@ -128,17 +129,17 @@ class SessionTest {
128129
Session.Deserializer().deserialize(JsonObjectReader(StringReader(writer.toString())), logger)
129130

130131
assertThat(deserialized.status).isEqualTo(Session.State.Unhandled)
131-
assertThat(deserialized.isPendingUnhandled).isTrue()
132+
assertThat(deserialized.hasNonTerminatingUnhandledError()).isTrue()
132133
}
133134

134135
@Test
135-
fun `pending unhandled defaults to false and is not serialized when unset`() {
136+
fun `a non-terminating unhandled error defaults to false and is not serialized when unset`() {
136137
val logger = mock<ILogger>()
137138
val session = okSession()
138139

139140
val writer = StringWriter()
140141
session.serialize(JsonObjectWriter(writer, 100), logger)
141142

142-
assertThat(writer.toString()).doesNotContain("pending_unhandled")
143+
assertThat(writer.toString()).doesNotContain("non_terminating_unhandled_error")
143144
}
144145
}

0 commit comments

Comments
 (0)