Skip to content

fix: retry capture v1 requests that get 404 or 429 - #979

Merged
turnipdabeets merged 6 commits into
feat/v4-capture-v1from
fix/v4-retry-404-429
Oct 28, 2026
Merged

turnipdabeets merged 6 commits into
feat/v4-capture-v1from
fix/v4-retry-404-429

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Stacked on #938.

We agreed that SDKs should drop events only when they can never be delivered (event too large, duplicate uuid) or when the buffer is full. A 404 usually means a misconfigured proxy, and a 429 now only comes from an overloaded Envoy, so both should be retried rather than dropped.

Capture v1 (/i/v1/analytics/events) and the AI endpoint (/i/v1/ai/events) now retry 404 and 429 alongside 408, 500, 502, 503 and 504, with the same backoff, Retry-After handling and retry behavior as the other retryable statuses. Both endpoints share isCaptureV1RetriableStatusCode, so the one-line policy change covers them both. Session replay and logs are unchanged.

Reference implementation: PostHog/posthog-go#395. This PR has the same scope: the retryable set, the classification tests, the migration guide and the changeset.

  • QueueEndpoint+Factories.swift: add 404 and 429 to the capture v1 policy and fix its doc comment.
  • .changeset/v4-capture-v1.md: edit the existing v4 entry instead of adding a contradictory one.
  • docs/migration-3.x-to-4.0.md: the retryable statuses are now 404, 408, 429, 500, 502, 503 and 504, plus network errors, and a 404 is retried.

💚 How did you test it?

  • swift test --no-parallel -Xswiftc -DTESTING --filter 'PostHogCaptureV1|PostHogQueueTest|PostHogApiTest|PostHogLogsQueueTest': 162 tests in 19 suites passed.
  • Flipped the tests that expected 404 or 429 to be terminal: the status table, the v1 404 transport test (it now checks that the retry reuses the request ID with PostHog-Attempt: 2), the AI 404 queue test, the queue 429 test (merged into the parameterized 404/408/429 retain test), and the transport-error disposition matrix (adds 404, which is retried for capture v1 but not for /snapshot).
  • Reverting the policy line makes all of these fail.
  • swiftformat --lint is clean and git diff --check passes.

📝 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 v4-capture-v1 changeset)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code ported the policy change from posthog-go#395. One judgment call: the existing changeset and migration guide were edited in place, because a separate changeset would contradict the unreleased v4 entry.

🤖 Generated with Claude Code

A 404 usually means a misconfigured proxy and a 429 now only comes from an overloaded Envoy, so neither should drop events. Matches posthog-go#395.
@turnipdabeets turnipdabeets self-assigned this Oct 27, 2026
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 27, 2026 18:25
A 404 now keeps events queued, so nothing tells the app that the route
is missing. Print one warning per client, even without debug, naming the
URL and the path a reverse proxy or self-hosted instance must serve.

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

Automated advisory code review: reproduced blocking finding.

Comment thread PostHog/PostHogApi.swift Outdated
@marandaneto
marandaneto requested a review from a team October 28, 2026 13:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from feat/v4-remove-batch to chore/v4-remove-deprecated October 28, 2026 15:00
@turnipdabeets
turnipdabeets changed the base branch from chore/v4-remove-deprecated to feat/v4-capture-v1 October 28, 2026 15:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-10-28T18:13:32.161201+00:00
Duration: 944140ms

⚠️ Some Tests Failed

214/219 tests passed, 5 failed


Capture_V1 Tests

⚠️ 99/101 tests passed, 2 failed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 2863ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2862ms
Required Headers.Has Authorization Bearer Header ✅ 2749ms
Required Headers.Has Content Type Json ✅ 2806ms
Required Headers.Has Posthog Sdk Info Format ✅ 2815ms
Required Headers.Has Posthog Attempt Header ✅ 2757ms
Required Headers.Has Posthog Request Id ✅ 3020ms
Required Headers.Has Posthog Request Timestamp ✅ 3147ms
Required Headers.Has User Agent ✅ 2826ms
Body Format.Body Has Created At And Batch ✅ 2915ms
Body Format.No Api Key In Body ✅ 2830ms
Body Format.No Sent At In Body ✅ 2780ms
Event Format.Event Has Required Root Fields ✅ 2737ms
Event Format.Event Uuid Is Valid ✅ 2747ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2886ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2835ms
Event Format.Distinct Id Is String ✅ 2830ms
Event Format.Distinct Id At Root Not Properties ✅ 2833ms
Event Format.Custom Properties Preserved ✅ 2698ms
Event Format.Set Properties Preserved ✅ 2659ms
Event Format.Set Once Properties Preserved ✅ 395ms
Event Format.Groups Properties Preserved ✅ 2750ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2848ms
Event Format.Event Has Required Root Fields Batch ✅ 2807ms
Event Format.Event Uuid Is Valid Batch ✅ 2792ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2958ms
Event Format.Distinct Id Is String Batch ✅ 2825ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2803ms
Event Format.Custom Properties Preserved Batch ✅ 2729ms
Event Format.Set Properties Preserved Batch ✅ 2802ms
Event Format.Set Once Properties Preserved Batch ✅ 2791ms
Event Format.Groups Properties Preserved Batch ✅ 2806ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2859ms
Batch Behavior.Multiple Events In Single Batch ✅ 2779ms
Batch Behavior.Batch Envelope Smoke ✅ 2806ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 313ms
Batch Behavior.Flush At Triggers Batch ✅ 1160ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2780ms
Deduplication.Generates Unique Uuids ✅ 2943ms
Deduplication.Different Events Same Content Different Uuids ✅ 2763ms
Deduplication.Preserves Uuid On Retry ✅ 9308ms
Deduplication.Preserves Timestamp On Retry ✅ 9324ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7792ms
Deduplication.No Duplicate Events In Batch ✅ 2991ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2809ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16700ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9247ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7381ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9173ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2953ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2887ms
Response Format Validation.Success No Retry After When All Ok ✅ 2796ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2907ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2806ms
Response Format Validation.Response Echoes Request Id ✅ 2803ms
Retry Behavior.Retries On 408 ✅ 9227ms
Retry Behavior.Retries On 500 ✅ 8796ms
Retry Behavior.Retries On 503 ✅ 11700ms
Retry Behavior.Retries On 504 ✅ 9485ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6230ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10859ms
Retry Behavior.Does Not Retry On 400 ✅ 4708ms
Retry Behavior.Does Not Retry On 401 ✅ 4757ms
Retry Behavior.Does Not Retry On 402 ✅ 4798ms
Retry Behavior.Does Not Retry On 413 ✅ 4698ms
Retry Behavior.Does Not Retry On 415 ✅ 2362ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4671ms
Retry Behavior.Implements Backoff ✅ 21314ms
Retry Behavior.Max Retries Respected ❌ 21300ms
Partial Batch Handling.Handles 200 Full Success ✅ 4720ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5897ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5770ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 5875ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7889ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7931ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7905ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7780ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7856ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7890ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7737ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5888ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 6002ms
Compression.Sends Gzip Content Encoding ✅ 2770ms
Compression.No Content Encoding When Disabled ✅ 2742ms
Compression.Compressed Body Is Decompressible ✅ 2892ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 2405ms
Event Options.Cookieless Mode Override ✅ 2825ms
Event Options.Disable Skew Correction Override ✅ 2771ms
Event Options.Process Person Profile Override ✅ 2867ms
Event Options.Product Tour Id Override ✅ 2744ms
Event Options.Unset Options Omitted ✅ 2756ms
Event Options.Options Override In Batch ✅ 2902ms
Event Options.Unknown Option Passes Through ✅ 2769ms
Event Options.Option Value Is Not Converted ✅ 2692ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2892ms
Event Options.Option Wins Over Legacy Property ✅ 2712ms
Event Options.Legacy Property Fills Unset Option ✅ 2835ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2866ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2799ms
Geoip And Historical Migration.Historical Migration Set In Body ❌ 405ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2837ms

Failures

retry_behavior.max_retries_respected

Expected 4 requests, got 5

geoip_and_historical_migration.historical_migration_set_in_body

Body missing 'historical_migration' field. Body keys: ['batch', 'created_at']

Capture_Ai_V1 Tests

