Skip to content

Commit

Permalink
API and integration tests for manually ending UI load traces
Browse files Browse the repository at this point in the history
  • Loading branch information
bidetofevil committed Jan 6, 2025
1 parent 505d329 commit 59d7670
Show file tree
Hide file tree
Showing 13 changed files with 209 additions and 15 deletions.
9 changes: 8 additions & 1 deletion embrace-android-api/api/embrace-android-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public final class io/embrace/android/embracesdk/Severity : java/lang/Enum {
public static fun values ()[Lio/embrace/android/embracesdk/Severity;
}

public abstract interface annotation class io/embrace/android/embracesdk/annotation/CustomTracedActivity : java/lang/annotation/Annotation {
}

public abstract interface annotation class io/embrace/android/embracesdk/annotation/InternalApi : java/lang/annotation/Annotation {
}

Expand All @@ -48,6 +51,10 @@ public abstract interface class io/embrace/android/embracesdk/internal/api/Embra
public abstract fun startView (Ljava/lang/String;)Z
}

public abstract interface class io/embrace/android/embracesdk/internal/api/InstrumentationApi {
public abstract fun activityLoaded (Landroid/app/Activity;)V
}

public abstract interface class io/embrace/android/embracesdk/internal/api/InternalWebViewApi {
public abstract fun logWebView (Ljava/lang/String;)V
public abstract fun trackWebViewPerformance (Ljava/lang/String;Landroid/webkit/ConsoleMessage;)V
Expand Down Expand Up @@ -82,7 +89,7 @@ public abstract interface class io/embrace/android/embracesdk/internal/api/OTelA
public abstract fun getOpenTelemetry ()Lio/opentelemetry/api/OpenTelemetry;
}

