Skip to content

docs(capture): specify capture v1 delivery for every v1 SDK - #146

Open
eli-r-ph wants to merge 50 commits into
mainfrom
eli/capture-v1-specs
Open

eli-r-ph wants to merge 50 commits into
mainfrom
eli/capture-v1-specs

Conversation

@eli-r-ph

@eli-r-ph eli-r-ph commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ DO NOT MERGE prior to v1 SDK public release! ⚠️

Problem

  • The next SDK to port to capture v1 has no contract to copy: the canonical capture, retry-queue, http-client and event-batcher specs still describe the legacy wire (/batch/, api_key in the body, $lib stamped into properties, global defaults overwriting the call, 429 retried, batches shrunk on 413).
  • posthog-python 8.0, posthog-go v2, posthog-rs 1.0 and posthog-node 6.0 already share one capture v1 contract on their v1 branches, worked out during the ports.
  • docs(capture-ai): specify the capture v1 AI lane and wrapper routing #147 (the capture v1 AI lane) was approved and squashed into this branch, so this PR lands both.

Changes

  • capture gains the capture v1 wire contract: the endpoint, the body envelope, strict field types, the required headers and which ones change on a retry, and the per-event results map with duplicates_dropped.
  • capture gains per-event options, the hoisting of the four legacy option properties after before_send, and global and context options where an SDK has global and context properties. SDK docs and examples set these settings through options.
  • capture gains the server event-building order: the call wins, context and global values fill only unset keys, SDK values fill last, before_send has the final say. This folds in the flag-property rule from docs(capture): place server feature flag properties in the merge order #145.
    • Breaking for posthog-python and posthog-go users whose global defaults used to overwrite the call; both majors already ship the new order.
  • capture gains the rules capture v1 makes observable: blank distinct ids, the $groupidentify fallback id, timestamps at millisecond precision or finer, per-event serialization, $lib/$lib_version sent in properties and stored as sent, with the PostHog-Sdk-Info header only as a fallback, fail-open on a missing or personal API key.
  • retry-queue gains the capture v1 status set (404/408/429/500/502/503/504 and network errors retried; 402/413 terminal), four attempts with 100 ms doubling waits and jitter, Retry-After as a clamped floor, and no HTTP-library retries. "429 retryable" and "shrink on 413" are marked legacy.
  • http-client gains bearer auth, the accepted Content-Encoding values, and same-origin 307/308 redirects only.
  • event-batcher gains byte-aware check-before-append batching and the end of 413 batch shrinking.
  • migration/yaml-parity-v1 mirrors the harness capture v1 suite update (posthog-sdk-test-harness#92): 404, 429 and 502 retry scenarios, and the warning result in place of limited, which capture never sends.
  • From docs(capture-ai): specify the capture v1 AI lane and wrapper routing #147: capture-ai gains the capture v1 AI lane (/i/v1/ai/events, lane isolation, misrouted_event, size rules and lane configuration), PostHog's AI wrappers always send through the AI method, and capture-ai-v1.feature / capture-ai-v1-delivery.feature add the v1 AI cases under capture_ai_v1.
  • Every new requirement is scoped to SDKs that deliver through capture v1; legacy SDKs keep the existing rules and the capture_v0* harness features are untouched.

Note

Three open PRs touch the same ground and need a decision before merge: #145 (folded in here), #140 (requires null for non-finite numbers, while posthog-go v2 drops the event locally; this change records both as allowed), #81 (gzip by default, while the v1 server SDKs default to no compression).

How did you test this code?

  • openspec validate add-capture-v1 --type change --strict passed before archiving.
  • openspec validate --specs --strict --no-interactive after archiving: 67 passed, 0 failed, the same totals as main.
  • Every requirement was checked against capture on PostHog/posthog master (rust/capture/src/v1/) and against the four SDK v1 branches, including their open pre-release PRs. Sources are listed in the archived design.md.
  • Harness docs(batching): define replay session and identity boundaries #92 branch with SDK_V2_SPECS pointing at this branch: discovery ready with no missing bindings (160 migrated cases), and the retry and outcome features pass against the harness reference hosts. The harness case-count test needs 157 → 160 when the harness bumps its specs pin.
  • Not run: the harness against a real SDK adapter. The scenarios are not opted in with @sdk:server; that needs an end-to-end run against a real adapter.

🤖 Agent context

  • Written by Claude Code (Claude Fable 5.1) with the SDK owner directing scope and decisions.
  • Skills invoked: writing-pr-descriptions.
  • The session surveyed capture-rs, the four SDK branches and the production ingress config, drafted the change with the OpenSpec CLI, archived it, and synced the narrative notes by hand.

🤖 Generated with Claude Code

@eli-r-ph eli-r-ph self-assigned this Oct 25, 2026
@eli-r-ph
eli-r-ph marked this pull request as ready for review October 25, 2026 18:24
@eli-r-ph
eli-r-ph requested a review from a team as a code owner October 25, 2026 18:24
eli-r-ph and others added 4 commits October 27, 2026 09:01
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o v2 sends it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d delta

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@eli-r-ph
eli-r-ph force-pushed the eli/capture-v1-specs branch from 6c6daa2 to 41b4023 Compare October 27, 2026 16:05
…rors

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
eli-r-ph and others added 2 commits October 27, 2026 15:24
…arning result

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread openspec/specs/retry-queue/spec.md Resolved
Comment thread openspec/specs/retry-queue/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
eli-r-ph and others added 2 commits October 28, 2026 18:18
… retries, 413 halving, request identity, wrapper names, global options, empty options)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… disabled-client error always

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

