Skip to content

Commit d2c16a2

Browse files
buenaflorclaude
andcommitted
fix(extend-app-start): Read the extended span finish date, not its finished flag
getExtendedEndTime() gated on 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 whenever the extension finished after the first frame. Read getFinishDate() (set before the finish callback) instead, which also keeps the extended end controllable in tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ec4da6c commit d2c16a2

2 files changed

Lines changed: 21 additions & 1 deletion

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,17 @@ public void finishTransaction(final @NotNull SentryDate endTimestamp) {
117117
public @Nullable SentryDate getExtendedEndTime() {
118118
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
119119
final @Nullable ISpan span = extendedSpan;
120-
if (span == null || !span.isFinished()) {
120+
if (span == null) {
121121
return null;
122122
}
123123
// A deadline timeout would report an artificially inflated duration; suppress the vital
124124
// instead.
125125
if (span.getStatus() == SpanStatus.DEADLINE_EXCEEDED) {
126126
return null;
127127
}
128+
// Read the finish date, not isFinished(): finishing the extended span completes the
129+
// waitForChildren transaction and runs the event processor re-entrantly before the span's
130+
// finished flag is set, but the finish timestamp is already in place. Null until finished.
128131
return span.getFinishDate();
129132
}
130133
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,23 @@ class AppStartExtensionTest {
188188
assertSame(finishDate, ext.extendedEndTime)
189189
}
190190

191+
@Test
192+
fun `getExtendedEndTime returns the finish date even when the span still reports unfinished`() {
193+
// Reproduces the waitForChildren reentrancy: finishing the extended span completes the
194+
// transaction and runs the event processor before the span's isFinished() flips, while the
195+
// finish timestamp is already set. getExtendedEndTime() must read the finish date, not the
196+
// flag.
197+
val ext = extension(windowOpen = true)
198+
val finishDate = SentryNanotimeDate()
199+
val span = mock<ISpan>()
200+
whenever(span.isFinished).thenReturn(false)
201+
whenever(span.status).thenReturn(SpanStatus.OK)
202+
whenever(span.finishDate).thenReturn(finishDate)
203+
ext.registerHandOver(span = span)
204+
ext.extendAppStart()
205+
assertSame(finishDate, ext.extendedEndTime)
206+
}
207+
191208
@Test
192209
fun `clear clears the extension state`() {
193210
val ext = extension(windowOpen = true)

0 commit comments

Comments
 (0)