Skip to content

Commit 8b0dfba

Browse files
fix(session): When capturing unhandled hybrid exception session should be ended (#3480)
1 parent cb9b3f6 commit 8b0dfba

File tree

8 files changed

+151
-27
lines changed

8 files changed

+151
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Use SecureRandom in favor of Random for Metrics ([#3495](https://github.com/getsentry/sentry-java/pull/3495))
1414
- Fix UncaughtExceptionHandlerIntegration Memory Leak ([#3398](https://github.com/getsentry/sentry-java/pull/3398))
1515
- Fix duplicated http spans ([#3526](https://github.com/getsentry/sentry-java/pull/3526))
16+
- When capturing unhandled hybrid exception session should be ended and new start if need ([#3480](https://github.com/getsentry/sentry-java/pull/3480))
1617

1718
### Dependencies
1819

sentry-android-core/api/sentry-android-core.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader {
204204

205205
public final class io/sentry/android/core/InternalSentrySdk {
206206
public fun <init> ()V
207-
public static fun captureEnvelope ([B)Lio/sentry/protocol/SentryId;
207+
public static fun captureEnvelope ([BZ)Lio/sentry/protocol/SentryId;
208208
public static fun getAppStartMeasurement ()Ljava/util/Map;
209209
public static fun getCurrentScope ()Lio/sentry/IScope;
210210
public static fun serializeScope (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/IScope;)Ljava/util/Map;

sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package io.sentry.android.core;
22

3+
import static io.sentry.SentryLevel.DEBUG;
4+
import static io.sentry.SentryLevel.INFO;
5+
import static io.sentry.SentryLevel.WARNING;
6+
37
import android.content.Context;
48
import android.content.pm.PackageInfo;
59
import android.content.pm.PackageManager;
@@ -19,12 +23,14 @@
1923
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
2024
import io.sentry.android.core.performance.AppStartMetrics;
2125
import io.sentry.android.core.performance.TimeSpan;
26+
import io.sentry.cache.EnvelopeCache;
2227
import io.sentry.protocol.App;
2328
import io.sentry.protocol.Device;
2429
import io.sentry.protocol.SentryId;
2530
import io.sentry.protocol.User;
2631
import io.sentry.util.MapObjectWriter;
2732
import java.io.ByteArrayInputStream;
33+
import java.io.File;
2834
import java.io.InputStream;
2935
import java.util.ArrayList;
3036
import java.util.HashMap;
@@ -148,7 +154,8 @@ public static Map<String, Object> serializeScope(
148154
* captured
149155
*/
150156
@Nullable
151-
public static SentryId captureEnvelope(final @NotNull byte[] envelopeData) {
157+
public static SentryId captureEnvelope(
158+
final @NotNull byte[] envelopeData, final boolean maybeStartNewSession) {
152159
final @NotNull IHub hub = HubAdapter.getInstance();
153160
final @NotNull SentryOptions options = hub.getOptions();
154161

@@ -184,6 +191,13 @@ public static SentryId captureEnvelope(final @NotNull byte[] envelopeData) {
184191
if (session != null) {
185192
final SentryEnvelopeItem sessionItem = SentryEnvelopeItem.fromSession(serializer, session);
186193
envelopeItems.add(sessionItem);
194+
deleteCurrentSessionFile(
195+
options,
196+
// should be sync if going to crash or already not a main thread
197+
!maybeStartNewSession || !hub.getOptions().getMainThreadChecker().isMainThread());
198+
if (maybeStartNewSession) {
199+
hub.startSession();
200+
}
187201
}
188202

189203
final SentryEnvelope repackagedEnvelope =
@@ -233,15 +247,15 @@ private static void addTimeSpanToSerializedSpans(TimeSpan span, List<Map<String,
233247
HubAdapter.getInstance()
234248
.getOptions()
235249
.getLogger()
236-
.log(SentryLevel.WARNING, "Can not convert not-started TimeSpan to Map for Hybrid SDKs.");
250+
.log(WARNING, "Can not convert not-started TimeSpan to Map for Hybrid SDKs.");
237251
return;
238252
}
239253

240254
if (span.hasNotStopped()) {
241255
HubAdapter.getInstance()
242256
.getOptions()
243257
.getLogger()
244-
.log(SentryLevel.WARNING, "Can not convert not-stopped TimeSpan to Map for Hybrid SDKs.");
258+
.log(WARNING, "Can not convert not-stopped TimeSpan to Map for Hybrid SDKs.");
245259
return;
246260
}
247261

@@ -252,6 +266,46 @@ private static void addTimeSpanToSerializedSpans(TimeSpan span, List<Map<String,
252266
spans.add(spanMap);
253267
}
254268

269+
private static void deleteCurrentSessionFile(
270+
final @NotNull SentryOptions options, boolean isSync) {
271+
if (!isSync) {
272+
try {
273+
options
274+
.getExecutorService()
275+
.submit(
276+
() -> {
277+
deleteCurrentSessionFile(options);
278+
});
279+
} catch (Throwable e) {
280+
options
281+
.getLogger()
282+
.log(WARNING, "Submission of deletion of the current session file rejected.", e);
283+
}
284+
} else {
285+
deleteCurrentSessionFile(options);
286+
}
287+
}
288+
289+
private static void deleteCurrentSessionFile(final @NotNull SentryOptions options) {
290+
final String cacheDirPath = options.getCacheDirPath();
291+
if (cacheDirPath == null) {
292+
options.getLogger().log(INFO, "Cache dir is not set, not deleting the current session.");
293+
return;
294+
}
295+
296+
if (!options.isEnableAutoSessionTracking()) {
297+
options
298+
.getLogger()
299+
.log(DEBUG, "Session tracking is disabled, bailing from deleting current session file.");
300+
return;
301+
}
302+
303+
final File sessionFile = EnvelopeCache.getCurrentSessionFile(cacheDirPath);
304+
if (!sessionFile.delete()) {
305+
options.getLogger().log(WARNING, "Failed to delete the current session file.");
306+
}
307+
}
308+
255309
@Nullable
256310
private static Session updateSession(
257311
final @NotNull IHub hub,
@@ -268,11 +322,14 @@ private static Session updateSession(
268322
if (updated) {
269323
if (session.getStatus() == Session.State.Crashed) {
270324
session.end();
325+
// Session needs to be removed from the scope, otherwise it will be send twice
326+
// standalone and with the crash event
327+
scope.clearSession();
271328
}
272329
sessionRef.set(session);
273330
}
274331
} else {
275-
options.getLogger().log(SentryLevel.INFO, "Session is null on updateSession");
332+
options.getLogger().log(INFO, "Session is null on updateSession");
276333
}
277334
});
278335
return sessionRef.get();

sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicReference
3939
import kotlin.test.BeforeTest
4040
import kotlin.test.Test
4141
import kotlin.test.assertEquals
42+
import kotlin.test.assertNotEquals
4243
import kotlin.test.assertNotNull
4344
import kotlin.test.assertNull
4445
import kotlin.test.assertTrue
@@ -84,7 +85,7 @@ class InternalSentrySdkTest {
8485
capturedEnvelopes.clear()
8586
}
8687

87-
fun captureEnvelopeWithEvent(event: SentryEvent = SentryEvent()) {
88+
fun captureEnvelopeWithEvent(event: SentryEvent = SentryEvent(), maybeStartNewSession: Boolean = false) {
8889
// create an envelope with session data
8990
val options = Sentry.getCurrentHub().options
9091
val eventId = SentryId()
@@ -103,7 +104,24 @@ class InternalSentrySdkTest {
103104
options.serializer.serialize(envelope, outputStream)
104105
val data = outputStream.toByteArray()
105106

106-
InternalSentrySdk.captureEnvelope(data)
107+
InternalSentrySdk.captureEnvelope(data, maybeStartNewSession)
108+
}
109+
110+
fun createSentryEventWithUnhandledException(): SentryEvent {
111+
return SentryEvent(RuntimeException()).apply {
112+
val mechanism = Mechanism()
113+
mechanism.isHandled = false
114+
115+
val factory = SentryExceptionFactory(mock())
116+
val sentryExceptions = factory.getSentryExceptions(
117+
ExceptionMechanismException(
118+
mechanism,
119+
Throwable(),
120+
Thread()
121+
)
122+
)
123+
exceptions = sentryExceptions
124+
}
107125
}
108126

109127
fun mockFinishedAppStart() {
@@ -313,7 +331,7 @@ class InternalSentrySdkTest {
313331

314332
@Test
315333
fun `captureEnvelope fails if payload is invalid`() {
316-
assertNull(InternalSentrySdk.captureEnvelope(ByteArray(8)))
334+
assertNull(InternalSentrySdk.captureEnvelope(ByteArray(8), false))
317335
}
318336

319337
@Test
@@ -337,27 +355,19 @@ class InternalSentrySdkTest {
337355
}
338356

339357
@Test
340-
fun `captureEnvelope correctly enriches the envelope with session data`() {
358+
fun `captureEnvelope correctly enriches the envelope with session data and does not start new session`() {
341359
val fixture = Fixture()
342360
fixture.init(context)
343361

344-
// when capture envelope is called with an crashed event
345-
fixture.captureEnvelopeWithEvent(
346-
SentryEvent(RuntimeException()).apply {
347-
val mechanism = Mechanism()
348-
mechanism.isHandled = false
362+
// keep reference for current session for later assertions
363+
// we need to get the reference now as it will be removed from the scope
364+
val sessionRef = AtomicReference<Session>()
365+
Sentry.configureScope { scope ->
366+
sessionRef.set(scope.session)
367+
}
349368

350-
val factory = SentryExceptionFactory(mock())
351-
val sentryExceptions = factory.getSentryExceptions(
352-
ExceptionMechanismException(
353-
mechanism,
354-
Throwable(),
355-
Thread()
356-
)
357-
)
358-
exceptions = sentryExceptions
359-
}
360-
)
369+
// when capture envelope is called with an crashed event
370+
fixture.captureEnvelopeWithEvent(fixture.createSentryEventWithUnhandledException())
361371

362372
val capturedEnvelope = fixture.capturedEnvelopes.first()
363373
val capturedEnvelopeItems = capturedEnvelope.items.toList()
@@ -374,12 +384,52 @@ class InternalSentrySdkTest {
374384
)!!
375385
assertEquals(Session.State.Crashed, capturedSession.status)
376386

377-
// and the local session should be marked as crashed too
387+
assertEquals(Session.State.Crashed, sessionRef.get().status)
388+
assertEquals(capturedSession.sessionId, sessionRef.get().sessionId)
389+
}
390+
391+
@Test
392+
fun `captureEnvelope starts new session when enabled`() {
393+
val fixture = Fixture()
394+
fixture.init(context)
395+
396+
// when capture envelope is called with an crashed event
397+
fixture.captureEnvelopeWithEvent(fixture.createSentryEventWithUnhandledException(), true)
398+
378399
val scopeRef = AtomicReference<IScope>()
379400
Sentry.configureScope { scope ->
380401
scopeRef.set(scope)
381402
}
382-
assertEquals(Session.State.Crashed, scopeRef.get().session!!.status)
403+
404+
// first envelope is the new session start
405+
val capturedStartSessionEnvelope = fixture.capturedEnvelopes.first()
406+
val capturedNewSessionStart = fixture.options.serializer.deserialize(
407+
InputStreamReader(ByteArrayInputStream(capturedStartSessionEnvelope.items.toList()[0].data)),
408+
Session::class.java
409+
)!!
410+
assertEquals(capturedNewSessionStart.sessionId, scopeRef.get().session!!.sessionId)
411+
assertEquals(Session.State.Ok, capturedNewSessionStart.status)
412+
413+
val capturedEnvelope = fixture.capturedEnvelopes.last()
414+
val capturedEnvelopeItems = capturedEnvelope.items.toList()
415+
416+
// there should be two envelopes session start and captured crash
417+
assertEquals(2, fixture.capturedEnvelopes.size)
418+
419+
// then it should contain the original event + session
420+
assertEquals(2, capturedEnvelopeItems.size)
421+
assertEquals(SentryItemType.Event, capturedEnvelopeItems[0].header.type)
422+
assertEquals(SentryItemType.Session, capturedEnvelopeItems[1].header.type)
423+
424+
// and then the sent session should be marked as crashed
425+
val capturedSession = fixture.options.serializer.deserialize(
426+
InputStreamReader(ByteArrayInputStream(capturedEnvelopeItems[1].data)),
427+
Session::class.java
428+
)!!
429+
assertEquals(Session.State.Crashed, capturedSession.status)
430+
431+
// and the local session should be a new session
432+
assertNotEquals(capturedSession.sessionId, scopeRef.get().session!!.sessionId)
383433
}
384434

385435
@Test

sentry/api/sentry.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ public abstract interface class io/sentry/IScope {
667667
public abstract fun clear ()V
668668
public abstract fun clearAttachments ()V
669669
public abstract fun clearBreadcrumbs ()V
670+
public abstract fun clearSession ()V
670671
public abstract fun clearTransaction ()V
671672
public abstract fun clone ()Lio/sentry/IScope;
672673
public abstract fun endSession ()Lio/sentry/Session;
@@ -1221,6 +1222,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope {
12211222
public fun clear ()V
12221223
public fun clearAttachments ()V
12231224
public fun clearBreadcrumbs ()V
1225+
public fun clearSession ()V
12241226
public fun clearTransaction ()V
12251227
public fun clone ()Lio/sentry/IScope;
12261228
public synthetic fun clone ()Ljava/lang/Object;
@@ -1592,6 +1594,7 @@ public final class io/sentry/Scope : io/sentry/IScope {
15921594
public fun clear ()V
15931595
public fun clearAttachments ()V
15941596
public fun clearBreadcrumbs ()V
1597+
public fun clearSession ()V
15951598
public fun clearTransaction ()V
15961599
public fun clone ()Lio/sentry/IScope;
15971600
public synthetic fun clone ()Ljava/lang/Object;

sentry/src/main/java/io/sentry/IScope.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ public interface IScope {
352352
@Nullable
353353
Session getSession();
354354

355+
@ApiStatus.Internal
356+
void clearSession();
357+
355358
@ApiStatus.Internal
356359
void setPropagationContext(final @NotNull PropagationContext propagationContext);
357360

sentry/src/main/java/io/sentry/NoOpScope.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ public void withTransaction(Scope.@NotNull IWithTransaction callback) {}
219219
return null;
220220
}
221221

222+
@ApiStatus.Internal
223+
@Override
224+
public void clearSession() {}
225+
222226
@ApiStatus.Internal
223227
@Override
224228
public void setPropagationContext(@NotNull PropagationContext propagationContext) {}

sentry/src/main/java/io/sentry/Scope.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,12 @@ public SentryOptions getOptions() {
913913
return session;
914914
}
915915

916+
@ApiStatus.Internal
917+
@Override
918+
public void clearSession() {
919+
session = null;
920+
}
921+
916922
@ApiStatus.Internal
917923
@Override
918924
public void setPropagationContext(final @NotNull PropagationContext propagationContext) {

0 commit comments

Comments
 (0)