Add extra analytics for notification performance (#6237)

* Add extra analytics for notification performance

Add technical spans to track how long a notification fetching work request takes to run, then how long it takes to actually fetch the events for the notifications

* Remove `withContext(io)` for `FetchNotificationsWorker`

The default `Dispatchers.Default` dispatcher used should be good enough and more performant

* Add network check span
This commit is contained in:
Jorge Martin Espinosa
2026-02-24 13:39:32 +01:00
committed by GitHub
parent 28c1c078a0
commit 2f80b101c5
12 changed files with 100 additions and 13 deletions

View File

@@ -86,6 +86,7 @@ dependencies {
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.services.appnavstate.impl) testImplementation(projects.services.appnavstate.impl)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.services.toolbox.impl) testImplementation(projects.services.toolbox.impl)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.featureflag.test)

View File

@@ -25,6 +25,9 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.finishLongRunningTransaction
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import timber.log.Timber import timber.log.Timber
@@ -36,6 +39,7 @@ class NotificationRenderer(
private val notificationDataFactory: NotificationDataFactory, private val notificationDataFactory: NotificationDataFactory,
private val enterpriseService: EnterpriseService, private val enterpriseService: EnterpriseService,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val analyticsService: AnalyticsService,
) { ) {
suspend fun render( suspend fun render(
currentUser: MatrixUser, currentUser: MatrixUser,
@@ -124,6 +128,12 @@ class NotificationRenderer(
notification = summaryNotification.notification notification = summaryNotification.notification
) )
} }
for (event in eventsToProcess) {
// Finish long-running transaction
val uploaded = analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(event.eventId.value))
Timber.d("Push-to-notification for event ${event.eventId} uploaded: $uploaded")
}
} }
} }

View File

@@ -40,6 +40,8 @@ import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.api.PushHandler
import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -69,6 +71,7 @@ class DefaultPushHandler(
private val fallbackNotificationFactory: FallbackNotificationFactory, private val fallbackNotificationFactory: FallbackNotificationFactory,
private val syncOnNotifiableEvent: SyncOnNotifiableEvent, private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val analyticsService: AnalyticsService,
) : PushHandler { ) : PushHandler {
init { init {
processPushEventResults() processPushEventResults()
@@ -215,6 +218,13 @@ class DefaultPushHandler(
* @param providerInfo the provider info. * @param providerInfo the provider info.
*/ */
override suspend fun handle(pushData: PushData, providerInfo: String) { override suspend fun handle(pushData: PushData, providerInfo: String) {
// Start measuring how long it takes to display a notification from when the push is received
Timber.d("Calculating push-to-notification for event ${pushData.eventId}")
val parent = analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(pushData.eventId.value))
if (featureFlagService.isFeatureEnabled(FeatureFlags.SyncNotificationsWithWorkManager)) {
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(pushData.eventId.value), parent)
}
Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}") Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}")
if (buildMeta.lowPrivacyLoggingEnabled) { if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.tag(loggerTag.value).d("## pushData: $pushData") Timber.tag(loggerTag.value).d("## pushData: $pushData")

View File

@@ -19,7 +19,6 @@ import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.binding import dev.zacsweers.metro.binding
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.auth.SessionRestorationException import io.element.android.libraries.matrix.api.auth.SessionRestorationException
@@ -31,10 +30,13 @@ import io.element.android.libraries.push.impl.notifications.NotificationResolver
import io.element.android.libraries.workmanager.api.WorkManagerScheduler import io.element.android.libraries.workmanager.api.WorkManagerScheduler
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import io.element.android.libraries.workmanager.api.di.WorkerKey import io.element.android.libraries.workmanager.api.di.WorkerKey
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.finishLongRunningTransaction
import io.element.android.services.analytics.api.recordTransaction
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber import timber.log.Timber
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -48,22 +50,45 @@ class FetchNotificationsWorker(
private val queue: NotificationResolverQueue, private val queue: NotificationResolverQueue,
private val workManagerScheduler: WorkManagerScheduler, private val workManagerScheduler: WorkManagerScheduler,
private val syncOnNotifiableEvent: SyncOnNotifiableEvent, private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
private val coroutineDispatchers: CoroutineDispatchers,
private val workerDataConverter: SyncNotificationsWorkerDataConverter, private val workerDataConverter: SyncNotificationsWorkerDataConverter,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val analyticsService: AnalyticsService,
) : CoroutineWorker(context, workerParams) { ) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) { override suspend fun doWork(): Result {
Timber.d("FetchNotificationsWorker started") Timber.d("FetchNotificationsWorker started")
val requests = workerDataConverter.deserialize(inputData) ?: return@withContext Result.failure() val requests = workerDataConverter.deserialize(inputData) ?: return Result.failure()
// Wait for network to be available, but not more than 10 seconds // Wait for network to be available, but not more than 10 seconds
val networkTimeoutSpans = requests.mapNotNull { request ->
val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(request.eventId.value))
parent?.startChild("Waiting for network connectivity", "await_network")
}
val hasNetwork = withTimeoutOrNull(10.seconds) { val hasNetwork = withTimeoutOrNull(10.seconds) {
networkMonitor.connectivity.first { it == NetworkStatus.Connected } networkMonitor.connectivity.first { it == NetworkStatus.Connected }
} != null } != null
for (span in networkTimeoutSpans) {
span.finish()
}
if (!hasNetwork) { if (!hasNetwork) {
Timber.w("No network, retrying later") Timber.w("No network, retrying later")
return@withContext Result.retry() for (request in requests) {
val eventId = request.eventId.value
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(eventId))
val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(eventId))
// Since we're retrying, start a new transaction
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(eventId), parent)
}
return Result.retry()
} }
val pendingAnalyticTransactions = requests.mapNotNull { request ->
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(request.eventId.value))
val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(request.eventId.value))
val transactionName = "WorkManager to event fetched"
parent?.startChild(transactionName)?.let { request.eventId to it }
}.toMap()
val failedSyncForSessions = mutableMapOf<SessionId, Throwable>() val failedSyncForSessions = mutableMapOf<SessionId, Throwable>()
val groupedRequests = requests.groupBy { it.sessionId }.toMutableMap() val groupedRequests = requests.groupBy { it.sessionId }.toMutableMap()
@@ -72,10 +97,17 @@ class FetchNotificationsWorker(
eventResolver.resolveEvents(sessionId, notificationRequests) eventResolver.resolveEvents(sessionId, notificationRequests)
.fold( .fold(
onSuccess = { result -> onSuccess = { result ->
for ((_, transaction) in pendingAnalyticTransactions) {
transaction.finish()
}
// Update the resolved results in the queue // Update the resolved results in the queue
(queue.results as MutableSharedFlow).emit(requests to result) (queue.results as MutableSharedFlow).emit(requests to result)
}, },
onFailure = { onFailure = {
for ((_, transaction) in pendingAnalyticTransactions) {
transaction.attachError(it)
transaction.finish()
}
failedSyncForSessions[sessionId] = it failedSyncForSessions[sessionId] = it
Timber.e(it, "Failed to resolve notification events for session $sessionId") Timber.e(it, "Failed to resolve notification events for session $sessionId")
} }
@@ -92,6 +124,18 @@ class FetchNotificationsWorker(
continue continue
} }
val requestsToRetry = groupedRequests[failedSessionId] ?: continue val requestsToRetry = groupedRequests[failedSessionId] ?: continue
for (request in requestsToRetry) {
val failedTransaction = pendingAnalyticTransactions[request.eventId]
failedTransaction?.attachError(exception)
failedTransaction?.finish()
val eventId = request.eventId.value
val parent = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.PushToNotification(eventId))
// Since we're retrying, start a new transaction
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.PushToWorkManager(eventId), parent)
}
Timber.d("Re-scheduling ${requestsToRetry.size} failed notification requests for session $failedSessionId") Timber.d("Re-scheduling ${requestsToRetry.size} failed notification requests for session $failedSessionId")
workManagerScheduler.submit( workManagerScheduler.submit(
SyncNotificationWorkManagerRequest( SyncNotificationWorkManagerRequest(
@@ -106,9 +150,11 @@ class FetchNotificationsWorker(
Timber.d("Notifications processed successfully") Timber.d("Notifications processed successfully")
performOpportunisticSyncIfNeeded(groupedRequests) analyticsService.recordTransaction("Opportunistic sync", "opportunistic_sync") {
performOpportunisticSyncIfNeeded(groupedRequests)
}
Result.success() return Result.success()
} }
private suspend fun performOpportunisticSyncIfNeeded( private suspend fun performOpportunisticSyncIfNeeded(

View File

@@ -40,6 +40,7 @@ import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
@@ -508,6 +509,7 @@ fun TestScope.createDefaultNotificationDrawerManager(
sessionStore: SessionStore = InMemorySessionStore(), sessionStore: SessionStore = InMemorySessionStore(),
enterpriseService: EnterpriseService = FakeEnterpriseService(), enterpriseService: EnterpriseService = FakeEnterpriseService(),
sessionObserver: SessionObserver = FakeSessionObserver(), sessionObserver: SessionObserver = FakeSessionObserver(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
): DefaultNotificationDrawerManager { ): DefaultNotificationDrawerManager {
return DefaultNotificationDrawerManager( return DefaultNotificationDrawerManager(
notificationDisplayer = notificationDisplayer, notificationDisplayer = notificationDisplayer,
@@ -521,6 +523,7 @@ fun TestScope.createDefaultNotificationDrawerManager(
), ),
enterpriseService = enterpriseService, enterpriseService = enterpriseService,
sessionStore = sessionStore, sessionStore = sessionStore,
analyticsService = analyticsService,
), ),
appNavigationStateService = appNavigationStateService, appNavigationStateService = appNavigationStateService,
coroutineScope = backgroundScope, coroutineScope = backgroundScope,

View File

@@ -29,6 +29,7 @@ import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNot
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -122,9 +123,11 @@ fun createNotificationRenderer(
notificationDataFactory: NotificationDataFactory = FakeNotificationDataFactory(), notificationDataFactory: NotificationDataFactory = FakeNotificationDataFactory(),
enterpriseService: EnterpriseService = FakeEnterpriseService(), enterpriseService: EnterpriseService = FakeEnterpriseService(),
sessionStore: SessionStore = InMemorySessionStore(), sessionStore: SessionStore = InMemorySessionStore(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
) = NotificationRenderer( ) = NotificationRenderer(
notificationDisplayer = notificationDisplayer, notificationDisplayer = notificationDisplayer,
notificationDataFactory = notificationDataFactory, notificationDataFactory = notificationDataFactory,
enterpriseService = enterpriseService, enterpriseService = enterpriseService,
sessionStore = sessionStore, sessionStore = sessionStore,
analyticsService = analyticsService,
) )

View File

@@ -56,6 +56,7 @@ import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushSto
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
import io.element.android.libraries.workmanager.api.WorkManagerRequest import io.element.android.libraries.workmanager.api.WorkManagerRequest
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.any
@@ -683,6 +684,7 @@ class DefaultPushHandlerTest {
syncOnNotifiableEvent: SyncOnNotifiableEvent = SyncOnNotifiableEvent {}, syncOnNotifiableEvent: SyncOnNotifiableEvent = SyncOnNotifiableEvent {},
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.SyncNotificationsWithWorkManager.key to false)), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.SyncNotificationsWithWorkManager.key to false)),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(), workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
): DefaultPushHandler { ): DefaultPushHandler {
return DefaultPushHandler( return DefaultPushHandler(
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived), onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventsReceived),
@@ -715,6 +717,7 @@ class DefaultPushHandlerTest {
), ),
syncOnNotifiableEvent = syncOnNotifiableEvent, syncOnNotifiableEvent = syncOnNotifiableEvent,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
analyticsService = analyticsService,
) )
} }
} }

View File

@@ -29,9 +29,9 @@ import io.element.android.libraries.push.test.notifications.FakeNotificationReso
import io.element.android.libraries.workmanager.api.WorkManagerRequest import io.element.android.libraries.workmanager.api.WorkManagerRequest
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceTimeBy
@@ -168,6 +168,7 @@ class FetchNotificationWorkerTest {
), ),
workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(), workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(),
syncOnNotifiableEvent: SyncOnNotifiableEvent = SyncOnNotifiableEvent {}, syncOnNotifiableEvent: SyncOnNotifiableEvent = SyncOnNotifiableEvent {},
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
) = FetchNotificationsWorker( ) = FetchNotificationsWorker(
workerParams = createWorkerParams(workDataOf("requests" to input)), workerParams = createWorkerParams(workDataOf("requests" to input)),
context = InstrumentationRegistry.getInstrumentation().context, context = InstrumentationRegistry.getInstrumentation().context,
@@ -176,9 +177,9 @@ class FetchNotificationWorkerTest {
queue = queue, queue = queue,
workManagerScheduler = workManagerScheduler, workManagerScheduler = workManagerScheduler,
syncOnNotifiableEvent = syncOnNotifiableEvent, syncOnNotifiableEvent = syncOnNotifiableEvent,
coroutineDispatchers = testCoroutineDispatchers(),
workerDataConverter = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()), workerDataConverter = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()),
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33), buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33),
analyticsService = analyticsService,
) )
private fun TestScope.createWorkerParams( private fun TestScope.createWorkerParams(

View File

@@ -28,4 +28,6 @@ sealed class AnalyticsLongRunningTransaction(
data object LoadJoinedRoomFlow : AnalyticsLongRunningTransaction("Load joined room UI", "ui.load") data object LoadJoinedRoomFlow : AnalyticsLongRunningTransaction("Load joined room UI", "ui.load")
data object LoadMessagesUi : AnalyticsLongRunningTransaction("Load messages UI", "ui.load") data object LoadMessagesUi : AnalyticsLongRunningTransaction("Load messages UI", "ui.load")
data object DisplayFirstTimelineItems : AnalyticsLongRunningTransaction("Get and display first timeline items", null) data object DisplayFirstTimelineItems : AnalyticsLongRunningTransaction("Get and display first timeline items", null)
data class PushToNotification(val eventId: String) : AnalyticsLongRunningTransaction(AnalyticsTransactions.pushToNotification)
data class PushToWorkManager(val eventId: String) : AnalyticsLongRunningTransaction("Push to WorkManager")
} }

View File

@@ -108,11 +108,12 @@ fun AnalyticsService.cancelLongRunningTransaction(
fun AnalyticsService.finishLongRunningTransaction( fun AnalyticsService.finishLongRunningTransaction(
longRunningTransaction: AnalyticsLongRunningTransaction, longRunningTransaction: AnalyticsLongRunningTransaction,
action: (AnalyticsTransaction) -> Unit = {}, action: (AnalyticsTransaction) -> Unit = {},
) { ): Boolean {
removeLongRunningTransaction(longRunningTransaction)?.let { return removeLongRunningTransaction(longRunningTransaction)?.let {
action(it) action(it)
it.finish() it.finish()
} true
} ?: false
} }
inline fun <T> AnalyticsService.inBridgeSdkSpan(parentTraceId: String?, block: (AnalyticsSdkSpan) -> T): T { inline fun <T> AnalyticsService.inBridgeSdkSpan(parentTraceId: String?, block: (AnalyticsSdkSpan) -> T): T {

View File

@@ -40,5 +40,6 @@ dependencies {
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.services.analyticsproviders.test) testImplementation(projects.services.analyticsproviders.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
} }

View File

@@ -37,6 +37,12 @@ object AnalyticsTransactions {
operation = "ux", operation = "ux",
description = "Send to sent state in timeline", description = "Send to sent state in timeline",
) )
val pushToNotification = TransactionDefinition(
name = "Push to notification",
operation = "push_to_notification",
description = "Time from receiving a push notification until it's displayed",
)
} }
data class TransactionDefinition( data class TransactionDefinition(