diff --git a/embrace-android-api/api/embrace-android-api.api b/embrace-android-api/api/embrace-android-api.api index f18759cba6..9d79655288 100644 --- a/embrace-android-api/api/embrace-android-api.api +++ b/embrace-android-api/api/embrace-android-api.api @@ -56,10 +56,15 @@ public abstract interface class io/embrace/android/embracesdk/internal/api/Instr public abstract fun addAttributeToLoadTrace (Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)V public abstract fun addChildSpanToLoadTrace (Landroid/app/Activity;Ljava/lang/String;JJ)V public abstract fun addChildSpanToLoadTrace (Landroid/app/Activity;Ljava/lang/String;JJLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;)V + public abstract fun addStartupChildSpan (Ljava/lang/String;JJ)V + public abstract fun addStartupChildSpan (Ljava/lang/String;JJLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;)V + public abstract fun addStartupTraceAttribute (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun getSdkCurrentTimeMs ()J } public final class io/embrace/android/embracesdk/internal/api/InstrumentationApi$DefaultImpls { public static fun addChildSpanToLoadTrace (Lio/embrace/android/embracesdk/internal/api/InstrumentationApi;Landroid/app/Activity;Ljava/lang/String;JJ)V + public static fun addStartupChildSpan (Lio/embrace/android/embracesdk/internal/api/InstrumentationApi;Ljava/lang/String;JJ)V } 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 public final class io/embrace/android/embracesdk/internal/api/SdkApi$DefaultImpls { public static fun addChildSpanToLoadTrace (Lio/embrace/android/embracesdk/internal/api/SdkApi;Landroid/app/Activity;Ljava/lang/String;JJ)V + public static fun addStartupChildSpan (Lio/embrace/android/embracesdk/internal/api/SdkApi;Ljava/lang/String;JJ)V 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; public static fun recordCompletedSpan (Lio/embrace/android/embracesdk/internal/api/SdkApi;Ljava/lang/String;JJ)Z public static fun recordCompletedSpan (Lio/embrace/android/embracesdk/internal/api/SdkApi;Ljava/lang/String;JJLio/embrace/android/embracesdk/spans/EmbraceSpan;)Z diff --git a/embrace-android-api/src/main/kotlin/io/embrace/android/embracesdk/internal/api/InstrumentationApi.kt b/embrace-android-api/src/main/kotlin/io/embrace/android/embracesdk/internal/api/InstrumentationApi.kt index e1d76888b9..35cd7aa6d7 100644 --- a/embrace-android-api/src/main/kotlin/io/embrace/android/embracesdk/internal/api/InstrumentationApi.kt +++ b/embrace-android-api/src/main/kotlin/io/embrace/android/embracesdk/internal/api/InstrumentationApi.kt @@ -17,6 +17,12 @@ public interface InstrumentationApi { */ public fun activityLoaded(activity: Activity) + /** + * Return the timestamp of the SDK clock. This value should be used when creating spans with specific start and + * end times so that + */ + public fun getSdkCurrentTimeMs(): Long + /** * Add an attribute to the trace generated by the loading of the given [Activity] */ @@ -53,4 +59,38 @@ public interface InstrumentationApi { events: List, errorCode: ErrorCode?, ) + + /** + * Add an attribute to the app startup trace + */ + public fun addStartupTraceAttribute(key: String, value: String) + + /** + * Add a successfully completed child span to the app startup trace + */ + public fun addStartupChildSpan( + name: String, + startTimeMs: Long, + endTimeMs: Long, + ): Unit = addStartupChildSpan( + name = name, + startTimeMs = startTimeMs, + endTimeMs = endTimeMs, + attributes = emptyMap(), + events = emptyList(), + errorCode = null, + ) + + /** + * Add a completed child span to the app startup trace with the given attributes and span events. + * Specify an [ErrorCode] if the span didn't complete successfully. + */ + public fun addStartupChildSpan( + name: String, + startTimeMs: Long, + endTimeMs: Long, + attributes: Map, + events: List, + errorCode: ErrorCode?, + ) } diff --git a/embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupDataCollector.kt b/embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupDataCollector.kt index 74e746ca4b..9ed2f111d4 100644 --- a/embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupDataCollector.kt +++ b/embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupDataCollector.kt @@ -59,4 +59,9 @@ interface AppStartupDataCollector { * Set an arbitrary time interval during startup that is of note */ fun addTrackedInterval(name: String, startTimeMs: Long, endTimeMs: Long) + + /** + * Add custom attribute to the root span of the trace logged for app startup + */ + fun addAttribute(key: String, value: String) } diff --git a/embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitter.kt b/embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitter.kt index b487b3e907..778dc98aab 100644 --- a/embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitter.kt +++ b/embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitter.kt @@ -12,6 +12,7 @@ import io.embrace.android.embracesdk.internal.utils.VersionChecker import io.embrace.android.embracesdk.internal.worker.BackgroundWorker import io.embrace.android.embracesdk.spans.EmbraceSpan import io.opentelemetry.sdk.common.Clock +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicBoolean @@ -46,6 +47,7 @@ internal class AppStartupTraceEmitter( private val processCreateRequestedMs: Long? private val processCreatedMs: Long? private val additionalTrackedIntervals = ConcurrentLinkedQueue() + private val customAttributes: MutableMap = ConcurrentHashMap() init { val timestampAtDeviceStart = nowMs() - clock.nanoTime().nanosToMillis() @@ -159,6 +161,10 @@ internal class AppStartupTraceEmitter( ) } + override fun addAttribute(key: String, value: String) { + customAttributes[key] = value + } + /** * Called when app startup is considered complete, i.e. the data can be used and any additional updates can be ignored */ @@ -250,6 +256,7 @@ internal class AppStartupTraceEmitter( parent = startupTrace, startTimeMs = trackedInterval.startTimeMs, endTimeMs = trackedInterval.endTimeMs, + internal = false, ) } } while (additionalTrackedIntervals.isNotEmpty()) @@ -388,6 +395,7 @@ internal class AppStartupTraceEmitter( private fun nowMs(): Long = clock.now().nanosToMillis() private fun PersistableEmbraceSpan.addTraceMetadata() { + addCustomAttributes() processCreateDelay()?.let { delay -> addAttribute("process-create-delay-ms", delay.toString()) } @@ -417,6 +425,12 @@ internal class AppStartupTraceEmitter( } } + private fun PersistableEmbraceSpan.addCustomAttributes() { + customAttributes.forEach { + addAttribute(it.key, it.value) + } + } + private data class TrackedInterval( val name: String, val startTimeMs: Long, diff --git a/embrace-android-features/src/test/java/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitterTest.kt b/embrace-android-features/src/test/java/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitterTest.kt index c62194047a..fc2e2a2003 100644 --- a/embrace-android-features/src/test/java/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitterTest.kt +++ b/embrace-android-features/src/test/java/io/embrace/android/embracesdk/internal/capture/startup/AppStartupTraceEmitterTest.kt @@ -223,16 +223,19 @@ internal class AppStartupTraceEmitterTest { private fun verifyColdStartWithRender(processCreateDelayMs: Long? = null) { clock.tick(100L) appStartupTraceEmitter.applicationInitStart() + val customSpanStartMs = clock.now() clock.tick(15L) val (sdkInitStart, sdkInitEnd) = startSdk() + appStartupTraceEmitter.addAttribute("custom-attribute", "true") appStartupTraceEmitter.applicationInitEnd() val applicationInitEnd = clock.now() clock.tick(50L) + appStartupTraceEmitter.addTrackedInterval("custom-span", customSpanStartMs, applicationInitEnd) val activityCreateEvents = createStartupActivity() val traceEnd = startupActivityRender().second - assertEquals(7, spanSink.completedSpans().size) + assertEquals(8, spanSink.completedSpans().size) val spanMap = spanSink.completedSpans().associateBy { it.name } val trace = checkNotNull(spanMap["emb-cold-time-to-initial-display"]) val processInit = checkNotNull(spanMap["emb-process-init"]) @@ -240,6 +243,7 @@ internal class AppStartupTraceEmitterTest { val activityInitDelay = checkNotNull(spanMap["emb-activity-init-gap"]) val activityCreate = checkNotNull(spanMap["emb-activity-create"]) val firstRender = checkNotNull(spanMap["emb-first-frame-render"]) + val customSpan = checkNotNull(spanMap["custom-span"]) val startupActivityStart = checkNotNull(activityCreateEvents.create) val startupActivityEnd = checkNotNull((activityCreateEvents.finished)) @@ -252,12 +256,14 @@ internal class AppStartupTraceEmitterTest { expectedActivityPreCreatedMs = activityCreateEvents.preCreate, expectedActivityPostCreatedMs = activityCreateEvents.postCreate, expectedFirstActivityLifecycleEventMs = activityCreateEvents.firstEvent, + expectedCustomAttributes = mapOf("custom-attribute" to "true") ) assertChildSpan(processInit, DEFAULT_FAKE_CURRENT_TIME, applicationInitEnd) assertChildSpan(embraceInit, sdkInitStart, sdkInitEnd) assertChildSpan(activityInitDelay, applicationInitEnd, startupActivityStart) assertChildSpan(activityCreate, startupActivityStart, startupActivityEnd) assertChildSpan(firstRender, startupActivityEnd, traceEnd) + assertChildSpan(customSpan, customSpanStartMs, applicationInitEnd) assertEquals(0, logger.internalErrorMessages.size) } @@ -542,6 +548,7 @@ internal class AppStartupTraceEmitterTest { expectedActivityPreCreatedMs: Long? = null, expectedActivityPostCreatedMs: Long? = null, expectedFirstActivityLifecycleEventMs: Long? = null, + expectedCustomAttributes: Map = emptyMap(), ) { val trace = input.toNewPayload() assertEquals(expectedStartTimeMs, trace.startTimeNanos?.nanosToMillis()) @@ -565,6 +572,10 @@ internal class AppStartupTraceEmitterTest { assertEquals("false", attrs.findAttributeValue("embrace-init-in-foreground")) assertEquals("main", attrs.findAttributeValue("embrace-init-thread-name")) assertEquals(1, dataCollectionCompletedCallbackInvokedCount) + + expectedCustomAttributes.forEach { entry -> + assertEquals(entry.value, trace.attributes?.findAttributeValue(entry.key)) + } } private fun assertChildSpan(span: EmbraceSpanData, expectedStartTimeNanos: Long, expectedEndTimeNanos: Long) { diff --git a/embrace-android-sdk/api/embrace-android-sdk.api b/embrace-android-sdk/api/embrace-android-sdk.api index 6b279ca27d..0f0cf1afac 100644 --- a/embrace-android-sdk/api/embrace-android-sdk.api +++ b/embrace-android-sdk/api/embrace-android-sdk.api @@ -8,6 +8,9 @@ public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/em public fun addLogRecordExporter (Lio/opentelemetry/sdk/logs/export/LogRecordExporter;)V public fun addSessionProperty (Ljava/lang/String;Ljava/lang/String;Z)Z public fun addSpanExporter (Lio/opentelemetry/sdk/trace/export/SpanExporter;)V + public fun addStartupChildSpan (Ljava/lang/String;JJ)V + public fun addStartupChildSpan (Ljava/lang/String;JJLjava/util/Map;Ljava/util/List;Lio/embrace/android/embracesdk/spans/ErrorCode;)V + public fun addStartupTraceAttribute (Ljava/lang/String;Ljava/lang/String;)V public fun addUserPersona (Ljava/lang/String;)V public fun clearAllUserPersonas ()V public fun clearUserEmail ()V @@ -26,6 +29,7 @@ public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/em public static final fun getInstance ()Lio/embrace/android/embracesdk/Embrace; public fun getLastRunEndState ()Lio/embrace/android/embracesdk/LastRunEndState; public fun getOpenTelemetry ()Lio/opentelemetry/api/OpenTelemetry; + public fun getSdkCurrentTimeMs ()J public fun getSpan (Ljava/lang/String;)Lio/embrace/android/embracesdk/spans/EmbraceSpan; public fun isStarted ()Z public fun logCustomStacktrace ([Ljava/lang/StackTraceElement;)V diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/AppStartupTraceTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/AppStartupTraceTest.kt new file mode 100644 index 0000000000..81d25cd5ea --- /dev/null +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/AppStartupTraceTest.kt @@ -0,0 +1,58 @@ +package io.embrace.android.embracesdk.testcases + +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.assertions.findSpansOfType +import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig +import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig +import io.embrace.android.embracesdk.internal.arch.schema.EmbType +import io.embrace.android.embracesdk.internal.spans.findAttributeValue +import io.embrace.android.embracesdk.testframework.IntegrationTestRule +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@Config(sdk = [Build.VERSION_CODES.LOLLIPOP]) +@RunWith(AndroidJUnit4::class) +internal class AppStartupTraceTest { + @Rule + @JvmField + val testRule: IntegrationTestRule = IntegrationTestRule() + + @Test + fun `startup spans recorded in foreground session when background activity is enabled`() { + testRule.runTest( + instrumentedConfig = FakeInstrumentedConfig( + enabledFeatures = FakeEnabledFeatureConfig( + bgActivityCapture = true + ) + ), + testCaseAction = { + val customStartTimeMs = clock.now() + val customEndTimeMs = clock.tick(100L) + embrace.addStartupChildSpan("custom-span", customStartTimeMs, customEndTimeMs) + embrace.addStartupTraceAttribute("custom-attribute", "yes") + simulateOpeningActivities( + addStartupActivity = false, + startInBackground = true + ) + }, + assertAction = { + with(getSingleSessionEnvelope()) { + val spans = findSpansOfType(EmbType.Performance.Default).associateBy { it.name } + assertTrue(spans.isNotEmpty()) + with(checkNotNull(spans["emb-cold-time-to-initial-display"])) { + assertEquals("yes", attributes?.findAttributeValue("custom-attribute")) + } + assertTrue(spans.containsKey("emb-embrace-init")) + assertTrue(spans.containsKey("custom-span")) + assertTrue(spans.containsKey("emb-activity-create")) + assertTrue(spans.containsKey("emb-activity-resume")) + } + } + ) + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.kt index 2fe88c2682..11cbc25d62 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/Embrace.kt @@ -428,6 +428,8 @@ public class Embrace private constructor( impl.activityLoaded(activity) } + override fun getSdkCurrentTimeMs(): Long = impl.getSdkCurrentTimeMs() + override fun addAttributeToLoadTrace(activity: Activity, key: String, value: String) { impl.addAttributeToLoadTrace(activity, key, value) } @@ -451,4 +453,26 @@ public class Embrace private constructor( errorCode = errorCode ) } + + override fun addStartupTraceAttribute(key: String, value: String) { + impl.addStartupTraceAttribute(key, value) + } + + override fun addStartupChildSpan( + name: String, + startTimeMs: Long, + endTimeMs: Long, + attributes: Map, + events: List, + errorCode: ErrorCode?, + ) { + impl.addStartupChildSpan( + name = name, + startTimeMs = startTimeMs, + endTimeMs = endTimeMs, + attributes = attributes, + events = events, + errorCode = errorCode + ) + } } diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/api/delegate/InstrumentationApiDelegate.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/api/delegate/InstrumentationApiDelegate.kt index 2ee233b00c..2fd73a731c 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/api/delegate/InstrumentationApiDelegate.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/internal/api/delegate/InstrumentationApiDelegate.kt @@ -18,6 +18,9 @@ internal class InstrumentationApiDelegate( private val uiLoadTraceEmitter by embraceImplInject(sdkCallChecker) { bootstrapper.dataCaptureServiceModule.uiLoadDataListener } + private val appStartupDataCollector by embraceImplInject(sdkCallChecker) { + bootstrapper.dataCaptureServiceModule.appStartupDataCollector + } override fun activityLoaded(activity: Activity) { if (sdkCallChecker.check("activity_fully_loaded")) { @@ -25,6 +28,8 @@ internal class InstrumentationApiDelegate( } } + override fun getSdkCurrentTimeMs(): Long = clock.now() + override fun addAttributeToLoadTrace(activity: Activity, key: String, value: String) { if (sdkCallChecker.check("add_attribute_to_load_trace")) { uiLoadTraceEmitter?.addAttribute(traceInstanceId(activity), key, value) @@ -52,4 +57,27 @@ internal class InstrumentationApiDelegate( ) } } + + override fun addStartupTraceAttribute(key: String, value: String) { + if (sdkCallChecker.check("add_attribute_to_app_startup_trace")) { + appStartupDataCollector?.addAttribute(key, value) + } + } + + override fun addStartupChildSpan( + name: String, + startTimeMs: Long, + endTimeMs: Long, + attributes: Map, + events: List, + errorCode: ErrorCode?, + ) { + if (sdkCallChecker.check("add_child_span_to_app_startup_trace")) { + appStartupDataCollector?.addTrackedInterval( + name = name, + startTimeMs = startTimeMs, + endTimeMs = endTimeMs + ) + } + } } diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeAppStartupDataCollector.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeAppStartupDataCollector.kt index 799e77ecac..707790a930 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeAppStartupDataCollector.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeAppStartupDataCollector.kt @@ -1,6 +1,10 @@ package io.embrace.android.embracesdk.fakes import io.embrace.android.embracesdk.internal.capture.startup.AppStartupDataCollector +import io.embrace.android.embracesdk.internal.clock.millisToNanos +import io.opentelemetry.sdk.trace.data.SpanData +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue class FakeAppStartupDataCollector( private val clock: FakeClock, @@ -14,6 +18,8 @@ class FakeAppStartupDataCollector( var startupActivityInitEndMs: Long? = null var startupActivityResumedMs: Long? = null var firstFrameRenderedMs: Long? = null + var customChildSpans = ConcurrentLinkedQueue() + var customAttributes: MutableMap = ConcurrentHashMap() override fun applicationInitStart(timestampMs: Long?) { applicationInitStartMs = timestampMs ?: clock.now() @@ -60,6 +66,16 @@ class FakeAppStartupDataCollector( } override fun addTrackedInterval(name: String, startTimeMs: Long, endTimeMs: Long) { - TODO("Not yet implemented") + customChildSpans.add( + FakeSpanData( + name = name, + startEpochNanos = startTimeMs.millisToNanos(), + endTimeNanos = endTimeMs.millisToNanos() + ) + ) + } + + override fun addAttribute(key: String, value: String) { + customAttributes[key] = value } }