Conversation
f2eee35 to
0c0e4fa
Compare
Enrich outgoing Prebid Server requests with fields that were already available in the AuctionRequest/context but not being mapped: - Imp: bidfloor/bidfloorcur (from AdSlot.floor_price), secure (1), tagid (slot id) - Geo: lat/lon (from GeoInfo), metro (DMA code) - User: consent (TCF consent string from UserInfo) - Regs: gdpr flag (when consent present), us_privacy promoted to first-class field - Device: dnt (from DNT header), language (from Accept-Language) - Site: ref (from Referer header), publisher object with domain - BidRequest: tmax (from config timeout_ms), cur (["USD"]) Also refactor build_openrtb_request to use a params struct to satisfy clippy argument limits, and fix pre-existing test compilation errors where ext fields were accessed with struct syntax instead of Map::get.
- Validate Sec-GPC header value is '1' (not just presence) - Add comment documenting GDPR flag trade-off (conservative approach) - Add comment about hardcoded USD currency - Remove dead RegsExt struct (us_privacy promoted to first-class field) - Remove unnecessary language.clone() - Add 13 tests covering all new OpenRTB field mappings: bidfloor, secure, tagid, consent/gdpr, Sec-GPC, DNT, language, geo lat/lon/ metro, tmax, cur, site.ref, site.publisher
Debug logging is only enabled in testing environments, so redacting sensitive fields (consent, IP, geo, referer) before logging adds complexity without meaningful privacy benefit. Log the request directly.
77539cf to
b3480cd
Compare
aram356
left a comment
There was a problem hiding this comment.
Staff Engineer Review
Overall this is a solid change — introducing a proto-generated OpenRTB crate is the right call for spec compliance, and the enrichment of the bid request with consent, DNT, language, geo, floor prices, etc. is well-tested and well-commented.
Findings below use the code review emoji guide.
Summary
| Emoji | Finding |
|---|---|
| 🔧 | build.rs text munging has no tests — add unit tests for postprocess() |
| 🔧 | Duplicated i32 conversion logic — extract shared helper |
| 🔧 | GDPR applicability inferred from consent string presence — can misclassify non-EEA traffic |
| 🤔 | prost runtime dep is unnecessary — strip prost::Enumeration or accept the bloat |
| 🤔 | ToExt blanket impl too broad — narrow to ext-relevant types |
| 🤔 | device.language keeps locale tag instead of primary language |
| 🤔 | Consider checking in generated code — eliminates protoc build dependency |
| 🌱 | Sec-GPC → us_privacy "1YYN" hardcoded — consider making configurable |
| ⛏ | Edition mismatch (2021 vs 2024) |
| ⛏ | Hardcoded "USD" / "1YYN" — extract named constants |
| ⛏ | pub use generated::* + manual re-exports redundancy |
| ⛏ | bool_as_int edge case tests |
| 📌 | CI protoc install is uncached and somewhat fragile |
47d2a4b to
b939bbf
Compare
…eo check, extract constants, and improve test coverage - Strip prost::Enumeration from codegen, remove prost runtime dependency - Replace prost alloc type paths with std equivalents in generated code - Narrow ToExt from blanket impl to opt-in supertrait with provided method - Remove pub use generated::*, use explicit re-exports only - Fix edition 2021 -> 2024 in openrtb crate - Add GDPR geo check (EU-27 + EEA + UK) via is_gdpr_country() in geo.rs - Fall back to consent-string heuristic only when geo is unavailable - Extract DEFAULT_CURRENCY and GPC_US_PRIVACY named constants - Strip locale subtag from device.language (en-US -> en) - Extract shared to_openrtb_i32() helper, deduplicate formats.rs/prebid.rs - Add typed get_prebid_ext() test helper for compile-time field safety - Move postprocess() to src/codegen.rs shared between build.rs and tests - Add 7 codegen tests, 8 bool_as_int/deserialization tests, 5 geo tests, 3 GDPR tests
…DPR, cleanup - bool_as_int: accept string "1"/"0"/"true"/"false" from bidders - codegen: document brace-counting assumption for prost output - geo: use LazyLock<HashSet> for O(1) GDPR country lookups - remove unused prost workspace dependency (only prost-build is used) - replace dead unwrap_or with expect in language parsing - CI: use arduino/setup-protoc@v3 for cached protoc installs
Move proto codegen from build.rs to an on-demand generate.sh script backed by a standalone openrtb-codegen crate (workspace-excluded). The checked-in src/generated.rs means normal builds and CI no longer need protoc installed — only developers editing the proto file do. - Delete build.rs, replace include!(OUT_DIR) with mod generated - Add crates/openrtb-codegen as excluded workspace member - Add crates/openrtb/generate.sh wrapper script - Remove prost-build from workspace deps and openrtb build-deps - Remove protoc install steps from CI workflows
Review Response SummaryAll 14 review comments have been addressed across 3 commits. Here's what changed: Commit 1:
|
| .and_then(|d| d.geo.as_ref()) | ||
| .map(|geo| is_gdpr_country(&geo.country)); | ||
|
|
||
| let gdpr = match gdpr_from_geo { |
There was a problem hiding this comment.
🔧 P1 — GDPR can be forced to false even when a consent string exists
When geo resolves to a non-GDPR country, gdpr_from_geo is Some(false), which overrides the conservative fallback — even if user.consent contains a valid TCF string. The test to_openrtb_sets_gdpr_false_for_non_eu_country explicitly locks in this behavior.
Risk: IP geo false-negatives (VPN, mobile carrier, IP DB drift) can mark EU traffic as non-GDPR and weaken compliance signaling. A user in Berlin on a US mobile carrier IP gets gdpr=0 despite having a TCF consent string from a CMP.
Suggested fix — when geo says non-GDPR but a consent string is present, the consent string is the stronger signal:
let gdpr = match gdpr_from_geo {
Some(true) => Some(true),
Some(false) if request.user.consent.is_some() => Some(true),
Some(false) => Some(false),
None => request.user.consent.as_ref().map(|_| true),
};| let value = Option::<serde_json::Value>::deserialize(deserializer)?; | ||
| match value { | ||
| Some(serde_json::Value::Bool(b)) => Ok(Some(b)), | ||
| Some(serde_json::Value::Number(n)) => Ok(Some(n.as_i64() != Some(0))), |
There was a problem hiding this comment.
🤔 P2 — bool_as_int deserialization misinterprets floating-point zero
n.as_i64() != Some(0) silently treats 0.0 as true. serde_json::Number returns None from as_i64() for 0.0 (stored as a float), so the expression evaluates to true.
While unlikely in well-formed OpenRTB, malformed payloads do appear in the wild. Suggested fix:
Some(serde_json::Value::Number(n)) => {
Ok(Some(n.as_i64().map_or_else(
|| n.as_f64().is_some_and(|f| f != 0.0),
|i| i != 0,
)))
}| @@ -553,11 +575,23 @@ impl PrebidAuctionProvider { | |||
| } | |||
|
|
|||
| Imp { | |||
There was a problem hiding this comment.
🤔 P2 — Invalid banner imps can still be emitted after dimension overflow filtering
When all formats for a slot have out-of-range dimensions, formats is empty but the Imp is still built with banner: Some(Banner { format: vec![], .. }). An impression with an empty format list is malformed per OpenRTB — some PBS adapters may reject it, reducing fill.
Consider changing .map() to .filter_map() and returning None when formats is empty:
.filter_map(|slot| {
// ... format collection ...
if formats.is_empty() {
log::warn!("prebid: dropping imp '{}' — no valid banner formats", slot.id);
return None;
}
Some(Imp { ... })
})| } | ||
| }); | ||
|
|
||
| let language = context |
There was a problem hiding this comment.
⛏ P3 — Empty Accept-Language header produces language = Some("")
If the header value is "", the split chain yields Some("".to_string()). An empty string for device.language is not useful and could confuse bidders.
Add .filter(|s| !s.is_empty()) after the final .to_string().
|
|
||
| /// CCPA/US-privacy string sent when the `Sec-GPC` header signals opt-out. | ||
| /// | ||
| /// Encodes: notice given (`1`), user opted out (`Y`), LSPA not signed (`N`). |
There was a problem hiding this comment.
⛏ P3 — US-privacy comment mislabels position 1
The comment says "notice given (1)" but position 1 of the US Privacy string is the version number. The format is V(version)N(notice)O(opt-out)L(lspa), so 1YYN = version 1, notice=Y, opt-out=Y, LSPA=N. Suggested:
/// Encodes: version `1`, notice given (`Y`), user opted out (`Y`), LSPA not signed (`N`).
| "BidResponse", | ||
| "SeatBid", | ||
| "Bid", | ||
| "Transparency", |
There was a problem hiding this comment.
⛏ P3 — Transparency listed in EXT_STRUCTS but absent from proto
"Transparency" is in the EXT_STRUCTS list but no matching struct exists in the proto or generated output. Dead config — harmless but confusing. Verify and remove if not needed.
Summary
Changes
crates/openrtb/Cargo.tomlcrates/openrtb/proto/openrtb.protocrates/openrtb/src/generated.rscrates/openrtb/src/codegen.rscrates/openrtb/src/lib.rsbool_as_intserde module (6 edge-case tests),ToExtsupertrait, explicit re-exports, deserialization testscrates/openrtb/generate.shcrates/openrtb/README.mdcrates/openrtb-codegen/prost-builddependencycrates/common/src/openrtb.rsto_openrtb_i32()helper; opt-inToExtimpls for 4 ext typescrates/common/src/geo.rsGDPR_COUNTRIESset (EU/EEA/UK),is_gdpr_country()function with 5 testscrates/common/src/integrations/prebid.rsDEFAULT_CURRENCYandGPC_US_PRIVACYconstants; language subtag stripping; typedget_prebid_ext()test helper; 17+ testscrates/common/src/auction/formats.rsOptionwrappers andToExtcrates/common/Cargo.tomltrusted-server-openrtbdependencyCargo.tomlopenrtbto workspace members, excludeopenrtb-codegen, removeprost-buildfrom workspace depsCargo.lock.github/workflows/format.yml.github/workflows/test.ymlKey design decisions
prost::Enumerationand alloc types stripped during postprocessing, so the openrtb crate only depends on serde.protocin CI/dev environments. Regenerate via./generate.shwhen the proto changes.geo.country_code()to determine GDPR applicability (EU/EEA/UK country list) rather than parsing TCF consent strings. Falls back to consent string presence when geo is unavailable.ToExtas supertrait:ToExt: Serializewith opt-in impls on the 4 Prebid ext types, preventing accidental misuse.Test plan
cargo test --workspace— all tests passcargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npx vitest runcd crates/js/lib && npm run formatcd docs && npm run format