Skip to content

fix: drop group_identify() calls with a missing group type or key (sdk-specs group-identify) - #835

Draft
posthog[bot] wants to merge 1 commit into
mainfrom
posthog-code/group-identify-validate-identity
Draft

fix: drop group_identify() calls with a missing group type or key (sdk-specs group-identify)#835
posthog[bot] wants to merge 1 commit into
mainfrom
posthog-code/group-identify-validate-identity

Conversation

@posthog

@posthog posthog Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Compliance gap against the cross-SDK contract in PostHog/sdk-specs.

openspec/specs/group-identify/spec.md — Requirement "Canonical group-identify behavior", Scenario "Group identify requires type and key (@both)":

WHEN group identify is called without a group key
THEN no event named $groupidentify should be enqueued
AND the SDK should record a validation warning

Mirrored in acceptance/public/group-identify.feature (@both), and restated as Behavior step 1 of the same spec: "Validate group identity. groupType and groupKey must be present and non-empty."

What was out of compliance. Client.group_identify() went straight from properties = properties or {} to building the $groupidentify message — neither argument was checked. So all of these enqueued an event:

posthog.group_identify("company", None)   # {"$group_type": "company", "$group_key": null}
posthog.group_identify("company", "")     # {"$group_type": "company", "$group_key": ""}
posthog.group_identify("", "acme")        # {"$group_type": "",        "$group_key": "acme"}

Each returns a UUID and logs nothing. A $groupidentify with a null or empty group type/key cannot address a group profile, so the event is unusable on arrival — it just consumes ingestion quota and shows up as a phantom group in the project.

How this fixes it. Both arguments are validated up front; a missing or empty value is dropped with a log.warning and None is returned. This is the same shape as the alias() validation merged in #831stringify_id(...) for the emptiness test so a legitimately falsy non-string key such as 0 is not mistaken for "missing", and one warning naming the specific argument that was absent.

Behavior change / compatibility risk: low, but not zero. Calls that previously enqueued an unusable event now return None and enqueue nothing. Anyone whose code path silently passed an empty group key will see those events stop — which is the point, but it is an observable drop in event volume for that path, and a caller asserting on a non-None return would start failing. No public signature, config, or property-name changes.

Deliberately not included: unlike alias(), this does not normalize a non-string group_key to a string on the wire. $group_key is part of the group's identity server-side, so silently turning 5 into "5" could split an existing group's profile — a much larger blast radius than the compliance gap being fixed here. The value is validated and passed through as-is; a test pins that.

💚 How did you test it?

Added five cases in posthog/test/test_client.py, following the conventions of the existing alias validation tests:

  • test_group_identify_without_group_type_is_dropped, parameterized over None and "" — asserts no HTTP post, None return, and that the warning names group_type.
  • test_group_identify_without_group_key_is_dropped, same two parameters, warning names group_key.
  • test_group_identify_accepts_falsy_non_string_group_keygroup_key=0 is still enqueued and reaches the wire as 0, not "0".

Ran posthog/test/test_client.py, test_module.py, and test_contexts.py (200 passed), plus ruff format --check, ruff check, mypy posthog/client.py, and .github/scripts/check_public_api.py (snapshot unchanged). No manual or integration testing against a live PostHog instance.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed. — docstrings on both the client method and the module-level wrapper now state the requirement.
  • No breaking change or entry added to the changelog. — behavior change, see above; changeset added at .sampo/changesets/group-identify-validates-group-identity.md.

If releasing new changes

🤖 Agent context

Autonomy: Fully autonomous

Opened by the scheduled SDK Spec Compliance Enforcer loop for posthog-python, running in PostHog Code (Claude Code harness). Each run reads PostHog/sdk-specs as the source of truth, audits the Python SDK against the contracts whose Applicability is both or server, and opens one focused draft PR per confirmed divergence.

This run swept the ~20 in-scope specs across five parallel read-only audit agents (capture/identify/alias/group-identify/before-send; the six flag getters; batcher/retry/http/flush/shutdown; local evaluation + definition loader + flag-called tracker; tracing-headers/bootstrap/logs/traces). logs and traces are simply not implemented in this SDK — a capability not yet ported rather than a violation — and bootstrap is a client-only concept.

