Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eff8a8e
wip on openrtb-full-implementation
ChristianPavilonis Mar 2, 2026
beaa313
review fixes
ChristianPavilonis Mar 2, 2026
d331345
use new impl
ChristianPavilonis Mar 2, 2026
1fdb0a9
Populate missing OpenRTB fields from available auction data
ChristianPavilonis Mar 4, 2026
3207ad3
Address PR review: validate Sec-GPC, remove dead code, add tests
ChristianPavilonis Mar 4, 2026
179252f
refactors
ChristianPavilonis Mar 4, 2026
c758b20
renaming
ChristianPavilonis Mar 4, 2026
a1c0088
docs: refresh prebid and lockr integration guides
ChristianPavilonis Mar 5, 2026
3c54eea
Redact sensitive OpenRTB fields in Prebid debug logs
ChristianPavilonis Mar 5, 2026
35c1399
Use u32 for OpenRTB dimensions and timeout
ChristianPavilonis Mar 5, 2026
7fffa95
cleanup helper
ChristianPavilonis Mar 5, 2026
706fa9f
revert lockr doc changes
ChristianPavilonis Mar 5, 2026
6cc7d00
revert doc changes
ChristianPavilonis Mar 5, 2026
de5a017
remove openrtb md files
ChristianPavilonis Mar 5, 2026
96e258b
remove unnecessary OpenRTB request redaction for debug logging
ChristianPavilonis Mar 5, 2026
0f52dd9
openrtb proto
ChristianPavilonis Mar 5, 2026
8a9cf46
Install protoc in CI for prost-build proto compilation
ChristianPavilonis Mar 5, 2026
e62696b
Serialize OpenRTB bool fields as integers (0/1) for PBS compatibility
ChristianPavilonis Mar 6, 2026
5071287
add readme to openrtb crate
ChristianPavilonis Mar 6, 2026
6978d84
address PR review: remove prost runtime dep, narrow ToExt, add GDPR g…
ChristianPavilonis Mar 6, 2026
e942bd7
address PR review findings: robust bool_as_int strings, HashSet for G…
ChristianPavilonis Mar 6, 2026
dab1a52
check in generated OpenRTB code, remove build-time protoc dependency
ChristianPavilonis Mar 6, 2026
9edef8c
address PR review: fix GDPR consent override, bool_as_int float handl…
ChristianPavilonis Mar 10, 2026
52a36d3
Address PR review feedback
ChristianPavilonis Mar 16, 2026
18faa19
Fix stale references, dead config, and update prebid docs
ChristianPavilonis Mar 16, 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
10 changes: 10 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ members = [
"crates/common",
"crates/fastly",
"crates/js",
"crates/openrtb",
]
# integration-tests is intentionally excluded from workspace members because it
# requires a native target (testcontainers, reqwest) while the workspace default
# is wasm32-wasip1. Run it via: ./scripts/integration-tests.sh
exclude = [
"crates/integration-tests",
"crates/openrtb-codegen",
]

default-members = [
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