Add some performance metrics for Sentry (#5760)
- Add `AnalyticsService.startTransaction(...)` to start a logging transaction that can be uploaded to Sentry if the user enabled the analytics upload. - Add `AnalyticsTransaction` wrapper to abstract the Sentry ones. - Added several helper methods to improve the UX around these transactions. - Then measure: - Time until the first sync, and how it ended. - Time until the first rooms are displayed. - Time to load a room or a preview. - Time to load a timeline.
This commit is contained in:
parent
de3ffca1af
commit
c6c2f4a267
25 changed files with 245 additions and 41 deletions
|
|
@ -90,6 +90,8 @@ import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
|
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
|
||||||
import io.element.android.libraries.ui.common.nodes.emptyNode
|
import io.element.android.libraries.ui.common.nodes.emptyNode
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -136,6 +138,7 @@ class LoggedInFlowNode(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Placeholder,
|
initialElement = NavTarget.Placeholder,
|
||||||
|
|
@ -212,6 +215,8 @@ class LoggedInFlowNode(
|
||||||
matrixClient.getMaxFileUploadSize()
|
matrixClient.getMaxFileUploadSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.FirstRoomsDisplayed)
|
||||||
|
|
||||||
ftueService.state
|
ftueService.state
|
||||||
.onEach { ftueState ->
|
.onEach { ftueState ->
|
||||||
when (ftueState) {
|
when (ftueState) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.coroutine.childScope
|
import io.element.android.libraries.core.coroutine.childScope
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.api.recordTransaction
|
||||||
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
import io.element.android.services.appnavstate.api.AppForegroundStateService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
|
@ -39,6 +41,7 @@ class SyncOrchestrator(
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
dispatchers: CoroutineDispatchers,
|
dispatchers: CoroutineDispatchers,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
) {
|
) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -69,10 +72,13 @@ class SyncOrchestrator(
|
||||||
// Perform an initial sync if the sync service is not running, to check whether the homeserver is accessible
|
// Perform an initial sync if the sync service is not running, to check whether the homeserver is accessible
|
||||||
// Otherwise, if the device is offline the sync service will never start and the SyncState will be Idle, not Offline
|
// Otherwise, if the device is offline the sync service will never start and the SyncState will be Idle, not Offline
|
||||||
Timber.tag(tag).d("performing initial sync attempt")
|
Timber.tag(tag).d("performing initial sync attempt")
|
||||||
syncService.startSync()
|
analyticsService.recordTransaction("First sync", "syncService.startSync()") { transaction ->
|
||||||
|
syncService.startSync()
|
||||||
|
|
||||||
// Wait until the sync service is not idle, either it will be running or in error/offline state
|
// Wait until the sync service is not idle, either it will be running or in error/offline state
|
||||||
syncService.syncState.first { it != SyncState.Idle }
|
val firstState = syncService.syncState.first { it != SyncState.Idle }
|
||||||
|
transaction.setData("first_sync_state", firstState.name)
|
||||||
|
}
|
||||||
|
|
||||||
observeStates()
|
observeStates()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
|
@ -390,5 +391,6 @@ class SyncOrchestratorTest {
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -129,6 +130,7 @@ class MatrixSessionCacheTest {
|
||||||
appForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService = FakeAppForegroundStateService(),
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||||
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -131,6 +132,7 @@ class RustMatrixClient(
|
||||||
clock: SystemClock,
|
clock: SystemClock,
|
||||||
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
) : MatrixClient {
|
) : MatrixClient {
|
||||||
override val sessionId: UserId = UserId(innerClient.userId())
|
override val sessionId: UserId = UserId(innerClient.userId())
|
||||||
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
|
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
|
||||||
|
|
@ -178,6 +180,7 @@ class RustMatrixClient(
|
||||||
roomListFactory = RoomListFactory(
|
roomListFactory = RoomListFactory(
|
||||||
innerRoomListService = innerRoomListService,
|
innerRoomListService = innerRoomListService,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
|
analyticsService = analyticsService,
|
||||||
),
|
),
|
||||||
roomSyncSubscriber = roomSyncSubscriber,
|
roomSyncSubscriber = roomSyncSubscriber,
|
||||||
)
|
)
|
||||||
|
|
@ -212,6 +215,7 @@ class RustMatrixClient(
|
||||||
roomMembershipObserver = roomMembershipObserver,
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
roomInfoMapper = roomInfoMapper,
|
roomInfoMapper = roomInfoMapper,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
analyticsService = analyticsService,
|
||||||
)
|
)
|
||||||
|
|
||||||
override val matrixMediaLoader: MatrixMediaLoader = RustMediaLoader(
|
override val matrixMediaLoader: MatrixMediaLoader = RustMediaLoader(
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ class RustMatrixClientFactory(
|
||||||
clock = clock,
|
clock = clock,
|
||||||
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
|
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
analyticsService = analyticsService,
|
||||||
).also {
|
).also {
|
||||||
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ 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.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.api.recordTransaction
|
||||||
|
import io.element.android.services.analyticsproviders.api.recordChildTransaction
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
|
@ -54,6 +57,7 @@ class RustRoomFactory(
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val roomMembershipObserver: RoomMembershipObserver,
|
private val roomMembershipObserver: RoomMembershipObserver,
|
||||||
private val roomInfoMapper: RoomInfoMapper,
|
private val roomInfoMapper: RoomInfoMapper,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
) {
|
) {
|
||||||
private val dispatcher = dispatchers.io.limitedParallelism(1)
|
private val dispatcher = dispatchers.io.limitedParallelism(1)
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
@ -106,48 +110,64 @@ class RustRoomFactory(
|
||||||
Timber.d("Room factory is destroyed, returning null for $roomId")
|
Timber.d("Room factory is destroyed, returning null for $roomId")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null
|
|
||||||
|
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withLock null
|
||||||
|
|
||||||
if (sdkRoom.membership() == Membership.JOINED) {
|
if (sdkRoom.membership() == Membership.JOINED) {
|
||||||
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
|
analyticsService.recordTransaction(
|
||||||
// Init the live timeline in the SDK from the Room
|
name = "Get joined room",
|
||||||
val timeline = sdkRoom.timelineWithConfiguration(
|
operation = "RustRoomFactory.getJoinedRoomOrPreview",
|
||||||
TimelineConfiguration(
|
) { transaction ->
|
||||||
focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents),
|
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
|
||||||
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
|
// Init the live timeline in the SDK from the Room
|
||||||
internalIdPrefix = "live",
|
val timeline = transaction.recordChildTransaction(
|
||||||
dateDividerMode = DateDividerMode.DAILY,
|
operation = "sdkRoom.timelineWithConfiguration",
|
||||||
trackReadReceipts = true,
|
description = "Get timeline from the SDK",
|
||||||
reportUtds = true,
|
) {
|
||||||
)
|
sdkRoom.timelineWithConfiguration(
|
||||||
)
|
TimelineConfiguration(
|
||||||
|
focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents),
|
||||||
|
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
|
||||||
|
internalIdPrefix = "live",
|
||||||
|
dateDividerMode = DateDividerMode.DAILY,
|
||||||
|
trackReadReceipts = true,
|
||||||
|
reportUtds = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
GetRoomResult.Joined(
|
GetRoomResult.Joined(
|
||||||
JoinedRustRoom(
|
JoinedRustRoom(
|
||||||
baseRoom = getBaseRoom(sdkRoom),
|
baseRoom = getBaseRoom(sdkRoom),
|
||||||
notificationSettingsService = notificationSettingsService,
|
notificationSettingsService = notificationSettingsService,
|
||||||
roomContentForwarder = roomContentForwarder,
|
roomContentForwarder = roomContentForwarder,
|
||||||
liveInnerTimeline = timeline,
|
liveInnerTimeline = timeline,
|
||||||
coroutineDispatchers = dispatchers,
|
coroutineDispatchers = dispatchers,
|
||||||
systemClock = systemClock,
|
systemClock = systemClock,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val preview = try {
|
|
||||||
sdkRoom.previewRoom(via = serverNames)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Failed to get room preview for $roomId")
|
|
||||||
return@withContext null
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
analyticsService.recordTransaction(
|
||||||
|
name = "Get preview of room",
|
||||||
|
operation = "RustRoomFactory.getJoinedRoomOrPreview",
|
||||||
|
) {
|
||||||
|
val preview = try {
|
||||||
|
sdkRoom.previewRoom(via = serverNames)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to get room preview for $roomId")
|
||||||
|
return@recordTransaction null
|
||||||
|
}
|
||||||
|
|
||||||
GetRoomResult.NotJoined(
|
GetRoomResult.NotJoined(
|
||||||
NotJoinedRustRoom(
|
NotJoinedRustRoom(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
localRoom = getBaseRoom(sdkRoom),
|
localRoom = getBaseRoom(sdkRoom),
|
||||||
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
|
previewInfo = RoomPreviewInfoMapper.map(preview.info()),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
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.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -36,6 +38,7 @@ private val ROOM_LIST_RUST_FILTERS = listOf(
|
||||||
internal class RoomListFactory(
|
internal class RoomListFactory(
|
||||||
private val innerRoomListService: RoomListService,
|
private val innerRoomListService: RoomListService,
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
|
private val analyticsService: AnalyticsService,
|
||||||
) {
|
) {
|
||||||
private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory()
|
private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory()
|
||||||
|
|
||||||
|
|
@ -59,6 +62,8 @@ internal class RoomListFactory(
|
||||||
val loadedPages = MutableStateFlow(1)
|
val loadedPages = MutableStateFlow(1)
|
||||||
var innerRoomList: InnerRoomList? = null
|
var innerRoomList: InnerRoomList? = null
|
||||||
|
|
||||||
|
val firstRoomsTransaction = analyticsService.startTransaction("Load first set of rooms", "innerRoomList.entriesFlow")
|
||||||
|
|
||||||
coroutineScope.launch(coroutineContext) {
|
coroutineScope.launch(coroutineContext) {
|
||||||
innerRoomList = innerProvider()
|
innerRoomList = innerProvider()
|
||||||
innerRoomList.let { innerRoomList ->
|
innerRoomList.let { innerRoomList ->
|
||||||
|
|
@ -67,6 +72,10 @@ internal class RoomListFactory(
|
||||||
roomListDynamicEvents = dynamicEvents,
|
roomListDynamicEvents = dynamicEvents,
|
||||||
initialFilterKind = RoomListEntriesDynamicFilterKind.All(ROOM_LIST_RUST_FILTERS),
|
initialFilterKind = RoomListEntriesDynamicFilterKind.All(ROOM_LIST_RUST_FILTERS),
|
||||||
).onEach { update ->
|
).onEach { update ->
|
||||||
|
if (!firstRoomsTransaction.isFinished()) {
|
||||||
|
analyticsService.stopLongRunningTransaction(AnalyticsLongRunningTransaction.FirstRoomsDisplayed)
|
||||||
|
firstRoomsTransaction.finish()
|
||||||
|
}
|
||||||
processor.postUpdate(update)
|
processor.postUpdate(update)
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import org.matrix.rustcomponents.sdk.SyncService as InnerSyncService
|
||||||
class RustSyncService(
|
class RustSyncService(
|
||||||
private val inner: InnerSyncService,
|
private val inner: InnerSyncService,
|
||||||
private val dispatcher: CoroutineDispatcher,
|
private val dispatcher: CoroutineDispatcher,
|
||||||
sessionCoroutineScope: CoroutineScope
|
sessionCoroutineScope: CoroutineScope,
|
||||||
) : SyncService {
|
) : SyncService {
|
||||||
private val isServiceReady = AtomicBoolean(true)
|
private val isServiceReady = AtomicBoolean(true)
|
||||||
|
|
||||||
|
|
@ -71,10 +71,10 @@ class RustSyncService(
|
||||||
override val syncState: StateFlow<SyncState> =
|
override val syncState: StateFlow<SyncState> =
|
||||||
inner.stateFlow()
|
inner.stateFlow()
|
||||||
.map(SyncServiceState::toSyncState)
|
.map(SyncServiceState::toSyncState)
|
||||||
|
.distinctUntilChanged()
|
||||||
.onEach { state ->
|
.onEach { state ->
|
||||||
Timber.i("Sync state=$state")
|
Timber.i("Sync state=$state")
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
|
||||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
|
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
|
||||||
|
|
||||||
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ private fun RustOtherState.map(): OtherState {
|
||||||
RustOtherState.RoomEncryption -> OtherState.RoomEncryption
|
RustOtherState.RoomEncryption -> OtherState.RoomEncryption
|
||||||
RustOtherState.RoomGuestAccess -> OtherState.RoomGuestAccess
|
RustOtherState.RoomGuestAccess -> OtherState.RoomGuestAccess
|
||||||
RustOtherState.RoomHistoryVisibility -> OtherState.RoomHistoryVisibility
|
RustOtherState.RoomHistoryVisibility -> OtherState.RoomHistoryVisibility
|
||||||
RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules
|
is RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules
|
||||||
is RustOtherState.RoomName -> OtherState.RoomName(name)
|
is RustOtherState.RoomName -> OtherState.RoomName(name)
|
||||||
is RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents(change.map())
|
is RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents(change.map())
|
||||||
is RustOtherState.RoomPowerLevels -> OtherState.RoomUserPowerLevels(users)
|
is RustOtherState.RoomPowerLevels -> OtherState.RoomUserPowerLevels(users)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
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.libraries.sessionstorage.test.aSessionData
|
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
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.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
|
||||||
|
|
@ -116,5 +117,6 @@ class RustMatrixClientTest {
|
||||||
clock = FakeSystemClock(),
|
clock = FakeSystemClock(),
|
||||||
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
|
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.roomlist
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomList
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomList
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -22,6 +23,7 @@ class RoomListFactoryTest {
|
||||||
val sut = RoomListFactory(
|
val sut = RoomListFactory(
|
||||||
innerRoomListService = FakeFfiRoomListService(),
|
innerRoomListService = FakeFfiRoomListService(),
|
||||||
sessionCoroutineScope = backgroundScope,
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
)
|
)
|
||||||
sut.createRoomList(
|
sut.createRoomList(
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService
|
||||||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
|
@ -52,6 +53,7 @@ private fun TestScope.createRustRoomListService(
|
||||||
roomListFactory = RoomListFactory(
|
roomListFactory = RoomListFactory(
|
||||||
innerRoomListService = roomListService,
|
innerRoomListService = roomListService,
|
||||||
sessionCoroutineScope = backgroundScope,
|
sessionCoroutineScope = backgroundScope,
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
),
|
),
|
||||||
roomSyncSubscriber = RoomSyncSubscriber(
|
roomSyncSubscriber = RoomSyncSubscriber(
|
||||||
roomListService = roomListService,
|
roomListService = roomListService,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.services.analytics.api
|
||||||
|
|
||||||
|
sealed class AnalyticsLongRunningTransaction(
|
||||||
|
val name: String,
|
||||||
|
val operation: String?,
|
||||||
|
) {
|
||||||
|
data object FirstRoomsDisplayed : AnalyticsLongRunningTransaction("First rooms displayed after login or restoration", null)
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
package io.element.android.services.analytics.api
|
package io.element.android.services.analytics.api
|
||||||
|
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.AnalyticsTracker
|
import io.element.android.services.analyticsproviders.api.trackers.AnalyticsTracker
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.ErrorTracker
|
import io.element.android.services.analyticsproviders.api.trackers.ErrorTracker
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -48,4 +49,23 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
|
||||||
* Update analyticsId from the AccountData.
|
* Update analyticsId from the AccountData.
|
||||||
*/
|
*/
|
||||||
suspend fun setAnalyticsId(analyticsId: String)
|
suspend fun setAnalyticsId(analyticsId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a transaction to measure the performance of an operation.
|
||||||
|
*/
|
||||||
|
fun startTransaction(name: String, operation: String? = null): AnalyticsTransaction
|
||||||
|
|
||||||
|
fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction)
|
||||||
|
|
||||||
|
fun stopLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> AnalyticsService.recordTransaction(name: String, operation: String, block: (AnalyticsTransaction) -> T): T {
|
||||||
|
val transaction = startTransaction(name, operation)
|
||||||
|
try {
|
||||||
|
val result = block(transaction)
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
transaction.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.services.analytics.api
|
||||||
|
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
|
|
||||||
|
object NoopAnalyticsTransaction : AnalyticsTransaction {
|
||||||
|
override fun startChild(operation: String, description: String?): AnalyticsTransaction = NoopAnalyticsTransaction
|
||||||
|
override fun setData(key: String, value: Any) {}
|
||||||
|
override fun isFinished(): Boolean = true
|
||||||
|
override fun finish() {}
|
||||||
|
}
|
||||||
|
|
@ -19,15 +19,19 @@ import im.vector.app.features.analytics.plan.UserProperties
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||||
|
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.NoopAnalyticsTransaction
|
||||||
import io.element.android.services.analytics.impl.log.analyticsTag
|
import io.element.android.services.analytics.impl.log.analyticsTag
|
||||||
import io.element.android.services.analytics.impl.store.AnalyticsStore
|
import io.element.android.services.analytics.impl.store.AnalyticsStore
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
|
|
@ -40,6 +44,8 @@ class DefaultAnalyticsService(
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val sessionObserver: SessionObserver,
|
private val sessionObserver: SessionObserver,
|
||||||
) : AnalyticsService, SessionListener {
|
) : AnalyticsService, SessionListener {
|
||||||
|
private val pendingLongRunningTransactions = ConcurrentHashMap<AnalyticsLongRunningTransaction, AnalyticsTransaction>()
|
||||||
|
|
||||||
// Cache for the store values
|
// Cache for the store values
|
||||||
private val userConsent = AtomicBoolean(false)
|
private val userConsent = AtomicBoolean(false)
|
||||||
|
|
||||||
|
|
@ -138,4 +144,20 @@ class DefaultAnalyticsService(
|
||||||
analyticsProviders.onEach { it.trackError(throwable) }
|
analyticsProviders.onEach { it.trackError(throwable) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction {
|
||||||
|
return if (userConsent.get()) {
|
||||||
|
analyticsProviders.firstNotNullOfOrNull { it.startTransaction(name, operation) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
} ?: NoopAnalyticsTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {
|
||||||
|
pendingLongRunningTransactions[longRunningTransaction] = startTransaction(longRunningTransaction.name, longRunningTransaction.operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {
|
||||||
|
pendingLongRunningTransactions.remove(longRunningTransaction)?.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,11 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||||
import im.vector.app.features.analytics.plan.SuperProperties
|
import im.vector.app.features.analytics.plan.SuperProperties
|
||||||
import im.vector.app.features.analytics.plan.UserProperties
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
|
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.NoopAnalyticsTransaction
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
|
|
@ -35,4 +38,7 @@ class NoopAnalyticsService : AnalyticsService {
|
||||||
override fun updateUserProperties(userProperties: UserProperties) = Unit
|
override fun updateUserProperties(userProperties: UserProperties) = Unit
|
||||||
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 startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {}
|
||||||
|
override fun stopLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||||
import im.vector.app.features.analytics.plan.SuperProperties
|
import im.vector.app.features.analytics.plan.SuperProperties
|
||||||
import im.vector.app.features.analytics.plan.UserProperties
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
|
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.NoopAnalyticsTransaction
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -65,4 +68,8 @@ class FakeAnalyticsService(
|
||||||
override fun updateSuperProperties(updatedProperties: SuperProperties) {
|
override fun updateSuperProperties(updatedProperties: SuperProperties) {
|
||||||
// No op
|
// No op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction = NoopAnalyticsTransaction
|
||||||
|
override fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {}
|
||||||
|
override fun stopLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,6 @@ interface AnalyticsProvider : AnalyticsTracker, ErrorTracker {
|
||||||
fun init()
|
fun init()
|
||||||
|
|
||||||
fun stop()
|
fun stop()
|
||||||
|
|
||||||
|
fun startTransaction(name: String, operation: String? = null): AnalyticsTransaction?
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.services.analyticsproviders.api
|
||||||
|
|
||||||
|
interface AnalyticsTransaction {
|
||||||
|
fun startChild(operation: String, description: String? = null): AnalyticsTransaction
|
||||||
|
fun setData(key: String, value: Any)
|
||||||
|
fun isFinished(): Boolean
|
||||||
|
fun finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> AnalyticsTransaction.recordChildTransaction(operation: String, description: String? = null, block: (AnalyticsTransaction) -> T): T {
|
||||||
|
val child = startChild(operation, description)
|
||||||
|
try {
|
||||||
|
val result = block(child)
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
child.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||||
import im.vector.app.features.analytics.plan.SuperProperties
|
import im.vector.app.features.analytics.plan.SuperProperties
|
||||||
import im.vector.app.features.analytics.plan.UserProperties
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
import io.element.android.services.analyticsproviders.posthog.log.analyticsTag
|
import io.element.android.services.analyticsproviders.posthog.log.analyticsTag
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -122,6 +123,8 @@ class PosthogAnalyticsProvider(
|
||||||
}
|
}
|
||||||
return withSuperProperties.takeIf { it.isEmpty().not() }
|
return withSuperProperties.takeIf { it.isEmpty().not() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Map<String, Any?>.keepOnlyNonNullValues(): Map<String, Any> {
|
private fun Map<String, Any?>.keepOnlyNonNullValues(): Map<String, Any> {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
import io.element.android.services.analyticsproviders.sentry.log.analyticsTag
|
import io.element.android.services.analyticsproviders.sentry.log.analyticsTag
|
||||||
import io.sentry.Breadcrumb
|
import io.sentry.Breadcrumb
|
||||||
import io.sentry.Sentry
|
import io.sentry.Sentry
|
||||||
|
|
@ -51,6 +52,7 @@ class SentryAnalyticsProvider(
|
||||||
options.isEnableUserInteractionTracing = true
|
options.isEnableUserInteractionTracing = true
|
||||||
options.environment = buildMeta.buildType.toSentryEnv()
|
options.environment = buildMeta.buildType.toSentryEnv()
|
||||||
}
|
}
|
||||||
|
Timber.tag(analyticsTag.value).d("Sentry was initialized correctly")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
|
@ -87,6 +89,10 @@ class SentryAnalyticsProvider(
|
||||||
override fun trackError(throwable: Throwable) {
|
override fun trackError(throwable: Throwable) {
|
||||||
Sentry.captureException(throwable)
|
Sentry.captureException(throwable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? {
|
||||||
|
return SentryAnalyticsTransaction(name, operation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BuildType.toSentryEnv() = when (this) {
|
private fun BuildType.toSentryEnv() = when (this) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.services.analyticsproviders.sentry
|
||||||
|
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
|
import io.sentry.ISpan
|
||||||
|
import io.sentry.Sentry
|
||||||
|
|
||||||
|
class SentryAnalyticsTransaction private constructor(span: ISpan) : AnalyticsTransaction {
|
||||||
|
constructor(name: String, operation: String?) : this(Sentry.startTransaction(name, operation.orEmpty()))
|
||||||
|
private val inner = span
|
||||||
|
|
||||||
|
override fun startChild(operation: String, description: String?): AnalyticsTransaction = SentryAnalyticsTransaction(
|
||||||
|
inner.startChild(operation, description)
|
||||||
|
)
|
||||||
|
override fun setData(key: String, value: Any) = inner.setData(key, value)
|
||||||
|
override fun isFinished(): Boolean = inner.isFinished
|
||||||
|
override fun finish() = inner.finish()
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||||
import im.vector.app.features.analytics.plan.SuperProperties
|
import im.vector.app.features.analytics.plan.SuperProperties
|
||||||
import im.vector.app.features.analytics.plan.UserProperties
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
|
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakeAnalyticsProvider(
|
class FakeAnalyticsProvider(
|
||||||
|
|
@ -32,4 +33,5 @@ class FakeAnalyticsProvider(
|
||||||
override fun updateUserProperties(userProperties: UserProperties) = updateUserPropertiesLambda(userProperties)
|
override fun updateUserProperties(userProperties: UserProperties) = updateUserPropertiesLambda(userProperties)
|
||||||
override fun trackError(throwable: Throwable) = trackErrorLambda(throwable)
|
override fun trackError(throwable: Throwable) = trackErrorLambda(throwable)
|
||||||
override fun updateSuperProperties(updatedProperties: SuperProperties) = updateSuperPropertiesLambda(updatedProperties)
|
override fun updateSuperProperties(updatedProperties: SuperProperties) = updateSuperPropertiesLambda(updatedProperties)
|
||||||
|
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? = null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue