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:
Jorge Martin Espinosa
2025-11-19 12:42:55 +01:00
committed by GitHub
parent de3ffca1af
commit c6c2f4a267
25 changed files with 245 additions and 41 deletions

View File

@@ -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)
}

View File

@@ -9,6 +9,7 @@
package io.element.android.services.analytics.api
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.ErrorTracker
import kotlinx.coroutines.flow.Flow
@@ -48,4 +49,23 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker {
* Update analyticsId from the AccountData.
*/
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()
}
}

View File

@@ -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() {}
}

View File

@@ -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.sessionstorage.api.observer.SessionListener
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.NoopAnalyticsTransaction
import io.element.android.services.analytics.impl.log.analyticsTag
import io.element.android.services.analytics.impl.store.AnalyticsStore
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
@SingleIn(AppScope::class)
@@ -40,6 +44,8 @@ class DefaultAnalyticsService(
private val coroutineScope: CoroutineScope,
private val sessionObserver: SessionObserver,
) : AnalyticsService, SessionListener {
private val pendingLongRunningTransactions = ConcurrentHashMap<AnalyticsLongRunningTransaction, AnalyticsTransaction>()
// Cache for the store values
private val userConsent = AtomicBoolean(false)
@@ -138,4 +144,20 @@ class DefaultAnalyticsService(
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()
}
}

View File

@@ -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.plan.SuperProperties
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.NoopAnalyticsTransaction
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.flowOf
@@ -35,4 +38,7 @@ class NoopAnalyticsService : AnalyticsService {
override fun updateUserProperties(userProperties: UserProperties) = Unit
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) {}
override fun stopLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {}
}

View File

@@ -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.plan.SuperProperties
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.NoopAnalyticsTransaction
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.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -65,4 +68,8 @@ class FakeAnalyticsService(
override fun updateSuperProperties(updatedProperties: SuperProperties) {
// No op
}
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction = NoopAnalyticsTransaction
override fun startLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {}
override fun stopLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction) {}
}

View File

@@ -20,4 +20,6 @@ interface AnalyticsProvider : AnalyticsTracker, ErrorTracker {
fun init()
fun stop()
fun startTransaction(name: String, operation: String? = null): AnalyticsTransaction?
}

View File

@@ -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()
}
}

View File

@@ -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.UserProperties
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 timber.log.Timber
@@ -122,6 +123,8 @@ class PosthogAnalyticsProvider(
}
return withSuperProperties.takeIf { it.isEmpty().not() }
}
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? = null
}
private fun Map<String, Any?>.keepOnlyNonNullValues(): Map<String, Any> {

View File

@@ -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.di.annotations.ApplicationContext
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.sentry.Breadcrumb
import io.sentry.Sentry
@@ -51,6 +52,7 @@ class SentryAnalyticsProvider(
options.isEnableUserInteractionTracing = true
options.environment = buildMeta.buildType.toSentryEnv()
}
Timber.tag(analyticsTag.value).d("Sentry was initialized correctly")
}
override fun stop() {
@@ -87,6 +89,10 @@ class SentryAnalyticsProvider(
override fun trackError(throwable: Throwable) {
Sentry.captureException(throwable)
}
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? {
return SentryAnalyticsTransaction(name, operation)
}
}
private fun BuildType.toSentryEnv() = when (this) {

View File

@@ -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()
}

View File

@@ -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.UserProperties
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
import io.element.android.services.analyticsproviders.api.AnalyticsTransaction
import io.element.android.tests.testutils.lambda.lambdaError
class FakeAnalyticsProvider(
@@ -32,4 +33,5 @@ class FakeAnalyticsProvider(
override fun updateUserProperties(userProperties: UserProperties) = updateUserPropertiesLambda(userProperties)
override fun trackError(throwable: Throwable) = trackErrorLambda(throwable)
override fun updateSuperProperties(updatedProperties: SuperProperties) = updateSuperPropertiesLambda(updatedProperties)
override fun startTransaction(name: String, operation: String?): AnalyticsTransaction? = null
}