Add transaction trees for opening a room so we can have a nice trace view

This commit is contained in:
Jorge Martín 2025-11-20 12:23:03 +01:00 committed by Jorge Martin Espinosa
parent 2a011bf072
commit 93feed38bf
13 changed files with 86 additions and 16 deletions

View file

@ -12,6 +12,7 @@ import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.ColdStartUntilCachedRoomList
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
@ -44,7 +45,7 @@ class DefaultAnalyticsColdStartWatcher(
if (hasConsent) { if (hasConsent) {
if (isColdStart.get()) { if (isColdStart.get()) {
Timber.d("Starting cold start check") Timber.d("Starting cold start check")
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStartUntilCachedRoomList) analyticsService.startLongRunningTransaction(ColdStartUntilCachedRoomList)
} else { } else {
error("The app is no longer in a cold start state") error("The app is no longer in a cold start state")
} }
@ -56,6 +57,7 @@ class DefaultAnalyticsColdStartWatcher(
override fun whenLoggingIn() { override fun whenLoggingIn() {
if (isColdStart.getAndSet(false)) { if (isColdStart.getAndSet(false)) {
analyticsService.removeLongRunningTransaction(ColdStartUntilCachedRoomList)
Timber.d("Canceled cold start check: user is logging in") Timber.d("Canceled cold start check: user is logging in")
} }
} }
@ -63,7 +65,7 @@ class DefaultAnalyticsColdStartWatcher(
override fun onRoomListVisible() { override fun onRoomListVisible() {
if (isColdStart.getAndSet(false)) { if (isColdStart.getAndSet(false)) {
Timber.d("Room list is visible, finishing cold start check") Timber.d("Room list is visible, finishing cold start check")
analyticsService.removeLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStartUntilCachedRoomList)?.finish() analyticsService.removeLongRunningTransaction(ColdStartUntilCachedRoomList)?.finish()
} }
} }
} }

View file

@ -48,7 +48,9 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadJoinedRoomFlow
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationTapOpensTimeline
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -111,7 +113,9 @@ class RoomFlowNode(
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.OpenRoom) val parentTransaction = analyticsService.getLongRunningTransaction(NotificationTapOpensTimeline)
val openRoomTransaction = analyticsService.startLongRunningTransaction(OpenRoom, parentTransaction)
analyticsService.startLongRunningTransaction(LoadJoinedRoomFlow, openRoomTransaction)
resolveRoomId() resolveRoomId()
} }

View file

