Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

[Full Changelog](In progress)

## ✨ What's New ✨

### Ads Client
- 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).

# v152.0 (_2026-05-18_)

## ⚠️ Breaking Changes ⚠️
Expand Down
10 changes: 6 additions & 4 deletions components/ads-client/docs/usage-javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,14 +246,16 @@ Options passed when making a single ad request.
/**
* @typedef {Object} MozAdsRequestOptions
* @property {MozAdsRequestCachePolicy|null} cachePolicy - Per-request caching policy.
* @property {Object.<string, boolean>} flags - Request-level flags forwarded as the `flags` object on the wire. An empty object omits it.
* @property {boolean} ohttp - Whether to route this request through OHTTP (default: false).
*/
```

| Field | Type | Description |
| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `cachePolicy` | `MozAdsRequestCachePolicy \| null` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. |
| `ohttp` | `boolean` | Whether to route this request through OHTTP. Defaults to `false`. |
| Field | Type | Description |
| ------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `cachePolicy` | `MozAdsRequestCachePolicy \| null` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. |
| `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 `{}`. |
| `ohttp` | `boolean` | Whether to route this request through OHTTP. Defaults to `false`. |

---

Expand Down
10 changes: 6 additions & 4 deletions components/ads-client/docs/usage-kotlin.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,16 @@ Options passed when making a single ad request.
```kotlin
data class MozAdsRequestOptions(
val cachePolicy: MozAdsRequestCachePolicy?,
val flags: Map<String, Boolean> = emptyMap(),
val ohttp: Boolean = false
)
```

| Field | Type | Description |
| -------------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. |
| `ohttp` | `Boolean` | Whether to route this request through OHTTP. Defaults to `false`. |
| Field | Type | Description |
| ------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. |
| `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()`. |
| `ohttp` | `Boolean` | Whether to route this request through OHTTP. Defaults to `false`. |

---

Expand Down
10 changes: 6 additions & 4 deletions components/ads-client/docs/usage-swift.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,16 @@ Options passed when making a single ad request.
```swift
struct MozAdsRequestOptions {
let cachePolicy: MozAdsRequestCachePolicy?
let flags: [String: Bool] // default: [:]
let ohttp: Bool // default: false
}
```

