Skip to content

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

Open
prk-Jr wants to merge 90 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 90 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.

@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

CI is green. I found one high-priority timing/initialization issue and two low-priority follow-ups.

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

Comment thread crates/trusted-server-core/src/integrations/gpt_bootstrap.js Outdated
Comment thread crates/js/lib/src/integrations/gpt/index.ts
Comment thread crates/trusted-server-core/src/auction/endpoints.rs Outdated
- Fix gpt_bootstrap.js APS beacon miss: listener now uses hb_bidder fallback
  when hb_adid is absent, matching the bundle's slotRenderEnded logic
- Fix stale divToSlotId in inline listener: read from window.__tsDivToSlotId
  dynamically instead of local closure so SPA navigation updates are seen;
  early-return for slots not managed by Trusted Server
- Populate window.__tsPrevGptSlots and window.__tsDivToSlotId from inline
  bootstrap so bundle's destroySlots and SPA nav path have correct state
- Call installSlimPrebidLoader() in module init so the slim-Prebid lazy
  loader activates when __tsjs_slim_prebid_url is set; add three Vitest cases
- Update /auction doc comment to distinguish /__ts/page-bids (SPA navigation)
  from /auction (initial render) and slim-Prebid (scroll/refresh)
@prk-Jr prk-Jr requested a review from ChristianPavilonis May 23, 2026 17:37

@aram356 aram356 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.

Summary

