feat(capture): v1 transport + partial-retry send loop (capture v1, 3/6)#703
Draft
eli-r-ph wants to merge 2 commits into
Draft
feat(capture): v1 transport + partial-retry send loop (capture v1, 3/6)#703eli-r-ph wants to merge 2 commits into
eli-r-ph wants to merge 2 commits into
Conversation
Contributor
|
Reviews (1): Last reviewed commit: "feat(capture): add v1 transport and part..." | Re-trigger Greptile |
4 tasks
Contributor
posthog-python Compliance ReportDate: 2026-06-28 00:55:20 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
|
a901fdc to
7fd7dcd
Compare
419d8ac to
41c6948
Compare
7fd7dcd to
32d7b02
Compare
41c6948 to
d6d4aa2
Compare
32d7b02 to
3677400
Compare
Adds the HTTP transport for POST /i/v1/analytics/events alongside the pure transforms: a single Bearer-authed attempt (post_v1) with the required v1 headers, response classification (parse_v1_response), and the send loop (send_v1_batch) that resends only the events the server tags "retry", logs drops, honors Retry-After, and raises CaptureV1Error on terminal/transport failure or retry exhaustion so the consumer's existing on_error path fires unchanged. 429 is terminal in v1 (unlike v0). A 2xx with an unparseable body is terminal to avoid an infinite resend loop. A stable PostHog-Request-Id and created_at span attempts; PostHog-Attempt increments. Factors a shared gzip_compress helper out of request.post (no v0 behavior change). Still inert: nothing calls send_v1_batch until the consumer wiring PR. 74 capture_v1 tests (47 transform + 27 transport); ruff/mypy clean.
Address review of the v1 transport: - Hoist the batch created_at out of the retry loop so the envelope stays stable across attempts (only the events list and PostHog-Attempt change). - Isolate v1 request compression behind a CaptureCompression selector supporting gzip and zlib-wrapped deflate (RFC 1950), reverting the gzip_compress extraction from request.py so the v1 path owns its codecs. - Stop logging per-event drops at WARNING; a server-chosen drop on a 2xx is not a delivery failure and is already carried on CaptureV1Error for batch-level surfacing via on_error.
3677400 to
c698bd5
Compare
d6d4aa2 to
4ee420a
Compare
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
Third PR in the stacked Capture V1 series (stacked on #702). Adds the HTTP transport and partial-retry send loop for
POST /i/v1/analytics/events, on top of the pure transforms from #702. Still inert —send_v1_batchhas no caller until the consumer-wiring PR.New in
posthog/capture_v1.py:post_v1(...)— a single attempt. Bearer auth (noapi_keyin the body), the required v1 headers (PostHog-Sdk-Info,PostHog-Attempt,PostHog-Request-Id,PostHog-Request-Timestamp), and optional gzip. Returns the raw response; this is also the monkeypatch seam the test harness adapter will drive.parse_v1_response(...)— classifies one response without raising: 2xx parses the per-uuidresultsmap (an unparseable 2xx body is flaggedmalformed), non-2xx best-effort extracts an error message, andRetry-After(delta-seconds or HTTP-date) is parsed in both cases.send_v1_batch(...)— the v1 sibling ofConsumer._send. Loops up tomax_retries + 1attempts, but shrinks the batch to only the events the server taggedretryafter each 2xx.ok/warning/absent events succeed silently;dropevents are logged (a request the server accepted-but-dropped is not a delivery failure, so it is not raised). RaisesCaptureV1Error(anAPIErrorsubclass, so the consumer's existingon_error(exc, batch)keeps working) on a batch-level terminal/transport failure or once retries are exhausted.CaptureV1Error/V1ParsedResponse/V1EventResulttypes.Behavior choices, verified against the Rust contract and posthog-go's
capture_v1_send.go:Retry-After.PostHog-Request-Idandcreated_atspan all attempts;PostHog-Attemptincrements — so the backend can correlate/dedupe a retried batch.Retry-Afterwins, else capped exponential) so both wire protocols back off identically.Also factors a shared
gzip_compresshelper out ofrequest.post— pure refactor, no v0 behavior change (covered by the existingtest_request.py).💚 How did you test it?
posthog/test/test_capture_v1.pynow has 74 cases (47 transform + 27 transport). New transport coverage:post_v1header/url/no-key-in-body/gzip-magic;parse_v1_responsesuccess/malformed/missing-results/error-body-variants/text-fallback/Retry-After; andsend_v1_batchdriven by a stubbedpost_v1with mocked sleeps — all-ok, absent-uuid-accepted, partial-retry-shrinks-to-retry-uuids, stable-request-id + incrementing-attempt, drop-logged-not-raised, retry-exhausted-raises, malformed-2xx-terminal, 400/429-terminal-not-retried, 503-then-success (honorsRetry-After), 503-exhausted-raises, transport-error-then-success, transport-error-exhausted-reraises.ruff format/checkclean;mypyclean oncapture_v1.py+request.py;test_request.py(61) still green after the gzip refactor; regeneratedreferences/public_api_snapshot.txt.📝 Checklist
send_v1_batchhas no caller yet).🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored with Cursor (Claude Opus 4.8) per the agreed plan. posthog-go's
sendV1is woven into its client lifecycle (channels,notifyFailure/notifySuccess,maxAttempts); this port instead fits posthog-python'sConsumer._sendshape — a synchronous loop that raises on failure so the existingupload()->on_error(exc, batch)path fires unchanged — while preserving go's partial-retry algorithm, status matrix, stable-request-id semantics, and per-event drop/retry handling.