fix(compliance): add /get_feature_flag endpoint to the test adapter#542
fix(compliance): add /get_feature_flag endpoint to the test adapter#542turnipdabeets wants to merge 7 commits into
Conversation
The harness was getting a 404 on /get_feature_flag, failing all 16 feature-flag tests. Implements the endpoint (mirrors posthog-ios): set person/group properties, reload flags, resolve the value, flush. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Satisfies the semgrep package-manager rules: add a 7-day cooldown to the Dependabot github-actions ecosystem and minimumReleaseAge: 10080 to the pnpm workspace, so newly published packages wait before being adopted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
posthog-android Compliance ReportDate: 2026-06-02 23:27:44 UTC
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 2345ms |
| Format Validation.Event Has Uuid | ✅ | 2023ms |
| Format Validation.Event Has Lib Properties | ✅ | 2025ms |
| Format Validation.Distinct Id Is String | ✅ | 2025ms |
| Format Validation.Token Is Present | ✅ | 2021ms |
| Format Validation.Custom Properties Preserved | ✅ | 2022ms |
| Format Validation.Event Has Timestamp | ✅ | 2022ms |
| Retry Behavior.Retries On 503 | ✅ | 7025ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 4022ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 4025ms |
| Retry Behavior.Respects Retry After Header | ✅ | 7025ms |
| Retry Behavior.Implements Backoff | ✅ | 17033ms |
| Retry Behavior.Retries On 500 | ✅ | 7024ms |
| Retry Behavior.Retries On 502 | ✅ | 7025ms |
| Retry Behavior.Retries On 504 | ✅ | 7027ms |
| Retry Behavior.Max Retries Respected | ✅ | 17021ms |
| Deduplication.Generates Unique Uuids | ✅ | 2038ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 7023ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 12031ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 7028ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 2032ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 2026ms |
| Compression.Sends Gzip When Enabled | ✅ | 2022ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 2018ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 2013ms |
| Batch Format.Multiple Events Batched Together | ✅ | 2029ms |
| Error Handling.Does Not Retry On 403 | ✅ | 4020ms |
| Error Handling.Does Not Retry On 413 | ❌ | 4019ms |
| Error Handling.Retries On 408 | ✅ | 7025ms |
Failures
error_handling.does_not_retry_on_413
Expected 1 requests, got 2
Feature_Flags Tests
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ❌ | 2028ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 2017ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 2015ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 2017ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 2017ms |
| Request Payload.Groups Round Trip | ✅ | 2021ms |
| Request Payload.Groups Default To Empty Object | ❌ | 2017ms |
| Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It | ❌ | 2029ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ❌ | 2015ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ❌ | 2018ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ❌ | 2017ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 8ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 2016ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 4024ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 2016ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 4022ms |
Failures
request_payload.request_with_person_properties_device_id
Expected distinct_id='test_user_123', got '019e8aa9-9556-71ed-83bf-6d6f7b24b4b2'
request_payload.groups_default_to_empty_object
Field 'groups' not found in /flags request body at path 'groups'. Available keys: ['$anon_distinct_id', '$device_id', 'api_key', 'distinct_id', 'timezone', 'person_properties']
request_payload.person_properties_distinct_id_auto_populated_when_caller_omits_it
Field 'distinct_id' not found in /flags request body at path 'person_properties.distinct_id'. Available keys: ['$lib', '$lib_version', 'email']
request_payload.disable_geoip_false_propagates_as_geoip_disable_false
Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['$anon_distinct_id', '$device_id', 'api_key', 'distinct_id', 'timezone', 'person_properties']
request_payload.disable_geoip_omitted_defaults_to_false
Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['$anon_distinct_id', '$device_id', 'api_key', 'distinct_id', 'timezone', 'person_properties']
request_payload.flag_keys_to_evaluate_contains_only_requested_key
Field 'flag_keys_to_evaluate' not found in /flags request body at path 'flag_keys_to_evaluate'. Available keys: ['$anon_distinct_id', '$device_id', 'api_key', 'distinct_id', 'timezone', 'person_properties']
…n init Declare capabilities ["capture_v0", "encoding_gzip"] in /health so the harness runs the capture suites (android posts events to /batch and gzips them). Also set remoteConfig = false in /init so tests don't depend on the remote config load. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt:352-354
**`ph.group()` fires an extra `$groupidentify` event and may trigger a second `/flags` request**
`PostHog.group()` calls `groupStateless()` internally, which calls `captureStateless(GROUP_IDENTIFY, ...)`, queuing a `$groupidentify` event. More critically, because `PostHog.reloadFeatureFlags: Boolean = true` by default, it also calls `reloadFeatureFlags(config?.onFeatureFlags)` whenever the group key is new. Since `/reset` clears persisted groups between tests, every test run sees a fresh group — making `reloadFeatureFlagsIfNewGroup` true every time. This means two `/flags` requests get made (one from `group()`, one from the explicit `reloadFeatureFlags { latch.countDown() }` below), and one `$groupidentify` batch request is created before the latch is even started. If the harness asserts exact batch or `/flags` request counts for group flag tests, those tests will remain red despite this fix.
Reviews (1): Last reviewed commit: "feat(compliance): declare capture capabi..." | Re-trigger Greptile |
| req.groups?.forEach { (type, key) -> | ||
| ph.group(type, key, req.group_properties?.get(type)) | ||
| } |
There was a problem hiding this comment.
ph.group() fires an extra $groupidentify event and may trigger a second /flags request
PostHog.group() calls groupStateless() internally, which calls captureStateless(GROUP_IDENTIFY, ...), queuing a $groupidentify event. More critically, because PostHog.reloadFeatureFlags: Boolean = true by default, it also calls reloadFeatureFlags(config?.onFeatureFlags) whenever the group key is new. Since /reset clears persisted groups between tests, every test run sees a fresh group — making reloadFeatureFlagsIfNewGroup true every time. This means two /flags requests get made (one from group(), one from the explicit reloadFeatureFlags { latch.countDown() } below), and one $groupidentify batch request is created before the latch is even started. If the harness asserts exact batch or /flags request counts for group flag tests, those tests will remain red despite this fix.
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt
Line: 352-354
Comment:
**`ph.group()` fires an extra `$groupidentify` event and may trigger a second `/flags` request**
`PostHog.group()` calls `groupStateless()` internally, which calls `captureStateless(GROUP_IDENTIFY, ...)`, queuing a `$groupidentify` event. More critically, because `PostHog.reloadFeatureFlags: Boolean = true` by default, it also calls `reloadFeatureFlags(config?.onFeatureFlags)` whenever the group key is new. Since `/reset` clears persisted groups between tests, every test run sees a fresh group — making `reloadFeatureFlagsIfNewGroup` true every time. This means two `/flags` requests get made (one from `group()`, one from the explicit `reloadFeatureFlags { latch.countDown() }` below), and one `$groupidentify` batch request is created before the latch is even started. If the harness asserts exact batch or `/flags` request counts for group flag tests, those tests will remain red despite this fix.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Accurate, thanks. There's no way around this with the public android API: group() is the only way to put a group into the /flags body, and it unconditionally emits $groupidentify and reloads flags on a new group — there's no suppression parameter (unlike setPersonPropertiesForFlags(reloadFeatureFlags = false), which the adapter does use for person properties). So the extra /flags + $groupidentify are inherent to using the public API for groups. It only affects the group-bearing flag tests (groups_round_trip), which are already red because /init fires a /flags regardless on the SDK side (every flag count is +1), so suppressing this wouldn't flip the test. Leaving the adapter on the public group() API.
There was a problem hiding this comment.
Is setGroupPropertiesForFlags not viable here?
|
|
||
| // Flush that event and wait so it can't spill into the next test's mock window. | ||
| ph.flush() | ||
| Thread.sleep(2000) |
There was a problem hiding this comment.
do we need this? Is there a better approach, like using a latch, waiting for the flush, or something?
Sleep slows down tests
There was a problem hiding this comment.
agreed, but tried this on iOS and it regressed 6 tests.
|
bsed on #542 (comment) that didnt work? |
Use setGroupPropertiesForFlags(type, props, reloadFeatureFlags = false) for group_properties (no extra reload), matching the iOS adapter pattern. group() is still used for the type->key registration since the /flags `groups` field reads from the GROUPS preference, which only group() writes — its $groupidentify event and reload-on-new-group remain (no suppression param). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| // Don't load remote config on init either — it chains into a /flags | ||
| // request, which would add a spurious request to every test's count |
There was a problem hiding this comment.
Not sure if this is the case cause even remoteConfig should only chain /flags call if preloadFeatureFlags = true. If it doesn't then I think this should be fixed?
There was a problem hiding this comment.
Fixed in 639d0cd by swapping to SDK.close() in the adapter's /reset, matching the iOS adapter.
…r-get-feature-flag # Conflicts: # pnpm-workspace.yaml
bfcf6e8 to
0f59b4e
Compare
SDK.reset() reloads flags as anon (PostHog.kt:1519 — intended behavior for
real-app logout), but that /flags lands in the next test's mock window and
inflates flag counts ("Expected 0, got 1" on no_flags_request_on_init_alone
and friends). close() has no network I/O; the next /init builds a fresh
instance. Matches the iOS adapter's pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e0fa4d5 to
639d0cd
Compare
Per review: the remoteConfig=false in /init claimed to stop a /flags reload, but PostHogRemoteConfig.kt:243 only chains to /flags when preloadFeatureFlags is true — so the config does nothing observable here. Dropping it. Also trims the capabilities and group() comments to one line each. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
For some reason I can't see this comment |
TL;DR: the compliance adapter was missing
/get_feature_flag→ harness 404s → 0/16 flag tests. This adds the endpoint (mirrors posthog-ios).