Skip to content

fix(flags): read an empty variant as disabled in getFeatureFlagResult and onFeatureFlags - #1037

Merged
turnipdabeets merged 3 commits into
chore/v4-remove-deprecatedfrom
fix/v4-empty-variant-disabled
Nov 9, 2026
Merged

turnipdabeets merged 3 commits into
chore/v4-remove-deprecatedfrom
fix/v4-empty-variant-disabled

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

#1001 made isFeatureEnabled return false for a flag whose variant is the empty string "", but getFeatureFlagResult(_:).enabled stayed true and PostHogFeatureFlagsLoaded still listed the flag, so the same flag read as enabled and disabled depending on the call (review thread). This makes "" read as disabled everywhere, matching posthog-js (browser), which is already consistent.

  • getFeatureFlagResult(_:) and getAllFeatureFlags() return enabled: false, with variant still "".
  • PostHogFeatureFlagsLoaded leaves the flag out of flags and variants.
  • Unchanged: isFeatureEnabled (already false, with or without defaultValue), getFeatureFlag (still returns ""), and the $feature_flag_called event (still reports $feature_flag_response: "").

Blast radius is small: since PostHog/posthog#118803 (2026-10-16) PostHog rejects new "" variant keys, so only flags created earlier through the API, MCP or Terraform can hit this.

The changeset's "Fix" line becomes a Breaking entry covering all three APIs, and MIGRATION.md lists them and notes that variant still returns "".

  • Captured events leave the flag out of $active_feature_flags; $feature/<key> still carries the raw "", as in posthog-js (browser), whose filterActiveFeatureFlags skips falsy values while $feature/<key> copies the raw value.

Parity with other SDKs

Target for every SDK: a "" variant gives isFeatureEnabled → false, getFeatureFlagResult().enabled → false, variant → "", and it's left out of the flags-loaded list.

