Skip to content

fix: skip the person-properties hash for maybe-personless updates and fill $lib as a pair - #1050

Merged
turnipdabeets merged 3 commits into
chore/v4-remove-deprecatedfrom
fix/v4-person-hash-and-lib-pair
Nov 10, 2026
Merged

turnipdabeets merged 3 commits into
chore/v4-remove-deprecatedfrom
fix/v4-person-hash-and-lib-pair

Conversation

@turnipdabeets

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Addresses dustinbyrne's two review comments on #873.

Person-properties hash (comment). The dedup guard in queueEvent read only options["process_person_profile"]. A registered or legacy $process_person_profile: false is still a property at that point and becomes an option only in captureV1JSON. So this sequence recorded a hash for a $set the server can't apply, then dropped the next, valid update as a duplicate:

sdk.register(["$process_person_profile": false])
sdk.setPersonProperties(userPropertiesToSet: ["plan": "pro"])
sdk.unregister("$process_person_profile")
sdk.setPersonProperties(userPropertiesToSet: ["plan": "pro"]) // dropped

main doesn't have this bug, because it overwrote $process_person_profile with the SDK's own state after the caller merge. The guard now reads the setting the way it's sent (a non-null option, then the legacy property). It records a hash only when the setting is missing or a Boolean true, so string "false" and any value the server might read as false skip the hash too. queueEvent runs after beforeSend, so a legacy false set by the hook is covered as well.

$lib / $lib_version (comment). This follows "Events name their own library" in sdk-specs #146. The SDK fills its own pair last, after the caller's properties and before beforeSend:

  • If $lib is missing or empty, the SDK sets both, replacing a lone $lib_version.
  • If $lib is the SDK's own name and $lib_version is missing, the SDK sets the version.
  • Otherwise both are left as the caller set them. Another library's $lib never gets this SDK's version.

Before this change the two keys were filled independently. Wrappers are unaffected because they set postHogSdkName, which counts as "own name".

The 4.0 changeset and MIGRATION.md (Property precedence) describe the new $lib behavior.

💚 How did you test it?

  • New tests in PostHogCaptureV1Test:
    • a maybe-personless $set doesn't suppress a later identical one: registered false, string "false" option, legacy false under a null option
    • an explicit true option beats a legacy false, so the later update is still deduplicated
    • six $lib pair cases, including the three chore: screenshot masking #146 scenarios
  • Confirmed the new tests fail without the SDK change (4 of 6 $lib cases and all 3 personless cases).
  • make test filter=PostHogCaptureV1Test passes. In full make test, everything passes except PostHogStorageTest's App Group cases. Those fail identically on the base branch on my machine, because of a permission error in ~/Library/Group Containers.
  • make buildSdkSpm and make buildSdkSpmNoTraits pass. make buildSdk couldn't run locally (xcpretty isn't installed). SwiftLint is clean on the changed lines. No public API change.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file (updated the existing 4.0 changeset)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code (Opus 5.5) checked both review comments against main and the PR head, read the #146 requirement and capture's coerce_bool, and implemented the fixes. The hash check is conservative on purpose: it doesn't copy the server's bool parsing, and it records a hash only for a missing setting or a Boolean true. The only cost when a value is ambiguous is one repeated $set.

🤖 Generated with Claude Code

… fill $lib as a pair

The hash guard read only the process_person_profile option, but a registered
or legacy $process_person_profile false is hoisted into options later, at send.
Such a $set recorded a hash it couldn't apply, so the same update sent after
unregistering was dropped as a duplicate. Resolve the setting the way it's sent
(non-null option, then the legacy property) and record a hash only when it's
missing or a Boolean true.

Fill the SDK's $lib and $lib_version as a pair after the caller's properties,
per sdk-specs #146: another library's $lib never gets this SDK's version, and a
$lib_version without a $lib is replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@turnipdabeets
turnipdabeets requested a review from a team as a code owner November 9, 2026 20:56
@turnipdabeets turnipdabeets self-assigned this Nov 9, 2026
@veria-ai