@turnipdabeets turnipdabeets 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.

A few more from checking the current text against the iOS, Android, Flutter and React Native plugin v1 branches.

[nit] Outside the diff: retry-queue (lines 19, 70, 94, 117), http-client and event-batcher still say Flutter has no Dart queue, transport or batcher of its own and delegates to native or posthog-js. That's no longer true for 6.0 on Windows and Linux.

Comment thread openspec/specs/event-batcher/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
@turnipdabeets
turnipdabeets requested a review from a team October 29, 2026 19:22
eli-r-ph and others added 6 commits October 29, 2026 15:31
…sonless default, wrappers, replay, group identify, Retry-After, timestamps and logs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep both new http-client requirements: capture v1 transport (this
branch) and request body compression and opt-out (#81).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… options

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ype, without debug on servers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e v1

- A per-call disable_geoip is a per-event value; false is sent as false.
- Capture's own 408/500/503/504 and 2xx-with-retry carry Retry-After: 1,
  so the timing scenario keeps only 404, 429 and 502.
- A server SDK may requeue events whose last attempt got no HTTP response.
- A 413 is terminal on server SDKs; client SDKs may halve.
- List the client majors in http-client; state Flutter desktop's own queue.
- Note that ingestion drops personless person-only events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 10, 2026
…52f899 and #146 d387ab7

Capture, Capture Exception and Capture AI move to Pass now that null-valued
properties are omitted (posthog-ios#1047) and the SDK fills its own $lib pair
after the caller's properties (posthog-ios#1050). On Feature Flags moves to
Pass: loads in flight across reset() are dropped (posthog-ios#1048) and empty
variants are left out of listener flags. HTTP Client is scored Pass on its
merits, since #146 now leaves a lane with its own compression setting out of
the whole-client opt-out. Surveys stays Partial only for the popup-delay
eligibility re-check (SHOULD); posthog-ios#1049 fixed the rest.

Opt In meets the new deferred-crash consent requirement (#199); the empty
variant (#193), variant key rename (#203) and job-retry (#205) changes need no
other status change. Line references move to e27cbe4cb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 10, 2026
Re-audits all 64 posthog-flutter rows at 6be9a1e9ee (the 6.0 release PR)
against sdk-specs 8306698 plus #146 at 5c26b9b, treated as merged, and adds
five ➖ rows. Mobile rows cite the posthog-ios 4.0 and posthog-android 4.0
audits. Tally 26/27/4/12 on 69 rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 10, 2026
Re-audits the posthog-flutter rows at 82123e6 against sdk-specs 752f899
plus #146 at d387ab7, with native rows checked at posthog-ios e27cbe4cb
and posthog-android aaa23d39.

On Feature Flags moves to Pass: posthog-ios now drops a flag load in
flight across reset(). Capture scores the $lib pair fill on every path,
HTTP Client and Capture AI drop the settled compression note, Surveys
keeps only posthog-android's last-seen date gap, Opt In records #199's
deferred crash consent, and Get Feature Flags notes posthog-js reading
an empty variant as enabled on web. Tally 27/26/4/12 on 69 rows.

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

@dustinbyrne dustinbyrne 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.

Approved with non-blocking comments on token-rule scope, server retry-overlap coverage, and nullable optional-ID parsing. The prior artifact corrections are addressed. The declared public-v1-release prerequisite remains separate; this approval does not clear that gate.

AI-assisted review.

Comment thread openspec/specs/capture/spec.md Resolved
Comment thread openspec/specs/capture/spec.md Resolved
eli-r-ph and others added 3 commits November 10, 2026 14:18
…with a call option

A layered process_person_profile false also reaches $set and registered
client layers. A call option true beats every layer; docs show it as the
way to keep identifying, not a per-call legacy property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 10, 2026
…on guide shows the identity call option

#146 at 15501ca requires migration guides to show a per-call
process_person_profile: true on identity calls; MIGRATION.md at e27cbe4cb
lacks it until PostHog/posthog-ios#1053 merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 10, 2026
#146 now requires migration guides to show a process_person_profile: true
call option on identity calls under a registered false. The 6.0 guide only
says those events get dropped; record the gap until posthog-flutter#718.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 10, 2026
…ter Register

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 10, 2026
…ity-call guide gap

#146 at 15501ca requires migration guides to show a process_person_profile:
true call option on identity calls. The 4.0 MIGRATION.md doesn't yet;
PostHog/posthog-android#966 adds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 11, 2026
The identity-call migration-guide example #146 requires lands in 4.0 with
PostHog/posthog-ios#1053, which targets the release branch. This audit
tracks 4.0, so a pending release-branch PR is noted rather than scored as a
gap. Reverts 85d4acd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 11, 2026
… 4.0

The identity-call migration-guide example #146 requires lands in 4.0 with
PostHog/posthog-android#966, which targets the release branch. This audit
tracks 4.0, so a pending release-branch PR is noted rather than scored as a
gap. Undoes the Capture downgrade from cfc72e1.

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

Copy link
Copy Markdown
Contributor

setup still says wire payload fields (api_key, token) "keep their names and carry the same value" (setup/spec.md:207). On capture v1 this PR sends the token only as Authorization: Bearer <token>, never in the body (capture/spec.md:697, http-client/spec.md:261), so the two disagree for v1 SDKs. Could that line scope the field names to legacy /batch/ delivery, or say the token keeps its value wherever the transport carries it?

turnipdabeets added a commit that referenced this pull request Nov 11, 2026
…-plugin 3.0 (#201)

* docs(compliance): re-audit posthog-react-native rows for react-native-plugin 3.0

Re-audits the nine rows that react-native-plugin 3.0's move to posthog-ios
4.0 and posthog-android 4.0 affects, at posthog-js 210bf35d8 with native
behavior read at posthog-ios e27cbe4cb and posthog-android aaa23d39,
against sdk-specs 752f899 plus #146 at d387ab7 treated as merged.
Is Session Replay Active, Ingestion Controls and Privacy go to ✅; Debug
Properties goes ❌→🟡; Opt In meets #138 and #199; Stop Session Recording
stays 🟡 on posthog-android. Tally 32/25/2/5.

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

* docs(compliance): keep RN ingestion controls partial for cached sample rate

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

* docs(compliance): score RN capture exception partial for null custom properties

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

* docs(compliance): score RN start session recording partial for event triggers

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

* docs(compliance): record RN capture null-property gap in n5

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

* docs(compliance): pin n5 null-property line refs to posthog-js 210bf35d8

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
eli-r-ph and others added 2 commits November 10, 2026 16:51
…reads

- Null-valued properties: an SDK MAY omit them; one that keeps them sends
  JSON null. Omission scenarios are labeled and their acceptance features
  require the null_property_omission capability.
- MCP acceptance asserts the process_person_profile option, not the
  legacy property.
- Close add-capture-v1 task 3.4: the strict JSON body requirement covers
  non-finite numbers for capture v1.
- Scope the client $lib pair fill and token fail-open to capture v1; use
  a key without a pair rule in the client precedence scenario; note that
  session_id and window_id may be null; update the proposal for the $lib
  reversal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 11, 2026
* docs(compliance): re-audit posthog-android capture rows for 4.0

Re-score Capture, Capture AI, Event Batcher, HTTP Client and Retry Queue
against posthog-android 4.0 (be3ed4b4) and the capture v1 text in #146
and #147. Capture AI now applies to android. Capture, Capture AI and
HTTP Client move to partial on post-beforeSend uuid/options restore and
a caller httpClient's redirect policy; Retry Queue stays a pass.

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

* docs(compliance): re-check posthog-android 4.0 rows against #146 at 5c26b9b

Re-checks the five capture v1 rows at posthog-android 8b63a135 and #146 at
5c26b9b, which includes #147. #146 now allows the post-hook uuid and options
restore and the redirect rule on a caller's httpClient, so Capture and HTTP
Client stay ✅ and Capture AI scores ✅. Android 4.0 now keeps $lib in
properties (#942). Two #146 conflicts (compression opt-out scope and client
$lib precedence) are recorded as open questions, not scored. Tally 38/16/5/5.

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

* docs(compliance): score posthog-android 4.0 rows against #146 as merged

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

* docs(compliance): treat the #146 $lib precedence conflict as resolved for clients

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

* docs(compliance): re-check posthog-android 4.0 rows at aaa23d39 against main 752f899 and #146 d387ab7

Score Capture against the client $lib pair fill #146 now requires
(posthog-android#956), the #193 empty-variant rule and kept null array
elements (#955). Drop the settled compression and $lib conflict notes:
#146 exempts a lane with its own compression setting from the opt-out.
Update line numbers and the MIGRATION.md links, list the out-of-scope
rows later 4.0 and spec changes leave stale, and add note anchors.

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

* docs(compliance): re-audit posthog-android consent, identify, setup, shutdown and lifecycle rows at 4.0

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

* docs(compliance): re-audit posthog-android remote config, flag cache, reload, flag-called tracker, before-send and logs rows at 4.0

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

* docs(compliance): re-audit posthog-android flag-read rows at 4.0

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

* docs(compliance): re-audit posthog-android session replay rows at 4.0

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

* docs(compliance): re-audit posthog-android surveys and exception rows at 4.0

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

* docs(compliance): update posthog-android 4.0 totals, scope and README roll-ups

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

* docs(compliance): re-score posthog-android Alias at 4.0

alias() sets properties.distinct_id since PostHog/posthog-android#819, which
is in aaa23d39, so the row moves from ❌ to ✅ and joins the 4.0 re-audit.

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

* docs(compliance): score posthog-android Capture partial for the identity-call guide gap

#146 at 15501ca requires migration guides to show a process_person_profile:
true call option on identity calls. The 4.0 MIGRATION.md doesn't yet;
PostHog/posthog-android#966 adds it.

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

* docs(compliance): add the five newer contracts to posthog-android as N/A

Experiment Exposure, AI Datasets, AI Scorers, Offline Evaluations and Emails
join the matrix, matching main's 69 contracts and the iOS 4.0 page. All are
N/A: android requests /flags?v=2 and the other four are server-only.

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

* docs(compliance): keep posthog-android Capture passing while tracking 4.0

The identity-call migration-guide example #146 requires lands in 4.0 with
PostHog/posthog-android#966, which targets the release branch. This audit
tracks 4.0, so a pending release-branch PR is noted rather than scored as a
gap. Undoes the Capture downgrade from cfc72e1.

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

* docs(compliance): make the 69-contract README note SDK-neutral

The same paragraph lands from both the iOS and Android 4.0 audits, so it
merges cleanly in either order and stays true after both.

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

* docs(compliance): point posthog-android Identify remediation at #967

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 11, 2026
* chore(compliance): audit posthog-flutter 6.0

Re-audits all 64 posthog-flutter rows at 6be9a1e9ee (the 6.0 release PR)
against sdk-specs 8306698 plus #146 at 5c26b9b, treated as merged, and adds
five ➖ rows. Mobile rows cite the posthog-ios 4.0 and posthog-android 4.0
audits. Tally 26/27/4/12 on 69 rows.

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

* docs(compliance): re-audit posthog-flutter 6.0 at 82123e6

Re-audits the posthog-flutter rows at 82123e6 against sdk-specs 752f899
plus #146 at d387ab7, with native rows checked at posthog-ios e27cbe4cb
and posthog-android aaa23d39.

On Feature Flags moves to Pass: posthog-ios now drops a flag load in
flight across reset(). Capture scores the $lib pair fill on every path,
HTTP Client and Capture AI drop the settled compression note, Surveys
keeps only posthog-android's last-seen date gap, Opt In records #199's
deferred crash consent, and Get Feature Flags notes posthog-js reading
an empty variant as enabled on web. Tally 27/26/4/12 on 69 rows.

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

* docs(compliance): keep flutter flag payload and result partial for web

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

* docs(compliance): score flutter capture and capture AI partial for desktop session id

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

* docs(compliance): keep the flutter desktop debug-property gap in n28

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

* docs(compliance): note the web flag-event default in flutter n13 and n15

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

* docs(compliance): exclude web from the posthog-flutter $lib pair fill

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

* docs(compliance): score posthog-flutter Capture against #146 at 23c2478

#146 now requires migration guides to show a process_person_profile: true
call option on identity calls under a registered false. The 6.0 guide only
says those events get dropped; record the gap until posthog-flutter#718.

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

* docs(compliance): cite #146's registered-options text in posthog-flutter Register

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

* docs(compliance): track the posthog-flutter migration-guide example for 6.0, not as a gap

PostHog/posthog-flutter#718 targets the 6.0 release branch, which this
audit tracks, so the identity-call example is noted rather than listed as a
Capture gap.

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

* docs(compliance): restore the posthog-flutter bulk flag getter gaps row and give its 64-row score

The ❌ rows for Get Feature Flags and Get Feature Flags And Payloads were
removed when both moved to 🟡, without the 🟡 row iOS and Android add.
The row also gives its shared 64-row score like the other 69-row lines.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets added a commit that referenced this pull request Nov 11, 2026
* chore(compliance): audit posthog-ios 4.0

Re-audit all 64 posthog-ios rows at 3460e534a (4.0 release branch)
against sdk-specs dd11a6b, add the five rows added to the README table
since the last audit, and update the iOS roll-up and gap entries.

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

* fix(compliance): score posthog-ios HTTP Client partial

compression = .none leaves captureAi requests gzipped through the
separate captureAiCompression setting, so iOS 4.0 has no whole-client
compression opt-out. Roll-up becomes 40/17/1/11 (58%), 18 gaps.

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

* docs(compliance): list iOS capture v1 retries as matching #146

Draft #146 already permits the client-SDK durable queue, unlimited
attempts, 1-30 s backoff and multi-event 413 halving for iOS 4.0.

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

* fix(compliance): score posthog-ios Session Replay Privacy partial

SwiftUI's postHogMask() wins over an enclosing postHogNoMask(), the
reverse of the spec's unmask-over-mask precedence. It over-masks rather
than leaks. Roll-up becomes 39/18/1/11 (57%), 19 gaps.

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

* docs(compliance): re-check posthog-ios 4.0 at 1f91c5b2 against main and #146

Re-checks the iOS audit at 1f91c5b2 and sdk-specs 8306698. #1036 keeps
$lib/$lib_version, which now matches #146. Opt In meets the #138 replay
restart requirements. Surveys gains a #102 gap: setSurveySeen moves the
last-seen date on submit and dismiss. HTTP Client goes back to ✅: its
compression gap comes from conflicting http-client and capture-ai rules in
#146, raised on that PR. Tally 40/17/1/11.

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

* docs(compliance): score posthog-ios 4.0 capture v1 rows against #146 as merged

Scores Capture, Capture AI, Before Send Hook, Event Batcher, HTTP Client and
Retry Queue against #146 at 5c26b9b, treated as merged. The capture v1 wire,
transport and retry policy conform. Capture AI applies to iOS 4.0 and is 🟡
for the same NSNull serialization gap as Capture; starting the AI queue at
setup is what #146 asks of client SDKs. Tally 40/18/1/10.

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

* docs(compliance): treat the #146 $lib precedence conflict as resolved for clients

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

* docs(compliance): re-check posthog-ios 4.0 at e27cbe4cb against main 752f899 and #146 d387ab7

Capture, Capture Exception and Capture AI move to Pass now that null-valued
properties are omitted (posthog-ios#1047) and the SDK fills its own $lib pair
after the caller's properties (posthog-ios#1050). On Feature Flags moves to
Pass: loads in flight across reset() are dropped (posthog-ios#1048) and empty
variants are left out of listener flags. HTTP Client is scored Pass on its
merits, since #146 now leaves a lane with its own compression setting out of
the whole-client opt-out. Surveys stays Partial only for the popup-delay
eligibility re-check (SHOULD); posthog-ios#1049 fixed the rest.

Opt In meets the new deferred-crash consent requirement (#199); the empty
variant (#193), variant key rename (#203) and job-retry (#205) changes need no
other status change. Line references move to e27cbe4cb.

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

* docs(compliance): score posthog-ios Surveys as pass

The popup-delay and wait-period SHALLs are met since posthog-ios#1049. The
one unmet requirement is a SHOULD (re-check eligibility when the delay
elapses), which is recorded but not scored. Totals become 45/13/1/10.

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

* docs(compliance): score iOS Exception Steps partial on NSNull normalization

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

* docs(compliance): score posthog-ios Capture partial until the migration guide shows the identity call option

#146 at 15501ca requires migration guides to show a per-call
process_person_profile: true on identity calls; MIGRATION.md at e27cbe4cb
lacks it until PostHog/posthog-ios#1053 merges.

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

* docs(compliance): note the README's 69 contracts and the iOS row's 69-row basis

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

* docs(compliance): keep posthog-ios Capture passing while tracking 4.0

The identity-call migration-guide example #146 requires lands in 4.0 with
PostHog/posthog-ios#1053, which targets the release branch. This audit
tracks 4.0, so a pending release-branch PR is noted rather than scored as a
gap. Reverts 85d4acd.

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

* docs(compliance): make the 69-contract README note SDK-neutral

The same paragraph lands from both the iOS and Android 4.0 audits, so it
merges cleanly in either order and stays true after both.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Thegreatsura pushed a commit to Thegreatsura/posthog-js that referenced this pull request Nov 11, 2026
…ostHog#5637)

With POSTHOG_CAPTURE_MODE=v1, a 404 or 429 from capture now gets the
same backoff, Retry-After handling and retry budget as 408 and 5xx,
instead of failing the batch at once. Matches the capture v1 status
classification in PostHog/sdk-specs#146 and posthog-python#1178.
eli-r-ph and others added 2 commits November 11, 2026 16:21
SDK docs, migration guides and examples set the four settings through
options. They may say once that legacy option properties still work, map
each option to its property, and show a before-and-after migration
example. posthog-ios and posthog-android have no public global options,
so their docs may register a legacy option property for every event.

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

Copy link
Copy Markdown
Contributor

posthog-kmp 0.11 is a capture v1 release too: it wraps posthog-android 4.0, posthog-ios 4.0 and the JVM core 7.0, so Android, JVM, iOS and macOS send to /i/v1/*. JavaScript and Wasm stay on legacy capture through posthog-js. PostHog/posthog-kmp#119 adds the v1 surface, built against this PR:

  • options on every event call, as a map sent as given, with registerOptions() / unregisterOptions() and options in beforeSend. JavaScript and Wasm map the four known keys to their legacy properties, as posthog-flutter web does.
  • captureAi(), returning the UUID. It's a no-op on JavaScript and Wasm.
  • captureAiCompression, captureAiMaxQueueSize, captureAiTimeout and captureAiMaxEventBytes on PostHogConfig, passed through to the native SDKs.

The spec doesn't name posthog-kmp in these places yet:

  • openspec/specs/capture/spec.md:5: the client SDKs that move to capture v1.
  • openspec/specs/capture/spec.md:860: the SDKs that offer registerOptions() / unregisterOptions().
  • openspec/specs/capture-ai/spec.md:26 and openspec/specs/capture-ai/spec.md:123, plus the same list in AI lane configuration is scoped to the AI lane (openspec/specs/capture-ai/spec.md:480): the SDKs that implement captureAi and expose the AI lane settings.

Could you add posthog-kmp 0.11 (native on Android, JVM, iOS and macOS; legacy on JavaScript and Wasm) next to posthog-flutter 6.0 in each?

eli-r-ph and others added 2 commits November 12, 2026 14:41
…server SDKs

A server SDK sets process_person_profile on $identify, $create_alias,
$merge_dangerously, $groupidentify, $set and $set_once from the call alone:
the call's option, else its legacy property moved into the option, else true
unless the SDK generated the distinct id. Layers no longer turn these events
personless, and before_send sees one value. Client SDKs keep the current rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…release

posthog-python 8.0, posthog-go v2, posthog-rs 1.0 and posthog-node 6.0 ship
capture v1 first; the client SDK majors follow in their own release. Name only
those four where a requirement lists SDKs or versions, and keep the client
SDK rules in general terms. The client SDK details move to a follow-up change.

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

An SDK that delivers events through capture v1 SHALL treat an empty or whitespace-only caller `distinct_id` as missing. A missing id keeps the SDK's existing behavior: a generated personless id (UUIDv7, with `process_person_profile: false` set as an option) or a clear error returned to the caller. The SDK SHALL never send an event without a `distinct_id` key.

The SDK SHALL NOT shorten, hash or reject distinct ids by length or by placeholder value: capture v1 drops an event whose `distinct_id` is over 200 UTF-8 bytes (`distinct_id_too_large`) and accepts placeholder values such as `anonymous` without a person profile, and reports both in that event's `results` entry. A `$groupidentify` event's `distinct_id` SHALL be the caller's id when given, else the request-context id where the SDK has one, else `$<group type>_<group key>`, and never a random value. An `alias` call SHALL never take its target from the request context. A server SDK SHALL default identify-style events (`$identify`, `$create_alias`, `$merge_dangerously`, `$groupidentify`, `$set` and `$set_once`) to person processing when the call submits them, before any layer fills and before `before_send`: a non-`null` `process_person_profile` option on the call stands; otherwise a non-`null` `$process_person_profile` property on the call moves into the option and leaves the properties; otherwise the option is `true`, unless the SDK generated the event's distinct id, which keeps the personless `false`. A `null` option or property counts as unset. So global, context and registered layers do not set `process_person_profile` on an identify-style event whose distinct id the SDK did not generate; they reach it like any other event for every other property and option, and `$groupidentify` never takes the context session id. Once the option is set, the SDK SHALL remove any `$process_person_profile` property from the event before `before_send`, so the hook sees one value; the hook can still change the option and has the final say. The default applies to the event name the call submitted, so a hook that builds an identify-style event, or renames an event to one, sets the option itself, and SDK docs SHALL say so. When the final `process_person_profile` on `$identify`, `$create_alias`, `$merge_dangerously` or `$groupidentify` is `false`, capture still accepts it and reports it `ok`, but ingestion drops it (ingestion warning `invalid_event_when_process_person_profile_is_false`), so the identify, alias, merge or group update never takes effect; a `$set` or `$set_once` event is kept without its person update. SDK docs and migration guides SHALL describe this default, and SHALL tell apps that want an identify-style event personless to set `process_person_profile: false` on the call or in `before_send`, or to turn person processing off for the project. On a client SDK, global and registered layers still reach these events like any other event, and a `process_person_profile: true` call option, or one set in `before_send`, keeps them. Client SDKs have no request context: they send `$groupidentify` with the current distinct id, and with the session id like any other event.

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.

[suggestion] Could we extend this rule to client SDKs too? posthog-ios#1067 and posthog-android#979 now ignore a registered $process_person_profile, and a wrapper-registered process_person_profile option, on the same six events. Flutter and KMP inherit that from native, and a posthog-flutter PR for the desktop (Dart) path is coming.

The reason is the same as on the server, and stronger: in 3.x the client SDKs overwrote a registered $process_person_profile with their own value, so a registered false never blocked identify. Without this, 4.0 would start dropping those calls for apps that register false.

How the client version differs from the server rule:

  • It removes the registered value instead of setting true. Ingestion treats the two the same, and the SDK's own personless false for an anonymous user still applies, which is the client counterpart of the generated-id exception.
  • A value passed on the call, or set in before_send, still applies.
  • person_profiles: never still skips these calls.

So we'd replace "On a client SDK, global and registered layers still reach these events like any other event" with something like: "A client SDK SHALL NOT apply a registered $process_person_profile property or registered process_person_profile option to an identify-style event; a value set on the call or in before_send still applies." We'd also tag the layered-personless scenarios below @client too, or add client scenarios for a registered false, a call false, a before_send false, and an ordinary-event control.

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.

[suggestion] The register spec needs the same exception: openspec/specs/register/spec.md says registered properties reach every later event (step 6), and that identify / alias / group include them too. For $process_person_profile, those two lines would need to exclude the six identity events, matching the capture rule above.

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.

4 participants