Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow optional reuse of anonymous id #229

Merged
merged 8 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading