Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0fea734
wip on openrtb-full-implementation
ChristianPavilonis Mar 2, 2026
6406e26
review fixes
ChristianPavilonis Mar 2, 2026
62caccb
use new impl
ChristianPavilonis Mar 2, 2026
cccb24c
Populate missing OpenRTB fields from available auction data
ChristianPavilonis Mar 4, 2026
a0e2fd2
Address PR review: validate Sec-GPC, remove dead code, add tests
ChristianPavilonis Mar 4, 2026
e3410de
refactors
ChristianPavilonis Mar 4, 2026
9877007
renaming
ChristianPavilonis Mar 4, 2026
de12183
docs: refresh prebid and lockr integration guides
ChristianPavilonis Mar 5, 2026
31f28fa
Redact sensitive OpenRTB fields in Prebid debug logs
ChristianPavilonis Mar 5, 2026
d4b7d8e
Use u32 for OpenRTB dimensions and timeout
ChristianPavilonis Mar 5, 2026
9eb1879
cleanup helper
ChristianPavilonis Mar 5, 2026
a74160e
revert lockr doc changes
ChristianPavilonis Mar 5, 2026
c184e61
revert doc changes
ChristianPavilonis Mar 5, 2026
f5c59b1
remove openrtb md files
ChristianPavilonis Mar 5, 2026
4508e8f
remove unnecessary OpenRTB request redaction for debug logging
ChristianPavilonis Mar 5, 2026
b3480cd
openrtb proto
ChristianPavilonis Mar 5, 2026
2e4c5f3
Install protoc in CI for prost-build proto compilation
ChristianPavilonis Mar 5, 2026
95579a1
Serialize OpenRTB bool fields as integers (0/1) for PBS compatibility
ChristianPavilonis Mar 6, 2026
b939bbf
add readme to openrtb crate
ChristianPavilonis Mar 6, 2026
f07ed22
address PR review: remove prost runtime dep, narrow ToExt, add GDPR g…
ChristianPavilonis Mar 6, 2026
d0c18bc
address PR review findings: robust bool_as_int strings, HashSet for G…
ChristianPavilonis Mar 6, 2026
0be0e0c
check in generated OpenRTB code, remove build-time protoc dependency
ChristianPavilonis Mar 6, 2026
64e1038
address PR review: fix GDPR consent override, bool_as_int float handl…
ChristianPavilonis Mar 10, 2026
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
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ members = [
"crates/common",
"crates/fastly",
"crates/js",
"crates/openrtb",
]
exclude = [
"crates/openrtb-codegen",
]

# Build defaults exclude the web-only tsjs crate, which is compiled via wasm-pack.
Expand Down
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ sha2 = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
trusted-server-js = { path = "../js" }
trusted-server-openrtb = { path = "../openrtb" }
url = { workspace = true }
urlencoding = { workspace = true }
uuid = { workspace = true }
Expand Down
33 changes: 22 additions & 11 deletions crates/common/src/auction/formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::auction::context::ContextValue;
use crate::creative;
use crate::error::TrustedServerError;
use crate::geo::GeoInfo;
use crate::openrtb::{OpenRtbBid, OpenRtbResponse, ResponseExt, SeatBid};
use crate::openrtb::{to_openrtb_i32, OpenRtbBid, OpenRtbResponse, ResponseExt, SeatBid, ToExt};
use crate::settings::Settings;
use crate::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id};

