Skip to content

Commit

Permalink
Add ttid/ttfd contribution flags (#3386)
Browse files Browse the repository at this point in the history
* Add ttid/ttfd contribution flags

* Update Changelog

* Add ui namespace

* Update according to develop docs

* consider main thread info for contribution flags
  • Loading branch information
markushi authored May 3, 2024
1 parent 201048a commit 8045023
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add start_type to app context ([#3379](https://github.com/getsentry/sentry-java/pull/3379))
- Add ttid/ttfd contribution flags ([#3386](https://github.com/getsentry/sentry-java/pull/3386))

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
appContext.setStartType(appStartType);
}

setContributingFlags(transaction);

final SentryId eventId = transaction.getEventId();
final SpanContext spanContext = transaction.getContexts().getTrace();

Expand All @@ -135,6 +137,71 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
return transaction;
}

private void setContributingFlags(SentryTransaction transaction) {

@Nullable SentrySpan ttidSpan = null;
@Nullable SentrySpan ttfdSpan = null;
for (final @NotNull SentrySpan span : transaction.getSpans()) {
if (ActivityLifecycleIntegration.TTID_OP.equals(span.getOp())) {
ttidSpan = span;
} else if (ActivityLifecycleIntegration.TTFD_OP.equals(span.getOp())) {
ttfdSpan = span;
}
// once both are found we can early exit
if (ttidSpan != null && ttfdSpan != null) {
break;
}
}

if (ttidSpan == null && ttfdSpan == null) {
return;
}

for (final @NotNull SentrySpan span : transaction.getSpans()) {
// as ttid and ttfd spans are artificially created, we don't want to set the flags on them
if (span == ttidSpan || span == ttfdSpan) {
continue;
}

// let's assume main thread, unless it's set differently
boolean spanOnMainThread = true;
final @Nullable Map<String, Object> spanData = span.getData();
if (spanData != null) {
final @Nullable Object threadName = spanData.get(SpanDataConvention.THREAD_NAME);
spanOnMainThread = threadName == null || "main".equals(threadName);
}

// for ttid, only main thread spans are relevant
final boolean withinTtid =
(ttidSpan != null)
&& isTimestampWithinSpan(span.getStartTimestamp(), ttidSpan)
&& spanOnMainThread;

final boolean withinTtfd =
(ttfdSpan != null) && isTimestampWithinSpan(span.getStartTimestamp(), ttfdSpan);

if (withinTtid || withinTtfd) {
@Nullable Map<String, Object> data = span.getData();
if (data == null) {
data = new ConcurrentHashMap<>();
span.setData(data);
}
if (withinTtid) {
data.put(SpanDataConvention.CONTRIBUTES_TTID, true);
}
if (withinTtfd) {
data.put(SpanDataConvention.CONTRIBUTES_TTFD, true);
}
}
}
}

private static boolean isTimestampWithinSpan(
final double timestamp, final @NotNull SentrySpan target) {
return timestamp >= target.getStartTimestamp()
&& (target.getTimestamp() == null || timestamp <= target.getTimestamp());
}

private boolean hasAppStartSpan(final @NotNull SentryTransaction txn) {
final @NotNull List<SentrySpan> spans = txn.getSpans();
for (final @NotNull SentrySpan span : spans) {
Expand Down Expand Up @@ -253,6 +320,9 @@ private static SentrySpan timeSpanToSentrySpan(
defaultSpanData.put(SpanDataConvention.THREAD_ID, Looper.getMainLooper().getThread().getId());
defaultSpanData.put(SpanDataConvention.THREAD_NAME, "main");

defaultSpanData.put(SpanDataConvention.CONTRIBUTES_TTID, true);
defaultSpanData.put(SpanDataConvention.CONTRIBUTES_TTFD, true);

return new SentrySpan(
span.getStartTimestampSecs(),
span.getProjectedStopTimestampSecs(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.sentry.IHub
import io.sentry.MeasurementUnit
import io.sentry.SentryTracer
import io.sentry.SpanContext
import io.sentry.SpanDataConvention
import io.sentry.SpanId
import io.sentry.SpanStatus
import io.sentry.TracesSamplingDecision
Expand Down Expand Up @@ -519,6 +520,282 @@ class PerformanceAndroidEventProcessorTest {
)
}

@Test
fun `adds ttid and ttfd contributing span data`() {
val sut = fixture.getSut()

val context = TransactionContext("Activity", UI_LOAD_OP)
val tracer = SentryTracer(context, fixture.hub)
val tr = SentryTransaction(tracer)

// given a ttid from 0.0 -> 1.0
// and a ttfd from 0.0 -> 2.0
val ttid = SentrySpan(
0.0,
1.0,
tr.contexts.trace!!.traceId,
SpanId(),
null,
ActivityLifecycleIntegration.TTID_OP,
"App Start",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)

val ttfd = SentrySpan(
0.0,
2.0,
tr.contexts.trace!!.traceId,
SpanId(),
null,
ActivityLifecycleIntegration.TTFD_OP,
"App Start",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)
tr.spans.add(ttid)
tr.spans.add(ttfd)

// and 3 spans
// one from 0.0 -> 0.5
val ttidContrib = SentrySpan(
0.0,
0.5,
tr.contexts.trace!!.traceId,
SpanId(),
null,
"example.op",
"",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)

// and another from 1.5 -> 3.5
val ttfdContrib = SentrySpan(
1.5,
3.5,
tr.contexts.trace!!.traceId,
SpanId(),
null,
"example.op",
"",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)

// and another from 2.1 -> 2.2
val outsideSpan = SentrySpan(
2.1,
2.2,
tr.contexts.trace!!.traceId,
SpanId(),
null,
"example.op",
"",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
mutableMapOf<String, Any>(
"tag" to "value"
)
)

tr.spans.add(ttidContrib)
tr.spans.add(ttfdContrib)

// when the processor processes the txn
sut.process(tr, Hint())

// then the ttid/ttfd spans themselves should have no flags set
assertNull(ttid.data?.get(SpanDataConvention.CONTRIBUTES_TTID))
assertNull(ttid.data?.get(SpanDataConvention.CONTRIBUTES_TTFD))

assertNull(ttfd.data?.get(SpanDataConvention.CONTRIBUTES_TTID))
assertNull(ttfd.data?.get(SpanDataConvention.CONTRIBUTES_TTFD))

// then the first span should have ttid and ttfd contributing flags
assertTrue(ttidContrib.data?.get(SpanDataConvention.CONTRIBUTES_TTID) == true)
assertTrue(ttidContrib.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true)

// and the second one should contribute to ttfd only
assertNull(ttfdContrib.data?.get(SpanDataConvention.CONTRIBUTES_TTID))
assertTrue(ttfdContrib.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true)

// and the third span should have no flags attached, as it's outside ttid/ttfd
assertNull(outsideSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTID))
assertNull(outsideSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTFD))
}

@Test
fun `adds no ttid and ttfd contributing span data if txn contains no ttid or ttfd`() {
val sut = fixture.getSut()

val context = TransactionContext("Activity", UI_LOAD_OP)
val tracer = SentryTracer(context, fixture.hub)
val tr = SentryTransaction(tracer)

val span = SentrySpan(
0.0,
1.0,
tr.contexts.trace!!.traceId,
SpanId(),
null,
"example.op",
"",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)

tr.spans.add(span)

// when the processor processes the txn
sut.process(tr, Hint())

// the span should have no flags attached
assertNull(span.data?.get(SpanDataConvention.CONTRIBUTES_TTID))
assertNull(span.data?.get(SpanDataConvention.CONTRIBUTES_TTFD))
}

@Test
fun `sets ttid and ttfd contributing flags according to span threads`() {
val sut = fixture.getSut()

val context = TransactionContext("Activity", UI_LOAD_OP)
val tracer = SentryTracer(context, fixture.hub)
val tr = SentryTransaction(tracer)

// given a ttid from 0.0 -> 1.0
// and a ttfd from 0.0 -> 1.0
val ttid = SentrySpan(
0.0,
1.0,
tr.contexts.trace!!.traceId,
SpanId(),
null,
ActivityLifecycleIntegration.TTID_OP,
"App Start",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)

val ttfd = SentrySpan(
0.0,
1.0,
tr.contexts.trace!!.traceId,
SpanId(),
null,
ActivityLifecycleIntegration.TTFD_OP,
"App Start",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)
tr.spans.add(ttid)
tr.spans.add(ttfd)

// one span with no thread info
val noThreadSpan = SentrySpan(
0.0,
0.5,
tr.contexts.trace!!.traceId,
SpanId(),
null,
"example.op",
"",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
null
)

// one span on the main thread
val mainThreadSpan = SentrySpan(
0.0,
0.5,
tr.contexts.trace!!.traceId,
SpanId(),
null,
"example.op",
"",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
mutableMapOf<String, Any>(
"thread.name" to "main"
)
)

// and another one off the main thread
val backgroundThreadSpan = SentrySpan(
0.0,
0.5,
tr.contexts.trace!!.traceId,
SpanId(),
null,
"example.op",
"",
SpanStatus.OK,
null,
emptyMap(),
emptyMap(),
null,
mutableMapOf<String, Any>(
"thread.name" to "background"
)
)

tr.spans.add(noThreadSpan)
tr.spans.add(mainThreadSpan)
tr.spans.add(backgroundThreadSpan)

// when the processor processes the txn
sut.process(tr, Hint())

// then the span with no thread info + main thread span should contribute to ttid and ttfd
assertTrue(noThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTID) == true)
assertTrue(noThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true)

assertTrue(mainThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTID) == true)
assertTrue(mainThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true)

// and the background thread span only contributes to ttfd
assertNull(backgroundThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTID))
assertTrue(backgroundThreadSpan.data?.get(SpanDataConvention.CONTRIBUTES_TTFD) == true)
}

private fun setAppStart(options: SentryAndroidOptions, coldStart: Boolean = true) {
AppStartMetrics.getInstance().apply {
appStartType = when (coldStart) {
Expand Down
Loading

0 comments on commit 8045023

Please sign in to comment.