Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
### Ads Client
- Added `rotation_days` parameter to `MozAdsClientBuilder` to allow embedders to configure the context ID rotation period. ([#7262](https://github.com/mozilla/application-services/pull/7262))
- Added `reason` parameter to `report_ad` to comply with the MARS `/v1/t` tracking endpoint spec. Accepted values: `inappropriate`, `not_interested`, `seen_too_many_times`.
- Added `MozAdsContextIdProvider` callback interface and `context_id_provider()` builder method, allowing embedders (e.g. HNT) to supply an externally managed context ID. When a provider is set the embedded `ContextIDComponent` is not created. Mobile consumers that do not set a provider are unaffected. ([AC-95](https://mozilla-hub.atlassian.net/browse/AC-95))
Comment thread
copyrighthero marked this conversation as resolved.

### Logins
- **BREAKING**: Removed `time_of_last_breach` field from `LoginMeta` and `Login`. This can be derived from Remote Settings during runtime instead.
Expand Down
89 changes: 65 additions & 24 deletions components/ads-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,23 @@ const DEFAULT_TTL_SECONDS: u64 = 300;
const DEFAULT_MAX_CACHE_SIZE_MIB: u64 = 10;
const DEFAULT_ROTATION_DAYS: u8 = 3;

pub trait ContextIdProvider: Send + Sync {
fn context_id(&self) -> context_id::ApiResult<String>;
}

impl ContextIdProvider for ContextIDComponent {
fn context_id(&self) -> context_id::ApiResult<String> {
self.request(DEFAULT_ROTATION_DAYS)
}
}

pub struct AdsClient<T>
where
T: Clone + Telemetry,
{
client: MARSClient<T>,
context_id_component: ContextIDComponent,
context_id_provider: Box<dyn ContextIdProvider>,
environment: Environment,
rotation_days: u8,
telemetry: T,
}

Expand All @@ -60,16 +69,17 @@ where
T: Clone + Telemetry,
{
pub fn new(client_config: AdsClientConfig<T>) -> Self {
let context_id = Uuid::new_v4().to_string();
let context_id_component = ContextIDComponent::new(
&context_id,
0,
cfg!(test),
Box::new(DefaultContextIdCallback),
);
let context_id_provider = client_config.context_id_provider.unwrap_or_else(|| {
Box::new(ContextIDComponent::new(
&Uuid::new_v4().to_string(),
0,
cfg!(test),
Box::new(DefaultContextIdCallback),
))
});

let telemetry = client_config.telemetry;
let environment = client_config.environment;
let rotation_days = client_config.rotation_days.unwrap_or(DEFAULT_ROTATION_DAYS);

// Configure the cache if a path is provided.
// Defaults for ttl and cache size are also set if unspecified.
Expand Down Expand Up @@ -97,9 +107,8 @@ where
let client = MARSClient::new(http_cache, telemetry.clone());
let client = Self {
client,
context_id_component,
context_id_provider,
environment,
rotation_days,
telemetry: telemetry.clone(),
};
telemetry.record(&ClientOperationEvent::New);
Expand All @@ -109,9 +118,8 @@ where
let client = MARSClient::new(None, telemetry.clone());
let client = Self {
client,
context_id_component,
context_id_provider,
environment,
rotation_days,
telemetry: telemetry.clone(),
};
telemetry.record(&ClientOperationEvent::New);
Expand All @@ -123,7 +131,7 @@ where
}

pub fn get_context_id(&self) -> context_id::ApiResult<String> {
self.context_id_component.request(self.rotation_days)
self.context_id_provider.context_id()
}

pub fn record_click(&self, click_url: Url) -> Result<(), RecordClickError> {
Expand Down Expand Up @@ -261,17 +269,15 @@ mod tests {
fn new_with_mars_client(
client: MARSClient<MozAdsTelemetryWrapper>,
) -> AdsClient<MozAdsTelemetryWrapper> {
let context_id_component = ContextIDComponent::new(
&uuid::Uuid::new_v4().to_string(),
0,
false,
Box::new(DefaultContextIdCallback),
);
AdsClient {
client,
context_id_component,
context_id_provider: Box::new(ContextIDComponent::new(
&Uuid::new_v4().to_string(),
0,
false,
Box::new(DefaultContextIdCallback),
)),
environment: Environment::Test,
rotation_days: DEFAULT_ROTATION_DAYS,
telemetry: MozAdsTelemetryWrapper::noop(),
}
}
Expand All @@ -280,8 +286,8 @@ mod tests {
fn test_get_context_id() {
let config = AdsClientConfig {
cache_config: None,
context_id_provider: None,
Comment thread
copyrighthero marked this conversation as resolved.
environment: Environment::Test,
rotation_days: None,
telemetry: MozAdsTelemetryWrapper::noop(),
};
let client = AdsClient::new(config);
Expand Down Expand Up @@ -355,6 +361,41 @@ mod tests {
assert!(result.is_ok());
}

#[test]
fn test_custom_context_id_provider() {
viaduct_dev::init_backend_dev();

struct FixedContextId;
impl ContextIdProvider for FixedContextId {
fn context_id(&self) -> context_id::ApiResult<String> {
Ok("custom-context-id-12345".to_string())
}
}

let expected_response = get_example_happy_image_response();
let _m = mockito::mock("POST", "/ads")
.match_body(mockito::Matcher::PartialJsonString(
r#"{"context_id":"custom-context-id-12345"}"#.to_string(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&expected_response.data).unwrap())
.create();

let config = AdsClientConfig {
cache_config: None,
context_id_provider: Some(Box::new(FixedContextId)),
environment: Environment::Test,
telemetry: MozAdsTelemetryWrapper::noop(),
};
let client = AdsClient::new(config);

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

let result = client.request_image_ads(make_happy_placement_requests(), None);
assert!(result.is_ok());
}

#[test]
#[ignore = "Cache invalidation temporarily disabled - will be re-enabled behind Nimbus experiment"]
fn test_record_click_invalidates_cache() {
Expand Down
2 changes: 1 addition & 1 deletion components/ads-client/src/client/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ where
T: Telemetry,
{
pub cache_config: Option<AdsCacheConfig>,
pub context_id_provider: Option<Box<dyn super::ContextIdProvider>>,
pub environment: Environment,
pub rotation_days: Option<u8>,
pub telemetry: T,
}

Expand Down
57 changes: 41 additions & 16 deletions components/ads-client/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ use crate::client::ad_response::{
AdCallbacks, AdImage, AdSpoc, AdTile, SpocFrequencyCaps, SpocRanking,
};
use crate::client::config::{AdsCacheConfig, AdsClientConfig, Environment};
use crate::client::AdsClient;
use crate::client::ReportReason;
use crate::client::{AdsClient, ContextIdProvider, ReportReason};
use crate::error::ComponentError;
use crate::ffi::telemetry::MozAdsTelemetryWrapper;
use crate::http_cache::CachePolicy;
Expand All @@ -32,14 +31,6 @@ pub enum MozAdsClientApiError {
Other { reason: String },
}

impl From<context_id::ApiError> for MozAdsClientApiError {
fn from(err: context_id::ApiError) -> Self {
MozAdsClientApiError::Other {
reason: err.to_string(),
}
}
}

impl GetErrorHandling for ComponentError {
type ExternalError = MozAdsClientApiError;

Expand All @@ -50,6 +41,33 @@ impl GetErrorHandling for ComponentError {
}
}

// TODO: Temporary workaround for HNT requirements — do not use for new integrations.
// Context ID management should remain internal to the ads client and this interface should be removed.
#[uniffi::export(with_foreign)]
pub trait MozAdsContextIdProvider: Send + Sync {
fn context_id(&self) -> String;
}
Comment thread
copyrighthero marked this conversation as resolved.

struct MozAdsContextIdProviderWrapper(Arc<dyn MozAdsContextIdProvider>);

impl MozAdsContextIdProviderWrapper {
fn new(provider: Arc<dyn MozAdsContextIdProvider>) -> Self {
Self(provider)
}
}

impl ContextIdProvider for MozAdsContextIdProviderWrapper {
fn context_id(&self) -> context_id::ApiResult<String> {
Ok(self.0.context_id())
}
}

impl From<MozAdsContextIdProviderWrapper> for Box<dyn ContextIdProvider> {
fn from(wrapper: MozAdsContextIdProviderWrapper) -> Self {
Box::new(wrapper)
}
}

#[derive(Default, uniffi::Record)]
pub struct MozAdsRequestOptions {
pub cache_policy: Option<MozAdsCachePolicy>,
Expand Down Expand Up @@ -89,8 +107,8 @@ pub struct MozAdsClientBuilder(Mutex<MozAdsClientBuilderInner>);
#[derive(Default)]
struct MozAdsClientBuilderInner {
cache_config: Option<MozAdsCacheConfig>,
context_id_provider: Option<Arc<dyn MozAdsContextIdProvider>>,
environment: Option<MozAdsEnvironment>,
rotation_days: Option<u8>,
telemetry: Option<Arc<dyn MozAdsTelemetry>>,
}

Expand All @@ -111,8 +129,12 @@ impl MozAdsClientBuilder {
let inner = self.0.lock();
let client_config = AdsClientConfig {
cache_config: inner.cache_config.clone().map(Into::into),
context_id_provider: inner
.context_id_provider
.clone()
.map(MozAdsContextIdProviderWrapper::new)
.map(Into::into),
environment: inner.environment.unwrap_or_default().into(),
rotation_days: inner.rotation_days,
telemetry: inner
.telemetry
.clone()
Expand All @@ -130,13 +152,16 @@ impl MozAdsClientBuilder {
self
}

pub fn environment(self: Arc<Self>, environment: MozAdsEnvironment) -> Arc<Self> {
self.0.lock().environment = Some(environment);
pub fn context_id_provider(
self: Arc<Self>,
provider: Arc<dyn MozAdsContextIdProvider>,
) -> Arc<Self> {
self.0.lock().context_id_provider = Some(provider);
self
}

pub fn rotation_days(self: Arc<Self>, rotation_days: u8) -> Arc<Self> {
self.0.lock().rotation_days = Some(rotation_days);
pub fn environment(self: Arc<Self>, environment: MozAdsEnvironment) -> Arc<Self> {
self.0.lock().environment = Some(environment);
self
}

Expand Down
Loading