feat: immediate-delivery capture (capture_immediate / capture_batch_immediate)#175
feat: immediate-delivery capture (capture_immediate / capture_batch_immediate)#175eli-r-ph wants to merge 3 commits into
Conversation
Add `capture_confirmed` / `capture_batch_confirmed` on both the async and blocking clients, returning `Result<CaptureSummary, Error>` after sending inline (bypassing the background worker) and awaiting a terminal outcome. Reuses the existing sans-IO request-build/retry/response-classification core, so both the v0 and capture-v1 pipelines are supported. These are for callers that must know a batch persisted before advancing their own durable state (e.g. a server-side importer committing an upstream offset). Fire-and-forget capture/capture_batch remain the primary API. The confirmed variants do NOT fire on_error hooks — the returned Result is the signal.
posthog-rs-v0 Compliance ReportDate: 2026-07-07 23:42:24 UTC ✅ All Tests Passed!46/46 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
posthog-rs-v1 Compliance ReportDate: 2026-07-07 23:42:56 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
|
A disabled client or a fully before_send-filtered batch returns a default CaptureSummary (submitted == 0, all_persisted == true). Callers that advance durable state on a confirmed delivery must also check submitted(), so document that all_persisted() alone is not a durability gate.
|
@eli-r-ph python has this https://github.com/PostHog/posthog-python/blob/6f75afe77ff059e4f3b0b6b7b30912612a7b5ff1/posthog/client.py#L284-L285 which bypasses queuing the events, i think something like this could be used in the rust async client so you can await the request Node has the other option is an on_success hook very similar to on_error hook (same shape, but rather for success use cases) the other option is to change the hook to have 2 methods, on_success and on_error, since its behind the v1 feature, we could break this API and make it nicer, and ofc rename it to something that makes more sense like idk i think all of those approaches are maybe a bit nicer than the one suggested in this PR WDYT? @PostHog/team-client-libraries opinions? |
|
Thanks for the reply, I'll take a look and update this accordingly - @PostHog/team-client-libraries please do chime in if you have additional thoughts on the matter, want to make sure I'm adding something that is in line with wider needs, doesn't just unblock internal SDK use cases 👍 |
|
I like the idea of standardizing on something like
For languages that have idiomatic return types that indicate a success/failure result, I think it's more expected that we prefer those over communicating via |
Rename capture_confirmed/capture_batch_confirmed to capture_immediate/capture_batch_immediate for cross-SDK naming consistency (mirrors the posthog-node captureImmediate method), keeping the richer Result<CaptureSummary> return. Extract the I/O-free setup (event prep, request building, payload encoding, URL resolution) into shared prepare_immediate (and v1 build_attempt_parts) helpers in v0_capture/v1_capture, collapsing the four client send methods to just the transport-specific send loop. No change to execution logic or timing. Parameterize the retryable-exhaustion and terminal-no-retry tests over the full shared status sets across all four client/pipeline modules, add a blocking-v1 terminal case, and assert not_persisted/all_persisted on the retry-then-empty-success path. Regenerate the public API snapshot.
|
👋 hey @dustinbyrne made a round of updates based on you an @marandaneto 's callouts, let me know if this feels like the right direction or needs larger rethink 🙇 |
|
looks clean, nice addition |
💡 Motivation and Context
capture/capture_batchare fire-and-forget: since v0.14.0 they enqueue onto the background transport worker and never report the outcome. That's the right default for analytics, but it leaves no path for a caller that must know a batch actually persisted before advancing its own durable state — the motivating case being a server-side importer that commits an upstream offset only once the events are safely ingested. Today such consumers are pinned to 0.13.3 (the last release wherecapture_batchreturned aResult).This restores an inline confirmed-delivery path as an additive, opt-in API without disturbing the fire-and-forget default:
Client::capture_immediate(event)andClient::capture_batch_immediate(events, historical_migration)on both the async and blocking clients.Result<CaptureSummary, Error>once the request reaches a terminal outcome, retrying transient failures per the client's existing retry configuration.v1_capture/v0_capture/retryhelpers), so both the v0 andcapture-v1pipelines are covered with no duplicated protocol logic.New public
CaptureSummary(#[non_exhaustive], accessor-only):submitted(),not_persisted(),all_persisted(), andevent_results()(undercapture-v1). On v0 a2xxpersists the whole batch (all_persisted()always true); oncapture-v1the per-event verdicts are surfaced, so a2xxwithdrop/retryverdicts reports as not fully persisted.Design notes:
*_immediatemirrors thecaptureImmediatemethod in posthog-node (send inline, resolve on completion). Unlike Node'sPromise<void>, the Rust API keeps the richerResult<CaptureSummary>so callers can inspect per-event durability.on_errorhooks — the returnedResultis the delivery signal, avoiding double-reporting.on_errorstays the observability channel for the fire-and-forget path.prepare_immediate/build_attempt_partshelpers inv0_capture/v1_capture. Each of the four client methods keeps only its transport-specific send loop — async awaitstokio::time::sleepbetween retries, blocking usesstd::thread::sleep— so there is no new runtime coupling and no protocol logic duplicated across clients.before_send-filtered batches return a defaultCaptureSummarywithout sending a request.💚 How did you test it?
tests/test_capture_immediate.rswith httpmock coverage across all four permutations (async x blocking, v0 x capture-v1): single/batch success, v1 partial-persist reporting, retryable-status-then-success and inline partial-retry pruning within one call, retry-budget exhaustion returningErr, terminal statuses without retry, historical-migration flag on the wire, gzip (query param +Content-Encodingheader) on v0, disabled/empty no-ops, and an explicit assertion that immediate calls never fireon_error.The retryable-exhaustion and terminal-no-retry cases are parameterized over the full shared status sets (
408/500/502/503/504retryable;400/401/402/403, plus a bare429on v0, terminal) in every client/pipeline module, and the retry-then-empty-success case now assertsnot_persisted()/all_persisted()directly.Verified
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --check, andRUSTDOCFLAGS=-D warnings cargo docclean across the feature matrix (default async v0,capture-v1,--no-default-featuresblocking v0/v1). Existing suites still pass (224 unit + integration). Public API snapshot regenerated viascripts/check-public-api.sh --update, and the downstreamcapture-v1-rs-smokee2e harness still builds against this branch.📝 Checklist
If releasing new changes
sampo addto generate a changeset file