Skip to content

Implement server-side ad slot templates with PBS and APS auction#680

Open
prk-Jr wants to merge 128 commits into
mainfrom
server-side-ad-templates-impl
Open

Implement server-side ad slot templates with PBS and APS auction#680
prk-Jr wants to merge 128 commits into
mainfrom
server-side-ad-templates-impl

Conversation

@prk-Jr

@prk-Jr prk-Jr commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds server-side ad slot templates under [creative_opportunities] in trusted-server.toml. Matching slots are selected from the incoming document URL at the edge, and window.tsjs.adSlots is injected at <head> open so initial GPT setup does not need a separate slot-discovery request.
  • Runs the server-side auction in parallel with the origin fetch for eligible document navigations. Configured providers such as Prebid Server and APS race under the creative-opportunity auction deadline; winning bids are price-bucketed and injected as window.tsjs.bids before </body>.
  • Adds the window.tsjs.adInit GPT runtime path. It reads window.tsjs.adSlots and window.tsjs.bids synchronously, defines or reuses GPT slots, applies slot-level and hb_* targeting, sets ts_initial=1, refreshes the initial slots, and fires nurl/burl only after slotRenderEnded confirms the TS bid won via hb_adid.
  • Adds PBS stored-request fallback keyed by slot ID when no inline PBS bidder params are present, filters non-PBS provider keys from PBS requests, and keeps bidder-param override rules for per-bidder/zone params.
  • Adds per-bidder PBS win/billing suppression with [integrations.prebid].suppress_nurl_bidders, while retaining the deployment-wide [integrations.prebid].suppress_nurl compatibility switch.
  • Improves the deferred slim-Prebid refresh path so GPT refresh auctions derive ad unit sizes and zone from injected window.tsjs.adSlots / GPT metadata instead of placeholder refresh sizes.
  • Implements EID forwarding for the server-side auction path: reads the ts-eids cookie written by TSJS, gates EIDs by consent, and forwards them as OpenRTB user.ext.eids to PBS.
  • Forwards real client IP and geo from Fastly ClientInfo into the server-side PBS request so bidders see the browser client context rather than the Fastly edge context.
  • Keeps SPA/CSR route changes covered by the existing GET /__ts/page-bids hook, which updates window.tsjs.adSlots / window.tsjs.bids and re-runs window.tsjs.adInit() after pushState, replaceState, or popstate navigations.

What the server-side auction sends to PBS

Field Value Source
user.id EC ID ts-ec cookie or generated EC identity
user.consent / user.ext.consent TCF v2 string when available consent cookies / request consent extraction
user.ext.eids EID array, consent-gated ts-eids cookie plus EC identity resolution
user.ext.ec_fresh fresh EC flag EC context
device.ua user agent User-Agent header
device.ip real client IP Fastly ClientInfo.client_ip
device.geo city/country/region/lat/lon/metro/asn Fastly geo lookup
device.dnt true if set DNT: 1 header
device.language primary language tag Accept-Language header
site.domain / site.page publisher domain + full URL request host/path/scheme
site.ref referrer Referer header
regs.gdpr / regs.us_privacy / regs.gpp privacy signals consent extraction
imp.* slots, formats, bidder params, floors [creative_opportunities] slot templates and Prebid config
tmax auction timeout ms [creative_opportunities].auction_timeout_ms, falling back to [auction].timeout_ms

Changes

