Skip to content

Commit 5388033

Browse files
Fix Prebid User ID diagnostics (#887)
1 parent fd2ab0f commit 5388033

16 files changed

Lines changed: 327 additions & 58 deletions

File tree

crates/trusted-server-core/src/auction/endpoints.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ fn parse_client_auction_uid(raw: &JsonValue) -> Option<Uid> {
472472
let atype = uid
473473
.get("atype")
474474
.and_then(JsonValue::as_u64)
475-
.and_then(|atype| u8::try_from(atype).ok());
475+
.and_then(|atype| i32::try_from(atype).ok());
476476

477477
let ext = match uid.get("ext") {
478478
Some(JsonValue::Object(_)) => uid.get("ext").cloned(),
@@ -1053,6 +1053,24 @@ mod tests {
10531053
assert_eq!(parsed[0].uids[0].id, "valid", "should keep valid UID");
10541054
}
10551055

1056+
#[test]
1057+
fn parse_client_auction_eids_preserves_pair_atype() {
1058+
let raw = json!([
1059+
{
1060+
"source": "google.com",
1061+
"uids": [{ "id": "pair-id", "atype": 571187 }]
1062+
}
1063+
]);
1064+
1065+
let parsed = parse_client_auction_eids(Some(&raw)).expect("should parse PAIR EID");
1066+
1067+
assert_eq!(
1068+
parsed[0].uids[0].atype,
1069+
Some(571187),
1070+
"should preserve PAIR's vendor-specific atype"
1071+
);
1072+
}
1073+
10561074
#[test]
10571075
fn parse_client_auction_eids_preserves_uid_ext_and_sanitizes_invalid_atype() {
10581076
let raw = json!([
@@ -1066,7 +1084,7 @@ mod tests {
10661084
},
10671085
{
10681086
"id": "uid-bad-atype",
1069-
"atype": 999,
1087+
"atype": 2_147_483_648_u64,
10701088
"ext": { "keep": true }
10711089
},
10721090
{

crates/trusted-server-core/src/ec/eids.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub struct ResolvedPartnerId {
2525
/// The synced user ID value.
2626
pub uid: String,
2727
/// `OpenRTB` agent type for this partner's identifiers.
28-
pub openrtb_atype: u8,
28+
pub openrtb_atype: i32,
2929
}
3030

3131
/// Resolves source-domain keyed IDs from a KV entry against the partner registry.
@@ -214,17 +214,29 @@ mod tests {
214214
source_domain: "id5-sync.com".to_owned(),
215215
openrtb_atype: 1,
216216
},
217+
ResolvedPartnerId {
218+
uid: "pair-id".to_owned(),
219+
source_domain: "google.com".to_owned(),
220+
openrtb_atype: 571187,
221+
},
217222
];
218223

219224
let eids = to_eids(&resolved);
220225

221-
assert_eq!(eids.len(), 2, "should produce one EID per resolved partner");
226+
assert_eq!(eids.len(), 3, "should produce one EID per resolved partner");
222227
assert_eq!(eids[0].source, "liveramp.com");
223228
assert_eq!(eids[0].uids[0].id, "LR_xyz");
224229
assert_eq!(eids[0].uids[0].atype, Some(3));
225230
assert_eq!(eids[1].source, "id5-sync.com");
226231
assert_eq!(eids[1].uids[0].id, "ID5_abc");
227232
assert_eq!(eids[1].uids[0].atype, Some(1));
233+
assert_eq!(eids[2].source, "google.com", "should preserve PAIR source");
234+
assert_eq!(eids[2].uids[0].id, "pair-id", "should preserve PAIR ID");
235+
assert_eq!(
236+
eids[2].uids[0].atype,
237+
Some(571187),
238+
"should preserve PAIR vendor-specific atype"
239+
);
228240
}
229241

230242
#[test]

crates/trusted-server-core/src/ec/prebid_eids.rs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,7 @@ const MAX_EIDS_COOKIE_BYTES: usize = 8 * 1024;
2929
struct LegacyCookieEid {
3030
source: String,
3131
id: String,
32-
#[allow(
33-
dead_code,
34-
reason = "legacy cookie field is deserialized for compatibility but not emitted"
35-
)]
36-
atype: u8,
32+
atype: i32,
3733
}
3834

3935
/// OpenRTB-style `ts-eids` cookie entry.
@@ -48,7 +44,7 @@ struct StructuredCookieEid {
4844
struct StructuredCookieUid {
4945
id: String,
5046
#[serde(default)]
51-
atype: Option<u8>,
47+
atype: Option<i32>,
5248
#[serde(default)]
5349
ext: Option<JsonValue>,
5450
}
@@ -318,14 +314,15 @@ fn structured_cookie_uid_to_openrtb(uid: StructuredCookieUid) -> Option<Uid> {
318314
return None;
319315
}
320316

317+
let atype = uid.atype.filter(|atype| *atype >= 0);
321318
let ext = match uid.ext {
322319
Some(JsonValue::Object(_)) => uid.ext,
323320
_ => None,
324321
};
325322

326323
Some(Uid {
327324
id: uid.id,
328-
atype: uid.atype,
325+
atype,
329326
ext,
330327
})
331328
}
@@ -338,7 +335,7 @@ fn legacy_cookie_eids_to_openrtb(entries: Vec<LegacyCookieEid>) -> Vec<Eid> {
338335
source: entry.source,
339336
uids: vec![Uid {
340337
id: entry.id,
341-
atype: Some(entry.atype),
338+
atype: (entry.atype >= 0).then_some(entry.atype),
342339
ext: None,
343340
}],
344341
})
@@ -407,15 +404,25 @@ mod tests {
407404
let eids = vec![
408405
json!({"source": "id5-sync.com", "id": "ID5_abc", "atype": 1}),
409406
json!({"source": "liveramp.com", "id": "LR_xyz", "atype": 3}),
407+
json!({"source": "google.com", "id": "pair-id", "atype": 571187}),
410408
];
411409
let encoded = BASE64.encode(serde_json::to_vec(&eids).expect("should serialize"));
412410

413411
let decoded = parse_prebid_eids_cookie(&encoded).expect("should decode valid payload");
414-
assert_eq!(decoded.len(), 2, "should parse both EIDs");
412+
assert_eq!(decoded.len(), 3, "should parse all EIDs");
415413
assert_eq!(decoded[0].source, "id5-sync.com");
416414
assert_eq!(decoded[0].uids[0].id, "ID5_abc");
417415
assert_eq!(decoded[1].source, "liveramp.com");
418416
assert_eq!(decoded[1].uids[0].id, "LR_xyz");
417+
assert_eq!(
418+
decoded[2].source, "google.com",
419+
"should preserve PAIR source"
420+
);
421+
assert_eq!(
422+
decoded[2].uids[0].atype,
423+
Some(571187),
424+
"should preserve PAIR vendor-specific atype"
425+
);
419426
}
420427

421428
#[test]
@@ -424,22 +431,45 @@ mod tests {
424431
"source": "sharedid.org",
425432
"uids": [
426433
{"id": "shared_123", "atype": 3},
427-
{"id": "shared_456", "ext": {"provider": "example"}}
434+
{"id": "shared_456", "ext": {"provider": "example"}},
435+
{"id": "shared_invalid", "atype": -1}
428436
]
429437
})];
430438
let encoded = BASE64.encode(serde_json::to_vec(&eids).expect("should serialize"));
431439

