Skip to content

Commit c99c320

Browse files
wip
1 parent 3ea33fb commit c99c320

6 files changed

Lines changed: 452 additions & 6 deletions

File tree

crates/common/src/integrations/prebid.rs

Lines changed: 303 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::collections::HashMap;
22
use std::sync::Arc;
33

4+
use crate::settings::map_from_obj_or_str;
45
use async_trait::async_trait;
56
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
67
use error_stack::{Report, ResultExt};
@@ -9,7 +10,6 @@ use fastly::{Request, Response};
910
use serde::{Deserialize, Serialize};
1011
use serde_json::{json, Value as Json};
1112
use validator::Validate;
12-
use crate::settings::map_from_obj_or_str;
1313

1414
use crate::auction::provider::AuctionProvider;
1515
use crate::auction::types::{
@@ -33,6 +33,7 @@ use crate::settings::{IntegrationConfig, Settings};
3333
const PREBID_INTEGRATION_ID: &str = "prebid";
3434
const TRUSTED_SERVER_BIDDER: &str = "trustedServer";
3535
const BIDDER_PARAMS_KEY: &str = "bidderParams";
36+
const ZONE_KEY: &str = "zone";
3637

3738
#[derive(Debug, Clone, Deserialize, Serialize, Validate)]
3839
pub 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

8097
impl 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
}

crates/common/src/openrtb.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ pub struct RequestExt {
109109
pub struct PrebidExt {
110110
#[serde(skip_serializing_if = "Option::is_none")]
111111
pub debug: Option<bool>,
112+
#[serde(skip_serializing_if = "Option::is_none")]
113+
pub returnallbidstatus: Option<bool>,
112114
}
113115

114116
#[derive(Debug, Serialize, Default)]

0 commit comments

Comments
 (0)