public abstract interface class io/embrace/android/embracesdk/internal/api/SdkApi : io/embrace/android/embracesdk/internal/api/BreadcrumbApi, io/embrace/android/embracesdk/internal/api/EmbraceAndroidApi, io/embrace/android/embracesdk/internal/api/InternalWebViewApi, io/embrace/android/embracesdk/internal/api/LogsApi, io/embrace/android/embracesdk/internal/api/NetworkRequestApi, io/embrace/android/embracesdk/internal/api/OTelApi, io/embrace/android/embracesdk/internal/api/SdkStateApi, io/embrace/android/embracesdk/internal/api/SessionApi, io/embrace/android/embracesdk/internal/api/UserApi, io/embrace/android/embracesdk/spans/TracingApi {
public abstract interface class io/embrace/android/embracesdk/internal/api/SdkApi : io/embrace/android/embracesdk/internal/api/BreadcrumbApi, io/embrace/android/embracesdk/internal/api/EmbraceAndroidApi, io/embrace/android/embracesdk/internal/api/InstrumentationApi, io/embrace/android/embracesdk/internal/api/InternalWebViewApi, io/embrace/android/embracesdk/internal/api/LogsApi, io/embrace/android/embracesdk/internal/api/NetworkRequestApi, io/embrace/android/embracesdk/internal/api/OTelApi, io/embrace/android/embracesdk/internal/api/SdkStateApi, io/embrace/android/embracesdk/internal/api/SessionApi, io/embrace/android/embracesdk/internal/api/UserApi, io/embrace/android/embracesdk/spans/TracingApi {
}

public final class io/embrace/android/embracesdk/internal/api/SdkApi$DefaultImpls {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.embrace.android.embracesdk.annotation

/**
* The loading of Activities annotated with this class will generate traces if the feature is enabled, irrespective of
* the configured defaults.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
public annotation class CustomTracedActivity
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.embrace.android.embracesdk.internal.api

import android.app.Activity
import io.embrace.android.embracesdk.annotation.InternalApi

/**
* API to control and customize Embrace instrumentation
*/
@InternalApi
public interface InstrumentationApi {

/**
* Notify the Embrace UI Load instrumentation that the given [Activity] instance has fully loaded, so its associated
* trace can be stopped
*/
public fun activityLoaded(activity: Activity)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ public interface SdkApi :
SdkStateApi,
OTelApi,
BreadcrumbApi,
InternalWebViewApi
InternalWebViewApi,
InstrumentationApi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Activity
import android.os.Build.VERSION_CODES
import android.os.Bundle
import androidx.annotation.RequiresApi
import io.embrace.android.embracesdk.annotation.CustomTracedActivity
import io.embrace.android.embracesdk.annotation.NotTracedActivity
import io.embrace.android.embracesdk.annotation.TracedActivity
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
Expand Down Expand Up @@ -35,6 +36,11 @@ fun createActivityLoadEventEmitter(
}
}

/**
* Return an ID to identify the trace for the given [Activity] instance
*/
fun traceInstanceId(activity: Activity): Int = activity.hashCode()

/**
* Implementation that works with Android 10+ APIs
*/
Expand Down Expand Up @@ -108,18 +114,18 @@ private class LifecycleEventEmitter(
) {

fun create(activity: Activity) {
if (activity.observe()) {
if (activity.traceLoad()) {
uiLoadEventListener.create(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs(),
manualEnd = false,
manualEnd = activity.isManualEnd(),
)
}
}

fun createEnd(activity: Activity) {
if (activity.observe()) {
if (activity.traceLoad()) {
uiLoadEventListener.createEnd(
instanceId = traceInstanceId(activity),
timestampMs = nowMs()
Expand All @@ -128,18 +134,18 @@ private class LifecycleEventEmitter(
}

fun start(activity: Activity) {
if (activity.observe()) {
if (activity.traceLoad()) {
uiLoadEventListener.start(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs(),
manualEnd = false,
manualEnd = activity.isManualEnd(),
)
}
}

fun startEnd(activity: Activity) {
if (activity.observe()) {
if (activity.traceLoad()) {
uiLoadEventListener.startEnd(
instanceId = traceInstanceId(activity),
timestampMs = nowMs()
Expand All @@ -148,7 +154,7 @@ private class LifecycleEventEmitter(
}

fun resume(activity: Activity) {
if (activity.observe()) {
if (activity.traceLoad()) {
uiLoadEventListener.resume(
instanceId = traceInstanceId(activity),
timestampMs = nowMs()
Expand All @@ -157,7 +163,7 @@ private class LifecycleEventEmitter(
}

fun resumeEnd(activity: Activity) {
if (activity.observe()) {
if (activity.traceLoad()) {
uiLoadEventListener.resumeEnd(
instanceId = traceInstanceId(activity),
timestampMs = nowMs()
Expand All @@ -166,20 +172,21 @@ private class LifecycleEventEmitter(
}

fun pause(activity: Activity) {
if (activity.observe()) {
if (activity.traceLoad()) {
uiLoadEventListener.discard(
instanceId = traceInstanceId(activity),
timestampMs = nowMs()
)
}
}

private fun Activity.observe(): Boolean {
return javaClass.isAnnotationPresent(TracedActivity::class.java) ||
autoTraceEnabled && !javaClass.isAnnotationPresent(NotTracedActivity::class.java)
private fun Activity.traceLoad(): Boolean {
return (autoTraceEnabled && !javaClass.isAnnotationPresent(NotTracedActivity::class.java)) ||
isManualEnd() ||
javaClass.isAnnotationPresent(TracedActivity::class.java)
}

private fun traceInstanceId(activity: Activity): Int = activity.hashCode()
private fun Activity.isManualEnd(): Boolean = javaClass.isAnnotationPresent(CustomTracedActivity::class.java)

private fun nowMs(): Long = clock.now().nanosToMillis()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.embrace.android.embracesdk.fakes.FakeClock
import io.embrace.android.embracesdk.fakes.FakeClock.Companion.DEFAULT_FAKE_CURRENT_TIME
import io.embrace.android.embracesdk.fakes.FakeCustomTracedActivity
import io.embrace.android.embracesdk.fakes.FakeNotTracedActivity
import io.embrace.android.embracesdk.fakes.FakeTracedActivity
import io.embrace.android.embracesdk.fakes.FakeUiLoadEventListener
Expand Down Expand Up @@ -84,6 +85,14 @@ internal class UiLoadExtTest {
assertTrue(uiLoadEventListener.events.isEmpty())
}

@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
@Test
fun `activities with manual ended trace will emit ui load events in U`() {
createEventEmitter(autoTraceEnabled = false, activityClass = FakeCustomTracedActivity::class)
stepThroughActivityLifecycle()
uiLoadEventListener.events.assertEventData(expectedColdOpenEvents)
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `check cold ui load event stages in L`() {
Expand Down Expand Up @@ -124,6 +133,14 @@ internal class UiLoadExtTest {
assertTrue(uiLoadEventListener.events.isEmpty())
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `activities with manual ended trace will emit ui load events in L`() {
createEventEmitter(autoTraceEnabled = false, activityClass = FakeCustomTracedActivity::class)
stepThroughActivityLifecycle()
uiLoadEventListener.events.assertEventData(legacyColdOpenEvents)
}

private fun <T : Activity> createEventEmitter(autoTraceEnabled: Boolean, activityClass: KClass<T>) {
eventEmitter = createActivityLoadEventEmitter(
uiLoadEventListener = uiLoadEventListener,
Expand Down
1 change: 1 addition & 0 deletions embrace-android-sdk/api/embrace-android-sdk.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
public final class io/embrace/android/embracesdk/Embrace : io/embrace/android/embracesdk/internal/api/SdkApi {
public static final field Companion Lio/embrace/android/embracesdk/Embrace$Companion;
public fun activityLoaded (Landroid/app/Activity;)V
public fun addBreadcrumb (Ljava/lang/String;)V
public fun addLogRecordExporter (Lio/opentelemetry/sdk/logs/export/LogRecordExporter;)V
public fun addSessionProperty (Ljava/lang/String;Ljava/lang/String;Z)Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.embrace.android.embracesdk.testcases.features
import android.app.Activity
import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.embrace.android.embracesdk.annotation.CustomTracedActivity
import io.embrace.android.embracesdk.annotation.NotTracedActivity
import io.embrace.android.embracesdk.annotation.TracedActivity
import io.embrace.android.embracesdk.assertions.assertEmbraceSpanData
Expand All @@ -13,6 +14,8 @@ import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.internal.capture.activity.LifecycleStage
import io.embrace.android.embracesdk.internal.payload.ApplicationState
import io.embrace.android.embracesdk.internal.payload.Span
import io.embrace.android.embracesdk.spans.ErrorCode
import io.embrace.android.embracesdk.testframework.IntegrationTestRule
import io.opentelemetry.api.trace.SpanId
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -77,6 +80,87 @@ internal class UiLoadTest {
)
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `activity open creates a trace for manually ended load`() {
var preLaunchTimeMs = 0L
testRule.runTest(
instrumentedConfig = FakeInstrumentedConfig(
enabledFeatures = FakeEnabledFeatureConfig(
uiLoadPerfCapture = true,
uiLoadPerfAutoCapture = false,
bgActivityCapture = true
)
),
setupAction = {
preLaunchTimeMs = overriddenClock.now()
},
testCaseAction = {
simulateOpeningActivities(
invokeManualEnd = true,
activitiesAndActions = listOf(
Robolectric.buildActivity(ManualStopActivity::class.java) to {},
)
)
},
assertAction = {
with(getSingleSessionEnvelope()) {
val trace = findSpansOfType(EmbType.Performance.UiLoad).single()
assertEquals("emb-$MANUAL_STOP_ACTIVITY_NAME-cold-time-to-initial-display", trace.name)

val expectedTraceStartTime = preLaunchTimeMs + 50
assertEmbraceSpanData(
span = trace,
expectedStartTimeMs = expectedTraceStartTime,
expectedEndTimeMs = expectedTraceStartTime + 400,
expectedParentId = SpanId.getInvalid(),
)
}
}
)
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `opening activity configured to be ended manually creates an abandoned trace if activityLoaded not invoked`() {
var preLaunchTimeMs = 0L
testRule.runTest(
instrumentedConfig = FakeInstrumentedConfig(
enabledFeatures = FakeEnabledFeatureConfig(
uiLoadPerfCapture = true,
uiLoadPerfAutoCapture = false,
bgActivityCapture = true
)
),
setupAction = {
preLaunchTimeMs = overriddenClock.now()
},
testCaseAction = {
simulateOpeningActivities(
activitiesAndActions = listOf(
Robolectric.buildActivity(ManualStopActivity::class.java) to {},
)
)
},
assertAction = {
with(getSingleSessionEnvelope()) {
val trace = findSpansOfType(EmbType.Performance.UiLoad).single()
assertEquals("emb-$MANUAL_STOP_ACTIVITY_NAME-cold-time-to-initial-display", trace.name)

val expectedTraceStartTime = preLaunchTimeMs + 50
assertEmbraceSpanData(
span = trace,
expectedStartTimeMs = expectedTraceStartTime,
expectedEndTimeMs = expectedTraceStartTime + 20301,
expectedParentId = SpanId.getInvalid(),
expectedStatus = Span.Status.ERROR,
expectedErrorCode = ErrorCode.USER_ABANDON,
)
}
}
)
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `activity open does not create a trace for explicitly disabled activity even if auto capture enabled`() {
Expand Down Expand Up @@ -263,7 +347,11 @@ internal class UiLoadTest {
@NotTracedActivity
class IgnoredActivity : Activity()

@CustomTracedActivity
class ManualStopActivity : Activity()

val ACTIVITY1_NAME = Robolectric.buildActivity(Activity1::class.java).get().localClassName
val ACTIVITY2_NAME = Robolectric.buildActivity(Activity2::class.java).get().localClassName
val MANUAL_STOP_ACTIVITY_NAME = Robolectric.buildActivity(ManualStopActivity::class.java).get().localClassName
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ internal class EmbraceActionInterface(
addStartupActivity: Boolean = true,
startInBackground: Boolean = false,
createFirstActivity: Boolean = true,
invokeManualEnd: Boolean = false,
activitiesAndActions: List<Pair<ActivityController<*>, () -> Unit>> = listOf(),
lifecycleEventGap: Long = 100L,
postActionDwell: Long = 20000L,
Expand Down Expand Up @@ -99,6 +100,10 @@ internal class EmbraceActionInterface(
}
activityController.resume()
setup.overriddenClock.tick(lifecycleEventGap)
if (invokeManualEnd) {
setup.overriddenClock.tick(lifecycleEventGap)
embrace.activityLoaded(activityController.get())
}
lastActivity?.stop()
setup.overriddenClock.tick()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package io.embrace.android.embracesdk

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.webkit.ConsoleMessage
import io.embrace.android.embracesdk.internal.Systrace
Expand Down Expand Up @@ -422,4 +423,8 @@ public class Embrace private constructor(
override fun disable() {
impl.disable()
}

override fun activityLoaded(activity: Activity) {
impl.activityLoaded(activity)
}
}
Loading

0 comments on commit 59d7670

Please sign in to comment.