Skip to content

Trace winning auction bids to rendered creatives on the page#922

Open
prk-Jr wants to merge 3 commits into
mainfrom
trace-auction-winners-to-creatives
Open

Trace winning auction bids to rendered creatives on the page#922
prk-Jr wants to merge 3 commits into
mainfrom
trace-auction-winners-to-creatives

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Verifying that a winning server-side auction bid actually rendered on the page was impossible: no TS-owned identifier survived from the auction to the DOM on either render path. This PR mints a trace tuple — (auction_id, slot_id, bidder, bid_id, crid, adm_hash) — server-side and carries it end-to-end.
  • Server-side ad templates (creative_opportunities) and SPA /__ts/page-bids now inject hb_auction_id/hb_crid/hb_adm_hash per slot; /auction responses carry bid.ext.ts = {auction_id, adm_hash} and pass through the upstream crid/bid id (with the legacy synthetic fallback when absent).
  • The client stamps rendered slot elements and iframes with data-ts-* attributes, keeps a per-slot registry at window.tsjs.renders, and fires a tsjs:adRendered CustomEvent — so a creative in the DOM joins back to the server-side auction winner: / auction delivered creative: log lines by inspection, and e2e tooling can await and assert renders. The adm_hash (16-hex SHA-256 prefix of the delivered markup) enables byte-exact content verification on the /auction iframe path and the inject_adm_for_testing path.

Changes

File Change
crates/trusted-server-core/src/auction/types.rs Add Bid.crid and Bid.bid_id (serde-default, skip-if-none); add adm_trace_hash() and Bid::creative_trace_hash() with tests
crates/trusted-server-core/src/auction/mod.rs Re-export adm_trace_hash
crates/trusted-server-core/src/auction/orchestrator.rs Log one auction winner: trace line per winning bid on the sync (run_auction) and dispatched-collect paths
crates/trusted-server-core/src/auction/formats.rs /auction bids carry ext.ts = {auction_id, adm_hash} (hash of post-sanitize/rewrite markup), pass through upstream crid/bid id with legacy fallback; auction delivered creative: info log; tests
crates/trusted-server-core/src/publisher.rs build_bid_map emits hb_auction_id/hb_crid/hb_adm_hash; auction ID threaded through write_bids_to_state (both streaming call sites) and handle_page_bids; debug_bid gains bid_id/crid; tests
crates/trusted-server-core/src/integrations/prebid.rs Extract crid and bid id from PBS bid responses; tests
crates/trusted-server-core/src/integrations/adserver_mock.rs Restore crid/bid_id from the original SSP bid when parsing mediation responses
crates/trusted-server-core/src/integrations/aps.rs New Bid fields set to None (APS carries no creative pre-decode)
crates/trusted-server-core/src/auction/telemetry.rs Test fixture updated for new Bid fields
crates/trusted-server-js/lib/src/core/trace.ts New: window.tsjs.renders registry, data-ts-* stamping (stale attributes removed on re-render), tsjs:adRendered event
crates/trusted-server-js/lib/src/core/auction.ts parseAuctionResponse retains auctionId (bid ext.ts first, top-level id fallback) and admHash
crates/trusted-server-js/lib/src/core/request.ts renderCreativeInline stamps container + iframe and records renders (including rendered:false for rejected/missing-slot)
crates/trusted-server-js/lib/src/core/types.ts AuctionBidData trace fields, RenderRecord, TsjsApi.renders, debug-bid mirror fields
crates/trusted-server-js/lib/src/integrations/gpt/index.ts slotRenderEnded stamps + records; pbRender bridge records debug-adm/pbs-cache serves with the slot element captured at message-receipt time (SPA-navigation safe); debug adm iframe marked
crates/trusted-server-js/lib/test/core/trace.test.ts New: registry, count/overwrite, event, stamping, stale-attribute removal
crates/trusted-server-js/lib/test/core/auction.test.ts Auction-ID retention and ext.ts precedence tests
crates/trusted-server-js/lib/test/core/request.test.ts Stamping/registry/event tests, rejected-render marker test
crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts slotRenderEnded stamping/registry test incl. empty-render and re-render count

Closes

Closes #920

Test plan

  • cargo test-fastly && cargo test-axum (also cargo test-cloudflare && cargo test-spin and the integration parity suite)
  • cargo clippy-fastly && cargo clippy-axum (also clippy-cloudflare, clippy-cloudflare-wasm, clippy-spin-native, clippy-spin-wasm)
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run (415 passed)
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: new unit tests for adm_trace_hash, bid-map trace fields, /auction ext.ts/crid/bid-id passthrough, PBS crid/bid-id extraction, and JS trace registry/stamping on both render paths

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses tracing macros (not println!)
  • New code has tests
  • No secrets or credentials committed

