Skip to content

Commit a622ea2

Browse files
buenaflorclaude
andcommitted
fix(extend-app-start): Record the extended end time independently of span state
getExtendedEndTime() read span.isFinished(), but finishing the extended span completes the waitForChildren transaction and runs the event processor re-entrantly within finishExtendedAppStart(), before the span's finished flag is set. The processor then saw an unfinished span and dropped the app start measurement. Capture the end time before finishing the span instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ec4da6c commit a622ea2

2 files changed

Lines changed: 36 additions & 19 deletions

File tree

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public interface ExtendAppStartListener {
3939
private @Nullable ExtendAppStartListener extendAppStartListener;
4040
private @Nullable ISpan extendedSpan;
4141
private @Nullable ITransaction extendedTransaction;
42+
private @Nullable SentryDate extendedEndDate;
4243

4344
public AppStartExtension(final @NotNull AppStartMetrics metrics) {
4445
this.metrics = metrics;
@@ -83,7 +84,13 @@ public void finishExtendedAppStart() {
8384
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
8485
final @Nullable ISpan span = extendedSpan;
8586
if (span != null && !span.isFinished()) {
86-
span.finish(SpanStatus.OK);
87+
// Capture the end time before finishing the span. Finishing the last child of a
88+
// waitForChildren transaction completes it and runs the event processor re-entrantly,
89+
// within this call, before span.isFinished() flips. Reading the span back there would see
90+
// an unfinished span, so we record the end here instead.
91+
final @NotNull SentryDate now = AndroidDateUtils.getCurrentSentryDateTime();
92+
extendedEndDate = now;
93+
span.finish(SpanStatus.OK, now);
8794
}
8895
}
8996
}
@@ -116,23 +123,17 @@ public void finishTransaction(final @NotNull SentryDate endTimestamp) {
116123

117124
public @Nullable SentryDate getExtendedEndTime() {
118125
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
119-
final @Nullable ISpan span = extendedSpan;
120-
if (span == null || !span.isFinished()) {
121-
return null;
122-
}
123-
// A deadline timeout would report an artificially inflated duration; suppress the vital
124-
// instead.
125-
if (span.getStatus() == SpanStatus.DEADLINE_EXCEEDED) {
126-
return null;
127-
}
128-
return span.getFinishDate();
126+
// Set only by an explicit finishExtendedAppStart(); stays null on a deadline timeout, which
127+
// suppresses the vital so we never report an artificially inflated duration.
128+
return extendedEndDate;
129129
}
130130
}
131131

132132
public void clear() {
133133
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
134134
extendedSpan = null;
135135
extendedTransaction = null;
136+
extendedEndDate = null;
136137
}
137138
}
138139
}

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
55
import io.sentry.ISpan
66
import io.sentry.ITransaction
77
import io.sentry.NoOpSpan
8+
import io.sentry.SentryDate
89
import io.sentry.SentryNanotimeDate
910
import io.sentry.SpanStatus
1011
import io.sentry.android.core.performance.AppStartMetrics
1112
import java.util.concurrent.atomic.AtomicInteger
1213
import kotlin.test.Test
1314
import kotlin.test.assertEquals
1415
import kotlin.test.assertFalse
16+
import kotlin.test.assertNotNull
1517
import kotlin.test.assertNull
1618
import kotlin.test.assertSame
1719
import kotlin.test.assertTrue
1820
import org.junit.runner.RunWith
1921
import org.mockito.kotlin.any
22+
import org.mockito.kotlin.argumentCaptor
23+
import org.mockito.kotlin.eq
2024
import org.mockito.kotlin.mock
2125
import org.mockito.kotlin.never
2226
import org.mockito.kotlin.verify
@@ -104,12 +108,14 @@ class AppStartExtensionTest {
104108
}
105109

106110
@Test
107-
fun `finishExtendedAppStart finishes the extended span`() {
111+
fun `finishExtendedAppStart finishes the extended span at the captured end`() {
108112
val ext = extension(windowOpen = true)
109113
val (_, span) = ext.registerHandOver()
110114
ext.extendAppStart()
111115
ext.finishExtendedAppStart()
112-
verify(span).finish(SpanStatus.OK)
116+
val dateCaptor = argumentCaptor<SentryDate>()
117+
verify(span).finish(eq(SpanStatus.OK), dateCaptor.capture())
118+
assertSame(dateCaptor.firstValue, ext.extendedEndTime)
113119
}
114120

115121
@Test
@@ -176,16 +182,26 @@ class AppStartExtensionTest {
176182
}
177183

178184
@Test
179-
fun `getExtendedEndTime returns the finish date on a user finish`() {
185+
fun `getExtendedEndTime returns the captured end after a user finish`() {
186+
val ext = extension(windowOpen = true)
187+
val (_, span) = ext.registerHandOver()
188+
ext.extendAppStart()
189+
ext.finishExtendedAppStart()
190+
assertNotNull(ext.extendedEndTime)
191+
}
192+
193+
@Test
194+
fun `getExtendedEndTime returns the end even when the span still reports unfinished`() {
195+
// Reproduces the waitForChildren reentrancy: finishing the extended span completes the
196+
// transaction and runs the event processor within finishExtendedAppStart(), before the span's
197+
// isFinished() flips. getExtendedEndTime() must not depend on the span's state.
180198
val ext = extension(windowOpen = true)
181-
val finishDate = SentryNanotimeDate()
182199
val span = mock<ISpan>()
183-
whenever(span.isFinished).thenReturn(true)
184-
whenever(span.status).thenReturn(SpanStatus.OK)
185-
whenever(span.finishDate).thenReturn(finishDate)
200+
whenever(span.isFinished).thenReturn(false)
186201
ext.registerHandOver(span = span)
187202
ext.extendAppStart()
188-
assertSame(finishDate, ext.extendedEndTime)
203+
ext.finishExtendedAppStart()
204+
assertNotNull(ext.extendedEndTime)
189205
}
190206

191207
@Test

0 commit comments

Comments
 (0)