Skip to content

Improve Prebid auction diagnostics#671

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

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

Conversation

@ChristianPavilonis
Copy link
Copy Markdown
Collaborator

Summary

  • Improve Prebid auction diagnostics so upstream HTTP failures surface status and a capped body preview in /auction provider metadata.
  • 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 capped Prebid response body previews, includes upstream HTTP status metadata for non-2xx responses, treats empty successful responses as no-bid, and adds tests for these cases.
crates/trusted-server-core/src/auction/orchestrator.rs Preserves provider launch/parse/conversion errors as provider response metadata so /auction responses include actionable diagnostics.
trusted-server.toml Changes default client_side_bidders from ["rubicon"] to [].

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

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

.chars()
.take(PROVIDER_ERROR_MESSAGE_CHARS)
.collect()
}
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 — Information disclosure: format!("{error:?}") on a Report<TrustedServerError> emits error-stack's full Debug stack, which includes at <file>:<line>:<col> location entries (and backtrace frames if compiled with them). That string flows through AuctionResponse.metadata["message"]ProviderSummary.metadata → public JSON body of POST /auction, exposing internal source paths to any caller.

This contradicts the explicit policy in error.rs:18-22 ("message field is included in client-facing HTTP responses … keep it free of internal implementation details") and the IntoHttpResponse::user_message() pattern ("Full error details are preserved in server logs").

Fix — use Display on the current context so the user-safe #[display(...)] text is preserved but file/line/stack info is dropped:

fn provider_error_message(error: &Report<TrustedServerError>) -> String {
    error
        .current_context()
        .to_string()
        .chars()
        .take(PROVIDER_ERROR_MESSAGE_CHARS)
        .collect()
}

The detailed {error:?} is still emitted to logs via the existing log::warn!("…: {:?}", e) calls.

if !body_preview.is_empty() {
auction_response =
auction_response.with_metadata("body_preview", serde_json::json!(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.

🤔 thinking — The first 1000 chars of the upstream Prebid Server's error body are placed in metadata["body_preview"] and shipped to whoever called /auction. Upstream Prebid error bodies routinely contain bidder-config echoes, internal request IDs, and account hints.

The trace-level body logging at line 1146 is gated on self.config.debug && trace_enabled; consider gating body_preview similarly (debug flag or request-time header) so the default /auction response stays free of upstream content. The warn! log line keeps the operator-side signal regardless.

let mut backend_to_provider: HashMap<String, (&str, Instant, &dyn AuctionProvider)> =
HashMap::new();
let mut pending_requests: Vec<PlatformPendingRequest> = Vec::new();
let mut responses = Vec::new();
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.

🤔 thinking — Moving let mut responses = Vec::new(); above the launch loop means launch failures are appended before any awaited responses. Previously the list was ordered by completion. select_winning_bids is order-insensitive (HashMap), but external consumers reading provider_details will see a different ordering than they did pre-PR. Worth either calling out in the PR description or sorting deterministically before serialization.

log::warn!("Prebid returned non-success status: {status} body: {body_preview}");
}

let mut auction_response = AuctionResponse::error("prebid", response_time_ms)
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.

♻️ refactor — Use PREBID_INTEGRATION_ID (defined at line 36) instead of the literal "prebid". Same applies to the no_bid("prebid", …) call below at line 1130. Other call sites in this file already use the constant.

.chars()
.take(PREBID_ERROR_BODY_PREVIEW_CHARS)
.collect()
}
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.

nitpick — No tests pin the truncation behavior (>1000 chars) or non-UTF-8 input handling for prebid_body_preview, nor for provider_error_message (500-char cap). Cheap to add and would lock in the security boundary that today's reviewers are relying on.

@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

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