This finding was chosen over the alternatives because the spec states it as an explicit @both acceptance scenario rather than prose, the fix has direct precedent in #831 that the team already accepted, and it carries the least backward-compatibility risk of the confirmed candidates. Runners-up left for human judgement, in rough priority order:

  • request.py:265 treats only HTTP 200 as success, so a 202/204 from an ingestion proxy becomes an APIError and (being classified retryable) causes duplicate batch delivery. The SDK's own v1 path already does 200 <= status < 300. One-line fix, but it changes error handling on the shared /batch/ + /flags/ path.
  • capture_v1.py:87 lists 429 as terminal, so a rate-limited batch is dropped on first response when capture_mode="v1"; the retry-queue spec names 429 as retryable and the default v0 lane already retries it. Fixing it makes shutdown()'s unbounded flush block through the backoff schedule under sustained rate limiting.
  • json.loads on flag payloads in types.py:234/:275 is unguarded, so a non-JSON payload string raises JSONDecodeError into caller code from get_feature_flag_payload / get_feature_flag_result. The tolerant _parse_flag_payload helper already exists but is only wired into the evaluate_flags() path.
  • get_all_flags_and_payloads() returns payloads as raw JSON strings while every other payload surface returns parsed values. Spec-supported, but it is a public return-type change.
  • A group flag evaluated with no group context returns False locally instead of signalling inconclusive, so the /flags fallback the spec asks for never happens. The current behavior is a deliberate, commented cost optimization — this one probably wants resolving in the spec, not the SDK.

Agent-authored, so no human co-author is claimed, and it needs human review before merge.


Created with PostHog Code

…k-specs group-identify)

The sdk-specs `group-identify` contract has an explicit `@both` scenario -
"Group identify requires type and key" - requiring that a call without a group
key enqueue no `$groupidentify` event and record a validation warning.
Behavior step 1 says `groupType` and `groupKey` "must be present and non-empty".

`Client.group_identify()` performed no validation, so `group_identify("company",
None)` (or an empty string for either argument) enqueued a `$groupidentify`
event carrying a null/empty `$group_type` or `$group_key`, which cannot address
a group profile.

Both arguments are now validated up front and the call is dropped with a
warning, mirroring the `alias()` validation merged in #831. Valid values -
including non-string group keys - are passed through to the wire unchanged.

Generated-By: PostHog Code
Task-Id: 24b9fcad-a8a5-4f09-83e8-cd0340ba0440
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

posthog-python Compliance Report

Date: 2026-08-06 07:55:24 UTC
Duration: 256450ms

✅ All Tests Passed!

111/111 tests passed


Capture_V1 Tests