| Field | Type | Description |
| -------------- | ----------------------------- | --------------------------------------------------------------------------------------------- |
| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `nil`, uses the client's default TTL with a `cacheFirst` mode. |
| `ohttp` | `Bool` | Whether to route this request through OHTTP. Defaults to `false`. |
| Field | Type | Description |
| ------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `nil`, uses the client's default TTL with a `cacheFirst` mode. |
| `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 `[:]`. |
| `ohttp` | `Bool` | Whether to route this request through OHTTP. Defaults to `false`. |

---

Expand Down
38 changes: 36 additions & 2 deletions components/ads-client/integration-tests/tests/mars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
use std::sync::Arc;

use ads_client::{
MozAdsClientBuilder, MozAdsEnvironment, MozAdsPlacementRequest,
MozAdsPlacementRequestWithCount, MozAdsReportReason, MozAdsRequestOptions,
MozAdsClientBuilder, MozAdsEnvironment, MozAdsIABContent, MozAdsIABContentTaxonomy,
MozAdsPlacementRequest, MozAdsPlacementRequestWithCount, MozAdsReportReason,
MozAdsRequestOptions,
};

fn init_backend() {
Expand Down Expand Up @@ -44,6 +45,39 @@ fn test_contract_image_prod() {
assert!(placements.contains_key("mock_billboard_1"));
}

#[test]
#[ignore = "integration test: run manually with -- --ignored"]
fn test_contract_image_with_categories_prod() {
init_backend();

let client = prod_client();
let result = client.request_image_ads(
vec![MozAdsPlacementRequest {
iab_content: Some(MozAdsIABContent {
category_ids: vec!["338".to_string()],
taxonomy: MozAdsIABContentTaxonomy::IAB3_0,
}),
placement_id: "mock_billboard_1".to_string(),
}],
Some(MozAdsRequestOptions {
flags: std::collections::HashMap::from([("contextual_placement".to_string(), true)]),
..Default::default()
}),
);

assert!(
result.is_ok(),
"Image ad request with categories failed: {:?}",
result.err()
);
let placements = result.unwrap();
let ad = placements
.get("mock_billboard_1")
.expect("mock_billboard_1 should be present in the response");
assert!(!ad.url.is_empty(), "destination url should be populated");
assert!(!ad.image_url.is_empty(), "image url should be populated");
}

#[test]
#[ignore = "integration test: run manually with -- --ignored"]
fn test_contract_spoc_prod() {
Expand Down
57 changes: 46 additions & 11 deletions components/ads-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::collections::HashMap;
use std::time::Duration;

use crate::http_cache::{ByteSize, CachePolicy, HttpCache};
use crate::mars::ad_request::AdPlacementRequest;
use crate::mars::ad_request::{AdPlacementRequest, AdRequestFlags};
use crate::mars::ad_response::{AdImage, AdResponse, AdResponseValue, AdSpoc, AdTile};
use crate::mars::error::{RecordClickError, RecordImpressionError, ReportAdError};
use crate::mars::{MARSClient, ReportReason};
Expand Down Expand Up @@ -160,11 +160,12 @@ where
pub fn request_image_ads(
&self,
ad_placement_requests: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
) -> Result<HashMap<String, AdImage>, RequestAdsError> {
let response = self
.request_ads::<AdImage>(ad_placement_requests, options, ohttp)
.request_ads::<AdImage>(ad_placement_requests, flags, options, ohttp)
.inspect_err(|e| {
self.telemetry.record(e);
})?;
Expand All @@ -175,10 +176,11 @@ where
pub fn request_spoc_ads(
&self,
ad_placement_requests: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
) -> Result<HashMap<String, Vec<AdSpoc>>, RequestAdsError> {
let result = self.request_ads::<AdSpoc>(ad_placement_requests, options, ohttp);
let result = self.request_ads::<AdSpoc>(ad_placement_requests, flags, options, ohttp);
result
.inspect_err(|e| {
self.telemetry.record(e);
Expand All @@ -192,10 +194,11 @@ where
pub fn request_tile_ads(
&self,
ad_placement_requests: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
) -> Result<HashMap<String, AdTile>, RequestAdsError> {
let result = self.request_ads::<AdTile>(ad_placement_requests, options, ohttp);
let result = self.request_ads::<AdTile>(ad_placement_requests, flags, options, ohttp);
result
.inspect_err(|e| {
self.telemetry.record(e);
Expand All @@ -209,6 +212,7 @@ where
fn request_ads<A>(
&self,
placements: Vec<AdPlacementRequest>,
flags: AdRequestFlags,
options: Option<CachePolicy>,
ohttp: bool,
) -> Result<AdResponse<A>, RequestAdsError>
Expand All @@ -219,7 +223,7 @@ where
let cache_policy = options.unwrap_or_default();
let (mut response, request_hash) =
self.client
.fetch_ads::<A>(context_id, placements, cache_policy, ohttp)?;
.fetch_ads::<A>(context_id, flags, placements, cache_policy, ohttp)?;
response.enrich_callbacks(&request_hash);
Ok(response)
}
Expand Down Expand Up @@ -289,7 +293,12 @@ mod tests {
let mars_client = MARSClient::new(Environment::Test, None, MozAdsTelemetryWrapper::noop());
let ads_client = new_with_mars_client(mars_client);

let result = ads_client.request_image_ads(make_happy_placement_requests(), None, false);
let result = ads_client.request_image_ads(
make_happy_placement_requests(),
AdRequestFlags::default(),
None,
false,
);
assert!(result.is_ok());
m.assert();
}
Expand All @@ -308,7 +317,12 @@ mod tests {
let mars_client = MARSClient::new(Environment::Test, None, MozAdsTelemetryWrapper::noop());
let ads_client = new_with_mars_client(mars_client);

let result = ads_client.request_spoc_ads(make_happy_placement_requests(), None, false);
let result = ads_client.request_spoc_ads(
make_happy_placement_requests(),
AdRequestFlags::default(),
None,
false,
);
assert!(result.is_ok());
m.assert();
}
Expand All @@ -327,7 +341,12 @@ mod tests {
let mars_client = MARSClient::new(Environment::Test, None, MozAdsTelemetryWrapper::noop());
let ads_client = new_with_mars_client(mars_client);

let result = ads_client.request_tile_ads(make_happy_placement_requests(), None, false);
let result = ads_client.request_tile_ads(
make_happy_placement_requests(),
AdRequestFlags::default(),
None,
false,
);
assert!(result.is_ok());
m.assert();
}
Expand Down Expand Up @@ -363,7 +382,12 @@ mod tests {

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

let result = client.request_image_ads(make_happy_placement_requests(), None, false);
let result = client.request_image_ads(
make_happy_placement_requests(),
AdRequestFlags::default(),
None,
false,
);
assert!(result.is_ok());
m.assert();
}
Expand Down Expand Up @@ -392,7 +416,12 @@ mod tests {
.create();

let response = ads_client
.request_image_ads(make_happy_placement_requests(), None, false)
.request_image_ads(
make_happy_placement_requests(),
AdRequestFlags::default(),
None,
false,
)
.unwrap();
let callback_url = response.values().next().unwrap().callbacks.click.clone();

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

ads_client
.request_image_ads(make_happy_placement_requests(), None, false)
.request_image_ads(
make_happy_placement_requests(),
AdRequestFlags::default(),
None,
false,
)
.unwrap();

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

ads_client
.request_ads::<AdImage>(
make_happy_placement_requests(),
AdRequestFlags::default(),
Some(CachePolicy::default()),
false,
)
Expand Down
19 changes: 12 additions & 7 deletions components/ads-client/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ use crate::client::config::{AdsCacheConfig, AdsClientConfig};
use crate::client::{AdsClient, ContextIdProvider};
use crate::ffi::telemetry::MozAdsTelemetryWrapper;
use crate::http_cache::CachePolicy;
use crate::mars::ad_request::{AdContentCategory, AdPlacementRequest, IABContentTaxonomy};
use crate::mars::ad_request::{
AdContentCategory, AdPlacementRequest, AdRequestFlags, IABContentTaxonomy,
};
use crate::mars::ad_response::{
AdCallbacks, AdImage, AdSpoc, AdTile, SpocFrequencyCaps, SpocRanking,
};
use crate::mars::Environment;
use crate::mars::ReportReason;
use crate::MozAdsClient;
use parking_lot::Mutex;
use std::collections::HashMap;
use url::Url;

pub use error::{AdsClientApiResult, MozAdsClientApiError};
Expand Down Expand Up @@ -55,6 +58,8 @@ impl From<MozAdsContextIdProviderWrapper> for Box<dyn ContextIdProvider> {
#[derive(Default, uniffi::Record)]
pub struct MozAdsRequestOptions {
pub cache_policy: Option<MozAdsCachePolicy>,
#[uniffi(default)]
pub flags: HashMap<String, bool>,
#[uniffi(default = false)]
pub ohttp: bool,
}
Expand Down Expand Up @@ -425,15 +430,15 @@ impl From<&MozAdsIABContent> for AdContentCategory {
}
}

impl From<MozAdsRequestOptions> for CachePolicy {
fn from(options: MozAdsRequestOptions) -> Self {
options.cache_policy.map(Into::into).unwrap_or_default()
impl From<&MozAdsRequestOptions> for AdRequestFlags {
fn from(options: &MozAdsRequestOptions) -> Self {
options.flags.clone()
}
}

impl From<Option<MozAdsRequestOptions>> for CachePolicy {
fn from(options: Option<MozAdsRequestOptions>) -> Self {
options.map(Into::into).unwrap_or_default()
impl From<MozAdsRequestOptions> for CachePolicy {
fn from(options: MozAdsRequestOptions) -> Self {
options.cache_policy.map(Into::into).unwrap_or_default()
}
}

Expand Down
20 changes: 13 additions & 7 deletions components/ads-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use url::Url as AdsClientUrl;

use client::AdsClient;
use http_cache::CachePolicy;
use mars::ad_request::AdPlacementRequest;
use mars::ad_request::{AdPlacementRequest, AdRequestFlags};

mod client;
mod ffi;
Expand Down Expand Up @@ -111,10 +111,12 @@ impl MozAdsClient {
) -> AdsClientApiResult<HashMap<String, MozAdsImage>> {
let inner = self.inner.lock();
let requests: Vec<AdPlacementRequest> = moz_ad_requests.iter().map(|r| r.into()).collect();
let ohttp = options.as_ref().map(|o| o.ohttp).unwrap_or(false);
let options = options.unwrap_or_default();
let flags = AdRequestFlags::from(&options);
let ohttp = options.ohttp;
let cache_policy: CachePolicy = options.into();
let response = inner
.request_image_ads(requests, Some(cache_policy), ohttp)
.request_image_ads(requests, flags, Some(cache_policy), ohttp)
.map_err(ComponentError::RequestAds)?;
Ok(response.into_iter().map(|(k, v)| (k, v.into())).collect())
}
Expand All @@ -128,10 +130,12 @@ impl MozAdsClient {
) -> AdsClientApiResult<HashMap<String, Vec<MozAdsSpoc>>> {
let inner = self.inner.lock();
let requests: Vec<AdPlacementRequest> = moz_ad_requests.iter().map(|r| r.into()).collect();
let ohttp = options.as_ref().map(|o| o.ohttp).unwrap_or(false);
let options = options.unwrap_or_default();
let flags = AdRequestFlags::from(&options);
let ohttp = options.ohttp;
let cache_policy: CachePolicy = options.into();
let response = inner
.request_spoc_ads(requests, Some(cache_policy), ohttp)
.request_spoc_ads(requests, flags, Some(cache_policy), ohttp)
.map_err(ComponentError::RequestAds)?;
Ok(response
.into_iter()
Expand All @@ -148,10 +152,12 @@ impl MozAdsClient {
) -> AdsClientApiResult<HashMap<String, MozAdsTile>> {
let inner = self.inner.lock();
let requests: Vec<AdPlacementRequest> = moz_ad_requests.iter().map(|r| r.into()).collect();
let ohttp = options.as_ref().map(|o| o.ohttp).unwrap_or(false);
let options = options.unwrap_or_default();
let flags = AdRequestFlags::from(&options);
let ohttp = options.ohttp;
let cache_policy: CachePolicy = options.into();
let response = inner
.request_tile_ads(requests, Some(cache_policy), ohttp)
.request_tile_ads(requests, flags, Some(cache_policy), ohttp)
.map_err(ComponentError::RequestAds)?;
Ok(response.into_iter().map(|(k, v)| (k, v.into())).collect())
}
Expand Down
Loading
Loading