Expand Down Expand Up @@ -205,7 +205,7 @@ pub fn convert_to_openrtb_response(
auction_request: &AuctionRequest,
) -> Result<Response, Report<TrustedServerError>> {
// Build OpenRTB-style seatbid array
let mut seatbids = Vec::new();
let mut seatbids = Vec::with_capacity(result.winning_bids.len());

for (slot_id, bid) in &result.winning_bids {
let price = bid.price.ok_or_else(|| {
Expand All @@ -217,6 +217,13 @@ pub fn convert_to_openrtb_response(
})
})?;

let bid_context = format!(
"auction {} slot {} bidder {}",
auction_request.id, slot_id, bid.bidder
);
let width = to_openrtb_i32(bid.width, "width", &bid_context);
let height = to_openrtb_i32(bid.height, "height", &bid_context);

// Process creative HTML if present - rewrite URLs and return inline
let creative_html = if let Some(ref raw_creative) = bid.creative {
// Rewrite creative HTML with proxy URLs for first-party delivery
Expand All @@ -241,19 +248,21 @@ pub fn convert_to_openrtb_response(
};

let openrtb_bid = OpenRtbBid {
id: format!("{}-{}", bid.bidder, slot_id),
impid: slot_id.to_string(),
price,
id: Some(format!("{}-{}", bid.bidder, slot_id)),
impid: Some(slot_id.to_string()),
price: Some(price),
adm: Some(creative_html),
crid: Some(format!("{}-creative", bid.bidder)),
w: Some(bid.width),
h: Some(bid.height),
adomain: Some(bid.adomain.clone().unwrap_or_default()),
w: width,
h: height,
adomain: bid.adomain.clone().unwrap_or_default(),
..Default::default()
};

seatbids.push(SeatBid {
seat: Some(bid.bidder.clone()),
bid: vec![openrtb_bid],
..Default::default()
});
}

Expand All @@ -272,17 +281,19 @@ pub fn convert_to_openrtb_response(
.collect();

let response_body = OpenRtbResponse {
id: auction_request.id.to_string(),
id: Some(auction_request.id.to_string()),
seatbid: seatbids,
ext: Some(ResponseExt {
ext: ResponseExt {
orchestrator: OrchestratorExt {
strategy: strategy_name.to_string(),
providers: result.provider_responses.len(),
total_bids: result.total_bids(),
time_ms: result.total_time_ms,
provider_details,
},
}),
}
.to_ext(),
..Default::default()
};

let body_bytes =
Expand Down
66 changes: 66 additions & 0 deletions crates/common/src/geo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,39 @@ impl GeoInfo {
}
}

use std::collections::HashSet;
use std::sync::LazyLock;

/// EU-27 + EEA-3 (Iceland, Liechtenstein, Norway) + UK (UK GDPR).
///
/// Two-letter ISO 3166-1 alpha-2 country codes for jurisdictions where GDPR
/// or equivalent legislation applies. Used to infer GDPR applicability from
/// IP-derived geolocation when a more authoritative signal (e.g. TCF consent
/// string) is not yet available.
static GDPR_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
[
// EU-27
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT",
"LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE",
// EEA (non-EU)
"IS", "LI", "NO", // UK GDPR
"GB",
]
.into_iter()
.collect()
});

/// Returns `true` if the given two-letter country code falls under GDPR
/// jurisdiction (EU-27, EEA, or UK).
///
/// The comparison is case-insensitive. Returns `false` for empty or
/// unrecognised codes.
#[must_use]
pub fn is_gdpr_country(country_code: &str) -> bool {
let upper = country_code.to_ascii_uppercase();
GDPR_COUNTRIES.contains(upper.as_str())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -211,6 +244,39 @@ mod tests {
);
}

#[test]
fn is_gdpr_country_detects_eu_members() {
assert!(is_gdpr_country("DE"), "Germany is EU");
assert!(is_gdpr_country("FR"), "France is EU");
assert!(is_gdpr_country("IT"), "Italy is EU");
}

#[test]
fn is_gdpr_country_detects_eea_and_uk() {
assert!(is_gdpr_country("NO"), "Norway is EEA");
assert!(is_gdpr_country("IS"), "Iceland is EEA");
assert!(is_gdpr_country("GB"), "UK has UK GDPR");
}

#[test]
fn is_gdpr_country_rejects_non_gdpr() {
assert!(!is_gdpr_country("US"), "US is not GDPR");
assert!(!is_gdpr_country("CN"), "China is not GDPR");
assert!(!is_gdpr_country("BR"), "Brazil is not GDPR");
}

#[test]
fn is_gdpr_country_is_case_insensitive() {
assert!(is_gdpr_country("de"), "lowercase should match");
assert!(is_gdpr_country("De"), "mixed case should match");
}

#[test]
fn is_gdpr_country_handles_empty_and_unknown() {
assert!(!is_gdpr_country(""), "empty string is not GDPR");
assert!(!is_gdpr_country("XX"), "unknown code is not GDPR");
}

#[test]
fn set_response_headers_omits_region_when_none() {
let geo = GeoInfo {
Expand Down
Loading
Loading