From 7fe3b18699e9df3dc7a67d0a487ce48615fc0192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 16 Dec 2025 17:45:56 +0100 Subject: [PATCH] Distinguish between indexable and non-indexable extra data --- .../android/appnav/di/MatrixSessionCache.kt | 2 +- .../android/appnav/di/SyncOrchestrator.kt | 3 +- .../impl/timeline/TimelinePresenter.kt | 3 +- .../analytics/api/NoopAnalyticsTransaction.kt | 3 +- .../analytics/impl/DefaultAnalyticsService.kt | 10 ++++-- .../api/AnalyticsTransaction.kt | 35 ++++++++++++++++++- .../api/AnalyticsUserData.kt | 3 ++ .../api/trackers/AnalyticsTracker.kt | 11 ++++-- .../sentry/SentryAnalyticsProvider.kt | 6 +++- .../sentry/SentryAnalyticsTransaction.kt | 4 ++- 10 files changed, 69 insertions(+), 11 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt index c639a9abd7..c6a031921f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt @@ -106,7 +106,7 @@ class MatrixSessionCache( .onSuccess { matrixClient -> // Add the current homeserver (hashed) to the extra info // This may not play well with multiple sessions, but it should work for now - analyticsService.addUserData(AnalyticsUserData.HOMESERVER, matrixClient.userIdServerName().hash()) + analyticsService.addIndexableData(AnalyticsUserData.HOMESERVER, matrixClient.userIdServerName().hash()) // Add the new client to the in-memory cache onNewMatrixClient(matrixClient) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt index 53ce50a788..9b1bbd1b81 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService 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.analyticsproviders.api.AnalyticsUserData import io.element.android.services.appnavstate.api.AppForegroundStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview @@ -77,7 +78,7 @@ class SyncOrchestrator( // Wait until the sync service is not idle, either it will be running or in error/offline state val firstState = syncService.syncState.first { it != SyncState.Idle } - transaction.setData("first_sync_state", firstState.name) + transaction.putIndexableData(AnalyticsUserData.FIRST_SYNC_STATE, firstState.name) } observeStates() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 289ec2924d..a953c6e349 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -60,6 +60,7 @@ import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.finishLongRunningTransaction +import io.element.android.services.analyticsproviders.api.AnalyticsUserData import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope @@ -249,7 +250,7 @@ class TimelinePresenter( 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()) + transaction?.putExtraData(AnalyticsUserData.TIMELINE_ITEM_COUNT, items.count().toString()) timelineItemsFactory.replaceWith( timelineItems = items, roomMembers = membersState.roomMembers().orEmpty() diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/NoopAnalyticsTransaction.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/NoopAnalyticsTransaction.kt index e6f69ae99b..914fac1b12 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/NoopAnalyticsTransaction.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/NoopAnalyticsTransaction.kt @@ -11,7 +11,8 @@ 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 putExtraData(key: String, value: String) {} + override fun putIndexableData(key: String, value: String) {} override fun isFinished(): Boolean = true override fun traceId(): String? = null override fun attachError(throwable: Throwable) {} diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt index 764d2e9796..5db1eb1b31 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt @@ -149,9 +149,15 @@ class DefaultAnalyticsService( } } - override fun addUserData(key: String, value: String) { + override fun addExtraData(key: String, value: String) { if (userConsent.get()) { - analyticsProviders.onEach { it.addUserData(key, value) } + analyticsProviders.onEach { it.addExtraData(key, value) } + } + } + + override fun addIndexableData(key: String, value: String) { + if (userConsent.get()) { + analyticsProviders.onEach { it.addIndexableData(key, value) } } } diff --git a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsTransaction.kt b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsTransaction.kt index 84575d5dd0..b5f81ac67e 100644 --- a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsTransaction.kt +++ b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsTransaction.kt @@ -8,14 +8,47 @@ package io.element.android.services.analyticsproviders.api interface AnalyticsTransaction { + /** + * Start a child span from this transaction. + */ fun startChild(operation: String, description: String? = null): AnalyticsTransaction - fun setData(key: String, value: Any) + + /** + * Adds extra data to the transaction. This data is not indexed, it's just listed. + */ + fun putExtraData(key: String, value: String) + + /** + * Similar to [putExtraData], adds extra data that *will be indexed* and can be used for filtering in the analytics portal. + * + * **Do not add numerical values using this function, use [putExtraData] instead.** + */ + fun putIndexableData(key: String, value: String) + + /** + * Whether the transaction has finished. + */ fun isFinished(): Boolean + + /** + * The optional trace id which can be used for distributed tracing. + */ fun traceId(): String? + + /** + * Attach a throwable to the transaction, so we can know it failed. + */ fun attachError(throwable: Throwable) + + /** + * Finish the transaction. This will schedule an upload of the data. + */ fun finish() } +/** + * Records a child span from this transaction. + */ inline fun AnalyticsTransaction.recordChildTransaction(operation: String, description: String? = null, block: (AnalyticsTransaction) -> T): T { val child = startChild(operation, description) try { diff --git a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsUserData.kt b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsUserData.kt index 44080f824d..28ae4a5668 100644 --- a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsUserData.kt +++ b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsUserData.kt @@ -14,4 +14,7 @@ object AnalyticsUserData { const val EVENT_CACHE_SIZE = "event_cache_size" const val CRYPTO_STORE_SIZE = "crypto_store_size" const val MEDIA_STORE_SIZE = "media_store_size" + + const val FIRST_SYNC_STATE = "first_sync_state" + const val TIMELINE_ITEM_COUNT = "timeline_item_count" } diff --git a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt index 9ac7d13f2a..5bb63338a8 100644 --- a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt +++ b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt @@ -37,9 +37,16 @@ interface AnalyticsTracker { fun updateSuperProperties(updatedProperties: SuperProperties) /** - * Adds user data that will be sent with every event. + * Adds extra data that will be sent with every event. */ - fun addUserData(key: String, value: String) {} + fun addExtraData(key: String, value: String) {} + + /** + * Similar to [addExtraData], adds data that will be indexed in the analytics portal. + * + * **Do not add numerical values using this, use [addExtraData] instead.** + */ + fun addIndexableData(key: String, value: String) {} } fun AnalyticsTracker.captureInteraction(name: Interaction.Name, type: Interaction.InteractionType? = null) { diff --git a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt index e7f49079a4..629426d6c2 100644 --- a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt +++ b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt @@ -113,10 +113,14 @@ class SentryAnalyticsProvider( override fun updateSuperProperties(updatedProperties: SuperProperties) { } - override fun addUserData(key: String, value: String) { + override fun addExtraData(key: String, value: String) { Sentry.setExtra(key, value) } + override fun addIndexableData(key: String, value: String) { + Sentry.setTag(key, value) + } + override fun trackError(throwable: Throwable) { Sentry.captureException(throwable) } diff --git a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsTransaction.kt b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsTransaction.kt index 77a008b0a4..84314ee35f 100644 --- a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsTransaction.kt +++ b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsTransaction.kt @@ -20,7 +20,9 @@ class SentryAnalyticsTransaction private constructor(span: ISpan) : AnalyticsTra 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 putIndexableData(key: String, value: String) = inner.setTag(key, value) + override fun putExtraData(key: String, value: String) = inner.setData(key, value) override fun traceId(): String? = inner.toSentryTrace().value override fun isFinished(): Boolean = inner.isFinished override fun attachError(throwable: Throwable) {