@@ -5,6 +5,7 @@ import android.content.ContentProvider
55import android.content.Context
66import androidx.test.core.app.ApplicationProvider
77import androidx.test.ext.junit.runners.AndroidJUnit4
8+ import com.google.common.truth.Truth.assertThat
89import io.sentry.Breadcrumb
910import io.sentry.Hint
1011import io.sentry.IScope
@@ -22,6 +23,7 @@ import io.sentry.Session
2223import io.sentry.SpanId
2324import io.sentry.android.core.performance.ActivityLifecycleTimeSpan
2425import io.sentry.android.core.performance.AppStartMetrics
26+ import io.sentry.cache.EnvelopeCache
2527import io.sentry.exception.ExceptionMechanismException
2628import io.sentry.protocol.App
2729import io.sentry.protocol.Contexts
@@ -107,6 +109,21 @@ class InternalSentrySdkTest {
107109 InternalSentrySdk .captureEnvelope(data, maybeStartNewSession)
108110 }
109111
112+ fun captureEnvelopeNonTerminatingWithEvent (event : SentryEvent = SentryEvent ()) {
113+ val options = Sentry .getCurrentScopes().options
114+ val eventId = SentryId ()
115+ val header = SentryEnvelopeHeader (eventId)
116+ val eventItem = SentryEnvelopeItem .fromEvent(options.serializer, event)
117+
118+ val envelope = SentryEnvelope (header, listOf (eventItem))
119+
120+ val outputStream = ByteArrayOutputStream ()
121+ options.serializer.serialize(envelope, outputStream)
122+ val data = outputStream.toByteArray()
123+
124+ InternalSentrySdk .captureEnvelopeNonTerminating(data)
125+ }
126+
110127 fun createSentryEventWithUnhandledException (): SentryEvent {
111128 return SentryEvent (RuntimeException ()).apply {
112129 val mechanism = Mechanism ()
@@ -452,6 +469,110 @@ class InternalSentrySdkTest {
452469 assertNotEquals(capturedSession.sessionId, scopeRef.get().session!! .sessionId)
453470 }
454471
472+ @Test
473+ fun `captureEnvelopeNonTerminating keeps the session Ok and marks it pending unhandled` () {
474+ val fixture = Fixture ()
475+ fixture.init (context)
476+
477+ val originalSid = AtomicReference <String >()
478+ Sentry .configureScope { scope -> originalSid.set(scope.session!! .sessionId) }
479+
480+ // when capture envelope is called with an unhandled event through the non-terminating API
481+ fixture.captureEnvelopeNonTerminatingWithEvent(
482+ fixture.createSentryEventWithUnhandledException()
483+ )
484+
485+ // then only the original event envelope is captured, without a session item
486+ assertThat(fixture.capturedEnvelopes).hasSize(1 )
487+ val capturedEnvelopeItems = fixture.capturedEnvelopes.first().items.toList()
488+ assertThat(capturedEnvelopeItems).hasSize(1 )
489+ assertThat(capturedEnvelopeItems[0 ].header.type).isEqualTo(SentryItemType .Event )
490+
491+ // and the session stays alive on the scope, same id, marked pending unhandled
492+ val scopeSession = AtomicReference <Session >()
493+ Sentry .configureScope { scope -> scopeSession.set(scope.session) }
494+ assertThat(scopeSession.get().status).isEqualTo(Session .State .Ok )
495+ assertThat(scopeSession.get().isPendingUnhandled).isTrue()
496+ assertThat(scopeSession.get().sessionId).isEqualTo(originalSid.get())
497+
498+ // and it is persisted so pending survives process death
499+ val sessionFile = EnvelopeCache .getCurrentSessionFile(fixture.options.cacheDirPath!! )
500+ val persistedSession =
501+ fixture.options.serializer.deserialize(sessionFile.reader(), Session ::class .java)!!
502+ assertThat(persistedSession.status).isEqualTo(Session .State .Ok )
503+ assertThat(persistedSession.isPendingUnhandled).isTrue()
504+ assertThat(persistedSession.sessionId).isEqualTo(originalSid.get())
505+ }
506+
507+ @Test
508+ fun `captureEnvelopeNonTerminating then endSession finalizes the session as unhandled` () {
509+ val fixture = Fixture ()
510+ fixture.init (context)
511+
512+ fixture.captureEnvelopeNonTerminatingWithEvent(
513+ fixture.createSentryEventWithUnhandledException()
514+ )
515+ fixture.capturedEnvelopes.clear()
516+
517+ // when the session is ended by normal lifecycle
518+ Sentry .endSession()
519+
520+ // then the ended session is captured as unhandled
521+ val sessionItems =
522+ fixture.capturedEnvelopes
523+ .flatMap { it.items.toList() }
524+ .filter {
525+ it.header.type == SentryItemType .Session
526+ }
527+ assertThat(sessionItems).hasSize(1 )
528+ val endedSession =
529+ fixture.options.serializer.deserialize(
530+ InputStreamReader (ByteArrayInputStream (sessionItems[0 ].data)),
531+ Session ::class .java,
532+ )!!
533+ assertThat(endedSession.status).isEqualTo(Session .State .Unhandled )
534+ }
535+
536+ @Test
537+ fun `captureEnvelopeNonTerminating then a crash finalizes old session and starts a new one` () {
538+ val fixture = Fixture ()
539+ fixture.init (context)
540+
541+ fixture.captureEnvelopeNonTerminatingWithEvent(
542+ fixture.createSentryEventWithUnhandledException()
543+ )
544+ val pendingSession = AtomicReference <Session >()
545+ Sentry .configureScope { scope -> pendingSession.set(scope.session) }
546+ val oldSid = pendingSession.get().sessionId
547+ assertThat(pendingSession.get().isPendingUnhandled).isTrue()
548+ fixture.capturedEnvelopes.clear()
549+
550+ // when a subsequent hard crash is captured through the existing terminating API
551+ fixture.captureEnvelopeWithEvent(fixture.createSentryEventWithUnhandledException(), true )
552+
553+ // then the crash envelope contains the finalized old session
554+ assertThat(fixture.capturedEnvelopes).hasSize(2 )
555+ val crashEnvelopeItems = fixture.capturedEnvelopes.last().items.toList()
556+ assertThat(crashEnvelopeItems).hasSize(2 )
557+ assertThat(crashEnvelopeItems[0 ].header.type).isEqualTo(SentryItemType .Event )
558+ assertThat(crashEnvelopeItems[1 ].header.type).isEqualTo(SentryItemType .Session )
559+ val crashedSession =
560+ fixture.options.serializer.deserialize(
561+ InputStreamReader (ByteArrayInputStream (crashEnvelopeItems[1 ].data)),
562+ Session ::class .java,
563+ )!!
564+ assertThat(crashedSession.status).isEqualTo(Session .State .Crashed )
565+ assertThat(crashedSession.isPendingUnhandled).isFalse()
566+ assertThat(crashedSession.sessionId).isEqualTo(oldSid)
567+
568+ // and a new Ok session with a different id is active
569+ val activeSession = AtomicReference <Session >()
570+ Sentry .configureScope { scope -> activeSession.set(scope.session) }
571+ assertThat(activeSession.get().status).isEqualTo(Session .State .Ok )
572+ assertThat(activeSession.get().isPendingUnhandled).isFalse()
573+ assertThat(activeSession.get().sessionId).isNotEqualTo(oldSid)
574+ }
575+
455576 @Test
456577 fun `getAppStartMeasurement returns correct serialized data from the app start instance` () {
457578 Fixture ().mockFinishedAppStart()
0 commit comments