Skip to content

Commit e2c3f97

Browse files
feat(android): Set app start vitals on standalone app start children (#6005)
* feat(android): Set app start vitals on standalone children Copy app.vitals.start.screen and app.vitals.start.type onto every span under standalone app.start, including user work under the extended span, so they can be grouped with the root. Co-authored-by: Cursor <cursoragent@cursor.com> * changelog Co-authored-by: Cursor <cursoragent@cursor.com> * test(android): Inline SentrySpan constructors in app start vitals test Co-authored-by: Cursor <cursoragent@cursor.com> * build(android): Drop duplicate Truth test dependency Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2557f1a commit e2c3f97

4 files changed

Lines changed: 167 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
### Features
1313

14+
- Set `app.vitals.start.screen` and `app.vitals.start.type` on standalone `app.start` children ([#6005](https://github.com/getsentry/sentry-java/pull/6005))
1415
- Add screenshot attachment button to the Android user feedback widget ([#5828](https://github.com/getsentry/sentry-java/pull/5828))
1516
- Users can now attach a screenshot when submitting feedback. Enabled by default; can be disabled via `SentryFeedbackOptions.setEnableAttachScreenshot(false)` or the `io.sentry.feedback.enable-attach-screenshot` manifest flag.
1617
- Requires the `androidx.activity` `>=1.8.2` dependency

‎sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public final class ActivityLifecycleIntegration
7373
static final long APP_START_TO_UI_LOAD_CONTINUATION_MAX_GAP_NANOS = TimeUnit.MINUTES.toNanos(1);
7474
private static final String TRACE_ORIGIN = "auto.ui.activity";
7575
static final String APP_START_SCREEN_DATA = "app.vitals.start.screen";
76+
static final String APP_START_TYPE_DATA = "app.vitals.start.type";
7677
static final String APP_START_REASON_DATA = "app.vitals.start.reason";
7778
static final String APP_START_TRACE_ORIGIN = "auto.app.start";
7879

‎sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD;
44
import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_SCREEN_DATA;
5+
import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_TYPE_DATA;
56
import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM;
67
import static io.sentry.android.core.ActivityLifecycleIntegration.STANDALONE_APP_START_OP;
78
import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP;
@@ -162,6 +163,10 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
162163
? "cold"
163164
: "warm";
164165
appContext.setStartType(appStartType);
166+
167+
if (isStandaloneAppStartTxn) {
168+
setAppStartVitals(transaction, appStartType);
169+
}
165170
}
166171

167172
setContributingFlags(transaction);
@@ -186,6 +191,29 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
186191
}
187192
}
188193

194+
private static void setAppStartVitals(
195+
final @NotNull SentryTransaction transaction, final @NotNull String appStartType) {
196+
final @Nullable SpanContext traceContext = transaction.getContexts().getTrace();
197+
if (traceContext == null) {
198+
return;
199+
}
200+
201+
traceContext.setData(APP_START_TYPE_DATA, appStartType);
202+
final @Nullable Object screen = traceContext.getData().get(APP_START_SCREEN_DATA);
203+
204+
for (final @NotNull SentrySpan span : transaction.getSpans()) {
205+
@Nullable Map<String, Object> data = span.getData();
206+
if (data == null) {
207+
data = new ConcurrentHashMap<>();
208+
span.setData(data);
209+
}
210+
data.put(APP_START_TYPE_DATA, appStartType);
211+
if (screen != null) {
212+
data.put(APP_START_SCREEN_DATA, screen);
213+
}
214+
}
215+
}
216+
189217
private void setContributingFlags(SentryTransaction transaction) {
190218

191219
@Nullable SentrySpan ttidSpan = null;

‎sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.sentry.android.core
22

33
import android.content.ContentProvider
44
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import com.google.common.truth.Truth.assertThat
56
import io.sentry.Hint
67
import io.sentry.IScopes
78
import io.sentry.ISpan
@@ -16,7 +17,9 @@ import io.sentry.SpanStatus
1617
import io.sentry.TracesSamplingDecision
1718
import io.sentry.TransactionContext
1819
import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD
20+
import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_EXTENDED_OP
1921
import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_SCREEN_DATA
22+
import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_TYPE_DATA
2023
import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM
2124
import io.sentry.android.core.ActivityLifecycleIntegration.STANDALONE_APP_START_OP
2225
import io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP
@@ -172,6 +175,140 @@ class PerformanceAndroidEventProcessorTest {
172175
}
173176
}
174177

178+
@Test
179+
fun `foreground standalone app start sets screen and type on root and breakdown children`() {
180+
val sut = fixture.getSut(enablePerformanceV2 = true)
181+
AppStartMetrics.getInstance().apply {
182+
appStartType = AppStartType.COLD
183+
isAppLaunchedInForeground = true
184+
classLoadedUptimeMs = 50
185+
appStartTimeSpan.apply {
186+
setStartedAt(1)
187+
setStoppedAt(100)
188+
}
189+
applicationOnCreateTimeSpan.apply {
190+
setStartedAt(10)
191+
description = "com.example.App.onCreate"
192+
setStoppedAt(42)
193+
}
194+
}
195+
196+
var tr = createStandaloneAppStartTransaction(appStartScreen = "Activity")
197+
tr = sut.process(tr, Hint())
198+
199+
assertThat(tr.contexts.trace!!.data[APP_START_SCREEN_DATA]).isEqualTo("Activity")
200+
assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isEqualTo("cold")
201+
assertThat(tr.spans).isNotEmpty()
202+
for (span in tr.spans) {
203+
assertThat(span.data?.get(APP_START_SCREEN_DATA)).isEqualTo("Activity")
204+
assertThat(span.data?.get(APP_START_TYPE_DATA)).isEqualTo("cold")
205+
}
206+
}
207+
208+
@Test
209+
fun `standalone app start sets screen and type on user spans under the extended span`() {
210+
val sut = fixture.getSut()
211+
AppStartMetrics.getInstance().apply {
212+
appStartType = AppStartType.COLD
213+
isAppLaunchedInForeground = true
214+
}
215+
216+
var tr = createStandaloneAppStartTransaction(appStartScreen = "Activity")
217+
val traceId = tr.contexts.trace!!.traceId
218+
val extendedSpanId = SpanId()
219+
val childSpanId = SpanId()
220+
tr.spans.add(
221+
SentrySpan(
222+
0.0,
223+
1.0,
224+
traceId,
225+
extendedSpanId,
226+
tr.contexts.trace!!.spanId,
227+
APP_START_EXTENDED_OP,
228+
APP_START_EXTENDED_OP,
229+
SpanStatus.OK,
230+
null,
231+
emptyMap(),
232+
emptyMap(),
233+
null,
234+
)
235+
)
236+
tr.spans.add(
237+
SentrySpan(
238+
0.0,
239+
1.0,
240+
traceId,
241+
childSpanId,
242+
extendedSpanId,
243+
"user.work",
244+
"user.work",
245+
SpanStatus.OK,
246+
null,
247+
emptyMap(),
248+
emptyMap(),
249+
null,
250+
)
251+
)
252+
tr.spans.add(
253+
SentrySpan(
254+
0.0,
255+
1.0,
256+
traceId,
257+
SpanId(),
258+
childSpanId,
259+
"user.work.child",
260+
"user.work.child",
261+
SpanStatus.OK,
262+
null,
263+
emptyMap(),
264+
emptyMap(),
265+
null,
266+
)
267+
)
268+
269+
tr = sut.process(tr, Hint())
270+
271+
assertThat(tr.contexts.trace!!.data[APP_START_SCREEN_DATA]).isEqualTo("Activity")
272+
assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isEqualTo("cold")
273+
assertThat(tr.spans).hasSize(3)
274+
assertThat(tr.spans.map { it.data?.get(APP_START_SCREEN_DATA) })
275+
.containsExactly("Activity", "Activity", "Activity")
276+
assertThat(tr.spans.map { it.data?.get(APP_START_TYPE_DATA) })
277+
.containsExactly("cold", "cold", "cold")
278+
}
279+
280+
@Test
281+
fun `headless standalone app start sets type but not screen on children`() {
282+
val sut = fixture.getSut(enablePerformanceV2 = true)
283+
setStandaloneColdAppStartMetrics(withApplicationOnCreate = true)
284+
285+
var tr = createStandaloneAppStartTransaction()
286+
tr = sut.process(tr, Hint())
287+
288+
assertThat(tr.contexts.trace!!.data[APP_START_SCREEN_DATA]).isNull()
289+
assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isEqualTo("cold")
290+
assertThat(tr.spans).isNotEmpty()
291+
for (span in tr.spans) {
292+
assertThat(span.data?.get(APP_START_SCREEN_DATA)).isNull()
293+
assertThat(span.data?.get(APP_START_TYPE_DATA)).isEqualTo("cold")
294+
}
295+
}
296+
297+
@Test
298+
fun `ui load attached app start child does not get standalone vitals attributes`() {
299+
setAppStart(fixture.options, coldStart = true)
300+
301+
val sut = fixture.getSut(enablePerformanceV2 = true)
302+
var tr = createUiLoadTransactionWithAppStartChildSpan()
303+
tr = sut.process(tr, Hint())
304+
305+
assertThat(tr.contexts.trace!!.data[APP_START_SCREEN_DATA]).isNull()
306+
assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isNull()
307+
val appStartSpan = tr.spans.single { it.op == APP_START_COLD }
308+
assertThat(appStartSpan.data?.get(APP_START_SCREEN_DATA)).isNull()
309+
assertThat(appStartSpan.data?.get(APP_START_TYPE_DATA)).isNull()
310+
}
311+
175312
@Test
176313
fun `foreground standalone app start measurement uses foreground fallback time span`() {
177314
val sut = fixture.getSut(enablePerformanceV2 = false)

0 commit comments

Comments
 (0)