Mint a trace tuple (auction_id, slot_id, bidder, bid/creative IDs,
adm_hash) server-side and carry it end-to-end on both render paths so a
creative in the DOM can be joined back to the winning bid in server
logs.

Server: Bid gains crid and bid_id (extracted from PBS, restored through
the mock mediator); adm_trace_hash() computes a 16-hex SHA-256 prefix of
the creative markup; build_bid_map emits hb_auction_id/hb_crid/
hb_adm_hash for SSAT and SPA page-bids; /auction responses carry
bid.ext.ts = {auction_id, adm_hash} and pass through upstream crid and
bid id with the legacy synthetic fallback; the orchestrator logs one
"auction winner:" line per winning bid on both the sync and dispatched
collect paths.

Client: new core/trace.ts writes a per-slot render registry to
window.tsjs.renders, stamps slot elements and iframes with data-ts-*
attributes (removing stale ones on re-render), and fires a
tsjs:adRendered CustomEvent. renderCreativeInline, the GPT
slotRenderEnded handler, and the pbRender bridge all record and stamp;
the bridge captures the slot element at message-receipt time so an SPA
navigation during a PBS Cache fetch cannot stamp the new route's slot
with stale data.
@prk-Jr prk-Jr self-assigned this Jul 16, 2026
ChristianPavilonis added a commit that referenced this pull request Jul 17, 2026
…tives

# Conflicts:
#	crates/trusted-server-core/src/auction/formats.rs
#	crates/trusted-server-core/src/auction/orchestrator.rs
#	crates/trusted-server-core/src/auction/telemetry.rs
#	crates/trusted-server-core/src/auction/types.rs
#	crates/trusted-server-core/src/integrations/adserver_mock.rs
#	crates/trusted-server-core/src/integrations/aps.rs
#	crates/trusted-server-core/src/integrations/prebid.rs
#	crates/trusted-server-core/src/publisher.rs
#	crates/trusted-server-js/lib/src/core/auction.ts
#	crates/trusted-server-js/lib/src/core/request.ts
#	crates/trusted-server-js/lib/src/core/types.ts
#	crates/trusted-server-js/lib/src/integrations/gpt/index.ts
prk-Jr added 2 commits July 18, 2026 09:51
Introduce an operator-armed render-trace overlay so the winning auction
recorded in window.tsjs.renders can be confirmed visually on the page,
on both the SSAT/GAM and /auction paths.

Server: GET /_ts/trace toggles a host-only ts-trace cookie and redirects
to /, gated by the new [debug] trace_route_enabled flag (404 when off).
Registered on all four adapters (Fastly/Cloudflare/Axum/Spin) and
documented in trusted-server.example.toml.

Client: while the cookie is armed, TSJS draws a floating
Google-Publisher-Console-style panel summarising every traced slot and a
confirmation badge on each genuinely-rendered creative. The panel reports
honest per-slot status — ok / hidden / gam-only / empty — derived from
separate signals (gamEmpty from GAM's slotRenderEnded, injected = whether
TS actually placed the creative, visible = ancestor-aware visibility) so
it never overclaims a render GAM merely fired. Clicking a row copies the
full record; the badge is shown only on ok slots.
The prod page runs two independent auctions against the same placements:
the one-time SSAT auction on navigation, and an ongoing client-side
/auction driven by GAM refresh through the Prebid.js trustedServer
adapter. Only the SSAT path was traced, so every Prebid render was
invisible to the overlay.

Instrument the /auction path at its authoritative render signal. The
adapter now forwards the server-side trace tuple to Prebid as
meta.tsAuctionId / meta.tsAdmHash, and a bidWon listener records an
`auction`-path entry (servedFrom: prebid) keyed by the ad-unit code with
that auction's own ID, hash and visibility. Only bids carrying
meta.tsAuctionId — i.e. the trustedServer seat — are traced, so a
client-side bidder's render is never attributed to Trusted Server. The
listener only observes and stamps; it does not touch Prebid's rendering.

Also fix an overclaim in the SSAT path: with inject_adm_for_testing off
there is no adm to inject, which left `injected` unset and let
panelStatus fall through to `ok` — claiming a confirmed TS render for a
slot TS had merely targeted. Targeting-only renders now report
`injected: false`, and `ok` requires an explicit confirmed placement so
any future path that omits the signal degrades to gam-only rather than
silently claiming success.
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.

Design how to trace winning TS auction to corresponding creatives on the page

1 participant