veria-ai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

PR overview

No security issues were found in this pull request.

Reviews

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 3dffbef: 1 Should fix.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Comment thread PostHog/PostHogSDK.swift Resolved
turnipdabeets and others added 2 commits November 9, 2026 16:03
… update

An update whose setting isn't a clear Boolean, such as the string "true",
skipped the hash but left the previous one in place. PostHog may still apply
it, so a later update back to the previous properties was dropped as a
duplicate. Clear the stored hash instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-11-09T22:30:54.496476+00:00
Duration: 1009125ms

✅ All Tests Passed!

234/234 tests passed


Capture_V1 Tests

✅ 106/106 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 2948ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2921ms
Required Headers.Has Authorization Bearer Header ✅ 2919ms
Required Headers.Has Content Type Json ✅ 2934ms
Required Headers.Has Posthog Sdk Info Format ✅ 2961ms
Required Headers.Has Posthog Attempt Header ✅ 2857ms
Required Headers.Has Posthog Request Id ✅ 2831ms
Required Headers.Has Posthog Request Timestamp ✅ 2828ms
Required Headers.Has User Agent ✅ 2916ms
Body Format.Body Has Created At And Batch ✅ 558ms
Body Format.No Api Key In Body ✅ 2964ms
Body Format.No Sent At In Body ✅ 3105ms
Event Format.Event Has Required Root Fields ✅ 2837ms
Event Format.Event Uuid Is Valid ✅ 2983ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2784ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2671ms
Event Format.Distinct Id Is String ✅ 2828ms
Event Format.Distinct Id At Root Not Properties ✅ 2873ms
Event Format.Custom Properties Preserved ✅ 3386ms
Event Format.Set Properties Preserved ✅ 2764ms
Event Format.Set Once Properties Preserved ✅ 2964ms
Event Format.Groups Properties Preserved ✅ 2742ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 422ms
Event Format.Event Has Required Root Fields Batch ✅ 2871ms
Event Format.Event Uuid Is Valid Batch ✅ 2825ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2909ms
Event Format.Distinct Id Is String Batch ✅ 3306ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 720ms
Event Format.Custom Properties Preserved Batch ✅ 3082ms
Event Format.Set Properties Preserved Batch ✅ 2865ms
Event Format.Set Once Properties Preserved Batch ✅ 2906ms
Event Format.Groups Properties Preserved Batch ✅ 2873ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2873ms
Batch Behavior.Multiple Events In Single Batch ✅ 2869ms
Batch Behavior.Batch Envelope Smoke ✅ 630ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 378ms
Batch Behavior.Flush At Triggers Batch ✅ 1432ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2922ms
Deduplication.Generates Unique Uuids ✅ 3015ms
Deduplication.Different Events Same Content Different Uuids ✅ 2933ms
Deduplication.Preserves Uuid On Retry ✅ 9337ms
Deduplication.Preserves Timestamp On Retry ✅ 9407ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7935ms
Deduplication.No Duplicate Events In Batch ✅ 3644ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2934ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16331ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9294ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7534ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9200ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 636ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 3207ms
Response Format Validation.Success No Retry After When All Ok ✅ 2951ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2708ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2788ms
Response Format Validation.Response Echoes Request Id ✅ 2704ms
Retry Behavior.Retries On 408 ✅ 9296ms
Retry Behavior.Retries On 500 ✅ 8725ms
Retry Behavior.Retries On 503 ✅ 11766ms
Retry Behavior.Retries On 504 ✅ 9288ms
Retry Behavior.Retries On 404 ✅ 8748ms
Retry Behavior.Retries On 429 ✅ 9249ms
Retry Behavior.Retries On 502 ✅ 9250ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6261ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10740ms
Retry Behavior.Does Not Retry On 400 ✅ 4782ms
Retry Behavior.Does Not Retry On 401 ✅ 4742ms
Retry Behavior.Does Not Retry On 402 ✅ 4682ms
Retry Behavior.Does Not Retry On 413 ✅ 4670ms
Retry Behavior.Does Not Retry On 415 ✅ 4757ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4638ms
Retry Behavior.Implements Backoff ✅ 18153ms
Partial Batch Handling.Handles 200 Full Success ✅ 2324ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5910ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5917ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 5947ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7857ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7874ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 8007ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 8240ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7791ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7967ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7827ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5765ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 5886ms
Compression.Sends Gzip Content Encoding ✅ 2818ms
Compression.No Content Encoding When Disabled ✅ 509ms
Compression.Compressed Body Is Decompressible ✅ 2784ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4836ms
Event Options.Cookieless Mode Override ✅ 2878ms
Event Options.Disable Skew Correction Override ✅ 456ms
Event Options.Process Person Profile Override ✅ 2833ms
Event Options.Product Tour Id Override ✅ 373ms
Event Options.Unset Options Omitted ✅ 2861ms
Event Options.Options Override In Batch ✅ 2797ms
Event Options.Unknown Option Passes Through ✅ 2902ms
Event Options.Option Value Is Not Converted ✅ 2831ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2857ms
Event Options.Option Wins Over Legacy Property ✅ 2878ms
Event Options.Legacy Property Fills Unset Option ✅ 2784ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2836ms
Property Layers.Caller Property Beats Registered Property ✅ 381ms
Property Layers.Before Send Runs Last ✅ 2874ms
Fail Open.Empty Api Key Sends Nothing ✅ 1260ms
Fail Open.Personal Api Key Sends Nothing ✅ 1302ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2916ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2912ms

