Add auction timeline offsets spec amendment - #1076
Conversation
Adds section 18 to the request phase timing spec: three first-call-wins T0 offsets (auction dispatched, resolved, committed) on RequestTimings, emitted as additive nullable columns on access_logs_raw with auction_id as the join key to the per-bidder auction dataset. Answers the overlap-proof questions the two existing clocks cannot: when the auction started relative to request entry, when the final bid landed, and when targeting was committed toward GAM.
Implements spec section 18: three first-call-wins marks on RequestTimings (dispatched at the DispatchAuctionOutcome::Dispatched arm, resolved after collect at both sites, committed after write_bids_to_state at both sites), carried through TimingSnapshot into four additive access_logs_raw columns: auction_dispatched_ms, auction_resolved_ms, auction_committed_ms, and auction_id as the join key to the per-bidder auction dataset. Null offsets mean no auction ran; a failed dispatch records nothing. FORWARD_QUERY fills the new columns with typed defaults for pre-existing rows. No header emission, no config surface, no adapter changes: the values ride the existing snapshot and the tinybird.access_enabled gate.
The Cloudflare integration harness writes wrangler.integration.generated.toml at test time; it was swept into the previous commit by accident. Ignore it so local CI=1 runs cannot commit it again.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Reviewed 1fa9f8cc09df889aec42039b13b7a108d1df9d47 against 38043d7464362d44519153a09fe850bacc256b58. Two actionable telemetry-correctness issues are posted inline. Focused WASM tests and Rust formatting passed; the Tinybird schema evolution could not be independently dry-run without credentials. The current format-docs CI check also fails on the new implementation plan.
| .await; | ||
| timings.record_auction_wait(*placement, wait_started.elapsed()); | ||
| // T0-anchored timeline mark (spec section 18): final bid or timeout. | ||
| timings.mark_auction_resolved(); |
There was a problem hiding this comment.
🔧 P1 / High: Resolved time is collection time, not bidder completion time
Issue: When bidder responses finish before the origin stream reaches </body>, nothing polls them until the seam. This line stamps auction_resolved_ms only after collect_dispatched_auction returns, potentially much later. An all-immediate provider result makes this explicit: the auction is terminal at dispatch, but this mark still waits for the seam.
Impact: R - D includes origin fetch and body-stream delay rather than auction duration. The documented overlap calculation can therefore substantially overstate auction runtime and cannot answer when the final bid landed, which is the main purpose of this change.
Evidence: Collection starts at the delayed body seam, while collect_dispatched_auction performs the first select over pending requests. The focused split_auction_accepts_an_all_immediate_no_bid_result test passes and confirms that Dispatched does not imply work remains.
Suggested fix: Capture the terminal timestamp when the final provider actually completes or times out, then pass that timestamp into RequestTimings. This likely requires polling collection concurrently or receiving completion timing from the transport. If that is unavailable, rename the field to auction_collected_ms and remove the auction-duration and overlap claims. Add a delayed-collection regression test.
| "auction_dispatched_ms": timings.auction_dispatched_ms, | ||
| "auction_resolved_ms": timings.auction_resolved_ms, | ||
| "auction_committed_ms": timings.auction_committed_ms, | ||
| "auction_id": timings.auction_id.as_deref().unwrap_or("none"), |
There was a problem hiding this comment.
🔧 P2 / Medium: Auction API requests serialize as if no auction ran
Issue: The new fields are marked only by the split initial-page auction path. Successful /auction and /_ts/page-bids requests run auctions and emit auction_events_raw rows, but their access rows retain null offsets and the none auction ID serialized here.
Impact: Every Fastly access row for these routes loses its join to per-bidder telemetry and violates the documented meaning that null or none means no auction ran.
Evidence: POST /auction calls run_auction in auction/endpoints.rs, and GET /_ts/page-bids calls it in publisher.rs; neither path invokes any of the new mark methods. The Fastly post-send emitter still serializes the shared RequestTimings snapshot for both routes.
Suggested fix: Instrument both handlers using their AuctionObservationContext::auction_id and accurate lifecycle timestamps. If these columns intentionally cover only initial publisher navigation, document and name that narrower scope rather than using a global no-auction sentinel. Add route-level row tests.
Spec-first follow-up to #1074, targeting the feature branch so it lands with (or after) the base spec rather than against main.
Adds section 18 to the request phase timing design: three T0-anchored auction milestones so the auction's timeline and the request's timeline finally share a clock.
Problem
Two clocks that never meet:
auction_events_rawmeasures the auction internally (total_time_ms, per-providerprovider_response_time_ms) on a clock that starts at auction creation; the access row is T0-anchored but only recordsauction_wait_ms(blocked time at collect). Nothing can answer: when did the auction start relative to request entry, when did the final bid land, and when was targeting committed toward GAM.Design
RequestTimings(same style asmark_headers_ready()): dispatched (bid requests left the edge), resolved (final bid or timeout), committed (write_bids_to_statereturned; targeting available to the response pipeline in both buffered and streaming modes).access_logs_raw:auction_dispatched_ms/auction_resolved_ms/auction_committed_ms(Nullable UInt32; null = no auction ran) plusauction_id(join key to the per-bidder auction dataset;nonesentinel).tinybird.access_enabledgate. Additive schema evolution with JSONPaths + FORWARD_QUERY, checked withtb --cloud deploy --check.Why it matters
This is the overlap proof: a client-side wrapper cannot dispatch until the browser boots (t~3000ms on measured prospect pages); the server-side auction dispatches while the origin fetch is in flight. One access row then reads as a timeline (dispatch at t=D, resolve at t=R, commit at t=C, headers at t=H), with
R - Djoining per-bidder detail viaauction_id, andauction_wait_msfinally interpretable next to it:(R - D) - auction_wait_msapproximates how much of the auction was absorbed by work the request needed anyway.Update: implementation is included in this PR (per owner direction), as separate commits on top of the spec: the three marks on
RequestTimings, the publisher call sites, the four row columns, and the datasource evolution (validated withtb --cloud deploy --check; the FORWARD_QUERY triggers a backfill at promotion, acceptable at current volume and required for thenonesentinel on pre-existing rows). All CI gates pass locally.🤖 Generated with Claude Code