diff --git a/changelog.d/8820.misc b/changelog.d/8820.misc new file mode 100644 index 00000000000..ee67a27457d --- /dev/null +++ b/changelog.d/8820.misc @@ -0,0 +1 @@ +Update posthog sdk to 3.2.0 diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 449fc11d438..f16ed4f809c 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -121,7 +121,7 @@ ext.groups = [ 'com.parse.bolts', 'com.pinterest', 'com.pinterest.ktlint', - 'com.posthog.android', + 'com.posthog', 'com.squareup', 'com.squareup.curtains', 'com.squareup.duktape', diff --git a/vector/build.gradle b/vector/build.gradle index db6a9d72e4c..368fa217db0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -234,9 +234,7 @@ dependencies { kapt libs.dagger.hiltCompiler // Analytics - implementation('com.posthog.android:posthog:2.0.3') { - exclude group: 'com.android.support', module: 'support-annotations' - } + implementation 'com.posthog:posthog-android:3.2.0' implementation libs.sentry.sentryAndroid // UnifiedPush 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 acc6ebf51e8..8520a40ca2d 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 @@ -16,9 +16,7 @@ package im.vector.app.features.analytics.impl -import com.posthog.android.Options -import com.posthog.android.PostHog -import com.posthog.android.Properties +import com.posthog.PostHogInterface import im.vector.app.core.di.NamedGlobalScope import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.VectorAnalytics @@ -36,9 +34,6 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -private val REUSE_EXISTING_ID: String? = null -private val IGNORED_OPTIONS: Options? = null - @Singleton class DefaultVectorAnalytics @Inject constructor( private val postHogFactory: PostHogFactory, @@ -49,9 +44,9 @@ class DefaultVectorAnalytics @Inject constructor( @NamedGlobalScope private val globalScope: CoroutineScope ) : VectorAnalytics { - private var posthog: PostHog? = null + private var posthog: PostHogInterface? = null - private fun createPosthog(): PostHog? { + private fun createPosthog(): PostHogInterface? { return when { analyticsConfig.isEnabled -> postHogFactory.createPosthog() else -> { @@ -126,7 +121,7 @@ class DefaultVectorAnalytics @Inject constructor( posthog?.reset() } else { Timber.tag(analyticsTag.value).d("identify") - posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS) + posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties()) } } @@ -155,7 +150,7 @@ class DefaultVectorAnalytics @Inject constructor( when (_userConsent) { true -> { posthog = createPosthog() - posthog?.optOut(false) + posthog?.optIn() identifyPostHog() pendingUserProperties?.let { doUpdateUserProperties(it) } pendingUserProperties = null @@ -163,8 +158,8 @@ class DefaultVectorAnalytics @Inject constructor( false -> { // When opting out, ensure that the queue is flushed first, or it will be flushed later (after user has revoked consent) posthog?.flush() - posthog?.optOut(true) - posthog?.shutdown() + posthog?.optOut() + posthog?.close() posthog = null } } @@ -177,6 +172,7 @@ class DefaultVectorAnalytics @Inject constructor( ?.takeIf { userConsent == true } ?.capture( event.getName(), + analyticsId, event.getProperties()?.toPostHogProperties() ) } @@ -197,27 +193,37 @@ 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(REUSE_EXISTING_ID, userProperties.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS) + ?.identify(distinctId, userProperties.getProperties()) } - private fun Map?.toPostHogProperties(): Properties? { + private fun Map?.toPostHogProperties(): Map? { if (this == null) return null - return Properties().apply { - putAll(this@toPostHogProperties) + val nonNulls = HashMap() + this.forEach { (key, value) -> + if (value != null) { + nonNulls[key] = value + } } + return nonNulls } /** * We avoid sending nulls as part of the UserProperties as this will reset the values across all devices. * The UserProperties event has nullable properties to allow for clients to opt in. */ - private fun Map.toPostHogUserProperties(): Properties { - return Properties().apply { - putAll(this@toPostHogUserProperties.filter { it.value != null }) + private fun Map.toPostHogUserProperties(): Map { + val nonNulls = HashMap() + this.forEach { (key, value) -> + if (value != null) { + nonNulls[key] = value + } } + return nonNulls } override fun trackError(throwable: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt index 7442989352f..cbb6b9dcfdd 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt @@ -17,7 +17,9 @@ package im.vector.app.features.analytics.impl import android.content.Context -import com.posthog.android.PostHog +import com.posthog.PostHogInterface +import com.posthog.android.PostHogAndroid +import com.posthog.android.PostHogAndroidConfig import im.vector.app.core.resources.BuildMeta import im.vector.app.features.analytics.AnalyticsConfig import javax.inject.Inject @@ -28,29 +30,17 @@ class PostHogFactory @Inject constructor( private val buildMeta: BuildMeta, ) { - fun createPosthog(): PostHog { - return PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost) - // Record certain application events automatically! (off/false by default) - // .captureApplicationLifecycleEvents() - // Record screen views automatically! (off/false by default) - // .recordScreenViews() - // Capture deep links as part of the screen call. (off by default) - // .captureDeepLinks() - // Maximum number of events to keep in queue before flushing (default 20) - // .flushQueueSize(20) - // Max delay before flushing the queue (30 seconds) - // .flushInterval(30, TimeUnit.SECONDS) - // Enable or disable collection of ANDROID_ID (true) - .collectDeviceId(false) - .logLevel(getLogLevel()) - .build() - } - - private fun getLogLevel(): PostHog.LogLevel { - return if (buildMeta.isDebug) { - PostHog.LogLevel.DEBUG - } else { - PostHog.LogLevel.INFO + fun createPosthog(): PostHogInterface { + val config = PostHogAndroidConfig( + apiKey = analyticsConfig.postHogApiKey, + host = analyticsConfig.postHogHost, + // we do that manually + captureScreenViews = false, + ).also { + if (buildMeta.isDebug) { + it.debug = true + } } + return PostHogAndroid.with(context, config) } } 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 226a2d8ecf6..ea8beaa86c8 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,9 +16,6 @@ package im.vector.app.features.analytics.impl -import com.posthog.android.Properties -import im.vector.app.features.analytics.itf.VectorAnalyticsEvent -import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.test.fakes.FakeAnalyticsStore import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakePostHog @@ -128,7 +125,7 @@ class DefaultVectorAnalyticsTest { defaultVectorAnalytics.screen(A_SCREEN_EVENT) - fakePostHog.verifyScreenTracked(A_SCREEN_EVENT.getName(), A_SCREEN_EVENT.toPostHogProperties()) + fakePostHog.verifyScreenTracked(A_SCREEN_EVENT.getName(), A_SCREEN_EVENT.getProperties()) } @Test @@ -146,7 +143,7 @@ class DefaultVectorAnalyticsTest { defaultVectorAnalytics.capture(AN_EVENT) - fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.toPostHogProperties()) + fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.getProperties().clearNulls()) } @Test @@ -176,16 +173,16 @@ class DefaultVectorAnalyticsTest { fakeSentryAnalytics.verifyNoErrorTracking() } -} -private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? { - return getProperties()?.let { properties -> - Properties().also { it.putAll(properties) } - } -} + private fun Map?.clearNulls(): Map? { + if (this == null) return null -private fun VectorAnalyticsEvent.toPostHogProperties(): Properties? { - return getProperties()?.let { properties -> - Properties().also { it.putAll(properties) } + val nonNulls = HashMap() + this.forEach { (key, value) -> + if (value != null) { + nonNulls[key] = value + } + } + return nonNulls } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt index e14f809e666..ccc326ba145 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt @@ -17,8 +17,7 @@ package im.vector.app.test.fakes import android.os.Looper -import com.posthog.android.PostHog -import com.posthog.android.Properties +import com.posthog.PostHogInterface import im.vector.app.features.analytics.plan.UserProperties import io.mockk.every import io.mockk.mockk @@ -36,16 +35,19 @@ class FakePostHog { every { Looper.getMainLooper() } returns looper } - val instance = mockk(relaxed = true) + val instance = mockk(relaxed = true) fun verifyOptOutStatus(optedOut: Boolean) { - verify { instance.optOut(optedOut) } + if (optedOut) { + verify { instance.optOut() } + } else { + verify { instance.optIn() } + } } fun verifyIdentifies(analyticsId: String, userProperties: UserProperties?) { verify { val postHogProperties = userProperties?.getProperties() - ?.let { rawProperties -> Properties().also { it.putAll(rawProperties) } } ?.takeIf { it.isNotEmpty() } instance.identify(analyticsId, postHogProperties, null) } @@ -55,7 +57,7 @@ class FakePostHog { verify { instance.reset() } } - fun verifyScreenTracked(name: String, properties: Properties?) { + fun verifyScreenTracked(name: String, properties: Map?) { verify { instance.screen(name, properties) } } @@ -63,12 +65,11 @@ class FakePostHog { verify(exactly = 0) { instance.screen(any()) instance.screen(any(), any()) - instance.screen(any(), any(), any()) } } - fun verifyEventTracked(name: String, properties: Properties?) { - verify { instance.capture(name, properties) } + fun verifyEventTracked(name: String, properties: Map?) { + verify { instance.capture(name, null, properties) } } fun verifyNoEventTracking() { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt index 1d18c97d325..3650aa6bba6 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt @@ -16,12 +16,12 @@ package im.vector.app.test.fakes -import com.posthog.android.PostHog +import com.posthog.PostHogInterface import im.vector.app.features.analytics.impl.PostHogFactory import io.mockk.every import io.mockk.mockk -class FakePostHogFactory(postHog: PostHog) { +class FakePostHogFactory(postHog: PostHogInterface) { val instance = mockk().also { every { it.createPosthog() } returns postHog }