From 9c91559a0e289bf8874c35b14e2f8042b6f04e2e Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Mon, 24 Feb 2025 22:48:12 +0530 Subject: [PATCH 1/6] allow optional reuse of anonymous id --- posthog-android/lint-baseline.xml | 21 +++- .../posthog-android-sample/lint-baseline.xml | 97 +++++++++++++++++-- posthog/api/posthog.api | 6 +- posthog/src/main/java/com/posthog/PostHog.kt | 9 +- .../main/java/com/posthog/PostHogConfig.kt | 6 ++ .../src/test/java/com/posthog/PostHogTest.kt | 17 ++++ 6 files changed, 137 insertions(+), 19 deletions(-) diff --git a/posthog-android/lint-baseline.xml b/posthog-android/lint-baseline.xml index 3b6ffb53..fc7c1106 100644 --- a/posthog-android/lint-baseline.xml +++ b/posthog-android/lint-baseline.xml @@ -36,7 +36,7 @@ + message="A newer version of org.mockito.kotlin:mockito-kotlin than 4.1.0 is available: 5.4.0" + errorLine1=" testImplementation("org.mockito.kotlin:mockito-kotlin:${PosthogBuildConfig.Dependencies.MOCKITO}")" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + + + + diff --git a/posthog-samples/posthog-android-sample/lint-baseline.xml b/posthog-samples/posthog-android-sample/lint-baseline.xml index 55483dd2..83c97d90 100644 --- a/posthog-samples/posthog-android-sample/lint-baseline.xml +++ b/posthog-samples/posthog-android-sample/lint-baseline.xml @@ -1,17 +1,61 @@ - + + + + + + + + + + + + + + + + + @@ -26,7 +70,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -37,14 +81,47 @@ file="src/main/res/drawable/android_logo.png"/> + + + + + + + + + + + + + message="Hardcoded string "EditText text", should use `@string` resource" + errorLine1=" android:text="EditText text"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index 2fe6ae95..b8e8f004 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -72,8 +72,8 @@ public final class com/posthog/PostHog$Companion : com/posthog/PostHogInterface public class com/posthog/PostHogConfig { public static final field Companion Lcom/posthog/PostHogConfig$Companion; public static final field DEFAULT_HOST Ljava/lang/String; - public fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;Lcom/posthog/PersonProfiles;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;Lcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V public final fun getApiKey ()Ljava/lang/String; public final fun getCachePreferences ()Lcom/posthog/internal/PostHogPreferences; @@ -97,6 +97,7 @@ public class com/posthog/PostHogConfig { public final fun getPreloadFeatureFlags ()Z public final fun getPropertiesSanitizer ()Lcom/posthog/PostHogPropertiesSanitizer; public final fun getReplayStoragePrefix ()Ljava/lang/String; + public final fun getReuseAnonymousId ()Z public final fun getSdkName ()Ljava/lang/String; public final fun getSdkVersion ()Ljava/lang/String; public final fun getSendFeatureFlagEvent ()Z @@ -124,6 +125,7 @@ public class com/posthog/PostHogConfig { public final fun setPreloadFeatureFlags (Z)V public final fun setPropertiesSanitizer (Lcom/posthog/PostHogPropertiesSanitizer;)V public final fun setReplayStoragePrefix (Ljava/lang/String;)V + public final fun setReuseAnonymousId (Z)V public final fun setSdkName (Ljava/lang/String;)V public final fun setSdkVersion (Ljava/lang/String;)V public final fun setSendFeatureFlagEvent (Z)V diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index e9337687..c7d4fa74 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -774,8 +774,13 @@ public class PostHog private constructor( // only remove properties, preserve BUILD and VERSION keys in order to to fix over-sending // of 'Application Installed' events and under-sending of 'Application Updated' events - val except = listOf(VERSION, BUILD) - getPreferences().clear(except = except) + val except = mutableListOf(VERSION, BUILD) + config?.let { + if (it.reuseAnonymousId) { + except.add(ANONYMOUS_ID) + } + } + getPreferences().clear(except = except.toList()) featureFlags?.clear() featureFlagsCalled.clear() synchronized(identifiedLock) { diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt index 21f6fb9b..6448bac3 100644 --- a/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -101,6 +101,12 @@ public open class PostHogConfig( * generating anonymous id (which as of now is just random UUID v7) */ public var getAnonymousId: ((UUID) -> UUID) = { it }, + /** + * Flag to reuse the anonymous id. + * If enabled, the anonymous id will be reused across `identify()` and `reset()` calls. + * Defaults to false. + */ + public var reuseAnonymousId: Boolean = false, /** * Determines the behavior for processing user profiles. * - `ALWAYS`: We will process persons data for all events. diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index af534383..edd89755 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -44,6 +44,7 @@ internal class PostHogTest { preloadFeatureFlags: Boolean = true, reloadFeatureFlags: Boolean = true, sendFeatureFlagEvent: Boolean = true, + reuseAnonymousId: Boolean = false, integration: PostHogIntegration? = null, cachePreferences: PostHogMemoryPreferences = PostHogMemoryPreferences(), propertiesSanitizer: PostHogPropertiesSanitizer? = null, @@ -59,6 +60,7 @@ internal class PostHogTest { addIntegration(integration) } this.sendFeatureFlagEvent = sendFeatureFlagEvent + this.reuseAnonymousId = reuseAnonymousId this.cachePreferences = cachePreferences this.propertiesSanitizer = propertiesSanitizer } @@ -1036,6 +1038,21 @@ internal class PostHogTest { assertEquals("myNewDistinctId", sut.distinctId()) } + @Test + fun `reuse anonymousId when flag reuseAnonymousId is true`() { + val http = mockHttp() + val url = http.url("/") + + val sut = getSut(url.toString(), preloadFeatureFlags = false, reloadFeatureFlags = false, reuseAnonymousId = true) + + val anonymousId = sut.distinctId() + sut.reset() + + queueExecutor.shutdownAndAwaitTermination() + + assertEquals(anonymousId, sut.distinctId()) + } + @Test fun `do not send feature flags called event twice`() { val file = File("src/test/resources/json/basic-decide-no-errors.json") From 2a24ebd6d607e76596248932a0ec21f8aced0efd Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Thu, 27 Feb 2025 00:56:47 +0530 Subject: [PATCH 2/6] fix users merging issue --- posthog/src/main/java/com/posthog/PostHog.kt | 27 ++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index c7d4fa74..c0fbcb27 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -549,11 +549,14 @@ public class PostHog private constructor( val previousDistinctId = this.distinctId val props = mutableMapOf() - val anonymousId = this.anonymousId - if (anonymousId.isNotBlank()) { - props["\$anon_distinct_id"] = anonymousId - } else { - config?.logger?.log("identify called with invalid anonymousId: $anonymousId.") + + if (config?.reuseAnonymousId != true) { + val anonymousId = this.anonymousId + if (anonymousId.isNotBlank()) { + props["\$anon_distinct_id"] = anonymousId + } else { + config?.logger?.log("identify called with invalid anonymousId: $anonymousId.") + } } val hasDifferentDistinctId = previousDistinctId != distinctId @@ -686,7 +689,11 @@ public class PostHog private constructor( val groups = getPreferences().getValue(GROUPS) as? Map val distinctId = this.distinctId - val anonymousId = this.anonymousId + var anonymousId: String? = null + + if (config?.reuseAnonymousId != true) { + anonymousId = this.anonymousId + } if (distinctId.isBlank()) { config?.logger?.log("Feature flags not loaded, distinctId is invalid: $distinctId") @@ -775,10 +782,10 @@ public class PostHog private constructor( // only remove properties, preserve BUILD and VERSION keys in order to to fix over-sending // of 'Application Installed' events and under-sending of 'Application Updated' events val except = mutableListOf(VERSION, BUILD) - config?.let { - if (it.reuseAnonymousId) { - except.add(ANONYMOUS_ID) - } + // preserve the ANONYMOUS_ID if reuseAnonymousId is enabled (for preserving a guest user + // account on the device) + if (config?.reuseAnonymousId == true) { + except.add(ANONYMOUS_ID) } getPreferences().clear(except = except.toList()) featureFlags?.clear() From 28ace3550d2da2d5a7a345895e669c1d1587c439 Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Thu, 27 Feb 2025 19:44:15 +0530 Subject: [PATCH 3/6] fix: prevent anon id overwrite when reusing it --- posthog/src/main/java/com/posthog/PostHog.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index c0fbcb27..eb322259 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -575,11 +575,13 @@ public class PostHog private constructor( userPropertiesSetOnce = userPropertiesSetOnce, ) - // We keep the AnonymousId to be used by decide calls and identify to link the previousId - if (previousDistinctId.isNotBlank()) { - this.anonymousId = previousDistinctId - } else { - config?.logger?.log("identify called with invalid former distinctId: $previousDistinctId.") + if (config?.reuseAnonymousId != true) { + // We keep the AnonymousId to be used by decide calls and identify to link the previousId + if (previousDistinctId.isNotBlank()) { + this.anonymousId = previousDistinctId + } else { + config?.logger?.log("identify called with invalid former distinctId: $previousDistinctId.") + } } this.distinctId = distinctId From 67376b814c363e4ceabc7319cdad03814e3a9140 Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Thu, 27 Feb 2025 19:52:19 +0530 Subject: [PATCH 4/6] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc1fa72f..77d54d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- feat: support reuse of `anonymousId` between user changes ([#229](https://github.com/PostHog/posthog-android/pull/229)) + ## 3.11.3 - 2025-02-26 - feat: support quota limiting for feature flags ([#228](https://github.com/PostHog/posthog-android/pull/228) and [#230](https://github.com/PostHog/posthog-android/pull/230)) From 4e40cc93b71144f5afaa2f3e9de1710d51052672 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 28 Feb 2025 13:41:16 +0100 Subject: [PATCH 5/6] fix --- .../src/main/java/com/posthog/PostHogConfig.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt index 6448bac3..bfc9b07a 100644 --- a/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -104,6 +104,24 @@ public open class PostHogConfig( /** * Flag to reuse the anonymous id. * If enabled, the anonymous id will be reused across `identify()` and `reset()` calls. + * + * Events captured before the user has identified won't be linked to the identified user, e.g.: + * + * Guest user (anonymous id) captures an event (click on X). + * + * User logs in (User A) calls identify + * User A captures an event (clicks on Y) + * User A logs out (calls reset) + * + * Guest user (reused anonymous id) captures an event (click on Z) + * + * click on X and click on Z events will be associated to the same user (anonymous id) + * + * clicks on Y event will be associated only with User A + * + * This will allow you to reuse the anonymous id as a Guest user, but all the events happening before + * or after the user logs in and logs out won't be associated. + * * Defaults to false. */ public var reuseAnonymousId: Boolean = false, From 12316dc02fe887b7b0908fde856ee4b27a2c40e0 Mon Sep 17 00:00:00 2001 From: Subhradeep Date: Sat, 1 Mar 2025 02:16:28 +0530 Subject: [PATCH 6/6] unit tests --- .../src/test/java/com/posthog/PostHogTest.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index edd89755..5ea57897 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -1053,6 +1053,45 @@ internal class PostHogTest { assertEquals(anonymousId, sut.distinctId()) } + @Test + fun `anonymousId is not overwritten on re-identify when reuseAnonymousId is true`() { + val http = mockHttp() + val url = http.url("/") + + val sut = getSut(url.toString(), preloadFeatureFlags = false, reloadFeatureFlags = false, reuseAnonymousId = true) + + val anonymousId = sut.distinctId() + sut.identify("myDistinctId") + sut.identify("myNewDistinctId") + sut.reset() + + queueExecutor.shutdownAndAwaitTermination() + + assertEquals(anonymousId, sut.distinctId()) + } + + @Test + fun `do not link anonymousId on identify when reuseAnonymousId is true`() { + val http = mockHttp() + val url = http.url("/") + + val sut = getSut(url.toString(), preloadFeatureFlags = false, reloadFeatureFlags = false, reuseAnonymousId = true) + + sut.identify("myDistinctId") + + queueExecutor.shutdownAndAwaitTermination() + + val request = http.takeRequest() + + assertEquals(1, http.requestCount) + val content = request.body.unGzip() + val batch = serializer.deserialize(content.reader()) + + val theEvent = batch.batch.first() + + assertNull(theEvent.properties!!["\$anon_distinct_id"]) + } + @Test fun `do not send feature flags called event twice`() { val file = File("src/test/resources/json/basic-decide-no-errors.json")