Analytics | Add support for SuperProperties
This commit is contained in:
@@ -19,6 +19,7 @@ package io.element.android.services.analytics.impl
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
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.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
@@ -148,6 +149,10 @@ class DefaultAnalyticsService @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateSuperProperties(updatedProperties: SuperProperties) {
|
||||
this.analyticsProviders.onEach { it.updateSuperProperties(updatedProperties) }
|
||||
}
|
||||
|
||||
override fun trackError(throwable: Throwable) {
|
||||
if (userConsent.get()) {
|
||||
analyticsProviders.onEach { it.trackError(throwable) }
|
||||
|
||||
@@ -23,6 +23,7 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import im.vector.app.features.analytics.plan.PollEnd
|
||||
import im.vector.app.features.analytics.plan.SuperProperties
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
|
||||
@@ -255,6 +256,23 @@ class DefaultAnalyticsServiceTest {
|
||||
updateUserPropertiesLambda.assertions().isCalledOnce().with(value(aUserProperty))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when super properties are updated, updateSuperProperties is sent to the provider`() = runCancellableScopeTest {
|
||||
val updateSuperPropertiesLambda = lambdaRecorder<SuperProperties, Unit> { _ -> }
|
||||
val sut = createDefaultAnalyticsService(
|
||||
coroutineScope = it,
|
||||
analyticsProviders = setOf(
|
||||
FakeAnalyticsProvider(
|
||||
initLambda = { },
|
||||
updateSuperPropertiesLambda = updateSuperPropertiesLambda,
|
||||
)
|
||||
),
|
||||
analyticsStore = FakeAnalyticsStore(defaultUserConsent = true),
|
||||
)
|
||||
sut.updateSuperProperties(aSuperProperty)
|
||||
updateSuperPropertiesLambda.assertions().isCalledOnce().with(value(aSuperProperty))
|
||||
}
|
||||
|
||||
private suspend fun createDefaultAnalyticsService(
|
||||
coroutineScope: CoroutineScope,
|
||||
analyticsProviders: Set<@JvmSuppressWildcards AnalyticsProvider> = setOf(
|
||||
@@ -280,6 +298,11 @@ class DefaultAnalyticsServiceTest {
|
||||
private val aUserProperty = UserProperties(
|
||||
ftueUseCaseSelection = UserProperties.FtueUseCaseSelection.WorkMessaging,
|
||||
)
|
||||
private val aSuperProperty = SuperProperties(
|
||||
appPlatform = SuperProperties.AppPlatform.EXA,
|
||||
cryptoSDK = SuperProperties.CryptoSDK.Rust,
|
||||
cryptoSDKVersion = "0.0"
|
||||
)
|
||||
private val anError = Exception("a reason")
|
||||
private const val AN_ID = "anId"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.element.android.services.analyticsproviders.api.trackers
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import im.vector.app.features.analytics.plan.SuperProperties
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
|
||||
interface AnalyticsTracker {
|
||||
@@ -36,6 +37,12 @@ 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)
|
||||
}
|
||||
|
||||
fun AnalyticsTracker.captureInteraction(name: Interaction.Name, type: Interaction.InteractionType? = null) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import com.posthog.PostHogInterface
|
||||
import com.squareup.anvil.annotations.ContributesMultibinding
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||
import im.vector.app.features.analytics.plan.Error
|
||||
import im.vector.app.features.analytics.plan.SuperProperties
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||
@@ -42,6 +42,8 @@ class PosthogAnalyticsProvider @Inject constructor(
|
||||
|
||||
private var pendingUserProperties: MutableMap<String, Any>? = null
|
||||
|
||||
private var superProperties: SuperProperties? = null
|
||||
|
||||
private val userPropertiesLock = Any()
|
||||
|
||||
override fun init() {
|
||||
@@ -64,7 +66,7 @@ class PosthogAnalyticsProvider @Inject constructor(
|
||||
synchronized(userPropertiesLock) {
|
||||
posthog?.capture(
|
||||
event = event.getName(),
|
||||
properties = event.getProperties()?.keepOnlyNonNullValues().withExtraProperties(),
|
||||
properties = event.getProperties()?.keepOnlyNonNullValues().withSuperProperties(),
|
||||
userProperties = pendingUserProperties,
|
||||
)
|
||||
pendingUserProperties = null
|
||||
@@ -74,7 +76,7 @@ class PosthogAnalyticsProvider @Inject constructor(
|
||||
override fun screen(screen: VectorAnalyticsScreen) {
|
||||
posthog?.screen(
|
||||
screenTitle = screen.getName(),
|
||||
properties = screen.getProperties().withExtraProperties(),
|
||||
properties = screen.getProperties().withSuperProperties(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,6 +96,14 @@ class PosthogAnalyticsProvider @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateSuperProperties(updatedProperties: SuperProperties) {
|
||||
this.superProperties = SuperProperties(
|
||||
cryptoSDK = updatedProperties.cryptoSDK ?: this.superProperties?.cryptoSDK,
|
||||
appPlatform = updatedProperties.appPlatform ?: this.superProperties?.appPlatform,
|
||||
cryptoSDKVersion = updatedProperties.cryptoSDKVersion ?: superProperties?.cryptoSDKVersion
|
||||
)
|
||||
}
|
||||
|
||||
override fun trackError(throwable: Throwable) {
|
||||
// Not implemented
|
||||
}
|
||||
@@ -110,6 +120,17 @@ class PosthogAnalyticsProvider @Inject constructor(
|
||||
// posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<String, Any>?.withSuperProperties(): Map<String, Any>? {
|
||||
val withSuperProperties = this.orEmpty().toMutableMap()
|
||||
val superProperties = this@PosthogAnalyticsProvider.superProperties?.getProperties()
|
||||
superProperties?.forEach {
|
||||
if (!withSuperProperties.containsKey(it.key)) {
|
||||
withSuperProperties[it.key] = it.value
|
||||
}
|
||||
}
|
||||
return withSuperProperties.takeIf { it.isEmpty().not() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<String, Any?>.keepOnlyNonNullValues(): Map<String, Any> {
|
||||
@@ -122,14 +143,3 @@ private fun Map<String, Any?>.keepOnlyNonNullValues(): Map<String, Any> {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties which will be added to all Events and Screens.
|
||||
*/
|
||||
private val extraProperties: Map<String, Any> = mapOf(
|
||||
"cryptoSDK" to Error.CryptoSDK.Rust
|
||||
)
|
||||
|
||||
private fun Map<String, Any>?.withExtraProperties(): Map<String, Any> {
|
||||
return orEmpty() + extraProperties
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ package io.element.android.services.analyticsproviders.posthog
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.posthog.PostHogInterface
|
||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import im.vector.app.features.analytics.plan.SuperProperties
|
||||
import im.vector.app.features.analytics.plan.UserProperties
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.mockk.every
|
||||
@@ -113,4 +115,113 @@ class PosthogAnalyticsProviderTest {
|
||||
assertThat(userPropertiesSlot.captured).isNotNull()
|
||||
assertThat(userPropertiesSlot.captured["verificationState"] as String).isEqualTo(testUserPropertiesUpdate.verificationState?.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Posthog - Test super properties added to all captured events`() = runTest {
|
||||
val mockPosthog = mockk<PostHogInterface>().also {
|
||||
every { it.optIn() } just runs
|
||||
every { it.capture(any(), any(), any(), any(), any(), any()) } just runs
|
||||
every { it.screen(any(), any()) } just runs
|
||||
}
|
||||
val mockPosthogFactory = mockk<PostHogFactory>()
|
||||
every { mockPosthogFactory.createPosthog() } returns mockPosthog
|
||||
|
||||
val analyticsProvider = PosthogAnalyticsProvider(mockPosthogFactory)
|
||||
analyticsProvider.init()
|
||||
|
||||
val testSuperProperties = SuperProperties(
|
||||
appPlatform = SuperProperties.AppPlatform.EXA,
|
||||
)
|
||||
analyticsProvider.updateSuperProperties(testSuperProperties)
|
||||
|
||||
// Test with events having different sort of properties
|
||||
listOf(
|
||||
mapOf("foo" to "bar"),
|
||||
mapOf("a" to "aValue1", "b" to "aValue2"),
|
||||
null
|
||||
).forEach { eventProperties ->
|
||||
// report an event with properties
|
||||
val mockEvent = mockk<VectorAnalyticsEvent>().also {
|
||||
every {
|
||||
it.getProperties()
|
||||
} returns eventProperties
|
||||
every { it.getName() } returns "MockEventName"
|
||||
}
|
||||
|
||||
analyticsProvider.capture(mockEvent)
|
||||
|
||||
val expectedProperties = eventProperties.orEmpty() + testSuperProperties.getProperties().orEmpty()
|
||||
verify { mockPosthog.capture(event = "MockEventName", any(), properties = expectedProperties, any()) }
|
||||
}
|
||||
|
||||
// / Test it is also added to screens
|
||||
val screenEvent = MobileScreen(null, MobileScreen.ScreenName.Home)
|
||||
analyticsProvider.screen(screenEvent)
|
||||
verify { mockPosthog.screen(MobileScreen.ScreenName.Home.rawValue, testSuperProperties.getProperties()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Posthog - Test super properties can be updated`() = runTest {
|
||||
val mockPosthog = mockk<PostHogInterface>().also {
|
||||
every { it.optIn() } just runs
|
||||
every { it.capture(any(), any(), any(), any(), any(), any()) } just runs
|
||||
}
|
||||
val mockPosthogFactory = mockk<PostHogFactory>()
|
||||
every { mockPosthogFactory.createPosthog() } returns mockPosthog
|
||||
|
||||
val analyticsProvider = PosthogAnalyticsProvider(mockPosthogFactory)
|
||||
analyticsProvider.init()
|
||||
|
||||
// Test with events having different sort of aggregation
|
||||
// left is the updated properties, right is the expected aggregated state
|
||||
mapOf(
|
||||
SuperProperties(appPlatform = SuperProperties.AppPlatform.EXA) to SuperProperties(appPlatform = SuperProperties.AppPlatform.EXA),
|
||||
SuperProperties(cryptoSDKVersion = "0.0") to SuperProperties(appPlatform = SuperProperties.AppPlatform.EXA, cryptoSDKVersion = "0.0"),
|
||||
SuperProperties(cryptoSDKVersion = "1.0") to SuperProperties(appPlatform = SuperProperties.AppPlatform.EXA, cryptoSDKVersion = "1.0"),
|
||||
SuperProperties(cryptoSDK = SuperProperties.CryptoSDK.Rust) to SuperProperties(
|
||||
appPlatform = SuperProperties.AppPlatform.EXA,
|
||||
cryptoSDKVersion = "1.0",
|
||||
cryptoSDK = SuperProperties.CryptoSDK.Rust
|
||||
),
|
||||
).entries.forEach { (updated, expected) ->
|
||||
// report an event with properties
|
||||
val mockEvent = mockk<VectorAnalyticsEvent>().also {
|
||||
every {
|
||||
it.getProperties()
|
||||
} returns null
|
||||
every { it.getName() } returns "MockEventName"
|
||||
}
|
||||
|
||||
analyticsProvider.updateSuperProperties(updated)
|
||||
analyticsProvider.capture(mockEvent)
|
||||
|
||||
verify { mockPosthog.capture(event = "MockEventName", any(), properties = expected.getProperties(), any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Posthog - Test super properties do not override property with same name on the event`() = runTest {
|
||||
val mockPosthog = mockk<PostHogInterface>().also {
|
||||
every { it.optIn() } just runs
|
||||
every { it.capture(any(), any(), any(), any(), any(), any()) } just runs
|
||||
}
|
||||
val mockPosthogFactory = mockk<PostHogFactory>()
|
||||
every { mockPosthogFactory.createPosthog() } returns mockPosthog
|
||||
|
||||
val analyticsProvider = PosthogAnalyticsProvider(mockPosthogFactory)
|
||||
analyticsProvider.init()
|
||||
|
||||
// report an event with properties
|
||||
val mockEvent = mockk<VectorAnalyticsEvent>().also {
|
||||
every {
|
||||
it.getProperties()
|
||||
} returns mapOf("appPlatform" to SuperProperties.AppPlatform.Other)
|
||||
every { it.getName() } returns "MockEventName"
|
||||
}
|
||||
|
||||
analyticsProvider.updateSuperProperties(SuperProperties(appPlatform = SuperProperties.AppPlatform.EXA))
|
||||
analyticsProvider.capture(mockEvent)
|
||||
|
||||
verify { mockPosthog.capture(event = "MockEventName", any(), properties = mapOf("appPlatform" to SuperProperties.AppPlatform.Other), any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.element.android.services.analyticsproviders.test
|
||||
|
||||
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.analyticsproviders.api.AnalyticsProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
@@ -29,6 +30,7 @@ class FakeAnalyticsProvider(
|
||||
private val captureLambda: (VectorAnalyticsEvent) -> Unit = { lambdaError() },
|
||||
private val screenLambda: (VectorAnalyticsScreen) -> Unit = { lambdaError() },
|
||||
private val updateUserPropertiesLambda: (UserProperties) -> Unit = { lambdaError() },
|
||||
private val updateSuperPropertiesLambda: (SuperProperties) -> Unit = { lambdaError() },
|
||||
private val trackErrorLambda: (Throwable) -> Unit = { lambdaError() }
|
||||
) : AnalyticsProvider {
|
||||
override fun init() = initLambda()
|
||||
@@ -37,4 +39,5 @@ class FakeAnalyticsProvider(
|
||||
override fun screen(screen: VectorAnalyticsScreen) = screenLambda(screen)
|
||||
override fun updateUserProperties(userProperties: UserProperties) = updateUserPropertiesLambda(userProperties)
|
||||
override fun trackError(throwable: Throwable) = trackErrorLambda(throwable)
|
||||
override fun updateSuperProperties(updatedProperties: SuperProperties) = updateSuperPropertiesLambda(updatedProperties)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user