Capture_Ai_V1 Tests

✅ 111/111 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 2899ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2820ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 2883ms
Endpoint And Method.Capture Ai Does Not Reroute Non Ai Events ✅ 2750ms
Lane Isolation.Failing Ai Lane Does Not Delay Analytics ✅ 11246ms
Required Headers.Has Authorization Bearer Header ✅ 2750ms
Required Headers.Has Content Type Json ✅ 2843ms
Required Headers.Has Posthog Sdk Info Format ✅ 2867ms
Required Headers.Has Posthog Attempt Header ✅ 2799ms
Required Headers.Has Posthog Request Id ✅ 2759ms
Required Headers.Has Posthog Request Timestamp ✅ 2873ms
Required Headers.Has User Agent ✅ 2859ms
Body Format.Body Has Created At And Batch ✅ 416ms
Body Format.No Api Key In Body ✅ 2802ms
Body Format.No Sent At In Body ✅ 415ms
Event Format.Event Has Required Root Fields ✅ 2781ms
Event Format.Event Uuid Is Valid ✅ 2834ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2740ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2769ms
Event Format.Distinct Id Is String ✅ 492ms
Event Format.Distinct Id At Root Not Properties ✅ 2823ms
Event Format.Custom Properties Preserved ✅ 2885ms
Event Format.Set Properties Preserved ✅ 2782ms
Event Format.Set Once Properties Preserved ✅ 2734ms
Event Format.Groups Properties Preserved ✅ 2796ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2766ms
Event Format.Event Has Required Root Fields Batch ✅ 2927ms
Event Format.Event Uuid Is Valid Batch ✅ 2817ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2843ms
Event Format.Distinct Id Is String Batch ✅ 2793ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2792ms
Event Format.Custom Properties Preserved Batch ✅ 2787ms
Event Format.Set Properties Preserved Batch ✅ 396ms
Event Format.Set Once Properties Preserved Batch ✅ 2854ms
Event Format.Groups Properties Preserved Batch ✅ 2876ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2863ms
Batch Behavior.Multiple Events In Single Batch ✅ 2968ms
Batch Behavior.Batch Envelope Smoke ✅ 2917ms
Batch Behavior.Flush At Triggers Batch ✅ 1176ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2829ms
Deduplication.Generates Unique Uuids ✅ 3121ms
Deduplication.Different Events Same Content Different Uuids ✅ 2787ms
Deduplication.Preserves Uuid On Retry ✅ 5410ms
Deduplication.Preserves Timestamp On Retry ✅ 5409ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7934ms
Deduplication.No Duplicate Events In Batch ✅ 2958ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2880ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16759ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9200ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7559ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9237ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2894ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2969ms
Response Format Validation.Success No Retry After When All Ok ✅ 2930ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2972ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2997ms
Response Format Validation.Response Echoes Request Id ✅ 465ms
Retry Behavior.Retries On 408 ✅ 5468ms
Retry Behavior.Retries On 500 ✅ 9285ms
Retry Behavior.Retries On 503 ✅ 11338ms
Retry Behavior.Retries On 504 ✅ 9316ms
Retry Behavior.Retries On 404 ✅ 9314ms
Retry Behavior.Retries On 429 ✅ 9154ms
Retry Behavior.Retries On 502 ✅ 9307ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6368ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 8366ms
Retry Behavior.Does Not Retry On 400 ✅ 4834ms
Retry Behavior.Does Not Retry On 401 ✅ 4899ms
Retry Behavior.Does Not Retry On 402 ✅ 4867ms
Retry Behavior.Does Not Retry On 413 ✅ 4782ms
Retry Behavior.Does Not Retry On 415 ✅ 4756ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4816ms
Retry Behavior.Implements Backoff ✅ 18922ms
Partial Batch Handling.Handles 200 Full Success ✅ 4878ms
Partial Batch Handling.Handles 200 With All Ok ✅ 6032ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5815ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 5961ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7894ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7899ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 8000ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7880ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7881ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7899ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7736ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5752ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 5837ms
Compression.Sends Gzip Content Encoding ✅ 2803ms
Compression.No Content Encoding When Disabled ✅ 2831ms
Compression.Compressed Body Is Decompressible ✅ 2807ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4895ms
Event Options.Cookieless Mode Override ✅ 2747ms
Event Options.Disable Skew Correction Override ✅ 2879ms
Event Options.Process Person Profile Override ✅ 2787ms
Event Options.Product Tour Id Override ✅ 2897ms
Event Options.Unset Options Omitted ✅ 2833ms
Event Options.Options Override In Batch ✅ 2860ms
Event Options.Unknown Option Passes Through ✅ 2772ms
Event Options.Option Value Is Not Converted ✅ 2904ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2796ms
Event Options.Option Wins Over Legacy Property ✅ 2851ms
Event Options.Legacy Property Fills Unset Option ✅ 2867ms
Event Options.Null Option Falls Back To Legacy Property ✅ 445ms
Event Size.Event Over Ceiling Is Not Sent ✅ 7905ms
Event Size.Over Ceiling Drop Is Reported Or Logged Without Content ✅ 2910ms
Event Size.Batches Close Before Byte Target ✅ 8921ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2987ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2926ms
Property Layers.Caller Property Beats Registered Property ✅ 2931ms
Property Layers.Before Send Runs Last ✅ 3011ms
Fail Open.Empty Api Key Sends Nothing ✅ 1317ms
Fail Open.Personal Api Key Sends Nothing ✅ 1384ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2937ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2897ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2913ms
Request Payload.Flags Request Omits Authorization Header ✅ 2857ms
Request Payload.Token In Flags Body Matches Init ✅ 2969ms
Request Payload.Groups Round Trip ✅ 2934ms
Request Payload.Groups Default To Empty Object ✅ 2950ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2982ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 3067ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2992ms
Request Lifecycle.No Flags Request On Init Alone ✅ 141ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2902ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5672ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2894ms
Retry Behavior.Retries Flags On 502 ✅ 2811ms
Retry Behavior.Retries Flags On 504 ✅ 2935ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 3002ms

@turnipdabeets
turnipdabeets merged commit 0a5e765 into chore/v4-remove-deprecated Nov 10, 2026
42 of 46 checks passed
@turnipdabeets
turnipdabeets deleted the fix/v4-person-hash-and-lib-pair branch November 10, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants