Repository navigation
fix: retry desktop capture v1 requests that get 404 or 429 - #678
Merged
turnipdabeets merged 10 commits intoOct 28, 2026
Merged
Conversation
A 404 usually means a misconfigured proxy and a 429 an overloaded Envoy; both are worth retrying with the same backoff, Retry-After handling and retry budget. Redirects stay terminal.
Contributor
posthog-flutter Compliance ReportDate: 2026-10-28 20:19:59 UTC ✅ All Tests Passed!45/45 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 16/16 tests passed View Details
|
…e 404 After a batch fails its retries, later flushes make one attempt each and pause 6s doubling to 30s until a batch is delivered. Before, every flush, including the one each capture starts at flushAt, sent 4 requests, so a missing capture route kept resending the batch. A 404 prints one warning per client even without debug, naming the URL and the path a proxy or self-hosted instance must serve.
Contributor
There was a problem hiding this comment.
PostHog Review · Standard · reviewed fef8375: 1 Should fix.
See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.
A host with proxy credentials in its userinfo would print them to app logs.
… fix/v6-desktop-retry-404-429 # Conflicts: # .changeset/v6-desktop-capture-v1.md
marandaneto
approved these changes
Oct 28, 2026
marandaneto
left a comment
Member
There was a problem hiding this comment.
The code looks good; no qualifying findings. Verdict: correct.
5 of 6 tasks
… fix/v6-desktop-retry-404-429 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g-android A transient failure or a 2xx whose per-event results ask to retry no longer resends within the flush (3 tries, 3 s apart). Each one pauses the queue for 1 s doubling to 30 s, with ±25% jitter from an injectable source. A Retry-After is a floor applied after the jitter, capped at 30 s, instead of replacing the backoff, so `Retry-After: 1` no longer resends every 3 s. A delivered or dropped batch resets the backoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uration The 10 s whole-call timeout could fail a large batch on a slow uplink on every attempt. A request now fails only after 10 s without progress: connecting (HttpClient.connectionTimeout), the socket taking a 64 KiB body chunk, the response headers, or a response body chunk. This is the closest dart:io gets to the per-read/per-write idle timeouts of OkHttp and URLSession: like them, it can't see the bytes still in the kernel send buffer, so the wait for headers starts once the last chunk is handed over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 413 halved only the batch size and resent at once in the same flush. Like posthog-ios and posthog-android, it now also halves flushAt, kept within the batch size, and the smaller batches go out with the next flush (the next capture over flushAt, or the periodic flush). No backoff is added: posthog-ios ends the flush without one; posthog-android pauses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like posthog-ios and posthog-android, a full capture v1 request's worth of queued bytes (captureV1MaxBatchBytes) now flushes as flushAt does, so a few large events go out before the queue starts dropping them. The file queue keeps each event's serialized size: measured when it is added, and read from the file sizes when a later process loads the queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
5 of 6 tasks
turnipdabeets
added a commit
that referenced
this pull request
Oct 28, 2026
Brings in #678's backoff, idle-progress timeout, 413 handling and 5 MiB queued-bytes flush trigger, applied per capture lane so the analytics and AI endpoints each keep their own backoff, flushAt and batch size. - FileEventQueue: one size tracker, a running total (byteCount) kept on add, remove and load, used by both the 5 MiB flush trigger and the AI 50 MiB cap. add() still takes pre-encoded json/bytes to skip a second encode. - _CaptureLane gains flushAt; failedFlushes now drives captureRetryDelay per lane, and a 413 halves the lane's batch size and flushAt and ends its flush. - captureAiTimeout is the AI lane's idle-progress timeout, like analytics' 10 s, instead of a whole-request bound. - Test: the AI and analytics endpoints back off independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A body that trickles in more often than the 10 s idle timeout reset it forever, holding the flush. The body now also fails after 30 s in total. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets
merged commit Oct 28, 2026
22d3e0e
into
feat/v6-desktop-capture-v1
24 of 25 checks passed
turnipdabeets
added a commit
that referenced
this pull request
Oct 28, 2026
2 of 5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
Stacked on #673. 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. Both can recover, so dropping the batch loses data for no reason.
This ports PostHog/posthog-go#395 to the Dart desktop transport (Windows/Linux): 404 and 429 join 408/500/502/503/504 as retried statuses. Redirects (other than same-origin 307/308) are still terminal. Flutter on iOS and Android gets the same fix through the native SDKs.
It also brings desktop retry, timeout and flush behavior in line with posthog-android 4.0 (PostHog/posthog-android#897) and posthog-ios 4.0 (PostHog/posthog-ios#979), so capture v1 behaves the same on every 6.0 platform:
Retry-Afteris a floor applied after the jitter, capped at 30 s. It no longer replaces the backoff, soRetry-After: 1doesn't make the SDK resend every 3 s. A delivered or dropped batch resets the backoff. The jitter source is injectable for tests.HttpClient.connectionTimeout), the socket taking a 64 KiB chunk of the body (addStreamrespects socket backpressure), the response headers, or a chunk of the response body. This is as close to OkHttp's and URLSession's idle timeouts asdart:ioallows. Like those, it can't see bytes still in the kernel send buffer, so the wait for headers starts once the last chunk is handed to the socket.flushAtalong with the batch size, keepingflushAt≤ the batch size, and ends the flush instead of resending at once. The smaller batches go out with the next flush (the next capture overflushAt, or the periodic flush). The natives differ here: Android pauses with backoff after a 413, while iOS ends the flush without one. Desktop follows iOS and adds no backoff.captureV1MaxBatchBytesfrom feat!: capture v1 on every platform, with per-event options and captureAi #673), not only atflushAt. The file queue tracks each event's size: measured when the event is added, and read from the file sizes when a later process loads the queue.Divergences from native: no backoff after a 413 (matches iOS, not Android), and the timeout limitation above.
💚 How did you test it?
queue_flush_test.dart, and 429 added to theRetry-Afterpause test. The capture v1 404 test checks that the batch stays queued and nothing falls back to/batch.captureRetryDelayunit tests (doubling and cap, jitter bounds, an out-of-range jitter unit,Retry-Afteras a floor and its 30 s cap). Client tests with fixed jitter cover the 1/2/4/8/16/30 s schedule, ±25% jitter (first retry at 750 ms and 1250 ms),Retry-After5 over a 1 s backoff andRetry-After: 1under a 16 s backoff, and a partial-retry 2xx advancing the backoff while a full delivery resets it.testClientfixes the jitter at the midpoint, so the existing timing tests stay deterministic.HttpClientRequest.addStreamstops pulling chunks once a peer stops reading.flushAtboth halve, each 413 ends its flush, the next capture over the newflushAtflushes, and the periodic flush sends the halved batches.flushAt, andFileEventQueue.byteCountis checked across add, remove and a reload from disk.flutter testinposthog_flutter(1324 passed), plusdart analyze .(no issues) anddart format --set-exit-if-changed .(no changes) from the repo root.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset fileNo new changeset is needed because capture v1 on desktop hasn't shipped yet. The existing
v6-desktop-capture-v1changeset from #673 now says that a 404 or 429 is retried, that redirects still drop the batch, and that the queue backs off like iOS and Android.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Claude Code made the 404/429 change from a brief that named posthog-go#395 as the reference. A later brief asked it to match the posthog-android 4.0 and posthog-ios 4.0 retry, timeout and flush behavior, and it checked each gap against the native queue code. It left alone property order, options,
process_person_profile, the AI endpoint and the per-path retry identity, which are being done separately./flagskeeps its own retry (502/504 only, once, after 300 ms).🤖 Generated with Claude Code