11use std:: collections:: HashMap ;
22use std:: sync:: Arc ;
33
4+ use crate :: settings:: map_from_obj_or_str;
45use async_trait:: async_trait;
56use base64:: { engine:: general_purpose:: STANDARD as BASE64 , Engine } ;
67use error_stack:: { Report , ResultExt } ;
@@ -9,7 +10,6 @@ use fastly::{Request, Response};
910use serde:: { Deserialize , Serialize } ;
1011use serde_json:: { json, Value as Json } ;
1112use validator:: Validate ;
12- use crate :: settings:: map_from_obj_or_str;
1313
1414use crate :: auction:: provider:: AuctionProvider ;
1515use crate :: auction:: types:: {
@@ -33,6 +33,7 @@ use crate::settings::{IntegrationConfig, Settings};
3333const PREBID_INTEGRATION_ID : & str = "prebid" ;
3434const TRUSTED_SERVER_BIDDER : & str = "trustedServer" ;
3535const BIDDER_PARAMS_KEY : & str = "bidderParams" ;
36+ const ZONE_KEY : & str = "zone" ;
3637
3738#[ derive( Debug , Clone , Deserialize , Serialize , Validate ) ]
3839pub struct PrebidIntegrationConfig {
@@ -75,6 +76,22 @@ pub struct PrebidIntegrationConfig {
7576 /// ```
7677 #[ serde( default , deserialize_with = "map_from_obj_or_str" ) ]
7778 pub bid_param_overrides : HashMap < String , Json > ,
79+ /// Per-bidder, per-zone param overrides. The outer key is a bidder name, the
80+ /// inner key is a zone name (sent by the JS adapter from the ad-unit code),
81+ /// and the value is a JSON object shallow-merged into that bidder's params.
82+ ///
83+ /// When a matching zone override is found for a bidder it takes precedence
84+ /// over any entry in [`bid_param_overrides`] for that bidder.
85+ ///
86+ /// Example in TOML:
87+ /// ```toml
88+ /// [integrations.prebid.bid_param_zone_overrides.kargo]
89+ /// header = {placementId = "_s2sHeaderId"}
90+ /// in_content = {placementId = "_s2sContentId"}
91+ /// fixed_bottom = {placementId = "_s2sBottomId"}
92+ /// ```
93+ #[ serde( default , deserialize_with = "map_from_obj_or_str" ) ]
94+ pub bid_param_zone_overrides : HashMap < String , HashMap < String , Json > > ,
7895}
7996
8097impl IntegrationConfig for PrebidIntegrationConfig {
@@ -495,6 +512,14 @@ impl PrebidAuctionProvider {
495512 } )
496513 . collect ( ) ;
497514
515+ // Extract zone from trustedServer params (sent by the JS
516+ // adapter from the ad-unit code, e.g. "header", "fixed_bottom").
517+ let zone: Option < & str > = slot
518+ . bidders
519+ . get ( TRUSTED_SERVER_BIDDER )
520+ . and_then ( |p| p. get ( ZONE_KEY ) )
521+ . and_then ( Json :: as_str) ;
522+
498523 // Build the bidder map for PBS.
499524 // The JS adapter sends "trustedServer" as the bidder (our orchestrator
500525 // adapter name). Replace it with the real PBS bidders from config.
@@ -515,9 +540,29 @@ impl PrebidAuctionProvider {
515540 }
516541 }
517542
518- // Apply bid_param_overrides from config (shallow merge)
543+ // Apply overrides. Zone-specific overrides take precedence
544+ // over the blanket `bid_param_overrides` for the same bidder.
519545 for ( name, params) in & mut bidder {
520- if let Some ( Json :: Object ( ovr) ) = self . config . bid_param_overrides . get ( name) {
546+ let zone_override = zone. and_then ( |z| {
547+ self . config
548+ . bid_param_zone_overrides
549+ . get ( name. as_str ( ) )
550+ . and_then ( |zones| zones. get ( z) )
551+ } ) ;
552+
553+ if let Some ( Json :: Object ( ovr) ) = zone_override {
554+ if let Json :: Object ( base) = params {
555+ log:: debug!(
556+ "prebid: zone override for '{}' zone '{}': keys {:?}" ,
557+ name,
558+ zone. unwrap_or( "" ) ,
559+ ovr. keys( ) . collect:: <Vec <_>>( )
560+ ) ;
561+ base. extend ( ovr. iter ( ) . map ( |( k, v) | ( k. clone ( ) , v. clone ( ) ) ) ) ;
562+ }
563+ } else if let Some ( Json :: Object ( ovr) ) =
564+ self . config . bid_param_overrides . get ( name)
565+ {
521566 if let Json :: Object ( base) = params {
522567 log:: debug!(
523568 "prebid: overriding bidder params for '{}': keys {:?}" ,
@@ -587,6 +632,7 @@ impl PrebidAuctionProvider {
587632 let ext = Some ( RequestExt {
588633 prebid : Some ( PrebidExt {
589634 debug : if self . config . debug { Some ( true ) } else { None } ,
635+ returnallbidstatus : Some ( true ) ,
590636 } ) ,
591637 trusted_server : Some ( TrustedServerExt {
592638 signature,
@@ -793,6 +839,11 @@ impl AuctionProvider for PrebidAuctionProvider {
793839 message : "Failed to parse Prebid response" . to_string ( ) ,
794840 } ) ?;
795841
842+ match serde_json:: to_string_pretty ( & response_json) {
843+ Ok ( json) => log:: debug!( "Prebid OpenRTB response:\n {}" , json) ,
844+ Err ( e) => log:: warn!( "Prebid: failed to serialize OpenRTB response for logging: {e}" ) ,
845+ }
846+
796847 let request_host = response_json
797848 . get ( "ext" )
798849 . and_then ( |ext| ext. get ( "trusted_server" ) )
@@ -905,6 +956,7 @@ mod tests {
905956 debug_query_params : None ,
906957 script_patterns : default_script_patterns ( ) ,
907958 bid_param_overrides : HashMap :: new ( ) ,
959+ bid_param_zone_overrides : HashMap :: new ( ) ,
908960 }
909961 }
910962
@@ -1627,4 +1679,252 @@ siteId = 88888
16271679 assert_eq ! ( config. bid_param_overrides[ "rubicon" ] [ "accountId" ] , 99999 ) ;
16281680 assert_eq ! ( config. bid_param_overrides[ "rubicon" ] [ "siteId" ] , 88888 ) ;
16291681 }
1682+
1683+ // ========================================================================
1684+ // bid_param_zone_overrides tests
1685+ // ========================================================================
1686+
1687+ /// Helper: build a slot whose bidders entry is a trustedServer payload
1688+ /// with per-bidder params and an optional zone.
1689+ fn make_ts_slot ( id : & str , bidder_params : & Json , zone : Option < & str > ) -> AdSlot {
1690+ let mut ts_params = json ! ( { BIDDER_PARAMS_KEY : bidder_params } ) ;
1691+ if let Some ( z) = zone {
1692+ ts_params[ ZONE_KEY ] = json ! ( z) ;
1693+ }
1694+ make_slot (
1695+ id,
1696+ HashMap :: from ( [ ( TRUSTED_SERVER_BIDDER . to_string ( ) , ts_params) ] ) ,
1697+ )
1698+ }
1699+
1700+ #[ test]
1701+ fn zone_override_replaces_placement_id ( ) {
1702+ let mut config = base_config ( ) ;
1703+ config. bidders = vec ! [ "kargo" . to_string( ) ] ;
1704+ config. bid_param_zone_overrides . insert (
1705+ "kargo" . to_string ( ) ,
1706+ HashMap :: from ( [ (
1707+ "header" . to_string ( ) ,
1708+ json ! ( { "placementId" : "s2s_header_id" } ) ,
1709+ ) ] ) ,
1710+ ) ;
1711+
1712+ let slot = make_ts_slot (
1713+ "ad-header-0" ,
1714+ & json ! ( { "kargo" : { "placementId" : "client_side_123" } } ) ,
1715+ Some ( "header" ) ,
1716+ ) ;
1717+ let request = make_auction_request ( vec ! [ slot] ) ;
1718+
1719+ let ortb = call_to_openrtb ( config, & request) ;
1720+ assert_eq ! (
1721+ bidder_params( & ortb) [ "kargo" ] [ "placementId" ] ,
1722+ "s2s_header_id" ,
1723+ "zone override should replace the client-side placementId"
1724+ ) ;
1725+ }
1726+
1727+ #[ test]
1728+ fn zone_override_skips_bid_param_overrides_for_matched_bidder ( ) {
1729+ let mut config = base_config ( ) ;
1730+ config. bidders = vec ! [ "kargo" . to_string( ) ] ;
1731+ // Both override types configured for kargo
1732+ config
1733+ . bid_param_overrides
1734+ . insert ( "kargo" . to_string ( ) , json ! ( { "placementId" : "blanket_id" } ) ) ;
1735+ config. bid_param_zone_overrides . insert (
1736+ "kargo" . to_string ( ) ,
1737+ HashMap :: from ( [ (
1738+ "header" . to_string ( ) ,
1739+ json ! ( { "placementId" : "zone_header_id" } ) ,
1740+ ) ] ) ,
1741+ ) ;
1742+
1743+ let slot = make_ts_slot (
1744+ "ad-header-0" ,
1745+ & json ! ( { "kargo" : { "placementId" : "client_123" } } ) ,
1746+ Some ( "header" ) ,
1747+ ) ;
1748+ let request = make_auction_request ( vec ! [ slot] ) ;
1749+
1750+ let ortb = call_to_openrtb ( config, & request) ;
1751+ assert_eq ! (
1752+ bidder_params( & ortb) [ "kargo" ] [ "placementId" ] ,
1753+ "zone_header_id" ,
1754+ "zone override should win over bid_param_overrides"
1755+ ) ;
1756+ }
1757+
1758+ #[ test]
1759+ fn zone_override_falls_back_to_bid_param_overrides_for_unknown_zone ( ) {
1760+ let mut config = base_config ( ) ;
1761+ config. bidders = vec ! [ "kargo" . to_string( ) ] ;
1762+ config. bid_param_overrides . insert (
1763+ "kargo" . to_string ( ) ,
1764+ json ! ( { "placementId" : "blanket_fallback" } ) ,
1765+ ) ;
1766+ config. bid_param_zone_overrides . insert (
1767+ "kargo" . to_string ( ) ,
1768+ HashMap :: from ( [ (
1769+ "header" . to_string ( ) ,
1770+ json ! ( { "placementId" : "zone_header_id" } ) ,
1771+ ) ] ) ,
1772+ ) ;
1773+
1774+ // Zone "sidebar" is NOT in the zone overrides map
1775+ let slot = make_ts_slot (
1776+ "ad-sidebar-0" ,
1777+ & json ! ( { "kargo" : { "placementId" : "client_123" } } ) ,
1778+ Some ( "sidebar" ) ,
1779+ ) ;
1780+ let request = make_auction_request ( vec ! [ slot] ) ;
1781+
1782+ let ortb = call_to_openrtb ( config, & request) ;
1783+ assert_eq ! (
1784+ bidder_params( & ortb) [ "kargo" ] [ "placementId" ] ,
1785+ "blanket_fallback" ,
1786+ "unrecognised zone should fall back to bid_param_overrides"
1787+ ) ;
1788+ }
1789+
1790+ #[ test]
1791+ fn zone_override_no_zone_uses_bid_param_overrides ( ) {
1792+ let mut config = base_config ( ) ;
1793+ config. bidders = vec ! [ "kargo" . to_string( ) ] ;
1794+ config
1795+ . bid_param_overrides
1796+ . insert ( "kargo" . to_string ( ) , json ! ( { "placementId" : "blanket_id" } ) ) ;
1797+ config. bid_param_zone_overrides . insert (
1798+ "kargo" . to_string ( ) ,
1799+ HashMap :: from ( [ (
1800+ "header" . to_string ( ) ,
1801+ json ! ( { "placementId" : "zone_header_id" } ) ,
1802+ ) ] ) ,
1803+ ) ;
1804+
1805+ // No zone in the trustedServer params
1806+ let slot = make_ts_slot (
1807+ "slot1" ,
1808+ & json ! ( { "kargo" : { "placementId" : "client_123" } } ) ,
1809+ None ,
1810+ ) ;
1811+ let request = make_auction_request ( vec ! [ slot] ) ;
1812+
1813+ let ortb = call_to_openrtb ( config, & request) ;
1814+ assert_eq ! (
1815+ bidder_params( & ortb) [ "kargo" ] [ "placementId" ] ,
1816+ "blanket_id" ,
1817+ "missing zone should use bid_param_overrides"
1818+ ) ;
1819+ }
1820+
1821+ #[ test]
1822+ fn zone_override_only_affects_configured_bidders ( ) {
1823+ let mut config = base_config ( ) ;
1824+ config. bidders = vec ! [ "kargo" . to_string( ) , "rubicon" . to_string( ) ] ;
1825+ config. bid_param_zone_overrides . insert (
1826+ "kargo" . to_string ( ) ,
1827+ HashMap :: from ( [ (
1828+ "header" . to_string ( ) ,
1829+ json ! ( { "placementId" : "s2s_header_id" } ) ,
1830+ ) ] ) ,
1831+ ) ;
1832+
1833+ let slot = make_ts_slot (
1834+ "ad-header-0" ,
1835+ & json ! ( {
1836+ "kargo" : { "placementId" : "client_kargo" } ,
1837+ "rubicon" : { "accountId" : 100 }
1838+ } ) ,
1839+ Some ( "header" ) ,
1840+ ) ;
1841+ let request = make_auction_request ( vec ! [ slot] ) ;
1842+
1843+ let ortb = call_to_openrtb ( config, & request) ;
1844+ let params = bidder_params ( & ortb) ;
1845+ assert_eq ! (
1846+ params[ "kargo" ] [ "placementId" ] , "s2s_header_id" ,
1847+ "kargo should get zone override"
1848+ ) ;
1849+ assert_eq ! (
1850+ params[ "rubicon" ] [ "accountId" ] , 100 ,
1851+ "rubicon should be untouched"
1852+ ) ;
1853+ }
1854+
1855+ #[ test]
1856+ fn zone_override_merges_with_existing_params ( ) {
1857+ let mut config = base_config ( ) ;
1858+ config. bidders = vec ! [ "kargo" . to_string( ) ] ;
1859+ config. bid_param_zone_overrides . insert (
1860+ "kargo" . to_string ( ) ,
1861+ HashMap :: from ( [ ( "header" . to_string ( ) , json ! ( { "placementId" : "s2s_header" } ) ) ] ) ,
1862+ ) ;
1863+
1864+ // Client sends extra field alongside placementId
1865+ let slot = make_ts_slot (
1866+ "ad-header-0" ,
1867+ & json ! ( { "kargo" : { "placementId" : "client_123" , "extra" : "keep_me" } } ) ,
1868+ Some ( "header" ) ,
1869+ ) ;
1870+ let request = make_auction_request ( vec ! [ slot] ) ;
1871+
1872+ let ortb = call_to_openrtb ( config, & request) ;
1873+ let kargo = & bidder_params ( & ortb) [ "kargo" ] ;
1874+ assert_eq ! (
1875+ kargo[ "placementId" ] , "s2s_header" ,
1876+ "overridden field should have the zone value"
1877+ ) ;
1878+ assert_eq ! (
1879+ kargo[ "extra" ] , "keep_me" ,
1880+ "non-overridden fields should be preserved"
1881+ ) ;
1882+ }
1883+
1884+ #[ test]
1885+ fn zone_overrides_config_parsing_from_toml ( ) {
1886+ let toml_str = r#"
1887+ [publisher]
1888+ domain = "test-publisher.com"
1889+ cookie_domain = ".test-publisher.com"
1890+ origin_url = "https://origin.test-publisher.com"
1891+ proxy_secret = "test-secret"
1892+
1893+ [synthetic]
1894+ counter_store = "test-counter-store"
1895+ opid_store = "test-opid-store"
1896+ secret_key = "test-secret-key"
1897+ template = "{{client_ip}}:{{user_agent}}"
1898+
1899+ [integrations.prebid]
1900+ enabled = true
1901+ server_url = "https://prebid.example"
1902+
1903+ [integrations.prebid.bid_param_zone_overrides.kargo]
1904+ header = {placementId = "_s2sHeader"}
1905+ in_content = {placementId = "_s2sContent"}
1906+ fixed_bottom = {placementId = "_s2sBottom"}
1907+ "# ;
1908+
1909+ let settings = Settings :: from_toml ( toml_str) . expect ( "should parse TOML" ) ;
1910+ let config = settings
1911+ . integration_config :: < PrebidIntegrationConfig > ( "prebid" )
1912+ . expect ( "should get config" )
1913+ . expect ( "should be enabled" ) ;
1914+
1915+ let kargo_zones = & config. bid_param_zone_overrides [ "kargo" ] ;
1916+ assert_eq ! ( kargo_zones. len( ) , 3 , "should have three zone entries" ) ;
1917+ assert_eq ! (
1918+ kargo_zones[ "header" ] [ "placementId" ] , "_s2sHeader" ,
1919+ "should parse header zone"
1920+ ) ;
1921+ assert_eq ! (
1922+ kargo_zones[ "in_content" ] [ "placementId" ] , "_s2sContent" ,
1923+ "should parse in_content zone"
1924+ ) ;
1925+ assert_eq ! (
1926+ kargo_zones[ "fixed_bottom" ] [ "placementId" ] , "_s2sBottom" ,
1927+ "should parse fixed_bottom zone"
1928+ ) ;
1929+ }
16301930}
0 commit comments