Merge pull request #8820 from element-hq/feature/bca/bump_posthog_version_3

Bump posthog version to 3.2.0
This commit is contained in:
Benoit Marty 2024-05-16 14:41:15 +02:00 committed by GitHub
commit 4acbe4e582
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 66 additions and 73 deletions

1
changelog.d/8820.misc Normal file
View file

@ -0,0 +1 @@
Update posthog sdk to 3.2.0

View file

@ -121,7 +121,7 @@ ext.groups = [
'com.parse.bolts', 'com.parse.bolts',
'com.pinterest', 'com.pinterest',
'com.pinterest.ktlint', 'com.pinterest.ktlint',
'com.posthog.android', 'com.posthog',
'com.squareup', 'com.squareup',
'com.squareup.curtains', 'com.squareup.curtains',
'com.squareup.duktape', 'com.squareup.duktape',

View file

@ -234,9 +234,7 @@ dependencies {
kapt libs.dagger.hiltCompiler kapt libs.dagger.hiltCompiler
// Analytics // Analytics
implementation('com.posthog.android:posthog:2.0.3') { implementation 'com.posthog:posthog-android:3.2.0'
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation libs.sentry.sentryAndroid implementation libs.sentry.sentryAndroid
// UnifiedPush // UnifiedPush

View file

@ -16,9 +16,7 @@
package im.vector.app.features.analytics.impl package im.vector.app.features.analytics.impl
import com.posthog.android.Options import com.posthog.PostHogInterface
import com.posthog.android.PostHog
import com.posthog.android.Properties
import im.vector.app.core.di.NamedGlobalScope import im.vector.app.core.di.NamedGlobalScope
import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
@ -36,9 +34,6 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private val REUSE_EXISTING_ID: String? = null
private val IGNORED_OPTIONS: Options? = null
@Singleton @Singleton
class DefaultVectorAnalytics @Inject constructor( class DefaultVectorAnalytics @Inject constructor(
private val postHogFactory: PostHogFactory, private val postHogFactory: PostHogFactory,
@ -49,9 +44,9 @@ class DefaultVectorAnalytics @Inject constructor(
@NamedGlobalScope private val globalScope: CoroutineScope @NamedGlobalScope private val globalScope: CoroutineScope
) : VectorAnalytics { ) : VectorAnalytics {
private var posthog: PostHog? = null private var posthog: PostHogInterface? = null
private fun createPosthog(): PostHog? { private fun createPosthog(): PostHogInterface? {
return when { return when {
analyticsConfig.isEnabled -> postHogFactory.createPosthog() analyticsConfig.isEnabled -> postHogFactory.createPosthog()
else -> { else -> {
@ -126,7 +121,7 @@ class DefaultVectorAnalytics @Inject constructor(
posthog?.reset() posthog?.reset()
} else { } else {
Timber.tag(analyticsTag.value).d("identify") 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) { when (_userConsent) {
true -> { true -> {
posthog = createPosthog() posthog = createPosthog()
posthog?.optOut(false) posthog?.optIn()
identifyPostHog() identifyPostHog()
pendingUserProperties?.let { doUpdateUserProperties(it) } pendingUserProperties?.let { doUpdateUserProperties(it) }
pendingUserProperties = null pendingUserProperties = null
@ -163,8 +158,8 @@ class DefaultVectorAnalytics @Inject constructor(
false -> { false -> {
// When opting out, ensure that the queue is flushed first, or it will be flushed later (after user has revoked consent) // When opting out, ensure that the queue is flushed first, or it will be flushed later (after user has revoked consent)
posthog?.flush() posthog?.flush()
posthog?.optOut(true) posthog?.optOut()
posthog?.shutdown() posthog?.close()
posthog = null posthog = null
} }
} }
@ -177,6 +172,7 @@ class DefaultVectorAnalytics @Inject constructor(
?.takeIf { userConsent == true } ?.takeIf { userConsent == true }
?.capture( ?.capture(
event.getName(), event.getName(),
analyticsId,
event.getProperties()?.toPostHogProperties() event.getProperties()?.toPostHogProperties()
) )
} }
@ -197,28 +193,38 @@ class DefaultVectorAnalytics @Inject constructor(
} }
private fun doUpdateUserProperties(userProperties: UserProperties) { private fun doUpdateUserProperties(userProperties: UserProperties) {
// we need a distinct id to set user properties
val distinctId = analyticsId ?: return
posthog posthog
?.takeIf { userConsent == true } ?.takeIf { userConsent == true }
?.identify(REUSE_EXISTING_ID, userProperties.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS) ?.identify(distinctId, userProperties.getProperties())
} }
private fun Map<String, Any?>?.toPostHogProperties(): Properties? { private fun Map<String, Any?>?.toPostHogProperties(): Map<String, Any>? {
if (this == null) return null if (this == null) return null
return Properties().apply { val nonNulls = HashMap<String, Any>()
putAll(this@toPostHogProperties) 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. * 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. * The UserProperties event has nullable properties to allow for clients to opt in.
*/ */
private fun Map<String, Any?>.toPostHogUserProperties(): Properties { private fun Map<String, Any?>.toPostHogUserProperties(): Map<String, Any> {
return Properties().apply { val nonNulls = HashMap<String, Any>()
putAll(this@toPostHogUserProperties.filter { it.value != null }) this.forEach { (key, value) ->
if (value != null) {
nonNulls[key] = value
} }
} }
return nonNulls
}
override fun trackError(throwable: Throwable) { override fun trackError(throwable: Throwable) {
sentryAnalytics sentryAnalytics

View file

@ -17,7 +17,9 @@
package im.vector.app.features.analytics.impl package im.vector.app.features.analytics.impl
import android.content.Context 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.core.resources.BuildMeta
import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import javax.inject.Inject import javax.inject.Inject
@ -28,29 +30,17 @@ class PostHogFactory @Inject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) { ) {
fun createPosthog(): PostHog { fun createPosthog(): PostHogInterface {
return PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost) val config = PostHogAndroidConfig(
// Record certain application events automatically! (off/false by default) apiKey = analyticsConfig.postHogApiKey,
// .captureApplicationLifecycleEvents() host = analyticsConfig.postHogHost,
// Record screen views automatically! (off/false by default) // we do that manually
// .recordScreenViews() captureScreenViews = false,
// Capture deep links as part of the screen call. (off by default) ).also {
// .captureDeepLinks() if (buildMeta.isDebug) {
// Maximum number of events to keep in queue before flushing (default 20) it.debug = true
// .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
} }
return PostHogAndroid.with(context, config)
} }
} }

View file

@ -16,9 +16,6 @@
package im.vector.app.features.analytics.impl 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.FakeAnalyticsStore
import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory
import im.vector.app.test.fakes.FakePostHog import im.vector.app.test.fakes.FakePostHog
@ -128,7 +125,7 @@ class DefaultVectorAnalyticsTest {
defaultVectorAnalytics.screen(A_SCREEN_EVENT) 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 @Test
@ -146,7 +143,7 @@ class DefaultVectorAnalyticsTest {
defaultVectorAnalytics.capture(AN_EVENT) defaultVectorAnalytics.capture(AN_EVENT)
fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.toPostHogProperties()) fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.getProperties().clearNulls())
} }
@Test @Test
@ -176,16 +173,16 @@ class DefaultVectorAnalyticsTest {
fakeSentryAnalytics.verifyNoErrorTracking() fakeSentryAnalytics.verifyNoErrorTracking()
} }
}
private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? { private fun Map<String, Any?>?.clearNulls(): Map<String, Any>? {
return getProperties()?.let { properties -> if (this == null) return null
Properties().also { it.putAll(properties) }
} val nonNulls = HashMap<String, Any>()
} this.forEach { (key, value) ->
if (value != null) {
private fun VectorAnalyticsEvent.toPostHogProperties(): Properties? { nonNulls[key] = value
return getProperties()?.let { properties -> }
Properties().also { it.putAll(properties) } }
return nonNulls
} }
} }

View file

@ -17,8 +17,7 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import android.os.Looper import android.os.Looper
import com.posthog.android.PostHog import com.posthog.PostHogInterface
import com.posthog.android.Properties
import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.plan.UserProperties
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -36,16 +35,19 @@ class FakePostHog {
every { Looper.getMainLooper() } returns looper every { Looper.getMainLooper() } returns looper
} }
val instance = mockk<PostHog>(relaxed = true) val instance = mockk<PostHogInterface>(relaxed = true)
fun verifyOptOutStatus(optedOut: Boolean) { fun verifyOptOutStatus(optedOut: Boolean) {
verify { instance.optOut(optedOut) } if (optedOut) {
verify { instance.optOut() }
} else {
verify { instance.optIn() }
}
} }
fun verifyIdentifies(analyticsId: String, userProperties: UserProperties?) { fun verifyIdentifies(analyticsId: String, userProperties: UserProperties?) {
verify { verify {
val postHogProperties = userProperties?.getProperties() val postHogProperties = userProperties?.getProperties()
?.let { rawProperties -> Properties().also { it.putAll(rawProperties) } }
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
instance.identify(analyticsId, postHogProperties, null) instance.identify(analyticsId, postHogProperties, null)
} }
@ -55,7 +57,7 @@ class FakePostHog {
verify { instance.reset() } verify { instance.reset() }
} }
fun verifyScreenTracked(name: String, properties: Properties?) { fun verifyScreenTracked(name: String, properties: Map<String, Any>?) {
verify { instance.screen(name, properties) } verify { instance.screen(name, properties) }
} }
@ -63,12 +65,11 @@ class FakePostHog {
verify(exactly = 0) { verify(exactly = 0) {
instance.screen(any()) instance.screen(any())
instance.screen(any(), any()) instance.screen(any(), any())
instance.screen(any(), any(), any())
} }
} }
fun verifyEventTracked(name: String, properties: Properties?) { fun verifyEventTracked(name: String, properties: Map<String, Any>?) {
verify { instance.capture(name, properties) } verify { instance.capture(name, null, properties) }
} }
fun verifyNoEventTracking() { fun verifyNoEventTracking() {

View file

@ -16,12 +16,12 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import com.posthog.android.PostHog import com.posthog.PostHogInterface
import im.vector.app.features.analytics.impl.PostHogFactory import im.vector.app.features.analytics.impl.PostHogFactory
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
class FakePostHogFactory(postHog: PostHog) { class FakePostHogFactory(postHog: PostHogInterface) {
val instance = mockk<PostHogFactory>().also { val instance = mockk<PostHogFactory>().also {
every { it.createPosthog() } returns postHog every { it.createPosthog() } returns postHog
} }