432440
let decoded = parse_prebid_eids_cookie(&encoded).expect("should decode valid payload");
433441
assert_eq!(decoded.len(), 1, "should parse one structured EID entry");
434442
assert_eq!(decoded[0].source, "sharedid.org");
435-
assert_eq!(decoded[0].uids.len(), 2, "should preserve multiple UIDs");
443+
assert_eq!(decoded[0].uids.len(), 3, "should preserve multiple UIDs");
436444
assert_eq!(decoded[0].uids[0].id, "shared_123");
437445
assert_eq!(decoded[0].uids[0].atype, Some(3));
438446
assert_eq!(
439447
decoded[0].uids[1].ext,
440448
Some(json!({"provider": "example"})),
441449
"should preserve UID ext objects"
442450
);
451+
assert_eq!(
452+
decoded[0].uids[2].atype, None,
453+
"should drop negative atype values"
454+
);
455+
}
456+
457+
#[test]
458+
fn parse_prebid_eids_cookie_preserves_pair_atype() {
459+
let encoded = encode_json(&json!([
460+
{
461+
"source": "google.com",
462+
"uids": [{ "id": "pair-id", "atype": 571187 }]
463+
}
464+
]));
465+
466+
let decoded = parse_prebid_eids_cookie(&encoded).expect("should decode PAIR EID");
467+
468+
assert_eq!(
469+
decoded[0].uids[0].atype,
470+
Some(571187),
471+
"should preserve PAIR's vendor-specific atype"
472+
);
443473
}
444474