Pass-4 review at fcb04d29 (latest head). Verified all prior-review fixes (passes 1–3 + Christian's comments) landed correctly; identified one trivial blocker (test-fixture typo), one open question on SPA-navigation race, and a handful of hardening seedlings. CI all green.

Blocking

🔧 wrench

  • examnple.com typo in sourcepoint.rs test fixture (sourcepoint.rs:1076, 1078) — see inline.

❓ question

  • SPA navigation race on rapid back-and-forth (gpt/index.ts:331) — see inline.

Non-blocking

🤔 thinking

  • prepend_auction_debug_comment None branch unreachable (publisher.rs:653) — see inline.

♻️ refactor

  • Empty-bids <script> fallback duplicated. build_bids_script (publisher.rs:1346–1353) and the no-bids fallback in html_processor.rs (~line 340) both emit the same window.__ts_bids=JSON.parse("{}");if(typeof window.__tsAdInit==="function")window.__tsAdInit();. Export a build_empty_bids_script() helper so the script shape can change in one place.

🌱 seedling

  • parse_ts_eids_cookie hardening — deny_unknown_fields + length cap (cookies.rs:143) — see inline.
  • CreativeOpportunitiesFile + CreativeOpportunitySlot lack deny_unknown_fields (creative_opportunities.rs:227) — see inline.
  • adserver_mock crid parsing silently strips bid metadata (adserver_mock.rs:253–256) — see inline.
  • adserver_mock accepts zero-dimension bids silently (adserver_mock.rs:263–264) — see inline.
  • APS parse_aps_slot defaults to 0×0 on bad size string (aps.rs:409, not in diff hunk so flagging here). Self::parse_size(&slot.size).unwrap_or((0, 0)) produces an invalid 0×0 bid when the APS response carries a malformed size. Fail closed by logging and skipping the bid, mirroring the adserver_mock seedling above.
  • No MediaType::Native or MediaType::Video slot-conversion test in creative_opportunities.rs — only Banner is exercised. Re-flag from pass-2; still applicable.
  • No negative-CPM test in price_bucket (price_bucket.rs:133–157). The early if !cpm.is_finite() || cpm <= 0.0 correctly returns the zero bucket, but the test matrix only pins NaN/Inf. One-line assert_eq!(price_bucket(-1.5, Dense), "0.00");.

⛏ nitpick

  • is_processable_content_type uses contains rather than parsing the MIME (publisher.rs:1401, not in diff hunk). Content-Type: text/notthml from a misbehaving origin would match text/. Best-effort and current callers are safe; flag for future hardening.

📌 out of scope

  • /__ts/page-bids accepts client-supplied ?path= and forwards to PBS as site.page (publisher.rs:1480–1488) — see inline.

CI status

All 13 GitHub checks pass at fcb04d29 — fmt, clippy, cargo test, vitest, format-docs, format-typescript, browser integration tests, integration tests, CodeQL, Analyze (rust + javascript-typescript + actions).

Comment thread crates/trusted-server-core/src/integrations/sourcepoint.rs Outdated
Comment thread crates/js/lib/src/integrations/gpt/index.ts Outdated
Comment thread crates/trusted-server-core/src/publisher.rs
Comment thread crates/trusted-server-core/src/cookies.rs
Comment thread crates/trusted-server-core/src/creative_opportunities.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/adserver_mock.rs
Comment thread crates/trusted-server-core/src/integrations/adserver_mock.rs Outdated
Comment thread crates/trusted-server-core/src/publisher.rs
- Fix examnple.com typo in sourcepoint.rs test fixture
- Guard SPA navigation race: check inflight controller identity after await res.json() before writing __ts_ad_slots/__ts_bids
- Extract build_empty_bids_script() helper; html_processor.rs now calls it instead of duplicating the inline literal
- Add invariant comment to unreachable None branch of prepend_auction_debug_comment
- Cap parse_ts_eids_cookie to 32 eids / 32 uids per eid; log and return None when exceeded
- Add #[serde(deny_unknown_fields)] to openrtb::Eid and Uid
- Add #[serde(deny_unknown_fields)] to CreativeOpportunitiesFile and CreativeOpportunitySlot
- Log debug message when adserver_mock crid does not match <bidder>-creative convention
- Skip zero-dimension bids in adserver_mock with debug log
- Fail closed in APS parse_aps_slot on malformed size string instead of producing 0x0 bid
@prk-Jr prk-Jr marked this pull request as draft May 29, 2026 15:36
@aram356 aram356 mentioned this pull request Jun 8, 2026
3 tasks
@aram356 aram356 linked an issue Jun 8, 2026 that may be closed by this pull request
prk-Jr and others added 6 commits June 9, 2026 12:57
… globals under window._ts (#745)

* Move slot templates from creative-opportunities.toml into trusted-server.toml

Add [[creative_opportunities.slot]] array to trusted-server.toml and remove
the separate creative-opportunities.toml file. Slots now deserialize directly
into CreativeOpportunitiesConfig.slot via the existing vec_from_seq_or_map
deserializer, with compile_slots() called in Settings::prepare_runtime().
Update publisher.rs and main.rs function signatures from &CreativeOpportunitiesFile
to &[CreativeOpportunitySlot]. Build.rs slot-ID validation now reads from the
merged settings rather than a separate file.

* Namespace window globals under window.tsjs

When slotRenderEnded fires before APS inserts its SafeFrame iframe
(gamIframe is null), retry on the next animation frame. If the iframe
appears by then, set its src to the adm creative URL. If still absent,
replace slot content directly. Prevents the fixed_bottom sticky unit
from being blanked when APS reveals it asynchronously after the event.
Merged EC identity subsystem (ec/ module, EcContext lifecycle, partner
registry) with server-side ad template additions (handle_page_bids,
AuctionDispatch, stream_publisher_body_async). Key reconciliations:

- publisher.rs: adopt EcContext param pattern; add AuctionDispatch struct
  to keep handle_publisher_request within 7-arg limit; box Stream variant
  params to fix enum size imbalance
- main.rs: use sync stream_publisher_body; pass AuctionDispatch to
  handle_publisher_request; add .await for async handle_publisher_request
- orchestrator.rs: import PlatformPendingRequest/RuntimeServices; add
  platform_response_to_fastly conversion helper
- prebid/index.ts: keep installRefreshHandler + installUserIdModules
  (HEAD) and EID cookie sync functions (main)
- formats.rs, aps.rs, settings.rs: fix test code for updated UserInfo
  (id: Option<String>, no fresh_id), AdRequest eids field, and settings
  validation requirements
Both handle_publisher_request and handle_page_bids now run the full
four-step EID pipeline (resolve_client_auction_eids → resolve_auction_eids
→ merge_auction_eids → gate_eids_by_consent) matching the client-side
/auction endpoint.

Previously both paths called parse_ts_eids_cookie, which read only the
ts-eids browser cookie and skipped the KV identity graph lookup entirely.

AuctionDispatch gains a registry field so the partner registry reaches
handle_publisher_request without exceeding the seven-argument limit.
handle_page_bids gains kv and registry parameters for the same reason.

parse_ts_eids_cookie is moved to #[cfg(test)] as it is now test-only.
The file only emitted a rerun-if-changed watch for creative-opportunities.toml,
which was deleted when slot config was consolidated into trusted-server.toml.
Config validation now runs entirely in trusted-server-core/build.rs.
…atch

Aligns log (0.4.29 → 0.4.32) and serde_json (1.0.149 → 1.0.150) with
the versions already pulled into crates/integration-tests/Cargo.lock.
Comment thread crates/trusted-server-core/src/auction/orchestrator.rs Dismissed
Comment thread crates/trusted-server-core/src/auction/orchestrator.rs Dismissed
Comment thread crates/trusted-server-core/src/auction/orchestrator.rs Fixed
prk-Jr added 6 commits June 9, 2026 17:33
…mespace

Replace all references to the deleted `creative-opportunities.toml` file with
the `[creative_opportunities]` section in `trusted-server.toml`. Update all
`window.__ts_*` global name references to the current `window.tsjs.*` namespace
(tsjs.bids, tsjs.adSlots, tsjs.adInit).
Add `suppress_nurl: bool` (default `false`) to `PrebidIntegrationConfig`.
When `true`, strips `nurl` and `burl` from PBS bids at parse time, preventing
client-side double-firing via `sendBeacon` when PBS fires win notifications
server-side (ext.prebid.events.enabled).

Update `adserver_mock` test-bidder response to include example `nurl`/`burl`
values so the win-notification pipeline is exercisable in unit tests.
@prk-Jr prk-Jr marked this pull request as ready for review June 10, 2026 11:47

@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 def951a against main. CI is green, but I found blocking privacy/security and correctness issues in the new server-side ad-template paths. In particular, the publisher/page-bids auction paths can forward persistent identifiers after EC consent is denied, the default config now enables automatic outbound ad calls to real endpoints, and the GPT/Prebid refresh paths can duplicate requests or reuse stale targeting. Details are inline.

Comment thread crates/trusted-server-core/src/publisher.rs Outdated
Comment thread crates/trusted-server-core/src/publisher.rs Outdated
Comment thread crates/js/lib/src/integrations/prebid/index.ts Outdated
Comment thread crates/js/lib/src/integrations/gpt/index.ts
Comment thread trusted-server.toml 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.

Summary

Reviewed the current branch PR #680 at 80fe39220 against main. I found several remaining issues that should be addressed in this PR because they affect the new server-side ad-template auction behavior: consent gating, the auction kill switch, PBS URL compatibility, SPA refresh flow, timeout budgets, and stale GPT targeting.

Per discussion, the raw browser-cookie forwarding concern is not included in this PR review and is tracked separately in #763.

Findings submitted inline: 0 P0, 4 P1, 1 P2, 1 P3. One additional P2 is included below because the affected provider timeout lines are not part of the final PR diff.

P2 — Provider payload timeouts ignore the effective auction budget

crates/trusted-server-core/src/integrations/prebid.rs:1171 and crates/trusted-server-core/src/integrations/aps.rs:367 still use provider config timeouts in the payload (tmax / APS timeout), while the Fastly backend timeout is capped to context.timeout_ms. When creative_opportunities.auction_timeout_ms is lower than the provider timeout, providers are told they have more time than the edge will actually wait, which can turn useful partial bids into edge timeouts. Please use the effective context.timeout_ms in provider payloads and add tests where provider config is 1000ms but auction context is 500ms.


// Non-GDPR regions (US, etc.) have no TCF string — auction is freely allowed.
// GDPR regions require TCF Purpose 1 (storage/access) before firing.
let consent_allows_auction = !consent_context.gdpr_applies

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.

P1 — GDPR/no-consent requests can still run server-side auctions

gdpr_applies is derived from TCF/GPP signal presence, not from geo jurisdiction. That means a GDPR-region request with no TC/GPP signal yet is treated as !gdpr_applies and can dispatch PBS/APS auctions. For this server-side ad stack, please fail closed for GDPR/unknown jurisdiction unless effective TCF/GPP Purpose 1 is granted.

Suggested fix: centralize this eligibility check in a helper and use it for both publisher navigation auctions and /__ts/page-bids. Also make page-bids use the adapter's geo-aware EcContext or pass geo into read_from_request_with_geo, so jurisdiction is available for this decision.

Comment thread trusted-server.toml
enabled = true
server_url = "http://68.183.113.79:8000"
enabled = false
server_url = "https://prebid-server.example.com/openrtb2/auction"

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.

P1 — Documented Prebid URL shape is double-appended by the provider

The committed config now shows server_url = "https://prebid-server.example.com/openrtb2/auction", but the provider constructs requests with format!("{}/openrtb2/auction", self.config.server_url). Operators copying this config/docs will request /openrtb2/auction/openrtb2/auction and get no PBS bids.

Suggested fix: normalize backward-compatibly: if server_url already ends with /openrtb2/auction, use it as-is; otherwise append the path. Alternatively, redefine the config field as a base origin and update all examples/docs to match.

!matched_slots.is_empty(),
consent_allows_auction,
);
let should_run_auction = should_run_ad_stack;

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.

P1 — [auction].enabled is ignored by the new automatic auction paths

should_run_auction is derived from slots/navigation/consent, but not from settings.auction.enabled or orchestrator.is_enabled(). A deployment with [auction].enabled = false can still dispatch automatic publisher/page-bids server-side auctions when creative-opportunity slots are configured.

Suggested fix: make the global auction flag a real kill switch for publisher navigation auctions, /__ts/page-bids, and ideally /auction for consistency. When disabled, skip the server-side ad stack behavior that would dispatch auctions or call adInit() from injected empty bids.

}

