Skip to content

Commit 707e582

Browse files
committed
Support creating custom spans and attributes in the startup trace
1 parent 1ffb976 commit 707e582

File tree

10 files changed

+208
-2
lines changed

10 files changed

+208
-2
lines changed

embrace-android-api/api/embrace-android-api.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,15 @@ public abstract interface class io/embrace/android/embracesdk/internal/api/Instr
5656
public abstract fun addAttributeToLoadTrace (Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)V
5757
public abstract fun addChildSpanToLoadTrace (Landroid/app/Activity;Ljava/lang/String;JJ)V
5858
public abstract fun addChildSpanToLoadTrace (Landroid/app/Activity;Ljava/lang/String;JJLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;)V
59+
public abstract fun addStartupChildSpan (Ljava/lang/String;JJ)V
60+
public abstract fun addStartupChildSpan (Ljava/lang/String;JJLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;)V
61+
public abstract fun addStartupTraceAttribute (Ljava/lang/String;Ljava/lang/String;)V
62+
public abstract fun getSdkCurrentTimeMs ()J
5963
}
6064

6165
public final class io/embrace/android/embracesdk/internal/api/InstrumentationApi$DefaultImpls {
6266
public static fun addChildSpanToLoadTrace (Lio/embrace/android/embracesdk/internal/api/InstrumentationApi;Landroid/app/Activity;Ljava/lang/String;JJ)V
67+
public static fun addStartupChildSpan (Lio/embrace/android/embracesdk/internal/api/InstrumentationApi;Ljava/lang/String;JJ)V
6368
}
6469