⚠️ 98/101 tests passed, 3 failed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 2656ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 540ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 2686ms
Required Headers.Has Authorization Bearer Header ✅ 277ms
Required Headers.Has Content Type Json ✅ 2660ms
Required Headers.Has Posthog Sdk Info Format ✅ 411ms
Required Headers.Has Posthog Attempt Header ✅ 2816ms
Required Headers.Has Posthog Request Id ✅ 3044ms
Required Headers.Has Posthog Request Timestamp ✅ 2794ms
Required Headers.Has User Agent ✅ 334ms
Body Format.Body Has Created At And Batch ✅ 3038ms
Body Format.No Api Key In Body ✅ 2835ms
Body Format.No Sent At In Body ✅ 2862ms
Event Format.Event Has Required Root Fields ✅ 2798ms
Event Format.Event Uuid Is Valid ✅ 289ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2696ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2748ms
Event Format.Distinct Id Is String ✅ 2738ms
Event Format.Distinct Id At Root Not Properties ✅ 2782ms
Event Format.Custom Properties Preserved ✅ 501ms
Event Format.Set Properties Preserved ✅ 372ms
Event Format.Set Once Properties Preserved ✅ 2872ms
Event Format.Groups Properties Preserved ✅ 441ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2838ms
Event Format.Capture Ai Keeps Supplied Uuid ❌ 137ms
Event Format.Event Has Required Root Fields Batch ❌ 2939ms
Event Format.Event Uuid Is Valid Batch ✅ 2845ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2736ms
Event Format.Distinct Id Is String Batch ✅ 2923ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2897ms
Event Format.Custom Properties Preserved Batch ✅ 2794ms
Event Format.Set Properties Preserved Batch ✅ 2944ms
Event Format.Set Once Properties Preserved Batch ✅ 2897ms
Event Format.Groups Properties Preserved Batch ✅ 2774ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2837ms
Batch Behavior.Multiple Events In Single Batch ✅ 2992ms
Batch Behavior.Batch Envelope Smoke ✅ 2897ms
Batch Behavior.Flush At Triggers Batch ✅ 1212ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 462ms
Deduplication.Generates Unique Uuids ✅ 3000ms
Deduplication.Different Events Same Content Different Uuids ✅ 2941ms
Deduplication.Preserves Uuid On Retry ✅ 9301ms
Deduplication.Preserves Timestamp On Retry ✅ 9232ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7962ms
Deduplication.No Duplicate Events In Batch ✅ 2919ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 411ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16680ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9241ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7484ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 5472ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2822ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2866ms
Response Format Validation.Success No Retry After When All Ok ✅ 2849ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2885ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2945ms
Response Format Validation.Response Echoes Request Id ✅ 2900ms
Retry Behavior.Retries On 408 ✅ 9408ms
Retry Behavior.Retries On 500 ✅ 9255ms
Retry Behavior.Retries On 503 ✅ 9156ms
Retry Behavior.Retries On 504 ✅ 9350ms
Retry Behavior.Retryable Errors Have Retry After ✅ 2434ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10946ms
Retry Behavior.Does Not Retry On 400 ✅ 4744ms
Retry Behavior.Does Not Retry On 401 ✅ 4865ms
Retry Behavior.Does Not Retry On 402 ✅ 4799ms
Retry Behavior.Does Not Retry On 413 ✅ 4808ms
Retry Behavior.Does Not Retry On 415 ✅ 2360ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4740ms
Retry Behavior.Implements Backoff ✅ 21772ms
Retry Behavior.Max Retries Respected ❌ 21757ms
Partial Batch Handling.Handles 200 Full Success ✅ 4887ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5869ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5914ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 6202ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7885ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7757ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7938ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7865ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7889ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 8006ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7891ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5935ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 5963ms
Compression.Sends Gzip Content Encoding ✅ 2801ms
Compression.No Content Encoding When Disabled ✅ 427ms
Compression.Compressed Body Is Decompressible ✅ 2901ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4803ms
Event Options.Cookieless Mode Override ✅ 2830ms
Event Options.Disable Skew Correction Override ✅ 2732ms
Event Options.Process Person Profile Override ✅ 2764ms
Event Options.Product Tour Id Override ✅ 2761ms
Event Options.Unset Options Omitted ✅ 2842ms
Event Options.Options Override In Batch ✅ 2865ms
Event Options.Unknown Option Passes Through ✅ 2732ms
Event Options.Option Value Is Not Converted ✅ 2833ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 432ms
Event Options.Option Wins Over Legacy Property ✅ 2744ms
Event Options.Legacy Property Fills Unset Option ✅ 394ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2730ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2840ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2839ms

Failures

event_format.capture_ai_keeps_supplied_uuid

Expected last action result '0198c0de-0000-7000-8000-000000000abc', got '01a183b1-0a31-7f6b-93b7-49c7c69a5b6b'

event_format.event_has_required_root_fields_batch

Expected 1 requests, got 2

retry_behavior.max_retries_respected

Expected 4 requests, got 5

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2781ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2741ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2760ms
Request Payload.Flags Request Omits Authorization Header ✅ 2866ms
Request Payload.Token In Flags Body Matches Init ✅ 2861ms
Request Payload.Groups Round Trip ✅ 2842ms
Request Payload.Groups Default To Empty Object ✅ 2860ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2904ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2768ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2810ms
Request Lifecycle.No Flags Request On Init Alone ✅ 85ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2768ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5541ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2773ms
Retry Behavior.Retries Flags On 502 ✅ 2775ms
Retry Behavior.Retries Flags On 504 ✅ 2806ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 3127ms

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

Verified 404/429 recovery through the SDK’s HTTP transport, stable retry identity, and credential-redacted once-per-client warning behavior. No issues found in this change.

AI-assisted review.

@turnipdabeets
turnipdabeets merged commit 0fd48c0 into feat/v4-capture-v1 Oct 28, 2026
43 checks passed
@turnipdabeets
turnipdabeets deleted the fix/v4-retry-404-429 branch October 28, 2026 20:05
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.

3 participants