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 io.element.android.libraries.di.annotations.AppCoroutineScope
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.catch
@@ -44,7 +45,7 @@ class DefaultAnalyticsColdStartWatcher(
if (hasConsent) {
if (isColdStart.get()) {
Timber.d("Starting cold start check")
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStartUntilCachedRoomList)
analyticsService.startLongRunningTransaction(ColdStartUntilCachedRoomList)
} else {
error("The app is no longer in a cold start state")
}
@@ -56,6 +57,7 @@ class DefaultAnalyticsColdStartWatcher(
override fun whenLoggingIn() {
if (isColdStart.getAndSet(false)) {
analyticsService.removeLongRunningTransaction(ColdStartUntilCachedRoomList)
Timber.d("Canceled cold start check: user is logging in")
}
}
@@ -63,7 +65,7 @@ class DefaultAnalyticsColdStartWatcher(
override fun onRoomListVisible() {
if (isColdStart.getAndSet(false)) {
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.alias.ResolvedRoomAlias
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 kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -111,7 +113,9 @@ class RoomFlowNode(
override fun onBuilt() {
super.onBuilt()
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.OpenRoom)
val parentTransaction = analyticsService.getLongRunningTransaction(NotificationTapOpensTimeline)
val openRoomTransaction = analyticsService.startLongRunningTransaction(OpenRoom, parentTransaction)
analyticsService.startLongRunningTransaction(LoadJoinedRoomFlow, openRoomTransaction)
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.permalink.PermalinkData
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.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
@@ -66,6 +70,7 @@ class JoinedRoomLoadedFlowNode(
private val sessionCoroutineScope: CoroutineScope,
private val matrixClient: MatrixClient,
private val activeRoomsHolder: ActiveRoomsHolder,
private val analyticsService: AnalyticsService,
roomGraphFactory: RoomGraphFactory,
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
backstack = BackStack(
@@ -93,6 +98,8 @@ class JoinedRoomLoadedFlowNode(
init {
lifecycle.subscribe(
onCreate = {
val parent = analyticsService.getLongRunningTransaction(OpenRoom)
analyticsService.startLongRunningTransaction(LoadMessagesUi, parent)
Timber.v("OnCreate => ${inputs.room.roomId}")
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
activeRoomsHolder.addRoom(inputs.room)
@@ -100,6 +107,7 @@ class JoinedRoomLoadedFlowNode(
trackVisitedRoom()
},
onResume = {
analyticsService.removeLongRunningTransaction(LoadJoinedRoomFlow)?.finish()
sessionCoroutineScope.launch {
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.FakeJoinedRoom
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.impl.DefaultActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
@@ -123,6 +124,7 @@ class JoinedRoomLoadedFlowNodeTest {
roomGraphFactory = FakeRoomGraphFactory(),
matrixClient = matrixClient,
activeRoomsHolder = activeRoomsHolder,
analyticsService = FakeAnalyticsService(),
)
@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.mediaplayer.api.MediaPlayer
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -136,6 +137,9 @@ class MessagesNode(
onCreate = {
sessionCoroutineScope.launch { analyticsService.capture(room.toAnalyticsViewRoom()) }
},
onResume = {
analyticsService.removeLongRunningTransaction(LoadMessagesUi)?.finish()
},
onDestroy = {
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.ui.room.canSendMessageAsState
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -111,6 +113,11 @@ class TimelinePresenter(
@Composable
override fun present(): TimelineState {
LaunchedEffect(Unit) {
val parent = analyticsService.getLongRunningTransaction(OpenRoom)
analyticsService.startLongRunningTransaction(DisplayFirstTimelineItems, parent)
}
val localScope = rememberCoroutineScope()
val timelineMode = remember { timelineController.mainTimelineMode() }
@@ -228,18 +235,26 @@ class TimelinePresenter(
LaunchedEffect(Unit) {
timelineItemsFactory.timelineItems
.onEach { newTimelineItems ->
analyticsService.removeLongRunningTransaction(AnalyticsLongRunningTransaction.NotificationTapOpensTimeline)?.finish()
analyticsService.removeLongRunningTransaction(AnalyticsLongRunningTransaction.OpenRoom)?.finish()
timelineItemIndexer.process(newTimelineItems)
timelineItems = newTimelineItems
analyticsService.run {
removeLongRunningTransaction(DisplayFirstTimelineItems)?.finish()
removeLongRunningTransaction(OpenRoom)?.finish()
removeLongRunningTransaction(NotificationTapOpensTimeline)?.finish()
}
}
.launchIn(this)
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(
timelineItems = items,
roomMembers = membersState.roomMembers().orEmpty()
)
transaction?.finish()
items
}
.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.impl.room.preview.RoomPreviewInfoMapper
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.recordTransaction
import io.element.android.services.analyticsproviders.api.recordChildTransaction
@@ -113,10 +114,13 @@ class RustRoomFactory(
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withLock null
val parentTransaction = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.OpenRoom)
if (sdkRoom.membership() == Membership.JOINED) {
analyticsService.recordTransaction(
name = "Get joined room",
operation = "RustRoomFactory.getJoinedRoomOrPreview",
parentTransaction = parentTransaction,
) { transaction ->
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
// 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(
JoinedRustRoom(
baseRoom = getBaseRoom(sdkRoom),
baseRoom = baseRoom,
notificationSettingsService = notificationSettingsService,
roomContentForwarder = roomContentForwarder,
liveInnerTimeline = timeline,
@@ -152,6 +157,7 @@ class RustRoomFactory(
analyticsService.recordTransaction(
name = "Get preview of room",
operation = "RustRoomFactory.getJoinedRoomOrPreview",
parentTransaction = parentTransaction,
) {
val preview = try {
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 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 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.
*/
fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction
fun startLongRunningTransaction(
longRunningTransaction: AnalyticsLongRunningTransaction,
parentTransaction: AnalyticsTransaction? = null
): AnalyticsTransaction
/**
* Gets an ongoing [AnalyticsLongRunningTransaction], if it exists.
@@ -71,8 +74,14 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
fun removeLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction?
}
inline fun <T> AnalyticsService.recordTransaction(name: String, operation: String, block: (AnalyticsTransaction) -> T): T {
val transaction = startTransaction(name, operation)
inline fun <T> AnalyticsService.recordTransaction(
name: String,
operation: String,
parentTransaction: AnalyticsTransaction? = null,
block: (AnalyticsTransaction) -> T
): T {
val transaction = parentTransaction?.startChild(name, operation)
?: startTransaction(name, operation)
try {
val result = block(transaction)
return result

View File

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

View File

@@ -39,7 +39,10 @@ class NoopAnalyticsService : AnalyticsService {
override fun trackError(throwable: Throwable) = Unit
override fun updateSuperProperties(updatedProperties: SuperProperties) = Unit
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 removeLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) = NoopAnalyticsTransaction
}

View File

@@ -71,7 +71,10 @@ class FakeAnalyticsService(
}
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
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.sentry.ISpan
import io.sentry.ITransaction
import io.sentry.Sentry
import timber.log.Timber
class SentryAnalyticsTransaction private constructor(span: ISpan) : AnalyticsTransaction {
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 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()
}
}