SDK Before After
posthog-js (browser) false everywhere unchanged, already the target
posthog-ios 4.0 isFeatureEnabled false (#1001), .enabled true, listed in flags-loaded target (this PR)
posthog-android 4.0 same split as iOS target (PostHog/posthog-android#949)
posthog-flutter v6 reads native enabled, so isFeatureEnabled(defaultValue:) returned true on iOS/Android target once iOS/Android land; docs in PostHog/posthog-flutter#711
posthog-kmp 0.11 Apple true, Android/JVM false, JS/Wasm .enabled true target on every target (PostHog/posthog-kmp#108, 481ea99)
posthog-js core / React Native, posthog-node isFeatureEnabled false, .enabled true not changed yet (follow-up)
posthog-python true everywhere not changed yet (follow-up)
sdk-specs contradicted itself (client "non-empty variant" vs server "any variant") adds the "" rule in PostHog/sdk-specs#193

So after this PR and its siblings, every mobile SDK matches posthog-js (browser) and the spec. The JS core and the server SDKs are the remaining gap.

💚 How did you test it?

  • Extended PostHogSDKTest "an empty variant reads as disabled" (with and without defaultValue) to assert getFeatureFlagResult(_:).enabled == false, variant == "", and that the reload callback's PostHogFeatureFlagsLoaded leaves the flag out.
  • Ran it against the pre-fix source: it fails on enabled, flags and variants; passes with the fix.
  • Added PostHogSDKTest "captured events leave a flag with an empty variant out of $active_feature_flags": fails before the fix (["empty-variant", "on"]), passes after. swift test --filter "PostHogSDKTests|PostHogFeatureFlags|PostHogEventSnapshot": 189 tests in 26 suites passed.
  • swift test --no-parallel -Xswiftc -DTESTING --filter "emptyVariantIsDisabled|PostHogFeatureFlagsTest|FeatureFlagsLoaded": 83 tests in 15 suites passed.

📝 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 (edited the existing 4.0 changeset)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code made the change across iOS, Android, Flutter and sdk-specs at Anna's direction. The fix is one condition in the result builder and one in PostHogFeatureFlagsLoaded; isFeatureEnabled was left as is since it already returns false.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-11-08T19:47:36.879920+00:00
Duration: 994059ms

✅ 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 ✅ 2875ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2800ms
Required Headers.Has Authorization Bearer Header ✅ 2788ms
Required Headers.Has Content Type Json ✅ 443ms
Required Headers.Has Posthog Sdk Info Format ✅ 2771ms
Required Headers.Has Posthog Attempt Header ✅ 2733ms
Required Headers.Has Posthog Request Id ✅ 2833ms
Required Headers.Has Posthog Request Timestamp ✅ 432ms
Required Headers.Has User Agent ✅ 2768ms
Body Format.Body Has Created At And Batch ✅ 2788ms
Body Format.No Api Key In Body ✅ 2750ms
Body Format.No Sent At In Body ✅ 2759ms
Event Format.Event Has Required Root Fields ✅ 2741ms
Event Format.Event Uuid Is Valid ✅ 2893ms
Event Format.Event Timestamp Is Rfc3339 ✅ 313ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2794ms
Event Format.Distinct Id Is String ✅ 2748ms
Event Format.Distinct Id At Root Not Properties ✅ 2704ms
Event Format.Custom Properties Preserved ✅ 2770ms
Event Format.Set Properties Preserved ✅ 2776ms
Event Format.Set Once Properties Preserved ✅ 2752ms
Event Format.Groups Properties Preserved ✅ 2748ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2791ms
Event Format.Event Has Required Root Fields Batch ✅ 2839ms
Event Format.Event Uuid Is Valid Batch ✅ 3016ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 313ms
Event Format.Distinct Id Is String Batch ✅ 2812ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2730ms
Event Format.Custom Properties Preserved Batch ✅ 2794ms
Event Format.Set Properties Preserved Batch ✅ 2867ms
Event Format.Set Once Properties Preserved Batch ✅ 2820ms
Event Format.Groups Properties Preserved Batch ✅ 2812ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2725ms
Batch Behavior.Multiple Events In Single Batch ✅ 2850ms
Batch Behavior.Batch Envelope Smoke ✅ 2885ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 253ms
Batch Behavior.Flush At Triggers Batch ✅ 1235ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2760ms
Deduplication.Generates Unique Uuids ✅ 3270ms
Deduplication.Different Events Same Content Different Uuids ✅ 2740ms
Deduplication.Preserves Uuid On Retry ✅ 5371ms
Deduplication.Preserves Timestamp On Retry ✅ 9252ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 8025ms
Deduplication.No Duplicate Events In Batch ✅ 2863ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2803ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16304ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 8781ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7506ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9341ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 367ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2733ms
Response Format Validation.Success No Retry After When All Ok ✅ 2812ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2798ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2789ms
Response Format Validation.Response Echoes Request Id ✅ 2814ms
Retry Behavior.Retries On 408 ✅ 9192ms
Retry Behavior.Retries On 500 ✅ 9257ms
Retry Behavior.Retries On 503 ✅ 11271ms
Retry Behavior.Retries On 504 ✅ 9211ms
Retry Behavior.Retries On 404 ✅ 9263ms
Retry Behavior.Retries On 429 ✅ 9199ms
Retry Behavior.Retries On 502 ✅ 9265ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6221ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10833ms
Retry Behavior.Does Not Retry On 400 ✅ 4741ms
Retry Behavior.Does Not Retry On 401 ✅ 4716ms
Retry Behavior.Does Not Retry On 402 ✅ 4760ms
Retry Behavior.Does Not Retry On 413 ✅ 4795ms
Retry Behavior.Does Not Retry On 415 ✅ 4766ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4694ms
Retry Behavior.Implements Backoff ✅ 18251ms
Partial Batch Handling.Handles 200 Full Success ✅ 4694ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5828ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5859ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 5863ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7813ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7765ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7752ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7845ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7735ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7717ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7785ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5805ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 5751ms
Compression.Sends Gzip Content Encoding ✅ 2802ms
Compression.No Content Encoding When Disabled ✅ 2765ms
Compression.Compressed Body Is Decompressible ✅ 2799ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4787ms
Event Options.Cookieless Mode Override ✅ 2745ms
Event Options.Disable Skew Correction Override ✅ 2750ms
Event Options.Process Person Profile Override ✅ 2769ms
Event Options.Product Tour Id Override ✅ 2729ms
Event Options.Unset Options Omitted ✅ 2753ms
Event Options.Options Override In Batch ✅ 2768ms
Event Options.Unknown Option Passes Through ✅ 2765ms
Event Options.Option Value Is Not Converted ✅ 2778ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2814ms
Event Options.Option Wins Over Legacy Property ✅ 2708ms
Event Options.Legacy Property Fills Unset Option ✅ 2740ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2817ms
Property Layers.Caller Property Beats Registered Property ✅ 2667ms
Property Layers.Before Send Runs Last ✅ 2673ms
Fail Open.Empty Api Key Sends Nothing ✅ 1228ms
Fail Open.Personal Api Key Sends Nothing ✅ 1316ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2724ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2727ms

Capture_Ai_V1 Tests

✅ 111/111 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 2781ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2688ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 426ms
Endpoint And Method.Capture Ai Does Not Reroute Non Ai Events ✅ 2890ms
Lane Isolation.Failing Ai Lane Does Not Delay Analytics ✅ 5365ms
Required Headers.Has Authorization Bearer Header ✅ 2742ms
Required Headers.Has Content Type Json ✅ 2802ms
Required Headers.Has Posthog Sdk Info Format ✅ 2707ms
Required Headers.Has Posthog Attempt Header ✅ 2768ms
Required Headers.Has Posthog Request Id ✅ 2722ms
Required Headers.Has Posthog Request Timestamp ✅ 2812ms
Required Headers.Has User Agent ✅ 2688ms
Body Format.Body Has Created At And Batch ✅ 2679ms
Body Format.No Api Key In Body ✅ 2823ms
Body Format.No Sent At In Body ✅ 2669ms
Event Format.Event Has Required Root Fields ✅ 2799ms
Event Format.Event Uuid Is Valid ✅ 2740ms
Event Format.Event Timestamp Is Rfc3339 ✅ 402ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2763ms
Event Format.Distinct Id Is String ✅ 2788ms
Event Format.Distinct Id At Root Not Properties ✅ 2776ms
Event Format.Custom Properties Preserved ✅ 2705ms
Event Format.Set Properties Preserved ✅ 278ms
Event Format.Set Once Properties Preserved ✅ 290ms
Event Format.Groups Properties Preserved ✅ 2789ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2699ms
Event Format.Event Has Required Root Fields Batch ✅ 2709ms
Event Format.Event Uuid Is Valid Batch ✅ 2837ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2764ms
Event Format.Distinct Id Is String Batch ✅ 2843ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2761ms
Event Format.Custom Properties Preserved Batch ✅ 2883ms
Event Format.Set Properties Preserved Batch ✅ 2809ms
Event Format.Set Once Properties Preserved Batch ✅ 2855ms
Event Format.Groups Properties Preserved Batch ✅ 2868ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2844ms
Batch Behavior.Multiple Events In Single Batch ✅ 2873ms
Batch Behavior.Batch Envelope Smoke ✅ 2747ms
Batch Behavior.Flush At Triggers Batch ✅ 1175ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2689ms
Deduplication.Generates Unique Uuids ✅ 2928ms
Deduplication.Different Events Same Content Different Uuids ✅ 2857ms
Deduplication.Preserves Uuid On Retry ✅ 9289ms
Deduplication.Preserves Timestamp On Retry ✅ 9101ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7847ms
Deduplication.No Duplicate Events In Batch ✅ 2868ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2779ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16258ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9361ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7417ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9250ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2763ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2795ms
Response Format Validation.Success No Retry After When All Ok ✅ 2871ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2765ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2804ms
Response Format Validation.Response Echoes Request Id ✅ 2795ms
Retry Behavior.Retries On 408 ✅ 9218ms
Retry Behavior.Retries On 500 ✅ 9237ms
Retry Behavior.Retries On 503 ✅ 5296ms
Retry Behavior.Retries On 504 ✅ 5266ms
Retry Behavior.Retries On 404 ✅ 8753ms
Retry Behavior.Retries On 429 ✅ 8769ms
Retry Behavior.Retries On 502 ✅ 9201ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6332ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 11010ms
Retry Behavior.Does Not Retry On 400 ✅ 4748ms
Retry Behavior.Does Not Retry On 401 ✅ 4826ms
Retry Behavior.Does Not Retry On 402 ✅ 4861ms
Retry Behavior.Does Not Retry On 413 ✅ 4876ms
Retry Behavior.Does Not Retry On 415 ✅ 4939ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 2287ms
Retry Behavior.Implements Backoff ✅ 17724ms
Partial Batch Handling.Handles 200 Full Success ✅ 2424ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5857ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5842ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 5927ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7886ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7944ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7927ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7960ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7957ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7967ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7986ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5887ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 6146ms
Compression.Sends Gzip Content Encoding ✅ 2833ms
Compression.No Content Encoding When Disabled ✅ 2726ms
Compression.Compressed Body Is Decompressible ✅ 2906ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4815ms
Event Options.Cookieless Mode Override ✅ 2708ms
Event Options.Disable Skew Correction Override ✅ 2777ms
Event Options.Process Person Profile Override ✅ 2833ms
Event Options.Product Tour Id Override ✅ 355ms
Event Options.Unset Options Omitted ✅ 2688ms
Event Options.Options Override In Batch ✅ 2839ms
Event Options.Unknown Option Passes Through ✅ 2819ms
Event Options.Option Value Is Not Converted ✅ 483ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2919ms
Event Options.Option Wins Over Legacy Property ✅ 2866ms
Event Options.Legacy Property Fills Unset Option ✅ 2886ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2879ms
Event Size.Event Over Ceiling Is Not Sent ✅ 7240ms
Event Size.Over Ceiling Drop Is Reported Or Logged Without Content ✅ 2873ms
Event Size.Batches Close Before Byte Target ✅ 9279ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2874ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2932ms
Property Layers.Caller Property Beats Registered Property ✅ 2954ms
Property Layers.Before Send Runs Last ✅ 2996ms
Fail Open.Empty Api Key Sends Nothing ✅ 1252ms
Fail Open.Personal Api Key Sends Nothing ✅ 1467ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2853ms
Request Payload.Flags Request Uses V2 Query Param ✅ 3240ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 3353ms
Request Payload.Flags Request Omits Authorization Header ✅ 2942ms
Request Payload.Token In Flags Body Matches Init ✅ 2913ms
Request Payload.Groups Round Trip ✅ 2826ms
Request Payload.Groups Default To Empty Object ✅ 2847ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 3164ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 3109ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2887ms
Request Lifecycle.No Flags Request On Init Alone ✅ 86ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2939ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5570ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2751ms
Retry Behavior.Retries Flags On 502 ✅ 3230ms
Retry Behavior.Retries Flags On 504 ✅ 2929ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 3007ms

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good; no qualifying findings. Verdict: correct. Approved at the user's explicit request after confirming the reviewed patch is unchanged. Focused before/after regression tests, 191 relevant existing tests, lint and the SwiftPM build passed; the full cross-platform build matrix was not run.

@turnipdabeets
turnipdabeets merged commit e409d64 into chore/v4-remove-deprecated Nov 9, 2026
46 checks passed
@turnipdabeets
turnipdabeets deleted the fix/v4-empty-variant-disabled branch November 9, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants