Improve Prebid auction diagnostics#671
Conversation
SummaryRicher Blocking🔧 wrench
Non-blocking🤔 thinking
♻️ refactor
🌱 seedling
📌 out of scope
⛏ nitpick
CI Status
|
aram356
left a comment
There was a problem hiding this comment.
Summary
The diagnostic improvements are well-scoped and the prior round's findings are cleanly addressed (current-context truncation, debug-gated body_preview, PREBID_INTEGRATION_ID constant, regression tests for the 500/1000-char caps and attached-detail suppression). CI is green and the new tests pin the right boundaries.
The remaining concerns are about consistency of that boundary: a few error paths slip past the debug gate that the 4xx path now establishes. Inline comments below cover the three specific call sites; cross-cutting items are listed here.
Blocking
🔧 wrench
- Parse-error message bypasses debug gate:
parse_responsereturns serde-error detail + body byte length unconditionally, while the 4xx branch hides body content behindconfig.debug(crates/trusted-server-core/src/integrations/prebid.rs:1141). warn!emits up to 1000 chars of upstream body without debug gate: previously trace-only; this is a logging behavior change worth gating in line with the metadata-path treatment (crates/trusted-server-core/src/integrations/prebid.rs:1112).- Launch-failure metadata exposes internal
messagestrings: e.g. signing-key kid viaRequestSigner::from_services().provider_error_messageis sound; the issue is at the call site choosing to emitcurrent_context()verbatim forerror_type=launch_failed(crates/trusted-server-core/src/auction/orchestrator.rs:385).
Non-blocking
🌱 seedling
- Network-layer
select()failures still drop providers fromprovider_responses(crates/trusted-server-core/src/auction/orchestrator.rs:481-486). The PR adds metadata forlaunch_failed,parse_response, andunsupported_response_body, but transport-level errors emerging fromselect()Err — and providers still pending after the deadline (backend_to_providerleftovers) — produce no entry./auctionclients can't distinguish "no bid" from "transport error" for those. Pre-existing, but the PR's stated intent is unified observability so it's a natural follow-up.
📌 out of scope
- JS bundle still defaults to bundling the Rubicon adapter (
crates/js/lib/build-all.mjs:32). Withclient_side_bidders = []now the default intrusted-server.toml, the bundled Rubicon Prebid.js adapter is dead weight unless a publisher overrides the list. Either defaultTSJS_PREBID_ADAPTERS=''to match, or havebuild-all.mjsderive the adapter list from the merged TOML. Worth a follow-up issue.
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
- integration / browser tests: PASS
- CodeQL / format-typescript / format-docs: PASS
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
The remaining review concern is in the Prebid error-body preview path. The public preview is capped, but the helper still performs uncapped UTF-8/lossy processing before the cap is applied, and it is invoked even when debug mode is off.
Blocking
🔧 wrench
- Uncapped preview work before truncation:
prebid_body_previewconverts the entire upstream body withString::from_utf8_lossy(body)before taking the first 1000 chars, and the non-success response path calls it before checkingconfig.debug. Large or invalid UTF-8 error bodies can still drive uncapped validation/allocation even when no preview will be exposed. See inline.
CI Status
- GitHub checks: Analyze (javascript-typescript) PASS
- Local fmt: PASS (
cargo fmt --all -- --check) - Local targeted Rust tests: PASS (
prebid_body_preview;provider_error_response)
25eaca6 to
cedc9cf
Compare
aram356
left a comment
There was a problem hiding this comment.
Summary
Deep re-review against main at HEAD 50e3000d. Two prior approvals (aram356, prk-Jr) and all CI green. The PR has iterated through three rounds of reviewer feedback and addressed every prior finding (Debug→Display, debug-gated body previews, static launch-failure messages, PREBID_INTEGRATION_ID constant, byte-bounded from_utf8_lossy, dedicated truncation tests). The remaining findings below are forward-looking invariants and one cross-cutting concern; none block correctness.
Blocking
🔧 wrench
- SECURITY invariant comment on
provider_error_message: safe today because allchange_contextsites use static or operator-controlled message strings, but a future provider interpolating upstream-controlled content intoTrustedServerError::*.messagewould silently undo this PR's security work. See inline atcrates/trusted-server-core/src/auction/orchestrator.rs:25. - Lift
error_typestrings to constants:"http_status"/"parse_response"/"unsupported_response_body"/"launch_failed"/"empty_response"are now an implicit public contract — promote to module constants to prevent typos and centralize the taxonomy. See inline atcrates/trusted-server-core/src/auction/orchestrator.rs:41.
Non-blocking
🌱 seedling
- Opt-in for provider diagnostics exposure:
error_type/message/http_status/body_previeware now unconditionally surfaced on/auction. Some production operators may prefer server-logs-only diagnostics and a minimal public response. Worth a follow-upexpose_provider_diagnostics: boolflag inAuctionConfig(defaulttruefor backward compat). Out of scope for this PR.
📝 note
- No-bid taxonomy shift: a 200 OK response with empty body previously failed
serde_json::from_slice(&[])and surfaced as a provider error; now it isBidStatus::NoBidwithreason = "empty_response". This will shift the error-rate ↔ no-bid-rate ratio in any dashboards comparing pre/post merge. Worth a one-line release note.
⛏ nitpick
- UTF-8 boundary test gap:
prebid_body_preview_ignores_bytes_after_bounded_sliceexercises the byte cap with ASCII input only; the adversarial case is a multi-byte character straddling the byte boundary. See inline atcrates/trusted-server-core/src/integrations/prebid.rs:3202.
CI Status
- fmt: PASS
- clippy: PASS
- cargo test: PASS
- vitest: PASS
- browser/integration: PASS
Locally verified cargo test -p trusted-server-core --lib against the touched test names (all 12 new tests pass) and cargo clippy -p trusted-server-core --all-targets --all-features -- -D warnings (clean).
aram356
left a comment
There was a problem hiding this comment.
Summary
Improves Prebid auction diagnostics: non-2xx upstream responses surface error_type/http_status (and a debug-gated body_preview) in /auction provider metadata, empty 204/200 responses become NoBid with reason=empty_response instead of provider error, and rubicon is dropped from the default client_side_bidders. Correctness is solid, the public-leak surface is handled deliberately, and coverage is thorough. Approving; the notes below are non-blocking.
I verified the full data path AuctionResponse.metadata → ProviderSummary → provider_details → public /auction response (no auth/debug gate), and confirmed all providers (prebid, aps, adserver_mock) keep upstream content out of error messages, so the diagnostic message field is safe.
Non-blocking
🌱 seedling
- Public-leak invariant is comment-enforced: exposing
metadata["message"]is safe only while every provider keeps upstream content out ofTrustedServerErrormessages — enforced by a comment, not types (crates/trusted-server-core/src/auction/orchestrator.rs:23).
🤔 thinking
- Partial launch-failure diagnostics: only the
request_bidsErrpath emits alaunch_failedentry; budget-exhausted / no-backend / disabled / unregistered skips vanish fromprovider_details(crates/trusted-server-core/src/auction/orchestrator.rs:402).
📝 note / ⛏ nitpick
- rubicon adapter still bundled by default despite empty
client_side_bidders(trusted-server.toml:48,crates/js/lib/build-all.mjs:32). - Over-engineered UTF-8 boundary test asserts std
from_utf8_lossybehavior more than the helper (crates/trusted-server-core/src/integrations/prebid.rs:3207).
CI Status
CI on the PR ran only CodeQL (Analyze: PASS); Rust gates did not run, so verified locally on head 5f00e971:
- fmt: PASS
- clippy (
-D warnings): PASS - rust tests: PASS (966 passed, 0 failed)
- js tests: not run (no JS changes in this PR)
| const ERROR_TYPE_UNSUPPORTED_BODY: &str = "unsupported_response_body"; | ||
| const ERROR_TYPE_LAUNCH_FAILED: &str = "launch_failed"; | ||
|
|
||
| // SECURITY: the returned string is included verbatim (truncated to |
There was a problem hiding this comment.
🌱 seedling — The safety of exposing metadata["message"] in the unauthenticated /auction response (ProviderSummary → provider_details, no debug/auth gate) rests entirely on every provider keeping upstream-controlled content out of TrustedServerError::*.message. That invariant is enforced by this comment, not the type system. Today all providers comply (verified prebid, aps, adserver_mock — all static messages, and current_context() strips error-stack attachments). But a future provider doing change_context(TrustedServerError::Auction { message: format!("bad body: {body}") }) would silently leak upstream bytes to all callers. Consider a dedicated safe-message newtype / redaction wrapper, or a test asserting the invariant across registered providers.
| provider.provider_name(), | ||
| e | ||
| ); | ||
| responses.push(provider_launch_failed_response( |
There was a problem hiding this comment.
🤔 thinking — Launch-failure diagnostics are partial: only the request_bids Err path emits a launch_failed provider entry. Providers skipped earlier in Phase 1 — effective_timeout == 0 (budget exhausted), missing backend_name, disabled, or not-registered — still continue silently and never appear in provider_details. The budget-exhausted case is arguably the most useful diagnostic and is the one now silently dropped. Intentional scope, or worth emitting a skip entry there too?
| # being routed through the server-side auction. Their adapter modules must | ||
| # be statically imported in the JS bundle. | ||
| client_side_bidders = ["rubicon"] | ||
| client_side_bidders = [] |
There was a problem hiding this comment.
📝 note — client_side_bidders = [] means nothing uses rubicon client-side by default, but crates/js/lib/build-all.mjs still sets DEFAULT_PREBID_ADAPTERS = 'rubicon', so the bundle continues to ship the rubicon Prebid.js adapter. Not a bug — just dead weight / a config inconsistency worth a follow-up.
| } | ||
|
|
||
| #[test] | ||
| fn prebid_body_preview_bounds_partial_utf8_at_byte_boundary() { |
There was a problem hiding this comment.
⛏ nitpick — This test spends most of its body asserting String::from_utf8_lossy (std) behavior on a hand-sliced buffer, and its own comment notes the char cap dominates so the boundary case is "beyond the public character cap for ASCII input." The valuable assertions are on the helper output (capped char count, no "tail"); the bounded-slice pinning exercises std, not our code, and could be trimmed.
Summary
/auctionprovider metadata, with capped body previews included only when Prebid debug mode is enabled.204 No Contentor empty200) asnobidinstead of providererror.client_side_bidderssample config so it is not enabled by default.Changes
crates/trusted-server-core/src/integrations/prebid.rsdebug, treats empty successful responses as no-bid, usesPREBID_INTEGRATION_IDfor provider response IDs, and adds tests for truncation/non-UTF-8 preview behavior.crates/trusted-server-core/src/auction/orchestrator.rstrusted-server.tomlclient_side_biddersfrom["rubicon"]to[].Compatibility notes
provider_detailsmay include launch-failure diagnostics before awaited provider responses. Bid selection is order-insensitive, but consumers should not rely onprovider_detailsbeing ordered by response completion.BidStatus::NoBidwithreason = "empty_response"instead of provider error, which may shift error-rate/no-bid-rate dashboards after merge.Closes
N/A — no issue filed.
Test plan
cargo test --workspacecargo clippy --workspace --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npx vitest runcd crates/js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute servecargo test -p trusted-server-core prebid -- --nocapture;cargo test -p trusted-server-core auction -- --nocapture; targeted review-feedback tests forprovider_error,prebid_body_preview, andparse_response_non_successChecklist
unwrap()in production code — useexpect("should ...")logmacros per CLAUDE.md (template saystracing, but this repo standard islog)