if (slotsToRefresh.length > 0) {
g.pubads!().refresh(slotsToRefresh);

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.

P1 — SPA adInit() refreshes can be intercepted by slim-Prebid and clear server-side bids

After /__ts/page-bids returns, adInit() applies the server-side hb_* targeting and then calls pubads.refresh(slotsToRefresh). Once slim-Prebid has wrapped refresh, that internal TS refresh is intercepted, clears the just-applied TS targeting, and starts a client-side Prebid auction instead. This defeats the SPA server-side bid path and adds duplicate auction/GAM latency.

Suggested fix: add a one-shot internal refresh bypass around this adInit() refresh (for example ts.adInitRefreshInProgress) and have the Prebid refresh wrapper call originalRefresh directly only for that internal refresh. Do not key this off ts_initial, because later publisher-initiated refreshes of these slots should still run fresh client-side auctions.

const prevSlotTargetingKeys = ts.prevSlotTargetingKeys ?? {};
const nextSlotTargetingKeys: Record<string, string[]> = {};

slots.forEach((slot) => {

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.

P2 — Stale TS targeting can remain when the next SPA route has no TS slots

The cleanup of prior hb_*, ts_initial, and route-specific targeting only runs inside the loop over the new ts.adSlots. If navigation moves to a route with no matching TS slots, or a previously TS-touched publisher-owned GPT slot is absent from the new slot list, old targeting can remain and later publisher refreshes can reuse it.

Suggested fix: before applying the current route, clear TS-managed keys and previously tracked slot-targeting keys from all previously TS-touched GPT slots/div IDs, then replace prevSlotTargetingKeys / divToSlotId and apply current slots.

});
// Keep in sync with TS_INITIAL_TARGETING_KEY in index.ts
s.setTargeting("ts_initial", "1");
divToSlotId[actualDivId] = slot.id;

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.

P3 — Inline GPT bootstrap misses container slot IDs for beacon lookup

When the bootstrap defines a slot on ${actualDivId}-container, slotRenderEnded reports the GPT slot element ID, but divToSlotId only maps actualDivId. In that fallback path, container-backed slots can miss nurl/burl firing.

Suggested fix: mirror the TS implementation: after defining/reusing the slot, read s.getSlotElementId() and map both actualDivId and the GPT slot element ID to slot.id.

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