diff --git a/vector/build.gradle b/vector/build.gradle index 368fa217db..2518e89e32 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -160,7 +160,7 @@ dependencies { api 'com.facebook.stetho:stetho:1.6.0' // Analytics - api 'com.github.matrix-org:matrix-analytics-events:0.15.0' + api 'com.github.matrix-org:matrix-analytics-events:0.22.0' api libs.google.phonenumber diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 5523c84994..1f4e4261f8 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -22,7 +22,8 @@ import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.session.ConfigureAndStartSessionUseCase -import im.vector.app.features.analytics.DecryptionFailureTracker +import im.vector.app.features.analytics.VectorAnalytics +import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler @@ -57,7 +58,7 @@ class ActiveSessionHolder @Inject constructor( private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val applicationCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, - private val decryptionFailureTracker: DecryptionFailureTracker, + private val vectorAnalytics: VectorAnalytics, ) { private var activeSessionReference: AtomicReference = AtomicReference() @@ -74,6 +75,13 @@ class ActiveSessionHolder @Inject constructor( session.callSignalingService().addCallListener(callManager) imageManager.onSessionStarted(session) guardServiceStarter.start() + vectorAnalytics.updateSuperProperties( + SuperProperties( + platformCodeName = SuperProperties.PlatformCodeName.EA, + cryptoSDK = SuperProperties.CryptoSDK.Rust, + cryptoSDKVersion = session.cryptoService().getCryptoVersion(applicationContext, false) + ) + ) } suspend fun clearActiveSession() { diff --git a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt index 871782e473..d233900d2c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/AnalyticsTracker.kt @@ -18,6 +18,7 @@ package im.vector.app.features.analytics 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 interface AnalyticsTracker { @@ -35,4 +36,10 @@ interface AnalyticsTracker { * Update user specific properties. */ fun updateUserProperties(userProperties: UserProperties) + + /** + * Update the super properties. + * Super properties are added to any tracked event automatically. + */ + fun updateSuperProperties(updatedProperties: SuperProperties) } diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt index 8520a40ca2..552af1b524 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt @@ -23,6 +23,7 @@ import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.log.analyticsTag +import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.store.AnalyticsStore import kotlinx.coroutines.CoroutineScope @@ -63,6 +64,8 @@ class DefaultVectorAnalytics @Inject constructor( // Cache for the properties to send private var pendingUserProperties: UserProperties? = null + private var superProperties: SuperProperties? = null + override fun init() { observeUserConsent() observeAnalyticsId() @@ -173,7 +176,7 @@ class DefaultVectorAnalytics @Inject constructor( ?.capture( event.getName(), analyticsId, - event.getProperties()?.toPostHogProperties() + event.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties() ) } @@ -181,7 +184,7 @@ class DefaultVectorAnalytics @Inject constructor( Timber.tag(analyticsTag.value).d("screen($screen)") posthog ?.takeIf { userConsent == true } - ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties()) + ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties()) } override fun updateUserProperties(userProperties: UserProperties) { @@ -226,9 +229,38 @@ class DefaultVectorAnalytics @Inject constructor( return nonNulls } + /** + * Adds super properties to the actual property set. + * If a property of the same name is already on the reported event it will not be overwritten. + */ + private fun Map.withSuperProperties(): Map { + val withSuperProperties = this.toMutableMap() + val superProperties = this@DefaultVectorAnalytics.superProperties?.getProperties() + superProperties?.forEach { + if (!withSuperProperties.containsKey(it.key)) { + withSuperProperties[it.key] = it.value + } + } + return withSuperProperties + } + override fun trackError(throwable: Throwable) { sentryAnalytics .takeIf { userConsent == true } ?.trackError(throwable) } + + override fun updateSuperProperties(updatedProperties: SuperProperties) { + if (this.superProperties == null) { + this.superProperties = updatedProperties + return + } + + this.superProperties = SuperProperties( + platformCodeName = updatedProperties.platformCodeName ?: this.superProperties?.platformCodeName, + cryptoSDK = updatedProperties.cryptoSDK ?: this.superProperties?.cryptoSDK, + appPlatform = updatedProperties.appPlatform ?: this.superProperties?.appPlatform, + cryptoSDKVersion = updatedProperties.cryptoSDKVersion ?: superProperties?.cryptoSDKVersion + ) + } } diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt index ea8beaa86c..ebf1d21ce2 100644 --- a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt +++ b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt @@ -16,6 +16,7 @@ package im.vector.app.features.analytics.impl +import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.test.fakes.FakeAnalyticsStore import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakePostHog @@ -174,6 +175,117 @@ class DefaultVectorAnalyticsTest { fakeSentryAnalytics.verifyNoErrorTracking() } + @Test + fun `Super properties should be added to all captured events`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + + val updatedProperties = SuperProperties( + platformCodeName = SuperProperties.PlatformCodeName.EA, + cryptoSDKVersion = "0.0", + cryptoSDK = SuperProperties.CryptoSDK.Rust + ) + + defaultVectorAnalytics.updateSuperProperties(updatedProperties) + + val fakeEvent = aVectorAnalyticsEvent("THE_NAME", mutableMapOf("foo" to "bar")) + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + fakeEvent.getProperties().clearNulls()?.toMutableMap()?.apply { + updatedProperties.getProperties()?.let { putAll(it) } + } + ) + + // Check with a screen event + val fakeScreen = aVectorAnalyticsScreen("Screen", mutableMapOf("foo" to "bar")) + defaultVectorAnalytics.screen(fakeScreen) + + fakePostHog.verifyScreenTracked( + "Screen", + fakeScreen.getProperties().clearNulls()?.toMutableMap()?.apply { + updatedProperties.getProperties()?.let { putAll(it) } + } + ) + } + + @Test + fun `Super properties can be updated`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + + val superProperties = SuperProperties( + platformCodeName = SuperProperties.PlatformCodeName.EA, + cryptoSDKVersion = "0.0", + cryptoSDK = SuperProperties.CryptoSDK.Rust + ) + + defaultVectorAnalytics.updateSuperProperties(superProperties) + + val fakeEvent = aVectorAnalyticsEvent("THE_NAME", mutableMapOf("foo" to "bar")) + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + fakeEvent.getProperties().clearNulls()?.toMutableMap()?.apply { + superProperties.getProperties()?.let { putAll(it) } + } + ) + + val superPropertiesUpdate = superProperties.copy(cryptoSDKVersion = "1.0") + defaultVectorAnalytics.updateSuperProperties(superPropertiesUpdate) + + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + fakeEvent.getProperties().clearNulls()?.toMutableMap()?.apply { + superPropertiesUpdate.getProperties()?.let { putAll(it) } + } + ) + } + + @Test + fun `Super properties should not override event property`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + + val superProperties = SuperProperties( + cryptoSDKVersion = "0.0", + ) + + defaultVectorAnalytics.updateSuperProperties(superProperties) + + val fakeEvent = aVectorAnalyticsEvent("THE_NAME", mutableMapOf("cryptoSDKVersion" to "XXX")) + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + mapOf( + "cryptoSDKVersion" to "XXX" + ) + ) + } + + @Test + fun `Super properties should be added to event with no properties`() = runTest { + fakeAnalyticsStore.givenUserContent(consent = true) + + val superProperties = SuperProperties( + cryptoSDKVersion = "0.0", + ) + + defaultVectorAnalytics.updateSuperProperties(superProperties) + + val fakeEvent = aVectorAnalyticsEvent("THE_NAME", null) + defaultVectorAnalytics.capture(fakeEvent) + + fakePostHog.verifyEventTracked( + "THE_NAME", + mapOf( + "cryptoSDKVersion" to "0.0" + ) + ) + } + private fun Map?.clearNulls(): Map? { if (this == null) return null