Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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<String, Object> data = span.getData();
if (data == null) {
data = new ConcurrentHashMap<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're iterating through all the transaction's spans, is it possible that another thread can also be modifying these spans? specifically the data? If so we have a race condition here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops, sry I just merged it, let me double check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as I can tell it's fine

SentryTransaction.spans is a private ArrayList built once and never leaves the capture pipeline, so nothing mutates it while we iterate. The data map is shared with the live Span, but it's a ConcurrentHashMap and we only put into it

the same behaviour happens below in setContributingFlags

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading