Skip to content

Improve Prebid auction diagnostics#671

Open
ChristianPavilonis wants to merge 3 commits intomainfrom
feature/prebid-auction-diagnostics
Open

Improve Prebid auction diagnostics#671
ChristianPavilonis wants to merge 3 commits intomainfrom
feature/prebid-auction-diagnostics

Conversation

@ChristianPavilonis
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis commented Apr 29, 2026

Summary

  • Improve Prebid auction diagnostics so upstream HTTP failures surface status in /auction provider metadata, with capped body previews included only when Prebid debug mode is enabled.
  • Treat successful empty Prebid responses (204 No Content or empty 200) as nobid instead of provider error.
  • Remove Rubicon from the default client_side_bidders sample config so it is not enabled by default.

Changes

File Change
crates/trusted-server-core/src/integrations/prebid.rs Adds upstream HTTP status metadata for non-2xx responses, gates capped Prebid response body previews behind debug, treats empty successful responses as no-bid, uses PREBID_INTEGRATION_ID for provider response IDs, and adds tests for truncation/non-UTF-8 preview behavior.
crates/trusted-server-core/src/auction/orchestrator.rs Preserves provider launch/parse/conversion errors as provider response metadata using client-safe current-context display text, and adds tests for safe output and truncation.
trusted-server.toml Changes default client_side_bidders from ["rubicon"] to [].

Compatibility notes

  • provider_details may include launch-failure diagnostics before awaited provider responses. Bid selection is order-insensitive, but consumers should not rely on provider_details being ordered by response completion.

Closes

N/A — no issue filed.

Test plan

  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/js/lib && npx vitest run
  • JS format: cd crates/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 -p trusted-server-core prebid -- --nocapture; cargo test -p trusted-server-core auction -- --nocapture; targeted review-feedback tests for provider_error, prebid_body_preview, and parse_response_non_success

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses log macros per CLAUDE.md (template says tracing, but this repo standard is log)
  • New code has tests
  • No secrets or credentials committed

Comment thread crates/trusted-server-core/src/auction/orchestrator.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
Comment thread crates/trusted-server-core/src/auction/orchestrator.rs
Comment thread crates/trusted-server-core/src/integrations/prebid.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/prebid.rs
@aram356
Copy link
Copy Markdown
Collaborator

aram356 commented May 1, 2026

Summary

Richer /auction diagnostics and correct no-bid classification for empty Prebid responses are well-motivated and well-tested. One blocker: error-stack Report Debug formatting bleeds internal source paths into the public /auction JSON response, which contradicts the explicit policy in error.rs separating client-facing messages from server-only details.

Blocking

🔧 wrench

  • Information disclosure via format!("{error:?}"): provider_error_message ships error-stack Debug output (including at <file>:<line> entries) into the public /auction provider_details[].metadata.message. See inline. Switch to error.current_context().to_string() to keep the user-safe Display text but drop file/line/stack.

Non-blocking

🤔 thinking

  • Upstream body_preview proxied to public clients: The first 1000 chars of upstream Prebid error bodies are returned in the /auction response. Consider gating on self.config.debug (mirrors the existing trace-log gate) or a request-time header. See inline.
  • provider_details ordering changed: Launch failures now precede awaited responses in responses, whereas previously the list was ordered by completion. select_winning_bids is unaffected, but external consumers parsing provider_details see a different order. See inline.

♻️ refactor

  • Use PREBID_INTEGRATION_ID constant: New AuctionResponse::error("prebid", …) / ::no_bid("prebid", …) call sites should use the existing constant (crates/trusted-server-core/src/integrations/prebid.rs:36) instead of the literal. See inline.

🌱 seedling

  • Standardize diagnostic metadata across providers: Orchestrator-level error_type tagging is provider-agnostic, but only Prebid produces http_status / body_preview for non-success upstream responses. APS/GAM/etc. would be silent on the same failure mode. Worth a follow-up.

📌 out of scope

  • client_side_bidders = [] is a silent client-side capability change: The JS bundle still ships the rubicon adapter at build time (crates/js/lib/build-all.mjs:32DEFAULT_PREBID_ADAPTERS = 'rubicon'), but the runtime default in trusted-server.toml no longer enables it. Publishers copying the example config will lose rubicon client-side without an adapter-time signal. Worth a CHANGELOG / release note.

⛏ nitpick

  • Missing truncation tests: Neither prebid_body_preview (1000-char cap, String::from_utf8_lossy) nor provider_error_message (500-char cap) has a test pinning truncation/non-UTF-8 behavior. See inline.
  • format!("{error:?}") allocates before truncation: Long error chains materialize a multi-KB string before chars().take(500) discards the tail. Becomes a non-issue if the wrench above is fixed (Display output is much shorter).

CI Status

  • fmt: PASS
  • clippy: PASS
  • rust tests: PASS (incl. 4 new prebid + 1 orchestrator)
  • js tests: PASS
  • format-typescript / format-docs / browser integration tests / CodeQL: PASS

@ChristianPavilonis ChristianPavilonis requested a review from aram356 May 4, 2026 14:28
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

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

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_response returns serde-error detail + body byte length unconditionally, while the 4xx branch hides body content behind config.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 message strings: e.g. signing-key kid via RequestSigner::from_services(). provider_error_message is sound; the issue is at the call site choosing to emit current_context() verbatim for error_type=launch_failed (crates/trusted-server-core/src/auction/orchestrator.rs:385).

Non-blocking

🌱 seedling

  • Network-layer select() failures still drop providers from provider_responses (crates/trusted-server-core/src/auction/orchestrator.rs:481-486). The PR adds metadata for launch_failed, parse_response, and unsupported_response_body, but transport-level errors emerging from select() Err — and providers still pending after the deadline (backend_to_provider leftovers) — produce no entry. /auction clients 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). With client_side_bidders = [] now the default in trusted-server.toml, the bundled Rubicon Prebid.js adapter is dead weight unless a publisher overrides the list. Either default TSJS_PREBID_ADAPTERS='' to match, or have build-all.mjs derive 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

let response_json: Json = serde_json::from_slice(&body_bytes).map_err(|e| {
Report::new(TrustedServerError::Prebid {
message: format!(
"Failed to parse Prebid response JSON (status {status}, {} bytes): {e}",
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.

🔧 wrench — Parse-error path bypasses the debug-gate that the 4xx path enforces.

The non-success branch above carefully gates body_preview behind config.debug so untrusted upstream content doesn't reach /auction consumers. But on a 200 with malformed JSON, the error message itself is "Failed to parse Prebid response JSON (status {N}, {len} bytes): {serde_err}", which then flows through provider_error_message (capped at 500 chars but not debug-gated) into provider_details[].metadata.message. serde_json::Error::Display is normally just "expected value at line 1 column 1", so the realistic leak is the body byte length + parser category — but the asymmetry undermines the boundary the PR set up for the 4xx path, and serde messages can grow if upstream JSON gets exotic.

Suggested fix — keep the rich detail in the operator log, but use a static public message:

let response_json: Json = match serde_json::from_slice(&body_bytes) {
    Ok(json) => json,
    Err(e) => {
        log::warn!(
            "Prebid: failed to parse response JSON (status {status}, {} bytes): {e}",
            body_bytes.len()
        );
        return Err(Report::new(TrustedServerError::Prebid {
            message: "Failed to parse Prebid response JSON".to_string(),
        }));
    }
};

That keeps full diagnostics for operators (via warn!) while ensuring /auction clients see a flat, non-leaky message regardless of debug mode.

if body_preview.is_empty() {
log::warn!("Prebid returned non-success status: {status} with empty body");
} else {
log::warn!("Prebid returned non-success status: {status} body: {body_preview}");
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.

🔧 wrenchwarn! now emits up to 1000 chars of upstream body unconditionally.

Pre-PR, the body preview was logged at trace only. Now it lands in the standard warn path on every non-success Prebid response, regardless of config.debug. Upstream Prebid 4xx/5xx bodies routinely contain bidder-config echoes, internal request IDs, account hints, and occasionally PII fragments — same concern that motivated debug-gating body_preview in metadata, just shifted to logs.

The PR description acknowledges this ("warn log path still records the capped preview for operators regardless of debug") which is a defensible operator-visibility choice, but production logs are persisted, indexed, and often shared more broadly than /auction response bodies. They deserve at least the same gate.

Suggested fix — log the status at warn always, but gate the body behind debug:

if !status.is_success() {
    let body_preview = prebid_body_preview(&body_bytes);
    log::warn!("Prebid returned non-success status: {status} ({} bytes)", body_bytes.len());
    if self.config.debug && !body_preview.is_empty() {
        log::debug!("Prebid non-success body: {body_preview}");
    }
    // ...
}

This preserves the operational signal (status + size) while keeping the upstream payload behind the same gate as the metadata path.

provider.provider_name(),
e
);
responses.push(provider_error_response(
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.

🔧 wrench — Launch-failure messages can surface internal identifiers to /auction clients.

provider_error_message returns error.current_context().to_string() (capped at 500 chars). That's a clean fix for the prior format!("{:?}") issue — but it still exposes whatever string each provider chose to put into its message field, and those weren't designed as user-facing.

Concrete example: when request_bids fails because RequestSigner::from_services() couldn't load the active key, the error message is "Configuration error: failed to get signing key for kid: <kid>" (request_signing/signing.rs:140-148). With this change, that kid (an internal Secret Store key identifier) lands in the public provider_details[].metadata.message. Kids aren't strictly secret, but they're internal state that didn't previously surface — and the same pattern means anyone adding a new change_context(...) site upstream of request_bids is now shipping that detail to /auction callers by default.

Suggested fix — give error_type=launch_failed a static public message, and keep the rich context in the existing warn! log on line 380:

Err(e) => {
    let response_time_ms = start_time.elapsed().as_millis() as u64;
    log::warn!(
        "Provider '{}' failed to launch request: {:?}",
        provider.provider_name(),
        e
    );
    let mut error_response = AuctionResponse::error(provider.provider_name(), response_time_ms)
        .with_metadata("error_type", serde_json::json!("launch_failed"));
    // Optionally surface error_type only here; full detail stays in logs.
    responses.push(error_response);
}

Or — if you want a per-error-type policy — split provider_error_response into a public path (static description per error_type) and an operator path (rich Report formatting), so future call sites can't accidentally widen the public surface.

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.

2 participants