Skip to content

Commit

Permalink
Merge pull request #491 from embrace-io/add-view-crumb-spans
Browse files Browse the repository at this point in the history
Add fragment breadcrumb data source
  • Loading branch information
fractalwrench committed Mar 7, 2024
2 parents 3371bc9 + 3242f45 commit dbc32b8
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal class EmbraceBreadcrumbService(
private val rnBreadcrumbDataSource = RnBreadcrumbDataSource(configService)
private val tapBreadcrumbDataSource = TapBreadcrumbDataSource(configService)
private val viewBreadcrumbDataSource = ViewBreadcrumbDataSource(configService, clock)
private val fragmentBreadcrumbDataSource = FragmentBreadcrumbDataSource(configService, clock)
private val fragmentBreadcrumbDataSource = LegacyFragmentBreadcrumbDataSource(configService, clock)
private val pushNotificationBreadcrumbDataSource =
PushNotificationBreadcrumbDataSource(configService, clock)
val fragmentStack = fragmentBreadcrumbDataSource.fragmentStack
Expand Down
Original file line number Diff line number Diff line change
@@ -1,90 +1,83 @@
package io.embrace.android.embracesdk.capture.crumbs

import io.embrace.android.embracesdk.arch.DataCaptureService
import io.embrace.android.embracesdk.arch.datasource.NoInputValidation
import io.embrace.android.embracesdk.arch.datasource.SpanDataSourceImpl
import io.embrace.android.embracesdk.arch.datasource.startSpanCapture
import io.embrace.android.embracesdk.arch.destination.StartSpanData
import io.embrace.android.embracesdk.arch.destination.StartSpanMapper
import io.embrace.android.embracesdk.arch.limits.UpToLimitStrategy
import io.embrace.android.embracesdk.config.ConfigService
import io.embrace.android.embracesdk.internal.clock.Clock
import io.embrace.android.embracesdk.logging.InternalEmbraceLogger
import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger
import io.embrace.android.embracesdk.internal.spans.SpanService
import io.embrace.android.embracesdk.payload.FragmentBreadcrumb
import io.embrace.android.embracesdk.utils.filter
import java.util.Collections
import io.embrace.android.embracesdk.spans.EmbraceSpan

/**
* Captures fragment breadcrumbs.
*/
internal class FragmentBreadcrumbDataSource(
private val configService: ConfigService,
configService: ConfigService,
private val clock: Clock,
private val store: BreadcrumbDataStore<FragmentBreadcrumb> = BreadcrumbDataStore {
configService.breadcrumbBehavior.getFragmentBreadcrumbLimit()
},
private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger
) : DataCaptureService<List<FragmentBreadcrumb>> by store {
spanService: SpanService
) : SpanDataSourceImpl(
spanService,
UpToLimitStrategy({ configService.breadcrumbBehavior.getFragmentBreadcrumbLimit() })
),
StartSpanMapper<FragmentBreadcrumb> {

companion object {

/**
* The default limit for how many open tracked fragments are allowed, which can be overridden
* by [RemoteConfig].
*/
private const val DEFAULT_VIEW_STACK_SIZE = 20
internal const val TYPE_NAME = "ux.view"
internal const val SPAN_NAME = "screen-view"
}

internal val fragmentStack: MutableList<FragmentBreadcrumb> = Collections.synchronizedList(ArrayList<FragmentBreadcrumb>())
private val fragmentSpans: MutableMap<String, EmbraceSpan> = mutableMapOf()

fun startFragment(name: String?): Boolean {
if (name == null) {
return false
}
synchronized(this) {
if (fragmentStack.size >= DEFAULT_VIEW_STACK_SIZE) {
return false
/**
* Called when a fragment is started.
*/
fun startFragment(name: String?): Boolean = captureSpanData(
countsTowardsLimits = true,
inputValidation = { !name.isNullOrEmpty() },
captureAction = {
val crumb = FragmentBreadcrumb(checkNotNull(name), clock.now())
startSpanCapture(crumb, ::toStartSpanData)?.apply {
fragmentSpans[name] = this
}
return fragmentStack.add(FragmentBreadcrumb(name, clock.now(), 0))
}
}
)

fun endFragment(name: String?): Boolean {
if (name == null) {
return false
}
var start: FragmentBreadcrumb
val end = FragmentBreadcrumb(name, 0, clock.now())
synchronized(this) {
val crumbs = filter(fragmentStack) { crumb: FragmentBreadcrumb -> crumb.name == name }
if (crumbs.isEmpty()) {
return false
}
start = crumbs[0]
fragmentStack.remove(start)
/**
* Called when a fragment is ended.
*/
fun endFragment(name: String?): Boolean = captureSpanData(
countsTowardsLimits = false,
inputValidation = { !name.isNullOrEmpty() },
captureAction = {
fragmentSpans.remove(name)?.stop()
}
end.setStartTime(start.getStartTime())
store.tryAddBreadcrumb(end)
return true
}
)

/**
* Close all open fragments when the activity closes
* Called when the activity is closed (and therefore all fragments are assumed to close).
*/
fun onViewClose() {
if (!configService.breadcrumbBehavior.isActivityBreadcrumbCaptureEnabled()) {
return
}
if (fragmentStack.size == 0) {
return
}
val ts = clock.now()
synchronized(fragmentStack) {
for (fragment in fragmentStack) {
fragment.endTime = ts
store.tryAddBreadcrumb(fragment)
}
fragmentStack.clear()
fragmentSpans.forEach { (_, span) ->
captureSpanData(
countsTowardsLimits = false,
inputValidation = NoInputValidation,
captureAction = {
span.stop()
}
)
}
}

override fun cleanCollections() {
store.cleanCollections()
fragmentStack.clear()
override fun toStartSpanData(obj: FragmentBreadcrumb): StartSpanData = with(obj) {
StartSpanData(
embType = TYPE_NAME,
spanName = SPAN_NAME,
spanStartTimeMs = start,
attributes = mapOf("view.name" to name)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.embrace.android.embracesdk.capture.crumbs

import io.embrace.android.embracesdk.arch.DataCaptureService
import io.embrace.android.embracesdk.config.ConfigService
import io.embrace.android.embracesdk.internal.clock.Clock
import io.embrace.android.embracesdk.logging.InternalEmbraceLogger
import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger
import io.embrace.android.embracesdk.payload.FragmentBreadcrumb
import io.embrace.android.embracesdk.utils.filter
import java.util.Collections

/**
* Captures fragment breadcrumbs.
*/
internal class LegacyFragmentBreadcrumbDataSource(
private val configService: ConfigService,
private val clock: Clock,
private val store: BreadcrumbDataStore<FragmentBreadcrumb> = BreadcrumbDataStore {
configService.breadcrumbBehavior.getFragmentBreadcrumbLimit()
},
private val logger: InternalEmbraceLogger = InternalStaticEmbraceLogger.logger
) : DataCaptureService<List<FragmentBreadcrumb>> by store {

companion object {

/**
* The default limit for how many open tracked fragments are allowed, which can be overridden
* by [RemoteConfig].
*/
private const val DEFAULT_VIEW_STACK_SIZE = 20
}

internal val fragmentStack: MutableList<FragmentBreadcrumb> = Collections.synchronizedList(ArrayList<FragmentBreadcrumb>())

fun startFragment(name: String?): Boolean {
if (name == null) {
return false
}
synchronized(this) {
if (fragmentStack.size >= DEFAULT_VIEW_STACK_SIZE) {
return false
}
return fragmentStack.add(FragmentBreadcrumb(name, clock.now(), 0))
}
}

fun endFragment(name: String?): Boolean {
if (name == null) {
return false
}
var start: FragmentBreadcrumb
val end = FragmentBreadcrumb(name, 0, clock.now())
synchronized(this) {
val crumbs = filter(fragmentStack) { crumb: FragmentBreadcrumb -> crumb.name == name }
if (crumbs.isEmpty()) {
return false
}
start = crumbs[0]
fragmentStack.remove(start)
}
end.setStartTime(start.getStartTime())
store.tryAddBreadcrumb(end)
return true
}

/**
* Close all open fragments when the activity closes
*/
fun onViewClose() {
if (!configService.breadcrumbBehavior.isActivityBreadcrumbCaptureEnabled()) {
return
}
if (fragmentStack.size == 0) {
return
}
val ts = clock.now()
synchronized(fragmentStack) {
for (fragment in fragmentStack) {
fragment.endTime = ts
store.tryAddBreadcrumb(fragment)
}
fragmentStack.clear()
}
}

override fun cleanCollections() {
store.cleanCollections()
fragmentStack.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.embrace.android.embracesdk.capture.crumbs

import io.embrace.android.embracesdk.fakes.FakeClock
import io.embrace.android.embracesdk.fakes.FakeConfigService
import io.embrace.android.embracesdk.fakes.FakeSpanService
import io.embrace.android.embracesdk.internal.spans.EmbraceAttributes
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

internal class FragmentBreadcrumbDataSourceTest {

private lateinit var configService: FakeConfigService
private lateinit var clock: FakeClock
private lateinit var spanService: FakeSpanService
private lateinit var dataSource: FragmentBreadcrumbDataSource

@Before
fun setUp() {
configService = FakeConfigService()
clock = FakeClock()
spanService = FakeSpanService()
dataSource = FragmentBreadcrumbDataSource(
configService,
clock,
spanService,
)
}

@Test
fun `fragment with start`() {
dataSource.startFragment("my_fragment")

val span = spanService.createdSpans.single()
assertEquals("screen-view", span.name)
assertEquals(EmbraceAttributes.Type.PERFORMANCE, span.type)
assertTrue(span.isRecording)
assertEquals(
mapOf(
"view.name" to "my_fragment",
"emb.type" to "ux.view"
),
span.attributes
)
}

@Test
fun `fragment with start and end`() {
dataSource.startFragment("my_fragment")
clock.tick(30000)
dataSource.endFragment("my_fragment")

val span = spanService.createdSpans.single()
assertEquals("screen-view", span.name)
assertEquals(EmbraceAttributes.Type.PERFORMANCE, span.type)
assertFalse(span.isRecording)
assertEquals(
mapOf(
"view.name" to "my_fragment",
"emb.type" to "ux.view"
),
span.attributes
)
}

@Test
fun `end an unknown fragment`() {
assertTrue(dataSource.endFragment("my_fragment"))
assertTrue(spanService.createdSpans.isEmpty())
}
}

0 comments on commit dbc32b8

Please sign in to comment.