445475
#[test]

crates/trusted-server-core/src/ec/registry.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub struct PartnerConfig {
2525
/// Canonical `OpenRTB` EID source domain and EC KV `ids` key.
2626
pub source_domain: String,
2727
/// `OpenRTB` `atype` value.
28-
pub openrtb_atype: u8,
28+
pub openrtb_atype: i32,
2929
/// Whether this partner's UIDs appear in auction `user.eids`.
3030
pub bidstream_enabled: bool,
3131
/// SHA-256 hex of the partner's API token (precomputed at startup).

crates/trusted-server-core/src/integrations/prebid.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4880,6 +4880,14 @@ external_bundle_sri = "sha384-AAAA"
48804880
ext: None,
48814881
}],
48824882
},
4883+
crate::openrtb::Eid {
4884+
source: "google.com".to_owned(),
4885+
uids: vec![crate::openrtb::Uid {
4886+
id: "pair-id".to_owned(),
4887+
atype: Some(571187),
4888+
ext: None,
4889+
}],
4890+
},
48834891
]);
48844892

48854893
let settings = make_settings();
@@ -4896,7 +4904,7 @@ external_bundle_sri = "sha384-AAAA"
48964904
let serialized = serde_json::to_value(&openrtb).expect("should serialize OpenRTB request");
48974905
let ext_eids = &serialized["user"]["ext"]["eids"];
48984906
assert!(ext_eids.is_array(), "should populate user.ext.eids");
4899-
assert_eq!(ext_eids.as_array().unwrap().len(), 2, "should have 2 EIDs");
4907+
assert_eq!(ext_eids.as_array().unwrap().len(), 3, "should have 3 EIDs");
49004908
assert_eq!(
49014909
ext_eids[0]["source"], "liveramp.com",
49024910
"should include liveramp EID"
@@ -4905,6 +4913,14 @@ external_bundle_sri = "sha384-AAAA"
49054913
ext_eids[1]["source"], "id5-sync.com",
49064914
"should include id5 EID"
49074915
);
4916+
assert_eq!(
4917+
ext_eids[2]["source"], "google.com",
4918+
"should include PAIR EID"
4919+
);
4920+
assert_eq!(
4921+
ext_eids[2]["uids"][0]["atype"], 571187,
4922+
"should preserve PAIR's vendor-specific atype"
4923+
);
49084924
}
49094925

49104926
#[test]

crates/trusted-server-core/src/openrtb.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ pub struct Eid {
8080
pub struct Uid {
8181
/// The identifier value.
8282
pub id: String,
83-
/// Agent type: 1 = cookie/device, 2 = person, 3 = user-provided.
83+
/// `OpenRTB` agent type, including vendor-specific values such as PAIR's `571187`.
8484
#[serde(skip_serializing_if = "Option::is_none")]
85-
pub atype: Option<u8>,
85+
pub atype: Option<i32>,
8686
/// Provider-specific extension data.
8787
#[serde(skip_serializing_if = "Option::is_none")]
8888
pub ext: Option<Value>,
@@ -400,4 +400,23 @@ mod tests {
400400
"ext should be omitted when None"
401401
);
402402
}
403+
404+
#[test]
405+
fn eid_serializes_vendor_specific_atype() {
406+
let eid = Eid {
407+
source: "google.com".to_owned(),
408+
uids: vec![Uid {
409+
id: "pair-id".to_owned(),
410+
atype: Some(571187),
411+
ext: None,
412+
}],
413+
};
414+
415+
let serialized = serde_json::to_value(&eid).expect("should serialize");
416+
417+
assert_eq!(
418+
serialized["uids"][0]["atype"], 571187,
419+
"should preserve PAIR's vendor-specific atype"
420+
);
421+
}
403422
}

crates/trusted-server-core/src/settings.rs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,13 @@ pub struct EcPartner {
299299
/// This normalized domain is also the canonical EC KV `ids` map key.
300300
#[validate(custom(function = EcPartner::validate_source_domain))]
301301
pub source_domain: String,
302-
/// `OpenRTB` `atype` value (typically 3).
302+
/// `OpenRTB` `atype` value, including vendor-specific values such as PAIR's `571187`.
303303
#[serde(
304304
default = "EcPartner::default_openrtb_atype",
305305
deserialize_with = "from_value_or_str"
306306
)]
307-
pub openrtb_atype: u8,
307+
#[validate(range(min = 0, message = "must be a non-negative OpenRTB agent type"))]
308+
pub openrtb_atype: i32,
308309
/// Whether this partner's UIDs appear in auction `user.eids`.
309310
#[serde(default, deserialize_with = "from_value_or_str")]
310311
pub bidstream_enabled: bool,
@@ -417,7 +418,7 @@ impl EcPartner {
417418
}
418419

419420
#[must_use]
420-
pub const fn default_openrtb_atype() -> u8 {
421+
pub const fn default_openrtb_atype() -> i32 {
421422
3
422423
}
423424

@@ -2813,6 +2814,46 @@ mod tests {
28132814
}
28142815
}
28152816

2817+
#[test]
2818+
fn validate_accepts_vendor_specific_ec_partner_atype() {
2819+
let toml_str = format!(
2820+
r#"{}
2821+
[[ec.partners]]
2822+
name = "PAIR Partner"
2823+
source_domain = "google.com"
2824+
openrtb_atype = 571187
2825+
api_token = "test-vendor-token-32-bytes-minimum"
2826+
"#,
2827+
crate_test_settings_str(),
2828+
);
2829+
2830+
let settings = Settings::from_toml(&toml_str)
2831+
.expect("should accept vendor-specific OpenRTB agent type");
2832+
2833+
assert_eq!(
2834+
settings.ec.partners[0].openrtb_atype, 571187,
2835+
"should preserve PAIR's vendor-specific atype"
2836+
);
2837+
}
2838+
2839+
#[test]
2840+
fn validate_rejects_negative_ec_partner_atype() {
2841+
let toml_str = format!(
2842+
r#"{}
2843+
[[ec.partners]]
2844+
name = "Invalid Partner"
2845+
source_domain = "partner.example.com"
2846+
openrtb_atype = -1
2847+
api_token = "test-vendor-token-32-bytes-minimum"
2848+
"#,
2849+
crate_test_settings_str(),
2850+
);
2851+
2852+
let result = Settings::from_toml(&toml_str);
2853+
2854+
assert!(result.is_err(), "should reject negative OpenRTB agent type");
2855+
}
2856+
28162857
#[test]
28172858
fn validate_accepts_origin_host_header_override() {
28182859
let toml_str = crate_test_settings_str().replace(
@@ -3660,7 +3701,7 @@ origin_host_header_overide = "www.example.com""#,
36603701
(origin_key, Some("https://origin.test-publisher.com")),
36613702
(partner_0_name_key, Some("Env Partner 0")),
36623703
(partner_0_source_domain_key, Some("envpartner0.example.com")),
3663-
(partner_0_openrtb_atype_key, Some("1")),
3704+
(partner_0_openrtb_atype_key, Some("571187")),
36643705
(partner_0_bidstream_enabled_key, Some("true")),
36653706
(partner_0_api_token_key, Some("env-token-0")),
36663707
(partner_1_name_key, Some("Env Partner 1")),
@@ -3679,7 +3720,7 @@ origin_host_header_overide = "www.example.com""#,
36793720
settings.ec.partners[0].source_domain,
36803721
"envpartner0.example.com"
36813722
);
3682-
assert_eq!(settings.ec.partners[0].openrtb_atype, 1);
3723+
assert_eq!(settings.ec.partners[0].openrtb_atype, 571187);
36833724
assert!(settings.ec.partners[0].bidstream_enabled);
36843725
assert_eq!(settings.ec.partners[0].api_token.expose(), "env-token-0");
36853726
assert_eq!(settings.ec.partners[1].name, "Env Partner 1");

0 commit comments

Comments
 (0)