Skip to content

Commit

Permalink
Hook up Activity Open tracing component
Browse files Browse the repository at this point in the history
  • Loading branch information
bidetofevil committed Oct 16, 2024
1 parent c7f26e7 commit fd9bcb8
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,9 @@ interface AutoDataCaptureBehavior {
* Gates whether the SDK should use the v2 storage implementation or the legacy one.
*/
fun isV2StorageEnabled(): Boolean

/**
* Whether the SDK is configured to capture traces for the performance of the opening of Activities.
*/
fun isActivityOpenPerfCaptureEnabled(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class AutoDataCaptureBehaviorImpl(

override fun isNativeCrashCaptureEnabled(): Boolean = cfg.isNativeCrashCaptureEnabled()
override fun isDiskUsageCaptureEnabled(): Boolean = cfg.isDiskUsageCaptureEnabled()
override fun isActivityOpenPerfCaptureEnabled(): Boolean = cfg.isActivityOpenPerfCaptureEnabled()

// this should remain immutable across the process lifecycle
private val v2StorageImpl by lazy { remote?.killSwitchConfig?.v2Storage ?: false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal class AutoDataCaptureBehaviorImplTest {
assertFalse(isNativeCrashCaptureEnabled())
assertTrue(isDiskUsageCaptureEnabled())
assertTrue(isThermalStatusCaptureEnabled())
assertFalse(isActivityOpenPerfCaptureEnabled())
assertTrue(isThermalStatusCaptureEnabled())
assertFalse(isV2StorageEnabled())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.view.ViewTreeObserver
import android.view.Window
import io.embrace.android.embracesdk.annotation.StartupActivity
import io.embrace.android.embracesdk.internal.logging.EmbLogger
import io.embrace.android.embracesdk.internal.session.lifecycle.ActivityLifecycleListener
import io.embrace.android.embracesdk.internal.utils.VersionChecker

/**
Expand All @@ -37,6 +38,7 @@ import io.embrace.android.embracesdk.internal.utils.VersionChecker
*/
class StartupTracker(
private val appStartupDataCollector: AppStartupDataCollector,
private val activityOpenEventEmitter: ActivityLifecycleListener?,
private val logger: EmbLogger,
private val versionChecker: VersionChecker,
) : Application.ActivityLifecycleCallbacks {
Expand Down Expand Up @@ -117,6 +119,9 @@ class StartupTracker(
private fun startupComplete(application: Application) {
if (!startupDataCollectionComplete) {
application.unregisterActivityLifecycleCallbacks(this)
activityOpenEventEmitter?.apply {
application.registerActivityLifecycleCallbacks(this)
}
startupDataCollectionComplete = true
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.embrace.android.embracesdk.internal.injection

import io.embrace.android.embracesdk.internal.capture.activity.OpenEventEmitter
import io.embrace.android.embracesdk.internal.capture.activity.OpenEvents
import io.embrace.android.embracesdk.internal.capture.crumbs.ActivityBreadcrumbTracker
import io.embrace.android.embracesdk.internal.capture.crumbs.PushNotificationCaptureService
import io.embrace.android.embracesdk.internal.capture.startup.AppStartupDataCollector
Expand Down Expand Up @@ -35,7 +37,11 @@ interface DataCaptureServiceModule {
*/
val startupService: StartupService

val appStartupDataCollector: AppStartupDataCollector

val startupTracker: StartupTracker

val appStartupDataCollector: AppStartupDataCollector
val activityOpenTraceEmitter: OpenEvents

val activityOpenTracker: OpenEventEmitter?
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.embrace.android.embracesdk.internal.injection

import io.embrace.android.embracesdk.internal.Systrace
import io.embrace.android.embracesdk.internal.capture.activity.OpenEventEmitter
import io.embrace.android.embracesdk.internal.capture.activity.OpenEvents
import io.embrace.android.embracesdk.internal.capture.activity.OpenTraceEmitter
import io.embrace.android.embracesdk.internal.capture.crumbs.ActivityBreadcrumbTracker
import io.embrace.android.embracesdk.internal.capture.crumbs.PushNotificationCaptureService
import io.embrace.android.embracesdk.internal.capture.startup.AppStartupDataCollector
Expand Down Expand Up @@ -63,8 +66,28 @@ internal class DataCaptureServiceModuleImpl @JvmOverloads constructor(
override val startupTracker: StartupTracker by singleton {
StartupTracker(
appStartupDataCollector = appStartupDataCollector,
activityOpenEventEmitter = activityOpenTracker,
logger = initModule.logger,
versionChecker = versionChecker,
)
}

override val activityOpenTraceEmitter: OpenEvents by singleton {
OpenTraceEmitter(
spanService = openTelemetryModule.spanService,
versionChecker = versionChecker,
)
}

override val activityOpenTracker: OpenEventEmitter? by singleton {
if (configService.autoDataCaptureBehavior.isActivityOpenPerfCaptureEnabled()) {
OpenEventEmitter(
openEvents = activityOpenTraceEmitter,
clock = openTelemetryModule.openTelemetryClock,
versionChecker = versionChecker,
)
} else {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.embrace.android.embracesdk.fakes.FakeClock
import io.embrace.android.embracesdk.fakes.FakeEmbLogger
import io.embrace.android.embracesdk.fakes.FakeSplashScreenActivity
import io.embrace.android.embracesdk.internal.logging.EmbLogger
import io.embrace.android.embracesdk.internal.session.lifecycle.ActivityLifecycleListener
import io.embrace.android.embracesdk.internal.utils.BuildVersionChecker
import org.junit.Assert.assertEquals
import org.junit.Before
Expand Down Expand Up @@ -37,6 +38,7 @@ internal class StartupTrackerTest {
dataCollector = FakeAppStartupDataCollector(clock = clock)
startupTracker = StartupTracker(
appStartupDataCollector = dataCollector,
activityOpenEventEmitter = object : ActivityLifecycleListener { },
logger = logger,
versionChecker = BuildVersionChecker
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,11 @@ object EnabledFeatureConfig {
* sdk_config.networking.enable_native_monitoring
*/
fun isHttpUrlConnectionCaptureEnabled(): Boolean = true

/**
* Gates whether the SDK will capture traces for the performance of the opening of Activities.
*
* sdk_config.automatic_data_capture.activity_open_perf_info
*/
fun isActivityOpenPerfCaptureEnabled(): Boolean = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
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.ObservedActivity
import io.embrace.android.embracesdk.assertions.assertEmbraceSpanData
import io.embrace.android.embracesdk.assertions.findSpansByName
import io.embrace.android.embracesdk.assertions.findSpansOfType
import io.embrace.android.embracesdk.fakes.behavior.FakeAutoDataCaptureBehavior
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.internal.capture.activity.OpenTraceEmitter.LifecycleEvent
import io.embrace.android.embracesdk.internal.payload.ApplicationState
import io.embrace.android.embracesdk.testframework.IntegrationTestRule
import io.opentelemetry.api.trace.SpanId
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
internal class ActivityOpenTest {

@Rule
@JvmField
val testRule = IntegrationTestRule()

private val defaultAutoDataCaptureBehavior = FakeAutoDataCaptureBehavior(activityOpenPerfEnabled = true)

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `activity open does not create a trace if feature flag is disabled`() {
testRule.runTest(
setupAction = {
overriddenConfigService.autoDataCaptureBehavior = FakeAutoDataCaptureBehavior(
activityOpenPerfEnabled = false
)
},
testCaseAction = {
simulateOpeningActivities(
addStartupActivity = false,
startInBackground = true,
activitiesAndActions = listOf(
Robolectric.buildActivity(Activity1::class.java) to {},
Robolectric.buildActivity(Activity2::class.java) to {},
)
)
},
assertAction = {
val payload = getSingleSessionEnvelope()
assertEquals(0, payload.findSpansOfType(EmbType.Performance.ActivityOpen).size)
}
)
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `opening first non-startup activity creates cold open trace in L`() {
var preLaunchTimeMs = 0L
testRule.runTest(
setupAction = {
preLaunchTimeMs = overriddenClock.now()
overriddenConfigService.autoDataCaptureBehavior = defaultAutoDataCaptureBehavior
},
testCaseAction = {
simulateOpeningActivities(
addStartupActivity = false,
startInBackground = true,
activitiesAndActions = listOf(
Robolectric.buildActivity(Activity1::class.java) to {},
Robolectric.buildActivity(Activity2::class.java) to {},
)
)
},
assertAction = {
val payload = getSingleSessionEnvelope()
val trace = payload.findSpansOfType(EmbType.Performance.ActivityOpen).single()
assertEquals("emb-$ACTIVITY2_NAME-cold-open", trace.name)
val rootSpanId = checkNotNull(trace.spanId)

val expectedTraceStartTime = preLaunchTimeMs + 20301
assertEmbraceSpanData(
span = trace,
expectedStartTimeMs = expectedTraceStartTime,
expectedEndTimeMs = expectedTraceStartTime + 250,
expectedParentId = SpanId.getInvalid(),
key = true
)

assertEmbraceSpanData(
span = payload.findSpansByName("emb-${LifecycleEvent.CREATE.spanName(ACTIVITY2_NAME)}").single(),
expectedStartTimeMs = expectedTraceStartTime + 50,
expectedEndTimeMs = expectedTraceStartTime + 150,
expectedParentId = rootSpanId,
)

assertEmbraceSpanData(
span = payload.findSpansByName("emb-${LifecycleEvent.START.spanName(ACTIVITY2_NAME)}").single(),
expectedStartTimeMs = expectedTraceStartTime + 150,
expectedEndTimeMs = expectedTraceStartTime + 250,
expectedParentId = rootSpanId,
)
}
)
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `foregrounding and initializing new activity creates cold open trace in L`() {
var preLaunchTimeMs = 0L
testRule.runTest(
setupAction = {
preLaunchTimeMs = overriddenClock.now()
overriddenConfigService.autoDataCaptureBehavior = defaultAutoDataCaptureBehavior
},
testCaseAction = {
simulateOpeningActivities(
addStartupActivity = true,
startInBackground = true,
activitiesAndActions = listOf(
Robolectric.buildActivity(Activity1::class.java) to {}
)
)
},
assertAction = {
val payload = getSessionEnvelopes(2)[1]
val trace = payload.findSpansOfType(EmbType.Performance.ActivityOpen).single()
assertEquals("emb-$ACTIVITY1_NAME-cold-open", trace.name)
val rootSpanId = checkNotNull(trace.spanId)

val expectedTraceStartTime = preLaunchTimeMs + 10000
assertEmbraceSpanData(
span = trace,
expectedStartTimeMs = expectedTraceStartTime,
expectedEndTimeMs = expectedTraceStartTime + 200,
expectedParentId = SpanId.getInvalid(),
key = true
)

val lastBackgroundActivity = getSessionEnvelopes(2, ApplicationState.BACKGROUND)[1]
assertEmbraceSpanData(
span = lastBackgroundActivity
.findSpansByName("emb-${LifecycleEvent.CREATE.spanName(ACTIVITY1_NAME)}")
.single(),
expectedStartTimeMs = expectedTraceStartTime,
expectedEndTimeMs = expectedTraceStartTime + 100,
expectedParentId = rootSpanId,
)

assertEmbraceSpanData(
span = payload.findSpansByName("emb-${LifecycleEvent.START.spanName(ACTIVITY1_NAME)}").single(),
expectedStartTimeMs = expectedTraceStartTime + 100,
expectedEndTimeMs = expectedTraceStartTime + 200,
expectedParentId = rootSpanId,
)
}
)
}

@Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
@Test
fun `foregrounding already-created activity creates hot open trace in L`() {
var preLaunchTimeMs = 0L
testRule.runTest(
setupAction = {
preLaunchTimeMs = overriddenClock.now()
overriddenConfigService.autoDataCaptureBehavior = defaultAutoDataCaptureBehavior
},
testCaseAction = {
simulateOpeningActivities(
addStartupActivity = true,
startInBackground = true,
createFirstActivity = false,
activitiesAndActions = listOf(
Robolectric.buildActivity(Activity1::class.java) to {},
)
)
},
assertAction = {
val payload = getSessionEnvelopes(2)[1]
val trace = payload.findSpansOfType(EmbType.Performance.ActivityOpen).single()
assertEquals("emb-$ACTIVITY1_NAME-hot-open", trace.name)
val rootSpanId = checkNotNull(trace.spanId)

val expectedTraceStartTime = preLaunchTimeMs + 10000
assertEmbraceSpanData(
span = trace,
expectedStartTimeMs = expectedTraceStartTime,
expectedEndTimeMs = expectedTraceStartTime + 100,
expectedParentId = SpanId.getInvalid(),
key = true
)

assertEmbraceSpanData(
span = payload.findSpansByName("emb-${LifecycleEvent.START.spanName(ACTIVITY1_NAME)}").single(),
expectedStartTimeMs = expectedTraceStartTime,
expectedEndTimeMs = expectedTraceStartTime + 100,
expectedParentId = rootSpanId,
)
}
)
}

private companion object {
@ObservedActivity
class Activity1 : Activity()

@ObservedActivity
class Activity2 : Activity()

val ACTIVITY1_NAME = Robolectric.buildActivity(Activity1::class.java).get().localClassName
val ACTIVITY2_NAME = Robolectric.buildActivity(Activity2::class.java).get().localClassName
}
}
Loading

0 comments on commit fd9bcb8

Please sign in to comment.