Multiple branches on rc/july (DO NOT MERGE)#931
Draft
prk-Jr wants to merge 19 commits into
Draft
Conversation
Adds GET /_ts/admin/ec/{id} (explicit EC ID) and GET /_ts/admin/ec
(EC ID from the caller's ts-ec cookie) so operators can inspect EC
identity graph entries and debug KV-to-auction EID propagation.
The core handler returns the stored KvEntry verbatim (including raw
consent strings and partner UIDs), the KV metadata mirror, the store
generation marker, and a derived auction view showing exactly which
EIDs the auction would attach and why each stored partner ID was
skipped (empty_uid, not_in_registry, bidstream_disabled). Corrupt
entries are returned with the parse error and raw body via the new
KvIdentityGraph::lookup_raw instead of failing closed.
The routes join Settings::ADMIN_ENDPOINTS so startup validation
rejects configs whose basic-auth handler regex does not cover them.
The EC identity graph is Fastly KV backed, so the Axum, Cloudflare,
and Spin adapters register the routes to local 501 responses, keeping
them off the publisher fallback that would forward the Authorization
header to the origin.
Closes #921
The dispatch arm reused EcRequestState::kv_graph, which is deliberately None for clients that fail the browser gate. Operators hit this auth-gated endpoint with curl, so every lookup returned 501 as if no EC store were configured. Build the identity graph directly from settings instead, and document why the bot-gated copy must not be used. Also point the bare-route no-cookie 404 at the explicit-id route, since the ts-ec cookie (Domain-scoped, Secure) cannot exist on localhost.
Adds GET /_ts/admin/eids, complementing the EC lookup endpoint with the client-side half of EID propagation: it decodes the request's ts-eids and sharedId cookies and previews what cookie ingestion would write into the EC entry's ids map — matched partner UIDs (deduplicated exactly like the ingestion path) and unmatched sources that would be dropped. The endpoint always responds 200; missing or malformed cookies are reported in the payload rather than as errors. It is pure request inspection with no KV access, so every adapter serves the real handler. The path joins Settings::ADMIN_ENDPOINTS for basic-auth coverage validation.
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.
Review feedback on the admin EC lookup asked for readable dates. The echoed entry now carries derived created_iso and consent.updated_iso fields (yyyy-MM-ddTHH:mm:ss.SSSZ) next to the stored unix-seconds values, which stay untouched so the echo remains faithful to KV.
…es' into rc/july_admin_ec_route # Conflicts: # crates/trusted-server-adapter-axum/src/app.rs # crates/trusted-server-adapter-cloudflare/src/app.rs # crates/trusted-server-adapter-spin/src/app.rs # crates/trusted-server-js/lib/src/core/request.ts # crates/trusted-server-js/lib/src/integrations/gpt/index.ts
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.
…ec_route # Conflicts: # crates/trusted-server-js/lib/src/integrations/gpt/index.ts # crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts
A client framework can replace the ad divs after GPT slots were bound to them: the publisher's React app serves ids like `ad-header-0-_R_ssr_` and swaps them for client ids (`ad-header-0-_r_1_`) during hydration. GPT is left holding slots whose element no longer exists — GAM reports "defineSlot was called without a corresponding DIV", still fetches a creative for them, and the bid is silently wasted with nowhere to render. Waiting for the divs to merely exist cannot help, because at adInit() time the server-rendered divs are present and are only later replaced. So rather than delaying the initial ad request, detect the swap after the fact: a debounced MutationObserver armed after adInit() looks for TS-defined slots whose element left the document and re-runs adInit(), which destroys the orphans and re-binds against the live DOM — reusing the publisher's own slot for that div when they have since defined one. Bounded deliberately, since each re-bind re-requests the affected slots: a 250ms quiet period, a 5s watch window (measured: the swap lands ~2-3s in, so the existing 2s SPA wait would miss it), and at most two re-binds per page load, with the attempt budget shared across the adInit() the watcher itself triggers so it cannot loop. Verified against the live page through the dev proxy: two orphaned slots were detected and re-bound, leaving zero orphans, with all three slots tracing to live client-id elements.
The panel collapsed each slot to a single row with a ×N counter, so a publisher page that refreshes its slots on every render (autoblog's ad-service refreshes from its own slotRenderEnded handler) showed a climbing number instead of what actually happened. Keep an append-only `window.tsjs.renderLog` alongside the per-slot `renders` registry and render it newest-first, one entry per render, with a wall-clock time and a `#N` sequence. The log is trimmed to the most recent entries so a page that refreshes indefinitely cannot grow it without bound; `renders` still collapses per slot for "did this ever render" checks. Also badge every slot that actually shows a creative, not just confirmed TS renders. Gating the badge on `ok` meant production — where inject_adm_for_testing is off and TS only applies GAM targeting — never displayed one at all, since every slot is honestly `gam-only`. The badge now carries its status colour and mark: green ✓ for a confirmed TS render, blue ◐ for gam-only. Slots with nothing on screen (`empty`) or nothing visible (`hidden`) stay unbadged, as there is no creative to label.
The server-side auction runs once per navigation, but the slotRenderEnded handler read the winning bid out of window.tsjs.bids on every render. That map never changes, so each publisher-driven GAM refresh re-stamped the page-load auction's id, bidder and adm hash and labelled itself `ssat` — claiming a render the server-side auction never produced. A slot refreshed six times over a minute showed six `ssat` rows all pointing at one auction. Scope the claim to the render that actually consumes it: adInit arms a per-slot flag when it applies bid targeting, and the first slotRenderEnded clears it. Later renders are recorded as `gam-refresh` with the stale attribution dropped from both the record and the DOM markers, since GAM re-requested the slot on its own and the returned creative cannot be traced to any Trusted Server auction. These rows are where the client-side /auction path will report real attribution via Prebid's bidWon once that bundle ships; labelling them `ssat` hid that gap instead of showing it. The /auction recording path is unchanged.
Three trace-panel fixes, all surfaced by the gam-refresh rows the previous commit introduced: - Restore the `gam:filled`/`gam:empty` marker on gam-refresh rows. It was gated on `path === 'ssat'`, which hid GAM's own fill signal on exactly the rows where "did GAM fill it this time" is the whole question. - Stop rendering absent attribution as `? · ?` and `auction ?`. An unattributed GAM refresh carries no bidder/hash/auction id by design, so the row now says `no TS attribution` and drops the auction segment rather than looking like a failed lookup. - Give each render a page-global `seq`, shown as `#N` on both the panel row and the on-creative badge so the two point at each other, and mark the row still live for its slot as `◂ current`. The per-slot render count keeps its own `×N`. seq is module-scoped, not stored on window.tsjs, so a re-executed bundle restarts the sequence instead of handing two renders one number.
The visible row text already says "no TS attribution" for a gam-refresh, but the badge title and row hover tooltip still rendered the same absent fields as `?`, which reads like a failed lookup rather than "there is nothing to attribute here by design". Switch both to `—`, matching the `gam_empty ?? '—'` convention the row tooltip already used elsewhere in the same list.
The pbRender bridge has two branches for serving a winning SSAT bid into GAM's Universal Creative: fetch from PBS Cache, or use `bid.adm` directly when present (added by the SSAT inline-creative work, and the only path production actually exercises for bidders that carry markup inline). The PBS Cache branch calls recordBridgeRender after replying; the inline-adm branch replied, fired win/billing beacons, and logged success, but never called it — so this render path, the strongest confirmation signal SSAT has (TS supplies the exact bytes GAM's own creative asked for by name), was invisible to the trace panel. Every SSAT row capped at gam-only even when this branch was serving real creative. Add the missing call, matching the PBS Cache branch's placement. Add regression coverage on both branches — neither had a trace assertion before, so this gap could recur silently on either one.
…re absent Confirmed live on autoblog: Kargo's response carries neither a Prebid Cache UUID nor an `adid`, so hb_adid was omitted for every SSAT bid from that bidder. That broke the pbRender bridge's reverse lookup (adId -> hb_adid) for GAM's Universal Creative postMessage protocol — verified in the browser that GAM sends real 'Prebid Request' messages with real adIds on this account, but window.tsjs.bids[slot].hb_adid was undefined for all three winning bids, so the bridge could never match any of them. Confirmed rendering (injected: true) was structurally unreachable for this bidder regardless of what GAM did. bid.bid_id (the OpenRTB bid's own `id`, always present per spec) already flows through the pipeline unused for this purpose. It is unique per bid instance rather than a creative identifier, but that is exactly what hb_adid needs here: a stable value GAM's Universal Creative echoes back so the bridge can find the winning bid. Add it as the last-resort fallback, after cache_id, the APS renderer's bid id, and ad_id — all three still take priority where present, locked in by test.
On the SSAT proxy path the browser calls /auction against the trusted-server edge domain (e.g. ts.example.com), which was leaking into ext.trusted_server.request_host on the outbound Prebid Server request. That field must track the publisher's own domain instead, matching site.domain/publisher.domain and what PBS's trusted_server verification module expects. (cherry picked from commit 5d7f696)
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.
Summary
/_ts/adminendpoints — an EC lookup by id (returning the KV graph, with ISO 8601 companions alongside epoch timestamps) and a request EID cookie echo — so identity issues can be diagnosed without shell access to the store.auction_id+adm_hashper winning bid (landed inrc/july), but nothing confirmed those creatives reached the page. This adds the client half: awindow.tsjs.renders/renderLogregistry fed by both render paths,data-ts-*markers on the slot element, and a/_ts/traceroute that arms a floating overlay showing each render as a timeline entry. A winning bid can now be followed from the server log to the pixels.Changes
core/src/ec/admin.rscore/src/ec/kv.rs,ec/mod.rs,ec/prebid_eids.rscore/src/trace_cookie.rsGET /_ts/tracearms/disarms the overlay via a host-only cookie, then 302s back to/. Gated ondebug.trace_route_enabled.core/src/settings.rsdebug.trace_route_enabled(defaults off) and admin endpoint config.core/src/constants.rs,core/src/lib.rsadapter-{fastly,cloudflare,axum,spin}/src/app.rsadapter-{axum,cloudflare,spin}/tests/routes.rsjs/lib/src/core/trace.tsrenderLog, floating overlay panel, on-creative badge,isEffectivelyVisibleancestor walk.js/lib/src/core/types.tsRenderRecord(incl.gamEmpty/injected/visible),pathunion,TsjsApiadditions.js/lib/src/core/request.ts/auctioniframe renders.js/lib/src/integrations/gpt/index.tsslotRenderEndedtracing; orphaned-slot recovery; scopes SSAT attribution to a single render.js/lib/src/integrations/prebid/index.tsbidWon/adRenderSucceededtrace hook; forwardstsAuctionId/tsAdmHashthrough bid meta.js/lib/test/**trusted-server.example.tomltrace_route_enabled, off by default.Notes for review
Three behaviours worth a careful look, since all three are about not overclaiming:
ok/hidden/gam-only/emptyrather than a single success mark.rendered(GAM said non-empty),injected(TS actually placed the markup) andvisible(the box is on screen) are tracked separately — a non-empty GAM slot is not proof the TS creative rendered, and the earlier version claimed it was.window.tsjs.bidsis frozen page-load data, so reading it on everyslotRenderEndedmade each publisher-driven refresh re-stamp a long-finished auction. Only the render consuming the targetingadInitapplied is labelledssat; later ones aregam-refreshwith the stale tuple dropped.trace_route_enableddefaults tofalse, the cookie is host-only, and the badge only renders for statuses that are actually confirmed.Test plan
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest run(505 passing)cd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatfastly compute serve+ts dev proxy2 TS slot(s) orphaned by a DOM re-render; re-binding).Not yet verifiable in production: the
/auctionrender rows. That hook compiles into the external Prebid bundle, which is served from R2 and does not ship with a deploy, so it needs a separate bundle rebuild + upload before those rows appear.Checklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)