diff --git a/CHANGELOG.md b/CHANGELOG.md index 82421279e3..4f4f8a26f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ ### Ads-Client - Telemetry is now a process-wide singleton internally; the binding API is unchanged, but the `MozAdsTelemetry` callback now receives metrics from every client in the process. +- `request_spoc_or_image_ads` is added to return ads that may ambiguously be spocs or images. - Added support for relevance scores ([AC-205](https://mozilla-hub.atlassian.net/browse/AC-205)) ([AC-206](https://mozilla-hub.atlassian.net/browse/AC-206)) Accepts a HashMap. ### Autofill diff --git a/components/ads-client/integration-tests/tests/mars.rs b/components/ads-client/integration-tests/tests/mars.rs index 2b0b109712..77a01a09ba 100644 --- a/components/ads-client/integration-tests/tests/mars.rs +++ b/components/ads-client/integration-tests/tests/mars.rs @@ -98,6 +98,57 @@ fn test_contract_spoc_prod() { assert!(placements.get("mock_spoc_1").unwrap().len() == 3); } +// Note: this test does not actually test a mock placement that returns spoc *and* image ads, as no such placement currently exists, +// and direct staging calls may be stochastic (depends on inventory, etc.). +// TODO: When such a mock placement exists, add a test for it. +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_contract_spoc_or_image_spocs_prod() { + init_backend(); + + let client = prod_client(); + let result = client.request_spoc_or_image_ads( + vec![MozAdsPlacementRequestWithCount { + count: 3, + iab_content: None, + placement_id: "mock_spoc_1".to_string(), + }], + None, + ); + + assert!(result.is_ok(), "Spoc ad request failed: {:?}", result.err()); + let placements = result.unwrap(); + assert!(placements.contains_key("mock_spoc_1")); + assert!(placements.get("mock_spoc_1").unwrap().len() == 3); +} + +// Note: this test does not actually test a mock placement that returns spoc *and* image ads, as no such placement currently exists, +// and direct staging calls may be stochastic (depends on inventory, etc.). +// TODO: When such a mock placement exists, add a test for it. +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_contract_spoc_or_image_images_prod() { + init_backend(); + + let client = prod_client(); + let result = client.request_spoc_or_image_ads( + vec![MozAdsPlacementRequestWithCount { + count: 1, + iab_content: None, + placement_id: "mock_billboard_1".to_string(), + }], + None, + ); + + assert!( + result.is_ok(), + "Image ad request failed: {:?}", + result.err() + ); + let placements = result.unwrap(); + assert!(placements.contains_key("mock_billboard_1")); +} + #[test] #[ignore = "integration test: run manually with -- --ignored"] fn test_contract_tile_prod() { diff --git a/components/ads-client/src/ads.rs b/components/ads-client/src/ads.rs index 861061c5ac..efc7f4f5cd 100644 --- a/components/ads-client/src/ads.rs +++ b/components/ads-client/src/ads.rs @@ -88,6 +88,13 @@ pub struct AdTile { pub url: Url, } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum AdImageOrSpoc { + Image(AdImage), + Spoc(AdSpoc), +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct SpocFrequencyCaps { pub cap_key: String, diff --git a/components/ads-client/src/client.rs b/components/ads-client/src/client.rs index e777ff38bb..8b645c5c97 100644 --- a/components/ads-client/src/client.rs +++ b/components/ads-client/src/client.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::ads::{AdImage, AdSpoc, AdTile}; +use crate::ads::{AdImage, AdImageOrSpoc, AdSpoc, AdTile}; #[cfg(feature = "stateful")] use crate::ads_store::AdsStore; use crate::bytesize::ByteSize; @@ -232,6 +232,36 @@ impl AdsClient { }) } + // Sometimes, spoc array requests can return multiple types of ads for specific placements. + // This allows both types to be returned without error (as instances of the enum `AdImageOrSpoc`) + // TODO: This, as well as the other synchronous functions, are to be deprecated + // when the stateful client rework is complete. + pub fn request_spoc_or_image_ads( + &self, + ad_placement_requests: Vec, + flags: AdRequestFlags, + options: Option, + ohttp: bool, + blocks: Vec, + ) -> Result>, RequestAdsError> { + let result = self.request_ads::( + ad_placement_requests, + flags, + options, + ohttp, + blocks, + AdRequestRelevance::default(), + ); + result + .inspect_err(|e| { + telemetry::record_client_error(ClientOperation::RequestAds, e); + }) + .map(|response| { + telemetry::record_client_operation(ClientOperation::RequestAds); + response.data + }) + } + pub fn request_tile_ads( &self, ad_placement_requests: Vec, diff --git a/components/ads-client/src/ffi.rs b/components/ads-client/src/ffi.rs index b84c9cd602..de57508ecd 100644 --- a/components/ads-client/src/ffi.rs +++ b/components/ads-client/src/ffi.rs @@ -5,7 +5,9 @@ pub mod error; pub mod telemetry; -use crate::ads::{AdCallbacks, AdImage, AdSpoc, AdTile, SpocFrequencyCaps, SpocRanking}; +use crate::ads::{ + AdCallbacks, AdImage, AdImageOrSpoc, AdSpoc, AdTile, SpocFrequencyCaps, SpocRanking, +}; #[cfg(feature = "stateful")] use crate::client::config::AdsStoreConfig; use crate::client::config::{AdsCacheConfig, AdsClientConfig}; @@ -241,6 +243,12 @@ pub struct MozAdsSpoc { pub url: AdsClientUrl, } +#[derive(Debug, PartialEq, uniffi::Enum)] +pub enum MozAdsImageOrSpoc { + Image(MozAdsImage), + Spoc(MozAdsSpoc), +} + #[derive(Debug, PartialEq, uniffi::Record)] pub struct MozAdsSpocFrequencyCaps { pub cap_key: String, @@ -303,6 +311,15 @@ impl From for MozAdsSpocRanking { } } +impl From for MozAdsImageOrSpoc { + fn from(ad: AdImageOrSpoc) -> Self { + match ad { + AdImageOrSpoc::Image(ad) => MozAdsImageOrSpoc::Image(ad.into()), + AdImageOrSpoc::Spoc(ad) => MozAdsImageOrSpoc::Spoc(ad.into()), + } + } +} + impl From for MozAdsImage { fn from(img: AdImage) -> Self { Self { diff --git a/components/ads-client/src/lib.rs b/components/ads-client/src/lib.rs index 46a75d6a07..5399eb611f 100644 --- a/components/ads-client/src/lib.rs +++ b/components/ads-client/src/lib.rs @@ -144,6 +144,29 @@ impl MozAdsClient { .collect()) } + #[handle_error(ComponentError)] + #[uniffi::method(default(options = None))] + pub fn request_spoc_or_image_ads( + &self, + moz_ad_requests: Vec, + options: Option, + ) -> AdsClientApiResult>> { + let requests: Vec = moz_ad_requests.iter().map(|r| r.into()).collect(); + let options = options.unwrap_or_default(); + let flags = AdRequestFlags::from(&options); + let ohttp = options.ohttp; + let cache_policy = options.cache_policy.map(CachePolicy::from); + let blocks = options.blocks; + let response = self + .inner + .request_spoc_or_image_ads(requests, flags, cache_policy, ohttp, blocks) + .map_err(ComponentError::RequestAds)?; + Ok(response + .into_iter() + .map(|(k, v)| (k, v.into_iter().map(|spoc| spoc.into()).collect())) + .collect()) + } + #[handle_error(ComponentError)] #[uniffi::method(default(options = None))] pub fn request_tile_ads( diff --git a/components/ads-client/src/mars/ad_response.rs b/components/ads-client/src/mars/ad_response.rs index e6d8c804bf..a84ed749da 100644 --- a/components/ads-client/src/mars/ad_response.rs +++ b/components/ads-client/src/mars/ad_response.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::ads::{AdCallbacks, AdImage, AdSpoc, AdTile}; +use crate::ads::{AdCallbacks, AdImage, AdImageOrSpoc, AdSpoc, AdTile}; use crate::http_cache::RequestHash; use crate::telemetry; use serde::de::DeserializeOwned; @@ -102,6 +102,22 @@ impl AdResponseValue for AdSpoc { } } +impl AdResponseValue for AdImageOrSpoc { + fn callbacks_mut(&mut self) -> &mut AdCallbacks { + match self { + AdImageOrSpoc::Image(ad) => &mut ad.callbacks, + AdImageOrSpoc::Spoc(ad) => &mut ad.callbacks, + } + } + + fn cap_key(&self) -> Option { + match self { + AdImageOrSpoc::Image(_ad) => None, + AdImageOrSpoc::Spoc(ad) => Some(ad.caps.cap_key.clone()), + } + } +} + impl AdResponseValue for AdTile { fn callbacks_mut(&mut self) -> &mut AdCallbacks { &mut self.callbacks