Skip to content

Commit

Permalink
allow optional reuse of anonymous id (#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
beradeep authored Mar 3, 2025
1 parent 6185d73 commit 103a969
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 25 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
97 changes: 87 additions & 10 deletions posthog-samples/posthog-android-sample/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,17 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.1.3" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.3)" variant="all" version="8.1.3">
<issues format="6" by="lint 8.2.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.2.2)" variant="all" version="8.2.2">

<issue
id="GradleDependency"
message="A newer version of androidx.activity:activity-compose than 1.8.0 is available: 1.8.2"
errorLine1=" implementation(&quot;androidx.activity:activity-compose:1.8.0&quot;)"
message="A newer version of androidx.activity:activity-compose than 1.7.2 is available: 1.10.0"
errorLine1=" implementation(&quot;androidx.activity:activity-compose:1.7.2&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle.kts"
line="76"
line="77"
column="21"/>
</issue>

<issue
id="GradleDependency"
message="A newer version of com.squareup.leakcanary:leakcanary-android than 2.12 is available: 2.14"
errorLine1=" debugImplementation(&quot;com.squareup.leakcanary:leakcanary-android:2.12&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle.kts"
line="87"
column="26"/>
</issue>

<issue
id="ComposableNaming"
message="Composable functions that return Unit should start with an uppercase letter"
errorLine1="fun greeting("
errorLine2=" ~~~~~~~~">
<location
file="src/main/java/com/posthog/android/sample/MainActivity.kt"
line="42"
column="5"/>
</issue>

<issue
id="ComposableNaming"
message="Composable functions that return Unit should start with an uppercase letter"
errorLine1="fun greetingPreview() {"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/posthog/android/sample/MainActivity.kt"
line="59"
column="5"/>
</issue>

<issue
id="ComposableNaming"
message="Composable functions that return Unit should start with an uppercase letter"
errorLine1="fun postHogAndroidSampleTheme("
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/posthog/android/sample/ui/theme/Theme.kt"
line="42"
column="5"/>
</issue>

<issue
id="UnusedResources"
message="The resource `R.drawable.android_logo` appears to be unused">
Expand All @@ -26,7 +70,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/strings.xml"
line="4"
line="5"
column="19"/>
</issue>

Expand All @@ -37,14 +81,47 @@
file="src/main/res/drawable/android_logo.png"/>
</issue>

<issue
id="Autofill"
message="Missing `autofillHints` attribute"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/normal_activity.xml"
line="49"
column="6"/>
</issue>

<issue
id="LabelFor"
message="Missing accessibility label: provide either a view with an `android:labelFor` that references this view or provide an `android:hint`"
errorLine1=" &lt;EditText"
errorLine2=" ~~~~~~~~">
<location
file="src/main/res/layout/normal_activity.xml"
line="49"
column="6"/>
</issue>

<issue
id="HardcodedText"
message="Hardcoded string &quot;Button text&quot;, should use `@string` resource"
errorLine1=" android:text=&quot;Button text&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/normal_activity.xml"
line="39"
column="9"/>
</issue>

<issue
id="HardcodedText"
message="Hardcoded string &quot;Text&quot;, should use `@string` resource"
errorLine1=" android:text=&quot;Text&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
message="Hardcoded string &quot;EditText text&quot;, should use `@string` resource"
errorLine1=" android:text=&quot;EditText text&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/nothing_activity.xml"
line="14"
file="src/main/res/layout/normal_activity.xml"
line="52"
column="9"/>
</issue>

Expand Down
6 changes: 4 additions & 2 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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 <init> (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 <init> (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 <init> (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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 27 additions & 13 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -549,11 +549,14 @@ public class PostHog private constructor(
val previousDistinctId = this.distinctId

val props = mutableMapOf<String, Any>()
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
Expand All @@ -572,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

Expand Down Expand Up @@ -686,7 +691,11 @@ public class PostHog private constructor(
val groups = getPreferences().getValue(GROUPS) as? Map<String, String>

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")
Expand Down Expand Up @@ -774,8 +783,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)
// 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()
featureFlagsCalled.clear()
synchronized(identifiedLock) {
Expand Down
24 changes: 24 additions & 0 deletions posthog/src/main/java/com/posthog/PostHogConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ 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.
*
* 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,
/**
* Determines the behavior for processing user profiles.
* - `ALWAYS`: We will process persons data for all events.
Expand Down
56 changes: 56 additions & 0 deletions posthog/src/test/java/com/posthog/PostHogTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -59,6 +60,7 @@ internal class PostHogTest {
addIntegration(integration)
}
this.sendFeatureFlagEvent = sendFeatureFlagEvent
this.reuseAnonymousId = reuseAnonymousId
this.cachePreferences = cachePreferences
this.propertiesSanitizer = propertiesSanitizer
}
Expand Down Expand Up @@ -1036,6 +1038,60 @@ 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 `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<PostHogBatchEvent>(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")
Expand Down

0 comments on commit 103a969

Please sign in to comment.