From e40f08b1f8641756b1bb0b30bcd5f91fc14d7565 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Thu, 29 Oct 2026 15:17:49 -0400 Subject: [PATCH] fix: let a caller's $process_person_profile override the personless option Reverts the person-processing half of #905. A passed or registered `$process_person_profile` property again stops the SDK from adding `process_person_profile: false` when person processing is off, so the caller wins, as the client property precedence in sdk-specs `capture` (#143) requires. The send still hoists the property into the option. Co-Authored-By: Claude Opus 5.5 --- .changeset/android-v4-major.md | 2 +- docs/migration-3.x-to-4.0.md | 2 +- posthog/src/main/java/com/posthog/PostHog.kt | 20 ++++++++++--------- .../com/posthog/PostHogCaptureOptionsTest.kt | 12 +++++------ .../com/posthog/PostHogPersonProfilesTest.kt | 10 +++++----- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.changeset/android-v4-major.md b/.changeset/android-v4-major.md index 44450d6c1..f7772aa54 100644 --- a/.changeset/android-v4-major.md +++ b/.changeset/android-v4-major.md @@ -24,7 +24,7 @@ - **Breaking:** add an `options` argument to `capture`, `screen`, `identify`, `setPersonProperties`, `alias`, `group` and `captureException` for per-event capture options such as `cookieless_mode`, sent apart from the properties and editable in `beforeSend` through `PostHogEvent.options`; Java callers pass the trailing argument or `null`, and classes that implement `PostHogInterface` add the parameter - **Breaking:** move `identify(distinctId, userProperties, userPropertiesSetOnce)` from `PostHogCoreInterface` to `PostHogStatelessInterface`; `PostHogInterface` declares `identify` with the trailing `options` parameter - **Breaking:** registered super properties now override the properties the SDK sets itself, such as `$os_name`; super and event properties also override `$is_identified`, `$process_person_profile` and the session replay debug properties, which the SDK no longer re-applies after the caller's values -- **Breaking:** the SDK sets the `process_person_profile` option to `false` only when person processing is off, and no longer adds a `$process_person_profile` property to every event; a passed or registered `$process_person_profile` property no longer overrides that `false`, so pass the `process_person_profile` option instead +- **Breaking:** the SDK sets the `process_person_profile` option to `false` only when person processing is off, and no longer adds a `$process_person_profile` property to every event - **Breaking:** keep the uuid of its own capture on the event `beforeSend` returns, so a uuid set in `beforeSend`, for example with `copy(uuid = …)`, is replaced - **Breaking:** add `captureAi` to send AI events such as `$ai_generation` to PostHog's AI endpoint (`/i/v1/ai/events`), which accepts events whose properties are up to 8 MiB, on its own disk queue; it returns the event uuid, or `null` when the event isn't queued, and classes that implement `PostHogInterface` add the method - use the current distinct ID when the `distinctId` argument to `capture` is empty or whitespace-only, instead of dropping the event diff --git a/docs/migration-3.x-to-4.0.md b/docs/migration-3.x-to-4.0.md index 4d1bed0aa..578d29a23 100644 --- a/docs/migration-3.x-to-4.0.md +++ b/docs/migration-3.x-to-4.0.md @@ -150,7 +150,7 @@ When the event is sent, the SDK removes each one from `properties` and uses it f - An option you pass to any of these methods or set in `beforeSend` wins over its legacy property. An option set to `null` counts as unset, so the legacy property fills it. - To change person processing in `beforeSend`, edit `event.options`. A `$process_person_profile` property the hook adds only fills an option that is still unset, so it doesn't override the `false` the SDK set for an anonymous event. -- When `personProfiles` turns person processing off, the SDK sets the `process_person_profile` option to `false`. Otherwise it sends neither, because PostHog processes person profiles by default. In 3.x, every event carried a `$process_person_profile` property. To change person processing for one event, pass the `process_person_profile` option. A `$process_person_profile` property, passed or registered, only fills that option when it's unset, so it can't override the `false` the SDK sets when person processing is off. +- When `personProfiles` turns person processing off, the SDK sets the `process_person_profile` option to `false`. Otherwise it sends neither, because PostHog processes person profiles by default. In 3.x, every event carried a `$process_person_profile` property. To change person processing for one event, pass the `process_person_profile` option or the `$process_person_profile` property. A registered `$process_person_profile` changes it for every event. - A `$set` sent with `process_person_profile: false` doesn't update the person, so it no longer suppresses a later `setPersonProperties` or `identify` call with the same properties as a duplicate. - The SDK sends option values as given. PostHog reads common forms of a value, such as `"false"` or `0` for a boolean, and drops an event whose option value it can't read. - Other option keys are sent unchanged, and PostHog ignores keys it doesn't know. diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index ae3debe5c..d4398bffe 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -809,8 +809,7 @@ public class PostHog private constructor( props["\$is_identified"] = isIdentified // No `$process_person_profile`: person processing is sent as the `process_person_profile` // option, only when it's off (see personProcessingOptions). A registered or caller - // `$process_person_profile` only fills that option when the send hoists the property and - // the option is still unset. + // `$process_person_profile` wins over it when the send hoists the property. } // Session replay should have the SDK info as well @@ -986,14 +985,17 @@ public class PostHog private constructor( /** * Adds `process_person_profile: false` when the SDK doesn't process person profiles for this - * event, unless the caller set that option. A legacy `$process_person_profile` property doesn't - * prevent it: the send hoists that property only into an unset option. Nothing is added - * otherwise: PostHog processes person profiles by default. Returns a copy, so `beforeSend` - * edits never reach the caller's map. + * event, unless the caller set that option or [properties] carry a non-null legacy + * `$process_person_profile` property (per call or registered). Nothing is added otherwise: + * PostHog processes person profiles by default. Returns a copy, so `beforeSend` edits never + * reach the caller's map. */ - private fun personProcessingOptions(options: Map?): MutableMap? { + private fun personProcessingOptions( + options: Map?, + properties: Map, + ): MutableMap? { val eventOptions = options?.toMutableMap() - if (hasPersonProcessing()) { + if (hasPersonProcessing() || properties["\$process_person_profile"] != null) { return eventOptions } val result = eventOptions ?: mutableMapOf() @@ -1192,7 +1194,7 @@ public class PostHog private constructor( // Snapshots don't carry the SDK's person-processing option. val eventOptions = - if (isSnapshotEvent) options?.toMutableMap() else personProcessingOptions(options) + if (isSnapshotEvent) options?.toMutableMap() else personProcessingOptions(options, mergedProperties) // This capture's identity, kept through beforeSend. val uuid = TimeBasedEpochGenerator.generate() diff --git a/posthog/src/test/java/com/posthog/PostHogCaptureOptionsTest.kt b/posthog/src/test/java/com/posthog/PostHogCaptureOptionsTest.kt index 5d52e754e..76e5045b0 100644 --- a/posthog/src/test/java/com/posthog/PostHogCaptureOptionsTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogCaptureOptionsTest.kt @@ -218,17 +218,15 @@ internal class PostHogCaptureOptionsTest { } @Test - fun `a registered legacy person processing property doesn't override the personless option`() { - val http = httpServers.mockHttp() - val sut = getSut(http.url("/").toString(), personProfiles = PersonProfiles.NEVER, beforeSend = { it }) + fun `a registered legacy person processing property suppresses the personless option`() { + val sut = getSut(personProfiles = PersonProfiles.NEVER) sut.register("\$process_person_profile", true) sut.capture("test_event") - queueExecutor.shutdownAndAwaitTermination() - val event = sentEvents(http).single() - assertEquals?>(mapOf("process_person_profile" to false), event.options) - assertFalse(event.properties!!.containsKey("\$process_person_profile")) + val event = captured.single() + assertNull(event.options) + assertEquals(true, event.properties!!["\$process_person_profile"]) sut.close() } diff --git a/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt b/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt index 963b8a067..193ee7860 100644 --- a/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogPersonProfilesTest.kt @@ -119,7 +119,7 @@ internal class PostHogPersonProfilesTest { } @Test - fun `caller person processing properties don't override the personless option if identified only and not identified`() { + fun `caller person processing properties win if identified only and not identified`() { val http = mockHttp() val url = http.url("/") @@ -138,8 +138,8 @@ internal class PostHogPersonProfilesTest { val event = batch.batch.first() - // the SDK's personless option wins; the caller's legacy property is removed when sent - assertEquals(false, event.processPersonProfile) + // the caller's legacy property is hoisted into the option when the event is sent + assertEquals(true, event.processPersonProfile) assertFalse(event.properties!!.containsKey("\$process_person_profile")) assertEquals(true, event.properties!!["\$is_identified"] as Boolean) @@ -424,7 +424,7 @@ internal class PostHogPersonProfilesTest { } @Test - fun `caller process person property doesn't override never`() { + fun `caller process person override wins if never`() { val http = mockHttp() val url = http.url("/") @@ -440,7 +440,7 @@ internal class PostHogPersonProfilesTest { val event = batch.batch.first() - assertEquals(false, event.processPersonProfile) + assertEquals(true, event.processPersonProfile) assertFalse(event.properties!!.containsKey("\$process_person_profile")) sut.close()