Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 247 additions & 15 deletions crates/common/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub struct PrebidIntegrationConfig {
pub auto_configure: bool,
#[serde(default)]
pub debug: bool,
#[serde(default)]
pub debug_query_params: Option<String>,
}

impl IntegrationConfig for PrebidIntegrationConfig {
Expand Down Expand Up @@ -150,6 +152,7 @@ impl PrebidIntegration {
&preview.chars().take(512).collect::<String>()
);
}
log::info!("openrtb: {:#?}", openrtb);

req.set_body_json(&openrtb)
.change_context(TrustedServerError::Prebid {
Expand Down Expand Up @@ -187,6 +190,16 @@ impl PrebidIntegration {
}));
}

// Parse bids from query parameter if present
Copy link
Collaborator

Choose a reason for hiding this comment

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

🤔 I know this is draft but can we finally move to specific structs.

let bids = qp.get("bids").and_then(|json_str| {
serde_json::from_str::<Vec<Bid>>(json_str)
.map_err(|e| {
log::warn!("Failed to parse bids parameter: {}", e);
e
})
.ok()
});

let ad_req = AdRequest {
ad_units: vec![AdUnit {
code: slot.clone(),
Expand All @@ -195,7 +208,7 @@ impl PrebidIntegration {
sizes: vec![vec![w, h]],
}),
}),
bids: None,
bids,
}],
config: None,
};
Expand Down Expand Up @@ -311,18 +324,18 @@ fn build_openrtb_from_ts(
.ad_units
.iter()
.map(|unit| {
let formats: Vec<Format> = unit
.media_types
.as_ref()
.and_then(|mt| mt.banner.as_ref())
.map(|b| {
b.sizes
.iter()
.filter(|s| s.len() >= 2)
.map(|s| Format { w: s[0], h: s[1] })
.collect::<Vec<_>>()
})
.unwrap_or_else(|| vec![Format { w: 300, h: 250 }]);
let formats: Vec<Format> = vec![
Format { w: 300, h: 250 },
Format { w: 320, h: 50 },
Format { w: 300, h: 50 },
Format { w: 300, h: 300 },
Format { w: 200, h: 200 },
Format { w: 250, h: 250 },
Format { w: 336, h: 280 },
Format { w: 320, h: 100 },
Format { w: 320, h: 250 },
Format { w: 840, h: 280 },
];

let mut bidder: HashMap<String, JsonValue> = HashMap::new();
if let Some(bids) = &unit.bids {
Expand Down Expand Up @@ -376,6 +389,8 @@ async fn pbs_auction_for_get(

fn extract_adm_for_slot(json: &Json, slot: &str) -> Option<String> {
let seatbids = json.get("seatbid")?.as_array()?;

// First pass: look for exact slot match
for sb in seatbids {
if let Some(bids) = sb.get("bid").and_then(|b| b.as_array()) {
for bid in bids {
Expand All @@ -388,6 +403,8 @@ fn extract_adm_for_slot(json: &Json, slot: &str) -> Option<String> {
}
}
}

// Second pass: return any bid's adm as fallback
for sb in seatbids {
if let Some(bids) = sb.get("bid").and_then(|b| b.as_array()) {
for bid in bids {
Expand All @@ -397,6 +414,7 @@ fn extract_adm_for_slot(json: &Json, slot: &str) -> Option<String> {
}
}
}

None
}

Expand Down Expand Up @@ -427,6 +445,7 @@ async fn handle_prebid_auction(
&fresh_id,
settings,
&req,
config,
)?;

let mut pbs_req = Request::new(
Expand All @@ -441,6 +460,8 @@ async fn handle_prebid_auction(
})?;

log::info!("Sending request to Prebid Server");
log::info!("request: {:#?}", openrtb_request);

let backend_name = ensure_backend_from_url(&config.server_url)?;
let mut pbs_response =
pbs_req
Expand All @@ -449,6 +470,8 @@ async fn handle_prebid_auction(
message: "Failed to send request to Prebid Server".to_string(),
})?;

log::info!("pbs_response {}: OK", pbs_response.get_status());

if pbs_response.get_status().is_success() {
let response_body = pbs_response.take_body_bytes();
match serde_json::from_slice::<Json>(&response_body) {
Expand Down Expand Up @@ -485,6 +508,7 @@ fn enhance_openrtb_request(
fresh_id: &str,
settings: &Settings,
req: &Request,
config: &PrebidIntegrationConfig,
) -> Result<(), Report<TrustedServerError>> {
if !request["user"].is_object() {
request["user"] = json!({});
Expand Down Expand Up @@ -521,10 +545,31 @@ fn enhance_openrtb_request(
}

if !request["site"].is_object() {
let mut page_url = format!("https://{}", settings.publisher.domain);

// Append debug query params if configured
if let Some(ref params) = config.debug_query_params {
page_url = format!("{}?{}", page_url, params);
}

request["site"] = json!({
"domain": settings.publisher.domain,
"page": format!("https://{}", settings.publisher.domain),
"page": page_url,
});
} else if config.debug_query_params.is_some() {
// If site already exists, append debug params to existing page URL
if let Some(page_url) = request["site"]["page"].as_str() {
let params = config.debug_query_params.as_ref().unwrap();
// Only append if params aren't already present
if !page_url.contains(params.as_str()) {
let updated_url = if page_url.contains('?') {
format!("{}&{}", page_url, params)
} else {
format!("{}?{}", page_url, params)
};
request["site"]["page"] = json!(updated_url);
}
}
}

if let Some(request_signing_config) = &settings.request_signing {
Expand All @@ -545,6 +590,16 @@ fn enhance_openrtb_request(
}
}

if config.debug {
if !request["ext"].is_object() {
request["ext"] = json!({});
}
if !request["ext"]["prebid"].is_object() {
request["ext"]["prebid"] = json!({});
}
request["ext"]["prebid"]["debug"] = json!(true);
}

Ok(())
}

Expand Down Expand Up @@ -691,6 +746,7 @@ mod tests {
bidders: vec!["exampleBidder".to_string()],
auto_configure: true,
debug: false,
debug_query_params: None,
}
}

Expand Down Expand Up @@ -855,7 +911,9 @@ mod tests {
let mut req = Request::new(Method::POST, "https://edge.example/third-party/ad");
req.set_header("Sec-GPC", "1");

enhance_openrtb_request(&mut request_json, synthetic_id, fresh_id, &settings, &req)
let config = base_config();

enhance_openrtb_request(&mut request_json, synthetic_id, fresh_id, &settings, &req, &config)
.expect("should enhance request");

assert_eq!(request_json["user"]["id"], synthetic_id);
Expand All @@ -877,6 +935,52 @@ mod tests {
);
}

#[test]
fn enhance_openrtb_request_adds_debug_flag_when_enabled() {
let settings = make_settings();
let mut request_json = json!({
"id": "openrtb-request-id"
});

let synthetic_id = "synthetic-123";
let fresh_id = "fresh-456";
let req = Request::new(Method::POST, "https://edge.example/third-party/ad");

let mut config = base_config();
config.debug = true;

enhance_openrtb_request(&mut request_json, synthetic_id, fresh_id, &settings, &req, &config)
.expect("should enhance request");

assert_eq!(
request_json["ext"]["prebid"]["debug"], true,
"debug flag should be set to true when config.debug is enabled"
);
}

#[test]
fn enhance_openrtb_request_does_not_add_debug_flag_when_disabled() {
let settings = make_settings();
let mut request_json = json!({
"id": "openrtb-request-id"
});

let synthetic_id = "synthetic-123";
let fresh_id = "fresh-456";
let req = Request::new(Method::POST, "https://edge.example/third-party/ad");

let mut config = base_config();
config.debug = false;

enhance_openrtb_request(&mut request_json, synthetic_id, fresh_id, &settings, &req, &config)
.expect("should enhance request");

assert!(
request_json["ext"]["prebid"]["debug"].is_null(),
"debug flag should not be set when config.debug is disabled"
);
}

#[test]
fn transform_prebid_response_rewrites_creatives_and_tracking() {
let mut response = json!({
Expand Down Expand Up @@ -957,4 +1061,132 @@ mod tests {
));
assert!(!is_prebid_script_url("https://cdn.com/app.js"));
}

#[test]
fn debug_query_params_added_in_enhance_not_build() {
// Verify that build_openrtb_from_ts doesn't add debug params
// They should only be added in enhance_openrtb_request
let settings = make_settings();
let mut config = base_config();
config.debug_query_params = Some("kargo_debug=true&force_bid=1".to_string());

let ad_request = AdRequest {
ad_units: vec![],
config: None,
};

let openrtb = build_openrtb_from_ts(&ad_request, &settings, &config);

assert!(openrtb.site.is_some());
let site = openrtb.site.unwrap();
assert!(site.page.is_some());
let page = site.page.unwrap();

// Debug params should NOT be in the URL from build_openrtb_from_ts
assert!(!page.contains("?"));
assert!(page.starts_with("https://"));
}

#[test]
fn debug_query_params_appended_to_existing_site_page_in_enhance() {
let settings = make_settings();
let mut config = base_config();
config.debug_query_params = Some("kargo_debug=true".to_string());

let req = Request::new(Method::GET, "https://example.com/test");
let synthetic_id = "test-synthetic-id";
let fresh_id = "test-fresh-id";

// Test with existing site.page
let mut request = json!({
"id": "test-id",
"site": {
"domain": "example.com",
"page": "https://example.com/page"
}
});

enhance_openrtb_request(&mut request, synthetic_id, fresh_id, &settings, &req, &config)
.expect("should enhance request");

let page = request["site"]["page"].as_str().unwrap();
assert_eq!(page, "https://example.com/page?kargo_debug=true");
}

#[test]
fn debug_query_params_appended_to_url_with_existing_query() {
let settings = make_settings();
let mut config = base_config();
config.debug_query_params = Some("kargo_debug=true".to_string());

let req = Request::new(Method::GET, "https://example.com/test");
let synthetic_id = "test-synthetic-id";
let fresh_id = "test-fresh-id";

// Test with existing query params in site.page
let mut request = json!({
"id": "test-id",
"site": {
"domain": "example.com",
"page": "https://example.com/page?existing=param"
}
});

enhance_openrtb_request(&mut request, synthetic_id, fresh_id, &settings, &req, &config)
.expect("should enhance request");

let page = request["site"]["page"].as_str().unwrap();
assert_eq!(page, "https://example.com/page?existing=param&kargo_debug=true");
}

#[test]
fn no_debug_query_params_when_none_configured() {
let settings = make_settings();
let config = base_config();

let ad_request = AdRequest {
ad_units: vec![],
config: None,
};

let openrtb = build_openrtb_from_ts(&ad_request, &settings, &config);

assert!(openrtb.site.is_some());
let site = openrtb.site.unwrap();
assert!(site.page.is_some());
let page = site.page.unwrap();

assert!(!page.contains("?"));
assert!(page.starts_with("https://"));
}

#[test]
fn debug_query_params_not_duplicated() {
// Verify that if params are already in the URL, they aren't added again
let settings = make_settings();
let mut config = base_config();
config.debug_query_params = Some("kargo_debug=true".to_string());

let req = Request::new(Method::GET, "https://example.com/test");
let synthetic_id = "test-synthetic-id";
let fresh_id = "test-fresh-id";

// Test with URL that already has the debug params
let mut request = json!({
"id": "test-id",
"site": {
"domain": "example.com",
"page": "https://example.com/page?kargo_debug=true"
}
});

enhance_openrtb_request(&mut request, synthetic_id, fresh_id, &settings, &req, &config)
.expect("should enhance request");

let page = request["site"]["page"].as_str().unwrap();
// Should still only have params once
assert_eq!(page, "https://example.com/page?kargo_debug=true");
// Verify params appear exactly once
assert_eq!(page.matches("kargo_debug=true").count(), 1);
}
}
Loading