diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index fe4cc3311b..0f83f57d01 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -53,6 +53,7 @@ import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.resources.BuildMeta 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.configuration.VectorConfiguration import im.vector.app.features.invite.InvitesAcceptor @@ -130,6 +131,12 @@ class VectorApplication : appContext = this flipperProxy.init(matrix) vectorAnalytics.init() + vectorAnalytics.updateSuperProperties( + SuperProperties( + appPlatform = SuperProperties.AppPlatform.EA, + cryptoSDK = SuperProperties.CryptoSDK.Rust, + ) + ) invitesAcceptor.initialize() autoRageShaker.initialize() decryptionFailureTracker.start() diff --git a/vector/build.gradle b/vector/build.gradle index 2518e89e32..dc8eb0641e 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.22.0' + api 'com.github.matrix-org:matrix-analytics-events:0.23.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 1f4e4261f8..f3d775b39f 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,8 +22,6 @@ 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.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 @@ -58,7 +56,6 @@ class ActiveSessionHolder @Inject constructor( private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val applicationCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, - private val vectorAnalytics: VectorAnalytics, ) { private var activeSessionReference: AtomicReference = AtomicReference() @@ -75,13 +72,6 @@ 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/impl/AutoSuperPropertiesFlowProvider.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/AutoSuperPropertiesFlowProvider.kt new file mode 100644 index 0000000000..76763e3bab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/AutoSuperPropertiesFlowProvider.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.analytics.impl + +import im.vector.app.ActiveSessionDataSource +import im.vector.app.features.analytics.plan.SuperProperties +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +/** + * Gathers the super properties that are static to this platform or + * that can be automatically resolved from the current session. + */ +class AutoSuperPropertiesFlowProvider @Inject constructor( + activeSessionDataSource: ActiveSessionDataSource, +) { + + val superPropertiesFlow: Flow = activeSessionDataSource.stream() + .map { session -> + SuperProperties( + appPlatform = SuperProperties.AppPlatform.EA, + cryptoSDK = SuperProperties.CryptoSDK.Rust, + cryptoSDKVersion = session.getOrNull()?.cryptoService()?.getCryptoVersion(false) + ) + } + .distinctUntilChanged() +} 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 552af1b524..baefe18cf5 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 @@ -42,6 +42,7 @@ class DefaultVectorAnalytics @Inject constructor( private val analyticsConfig: AnalyticsConfig, private val analyticsStore: AnalyticsStore, private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, + private val autoSuperPropertiesFlowProvider: AutoSuperPropertiesFlowProvider, @NamedGlobalScope private val globalScope: CoroutineScope ) : VectorAnalytics { @@ -69,6 +70,7 @@ class DefaultVectorAnalytics @Inject constructor( override fun init() { observeUserConsent() observeAnalyticsId() + observeAutoSuperProperties() } override fun getUserConsent(): Flow { @@ -116,6 +118,12 @@ class DefaultVectorAnalytics @Inject constructor( .launchIn(globalScope) } + private fun observeAutoSuperProperties() { + autoSuperPropertiesFlowProvider.superPropertiesFlow.onEach { + updateSuperProperties(it) + }.launchIn(globalScope) + } + private suspend fun identifyPostHog() { val id = analyticsId ?: return if (!userConsent.orFalse()) return @@ -171,20 +179,14 @@ class DefaultVectorAnalytics @Inject constructor( override fun capture(event: VectorAnalyticsEvent) { Timber.tag(analyticsTag.value).d("capture($event)") - posthog - ?.takeIf { userConsent == true } - ?.capture( - event.getName(), - analyticsId, - event.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties() + posthog?.takeIf { userConsent == true }?.capture( + event.getName(), analyticsId, event.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties() ) } override fun screen(screen: VectorAnalyticsScreen) { Timber.tag(analyticsTag.value).d("screen($screen)") - posthog - ?.takeIf { userConsent == true } - ?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties()) + posthog?.takeIf { userConsent == true }?.screen(screen.getName(), screen.getProperties()?.toPostHogProperties().orEmpty().withSuperProperties()) } override fun updateUserProperties(userProperties: UserProperties) { @@ -198,9 +200,7 @@ class DefaultVectorAnalytics @Inject constructor( private fun doUpdateUserProperties(userProperties: UserProperties) { // we need a distinct id to set user properties val distinctId = analyticsId ?: return - posthog - ?.takeIf { userConsent == true } - ?.identify(distinctId, userProperties.getProperties()) + posthog?.takeIf { userConsent == true }?.identify(distinctId, userProperties.getProperties()) } private fun Map?.toPostHogProperties(): Map? { @@ -233,7 +233,7 @@ class DefaultVectorAnalytics @Inject constructor( * 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 { + private fun Map.withSuperProperties(): Map? { val withSuperProperties = this.toMutableMap() val superProperties = this@DefaultVectorAnalytics.superProperties?.getProperties() superProperties?.forEach { @@ -241,7 +241,7 @@ class DefaultVectorAnalytics @Inject constructor( withSuperProperties[it.key] = it.value } } - return withSuperProperties + return withSuperProperties.takeIf { it.isEmpty().not() } } override fun trackError(throwable: Throwable) { @@ -251,13 +251,7 @@ class DefaultVectorAnalytics @Inject constructor( } 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 ebf1d21ce2..70c97c234f 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 @@ -26,9 +26,12 @@ import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig import im.vector.app.test.fixtures.aUserProperties import im.vector.app.test.fixtures.aVectorAnalyticsEvent import im.vector.app.test.fixtures.aVectorAnalyticsScreen +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -45,6 +48,9 @@ class DefaultVectorAnalyticsTest { private val fakeAnalyticsStore = FakeAnalyticsStore() private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory() private val fakeSentryAnalytics = FakeSentryAnalytics() + private val mockAutoSuperPropertiesFlowProvider = mockk().also { + every { it.superPropertiesFlow } returns flowOf(SuperProperties()) + } private val defaultVectorAnalytics = DefaultVectorAnalytics( postHogFactory = FakePostHogFactory(fakePostHog.instance).instance, @@ -52,7 +58,8 @@ class DefaultVectorAnalyticsTest { analyticsStore = fakeAnalyticsStore.instance, globalScope = CoroutineScope(Dispatchers.Unconfined), analyticsConfig = anAnalyticsConfig(isEnabled = true), - lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance + lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance, + autoSuperPropertiesFlowProvider = mockAutoSuperPropertiesFlowProvider, ) @Before @@ -180,7 +187,7 @@ class DefaultVectorAnalyticsTest { fakeAnalyticsStore.givenUserContent(consent = true) val updatedProperties = SuperProperties( - platformCodeName = SuperProperties.PlatformCodeName.EA, + appPlatform = SuperProperties.AppPlatform.EA, cryptoSDKVersion = "0.0", cryptoSDK = SuperProperties.CryptoSDK.Rust ) @@ -214,7 +221,7 @@ class DefaultVectorAnalyticsTest { fakeAnalyticsStore.givenUserContent(consent = true) val superProperties = SuperProperties( - platformCodeName = SuperProperties.PlatformCodeName.EA, + appPlatform = SuperProperties.AppPlatform.EA, cryptoSDKVersion = "0.0", cryptoSDK = SuperProperties.CryptoSDK.Rust )