Skip to content

Commit 8490290

Browse files
authored
feat(ads-client): support content categories on ad requests (AC-109) (#7390)
Add per-placement IAB content (taxonomy + category IDs) and a request-level `flags` object plumbed through MARSClient/AdsClient. Callers opt into flags via `MozAdsRequestOptions.flags: MozAdsRequestFlags?` (currently exposing `contextualPlacement`), with room to grow as new flags are added. The `flags` object is included in the cache key only when at least one flag opts away from its default, so non-contextual callers keep their existing cache entries.
1 parent f1137e8 commit 8490290

10 files changed

Lines changed: 304 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
[Full Changelog](In progress)
44

5+
## ✨ What's New ✨
6+
7+
### Ads Client
8+
- Add support for IAB content categories and the `flags.contextual_placement` request flag on ad requests. Callers can attach an `iabContent` (taxonomy + category IDs) to each `MozAdsPlacementRequest`, and set `contextualPlacement: true` on `MozAdsRequestOptions` to opt the request into contextual placement on the server side (AC-109).
9+
510
### Glean
611
- Updated to v68.0.0 ([#7438](https://github.com/mozilla/application-services/issues/7438))
712

components/ads-client/docs/usage-javascript.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,16 @@ Options passed when making a single ad request.
246246
/**
247247
* @typedef {Object} MozAdsRequestOptions
248248
* @property {MozAdsRequestCachePolicy|null} cachePolicy - Per-request caching policy.
249+
* @property {Object.<string, boolean>} flags - Request-level flags forwarded as the `flags` object on the wire. An empty object omits it.
249250
* @property {boolean} ohttp - Whether to route this request through OHTTP (default: false).
250251
*/
251252
```
252253

253-
| Field | Type | Description |
254-
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- |
255-
| `cachePolicy` | `MozAdsRequestCachePolicy \| null` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. |
256-
| `ohttp` | `boolean` | Whether to route this request through OHTTP. Defaults to `false`. |
254+
| Field | Type | Description |
255+
| ------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
256+
| `cachePolicy` | `MozAdsRequestCachePolicy \| null` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. |
257+
| `flags` | `Object.<string, boolean>` | Request-level flags forwarded verbatim as the `flags` object on the wire. An empty object omits it. e.g. `{ contextual_placement: true }`. Defaults to `{}`. |
258+
| `ohttp` | `boolean` | Whether to route this request through OHTTP. Defaults to `false`. |
257259

258260
---
259261

components/ads-client/docs/usage-kotlin.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,16 @@ Options passed when making a single ad request.
220220
```kotlin
221221
data class MozAdsRequestOptions(
222222
val cachePolicy: MozAdsRequestCachePolicy?,
223+
val flags: Map<String, Boolean> = emptyMap(),
223224
val ohttp: Boolean = false
224225
)
225226
```
226227

227-
| Field | Type | Description |
228-
| -------------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
229-
| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. |
230-
| `ohttp` | `Boolean` | Whether to route this request through OHTTP. Defaults to `false`. |
228+
| Field | Type | Description |
229+
| ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
230+
| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. |
231+
| `flags` | `Map<String, Boolean>` | Request-level flags forwarded verbatim as the `flags` object on the wire. An empty map omits it. e.g. `mapOf("contextual_placement" to true)`. Defaults to `emptyMap()`. |
232+
| `ohttp` | `Boolean` | Whether to route this request through OHTTP. Defaults to `false`. |
231233

232234
---
233235

components/ads-client/docs/usage-swift.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,16 @@ Options passed when making a single ad request.
220220
```swift
221221
struct MozAdsRequestOptions {
222222
let cachePolicy: MozAdsRequestCachePolicy?
223+
let flags: [String: Bool] // default: [:]
223224
let ohttp: Bool // default: false
224225
}
225226
```
226227

227-
| Field | Type | Description |
228-
| -------------- | ----------------------------- | --------------------------------------------------------------------------------------------- |
229-
| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `nil`, uses the client's default TTL with a `cacheFirst` mode. |
230-
| `ohttp` | `Bool` | Whether to route this request through OHTTP. Defaults to `false`. |
228+
| Field | Type | Description |
229+
| ------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
230+
| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `nil`, uses the client's default TTL with a `cacheFirst` mode. |
231+
| `flags` | `[String: Bool]` | Request-level flags forwarded verbatim as the `flags` object on the wire. An empty dictionary omits it. e.g. `["contextual_placement": true]`. Defaults to `[:]`. |
232+
| `ohttp` | `Bool` | Whether to route this request through OHTTP. Defaults to `false`. |
231233

232234
---
233235

components/ads-client/integration-tests/tests/mars.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
use std::sync::Arc;
77

88
use ads_client::{
9-
MozAdsClientBuilder, MozAdsEnvironment, MozAdsPlacementRequest,
10-
MozAdsPlacementRequestWithCount, MozAdsReportReason, MozAdsRequestOptions,
9+
MozAdsClientBuilder, MozAdsEnvironment, MozAdsIABContent, MozAdsIABContentTaxonomy,
10+
MozAdsPlacementRequest, MozAdsPlacementRequestWithCount, MozAdsReportReason,
11+
MozAdsRequestOptions,
1112
};
1213

1314
fn init_backend() {
@@ -44,6 +45,39 @@ fn test_contract_image_prod() {
4445
assert!(placements.contains_key("mock_billboard_1"));
4546
}
4647

48+
#[test]
49+
#[ignore = "integration test: run manually with -- --ignored"]
50+
fn test_contract_image_with_categories_prod() {
51+
init_backend();
52+
53+
let client = prod_client();
54+
let result = client.request_image_ads(
55+
vec![MozAdsPlacementRequest {
56+
iab_content: Some(MozAdsIABContent {
57+
category_ids: vec!["338".to_string()],
58+
taxonomy: MozAdsIABContentTaxonomy::IAB3_0,
59+
}),
60+
placement_id: "mock_billboard_1".to_string(),
61+
}],
62+
Some(MozAdsRequestOptions {
63+
flags: std::collections::HashMap::from([("contextual_placement".to_string(), true)]),
64+
..Default::default()
65+
}),
66+
);
67+
68+
assert!(
69+
result.is_ok(),
70+
"Image ad request with categories failed: {:?}",
71+
result.err()
72+
);
73+
let placements = result.unwrap();
74+
let ad = placements
75+
.get("mock_billboard_1")
76+
.expect("mock_billboard_1 should be present in the response");
77+
assert!(!ad.url.is_empty(), "destination url should be populated");
78+
assert!(!ad.image_url.is_empty(), "image url should be populated");
79+
}
80+
4781
#[test]
4882
#[ignore = "integration test: run manually with -- --ignored"]
4983
fn test_contract_spoc_prod() {

components/ads-client/src/client.rs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::collections::HashMap;
77
use std::time::Duration;
88

99
use crate::http_cache::{ByteSize, CachePolicy, HttpCache};
10-
use crate::mars::ad_request::AdPlacementRequest;
10+
use crate::mars::ad_request::{AdPlacementRequest, AdRequestFlags};
1111
use crate::mars::ad_response::{AdImage, AdResponse, AdResponseValue, AdSpoc, AdTile};
1212
use crate::mars::error::{RecordClickError, RecordImpressionError, ReportAdError};
1313
use crate::mars::{MARSClient, ReportReason};
@@ -160,11 +160,12 @@ where
160160
pub fn request_image_ads(
161161
&self,
162162
ad_placement_requests: Vec<AdPlacementRequest>,
163+
flags: AdRequestFlags,
163164
options: Option<CachePolicy>,
164165
ohttp: bool,
165166
) -> Result<HashMap<String, AdImage>, RequestAdsError> {
166167
let response = self
167-
.request_ads::<AdImage>(ad_placement_requests, options, ohttp)
168+
.request_ads::<AdImage>(ad_placement_requests, flags, options, ohttp)
168169
.inspect_err(|e| {
169170
self.telemetry.record(e);
170171
})?;
@@ -175,10 +176,11 @@ where
175176
pub fn request_spoc_ads(
176177
&self,
177178
ad_placement_requests: Vec<AdPlacementRequest>,
179+
flags: AdRequestFlags,
178180
options: Option<CachePolicy>,
179181
ohttp: bool,
180182
) -> Result<HashMap<String, Vec<AdSpoc>>, RequestAdsError> {
181-
let result = self.request_ads::<AdSpoc>(ad_placement_requests, options, ohttp);
183+
let result = self.request_ads::<AdSpoc>(ad_placement_requests, flags, options, ohttp);
182184
result
183185
.inspect_err(|e| {
184186
self.telemetry.record(e);
@@ -192,10 +194,11 @@ where
192194
pub fn request_tile_ads(
193195
&self,
194196
ad_placement_requests: Vec<AdPlacementRequest>,
197+
flags: AdRequestFlags,
195198
options: Option<CachePolicy>,
196199
ohttp: bool,
197200
) -> Result<HashMap<String, AdTile>, RequestAdsError> {
198-
let result = self.request_ads::<AdTile>(ad_placement_requests, options, ohttp);
201+
let result = self.request_ads::<AdTile>(ad_placement_requests, flags, options, ohttp);
199202
result
200203
.inspect_err(|e| {
201204
self.telemetry.record(e);
@@ -209,6 +212,7 @@ where
209212
fn request_ads<A>(
210213
&self,
211214
placements: Vec<AdPlacementRequest>,
215+
flags: AdRequestFlags,
212216
options: Option<CachePolicy>,
213217
ohttp: bool,
214218
) -> Result<AdResponse<A>, RequestAdsError>
@@ -219,7 +223,7 @@ where
219223
let cache_policy = options.unwrap_or_default();
220224
let (mut response, request_hash) =
221225
self.client
222-
.fetch_ads::<A>(context_id, placements, cache_policy, ohttp)?;
226+
.fetch_ads::<A>(context_id, flags, placements, cache_policy, ohttp)?;
223227
response.enrich_callbacks(&request_hash);
224228
Ok(response)
225229
}
@@ -289,7 +293,12 @@ mod tests {
289293
let mars_client = MARSClient::new(Environment::Test, None, MozAdsTelemetryWrapper::noop());
290294
let ads_client = new_with_mars_client(mars_client);
291295

292-
let result = ads_client.request_image_ads(make_happy_placement_requests(), None, false);
296+
let result = ads_client.request_image_ads(
297+
make_happy_placement_requests(),
298+
AdRequestFlags::default(),
299+
None,
300+
false,
301+
);
293302
assert!(result.is_ok());
294303
m.assert();
295304
}
@@ -308,7 +317,12 @@ mod tests {
308317
let mars_client = MARSClient::new(Environment::Test, None, MozAdsTelemetryWrapper::noop());
309318
let ads_client = new_with_mars_client(mars_client);
310319

311-
let result = ads_client.request_spoc_ads(make_happy_placement_requests(), None, false);
320+
let result = ads_client.request_spoc_ads(
321+
make_happy_placement_requests(),
322+
AdRequestFlags::default(),
323+
None,
324+
false,
325+
);
312326
assert!(result.is_ok());
313327
m.assert();
314328
}
@@ -327,7 +341,12 @@ mod tests {
327341
let mars_client = MARSClient::new(Environment::Test, None, MozAdsTelemetryWrapper::noop());
328342
let ads_client = new_with_mars_client(mars_client);
329343

330-
let result = ads_client.request_tile_ads(make_happy_placement_requests(), None, false);
344+
let result = ads_client.request_tile_ads(
345+
make_happy_placement_requests(),
346+
AdRequestFlags::default(),
347+
None,
348+
false,
349+
);
331350
assert!(result.is_ok());
332351
m.assert();
333352
}
@@ -363,7 +382,12 @@ mod tests {
363382

364383
assert_eq!(client.get_context_id().unwrap(), "custom-context-id-12345");
365384

366-
let result = client.request_image_ads(make_happy_placement_requests(), None, false);
385+
let result = client.request_image_ads(
386+
make_happy_placement_requests(),
387+
AdRequestFlags::default(),
388+
None,
389+
false,
390+
);
367391
assert!(result.is_ok());
368392
m.assert();
369393
}
@@ -392,7 +416,12 @@ mod tests {
392416
.create();
393417

394418
let response = ads_client
395-
.request_image_ads(make_happy_placement_requests(), None, false)
419+
.request_image_ads(
420+
make_happy_placement_requests(),
421+
AdRequestFlags::default(),
422+
None,
423+
false,
424+
)
396425
.unwrap();
397426
let callback_url = response.values().next().unwrap().callbacks.click.clone();
398427

@@ -401,14 +430,20 @@ mod tests {
401430
.create();
402431

403432
ads_client
404-
.request_image_ads(make_happy_placement_requests(), None, false)
433+
.request_image_ads(
434+
make_happy_placement_requests(),
435+
AdRequestFlags::default(),
436+
None,
437+
false,
438+
)
405439
.unwrap();
406440

407441
ads_client.record_click(callback_url, false).unwrap();
408442

409443
ads_client
410444
.request_ads::<AdImage>(
411445
make_happy_placement_requests(),
446+
AdRequestFlags::default(),
412447
Some(CachePolicy::default()),
413448
false,
414449
)

components/ads-client/src/ffi.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ use crate::client::config::{AdsCacheConfig, AdsClientConfig};
1212
use crate::client::{AdsClient, ContextIdProvider};
1313
use crate::ffi::telemetry::MozAdsTelemetryWrapper;
1414
use crate::http_cache::CachePolicy;
15-
use crate::mars::ad_request::{AdContentCategory, AdPlacementRequest, IABContentTaxonomy};
15+
use crate::mars::ad_request::{
16+
AdContentCategory, AdPlacementRequest, AdRequestFlags, IABContentTaxonomy,
17+
};
1618
use crate::mars::ad_response::{
1719
AdCallbacks, AdImage, AdSpoc, AdTile, SpocFrequencyCaps, SpocRanking,
1820
};
1921
use crate::mars::Environment;
2022
use crate::mars::ReportReason;
2123
use crate::MozAdsClient;
2224
use parking_lot::Mutex;
25+
use std::collections::HashMap;
2326
use url::Url;
2427

2528
pub use error::{AdsClientApiResult, MozAdsClientApiError};
@@ -55,6 +58,8 @@ impl From<MozAdsContextIdProviderWrapper> for Box<dyn ContextIdProvider> {
5558
#[derive(Default, uniffi::Record)]
5659
pub struct MozAdsRequestOptions {
5760
pub cache_policy: Option<MozAdsCachePolicy>,
61+
#[uniffi(default)]
62+
pub flags: HashMap<String, bool>,
5863
#[uniffi(default = false)]
5964
pub ohttp: bool,
6065
}
@@ -425,15 +430,15 @@ impl From<&MozAdsIABContent> for AdContentCategory {
425430
}
426431
}
427432

428-
impl From<MozAdsRequestOptions> for CachePolicy {
429-
fn from(options: MozAdsRequestOptions) -> Self {
430-
options.cache_policy.map(Into::into).unwrap_or_default()
433+
impl From<&MozAdsRequestOptions> for AdRequestFlags {
434+
fn from(options: &MozAdsRequestOptions) -> Self {
435+
options.flags.clone()
431436
}
432437
}
433438

434-
impl From<Option<MozAdsRequestOptions>> for CachePolicy {
435-
fn from(options: Option<MozAdsRequestOptions>) -> Self {
436-
options.map(Into::into).unwrap_or_default()
439+
impl From<MozAdsRequestOptions> for CachePolicy {
440+
fn from(options: MozAdsRequestOptions) -> Self {
441+
options.cache_policy.map(Into::into).unwrap_or_default()
437442
}
438443
}
439444

components/ads-client/src/lib.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use url::Url as AdsClientUrl;
1313

1414
use client::AdsClient;
1515
use http_cache::CachePolicy;
16-
use mars::ad_request::AdPlacementRequest;
16+
use mars::ad_request::{AdPlacementRequest, AdRequestFlags};
1717

1818
mod client;
1919
mod ffi;
@@ -111,10 +111,12 @@ impl MozAdsClient {
111111
) -> AdsClientApiResult<HashMap<String, MozAdsImage>> {
112112
let inner = self.inner.lock();
113113
let requests: Vec<AdPlacementRequest> = moz_ad_requests.iter().map(|r| r.into()).collect();
114-
let ohttp = options.as_ref().map(|o| o.ohttp).unwrap_or(false);
114+
let options = options.unwrap_or_default();
115+
let flags = AdRequestFlags::from(&options);
116+
let ohttp = options.ohttp;
115117
let cache_policy: CachePolicy = options.into();
116118
let response = inner
117-
.request_image_ads(requests, Some(cache_policy), ohttp)
119+
.request_image_ads(requests, flags, Some(cache_policy), ohttp)
118120
.map_err(ComponentError::RequestAds)?;
119121
Ok(response.into_iter().map(|(k, v)| (k, v.into())).collect())
120122
}
@@ -128,10 +130,12 @@ impl MozAdsClient {
128130
) -> AdsClientApiResult<HashMap<String, Vec<MozAdsSpoc>>> {
129131
let inner = self.inner.lock();
130132
let requests: Vec<AdPlacementRequest> = moz_ad_requests.iter().map(|r| r.into()).collect();
131-
let ohttp = options.as_ref().map(|o| o.ohttp).unwrap_or(false);
133+
let options = options.unwrap_or_default();
134+
let flags = AdRequestFlags::from(&options);
135+
let ohttp = options.ohttp;
132136
let cache_policy: CachePolicy = options.into();
133137
let response = inner
134-
.request_spoc_ads(requests, Some(cache_policy), ohttp)
138+
.request_spoc_ads(requests, flags, Some(cache_policy), ohttp)
135139
.map_err(ComponentError::RequestAds)?;
136140
Ok(response
137141
.into_iter()
@@ -148,10 +152,12 @@ impl MozAdsClient {
148152
) -> AdsClientApiResult<HashMap<String, MozAdsTile>> {
149153
let inner = self.inner.lock();
150154
let requests: Vec<AdPlacementRequest> = moz_ad_requests.iter().map(|r| r.into()).collect();
151-
let ohttp = options.as_ref().map(|o| o.ohttp).unwrap_or(false);
155+
let options = options.unwrap_or_default();
156+
let flags = AdRequestFlags::from(&options);
157+
let ohttp = options.ohttp;
152158
let cache_policy: CachePolicy = options.into();
153159
let response = inner
154-
.request_tile_ads(requests, Some(cache_policy), ohttp)
160+
.request_tile_ads(requests, flags, Some(cache_policy), ohttp)
155161
.map_err(ComponentError::RequestAds)?;
156162
Ok(response.into_iter().map(|(k, v)| (k, v.into())).collect())
157163
}

0 commit comments

Comments
 (0)