94/94 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint 518ms
Endpoint And Method.Does Not Use Legacy Endpoints 511ms
Required Headers.Has Authorization Bearer Header 511ms
Required Headers.Has Content Type Json 510ms
Required Headers.Has Posthog Sdk Info Format 511ms
Required Headers.Has Posthog Attempt Header 511ms
Required Headers.Has Posthog Request Id 511ms
Required Headers.Has Posthog Request Timestamp 510ms
Required Headers.Has User Agent 511ms
Body Format.Body Has Created At And Batch 511ms
Body Format.No Api Key In Body 510ms
Body Format.No Sent At In Body 510ms
Event Format.Event Has Required Root Fields 510ms
Event Format.Event Uuid Is Valid 511ms
Event Format.Event Timestamp Is Rfc3339 511ms
Event Format.Distinct Id Is String 511ms
Event Format.Distinct Id At Root Not Properties 511ms
Event Format.Custom Properties Preserved 511ms
Event Format.Set Properties Preserved 511ms
Event Format.Set Once Properties Preserved 511ms
Event Format.Groups Properties Preserved 512ms
Event Format.Sdk Generates Uuid If Not Provided 510ms
Event Format.Event Has Required Root Fields Batch 514ms
Event Format.Event Uuid Is Valid Batch 514ms
Event Format.Event Timestamp Is Rfc3339 Batch 513ms
Event Format.Distinct Id Is String Batch 514ms
Event Format.Distinct Id At Root Not Properties Batch 514ms
Event Format.Custom Properties Preserved Batch 514ms
Event Format.Set Properties Preserved Batch 515ms
Event Format.Set Once Properties Preserved Batch 513ms
Event Format.Groups Properties Preserved Batch 515ms
Event Format.Sdk Generates Uuid If Not Provided Batch 515ms
Batch Behavior.Multiple Events In Single Batch 520ms
Batch Behavior.Batch Envelope Smoke 516ms
Batch Behavior.Flush With No Events Sends Nothing 507ms
Batch Behavior.Flush At Triggers Batch 1011ms
Batch Behavior.Created At Reflects Batch Creation Time 510ms
Deduplication.Generates Unique Uuids 519ms
Deduplication.Different Events Same Content Different Uuids 512ms
Deduplication.Preserves Uuid On Retry 6517ms
Deduplication.Preserves Timestamp On Retry 6521ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 6522ms
Deduplication.No Duplicate Events In Batch 521ms
Header Behavior On Retry.Attempt Header Starts At One 510ms
Header Behavior On Retry.Attempt Header Increments On Retry 13522ms
Header Behavior On Retry.Request Id Preserved On Retry 6516ms
Header Behavior On Retry.Different Requests Have Different Request Ids 3021ms
Header Behavior On Retry.Request Timestamp Changes On Retry 6521ms
Response Format Validation.Success Response Has Uuid Keyed Results 512ms
Response Format Validation.Success Response Has Ok For Each Event 515ms
Response Format Validation.Success No Retry After When All Ok 512ms
Response Format Validation.Success Retry After Present When Retry Events 1517ms
Response Format Validation.Success No Retry After When Drop Only 514ms
Response Format Validation.Response Echoes Request Id 510ms
Retry Behavior.Retries On 408 6516ms
Retry Behavior.Retries On 500 6518ms
Retry Behavior.Retries On 503 8525ms
Retry Behavior.Retries On 504 6520ms
Retry Behavior.Retryable Errors Have Retry After 3518ms
Retry Behavior.Respects Retry After On Retryable Error 11523ms
Retry Behavior.Does Not Retry On 400 2517ms
Retry Behavior.Does Not Retry On 401 2515ms
Retry Behavior.Does Not Retry On 402 2512ms
Retry Behavior.Does Not Retry On 413 2514ms
Retry Behavior.Does Not Retry On 415 2512ms
Retry Behavior.Non Retryable Errors Have No Retry After 2514ms
Retry Behavior.Implements Backoff 22537ms
Retry Behavior.Max Retries Respected 22525ms
Partial Batch Handling.Handles 200 Full Success 2511ms
Partial Batch Handling.Handles 200 With All Ok 3517ms
Partial Batch Handling.Does Not Retry Dropped Events 3514ms
Partial Batch Handling.Does Not Retry Limited Events 3516ms
Partial Batch Handling.Prunes Ok Events On Partial Retry 6520ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry 6521ms
Partial Batch Handling.Retries Only Retry Events From Partial 6524ms
Partial Batch Handling.Partial Retry Preserves Uuids 6522ms
Partial Batch Handling.Partial Retry Attempt Header Increments 6517ms
Partial Batch Handling.Partial Retry Request Id Preserved 6521ms
Partial Batch Handling.Respects Retry After On Partial 8522ms
Partial Batch Handling.Unknown Result Treated As Terminal 3516ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry 3520ms
Compression.Sends Gzip Content Encoding 511ms
Compression.No Content Encoding When Disabled 510ms
Compression.Compressed Body Is Decompressible 510ms
Error Handling.Does Not Retry On Unknown 4Xx 2513ms
Event Options.Cookieless Mode Override 511ms
Event Options.Disable Skew Correction Override 510ms
Event Options.Process Person Profile Override 511ms
Event Options.Product Tour Id Override 511ms
Event Options.Unset Options Omitted 510ms
Event Options.Options Override In Batch 513ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties 510ms
Geoip And Historical Migration.Historical Migration Set In Body 510ms
Geoip And Historical Migration.Historical Migration Absent By Default 510ms

Feature_Flags Tests

17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 11ms
Request Payload.Flags Request Uses V2 Query Param 9ms
Request Payload.Flags Request Hits Flags Path Not Decide 10ms
Request Payload.Flags Request Omits Authorization Header 11ms
Request Payload.Token In Flags Body Matches Init 9ms
Request Payload.Groups Round Trip 9ms
Request Payload.Groups Default To Empty Object 10ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 9ms
Request Payload.Disable Geoip Omitted Defaults To False 9ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 9ms
Request Lifecycle.No Flags Request On Init Alone 4ms
Request Lifecycle.No Flags Request On Normal Capture 509ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 14ms
Request Lifecycle.Mock Response Value Is Returned To Caller 9ms
Retry Behavior.Retries Flags On 502 313ms
Retry Behavior.Retries Flags On 504 314ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 512ms

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.

0 participants