Skip to content

Always emit hb_adid so server-side ad template creatives render - #996

Open
prk-Jr wants to merge 1 commit into
mainfrom
fix/hb-adid-bid-id-fallback
Open

Always emit hb_adid so server-side ad template creatives render#996
prk-Jr wants to merge 1 commit into
mainfrom
fix/hb-adid-bid-id-fallback

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Bidders that return neither a Prebid Cache UUID nor an adid produced no hb_adid in window.tsjs.bids. adInit only sets targeting keys that exist on the bid, so GAM never received an hb_adid key, the Universal Creative's %%PATTERN:hb_adid%% expanded to empty, and the render bridge rejected the resulting Prebid Request for want of an ad ID. The line item won and served its wrapper, but the creative never rendered — a silent, per-bidder loss of every SSAT impression.
  • Adds Bid::bid_id, populated from the OpenRTB bid object's own id, and uses it as the last-resort hb_adid source. id is mandatory per the OpenRTB spec, so this closes the gap for every bidder.
  • cache_id and ad_id keep priority, in that order. This is not cosmetic: when hb_cache_host/hb_cache_path are present the Universal Creative uses hb_adid as the Prebid Cache lookup key (https://<host><path>?uuid=<hb_adid>), so an arbitrary value there would break cache retrieval. Both priorities are locked in by test.

Why a separate field rather than folding into ad_id

ad_id is the creative/ad identifier and is exposed raw in the debug bid. The OpenRTB id is unique per bid instance, not per creative — surfacing it as ad_id would mislead any consumer treating that field as a creative identifier. The existing comment in parse_bid says as much, so the bid ID is carried as its own field instead.

Changes

File Change
crates/trusted-server-core/src/auction/types.rs Add Bid::bid_id: Option<String> with #[serde(default, skip_serializing_if = "Option::is_none")], so the serialized wire shape is unchanged when absent
crates/trusted-server-core/src/integrations/prebid.rs Populate bid_id from the OpenRTB bid id in parse_bid; two tests covering it alongside and without adid/cache
crates/trusted-server-core/src/publisher.rs build_bid_map hb_adid chain becomes cache_id → ad_id → bid_id; test for the new fallback, renamed omit-case test, and bid_id added to the cache/ad-id tests to prove precedence
crates/trusted-server-core/src/auction/orchestrator.rs bid_id: None at Bid literals (tests)
crates/trusted-server-core/src/auction/formats.rs bid_id: None at Bid literal
crates/trusted-server-core/src/auction/telemetry.rs bid_id: None at Bid literal (test)
crates/trusted-server-core/src/integrations/adserver_mock.rs bid_id: None at Bid literals
crates/trusted-server-core/src/integrations/aps.rs bid_id: None at Bid literal
CHANGELOG.md Entry under Unreleased → Fixed

Test plan

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • 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: cargo test-cloudflare, cargo test-spin, cargo clippy-cloudflare, cargo clippy-cloudflare-wasm, cargo clippy-spin-native, cargo clippy-spin-wasm, and the parity suite (cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity)

The new bid_map_falls_back_to_bid_id_when_cache_id_and_ad_id_absent test was confirmed to fail with the fallback removed (left: None), so it genuinely gates the fix rather than passing vacuously.

Not covered by automated tests: the browser-side half of the loop — that GAM echoes the new hb_adid back and the bridge serves the creative. That path is the subject of #926.

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

…e absent

Bidders that return neither a Prebid Cache UUID nor an `adid` produced no
`hb_adid` in `window.tsjs.bids` at all. `adInit` only sets targeting keys that
exist on the bid, so GAM never received an `hb_adid` key, the Universal
Creative's `%%PATTERN:hb_adid%%` expanded to empty, and the render bridge
rejected the resulting `Prebid Request` message for want of an ad ID. The line
item won and served its wrapper, but the creative never rendered.

Add `Bid::bid_id`, populated from the OpenRTB bid object's own `id`, and use it
as the last-resort `hb_adid` source. Per spec `id` is mandatory, so this closes
the gap for every bidder. It is unique per bid instance rather than a creative
identifier, which is exactly what `hb_adid` needs here: a stable value GAM
echoes back verbatim so the bridge can find this winning bid.

`cache_id` and `ad_id` keep priority in that order — locked in by test, since
the Universal Creative treats `hb_adid` as the Prebid Cache lookup key whenever
`hb_cache_host`/`hb_cache_path` are present. `bid_id` is carried as its own
field rather than folded into `ad_id`, which is exposed raw in the debug bid
and would mislead consumers treating it as a creative identifier.

Verified: cargo fmt, all six clippy targets, test-fastly / test-axum /
test-cloudflare / test-spin, the parity suite, JS vitest, and JS + docs
prettier checks all pass. The new bid_map test was confirmed to fail with the
fallback removed.
@prk-Jr prk-Jr self-assigned this Aug 4, 2026
prk-Jr added a commit that referenced this pull request Aug 4, 2026
@prk-Jr
prk-Jr requested review from ChristianPavilonis and aram356 and removed request for aram356 August 4, 2026 16:51
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.

1 participant