diff --git a/CHANGELOG.md b/CHANGELOG.md index 878238076f..b697db9add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- Set `app.vitals.start.screen` and `app.vitals.start.type` on standalone `app.start` children ([#6005](https://github.com/getsentry/sentry-java/pull/6005)) - Add screenshot attachment button to the Android user feedback widget ([#5828](https://github.com/getsentry/sentry-java/pull/5828)) - 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. - Requires the `androidx.activity` `>=1.8.2` dependency diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index f416df6a98..505bb84ea2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -73,6 +73,7 @@ public final class ActivityLifecycleIntegration static final long APP_START_TO_UI_LOAD_CONTINUATION_MAX_GAP_NANOS = TimeUnit.MINUTES.toNanos(1); private static final String TRACE_ORIGIN = "auto.ui.activity"; static final String APP_START_SCREEN_DATA = "app.vitals.start.screen"; + static final String APP_START_TYPE_DATA = "app.vitals.start.type"; static final String APP_START_REASON_DATA = "app.vitals.start.reason"; static final String APP_START_TRACE_ORIGIN = "auto.app.start"; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index d758470baf..1340ca6fad 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -2,6 +2,7 @@ import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD; import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_SCREEN_DATA; +import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_TYPE_DATA; import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM; import static io.sentry.android.core.ActivityLifecycleIntegration.STANDALONE_APP_START_OP; import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP; @@ -162,6 +163,10 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { ? "cold" : "warm"; appContext.setStartType(appStartType); + + if (isStandaloneAppStartTxn) { + setAppStartVitals(transaction, appStartType); + } } setContributingFlags(transaction); @@ -186,6 +191,29 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { } } + private static void setAppStartVitals( + final @NotNull SentryTransaction transaction, final @NotNull String appStartType) { + final @Nullable SpanContext traceContext = transaction.getContexts().getTrace(); + if (traceContext == null) { + return; + } + + traceContext.setData(APP_START_TYPE_DATA, appStartType); + final @Nullable Object screen = traceContext.getData().get(APP_START_SCREEN_DATA); + + for (final @NotNull SentrySpan span : transaction.getSpans()) { + @Nullable Map data = span.getData(); + if (data == null) { + data = new ConcurrentHashMap<>(); + span.setData(data); + } + data.put(APP_START_TYPE_DATA, appStartType); + if (screen != null) { + data.put(APP_START_SCREEN_DATA, screen); + } + } + } + private void setContributingFlags(SentryTransaction transaction) { @Nullable SentrySpan ttidSpan = null; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 1dc00f09f9..0f5c731aaf 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -2,6 +2,7 @@ package io.sentry.android.core import android.content.ContentProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import io.sentry.Hint import io.sentry.IScopes import io.sentry.ISpan @@ -16,7 +17,9 @@ import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD +import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_EXTENDED_OP import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_SCREEN_DATA +import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_TYPE_DATA import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM import io.sentry.android.core.ActivityLifecycleIntegration.STANDALONE_APP_START_OP import io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP @@ -172,6 +175,140 @@ class PerformanceAndroidEventProcessorTest { } } + @Test + fun `foreground standalone app start sets screen and type on root and breakdown children`() { + val sut = fixture.getSut(enablePerformanceV2 = true) + AppStartMetrics.getInstance().apply { + appStartType = AppStartType.COLD + isAppLaunchedInForeground = true + classLoadedUptimeMs = 50 + appStartTimeSpan.apply { + setStartedAt(1) + setStoppedAt(100) + } + applicationOnCreateTimeSpan.apply { + setStartedAt(10) + description = "com.example.App.onCreate" + setStoppedAt(42) + } + } + + var tr = createStandaloneAppStartTransaction(appStartScreen = "Activity") + tr = sut.process(tr, Hint()) + + assertThat(tr.contexts.trace!!.data[APP_START_SCREEN_DATA]).isEqualTo("Activity") + assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isEqualTo("cold") + assertThat(tr.spans).isNotEmpty() + for (span in tr.spans) { + assertThat(span.data?.get(APP_START_SCREEN_DATA)).isEqualTo("Activity") + assertThat(span.data?.get(APP_START_TYPE_DATA)).isEqualTo("cold") + } + } + + @Test + fun `standalone app start sets screen and type on user spans under the extended span`() { + val sut = fixture.getSut() + AppStartMetrics.getInstance().apply { + appStartType = AppStartType.COLD + isAppLaunchedInForeground = true + } + + var tr = createStandaloneAppStartTransaction(appStartScreen = "Activity") + val traceId = tr.contexts.trace!!.traceId + val extendedSpanId = SpanId() + val childSpanId = SpanId() + tr.spans.add( + SentrySpan( + 0.0, + 1.0, + traceId, + extendedSpanId, + tr.contexts.trace!!.spanId, + APP_START_EXTENDED_OP, + APP_START_EXTENDED_OP, + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + ) + ) + tr.spans.add( + SentrySpan( + 0.0, + 1.0, + traceId, + childSpanId, + extendedSpanId, + "user.work", + "user.work", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + ) + ) + tr.spans.add( + SentrySpan( + 0.0, + 1.0, + traceId, + SpanId(), + childSpanId, + "user.work.child", + "user.work.child", + SpanStatus.OK, + null, + emptyMap(), + emptyMap(), + null, + ) + ) + + tr = sut.process(tr, Hint()) + + assertThat(tr.contexts.trace!!.data[APP_START_SCREEN_DATA]).isEqualTo("Activity") + assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isEqualTo("cold") + assertThat(tr.spans).hasSize(3) + assertThat(tr.spans.map { it.data?.get(APP_START_SCREEN_DATA) }) + .containsExactly("Activity", "Activity", "Activity") + assertThat(tr.spans.map { it.data?.get(APP_START_TYPE_DATA) }) + .containsExactly("cold", "cold", "cold") + } + + @Test + fun `headless standalone app start sets type but not screen on children`() { + val sut = fixture.getSut(enablePerformanceV2 = true) + setStandaloneColdAppStartMetrics(withApplicationOnCreate = true) + + var tr = createStandaloneAppStartTransaction() + tr = sut.process(tr, Hint()) + + assertThat(tr.contexts.trace!!.data[APP_START_SCREEN_DATA]).isNull() + assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isEqualTo("cold") + assertThat(tr.spans).isNotEmpty() + for (span in tr.spans) { + assertThat(span.data?.get(APP_START_SCREEN_DATA)).isNull() + assertThat(span.data?.get(APP_START_TYPE_DATA)).isEqualTo("cold") + } + } + + @Test + fun `ui load attached app start child does not get standalone vitals attributes`() { + setAppStart(fixture.options, coldStart = true) + + val sut = fixture.getSut(enablePerformanceV2 = true) + var tr = createUiLoadTransactionWithAppStartChildSpan() + tr = sut.process(tr, Hint()) + + assertThat(tr.contexts.trace!!.data[APP_START_SCREEN_DATA]).isNull() + assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isNull() + val appStartSpan = tr.spans.single { it.op == APP_START_COLD } + assertThat(appStartSpan.data?.get(APP_START_SCREEN_DATA)).isNull() + assertThat(appStartSpan.data?.get(APP_START_TYPE_DATA)).isNull() + } + @Test fun `foreground standalone app start measurement uses foreground fallback time span`() { val sut = fixture.getSut(enablePerformanceV2 = false)