From 62630dd09b586c71734e949726b53688bfae3749 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 25 Feb 2026 14:42:38 -0600 Subject: [PATCH 1/8] Add test, returnallbidstatus, and response logging debug flags to Prebid OpenRTB requests --- crates/common/src/integrations/prebid.rs | 10 +++++++--- crates/common/src/openrtb.rs | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index 8177a823..f55bc360 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -611,6 +611,7 @@ impl PrebidAuctionProvider { let ext = Some(RequestExt { prebid: Some(PrebidExt { debug: if self.config.debug { Some(true) } else { None }, + returnallbidstatus: if self.config.debug { Some(true) } else { None }, }), trusted_server: Some(TrustedServerExt { version, @@ -632,6 +633,7 @@ impl PrebidAuctionProvider { user, device, regs, + test: if self.config.debug { Some(1) } else { None }, ext, } } @@ -827,11 +829,13 @@ impl AuctionProvider for PrebidAuctionProvider { message: "Failed to parse Prebid response".to_string(), })?; - if log::log_enabled!(log::Level::Debug) { + // Log the full response body when debug is enabled to surface + // ext.debug.httpcalls, resolvedrequest, bidstatus, errors, etc. + if self.config.debug { match serde_json::to_string_pretty(&response_json) { - Ok(json) => log::debug!("Prebid OpenRTB response:\n{}", json), + Ok(json) => log::debug!("Prebid OpenRTB response:\n{json}"), Err(e) => { - log::warn!("Prebid: failed to serialize OpenRTB response for logging: {e}") + log::warn!("Prebid: failed to serialize response for logging: {e}"); } } } diff --git a/crates/common/src/openrtb.rs b/crates/common/src/openrtb.rs index 5e6d4b70..986f4f5b 100644 --- a/crates/common/src/openrtb.rs +++ b/crates/common/src/openrtb.rs @@ -19,6 +19,8 @@ pub struct OpenRtbRequest { #[serde(skip_serializing_if = "Option::is_none")] pub regs: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub test: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub ext: Option, } @@ -109,6 +111,8 @@ pub struct RequestExt { pub struct PrebidExt { #[serde(skip_serializing_if = "Option::is_none")] pub debug: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub returnallbidstatus: Option, } #[derive(Debug, Serialize, Default)] From 8e58450d62f65ce164c7144f5cc4f55df145a724 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 25 Feb 2026 15:07:02 -0600 Subject: [PATCH 2/8] Add tests for Prebid debug OpenRTB flags --- crates/common/src/integrations/prebid.rs | 154 ++++++++++++++++++++++- 1 file changed, 150 insertions(+), 4 deletions(-) diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index f55bc360..de252e00 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -608,10 +608,12 @@ impl PrebidAuctionProvider { }) .unwrap_or((None, None, None, None)); + let debug_enabled = self.config.debug; + let ext = Some(RequestExt { prebid: Some(PrebidExt { - debug: if self.config.debug { Some(true) } else { None }, - returnallbidstatus: if self.config.debug { Some(true) } else { None }, + debug: debug_enabled.then_some(true), + returnallbidstatus: debug_enabled.then_some(true), }), trusted_server: Some(TrustedServerExt { version, @@ -633,7 +635,7 @@ impl PrebidAuctionProvider { user, device, regs, - test: if self.config.debug { Some(1) } else { None }, + test: debug_enabled.then_some(1), ext, } } @@ -936,7 +938,7 @@ pub fn register_auction_provider(settings: &Settings) -> Vec Settings { @@ -967,6 +971,47 @@ mod tests { } } + fn create_test_auction_request() -> AuctionRequest { + AuctionRequest { + id: "auction-123".to_string(), + slots: vec![AdSlot { + id: "slot-1".to_string(), + formats: vec![AdFormat { + media_type: MediaType::Banner, + width: 300, + height: 250, + }], + floor_price: None, + targeting: HashMap::new(), + bidders: HashMap::new(), + }], + publisher: PublisherInfo { + domain: "pub.example".to_string(), + page_url: Some("https://pub.example/article".to_string()), + }, + user: UserInfo { + id: "user-123".to_string(), + fresh_id: "fresh-456".to_string(), + consent: None, + }, + device: None, + site: None, + context: HashMap::new(), + } + } + + fn create_test_auction_context<'a>( + settings: &'a Settings, + request: &'a Request, + ) -> AuctionContext<'a> { + AuctionContext { + settings, + request, + timeout_ms: 1000, + provider_responses: None, + } + } + fn config_from_settings( settings: &Settings, registry: &IntegrationRegistry, @@ -1373,6 +1418,107 @@ server_url = "https://prebid.example" ); } + #[test] + fn to_openrtb_includes_debug_flags_when_enabled() { + let mut config = base_config(); + config.debug = true; + + let provider = PrebidAuctionProvider::new(config); + let auction_request = create_test_auction_request(); + let settings = make_settings(); + let request = Request::get("https://pub.example/auction"); + let context = create_test_auction_context(&settings, &request); + + let openrtb = provider.to_openrtb(&auction_request, &context, None); + + assert_eq!( + openrtb.test, + Some(1), + "should set top-level OpenRTB test field when debug is enabled" + ); + + let prebid_ext = openrtb + .ext + .as_ref() + .and_then(|ext| ext.prebid.as_ref()) + .expect("should include ext.prebid"); + assert_eq!( + prebid_ext.debug, + Some(true), + "should include ext.prebid.debug when debug is enabled" + ); + assert_eq!( + prebid_ext.returnallbidstatus, + Some(true), + "should include ext.prebid.returnallbidstatus when debug is enabled" + ); + + let serialized = serde_json::to_value(&openrtb).expect("should serialize OpenRTB request"); + assert_eq!( + serialized["test"], + json!(1), + "should serialize top-level test as 1 when debug is enabled" + ); + assert_eq!( + serialized["ext"]["prebid"]["debug"], + json!(true), + "should serialize ext.prebid.debug when debug is enabled" + ); + assert_eq!( + serialized["ext"]["prebid"]["returnallbidstatus"], + json!(true), + "should serialize ext.prebid.returnallbidstatus when debug is enabled" + ); + } + + #[test] + fn to_openrtb_omits_debug_flags_when_disabled() { + let provider = PrebidAuctionProvider::new(base_config()); + let auction_request = create_test_auction_request(); + let settings = make_settings(); + let request = Request::get("https://pub.example/auction"); + let context = create_test_auction_context(&settings, &request); + + let openrtb = provider.to_openrtb(&auction_request, &context, None); + + assert_eq!( + openrtb.test, None, + "should omit top-level OpenRTB test field when debug is disabled" + ); + + let prebid_ext = openrtb + .ext + .as_ref() + .and_then(|ext| ext.prebid.as_ref()) + .expect("should include ext.prebid"); + assert_eq!( + prebid_ext.debug, None, + "should omit ext.prebid.debug when debug is disabled" + ); + assert_eq!( + prebid_ext.returnallbidstatus, None, + "should omit ext.prebid.returnallbidstatus when debug is disabled" + ); + + let serialized = serde_json::to_value(&openrtb).expect("should serialize OpenRTB request"); + assert!( + serialized.get("test").is_none(), + "should not serialize top-level test when debug is disabled" + ); + + let prebid = serialized["ext"]["prebid"] + .as_object() + .expect("should serialize ext.prebid object"); + assert!( + !prebid.contains_key("debug"), + "should not serialize ext.prebid.debug when debug is disabled" + ); + assert!( + !prebid.contains_key("returnallbidstatus"), + "should not serialize ext.prebid.returnallbidstatus when debug is disabled" + ); + } + #[test] fn expand_trusted_server_bidders_uses_per_bidder_map_when_present() { let params = json!({ From 3d752dc3608979dae6c295972dcc586fbbdb72f9 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 26 Feb 2026 14:40:31 -0600 Subject: [PATCH 3/8] Surface Prebid debug data (httpcalls, bidstatus, warnings) in /auction response metadata --- crates/common/src/integrations/prebid.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index de252e00..ccb3b894 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -876,6 +876,24 @@ impl AuctionProvider for PrebidAuctionProvider { if let Some(errors) = ext.and_then(|e| e.get("errors")) { auction_response = auction_response.with_metadata("errors", errors.clone()); } + if let Some(warnings) = ext.and_then(|e| e.get("warnings")) { + auction_response = auction_response.with_metadata("warnings", warnings.clone()); + } + + // When debug is enabled, surface the additional Prebid Server debug + // data: httpcalls, resolvedrequest, and per-bid status. + if self.config.debug { + if let Some(debug) = ext.and_then(|e| e.get("debug")) { + auction_response = auction_response.with_metadata("debug", debug.clone()); + } + if let Some(bidstatus) = ext + .and_then(|e| e.get("prebid")) + .and_then(|p| p.get("bidstatus")) + { + auction_response = + auction_response.with_metadata("bidstatus", bidstatus.clone()); + } + } log::info!( "Prebid returned {} bids in {}ms", From 4cabee5511f8f94d65ba07f992d47bd882dbec5e Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 26 Feb 2026 14:50:56 -0600 Subject: [PATCH 4/8] Address review: add log guard, startup warning, extract enrich_response_metadata with tests --- crates/common/src/integrations/prebid.rs | 190 +++++++++++++++++------ 1 file changed, 142 insertions(+), 48 deletions(-) diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index ccb3b894..87a776c6 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -668,6 +668,54 @@ impl PrebidAuctionProvider { } } + /// Enrich an [`AuctionResponse`] with metadata extracted from the Prebid + /// Server response `ext`. + /// + /// Always-on fields: `responsetimemillis`, `errors`, `warnings`. + /// Debug-only fields (gated on `config.debug`): `debug`, `bidstatus`. + fn enrich_response_metadata( + &self, + response_json: &Json, + auction_response: &mut AuctionResponse, + ) { + let ext = response_json.get("ext"); + + // Always attach per-bidder timing and diagnostics. + if let Some(rtm) = ext.and_then(|e| e.get("responsetimemillis")) { + auction_response + .metadata + .insert("responsetimemillis".to_string(), rtm.clone()); + } + if let Some(errors) = ext.and_then(|e| e.get("errors")) { + auction_response + .metadata + .insert("errors".to_string(), errors.clone()); + } + if let Some(warnings) = ext.and_then(|e| e.get("warnings")) { + auction_response + .metadata + .insert("warnings".to_string(), warnings.clone()); + } + + // When debug is enabled, surface httpcalls, resolvedrequest, and + // per-bid status from the Prebid Server response. + if self.config.debug { + if let Some(debug) = ext.and_then(|e| e.get("debug")) { + auction_response + .metadata + .insert("debug".to_string(), debug.clone()); + } + if let Some(bidstatus) = ext + .and_then(|e| e.get("prebid")) + .and_then(|p| p.get("bidstatus")) + { + auction_response + .metadata + .insert("bidstatus".to_string(), bidstatus.clone()); + } + } + } + /// Parse a single bid from `OpenRTB` response. fn parse_bid(&self, bid_obj: &Json, seat: &str) -> Result { let slot_id = bid_obj @@ -833,7 +881,7 @@ impl AuctionProvider for PrebidAuctionProvider { // Log the full response body when debug is enabled to surface // ext.debug.httpcalls, resolvedrequest, bidstatus, errors, etc. - if self.config.debug { + if self.config.debug && log::log_enabled!(log::Level::Debug) { match serde_json::to_string_pretty(&response_json) { Ok(json) => log::debug!("Prebid OpenRTB response:\n{json}"), Err(e) => { @@ -864,36 +912,7 @@ impl AuctionProvider for PrebidAuctionProvider { } let mut auction_response = self.parse_openrtb_response(&response_json, response_time_ms); - - // Attach per-bidder timing and errors from the Prebid Server response. - // `responsetimemillis` contains an entry for every invited bidder, even - // those that returned no bids, making it the canonical source for - // "who was in the auction." - let ext = response_json.get("ext"); - if let Some(rtm) = ext.and_then(|e| e.get("responsetimemillis")) { - auction_response = auction_response.with_metadata("responsetimemillis", rtm.clone()); - } - if let Some(errors) = ext.and_then(|e| e.get("errors")) { - auction_response = auction_response.with_metadata("errors", errors.clone()); - } - if let Some(warnings) = ext.and_then(|e| e.get("warnings")) { - auction_response = auction_response.with_metadata("warnings", warnings.clone()); - } - - // When debug is enabled, surface the additional Prebid Server debug - // data: httpcalls, resolvedrequest, and per-bid status. - if self.config.debug { - if let Some(debug) = ext.and_then(|e| e.get("debug")) { - auction_response = auction_response.with_metadata("debug", debug.clone()); - } - if let Some(bidstatus) = ext - .and_then(|e| e.get("prebid")) - .and_then(|p| p.get("bidstatus")) - { - auction_response = - auction_response.with_metadata("bidstatus", bidstatus.clone()); - } - } + self.enrich_response_metadata(&response_json, &mut auction_response); log::info!( "Prebid returned {} bids in {}ms", @@ -939,6 +958,12 @@ pub fn register_auction_provider(settings: &Settings) -> Vec { @@ -1890,10 +1915,9 @@ fixed_bottom = {placementId = "_s2sBottom"} } #[test] - fn parse_response_preserves_responsetimemillis_and_errors_metadata() { + fn enrich_response_metadata_attaches_always_on_fields() { let provider = PrebidAuctionProvider::new(base_config()); - // Minimal valid OpenRTB response with ext containing diagnostics. let response_json = json!({ "seatbid": [{ "seat": "kargo", @@ -1911,39 +1935,109 @@ fixed_bottom = {placementId = "_s2sBottom"} }, "errors": { "openx": [{"code": 1, "message": "timeout"}] + }, + "warnings": { + "kargo": [{"code": 10, "message": "bid floor"}] + }, + "debug": { + "httpcalls": {"kargo": []} + }, + "prebid": { + "bidstatus": [{"bidder": "kargo", "status": "bid"}] } } }); - // Replicate the metadata extraction logic from `run_auction`. let mut auction_response = provider.parse_openrtb_response(&response_json, 150); + provider.enrich_response_metadata(&response_json, &mut auction_response); - let ext = response_json.get("ext"); - if let Some(rtm) = ext.and_then(|e| e.get("responsetimemillis")) { - auction_response = auction_response.with_metadata("responsetimemillis", rtm.clone()); - } - if let Some(errors) = ext.and_then(|e| e.get("errors")) { - auction_response = auction_response.with_metadata("errors", errors.clone()); - } - - // Verify bids were parsed. assert_eq!(auction_response.bids.len(), 1, "should parse one bid"); - // Verify responsetimemillis is preserved. + // Always-on fields should be present. let rtm = auction_response .metadata .get("responsetimemillis") - .expect("should have responsetimemillis in metadata"); + .expect("should have responsetimemillis"); assert_eq!(rtm["kargo"], 98); assert_eq!(rtm["appnexus"], 0); assert_eq!(rtm["ix"], 120); - // Verify errors are preserved. let errors = auction_response .metadata .get("errors") - .expect("should have errors in metadata"); + .expect("should have errors"); assert_eq!(errors["openx"][0]["code"], 1); - assert_eq!(errors["openx"][0]["message"], "timeout"); + + let warnings = auction_response + .metadata + .get("warnings") + .expect("should have warnings"); + assert_eq!(warnings["kargo"][0]["code"], 10); + + // Debug-gated fields should NOT be present (base_config has debug: false). + assert!( + !auction_response.metadata.contains_key("debug"), + "should not include debug when config.debug is false" + ); + assert!( + !auction_response.metadata.contains_key("bidstatus"), + "should not include bidstatus when config.debug is false" + ); + } + + #[test] + fn enrich_response_metadata_includes_debug_fields_when_enabled() { + let mut config = base_config(); + config.debug = true; + let provider = PrebidAuctionProvider::new(config); + + let response_json = json!({ + "seatbid": [], + "ext": { + "responsetimemillis": {"kargo": 50}, + "debug": { + "httpcalls": {"kargo": [{"uri": "https://pbs.example/bid", "status": 200}]}, + "resolvedrequest": {"id": "resolved-123"} + }, + "prebid": { + "bidstatus": [ + {"bidder": "kargo", "status": "bid"}, + {"bidder": "openx", "status": "timeout"} + ] + } + } + }); + + let mut auction_response = provider.parse_openrtb_response(&response_json, 100); + provider.enrich_response_metadata(&response_json, &mut auction_response); + + // Always-on field should still be present. + assert!( + auction_response.metadata.contains_key("responsetimemillis"), + "should have responsetimemillis" + ); + + // Debug-gated fields should now be present. + let debug = auction_response + .metadata + .get("debug") + .expect("should have debug when config.debug is true"); + assert_eq!( + debug["httpcalls"]["kargo"][0]["status"], 200, + "should include httpcalls from PBS debug response" + ); + assert_eq!( + debug["resolvedrequest"]["id"], "resolved-123", + "should include resolvedrequest from PBS debug response" + ); + + let bidstatus = auction_response + .metadata + .get("bidstatus") + .expect("should have bidstatus when config.debug is true"); + let statuses = bidstatus.as_array().expect("bidstatus should be array"); + assert_eq!(statuses.len(), 2); + assert_eq!(statuses[0]["bidder"], "kargo"); + assert_eq!(statuses[1]["status"], "timeout"); } } From 19c7feb95439a44a281b36437906c16fb88d060f Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 26 Feb 2026 17:19:56 -0600 Subject: [PATCH 5/8] Decouple OpenRTB test flag from debug to avoid suppressing bidder demand PubMatic (and likely other bidders) heavily throttle fill on test:1 traffic (~10% vs ~100%). Previously debug=true set test:1, debug ext fields, and returnallbidstatus together. Now test:1 is controlled by a separate test_mode config param so debug diagnostics can be enabled in production without killing fill rates. --- .env.example | 1 + crates/common/src/integrations/prebid.rs | 52 +++++++++++++++++++----- trusted-server.toml | 1 + 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index e18c9860..c62b4a25 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,7 @@ TRUSTED_SERVER__INTEGRATIONS__PREBID__ENABLED=false # TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,rubicon,appnexus # TRUSTED_SERVER__INTEGRATIONS__PREBID__AUTO_CONFIGURE=false # TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false +# TRUSTED_SERVER__INTEGRATIONS__PREBID__TEST_MODE=false # Next.js TRUSTED_SERVER__INTEGRATIONS__NEXTJS__ENABLED=false diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index 87a776c6..4c617b3e 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -53,6 +53,12 @@ pub struct PrebidIntegrationConfig { pub bidders: Vec, #[serde(default)] pub debug: bool, + /// Sets the `OpenRTB` `test: 1` flag on outgoing requests. When enabled, + /// bidders treat the auction as non-billable test traffic, which can + /// significantly reduce fill rates. Separate from `debug` so you can get + /// debug diagnostics without suppressing real demand. + #[serde(default)] + pub test_mode: bool, #[serde(default)] pub debug_query_params: Option, /// Patterns to match Prebid script URLs for serving empty JS. @@ -635,7 +641,7 @@ impl PrebidAuctionProvider { user, device, regs, - test: debug_enabled.then_some(1), + test: self.config.test_mode.then_some(1), ext, } } @@ -1008,6 +1014,7 @@ mod tests { timeout_ms: 1000, bidders: vec!["exampleBidder".to_string()], debug: false, + test_mode: false, debug_query_params: None, script_patterns: default_script_patterns(), bid_param_zone_overrides: HashMap::new(), @@ -1475,9 +1482,8 @@ server_url = "https://prebid.example" let openrtb = provider.to_openrtb(&auction_request, &context, None); assert_eq!( - openrtb.test, - Some(1), - "should set top-level OpenRTB test field when debug is enabled" + openrtb.test, None, + "debug alone should not set top-level OpenRTB test field" ); let prebid_ext = openrtb @@ -1497,10 +1503,9 @@ server_url = "https://prebid.example" ); let serialized = serde_json::to_value(&openrtb).expect("should serialize OpenRTB request"); - assert_eq!( - serialized["test"], - json!(1), - "should serialize top-level test as 1 when debug is enabled" + assert!( + serialized.get("test").is_none(), + "debug alone should not serialize top-level test" ); assert_eq!( serialized["ext"]["prebid"]["debug"], @@ -1514,6 +1519,33 @@ server_url = "https://prebid.example" ); } + #[test] + fn to_openrtb_sets_test_flag_when_test_mode_enabled() { + let mut config = base_config(); + config.test_mode = true; + + let provider = PrebidAuctionProvider::new(config); + let auction_request = create_test_auction_request(); + let settings = make_settings(); + let request = Request::get("https://pub.example/auction"); + let context = create_test_auction_context(&settings, &request); + + let openrtb = provider.to_openrtb(&auction_request, &context, None); + + assert_eq!( + openrtb.test, + Some(1), + "should set top-level OpenRTB test field when test_mode is enabled" + ); + + let serialized = serde_json::to_value(&openrtb).expect("should serialize OpenRTB request"); + assert_eq!( + serialized["test"], + json!(1), + "should serialize top-level test as 1 when test_mode is enabled" + ); + } + #[test] fn to_openrtb_omits_debug_flags_when_disabled() { let provider = PrebidAuctionProvider::new(base_config()); @@ -1526,7 +1558,7 @@ server_url = "https://prebid.example" assert_eq!( openrtb.test, None, - "should omit top-level OpenRTB test field when debug is disabled" + "should omit top-level OpenRTB test field when test_mode is disabled" ); let prebid_ext = openrtb @@ -1546,7 +1578,7 @@ server_url = "https://prebid.example" let serialized = serde_json::to_value(&openrtb).expect("should serialize OpenRTB request"); assert!( serialized.get("test").is_none(), - "should not serialize top-level test when debug is disabled" + "should not serialize top-level test when test_mode is disabled" ); let prebid = serialized["ext"]["prebid"] diff --git a/trusted-server.toml b/trusted-server.toml index 79c0f31c..1f5824f4 100644 --- a/trusted-server.toml +++ b/trusted-server.toml @@ -42,6 +42,7 @@ server_url = "http://68.183.113.79:8000" timeout_ms = 1000 bidders = ["kargo", "rubicon", "appnexus", "openx"] debug = false +# test_mode = false # debug_query_params = "" # script_patterns = ["/prebid.js"] From cb0486f691f77ea1568bde0b44ec90147ce3ebd4 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 26 Feb 2026 17:30:36 -0600 Subject: [PATCH 6/8] Forward real client IP in device.ip to PBS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, PBS infers the IP from the incoming connection which is a Fastly edge/data-center IP. Bidders like PubMatic filter data-center IPs as non-human traffic, resulting in 0% fill through Trusted Server. The client IP is already available via get_client_ip_addr() and stored in DeviceInfo.ip — it just wasn't being passed through to the OpenRTB Device object. --- crates/common/src/integrations/prebid.rs | 6 +++++- crates/common/src/openrtb.rs | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index 4c617b3e..08186440 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -579,9 +579,13 @@ impl PrebidAuctionProvider { }), }); - // Build device object with user-agent and geo if available + // Build device object with user-agent, client IP, and geo if available. + // Forwarding the real client IP is critical: without it PBS infers the + // IP from the incoming connection (a data-center / edge IP), causing + // bidders like PubMatic to filter the traffic as non-human. let device = request.device.as_ref().map(|d| Device { ua: d.user_agent.clone(), + ip: d.ip.clone(), geo: d.geo.as_ref().map(|geo| Geo { geo_type: 2, // IP address per OpenRTB spec country: Some(geo.country.clone()), diff --git a/crates/common/src/openrtb.rs b/crates/common/src/openrtb.rs index 986f4f5b..9af3be69 100644 --- a/crates/common/src/openrtb.rs +++ b/crates/common/src/openrtb.rs @@ -71,6 +71,8 @@ pub struct Device { #[serde(skip_serializing_if = "Option::is_none")] pub ua: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub ip: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub geo: Option, } From 45c3b40107b37b58956d40750bc2cf0fd2b25213 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 26 Feb 2026 18:12:36 -0600 Subject: [PATCH 7/8] Document Prebid debug flags, test_mode, and response metadata enrichment --- docs/guide/auction-orchestration.md | 19 ++++---- docs/guide/configuration.md | 21 +++++---- docs/guide/integrations/prebid.md | 69 ++++++++++++++++++++++++----- 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/docs/guide/auction-orchestration.md b/docs/guide/auction-orchestration.md index 6aef065e..520a0e6a 100644 --- a/docs/guide/auction-orchestration.md +++ b/docs/guide/auction-orchestration.md @@ -341,6 +341,8 @@ Transforms auction requests into OpenRTB 2.x format and sends them to a Prebid S - Bids include decoded `price` (clear decimal CPM) - Creative HTML provided in `adm` field - Creative URLs rewritten to first-party proxy format +- Per-bidder timing (`responsetimemillis`), errors, and warnings always attached as response metadata +- When `debug` is enabled, PBS debug payload and per-bid status (`bidstatus`) also included ```toml [integrations.prebid] @@ -619,14 +621,15 @@ price_floor = 0.50 #### `[integrations.prebid]` -| Field | Type | Default | Description | -| ---------------- | -------- | ----------------- | ------------------------------------------- | -| `enabled` | bool | `true` | Enable Prebid provider | -| `server_url` | string | — | Prebid Server URL (required) | -| `timeout_ms` | u32 | `1000` | Request timeout | -| `bidders` | string[] | `["mocktioneer"]` | Default bidders when not specified per-slot | -| `auto_configure` | bool | `true` | Auto-remove client-side prebid.js scripts | -| `debug` | bool | `false` | Enable Prebid debug mode | +| Field | Type | Default | Description | +| ---------------- | -------- | ----------------- | --------------------------------------------------------------------------- | +| `enabled` | bool | `true` | Enable Prebid provider | +| `server_url` | string | — | Prebid Server URL (required) | +| `timeout_ms` | u32 | `1000` | Request timeout | +| `bidders` | string[] | `["mocktioneer"]` | Default bidders when not specified per-slot | +| `auto_configure` | bool | `true` | Auto-remove client-side prebid.js scripts | +| `debug` | bool | `false` | Enable Prebid debug mode (sets `ext.prebid.debug` and `returnallbidstatus`) | +| `test_mode` | bool | `false` | Set OpenRTB `test: 1` for non-billable test traffic | #### `[integrations.aps]` diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 8ae8243b..0984f2cd 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -735,15 +735,16 @@ apply when the integration section exists in `trusted-server.toml`. **Section**: `[integrations.prebid]` -| Field | Type | Default | Description | -| -------------------- | ------------- | ---------------------------------------------------------------------- | ------------------------------------------- | -| `enabled` | Boolean | `true` | Enable Prebid integration | -| `server_url` | String | Required | Prebid Server endpoint URL | -| `timeout_ms` | Integer | `1000` | Request timeout in milliseconds | -| `bidders` | Array[String] | `["mocktioneer"]` | List of enabled bidders | -| `debug` | Boolean | `false` | Enable debug logging | -| `debug_query_params` | String | `None` | Extra query params appended for debugging | -| `script_patterns` | Array[String] | `["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"]` | URL patterns for Prebid script interception | +| Field | Type | Default | Description | +| -------------------- | ------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `enabled` | Boolean | `true` | Enable Prebid integration | +| `server_url` | String | Required | Prebid Server endpoint URL | +| `timeout_ms` | Integer | `1000` | Request timeout in milliseconds | +| `bidders` | Array[String] | `["mocktioneer"]` | List of enabled bidders | +| `debug` | Boolean | `false` | Enable debug mode (sets `ext.prebid.debug` and `returnallbidstatus`; surfaces debug metadata in responses) | +| `test_mode` | Boolean | `false` | Set OpenRTB `test: 1` flag for non-billable test traffic (independent of `debug`) | +| `debug_query_params` | String | `None` | Extra query params appended for debugging | +| `script_patterns` | Array[String] | `["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"]` | URL patterns for Prebid script interception | **Example**: @@ -754,6 +755,7 @@ server_url = "https://prebid-server.example/openrtb2/auction" timeout_ms = 1200 bidders = ["kargo", "rubicon", "appnexus", "openx"] debug = false +# test_mode = false # Customize script interception (optional) script_patterns = ["/prebid.js", "/prebid.min.js"] @@ -767,6 +769,7 @@ TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL=https://prebid.example/auction TRUSTED_SERVER__INTEGRATIONS__PREBID__TIMEOUT_MS=1200 TRUSTED_SERVER__INTEGRATIONS__PREBID__BIDDERS=kargo,rubicon,appnexus TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG=false +TRUSTED_SERVER__INTEGRATIONS__PREBID__TEST_MODE=false TRUSTED_SERVER__INTEGRATIONS__PREBID__DEBUG_QUERY_PARAMS=debug=1 TRUSTED_SERVER__INTEGRATIONS__PREBID__SCRIPT_PATTERNS='["/prebid.js","/prebid.min.js"]' ``` diff --git a/docs/guide/integrations/prebid.md b/docs/guide/integrations/prebid.md index cad1f8b7..5dddc1ff 100644 --- a/docs/guide/integrations/prebid.md +++ b/docs/guide/integrations/prebid.md @@ -21,6 +21,7 @@ server_url = "https://prebid-server.example.com/openrtb2/auction" timeout_ms = 1200 bidders = ["kargo", "rubicon", "appnexus"] debug = false +# test_mode = false # Script interception patterns (optional - defaults shown below) script_patterns = ["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"] @@ -33,16 +34,60 @@ in_content = {placementId = "_s2sContentPlacement"} ### Configuration Options -| Field | Type | Default | Description | -| -------------------------- | ------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| `enabled` | Boolean | `true` | Enable Prebid integration | -| `server_url` | String | Required | Prebid Server endpoint URL | -| `timeout_ms` | Integer | `1000` | Request timeout in milliseconds | -| `bidders` | Array[String] | `["mocktioneer"]` | List of enabled bidders | -| `bid_param_zone_overrides` | Table | `{}` | Per-bidder, per-zone param overrides; zone values are shallow-merged into bidder params | -| `debug` | Boolean | `false` | Enable debug logging | -| `debug_query_params` | String | `None` | Extra query params appended for debugging | -| `script_patterns` | Array[String] | `["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"]` | URL patterns for Prebid script interception | +| Field | Type | Default | Description | +| -------------------------- | ------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `enabled` | Boolean | `true` | Enable Prebid integration | +| `server_url` | String | Required | Prebid Server endpoint URL | +| `timeout_ms` | Integer | `1000` | Request timeout in milliseconds | +| `bidders` | Array[String] | `["mocktioneer"]` | List of enabled bidders | +| `bid_param_zone_overrides` | Table | `{}` | Per-bidder, per-zone param overrides; zone values are shallow-merged into bidder params | +| `debug` | Boolean | `false` | Enable Prebid debug mode (sets `ext.prebid.debug` and `ext.prebid.returnallbidstatus`; surfaces debug metadata in auction responses) | +| `test_mode` | Boolean | `false` | Set the OpenRTB `test: 1` flag so bidders treat the auction as non-billable test traffic. Separate from `debug` to avoid suppressing real demand | +| `debug_query_params` | String | `None` | Extra query params appended for debugging | +| `script_patterns` | Array[String] | `["/prebid.js", "/prebid.min.js", "/prebidjs.js", "/prebidjs.min.js"]` | URL patterns for Prebid script interception | + +## Debug Mode + +When `debug = true`, the Prebid integration enables additional diagnostics on both the outgoing OpenRTB request and the incoming response. + +### Outgoing request flags + +| OpenRTB field | Value | Purpose | +| ------------------------------- | ------ | --------------------------------------------------------------------------------------------------- | +| `ext.prebid.debug` | `true` | Tells Prebid Server to include `ext.debug` in the response (httpcalls, resolvedrequest) | +| `ext.prebid.returnallbidstatus` | `true` | Asks Prebid Server to return per-bid status for every bidder, including those that returned no bids | + +### Response metadata enrichment + +The Prebid provider extracts metadata from the Prebid Server response and attaches it to the `AuctionResponse.metadata` map: + +**Always-on fields** (present regardless of `debug`): + +| Key | Source | Description | +| -------------------- | ------------------------ | ------------------------------ | +| `responsetimemillis` | `ext.responsetimemillis` | Per-bidder response times (ms) | +| `errors` | `ext.errors` | Per-bidder error diagnostics | +| `warnings` | `ext.warnings` | Per-bidder warning diagnostics | + +**Debug-only fields** (only when `debug = true`): + +| Key | Source | Description | +| ----------- | ---------------------- | -------------------------------------------------------- | +| `debug` | `ext.debug` | Prebid Server debug payload (httpcalls, resolvedrequest) | +| `bidstatus` | `ext.prebid.bidstatus` | Per-bid status from every invited bidder | + +::: warning +Enabling `debug` increases response sizes and adds overhead. Use it in development or when diagnosing auction issues — not in production. +::: + +### Test mode vs. debug + +`test_mode` and `debug` are independent flags: + +- **`debug`** — Enables diagnostic data without affecting bidder behavior. Bidders still treat the auction as live. +- **`test_mode`** — Sets the top-level OpenRTB `test: 1` flag. Bidders treat the request as non-billable test traffic, which can significantly reduce fill rates. + +You can combine both to get debug diagnostics on test traffic, or use `debug` alone to inspect live auctions without affecting revenue. ## Features @@ -197,7 +242,9 @@ The `to_openrtb()` method in `PrebidAuctionProvider` builds OpenRTB requests: - Converts ad slots to OpenRTB `imp` objects with bidder params - Adds site metadata with publisher domain and page URL - Injects synthetic ID in the user object -- Includes device/geo information when available +- Includes device info (user-agent, client IP) and geo when available +- Sets `ext.prebid.debug` and `ext.prebid.returnallbidstatus` when `debug` is enabled +- Sets the top-level `test: 1` flag when `test_mode` is enabled - Appends `debug_query_params` to page URL when configured - Applies `bid_param_zone_overrides` to `imp.ext.prebid.bidder` before request dispatch - Signs requests when request signing is enabled From db544bf2b9cd52cb73878bed3fb1ec19ed13c581 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 27 Feb 2026 06:19:07 -0600 Subject: [PATCH 8/8] Add device.ip regression test and clarify Prebid debug docs --- crates/common/src/integrations/prebid.rs | 32 ++++++++++++++++++++++++ docs/guide/auction-orchestration.md | 18 ++++++------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/crates/common/src/integrations/prebid.rs b/crates/common/src/integrations/prebid.rs index 08186440..fb3fccc8 100644 --- a/crates/common/src/integrations/prebid.rs +++ b/crates/common/src/integrations/prebid.rs @@ -1550,6 +1550,38 @@ server_url = "https://prebid.example" ); } + #[test] + fn to_openrtb_serializes_device_ip_when_present() { + let provider = PrebidAuctionProvider::new(base_config()); + let mut auction_request = create_test_auction_request(); + auction_request.device = Some(DeviceInfo { + user_agent: Some("test-agent".to_string()), + ip: Some("203.0.113.42".to_string()), + geo: None, + }); + let settings = make_settings(); + let request = Request::get("https://pub.example/auction"); + let context = create_test_auction_context(&settings, &request); + + let openrtb = provider.to_openrtb(&auction_request, &context, None); + + assert_eq!( + openrtb + .device + .as_ref() + .and_then(|device| device.ip.as_deref()), + Some("203.0.113.42"), + "should propagate client IP into OpenRTB device.ip" + ); + + let serialized = serde_json::to_value(&openrtb).expect("should serialize OpenRTB request"); + assert_eq!( + serialized["device"]["ip"], + json!("203.0.113.42"), + "should serialize device.ip when client IP is available" + ); + } + #[test] fn to_openrtb_omits_debug_flags_when_disabled() { let provider = PrebidAuctionProvider::new(base_config()); diff --git a/docs/guide/auction-orchestration.md b/docs/guide/auction-orchestration.md index 520a0e6a..5c3ca8dd 100644 --- a/docs/guide/auction-orchestration.md +++ b/docs/guide/auction-orchestration.md @@ -621,15 +621,15 @@ price_floor = 0.50 #### `[integrations.prebid]` -| Field | Type | Default | Description | -| ---------------- | -------- | ----------------- | --------------------------------------------------------------------------- | -| `enabled` | bool | `true` | Enable Prebid provider | -| `server_url` | string | — | Prebid Server URL (required) | -| `timeout_ms` | u32 | `1000` | Request timeout | -| `bidders` | string[] | `["mocktioneer"]` | Default bidders when not specified per-slot | -| `auto_configure` | bool | `true` | Auto-remove client-side prebid.js scripts | -| `debug` | bool | `false` | Enable Prebid debug mode (sets `ext.prebid.debug` and `returnallbidstatus`) | -| `test_mode` | bool | `false` | Set OpenRTB `test: 1` for non-billable test traffic | +| Field | Type | Default | Description | +| ---------------- | -------- | ----------------- | -------------------------------------------------------------------------------------- | +| `enabled` | bool | `true` | Enable Prebid provider | +| `server_url` | string | — | Prebid Server URL (required) | +| `timeout_ms` | u32 | `1000` | Request timeout | +| `bidders` | string[] | `["mocktioneer"]` | Default bidders when not specified per-slot | +| `auto_configure` | bool | `true` | Auto-remove client-side prebid.js scripts | +| `debug` | bool | `false` | Enable Prebid debug mode (sets `ext.prebid.debug` and `ext.prebid.returnallbidstatus`) | +| `test_mode` | bool | `false` | Set OpenRTB `test: 1` for non-billable test traffic | #### `[integrations.aps]`