Skip to content

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

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

turnipdabeets merged 3 commits into
feat/v4-capture-aifrom
fix/v4-retry-404-429

Conversation

@turnipdabeets

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Stacked on #884.

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 comes only from an overloaded Envoy. Neither should drop events.

This adds 404 and 429 to the capture v1 retry set for analytics and AI events. They use the same backoff, Retry-After handling and retry budget as 408 and 5xx. 3xx stays terminal. Reference: PostHog/posthog-go#395.

  • isCaptureV1RetriableStatusCode now covers 404 and 429. The AI endpoint, the queue and the legacy cached-events path all use it.
  • Tests that expected 404 or 429 to be terminal now expect them to be retried. Tests whose names no longer fit are renamed.
  • The v4 changeset and the migration guide say that 404 and 429 are retried and that 3xx still drops events.

💚 How did you test it?

  • ./gradlew :posthog:test --tests com.posthog.PostHogCaptureAiTest --tests com.posthog.internal.EndpointSpecTest --tests com.posthog.internal.PostHogQueueTest --tests com.posthog.internal.PostHogCaptureV1Test --tests com.posthog.internal.PostHogQueueDurabilityTest --tests com.posthog.internal.PostHogSendCachedLegacyEventsIntegrationTest: 125 tests passed.
  • ./gradlew spotlessCheck passed.
  • With the old predicate restored, the 3 updated behavior tests fail. They pass with this change.

📝 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 (updated the existing unreleased v4 major changeset)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Claude Code applied the policy change from posthog-go#395 to the capture v1 predicate, tests and docs. The unreleased v4 changeset already describes the retry behavior, so this PR updates it rather than adding a new changeset. Logs, replay and push-subscription retry policies are unchanged.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Oct 27, 2026
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 27, 2026 18:24
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-10-28T14:59:52.481779+00:00
Duration: 418607ms

⚠️ Some Tests Failed

212/219 tests passed, 7 failed


Capture_V1 Tests

⚠️ 98/101 tests passed, 3 failed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 434ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 43ms
Required Headers.Has Authorization Bearer Header ✅ 33ms
Required Headers.Has Content Type Json ✅ 35ms
Required Headers.Has Posthog Sdk Info Format ✅ 30ms
Required Headers.Has Posthog Attempt Header ✅ 26ms
Required Headers.Has Posthog Request Id ✅ 29ms
Required Headers.Has Posthog Request Timestamp ✅ 36ms
Required Headers.Has User Agent ✅ 28ms
Body Format.Body Has Created At And Batch ✅ 28ms
Body Format.No Api Key In Body ✅ 27ms
Body Format.No Sent At In Body ✅ 27ms
Event Format.Event Has Required Root Fields ✅ 27ms
Event Format.Event Uuid Is Valid ✅ 27ms
Event Format.Event Timestamp Is Rfc3339 ✅ 25ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 27ms
Event Format.Distinct Id Is String ✅ 25ms
Event Format.Distinct Id At Root Not Properties ✅ 26ms
Event Format.Custom Properties Preserved ✅ 23ms
Event Format.Set Properties Preserved ✅ 29ms
Event Format.Set Once Properties Preserved ✅ 26ms
Event Format.Groups Properties Preserved ✅ 26ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 26ms
Event Format.Event Has Required Root Fields Batch ✅ 37ms
Event Format.Event Uuid Is Valid Batch ✅ 37ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 31ms
Event Format.Distinct Id Is String Batch ✅ 36ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 29ms
Event Format.Custom Properties Preserved Batch ✅ 33ms
Event Format.Set Properties Preserved Batch ✅ 30ms
Event Format.Set Once Properties Preserved Batch ✅ 35ms
Event Format.Groups Properties Preserved Batch ✅ 34ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 33ms
Batch Behavior.Multiple Events In Single Batch ✅ 52ms
Batch Behavior.Batch Envelope Smoke ✅ 36ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 24ms
Batch Behavior.Flush At Triggers Batch ✅ 1027ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 29ms
Deduplication.Generates Unique Uuids ✅ 46ms
Deduplication.Different Events Same Content Different Uuids ✅ 28ms
Deduplication.Preserves Uuid On Retry ✅ 7016ms
Deduplication.Preserves Timestamp On Retry ✅ 7017ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7025ms
Deduplication.No Duplicate Events In Batch ✅ 35ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 19ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 12032ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 7016ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 2031ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 7016ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 17ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 23ms
Response Format Validation.Success No Retry After When All Ok ✅ 22ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 27ms
Response Format Validation.Success No Retry After When Drop Only ✅ 23ms
Response Format Validation.Response Echoes Request Id ✅ 17ms
Retry Behavior.Retries On 408 ✅ 7016ms
Retry Behavior.Retries On 500 ✅ 7016ms
Retry Behavior.Retries On 503 ✅ 7024ms
Retry Behavior.Retries On 504 ✅ 7016ms
Retry Behavior.Retryable Errors Have Retry After ✅ 4013ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10029ms
Retry Behavior.Does Not Retry On 400 ✅ 4023ms
Retry Behavior.Does Not Retry On 401 ✅ 4018ms
Retry Behavior.Does Not Retry On 402 ✅ 4021ms
Retry Behavior.Does Not Retry On 413 ✅ 4021ms
Retry Behavior.Does Not Retry On 415 ✅ 4018ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4021ms
Retry Behavior.Implements Backoff ✅ 17031ms
Retry Behavior.Max Retries Respected ❌ 17039ms
Partial Batch Handling.Handles 200 Full Success ✅ 2017ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3024ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3028ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 3027ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 5023ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 5025ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 5029ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 5028ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 5025ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 5029ms
Partial Batch Handling.Respects Retry After On Partial ✅ 5024ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3022ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 3032ms
Compression.Sends Gzip Content Encoding ✅ 15ms
Compression.No Content Encoding When Disabled ✅ 14ms
Compression.Compressed Body Is Decompressible ✅ 17ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4019ms
Event Options.Cookieless Mode Override ✅ 15ms
Event Options.Disable Skew Correction Override ✅ 11ms
Event Options.Process Person Profile Override ✅ 16ms
Event Options.Product Tour Id Override ✅ 14ms
Event Options.Unset Options Omitted ✅ 14ms
Event Options.Options Override In Batch ✅ 19ms
Event Options.Unknown Option Passes Through ✅ 17ms
Event Options.Option Value Is Not Converted ✅ 15ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 18ms
Event Options.Option Wins Over Legacy Property ✅ 15ms
Event Options.Legacy Property Fills Unset Option ✅ 16ms
Event Options.Null Option Falls Back To Legacy Property ✅ 17ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ❌ 19ms
Geoip And Historical Migration.Historical Migration Set In Body ❌ 17ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 13ms

Failures

retry_behavior.max_retries_respected

Expected 4 requests, got 5

geoip_and_historical_migration.geoip_disable_injected_into_properties

Expected $geoip_disable='True', got 'None'. Available properties: ['$is_identified', '$recording_status', '$sdk_debug_pending_queue_size']

geoip_and_historical_migration.historical_migration_set_in_body

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

Capture_Ai_V1 Tests

⚠️ 97/101 tests passed, 4 failed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 20ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 16ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 18ms
Required Headers.Has Authorization Bearer Header ✅ 15ms
Required Headers.Has Content Type Json ✅ 16ms
Required Headers.Has Posthog Sdk Info Format ✅ 15ms
Required Headers.Has Posthog Attempt Header ✅ 14ms
Required Headers.Has Posthog Request Id ✅ 14ms
Required Headers.Has Posthog Request Timestamp ✅ 17ms
Required Headers.Has User Agent ✅ 18ms
Body Format.Body Has Created At And Batch ✅ 21ms
Body Format.No Api Key In Body ✅ 20ms
Body Format.No Sent At In Body ✅ 18ms
Event Format.Event Has Required Root Fields ✅ 26ms
Event Format.Event Uuid Is Valid ✅ 22ms
Event Format.Event Timestamp Is Rfc3339 ✅ 20ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 20ms
Event Format.Distinct Id Is String ✅ 18ms
Event Format.Distinct Id At Root Not Properties ✅ 16ms
Event Format.Custom Properties Preserved ✅ 17ms
Event Format.Set Properties Preserved ✅ 18ms
Event Format.Set Once Properties Preserved ✅ 18ms
Event Format.Groups Properties Preserved ✅ 16ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 21ms
Event Format.Capture Ai Keeps Supplied Uuid ❌ 15ms
Event Format.Event Has Required Root Fields Batch ❌ 34ms
Event Format.Event Uuid Is Valid Batch ✅ 37ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 30ms
Event Format.Distinct Id Is String Batch ✅ 24ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 28ms
Event Format.Custom Properties Preserved Batch ✅ 31ms
Event Format.Set Properties Preserved Batch ✅ 29ms
Event Format.Set Once Properties Preserved Batch ✅ 35ms
Event Format.Groups Properties Preserved Batch ✅ 35ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 32ms
Batch Behavior.Multiple Events In Single Batch ✅ 44ms
Batch Behavior.Batch Envelope Smoke ✅ 38ms
Batch Behavior.Flush At Triggers Batch ✅ 1022ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 14ms
Deduplication.Generates Unique Uuids ✅ 30ms
Deduplication.Different Events Same Content Different Uuids ✅ 19ms
Deduplication.Preserves Uuid On Retry ✅ 7014ms
Deduplication.Preserves Timestamp On Retry ✅ 7015ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7015ms
Deduplication.No Duplicate Events In Batch ✅ 30ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 17ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 12025ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 7013ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 2025ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 7016ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 17ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 24ms
Response Format Validation.Success No Retry After When All Ok ✅ 18ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 23ms
Response Format Validation.Success No Retry After When Drop Only ✅ 20ms
Response Format Validation.Response Echoes Request Id ✅ 15ms
Retry Behavior.Retries On 408 ✅ 7021ms
Retry Behavior.Retries On 500 ✅ 7015ms
Retry Behavior.Retries On 503 ✅ 7021ms
Retry Behavior.Retries On 504 ✅ 7016ms
Retry Behavior.Retryable Errors Have Retry After ✅ 4013ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10021ms
Retry Behavior.Does Not Retry On 400 ✅ 4017ms
Retry Behavior.Does Not Retry On 401 ✅ 4025ms
Retry Behavior.Does Not Retry On 402 ✅ 4020ms
Retry Behavior.Does Not Retry On 413 ✅ 4018ms
Retry Behavior.Does Not Retry On 415 ✅ 4016ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4017ms
Retry Behavior.Implements Backoff ✅ 17027ms
Retry Behavior.Max Retries Respected ❌ 17032ms
Partial Batch Handling.Handles 200 Full Success ✅ 2017ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3023ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3026ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 3019ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 5027ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 5025ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 5027ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 5024ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 5027ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 5018ms
Partial Batch Handling.Respects Retry After On Partial ✅ 5020ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3021ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 3027ms
Compression.Sends Gzip Content Encoding ✅ 18ms
Compression.No Content Encoding When Disabled ✅ 15ms
Compression.Compressed Body Is Decompressible ✅ 12ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4015ms
Event Options.Cookieless Mode Override ✅ 16ms
Event Options.Disable Skew Correction Override ✅ 14ms
Event Options.Process Person Profile Override ✅ 14ms
Event Options.Product Tour Id Override ✅ 15ms
Event Options.Unset Options Omitted ✅ 23ms
Event Options.Options Override In Batch ✅ 24ms
Event Options.Unknown Option Passes Through ✅ 15ms
Event Options.Option Value Is Not Converted ✅ 16ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 18ms
Event Options.Option Wins Over Legacy Property ✅ 14ms
Event Options.Legacy Property Fills Unset Option ✅ 14ms
Event Options.Null Option Falls Back To Legacy Property ✅ 14ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ❌ 16ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 15ms

Failures

event_format.capture_ai_keeps_supplied_uuid

Expected last action result '0198c0de-0000-7000-8000-000000000abc', got '01a18303-256b-7bf7-bb47-3b26e578bd65'

event_format.event_has_required_root_fields_batch

Expected 1 requests, got 2

retry_behavior.max_retries_respected

Expected 4 requests, got 5

geoip_and_historical_migration.geoip_disable_injected_into_properties

Expected $geoip_disable='True', got 'None'. Available properties: ['$is_identified', '$recording_status', '$sdk_debug_session_start', '$sdk_debug_pending_queue_size']

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 33ms
Request Payload.Flags Request Uses V2 Query Param ✅ 19ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 18ms
Request Payload.Flags Request Omits Authorization Header ✅ 18ms
Request Payload.Token In Flags Body Matches Init ✅ 21ms
Request Payload.Groups Round Trip ✅ 20ms
Request Payload.Groups Default To Empty Object ✅ 19ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 16ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 16ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 19ms
Request Lifecycle.No Flags Request On Init Alone ✅ 6ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 15ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 22ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 22ms
Retry Behavior.Retries Flags On 502 ✅ 323ms
Retry Behavior.Retries Flags On 504 ✅ 319ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 18ms

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

@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 security follow-up found a reproduced credential disclosure. This supersedes my earlier clean-review conclusion.

turnipdabeets and others added 3 commits October 28, 2026 10:48
A 404 usually means a misconfigured proxy and a 429 now only comes from an overloaded ingress, so keep the events queued and retry them with the usual backoff and Retry-After handling instead of dropping them. Matches PostHog/posthog-go#395.
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.
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.

2 participants