6570
public abstract interface class io/embrace/android/embracesdk/internal/api/InternalWebViewApi {
@@ -101,6 +106,7 @@ public abstract interface class io/embrace/android/embracesdk/internal/api/SdkAp
101106

102107
public final class io/embrace/android/embracesdk/internal/api/SdkApi$DefaultImpls {
103108
public static fun addChildSpanToLoadTrace (Lio/embrace/android/embracesdk/internal/api/SdkApi;Landroid/app/Activity;Ljava/lang/String;JJ)V
109+
public static fun addStartupChildSpan (Lio/embrace/android/embracesdk/internal/api/SdkApi;Ljava/lang/String;JJ)V
104110
public static fun createSpan (Lio/embrace/android/embracesdk/internal/api/SdkApi;Ljava/lang/String;Lio/embrace/android/embracesdk/spans/AutoTerminationMode;)Lio/embrace/android/embracesdk/spans/EmbraceSpan;
105111
public static fun recordCompletedSpan (Lio/embrace/android/embracesdk/internal/api/SdkApi;Ljava/lang/String;JJ)Z
106112
public static fun recordCompletedSpan (Lio/embrace/android/embracesdk/internal/api/SdkApi;Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/EmbraceSpan;)Z

embrace-android-api/src/main/kotlin/io/embrace/android/embracesdk/internal/api/InstrumentationApi.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ public interface InstrumentationApi {
1717
*/
1818
public fun activityLoaded(activity: Activity)
1919

20+
/**
21+
* Return the timestamp of the SDK clock. This value should be used when creating spans with specific start and
22+
* end times so that
23+
*/
24+
public fun getSdkCurrentTimeMs(): Long
25+
2026
/**
2127
* Add an attribute to the trace generated by the loading of the given [Activity]
2228
*/
@@ -53,4 +59,38 @@ public interface InstrumentationApi {
5359
events: List<EmbraceSpanEvent>,
5460
errorCode: ErrorCode?,
5561
)
62+
63+
/**
64+
* Add an attribute to the app startup trace
65+
*/
66+
public fun addStartupTraceAttribute(key: String, value: String)
67+
68+
/**
69+
* Add a successfully completed child span to the app startup trace
70+
*/
71+
public fun addStartupChildSpan(
72+
name: String,
73+
startTimeMs: Long,
74+
endTimeMs: Long,
75+
): Unit = addStartupChildSpan(
76+
name = name,
77+
startTimeMs = startTimeMs,
78+
endTimeMs = endTimeMs,
79+
attributes = emptyMap(),
80+
events = emptyList(),
81+
errorCode = null,
82+
)
83+
84+
/**
85+
* Add a completed child span to the app startup trace with the given attributes and span events.
86+
* Specify an [ErrorCode] if the span didn't complete successfully.
87+
*/
88+
public fun addStartupChildSpan(
89+
name: String,
90+
startTimeMs: Long,
91+
endTimeMs: Long,
92+
attributes: Map<String, String>,
93+
events: List<EmbraceSpanEvent>,
94+
errorCode: ErrorCode?,
95+
)
5696
}

embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupDataCollector.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,9 @@ interface AppStartupDataCollector {
5959
* Set an arbitrary time interval during startup that is of note
6060
*/
6161
fun addTrackedInterval(name: String, startTimeMs: Long, endTimeMs: Long)
62+
63+
/**
64+
* Add custom attribute to the root span of the trace logged for app startup
65+
*/
66+
fun addAttribute(key: String, value: String)
6267
}

embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitter.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import io.embrace.android.embracesdk.internal.utils.VersionChecker
1212
import io.embrace.android.embracesdk.internal.worker.BackgroundWorker
1313
import io.embrace.android.embracesdk.spans.EmbraceSpan
1414
import io.opentelemetry.sdk.common.Clock
15+
import java.util.concurrent.ConcurrentHashMap
1516
import java.util.concurrent.ConcurrentLinkedQueue
1617
import java.util.concurrent.atomic.AtomicBoolean
1718

@@ -46,6 +47,7 @@ internal class AppStartupTraceEmitter(
4647
private val processCreateRequestedMs: Long?
4748
private val processCreatedMs: Long?
4849
private val additionalTrackedIntervals = ConcurrentLinkedQueue<TrackedInterval>()
50+
private val customAttributes: MutableMap<String, String> = ConcurrentHashMap()
4951

5052
init {
5153
val timestampAtDeviceStart = nowMs() - clock.nanoTime().nanosToMillis()
@@ -159,6 +161,10 @@ internal class AppStartupTraceEmitter(
159161
)
160162
}
161163

164+
override fun addAttribute(key: String, value: String) {
165+
customAttributes[key] = value
166+
}
167+
162168
/**
163169
* Called when app startup is considered complete, i.e. the data can be used and any additional updates can be ignored
164170
*/
@@ -250,6 +256,7 @@ internal class AppStartupTraceEmitter(
250256
parent = startupTrace,
251257
startTimeMs = trackedInterval.startTimeMs,
252258
endTimeMs = trackedInterval.endTimeMs,
259+
internal = false,
253260
)
254261
}
255262
} while (additionalTrackedIntervals.isNotEmpty())
@@ -388,6 +395,7 @@ internal class AppStartupTraceEmitter(
388395
private fun nowMs(): Long = clock.now().nanosToMillis()
389396

390397
private fun PersistableEmbraceSpan.addTraceMetadata() {
398+
addCustomAttributes()
391399
processCreateDelay()?.let { delay ->
392400
addAttribute("process-create-delay-ms", delay.toString())
393401
}
@@ -417,6 +425,12 @@ internal class AppStartupTraceEmitter(
417425
}
418426
}
419427

428+
private fun PersistableEmbraceSpan.addCustomAttributes() {
429+
customAttributes.forEach {
430+
addAttribute(it.key, it.value)
431+
}
432+
}
433+
420434
private data class TrackedInterval(
421435
val name: String,
422436
val startTimeMs: Long,

embrace-android-features/src/test/java/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitterTest.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,23 +223,27 @@ internal class AppStartupTraceEmitterTest {
223223
private fun verifyColdStartWithRender(processCreateDelayMs: Long? = null) {
224224
clock.tick(100L)
225225
appStartupTraceEmitter.applicationInitStart()
226+
val customSpanStartMs = clock.now()
226227
clock.tick(15L)
227228
val (sdkInitStart, sdkInitEnd) = startSdk()
229+
appStartupTraceEmitter.addAttribute("custom-attribute", "true")
228230
appStartupTraceEmitter.applicationInitEnd()
229231
val applicationInitEnd = clock.now()
230232
clock.tick(50L)
233+
appStartupTraceEmitter.addTrackedInterval("custom-span", customSpanStartMs, applicationInitEnd)
231234

232235
val activityCreateEvents = createStartupActivity()
233236
val traceEnd = startupActivityRender().second
234237

235-
assertEquals(7, spanSink.completedSpans().size)
238+
assertEquals(8, spanSink.completedSpans().size)
236239
val spanMap = spanSink.completedSpans().associateBy { it.name }
237240
val trace = checkNotNull(spanMap["emb-cold-time-to-initial-display"])
238241
val processInit = checkNotNull(spanMap["emb-process-init"])
239242
val embraceInit = checkNotNull(spanMap["emb-embrace-init"])
240243
val activityInitDelay = checkNotNull(spanMap["emb-activity-init-gap"])
241244
val activityCreate = checkNotNull(spanMap["emb-activity-create"])
242245
val firstRender = checkNotNull(spanMap["emb-first-frame-render"])
246+
val customSpan = checkNotNull(spanMap["custom-span"])
243247

244248
val startupActivityStart = checkNotNull(activityCreateEvents.create)
245249
val startupActivityEnd = checkNotNull((activityCreateEvents.finished))
@@ -252,12 +256,14 @@ internal class AppStartupTraceEmitterTest {
252256
expectedActivityPreCreatedMs = activityCreateEvents.preCreate,
253257
expectedActivityPostCreatedMs = activityCreateEvents.postCreate,
254258
expectedFirstActivityLifecycleEventMs = activityCreateEvents.firstEvent,
259+
expectedCustomAttributes = mapOf("custom-attribute" to "true")
255260
)
256261
assertChildSpan(processInit, DEFAULT_FAKE_CURRENT_TIME, applicationInitEnd)
257262
assertChildSpan(embraceInit, sdkInitStart, sdkInitEnd)
258263
assertChildSpan(activityInitDelay, applicationInitEnd, startupActivityStart)
259264
assertChildSpan(activityCreate, startupActivityStart, startupActivityEnd)
260265
assertChildSpan(firstRender, startupActivityEnd, traceEnd)
266+
assertChildSpan(customSpan, customSpanStartMs, applicationInitEnd)
261267
assertEquals(0, logger.internalErrorMessages.size)
262268
}
263269

@@ -542,6 +548,7 @@ internal class AppStartupTraceEmitterTest {
542548
expectedActivityPreCreatedMs: Long? = null,
543549
expectedActivityPostCreatedMs: Long? = null,
544550
expectedFirstActivityLifecycleEventMs: Long? = null,
551+
expectedCustomAttributes: Map<String, String> = emptyMap(),
545552
) {
546553
val trace = input.toNewPayload()
547554
assertEquals(expectedStartTimeMs, trace.startTimeNanos?.nanosToMillis())
@@ -565,6 +572,10 @@ internal class AppStartupTraceEmitterTest {
565572
assertEquals("false", attrs.findAttributeValue("embrace-init-in-foreground"))
566573
assertEquals("main", attrs.findAttributeValue("embrace-init-thread-name"))
567574
assertEquals(1, dataCollectionCompletedCallbackInvokedCount)
575+
576+
expectedCustomAttributes.forEach { entry ->
577+
assertEquals(entry.value, trace.attributes?.findAttributeValue(entry.key))
578+
}
568579
}
569580

570581
private fun assertChildSpan(span: EmbraceSpanData, expectedStartTimeNanos: Long, expectedEndTimeNanos: Long) {

embrace-android-sdk/api/embrace-android-sdk.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/em
88
public fun addLogRecordExporter (Lio/opentelemetry/sdk/logs/export/LogRecordExporter;)V
99
public fun addSessionProperty (Ljava/lang/String;Ljava/lang/String;Z)Z
1010
public fun addSpanExporter (Lio/opentelemetry/sdk/trace/export/SpanExporter;)V
11+
public fun addStartupChildSpan (Ljava/lang/String;JJ)V
12+
public fun addStartupChildSpan (Ljava/lang/String;JJLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;)V
13+
public fun addStartupTraceAttribute (Ljava/lang/String;Ljava/lang/String;)V
1114
public fun addUserPersona (Ljava/lang/String;)V
1215
public fun clearAllUserPersonas ()V
1316
public fun clearUserEmail ()V
@@ -26,6 +29,7 @@ public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/em
2629
public static final fun getInstance ()Lio/embrace/android/embracesdk/Embrace;
2730
public fun getLastRunEndState ()Lio/embrace/android/embracesdk/LastRunEndState;
2831
public fun getOpenTelemetry ()Lio/opentelemetry/api/OpenTelemetry;
32+
public fun getSdkCurrentTimeMs ()J
2933
public fun getSpan (Ljava/lang/String;)Lio/embrace/android/embracesdk/spans/EmbraceSpan;
3034
public fun isStarted ()Z
3135
public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;)V
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.embrace.android.embracesdk.testcases
2+
3+
import android.os.Build
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import io.embrace.android.embracesdk.assertions.findSpansOfType
6+
import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig
7+
import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
8+
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
9+
import io.embrace.android.embracesdk.internal.spans.findAttributeValue
10+
import io.embrace.android.embracesdk.testframework.IntegrationTestRule
11+
import org.junit.Assert.assertEquals
12+
import org.junit.Assert.assertTrue
13+
import org.junit.Rule
14+
import org.junit.Test
15+
import org.junit.runner.RunWith
16+
import org.robolectric.annotation.Config
17+
18+
@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
19+
@RunWith(AndroidJUnit4::class)
20+
internal class AppStartupTraceTest {
21+
@Rule
22+
@JvmField
23+
val testRule: IntegrationTestRule = IntegrationTestRule()
24+
25+
@Test
26+
fun `startup spans recorded in foreground session when background activity is enabled`() {
27+
testRule.runTest(
28+
instrumentedConfig = FakeInstrumentedConfig(
29+
enabledFeatures = FakeEnabledFeatureConfig(
30+
bgActivityCapture = true
31+
)
32+
),
33+
testCaseAction = {
34+
val customStartTimeMs = clock.now()
35+
val customEndTimeMs = clock.tick(100L)
36+
embrace.addStartupChildSpan("custom-span", customStartTimeMs, customEndTimeMs)
37+
embrace.addStartupTraceAttribute("custom-attribute", "yes")
38+
simulateOpeningActivities(
39+
addStartupActivity = false,
40+
startInBackground = true
41+
)
42+
},
43+
assertAction = {
44+
with(getSingleSessionEnvelope()) {
45+
val spans = findSpansOfType(EmbType.Performance.Default).associateBy { it.name }
46+
assertTrue(spans.isNotEmpty())
47+
with(checkNotNull(spans["emb-cold-time-to-initial-display"])) {
48+
assertEquals("yes", attributes?.findAttributeValue("custom-attribute"))
49+
}
50+
assertTrue(spans.containsKey("emb-embrace-init"))
51+
assertTrue(spans.containsKey("custom-span"))
52+
assertTrue(spans.containsKey("emb-activity-create"))
53+
assertTrue(spans.containsKey("emb-activity-resume"))
54+
}
55+
}
56+
)
57+
}
58+
}

embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,8 @@ public class Embrace private constructor(
428428
impl.activityLoaded(activity)
429429
}
430430

431+
override fun getSdkCurrentTimeMs(): Long = impl.getSdkCurrentTimeMs()
432+
431433
override fun addAttributeToLoadTrace(activity: Activity, key: String, value: String) {
432434
impl.addAttributeToLoadTrace(activity, key, value)
433435
}
@@ -451,4 +453,26 @@ public class Embrace private constructor(
451453
errorCode = errorCode
452454
)
453455
}
456+
457+
override fun addStartupTraceAttribute(key: String, value: String) {
458+
impl.addStartupTraceAttribute(key, value)
459+
}
460+
461+
override fun addStartupChildSpan(
462+
name: String,
463+
startTimeMs: Long,
464+
endTimeMs: Long,
465+
attributes: Map<String, String>,
466+
events: List<EmbraceSpanEvent>,
467+
errorCode: ErrorCode?,
468+
) {
469+
impl.addStartupChildSpan(
470+
name = name,
471+
startTimeMs = startTimeMs,
472+
endTimeMs = endTimeMs,
473+
attributes = attributes,
474+
events = events,
475+
errorCode = errorCode
476+
)
477+
}
454478
}

embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/api/delegate/InstrumentationApiDelegate.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,18 @@ internal class InstrumentationApiDelegate(
1818
private val uiLoadTraceEmitter by embraceImplInject(sdkCallChecker) {
1919
bootstrapper.dataCaptureServiceModule.uiLoadDataListener
2020
}
21+
private val appStartupDataCollector by embraceImplInject(sdkCallChecker) {
22+
bootstrapper.dataCaptureServiceModule.appStartupDataCollector
23+
}
2124

2225
override fun activityLoaded(activity: Activity) {
2326
if (sdkCallChecker.check("activity_fully_loaded")) {
2427
uiLoadTraceEmitter?.complete(traceInstanceId(activity), clock.now())
2528
}
2629
}
2730

31+
override fun getSdkCurrentTimeMs(): Long = clock.now()
32+
2833
override fun addAttributeToLoadTrace(activity: Activity, key: String, value: String) {
2934
if (sdkCallChecker.check("add_attribute_to_load_trace")) {
3035
uiLoadTraceEmitter?.addAttribute(traceInstanceId(activity), key, value)
@@ -52,4 +57,27 @@ internal class InstrumentationApiDelegate(
5257
)
5358
}
5459
}
60+
61+
override fun addStartupTraceAttribute(key: String, value: String) {
62+
if (sdkCallChecker.check("add_attribute_to_app_startup_trace")) {
63+
appStartupDataCollector?.addAttribute(key, value)
64+
}
65+
}
66+
67+
override fun addStartupChildSpan(
68+
name: String,
69+
startTimeMs: Long,
70+
endTimeMs: Long,
71+
attributes: Map<String, String>,
72+
events: List<EmbraceSpanEvent>,
73+
errorCode: ErrorCode?,
74+
) {
75+
if (sdkCallChecker.check("add_child_span_to_app_startup_trace")) {
76+
appStartupDataCollector?.addTrackedInterval(
77+
name = name,
78+
startTimeMs = startTimeMs,
79+
endTimeMs = endTimeMs
80+
)
81+
}
82+
}
5583
}

0 commit comments

Comments
 (0)