@ -45,6 +45,10 @@ import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadJoinedRoomFlow
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -66,6 +70,7 @@ class JoinedRoomLoadedFlowNode(
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val activeRoomsHolder: ActiveRoomsHolder, private val activeRoomsHolder: ActiveRoomsHolder,
private val analyticsService: AnalyticsService,
roomGraphFactory: RoomGraphFactory, roomGraphFactory: RoomGraphFactory,
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>( ) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@ -93,6 +98,8 @@ class JoinedRoomLoadedFlowNode(
init { init {
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
val parent = analyticsService.getLongRunningTransaction(OpenRoom)
analyticsService.startLongRunningTransaction(LoadMessagesUi, parent)
Timber.v("OnCreate => ${inputs.room.roomId}") Timber.v("OnCreate => ${inputs.room.roomId}")
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId) appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
activeRoomsHolder.addRoom(inputs.room) activeRoomsHolder.addRoom(inputs.room)
@ -100,6 +107,7 @@ class JoinedRoomLoadedFlowNode(
trackVisitedRoom() trackVisitedRoom()
}, },
onResume = { onResume = {
analyticsService.removeLongRunningTransaction(LoadJoinedRoomFlow)?.finish()
sessionCoroutineScope.launch { sessionCoroutineScope.launch {
inputs.room.subscribeToSync() inputs.room.subscribeToSync()
} }

View file

@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
@ -123,6 +124,7 @@ class JoinedRoomLoadedFlowNodeTest {
roomGraphFactory = FakeRoomGraphFactory(), roomGraphFactory = FakeRoomGraphFactory(),
matrixClient = matrixClient, matrixClient = matrixClient,
activeRoomsHolder = activeRoomsHolder, activeRoomsHolder = activeRoomsHolder,
analyticsService = FakeAnalyticsService(),
) )
@Test @Test

View file

@ -68,6 +68,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -136,6 +137,9 @@ class MessagesNode(
onCreate = { onCreate = {
sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) } sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
}, },
onResume = {
analyticsService.removeLongRunningTransaction(LoadMessagesUi)?.finish()
},
onDestroy = { onDestroy = {
mediaPlayer.close() mediaPlayer.close()
} }

View file

@ -55,7 +55,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationTapOpensTimeline
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -111,6 +113,11 @@ class TimelinePresenter(
@Composable @Composable
override fun present(): TimelineState { override fun present(): TimelineState {
LaunchedEffect(Unit) {
val parent = analyticsService.getLongRunningTransaction(OpenRoom)
analyticsService.startLongRunningTransaction(DisplayFirstTimelineItems, parent)
}
val localScope = rememberCoroutineScope() val localScope = rememberCoroutineScope()
val timelineMode = remember { timelineController.mainTimelineMode() } val timelineMode = remember { timelineController.mainTimelineMode() }
@ -228,18 +235,26 @@ class TimelinePresenter(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
timelineItemsFactory.timelineItems timelineItemsFactory.timelineItems
.onEach { newTimelineItems -> .onEach { newTimelineItems ->
analyticsService.removeLongRunningTransaction(AnalyticsLongRunningTransaction.NotificationTapOpensTimeline)?.finish()
analyticsService.removeLongRunningTransaction(AnalyticsLongRunningTransaction.OpenRoom)?.finish()
timelineItemIndexer.process(newTimelineItems) timelineItemIndexer.process(newTimelineItems)
timelineItems = newTimelineItems timelineItems = newTimelineItems
analyticsService.run {
removeLongRunningTransaction(DisplayFirstTimelineItems)?.finish()
removeLongRunningTransaction(OpenRoom)?.finish()
removeLongRunningTransaction(NotificationTapOpensTimeline)?.finish()
}
} }
.launchIn(this) .launchIn(this)
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState -> combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
val parent = analyticsService.getLongRunningTransaction(DisplayFirstTimelineItems)
val transaction = parent?.startChild("timelineItemsFactory.replaceWith", "Processing timeline items")
transaction?.setData("items", items.count())
timelineItemsFactory.replaceWith( timelineItemsFactory.replaceWith(
timelineItems = items, timelineItems = items,
roomMembers = membersState.roomMembers().orEmpty() roomMembers = membersState.roomMembers().orEmpty()
) )
transaction?.finish()
items items
} }
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem) .onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.recordTransaction import io.element.android.services.analytics.api.recordTransaction
import io.element.android.services.analyticsproviders.api.recordChildTransaction import io.element.android.services.analyticsproviders.api.recordChildTransaction
@ -113,10 +114,13 @@ class RustRoomFactory(
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withLock null val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withLock null
val parentTransaction = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.OpenRoom)
if (sdkRoom.membership() == Membership.JOINED) { if (sdkRoom.membership() == Membership.JOINED) {
analyticsService.recordTransaction( analyticsService.recordTransaction(
name = "Get joined room", name = "Get joined room",
operation = "RustRoomFactory.getJoinedRoomOrPreview", operation = "RustRoomFactory.getJoinedRoomOrPreview",
parentTransaction = parentTransaction,
) { transaction -> ) { transaction ->
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads) val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
// Init the live timeline in the SDK from the Room // Init the live timeline in the SDK from the Room
@ -136,9 +140,10 @@ class RustRoomFactory(
) )
} }
val baseRoom = transaction.recordChildTransaction(operation = "getBaseRoom", description = "Get room from SDK") { getBaseRoom(sdkRoom) }
GetRoomResult.Joined( GetRoomResult.Joined(
JoinedRustRoom( JoinedRustRoom(
baseRoom = getBaseRoom(sdkRoom), baseRoom = baseRoom,
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
roomContentForwarder = roomContentForwarder, roomContentForwarder = roomContentForwarder,
liveInnerTimeline = timeline, liveInnerTimeline = timeline,
@ -152,6 +157,7 @@ class RustRoomFactory(
analyticsService.recordTransaction( analyticsService.recordTransaction(
name = "Get preview of room", name = "Get preview of room",
operation = "RustRoomFactory.getJoinedRoomOrPreview", operation = "RustRoomFactory.getJoinedRoomOrPreview",
parentTransaction = parentTransaction,
) { ) {
val preview = try { val preview = try {
sdkRoom.previewRoom(via = serverNames) sdkRoom.previewRoom(via = serverNames)

View file

@ -16,4 +16,7 @@ sealed class AnalyticsLongRunningTransaction(
data object ResumeAppUntilNewRoomsReceived : AnalyticsLongRunningTransaction("App was resumed and new room list items arrived", null) data object ResumeAppUntilNewRoomsReceived : AnalyticsLongRunningTransaction("App was resumed and new room list items arrived", null)
data object NotificationTapOpensTimeline : AnalyticsLongRunningTransaction("A notification was tapped and it opened a timeline", null) data object NotificationTapOpensTimeline : AnalyticsLongRunningTransaction("A notification was tapped and it opened a timeline", null)
data object OpenRoom : AnalyticsLongRunningTransaction("Open a room and see loaded items in the timeline", null) data object OpenRoom : AnalyticsLongRunningTransaction("Open a room and see loaded items in the timeline", null)
data object LoadJoinedRoomFlow : AnalyticsLongRunningTransaction("Load joined room UI", "ui.load")
data object LoadMessagesUi : AnalyticsLongRunningTransaction("Load messages UI", "ui.load")
data object DisplayFirstTimelineItems : AnalyticsLongRunningTransaction("Get and display first timeline items", null)
} }

View file

@ -58,7 +58,10 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
/** /**
* Starts an [AnalyticsLongRunningTransaction], that can be shared with other components. * Starts an [AnalyticsLongRunningTransaction], that can be shared with other components.
*/ */
fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction fun startLongRunningTransaction(
longRunningTransaction: AnalyticsLongRunningTransaction,
parentTransaction: AnalyticsTransaction? = null
): AnalyticsTransaction
/** /**
* Gets an ongoing [AnalyticsLongRunningTransaction], if it exists. * Gets an ongoing [AnalyticsLongRunningTransaction], if it exists.
@ -71,8 +74,14 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
fun removeLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction? fun removeLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction?
} }
inline fun <T> AnalyticsService.recordTransaction(name: String, operation: String, block: (AnalyticsTransaction) -> T): T { inline fun <T> AnalyticsService.recordTransaction(
val transaction = startTransaction(name, operation) name: String,
operation: String,
parentTransaction: AnalyticsTransaction? = null,
block: (AnalyticsTransaction) -> T
): T {
val transaction = parentTransaction?.startChild(name, operation)
?: startTransaction(name, operation)
try { try {
val result = block(transaction) val result = block(transaction)
return result return result

View file

@ -153,8 +153,13 @@ class DefaultAnalyticsService(
} ?: NoopAnalyticsTransaction } ?: NoopAnalyticsTransaction
} }
override fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction { override fun startLongRunningTransaction(
val transaction = startTransaction(longRunningTransaction.name, longRunningTransaction.operation) longRunningTransaction: AnalyticsLongRunningTransaction,
parentTransaction: AnalyticsTransaction?,
): AnalyticsTransaction {
val transaction = parentTransaction?.startChild(longRunningTransaction.name, longRunningTransaction.operation)
?: startTransaction(longRunningTransaction.name, longRunningTransaction.operation)
pendingLongRunningTransactions[longRunningTransaction] = transaction pendingLongRunningTransactions[longRunningTransaction] = transaction
return transaction return transaction
} }

View file

@ -39,7 +39,10 @@ class NoopAnalyticsService : AnalyticsService {
override fun trackError(throwable: Throwable) = Unit override fun trackError(throwable: Throwable) = Unit
override fun updateSuperProperties(updatedProperties: SuperProperties) = Unit override fun updateSuperProperties(updatedProperties: SuperProperties) = Unit
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction = NoopAnalyticsTransaction override fun startTransaction(name: String, operation: String?): AnalyticsTransaction = NoopAnalyticsTransaction
override fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction = NoopAnalyticsTransaction override fun startLongRunningTransaction(
longRunningTransaction: AnalyticsLongRunningTransaction,
parentTransaction: AnalyticsTransaction?,
): AnalyticsTransaction = NoopAnalyticsTransaction
override fun getLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction? = null override fun getLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction? = null
override fun removeLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) = NoopAnalyticsTransaction override fun removeLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) = NoopAnalyticsTransaction
} }

View file

@ -71,7 +71,10 @@ class FakeAnalyticsService(
} }
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction = NoopAnalyticsTransaction override fun startTransaction(name: String, operation: String?): AnalyticsTransaction = NoopAnalyticsTransaction
override fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction { override fun startLongRunningTransaction(
longRunningTransaction: AnalyticsLongRunningTransaction,
parentTransaction: AnalyticsTransaction?
): AnalyticsTransaction {
longRunningTransactions[longRunningTransaction] = NoopAnalyticsTransaction longRunningTransactions[longRunningTransaction] = NoopAnalyticsTransaction
return NoopAnalyticsTransaction return NoopAnalyticsTransaction
} }

View file

@ -9,7 +9,9 @@ package io.element.android.services.analyticsproviders.sentry
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
import io.sentry.ISpan import io.sentry.ISpan
import io.sentry.ITransaction
import io.sentry.Sentry import io.sentry.Sentry
import timber.log.Timber
class SentryAnalyticsTransaction private constructor(span: ISpan) : AnalyticsTransaction { class SentryAnalyticsTransaction private constructor(span: ISpan) : AnalyticsTransaction {
constructor(name: String, operation: String?) : this(Sentry.startTransaction(name, operation.orEmpty())) constructor(name: String, operation: String?) : this(Sentry.startTransaction(name, operation.orEmpty()))
@ -20,5 +22,9 @@ class SentryAnalyticsTransaction private constructor(span: ISpan) : AnalyticsTra
) )
override fun setData(key: String, value: Any) = inner.setData(key, value) override fun setData(key: String, value: Any) = inner.setData(key, value)
override fun isFinished(): Boolean = inner.isFinished override fun isFinished(): Boolean = inner.isFinished
override fun finish() = inner.finish() override fun finish() {
val name = if (inner is ITransaction) inner.name else inner.operation
Timber.d("Finishing transaction: $name")
inner.finish()
}
} }