File / area Change
trusted-server.toml Adds [creative_opportunities] slot templates and documents Prebid nurl suppression knobs.
.env.example / docs Documents Prebid bidder override env vars and SUPPRESS_NURL_BIDDERS.
crates/trusted-server-core/src/creative_opportunities.rs Defines slot-template config, URL glob matching, slot-to-auction conversion, provider params, and slot ID validation helpers.
crates/trusted-server-core/src/settings.rs / build.rs Wires creative opportunities into settings and build-time slot ID validation from trusted-server.toml.
crates/trusted-server-core/src/publisher.rs Matches slots, dispatches server-side auctions, injects window.tsjs.adSlots and window.tsjs.bids, applies cache-control safeguards, handles page-bids responses, and forwards client context.
crates/trusted-server-core/src/html_processor.rs Adds head-open and bounded body-close injection points with a guard against duplicate tsjs.bids injection.
crates/trusted-server-core/src/auction/* Extends auction types, request parsing, provider orchestration, floor filtering, diagnostics, and provider response handling.
crates/trusted-server-core/src/integrations/prebid.rs Adds OpenRTB enrichment, stored-request fallback, EID forwarding, bidder override rules, PBS cache fields, nurl/burl propagation, and per-bidder suppression.
crates/trusted-server-core/src/integrations/aps.rs Adds APS/TAM provider support.
crates/trusted-server-core/src/integrations/adserver_mock.rs Adds mock mediation support with decoded provider prices.
crates/trusted-server-core/src/integrations/gpt.rs / gpt_bootstrap.js Adds the head-injected window.tsjs.adInit bootstrap path.
crates/js/lib/src/integrations/gpt/index.ts Implements the browser GPT path for window.tsjs.adSlots, window.tsjs.bids, render-confirmed beacon firing, SPA updates, slim-Prebid loading, and Prebid creative rendering bridge.
crates/js/lib/src/integrations/prebid/index.ts Adds deferred Prebid integration, user ID module/EID cookie sync, client-side bidder support, and metadata-derived refresh auctions.
crates/trusted-server-adapter-fastly/src/main.rs Wires auction/page-bids routes and publisher handler inputs into the Fastly adapter.

Closes

Closes #677
Closes #697
Closes #698
Closes #699
Closes #700
Closes #702

Test plan

Automated

  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • git diff --check
  • JS build: cd crates/js/lib && node build-all.mjs
  • JS lint: cd crates/js/lib && npm run lint
  • JS format: cd crates/js/lib && npm run format
  • Docs format: cd docs && npm run format
  • GitHub Vitest check passes on the current PR head
  • Local cd crates/js/lib && npx vitest run currently fails before test discovery in this workspace with ERR_REQUIRE_ESM from html-encoding-sniffer requiring @exodus/bytes/encoding-lite.js; no local JS test assertions execute under that failure mode.

Manual end-to-end (browser DevTools console)

The steps below build on each other. Use a URL whose path matches one of the configured [[creative_opportunities.slot]] page_patterns.

Step 1 - Verify slot config is injected at <head> open

window.tsjs?.adSlots

Expected: an array of slot objects. Each entry has id, gam_unit_path, div_id, formats, and targeting. Note the div_id value from one matching slot for step 3.

Step 2 - Verify server-side auction results are injected before </body>

window.tsjs?.bids

Expected: an object keyed by slot ID. Winning slots include hb_bidder, hb_pb, and, for Prebid cache-backed bids, hb_adid / cache fields.

Step 3 - Verify window.tsjs.adInit wired GPT targeting

Replace SLOT_DIV_ID with the div_id from step 1.

googletag
  .pubads()
  .getSlots()
  .filter((slot) => slot.getSlotElementId() === 'SLOT_DIV_ID')
  .map((slot) => ({
    path: slot.getAdUnitPath(),
    targeting: slot.getTargetingMap(),
  }))

Expected: the slot has the configured GAM unit path and targeting includes hb_pb, hb_bidder, any slot-level keys such as pos / zone, and ts_initial: ["1"].

Step 4 - Verify slot matching is page-pattern-aware

Navigate to a different configured path, for example / when homepage slots are configured, and repeat step 2.

Expected: window.tsjs.bids is keyed by the slots matching that page, not by slots from the previous page type.

Step 5 - Confirm no duplicate bids injection

View page source and search for .bids=JSON.parse.

Expected: exactly one window.tsjs.bids assignment before </body> on pages where an auction ran.

Pending (GAM line items required)

Creative delivery requires standard GAM line items targeting hb_pb, hb_bidder, and related Prebid keys. That setup is outside this PR.

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code
  • Uses log macros instead of println!
  • New code paths have Rust and/or TS test coverage
  • No secrets or credentials intentionally committed

jevansnyc and others added 26 commits April 15, 2026 20:47
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Incorporate all review feedback (aram356 + jevansnyc): cache contract,
  consent/GDPR gating, async restructuring detail, CreativeOpportunityFormat
  schema, glob pattern fix, XSS escaping, win notifications, APS params,
  timeout config key, defineSlot fix, gpt.rs ownership, KV migration path,
  Phase 2 sketch
- Fix Prettier formatting (format-docs CI)
- Add implementation plan (12 tasks, TDD, ordered by dependency)
- Incorporate all review feedback (aram356 + jevansnyc): cache contract,
  consent/GDPR gating, async restructuring detail, CreativeOpportunityFormat
  schema, glob pattern fix, XSS escaping, win notifications, APS params,
  timeout config key, defineSlot fix, gpt.rs ownership, KV migration path,
  Phase 2 sketch
- Fix Prettier formatting (format-docs CI)
- Add implementation plan (12 tasks, TDD, ordered by dependency)
Replace the head-injected __ts_bids design with a server-cached bid
delivery model fetched by the client via a new /ts-bids endpoint. The
auction never blocks page rendering — </head> flushes immediately, body
parses without waiting for bids, and the client fetches bids in parallel
with content paint.

Key changes:
- §2 Goal: bid delivery decoupled from page rendering; FCP unchanged from
  no-TS baseline
- §4.3 Auction Trigger: drop buffered/streaming dichotomy; single mode
  forces chunked encoding on all origins (WordPress, NextJS, etc.)
- §4.4 Head Injection: only __ts_ad_slots and __ts_request_id injected at
  <head> open; bid results moved to /ts-bids endpoint
- §4.6 Client Residual: __tsAdInit defines slots immediately, fetches
  bids via /ts-bids, applies targeting and fires refresh() after resolve
- §4.7 (new) Caching Behavior: explicit cacheability table for HTML, JS,
  CSS, tsjs bundle, bid results; Fastly edge HTTP cache leveraged for
  origin HTML
- §5 Request-Time Sequence: full mermaid diagram covering content +
  creative + burl flow with cache-hit and cache-miss branches; separate
  text sequences for cache-hit (~80ms FCP, ~900ms ad-visible) and
  cache-miss (~250ms FCP, ~1,050ms ad-visible)
- §6 Performance Summary: cache-hit and cache-miss columns; FCP added
  as a tracked metric
- §7 Implementation Scope: add bid_cache.rs, /ts-bids endpoint, force
  chunked encoding step
- §8 Edge Cases: origin-agnostic entries; new entries for /ts-bids 404
  and client-never-fetches-/ts-bids

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pivot from the /ts-bids fetch endpoint + in-process bid_cache design to
inline __ts_bids injection before </body>. The earlier design relied on
shared state that doesn't reliably survive Fastly Compute's per-request Wasm
isolate model — body injection achieves the same FCP property in a single
response with no shared-state requirement.

Key changes:

- §4.3: replace /ts-bids long-poll with bounded </body> hold tied to
  A_deadline. Body content above </body> paints first; close-tag held
  until auction completes or A_deadline fires (graceful __ts_bids = {}
  fallback).
- §4.3: add auction-eligibility gating (consent, bot UA, prefetch hints,
  HEAD method, slot match) so auctions fire on real first-page-load
  impressions only.
- §4.4: replace __ts_request_id + /ts-bids machinery with two inline
  <script> blocks — __ts_ad_slots at <head> open, __ts_bids before
  </body> via lol_html el.on_end_tag().
- §4.5: move both nurl and burl to client-side firing from
  slotRenderEnded after hb_adid match. Server-side firing rejected to
  avoid billing inflation on bids that never render.
- §4.6: replace fetch+Promise pattern with synchronous __ts_bids read.
  Add lazy slim-Prebid loader (post-window.load) for scroll/refresh
  auctions and Phase B identity warm-up. Add ts_initial=1 slot-ownership
  sentinel.
- §4.7: switch Cache-Control from private, no-store to private,
  max-age=0 to preserve browser BFCache eligibility while still
  preventing intermediate-cache leaks.
- §4.8 (new): document the EC/KV identity model as load-bearing auction
  input — Phase A retrieval at request time, Phase B post-render
  enrichment via slim-Prebid userID modules. Add bare-EC first-impression
  caveat and auction_eid_count metric. Note federated-consortium
  passphrase property and clickstream-compounding speed win.
- §5: update mermaid + cache-hit/miss timelines for bounded body hold;
  ad-visible converges to ~870ms (hit) / ~1,020ms (miss).
- §6: drop /ts-bids RTT row; add DCL row; add clickstream-compounding,
  TS-overhead, identity-coverage, and confidence-interval framing.
- §7: drop bid_cache.rs and /ts-bids endpoint from scope; add
  auction-eligibility gating and slim-Prebid bundle build target. Add
  explicit "Deleted" subsection.
- §8: drop /ts-bids edge cases; add SPA/pushState, bare-EC, bot/prefetch,
  HEAD, BFCache restoration cases.
- §9.6: server-side GAM downgraded from "Phase 2 commitment" to
  aspirational and contingent on Google agreement. §9.8 (slim-Prebid
  bundle composition), §9.9 (Privacy Sandbox), §9.10 (per-bidder consent)
  added as follow-ups.

Implementation plan at docs/superpowers/plans/2026-04-30-server-side-ad-templates.md
is now stale relative to this spec; needs regenerating before code lands.
…ities.toml

Adds the creative_opportunities field to Settings struct to deserialize
configuration for the server-side ad auction feature. Includes build.rs
stubs for types required during build-time configuration validation.

Creates creative-opportunities.toml with example slot configuration and
updates trusted-server.toml with the [creative_opportunities] section
defining GAM network ID, auction timeout, and price granularity settings.

Tests pass with proper TOML parsing of the creative_opportunities section.
…ared auction state

- Add `ad_slots_script: Option<String>` and `ad_bids_state: Arc<RwLock<Option<String>>>` fields to `HtmlProcessorConfig`
- Update `from_settings` to initialize both new fields with safe defaults
- Prepend `ad_slots_script` inside the existing `<head>` handler before integration inserts
- Add `element!("body", ...)` handler that uses `end_tag_handlers()` to inject `__ts_bids` before `</body>`; falls back to empty `{}` when auction state is `None`
- Add `IntegrationRegistry::empty_for_tests()` test helper
- Add three new tests covering all injection paths
…gibility gates; max-age=0

- Make handle_publisher_request async; add orchestrator and slots_file params
- Dispatch origin request with send_async before running auction in parallel
- Gate auction on GET, no prefetch, no bot, matched slots, TCF purpose-1 consent
- Run server-side auction and write bucketed bids to ad_bids_state Arc<RwLock>
- Compute ad_slots_script after response headers; set Cache-Control: private, max-age=0
- Fix Stream arm to thread actual ad_slots_script and ad_bids_state through
- Add build_auction_request, build_bid_map, build_bids_script, build_ad_slots_script helpers
- Update route_tests.rs to pass empty slots_file to route_request
- build_bid_map now returns serde_json::Map with full bid objects (hb_pb,
  hb_bidder, hb_adid, nurl, burl) instead of a plain CPM string map
- build_bids_script / build_ad_slots_script now emit full <script> tags
  using JSON.parse("…") for safe inline embedding; add html_escape_for_script helper
- build_ad_slots_script uses correct property names (gam_unit_path, div_id,
  formats, targeting) matching the client-side TSJS bundle expectations
- Replace map_or(false, …) with is_some_and(…) on lines 546, 549, 567
- Add # Panics doc sections to handle_publisher_request and create_html_processor
… from slotRenderEnded; slim-Prebid lazy loader
- Enable APS and adserver_mock in auction config; set providers and mediator
- Increase auction_timeout_ms from 500ms to 3000ms — 500ms was too tight
  for HTTPS round-trips to mocktioneer, leaving the mediator zero budget
- Fix mediation request: send numeric price instead of opaque encoded_price;
  mocktioneer requires a decoded price field and does not support encoded_price
- Expand creative-opportunities slot page_patterns to include /news/**
@prk-Jr prk-Jr self-assigned this May 6, 2026
Define SlotRenderEndedEvent, SlotRenderEvent, and TestWindow types to
eliminate all @typescript-eslint/no-explicit-any violations in
gpt/index.ts and gpt/index.test.ts. Extend GptWindow with
__tsjs_slim_prebid_url so installSlimPrebidLoader avoids the any cast.
@prk-Jr prk-Jr changed the title Implement server-side ad slot templates with APS auction Implement server-side ad slot templates with PBS and APS auction May 6, 2026
Set gam_network_id to 88059007 (autoblog production network). Update
atf_sidebar_ad slot to /88059007/autoblog/news with div_id
ad-atf_sidebar-0-_r_2_ (desktop ATF sidebar, 300x250); restrict
page_patterns to article paths only (/20**, /news/**) since that div
does not exist on the homepage. Add homepage_header_ad slot targeting
/88059007/autoblog/homepage with ad-header-0-_R_jpalubtak5lb_ for
970x90/728x90/970x250 leaderboard formats. Reduce auction_timeout_ms
from 3000 to 500 to cap TTFB at the spec-recommended ceiling.
prk-Jr added 8 commits June 16, 2026 09:26
finalize_response checked for lowercase "private"/"no-store" substrings, but
Cache-Control directives are case-insensitive (RFC 9111). A Cache-Control:
No-Store on a Set-Cookie response was treated as cacheable and downgraded to the
weaker private, max-age=0, and a Cache-Control: Private did not block operator
response_headers from re-enabling shared caching. Lowercase the header value
before matching. Add mixed-case No-Store / Private tests.
The comment recommends a 500ms default because the value bounds the
DOMContentLoaded/window.load slip, but the checked-in value was 1500ms, so a
first rollout that enables slots while inheriting the default would impose a
1.5s close-body hold on cache-hit pages. Set the sample default to 500ms.
Many two-decimal CPMs are not exactly representable in binary floating point:
0.29 * 100.0 is 28.999…, so flooring truncated it to 28 ("0.28"), and 1.15
became "1.14". These values feed hb_pb targeting keys, so the auction reported a
cent low. Convert CPM to whole cents through a helper that nudges values sitting
an ULP below a cent boundary up before flooring, leaving genuinely sub-cent
values (0.015 -> "0.01") untouched. Adds a float-boundary regression test.
run_parallel_mediation gave the mediator the full remaining auction budget,
while the dispatched collect path bounds it by remaining.min(mediator.timeout_ms()).
Apply the same cap for symmetry between the two paths.
should_run_auction is decided from request signals before the origin
content-type/status/encoding is known. A navigation that dispatched SSP bid
requests but then routes to PassThrough (2xx non-HTML) or BufferedUnmodified
(non-2xx, unsupported encoding, empty host) dropped the DispatchedAuction
without collecting it — wasted SSP quota with no visibility. Log a warning on
those arms when an auction was dispatched.
Lock in that a TRUSTED_SERVER__CREATIVE_OPPORTUNITIES__SLOT override with an
invalid id is rejected through from_toml_and_env, complementing the existing
TOML-path test and exercising the same validation the build-time check uses.
"/20**" is an invalid glob that only matches via the **->* normalization
fallback; using it as the canonical example invites copy-paste of broken config.
Show "/2024/*" as the primary example and keep the normalization note as the
edge-case caveat.
parse_ts_eids_cookie was gated to #[cfg(test)] and exercised only by its own
tests; production reads the ts-eids cookie through resolve_client_auction_eids
-> parse_prebid_eids_cookie (which enforces its own size/length caps). Remove
the function, its tests, and the now-orphaned cfg(test) imports/helpers.
@prk-Jr

prk-Jr commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

Re: review pullrequestreview-4499471630 — blocking body finding.

P1 — synchronous mediated auctions lose restored render/accounting fields. Fixed in c99bac8.

run_parallel_mediation now calls mediator.parse_response_with_context(platform_resp, response_time_ms, &mediator_context).await, matching the dispatched collect path, so nurl/burl/ad_id and PBS cache fields are restored on the synchronous POST /auction and /__ts/page-bids paths (the mediator context already carries the collected SSP provider_responses). Added a regression test, mediated_bid_preserves_restored_fields_through_run_auction, that registers a context-restoring mediator and asserts the winning bid keeps its nurl/ad_id through run_auction. The four inline findings (GPT display, build-time glob validation, case-insensitive Cache-Control, creative timeout default) are answered on their threads.

@prk-Jr

prk-Jr commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

Re: review pullrequestreview-4500409866 (second pass) — all items addressed.

Blocking

  • price_bucket under-buckets via float truncation — 3a5c4b4. 0.29 * 100.0 is 28.999…, so flooring truncated to "0.28". CPM→cents now goes through a helper that corrects the binary-float representation error before flooring, so 0.29"0.29" and 1.15"1.15" while genuinely sub-cent values still floor (0.015"0.01", 0.289"0.28"). Pure round-half was avoided because it would break Prebid floor semantics. Regression test added.
  • Sync mediation drops bid metadata — c99bac8 (details in the reply on the other review). The adserver_mock::parse_response comment is now accurate since both mediator paths use parse_response_with_context.

Non-blocking

  • Dispatched auction dropped on Buffered/PassThrough — 64ecc74. Added log::warn! on both arms when dispatched_auction.is_some(). Kept it to a warning (not await-and-discard): the SSP requests are already dispatched so the quota is spent, and those routes have no </body> to inject bids into, so awaiting only adds latency to non-HTML responses.
  • Sync mediator timeout cap — 0f241ca. Now remaining_ms.min(mediator.timeout_ms()), matching the collect path.
  • Env-injected slot-id rejection test — ac0add3. Asserts TRUSTED_SERVER__CREATIVE_OPPORTUNITIES__SLOT with an invalid id is rejected via from_toml_and_env; the build-time path uses the same validate_creative_slot.
  • /20** doc example — c324e09. Now /2024/*, with the normalization note kept as the edge-case caveat.

Inline / cleanup

  • parse_ts_eids_cookie dead code — bdb00f5. Removed the #[cfg(test)]-only function, its tests, and orphaned imports. Production parses the cookie through resolve_client_auction_eidsparse_cookie_auction_eidsparse_prebid_eids_cookie, which enforces its own size/length caps, so no behavior or hardening is lost.
  • APS provider Mutex state — deferred. Unlike adserver_mock (which derives its bid index from context.provider_responses), APS's slot_id_map derives from the AuctionRequest, which AuctionContext does not carry; the existing code comment documents this. It is single-threaded-safe (one request per Wasm instance), and migrating it cleanly needs an AuctionProvider/AuctionContext change to thread the request through — out of scope for this PR. Happy to track it as a follow-up.

CI: cargo fmt / clippy -D warnings / cargo test --workspace (1558) / vitest / wasm release all green.

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: I reviewed PR #680 at bdb00f5 against main. CI is green, but I found one blocking privacy/cacheability issue that should be fixed before merge: EC cookie writes happen after the cache-safety finalizer, so first-visit cookie-bearing responses can retain shared-cache headers. I also found two non-blocking but important issues around refresh bidder params and build-time/runtime config validation. Details are inline.

Comment thread crates/trusted-server-adapter-fastly/src/main.rs Outdated
Comment thread crates/js/lib/src/integrations/prebid/index.ts Outdated
Comment thread crates/trusted-server-core/build.rs
prk-Jr added 4 commits June 17, 2026 13:01
Resolve route_request signature conflict by keeping the creative-opportunity
slots parameter alongside main's mutable request binding. Align the merged-in
DataDome auction protection test with the branch's server-side auction consent
gate by resolving a non-regulated jurisdiction so the empty-provider auction
still reaches orchestration.
finalize_response applies the cookie cache-privacy downgrade on the
HttpResponse, but the EC identity cookie is written later by
ec_finalize_response onto the converted Fastly response. A first-visit
navigation whose only per-user payload is the EC cookie therefore kept any
public/surrogate cache headers from the origin or operator response headers,
so a shared cache could store and replay one visitor's EC cookie to others.

Re-apply the downgrade with enforce_set_cookie_cache_privacy after EC
finalization in both the buffered and streaming branches, mirror it in the
route test helper, and cover the first-visit ordering with route tests.
The synthetic refresh ad unit only carried the trustedServer bid with a zone,
so the requestBids shim had no original server-side bidder entries to collect
into bidderParams. Refresh/scroll /auction requests therefore sent {} for
inline PBS params and dropped demand the publisher configured only on the
initial ad unit.

Recover the matching original pbjs.adUnits server-side params by ad unit code —
from both raw bidder entries and params already folded onto the initial
trustedServer bid — and attach them to the synthetic refresh bid.
The build script types price_granularity as a String and slots as raw JSON
values, so values the runtime schema rejects — a price_granularity outside the
PriceGranularity enum (e.g. custom), or unknown slot keys under the slot's
deny_unknown_fields — embedded cleanly and then failed settings load on every
non-health request, turning a green build into a request-time outage.

Validate price_granularity against the real PriceGranularity enum and reject
unknown top-level slot fields in the shared build-check validator before the
merged config is embedded, with tests for both.

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: Reviewed PR #680 at 32de4aa against main, focusing on the current server-side ad-template, page-bids, consent/identity, and config paths. CI is green, but I found remaining blocking correctness/privacy issues in the publisher/page-bids flow: EC generation can bypass the adapter's real-browser gate, and /__ts/page-bids can still drive GPT slot creation/refresh when the kill switch or consent gate disables the server-side ad stack. I also left one medium config-safety comment.

CI checked: gh pr checks reports all current checks passing (fmt, clippy/analyze, cargo test, vitest, format docs/typescript, browser/integration tests, CodeQL). I reviewed existing comments and avoided re-reporting resolved historical findings; the page-bids finding is a remaining/incomplete edge of the earlier kill-switch/consent fixes.

Comment thread crates/trusted-server-core/src/publisher.rs Outdated
Comment thread crates/trusted-server-core/src/publisher.rs Outdated
Comment thread crates/trusted-server-core/src/creative_opportunities.rs
Stop handle_publisher_request from minting its own EC ID. EC generation
is the adapter's real-browser-gated responsibility; the duplicate inline
call re-ran for any navigation with no real-browser signal, so a
non-real-browser client could get an IP-derived EC minted in memory and
forwarded to PBS/APS even though the adapter blocked EC operations.

Gate /__ts/page-bids slot output on the effective ad-stack condition
(auction kill switch + consent), not just winning bids. Returning slots
while the stack is disabled let the SPA hook run adInit() and create or
refresh GPT slots client-side, defeating the kill switch. This matches
the publisher navigation path's should_run_server_side_ad_stack gate.

Add deny_unknown_fields to the top-level creative-opportunities config
and nested provider/format structs so misspelled keys fail at startup
instead of silently disabling or mis-timing the ad stack.

Add regression tests for all three and update the page-bids tests to
isolate the bot/prefetch variable from the consent gate.

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: reviewed PR #680 at f456b50 against main. CI is currently green, but I found blocking cache/privacy regressions around late Set-Cookie response effects and surrogate-cache headers, plus one medium refresh-demand correctness issue. Details are inline.

Comment thread crates/trusted-server-adapter-fastly/src/main.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/main.rs Outdated
Comment thread crates/js/lib/src/integrations/prebid/index.ts Outdated

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

🔧 Requesting changes on PR #680 at f456b506. CI is green, but I found two high-priority correctness/security issues and one medium config-safety issue in the server-side ad-template flow.

I avoided duplicating the latest existing inline comments on request-filter Set-Cookie cache privacy, no-store surrogate headers, and container-backed Prebid refresh recovery.

Findings submitted inline: 0 P0, 2 P1, 1 P2, 0 P3.

Comment thread crates/trusted-server-core/src/auction/orchestrator.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/main.rs
Comment thread crates/trusted-server-core/src/creative_opportunities.rs
Apply request-filter response effects before the final Set-Cookie cache
guard in every Fastly response path (buffered, streaming, asset
streaming) so a per-user cookie added by a DataDome allow can no longer
leave with public/surrogate cache headers. Strip surrogate cache headers
on every Set-Cookie response — even one keeping a stricter no-store
directive — and treat no-store as protected in the operator-header guard.

Reject OPTIONS /__ts/page-bids at the adapter so the side-effecting
endpoint never grants a CORS preflight the publisher origin might.

Drain every dispatched SSP request in the collect loop instead of
breaking on the auction deadline, so a slow origin can no longer discard
SSP responses that already arrived.

Reject empty/whitespace div_id overrides at runtime validation, which
would otherwise bind a slot to the first id-bearing DOM element.

Recover Prebid refresh params and client-side bids from candidate codes
([gpt element id, injected div_id]) so container-backed slots keep the
publisher's configured demand on refresh/scroll auctions.

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: I reviewed PR #680 at a3160d5 against main. CI is currently green, and many prior review issues have been fixed, but I found two remaining high-impact issues that should be addressed before merge: /auction can still forward client-provided EIDs when the current consent context forbids EC identity use, and TS-owned GPT slots can stay blank on pages that use GPT disableInitialLoad(). Details are inline.

Review Summary

Reviewed the current server-side ad templates implementation, focusing on consent/privacy, the /auction and /__ts/page-bids paths, GPT/Prebid client behavior, and existing review context. Overall risk is reduced compared with earlier revisions, but these two issues affect privacy/compliance and first-impression serving compatibility.

Findings

P0 / Blockers

None.

P1 / High

  1. /auction resolves and forwards client EIDs even when EC identity is not allowed — see inline.
  2. TS-owned GPT slots are only displayed, not refreshed, so pages using GPT disableInitialLoad() can get blank first impressions — see inline.

P2 / Medium

None included.

P3 / Low

None included.

CI / Existing Reviews

All current GitHub checks pass (cargo fmt/test, clippy/Analyze, vitest, integration/browser tests, CodeQL, docs/TS format). I checked prior review threads and avoided re-reporting issues that were already fixed or explicitly deferred.

Comment thread crates/trusted-server-core/src/auction/endpoints.rs
Comment thread crates/js/lib/src/integrations/gpt/index.ts
prk-Jr added 2 commits June 20, 2026 19:27
…tes-impl

# Conflicts:
#	crates/trusted-server-core/src/publisher.rs
Gate /auction client EID resolution on the same identity-consent condition
as the EC ID (`ec_id.is_some()`, already filtered by `ec_allowed()`).
Previously client-provided EIDs from the request body or ts-eids cookie were
resolved unconditionally, so a US/GPC or US-Privacy opt-out context — where
EC identity use is denied but a non-personalized auction may still run — could
forward persistent EIDs, since `gate_eids_by_consent` only strips on TCF/GDPR
signals. This matches the publisher and /__ts/page-bids paths.

Refresh TS-defined GPT slots when the publisher disabled initial load. With
pubads().disableInitialLoad(), display() only registers a freshly defined
slot and the ad request must come from refresh(); TS-owned first-impression
slots were only display()ed, so they rendered blank. A wrapper around
disableInitialLoad() records the state on window.tsjs, and adInit() refreshes
its own slots when it is set (bundle and gpt_bootstrap.js). The detector only
hooks an existing googletag stub so a plain import never touches window.googletag.

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: I reviewed PR #680 at bf76d41 against origin/main, including the Rust publisher/page-bids auction path, the GPT/Prebid browser code, config/build validation, CI status, and existing review threads. I did not find any new blocking correctness/security/data-loss issues. I left two non-blocking findings inline: one concrete build-time validation gap for creative-opportunity slot config, and one stale code-doc contract around SPA navigation ownership. CI is currently green across the reported PR checks.

mod glob {
pub struct Pattern;
impl Pattern {
pub fn new(_: &str) -> Result<Self, String> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: P2 — Build-time slot-pattern validation is ineffective in the build.rs context.

The new creative_slot_build_check::validate_creative_slot() path says it rejects invalid page_patterns before embedding config, but in build.rs the local glob::Pattern::new stub unconditionally returns Ok. That means a private/env slot such as page_patterns = ["["] passes the build-time check and is serialized into target/trusted-server-out.toml; the real runtime loader later uses the actual glob crate, drops/rejects the pattern during Settings::from_toml, and can fail service startup/request settings load instead of failing the build. This also makes the build-time tests misleading because they run against the real crate dependency, not this stub.

Suggested fix: avoid stubbing glob for this path (depend on the real glob crate from the build script, or move the pattern-validity check into a small helper that build.rs can compile with the real dependency). Add a build-context regression test or an integration-style check that an invalid env-injected slot pattern fails during build-time validation.

///
/// Post-`window.load`, slim-Prebid takes over: it listens for GPT refresh
/// events, runs client-side auctions, and sets targeting for subsequent
/// impressions. SPA pushState navigation is also slim-Prebid's domain.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: P3 — This handoff comment no longer matches the implemented SPA flow.

The PR now installs installSpaAuctionHook() from the GPT bundle and GET /__ts/page-bids handles pushState / replaceState / popstate route changes. Saying that SPA pushState navigation is slim-Prebid's domain contradicts auction/endpoints.rs and the actual client code, and can mislead future maintainers about which component owns route-level server-side auctions.

Suggested fix: update this paragraph to say slim-Prebid owns scroll/GPT refresh, while SPA navigation is handled by installSpaAuctionHook() + /__ts/page-bids.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

5 participants