From 058816ec65e8c800aab8e138fb0ba0d6e555f6e8 Mon Sep 17 00:00:00 2001 From: ahanot Date: Fri, 10 Apr 2026 12:50:39 -0400 Subject: [PATCH] =?UTF-8?q?refactor(ads-client):=20housekeeping=20?= =?UTF-8?q?=E2=80=94=20sort,=20UniFFI=20defaults,=20cache=20refactor,=20sp?= =?UTF-8?q?lit=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sort struct fields and enum variants alphabetically throughout - Sort methods: constructors → pub (alpha) → private (alpha) - Add #[uniffi(default = None)] to optional FFI fields for better consumer ergonomics (iab_content, cache config fields, ttl_seconds) - Replace manual Default impl with #[derive(Default)] (clippy) - Merge CacheMode struct+enum into single CachePolicy enum with Duration ttl; rename send_with_options → send_with_policy - Simplify fetch_and_cache TTL resolution: min(caller_ttl, server_ttl) - Add Client to HttpCache with set_client() for transport abstraction - Use Duration::from_secs() consistently instead of Duration::new(n, 0) - Make each language doc (JS, Kotlin, Swift) a complete standalone guide --- components/ads-client/README.md | 8 +- .../docs/building-locally-for-android.md | 2 +- .../ads-client/docs/usage-javascript.md | 555 +++++++++++++++++ components/ads-client/docs/usage-kotlin.md | 515 ++++++++++++++++ components/ads-client/docs/usage-swift.md | 515 ++++++++++++++++ components/ads-client/docs/usage.md | 571 ------------------ .../integration-tests/tests/http_cache.rs | 15 +- .../integration-tests/tests/mars.rs | 6 +- components/ads-client/src/client.rs | 170 +++--- .../ads-client/src/client/ad_request.rs | 70 +-- components/ads-client/src/client/config.rs | 22 +- components/ads-client/src/error.rs | 26 +- components/ads-client/src/ffi.rs | 129 ++-- components/ads-client/src/http_cache.rs | 149 +++-- .../ads-client/src/http_cache/builder.rs | 4 +- components/ads-client/src/http_cache/store.rs | 34 +- components/ads-client/src/lib.rs | 8 +- components/ads-client/src/mars.rs | 51 +- components/ads-client/src/test_utils.rs | 20 +- 19 files changed, 1931 insertions(+), 939 deletions(-) create mode 100644 components/ads-client/docs/usage-javascript.md create mode 100644 components/ads-client/docs/usage-kotlin.md create mode 100644 components/ads-client/docs/usage-swift.md delete mode 100644 components/ads-client/docs/usage.md diff --git a/components/ads-client/README.md b/components/ads-client/README.md index fbe9a0a85b..84ac4d0621 100644 --- a/components/ads-client/README.md +++ b/components/ads-client/README.md @@ -44,4 +44,10 @@ cargo test -p ads-client-integration-tests --test mars test_contract_image_stagi ## Usage -Please refer to `./docs/usage.md` for information on using the component. +Full API reference and usage guides for each supported language: + +- [Swift](./docs/usage-swift.md) +- [Kotlin](./docs/usage-kotlin.md) +- [JavaScript](./docs/usage-javascript.md) + +Each guide is a complete standalone document containing all type definitions, API tables, cache behavior documentation, and code examples in that language. diff --git a/components/ads-client/docs/building-locally-for-android.md b/components/ads-client/docs/building-locally-for-android.md index c07d2786b6..4650b74ca3 100644 --- a/components/ads-client/docs/building-locally-for-android.md +++ b/components/ads-client/docs/building-locally-for-android.md @@ -189,4 +189,4 @@ If Fenix doesn't auto-publish application-services: - [Main Application Services Building Guide](../../../docs/building.md) - [Auto-Publishing Workflow Documentation](../../../docs/howtos/locally-published-components-in-fenix.md) - [Android FAQs](../../../docs/android-faqs.md) -- [ads-client Usage Documentation](./usage.md) +- [ads-client Usage Documentation (Kotlin)](./usage-kotlin.md) diff --git a/components/ads-client/docs/usage-javascript.md b/components/ads-client/docs/usage-javascript.md new file mode 100644 index 0000000000..af36008d0c --- /dev/null +++ b/components/ads-client/docs/usage-javascript.md @@ -0,0 +1,555 @@ +# Mozilla Ads Client (MAC) — JavaScript API Reference + +## Overview + +This document covers the full API reference for the `ads_client` component, with all examples and type definitions in JavaScript. +It includes every type and function exposed via UniFFI that is part of the public API surface. + +--- + +## `MozAdsClient` + +Top-level client object for requesting ads and recording lifecycle events. + +```javascript +// No public fields — use MozAdsClientBuilder to create instances +const client = MozAdsClientBuilder() + .environment(MozAdsEnvironment.Prod) + .cacheConfig(cache) + .telemetry(telemetry) + .build(); +``` + +#### Creating a Client + +Use the `MozAdsClientBuilder` to configure and create the client. The builder provides a fluent API for setting configuration options. + +```javascript +const client = MozAdsClientBuilder() + .environment(MozAdsEnvironment.Prod) + .cacheConfig(cache) + .telemetry(telemetry) + .build(); +``` + +#### Methods + +| Method | Return Type | Description | +| ----------------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `clearCache()` | `void` | Clears the client's HTTP cache. Throws on failure. | +| `recordClick(clickUrl)` | `void` | Records a click using the provided callback URL (typically from `ad.callbacks.click`). | +| `recordImpression(impressionUrl)` | `void` | Records an impression using the provided callback URL (typically from `ad.callbacks.impression`). | +| `reportAd(reportUrl)` | `void` | Reports an ad using the provided callback URL (typically from `ad.callbacks.report`). | +| `requestImageAds(mozAdRequests, options?)` | `Object.` | Requests one image ad per placement. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns an object keyed by `placementId`. | +| `requestSpocAds(mozAdRequests, options?)` | `Object.>` | Requests spoc ads per placement. Each placement request specifies its own count. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns an object keyed by `placementId`. | +| `requestTileAds(mozAdRequests, options?)` | `Object.` | Requests one tile ad per placement. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns an object keyed by `placementId`. | + +> **Notes** +> +> - We recommend that this client be initialized as a singleton or something similar so that multiple instances of the client do not exist at once. +> - Responses omit placements with no fill. Empty placements do not appear in the returned objects. +> - The HTTP cache is internally managed. Configuration can be set with `MozAdsClientBuilder`. Per-request cache settings can be set with `MozAdsRequestOptions`. +> - If `cacheConfig` is `null`, caching is disabled entirely. + +--- + +## `MozAdsClientBuilder` + +Builder for configuring and creating the ads client. Use the fluent builder pattern to set configuration options. + +```javascript +/** + * @returns {MozAdsClientBuilder} + */ +MozAdsClientBuilder() + +/** + * @param {MozAdsEnvironment} environment + * @returns {MozAdsClientBuilder} + */ +builder.environment(environment) + +/** + * @param {MozAdsCacheConfig} cacheConfig + * @returns {MozAdsClientBuilder} + */ +builder.cacheConfig(cacheConfig) + +/** + * @param {MozAdsTelemetry} telemetry + * @returns {MozAdsClientBuilder} + */ +builder.telemetry(telemetry) + +/** + * @returns {MozAdsClient} + */ +builder.build() +``` + +#### Methods + +- **`MozAdsClientBuilder()`** - Creates a new builder with default values +- **`environment(environment)`** - Sets the MARS environment (Prod, Staging, or Test) +- **`cacheConfig(cacheConfig)`** - Sets the cache configuration +- **`telemetry(telemetry)`** - Sets the telemetry implementation +- **`build()`** - Builds and returns the configured client + +| Configuration | Type | Description | +| -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------ | +| `environment` | `MozAdsEnvironment` | Selects which MARS environment to connect to. Unless in a dev build, this value can only ever be Prod. Defaults to Prod. | +| `cacheConfig` | `MozAdsCacheConfig \| null`| Optional configuration for the internal cache. | +| `telemetry` | `MozAdsTelemetry \| null` | Optional telemetry instance for recording metrics. If not provided, a no-op implementation is used. | + +--- + +## `MozAdsTelemetry` + +Telemetry interface for recording ads client metrics. You must provide an implementation of this interface to the `MozAdsClientBuilder` to enable telemetry collection. If no telemetry instance is provided, a no-op implementation is used and no metrics will be recorded. + +```javascript +/** + * @typedef {Object} MozAdsTelemetry + * @property {function(string, string): void} recordBuildCacheError + * @property {function(string, string): void} recordClientError + * @property {function(string): void} recordClientOperationTotal + * @property {function(string, string): void} recordDeserializationError + * @property {function(string, string): void} recordHttpCacheOutcome + */ +``` + +#### Implementation Example + +```javascript +class AdsClientTelemetry { + recordBuildCacheError(label, value) { + // Bind to your telemetry system + } + + recordClientError(label, value) { + // Bind to your telemetry system + } + + recordClientOperationTotal(label) { + // Bind to your telemetry system + } + + recordDeserializationError(label, value) { + // Bind to your telemetry system + } + + recordHttpCacheOutcome(label, value) { + // Bind to your telemetry system + } +} +``` + +--- + +## `MozAdsCacheConfig` + +Describes the behavior and location of the on-disk HTTP cache. + +```javascript +/** + * @typedef {Object} MozAdsCacheConfig + * @property {string} dbPath - Path to the SQLite database file. + * @property {number|null} defaultCacheTtlSeconds - Default TTL in seconds (default: 300). + * @property {number|null} maxSizeMib - Maximum cache size in MiB (default: 10). + */ +``` + +| Field | Type | Description | +| --------------------------- | ---------------- | ------------------------------------------------------------------------------------ | +| `dbPath` | `string` | Path to the SQLite database file used for cache storage. Required to enable caching. | +| `defaultCacheTtlSeconds` | `number \| null` | Default TTL for cached entries. If omitted, defaults to 300 seconds (5 minutes). | +| `maxSizeMib` | `number \| null` | Maximum cache size. If omitted, defaults to 10 MiB. | + +**Defaults** + +- defaultCacheTtlSeconds: 300 seconds (5 min) +- maxSizeMib: 10 MiB + +#### Configuration Example + +```javascript +const cache = MozAdsCacheConfig({ + dbPath: "/tmp/ads_cache.sqlite", + defaultCacheTtlSeconds: 600, // 10 min + maxSizeMib: 20 // 20 MiB +}); + +const telemetry = new AdsClientTelemetry(); + +const client = MozAdsClientBuilder() + .environment(MozAdsEnvironment.Prod) + .cacheConfig(cache) + .telemetry(telemetry) + .build(); +``` + +--- + +## `MozAdsPlacementRequest` + +Describes a single ad placement to request from MARS. An array of these is required for the `requestImageAds` and `requestTileAds` methods on the client. + +```javascript +/** + * @typedef {Object} MozAdsPlacementRequest + * @property {string} placementId - Unique identifier for the ad placement. + * @property {MozAdsIABContent|null} iabContent - Optional IAB content classification. + */ +``` + +| Field | Type | Description | +| -------------- | -------------------------- | ------------------------------------------------------------------------------- | +| `placementId` | `string` | Unique identifier for the ad placement. Must be unique within one request call. | +| `iabContent` | `MozAdsIABContent \| null` | Optional IAB content classification for targeting. | + +**Validation Rules:** + +- `placementId` values must be unique per request. + +--- + +## `MozAdsPlacementRequestWithCount` + +Describes a single ad placement to request from MARS with a count parameter. An array of these is required for the `requestSpocAds` method on the client. + +```javascript +/** + * @typedef {Object} MozAdsPlacementRequestWithCount + * @property {number} count - Number of spoc ads to request. + * @property {string} placementId - Unique identifier for the ad placement. + * @property {MozAdsIABContent|null} iabContent - Optional IAB content classification. + */ +``` + +| Field | Type | Description | +| -------------- | -------------------------- | ------------------------------------------------------------------------------- | +| `count` | `number` | Number of spoc ads to request for this placement. | +| `placementId` | `string` | Unique identifier for the ad placement. Must be unique within one request call. | +| `iabContent` | `MozAdsIABContent \| null` | Optional IAB content classification for targeting. | + +**Validation Rules:** + +- `placementId` values must be unique per request. + +--- + +## `MozAdsRequestOptions` + +Options passed when making a single ad request. + +```javascript +/** + * @typedef {Object} MozAdsRequestOptions + * @property {MozAdsRequestCachePolicy|null} cachePolicy - Per-request caching policy. + */ +``` + +| Field | Type | Description | +| -------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `cachePolicy` | `MozAdsRequestCachePolicy \| null` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. | + +--- + +## `MozAdsRequestCachePolicy` + +Defines how each request interacts with the cache. + +```javascript +/** + * @typedef {Object} MozAdsRequestCachePolicy + * @property {MozAdsCacheMode} mode - Strategy for combining cache and network. + * @property {number|null} ttlSeconds - Optional per-request TTL override in seconds. + */ +``` + +| Field | Type | Description | +| ------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `mode` | `MozAdsCacheMode` | Strategy for combining cache and network. Can be `CacheFirst` or `NetworkFirst`. | +| `ttlSeconds` | `number \| null` | Optional per-request TTL override in seconds. `null` uses the client default. `0` disables caching for this request. | + +#### Per-Request Cache Policy Override Example + +```javascript +// Always fetch from network but only cache for 60 seconds +const options = MozAdsRequestOptions({ + cachePolicy: MozAdsRequestCachePolicy({ mode: MozAdsCacheMode.NetworkFirst, ttlSeconds: 60 }) +}); + +// Use it when requesting ads +const placements = client.requestImageAds(configs, options); +``` + +--- + +## `MozAdsCacheMode` + +Determines how the cache is used during a request. + +```javascript +/** + * @enum {string} + */ +const MozAdsCacheMode = { + CacheFirst: "CacheFirst", + NetworkFirst: "NetworkFirst" +}; +``` + +| Variant | Behavior | +| -------------- | -------------------------------------------------------------------------------------------------- | +| `CacheFirst` | Check cache first, return cached response if found, otherwise make a network request and store it. | +| `NetworkFirst` | Always fetch from network, then cache the result. | + +--- + +## `MozAdsImage` + +The image ad creative, callbacks, and metadata provided for each image ad returned from MARS. + +```javascript +/** + * @typedef {Object} MozAdsImage + * @property {string|null} altText - Alt text if available. + * @property {string} blockKey - The block key generated for the advertiser. + * @property {MozAdsCallbacks} callbacks - Lifecycle callback endpoints. + * @property {string} format - Ad format e.g., "skyscraper". + * @property {string} imageUrl - Creative asset URL. + * @property {string} url - Destination URL. + */ +``` + +| Field | Type | Description | +| ----------- | ----------------- | ------------------------------------------- | +| `url` | `string` | Destination URL. | +| `imageUrl` | `string` | Creative asset URL. | +| `format` | `string` | Ad format e.g., `"skyscraper"`. | +| `blockKey` | `string` | The block key generated for the advertiser. | +| `altText` | `string \| null` | Alt text if available. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsSpoc` + +The spoc ad creative, callbacks, and metadata provided for each spoc ad returned from MARS. + +```javascript +/** + * @typedef {Object} MozAdsSpoc + * @property {string} blockKey - The block key generated for the advertiser. + * @property {MozAdsCallbacks} callbacks - Lifecycle callback endpoints. + * @property {MozAdsSpocFrequencyCaps} caps - Frequency capping information. + * @property {string} domain - Domain of the spoc ad. + * @property {string} excerpt - Spoc ad excerpt/description. + * @property {string} format - Ad format e.g., "spoc". + * @property {string} imageUrl - Creative asset URL. + * @property {MozAdsSpocRanking} ranking - Ranking and personalization information. + * @property {string} sponsor - Sponsor name. + * @property {string|null} sponsoredByOverride - Optional override for sponsor name. + * @property {string} title - Spoc ad title. + * @property {string} url - Destination URL. + */ +``` + +| Field | Type | Description | +| ----------------------- | ------------------------- | ------------------------------------------- | +| `url` | `string` | Destination URL. | +| `imageUrl` | `string` | Creative asset URL. | +| `format` | `string` | Ad format e.g., `"spoc"`. | +| `blockKey` | `string` | The block key generated for the advertiser. | +| `title` | `string` | Spoc ad title. | +| `excerpt` | `string` | Spoc ad excerpt/description. | +| `domain` | `string` | Domain of the spoc ad. | +| `sponsor` | `string` | Sponsor name. | +| `sponsoredByOverride` | `string \| null` | Optional override for sponsor name. | +| `caps` | `MozAdsSpocFrequencyCaps` | Frequency capping information. | +| `ranking` | `MozAdsSpocRanking` | Ranking and personalization information. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsTile` + +The tile ad creative, callbacks, and metadata provided for each tile ad returned from MARS. + +```javascript +/** + * @typedef {Object} MozAdsTile + * @property {string} blockKey - The block key generated for the advertiser. + * @property {MozAdsCallbacks} callbacks - Lifecycle callback endpoints. + * @property {string} format - Ad format e.g., "tile". + * @property {string} imageUrl - Creative asset URL. + * @property {string} name - Tile ad name. + * @property {string} url - Destination URL. + */ +``` + +| Field | Type | Description | +| ----------- | ----------------- | ------------------------------------------- | +| `url` | `string` | Destination URL. | +| `imageUrl` | `string` | Creative asset URL. | +| `format` | `string` | Ad format e.g., `"tile"`. | +| `blockKey` | `string` | The block key generated for the advertiser. | +| `name` | `string` | Tile ad name. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsSpocFrequencyCaps` + +Frequency capping information for spoc ads. + +```javascript +/** + * @typedef {Object} MozAdsSpocFrequencyCaps + * @property {string} capKey - Frequency cap key identifier. + * @property {number} day - Day number for the frequency cap. + */ +``` + +| Field | Type | Description | +| -------- | -------- | --------------------------------- | +| `capKey` | `string` | Frequency cap key identifier. | +| `day` | `number` | Day number for the frequency cap. | + +--- + +## `MozAdsSpocRanking` + +Ranking and personalization information for spoc ads. + +```javascript +/** + * @typedef {Object} MozAdsSpocRanking + * @property {number} priority - Priority score for ranking. + * @property {Object.} personalizationModels - Personalization model scores. + * @property {number} itemScore - Overall item score. + */ +``` + +| Field | Type | Description | +| ------------------------ | ------------------------- | ----------------------------- | +| `priority` | `number` | Priority score for ranking. | +| `personalizationModels` | `Object.` | Personalization model scores. | +| `itemScore` | `number` | Overall item score. | + +--- + +## `MozAdsCallbacks` + +```javascript +/** + * @typedef {Object} MozAdsCallbacks + * @property {string} click - Click callback URL. + * @property {string} impression - Impression callback URL. + * @property {string|null} report - Report callback URL. + */ +``` + +| Field | Type | Description | +| ------------ | ---------------- | ------------------------ | +| `click` | `string` | Click callback URL. | +| `impression` | `string` | Impression callback URL. | +| `report` | `string \| null` | Report callback URL. | + +--- + +## `MozAdsIABContent` + +Provides IAB content classification context for a placement. + +```javascript +/** + * @typedef {Object} MozAdsIABContent + * @property {MozAdsIABContentTaxonomy} taxonomy - IAB taxonomy version. + * @property {string[]} categoryIds - One or more IAB category identifiers. + */ +``` + +| Field | Type | Description | +| -------------- | -------------------------- | ------------------------------------- | +| `taxonomy` | `MozAdsIABContentTaxonomy` | IAB taxonomy version. | +| `categoryIds` | `string[]` | One or more IAB category identifiers. | + +--- + +## `MozAdsIABContentTaxonomy` + +The [IAB Content Taxonomy](https://www.iab.com/guidelines/content-taxonomy/) version to be used in the request. e.g `IAB-1.0` + +```javascript +/** + * @enum {string} + */ +const MozAdsIABContentTaxonomy = { + Iab1_0: "Iab1_0", + Iab2_0: "Iab2_0", + Iab2_1: "Iab2_1", + Iab2_2: "Iab2_2", + Iab3_0: "Iab3_0" +}; +``` + +> Note: The generated UniFFI bindings may use different casing for enum values depending on the JavaScript environment. + +--- + +## Internal Cache Behavior + +### Cache Overview + +The internal HTTP cache is a SQLite-backed key-value store layered over the HTTP request layer. +It reduces redundant network traffic and improves latency for repeated or identical ad requests. + +### Cache Lifecycle + +Each network response can be stored in the cache with an associated effective TTL, computed as: + +``` +effective_ttl = min(server_max_age, client_default_ttl, per_request_ttl) +``` + +where: + +- `server_max_age` comes from the HTTP `Cache-Control: max-age=N` header (if present), +- `client_default_ttl` is set in `MozAdsCacheConfig`, +- `per_request_ttl` is an optional override set in `MozAdsRequestCachePolicy`. + +If the effective TTL resolves to 0 seconds, the response is not cached. + +### Configuring The Cache + +```javascript +const cache = MozAdsCacheConfig({ + dbPath: "/tmp/ads_cache.sqlite", + defaultCacheTtlSeconds: 600, // 10 min + maxSizeMib: 20 // 20 MiB +}); + +const telemetry = new AdsClientTelemetry(); + +const client = MozAdsClientBuilder() + .environment(MozAdsEnvironment.Prod) + .cacheConfig(cache) + .telemetry(telemetry) + .build(); +``` + +Where `dbPath` represents the location of the SQLite file. This must be a file that the client has permission to write to. + +### Cache Invalidation + +**TTL-based expiry (automatic):** + +At the start of each send, the cache computes a cutoff from the current time minus the TTL and deletes rows older than that. This is a coarse, global freshness window that bounds how long entries can live. + +**Size-based trimming (automatic):** +After storing a cacheable miss, the cache enforces `maxSizeMib` by deleting the oldest rows until the total stored size is at or below the maximum allowed size of the cache. Due to the small size of items in the cache and the relatively short TTL, this behavior should be rare. + +**Manual clearing (explicit):** +The cache can be manually cleared by the client using the exposed `client.clearCache()` method. This clears _all_ objects in the cache. diff --git a/components/ads-client/docs/usage-kotlin.md b/components/ads-client/docs/usage-kotlin.md new file mode 100644 index 0000000000..4ebaa7b6d4 --- /dev/null +++ b/components/ads-client/docs/usage-kotlin.md @@ -0,0 +1,515 @@ +# Mozilla Ads Client (MAC) — Kotlin API Reference + +## Overview + +This document covers the full API reference for the `ads_client` component, with all examples and type definitions in Kotlin. +It includes every type and function exposed via UniFFI that is part of the public API surface. + +--- + +## `MozAdsClient` + +Top-level client object for requesting ads and recording lifecycle events. + +```kotlin +class MozAdsClient { + // No public fields — use MozAdsClientBuilder to create instances +} +``` + +#### Creating a Client + +Use the `MozAdsClientBuilder` to configure and create the client. The builder provides a fluent API for setting configuration options. + +```kotlin +val client = MozAdsClientBuilder() + .environment(MozAdsEnvironment.PROD) + .cacheConfig(cache) + .telemetry(telemetry) + .build() +``` + +#### Methods + +| Method | Return Type | Description | +| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `clearCache()` | `Unit` | Clears the client's HTTP cache. Throws on failure. | +| `recordClick(clickUrl: String)` | `Unit` | Records a click using the provided callback URL (typically from `ad.callbacks.click`). | +| `recordImpression(impressionUrl: String)` | `Unit` | Records an impression using the provided callback URL (typically from `ad.callbacks.impression`). | +| `reportAd(reportUrl: String)` | `Unit` | Reports an ad using the provided callback URL (typically from `ad.callbacks.report`). | +| `requestImageAds(mozAdRequests: List, options: MozAdsRequestOptions?)` | `Map` | Requests one image ad per placement. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a map keyed by `placementId`. | +| `requestSpocAds(mozAdRequests: List, options: MozAdsRequestOptions?)` | `Map>` | Requests spoc ads per placement. Each placement request specifies its own count. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a map keyed by `placementId`. | +| `requestTileAds(mozAdRequests: List, options: MozAdsRequestOptions?)` | `Map` | Requests one tile ad per placement. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a map keyed by `placementId`. | + +> **Notes** +> +> - We recommend that this client be initialized as a singleton or something similar so that multiple instances of the client do not exist at once. +> - Responses omit placements with no fill. Empty placements do not appear in the returned maps. +> - The HTTP cache is internally managed. Configuration can be set with `MozAdsClientBuilder`. Per-request cache settings can be set with `MozAdsRequestOptions`. +> - If `cacheConfig` is `null`, caching is disabled entirely. + +--- + +## `MozAdsClientBuilder` + +Builder for configuring and creating the ads client. Use the fluent builder pattern to set configuration options. + +```kotlin +class MozAdsClientBuilder { + fun environment(environment: MozAdsEnvironment): MozAdsClientBuilder + fun cacheConfig(cacheConfig: MozAdsCacheConfig): MozAdsClientBuilder + fun telemetry(telemetry: MozAdsTelemetry): MozAdsClientBuilder + fun build(): MozAdsClient +} +``` + +#### Methods + +- **`MozAdsClientBuilder()`** - Creates a new builder with default values +- **`environment(environment: MozAdsEnvironment)`** - Sets the MARS environment (Prod, Staging, or Test) +- **`cacheConfig(cacheConfig: MozAdsCacheConfig)`** - Sets the cache configuration +- **`telemetry(telemetry: MozAdsTelemetry)`** - Sets the telemetry implementation +- **`build()`** - Builds and returns the configured client + +| Configuration | Type | Description | +| -------------- | --------------------- | ------------------------------------------------------------------------------------------------------ | +| `environment` | `MozAdsEnvironment` | Selects which MARS environment to connect to. Unless in a dev build, this value can only ever be Prod. Defaults to Prod. | +| `cacheConfig` | `MozAdsCacheConfig?` | Optional configuration for the internal cache. | +| `telemetry` | `MozAdsTelemetry?` | Optional telemetry instance for recording metrics. If not provided, a no-op implementation is used. | + +--- + +## `MozAdsTelemetry` + +Telemetry interface for recording ads client metrics. You must provide an implementation of this interface to the `MozAdsClientBuilder` to enable telemetry collection. If no telemetry instance is provided, a no-op implementation is used and no metrics will be recorded. + +```kotlin +interface MozAdsTelemetry { + fun recordBuildCacheError(label: String, value: String) + fun recordClientError(label: String, value: String) + fun recordClientOperationTotal(label: String) + fun recordDeserializationError(label: String, value: String) + fun recordHttpCacheOutcome(label: String, value: String) +} +``` + +#### Implementation Example + +```kotlin +import mozilla.appservices.adsclient.MozAdsTelemetry +import org.mozilla.appservices.ads_client.GleanMetrics.AdsClient + +class AdsClientTelemetry : MozAdsTelemetry { + override fun recordBuildCacheError(label: String, value: String) { + AdsClient.buildCacheError[label].set(value) + } + + override fun recordClientError(label: String, value: String) { + AdsClient.clientError[label].set(value) + } + + override fun recordClientOperationTotal(label: String) { + AdsClient.clientOperationTotal[label].add() + } + + override fun recordDeserializationError(label: String, value: String) { + AdsClient.deserializationError[label].set(value) + } + + override fun recordHttpCacheOutcome(label: String, value: String) { + AdsClient.httpCacheOutcome[label].set(value) + } +} +``` + +--- + +## `MozAdsCacheConfig` + +Describes the behavior and location of the on-disk HTTP cache. + +```kotlin +data class MozAdsCacheConfig( + val dbPath: String, + val defaultCacheTtlSeconds: Long?, + val maxSizeMib: Long? +) +``` + +| Field | Type | Description | +| --------------------------- | ------- | ------------------------------------------------------------------------------------ | +| `dbPath` | `String`| Path to the SQLite database file used for cache storage. Required to enable caching. | +| `defaultCacheTtlSeconds` | `Long?` | Default TTL for cached entries. If omitted, defaults to 300 seconds (5 minutes). | +| `maxSizeMib` | `Long?` | Maximum cache size. If omitted, defaults to 10 MiB. | + +**Defaults** + +- defaultCacheTtlSeconds: 300 seconds (5 min) +- maxSizeMib: 10 MiB + +#### Configuration Example + +```kotlin +val cache = MozAdsCacheConfig( + dbPath = "/tmp/ads_cache.sqlite", + defaultCacheTtlSeconds = 600L, // 10 min + maxSizeMib = 20L // 20 MiB +) + +val telemetry = AdsClientTelemetry() + +val client = MozAdsClientBuilder() + .environment(MozAdsEnvironment.PROD) + .cacheConfig(cache) + .telemetry(telemetry) + .build() +``` + +--- + +## `MozAdsPlacementRequest` + +Describes a single ad placement to request from MARS. A list of these is required for the `requestImageAds` and `requestTileAds` methods on the client. + +```kotlin +data class MozAdsPlacementRequest( + val placementId: String, + val iabContent: MozAdsIABContent? +) +``` + +| Field | Type | Description | +| -------------- | ------------------- | ------------------------------------------------------------------------------- | +| `placementId` | `String` | Unique identifier for the ad placement. Must be unique within one request call. | +| `iabContent` | `MozAdsIABContent?` | Optional IAB content classification for targeting. | + +**Validation Rules:** + +- `placementId` values must be unique per request. + +--- + +## `MozAdsPlacementRequestWithCount` + +Describes a single ad placement to request from MARS with a count parameter. A list of these is required for the `requestSpocAds` method on the client. + +```kotlin +data class MozAdsPlacementRequestWithCount( + val count: Int, + val placementId: String, + val iabContent: MozAdsIABContent? +) +``` + +| Field | Type | Description | +| -------------- | ------------------- | ------------------------------------------------------------------------------- | +| `count` | `Int` | Number of spoc ads to request for this placement. | +| `placementId` | `String` | Unique identifier for the ad placement. Must be unique within one request call. | +| `iabContent` | `MozAdsIABContent?` | Optional IAB content classification for targeting. | + +**Validation Rules:** + +- `placementId` values must be unique per request. + +--- + +## `MozAdsRequestOptions` + +Options passed when making a single ad request. + +```kotlin +data class MozAdsRequestOptions( + val cachePolicy: MozAdsRequestCachePolicy? +) +``` + +| Field | Type | Description | +| -------------- | ---------------------------- | ---------------------------------------------------------------------------------------------- | +| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `null`, uses the client's default TTL with a `CacheFirst` mode. | + +--- + +## `MozAdsRequestCachePolicy` + +Defines how each request interacts with the cache. + +```kotlin +data class MozAdsRequestCachePolicy( + val mode: MozAdsCacheMode, + val ttlSeconds: Long? +) +``` + +| Field | Type | Description | +| ------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `mode` | `MozAdsCacheMode` | Strategy for combining cache and network. Can be `CACHE_FIRST` or `NETWORK_FIRST`. | +| `ttlSeconds` | `Long?` | Optional per-request TTL override in seconds. `null` uses the client default. `0` disables caching for this request. | + +#### Per-Request Cache Policy Override Example + +```kotlin +// Always fetch from network but only cache for 60 seconds +val options = MozAdsRequestOptions( + cachePolicy = MozAdsRequestCachePolicy(mode = MozAdsCacheMode.NETWORK_FIRST, ttlSeconds = 60L) +) + +// Use it when requesting ads +val placements = client.requestImageAds(configs, options = options) +``` + +--- + +## `MozAdsCacheMode` + +Determines how the cache is used during a request. + +```kotlin +enum class MozAdsCacheMode { + CACHE_FIRST, + NETWORK_FIRST +} +``` + +| Variant | Behavior | +| --------------- | -------------------------------------------------------------------------------------------------- | +| `CACHE_FIRST` | Check cache first, return cached response if found, otherwise make a network request and store it. | +| `NETWORK_FIRST` | Always fetch from network, then cache the result. | + +--- + +## `MozAdsImage` + +The image ad creative, callbacks, and metadata provided for each image ad returned from MARS. + +```kotlin +data class MozAdsImage( + val altText: String?, + val blockKey: String, + val callbacks: MozAdsCallbacks, + val format: String, + val imageUrl: String, + val url: String +) +``` + +| Field | Type | Description | +| ----------- | ----------------- | ------------------------------------------- | +| `url` | `String` | Destination URL. | +| `imageUrl` | `String` | Creative asset URL. | +| `format` | `String` | Ad format e.g., `"skyscraper"`. | +| `blockKey` | `String` | The block key generated for the advertiser. | +| `altText` | `String?` | Alt text if available. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsSpoc` + +The spoc ad creative, callbacks, and metadata provided for each spoc ad returned from MARS. + +```kotlin +data class MozAdsSpoc( + val blockKey: String, + val callbacks: MozAdsCallbacks, + val caps: MozAdsSpocFrequencyCaps, + val domain: String, + val excerpt: String, + val format: String, + val imageUrl: String, + val ranking: MozAdsSpocRanking, + val sponsor: String, + val sponsoredByOverride: String?, + val title: String, + val url: String +) +``` + +| Field | Type | Description | +| ----------------------- | ------------------------- | ------------------------------------------- | +| `url` | `String` | Destination URL. | +| `imageUrl` | `String` | Creative asset URL. | +| `format` | `String` | Ad format e.g., `"spoc"`. | +| `blockKey` | `String` | The block key generated for the advertiser. | +| `title` | `String` | Spoc ad title. | +| `excerpt` | `String` | Spoc ad excerpt/description. | +| `domain` | `String` | Domain of the spoc ad. | +| `sponsor` | `String` | Sponsor name. | +| `sponsoredByOverride` | `String?` | Optional override for sponsor name. | +| `caps` | `MozAdsSpocFrequencyCaps` | Frequency capping information. | +| `ranking` | `MozAdsSpocRanking` | Ranking and personalization information. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsTile` + +The tile ad creative, callbacks, and metadata provided for each tile ad returned from MARS. + +```kotlin +data class MozAdsTile( + val blockKey: String, + val callbacks: MozAdsCallbacks, + val format: String, + val imageUrl: String, + val name: String, + val url: String +) +``` + +| Field | Type | Description | +| ----------- | ----------------- | ------------------------------------------- | +| `url` | `String` | Destination URL. | +| `imageUrl` | `String` | Creative asset URL. | +| `format` | `String` | Ad format e.g., `"tile"`. | +| `blockKey` | `String` | The block key generated for the advertiser. | +| `name` | `String` | Tile ad name. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsSpocFrequencyCaps` + +Frequency capping information for spoc ads. + +```kotlin +data class MozAdsSpocFrequencyCaps( + val capKey: String, + val day: Int +) +``` + +| Field | Type | Description | +| -------- | -------- | --------------------------------- | +| `capKey` | `String` | Frequency cap key identifier. | +| `day` | `Int` | Day number for the frequency cap. | + +--- + +## `MozAdsSpocRanking` + +Ranking and personalization information for spoc ads. + +```kotlin +data class MozAdsSpocRanking( + val priority: Int, + val personalizationModels: Map, + val itemScore: Double +) +``` + +| Field | Type | Description | +| ------------------------ | ------------------- | ----------------------------- | +| `priority` | `Int` | Priority score for ranking. | +| `personalizationModels` | `Map` | Personalization model scores. | +| `itemScore` | `Double` | Overall item score. | + +--- + +## `MozAdsCallbacks` + +```kotlin +data class MozAdsCallbacks( + val click: String, + val impression: String, + val report: String? +) +``` + +| Field | Type | Description | +| ------------ | --------- | ------------------------ | +| `click` | `String` | Click callback URL. | +| `impression` | `String` | Impression callback URL. | +| `report` | `String?` | Report callback URL. | + +--- + +## `MozAdsIABContent` + +Provides IAB content classification context for a placement. + +```kotlin +data class MozAdsIABContent( + val taxonomy: MozAdsIABContentTaxonomy, + val categoryIds: List +) +``` + +| Field | Type | Description | +| -------------- | -------------------------- | ------------------------------------- | +| `taxonomy` | `MozAdsIABContentTaxonomy` | IAB taxonomy version. | +| `categoryIds` | `List` | One or more IAB category identifiers. | + +--- + +## `MozAdsIABContentTaxonomy` + +The [IAB Content Taxonomy](https://www.iab.com/guidelines/content-taxonomy/) version to be used in the request. e.g `IAB-1.0` + +```kotlin +enum class MozAdsIABContentTaxonomy { + IAB1_0, + IAB2_0, + IAB2_1, + IAB2_2, + IAB3_0 +} +``` + +> Note: The generated UniFFI bindings use screaming snake-case for enum variants in Kotlin. + +--- + +## Internal Cache Behavior + +### Cache Overview + +The internal HTTP cache is a SQLite-backed key-value store layered over the HTTP request layer. +It reduces redundant network traffic and improves latency for repeated or identical ad requests. + +### Cache Lifecycle + +Each network response can be stored in the cache with an associated effective TTL, computed as: + +``` +effective_ttl = min(server_max_age, client_default_ttl, per_request_ttl) +``` + +where: + +- `server_max_age` comes from the HTTP `Cache-Control: max-age=N` header (if present), +- `client_default_ttl` is set in `MozAdsCacheConfig`, +- `per_request_ttl` is an optional override set in `MozAdsRequestCachePolicy`. + +If the effective TTL resolves to 0 seconds, the response is not cached. + +### Configuring The Cache + +```kotlin +val cache = MozAdsCacheConfig( + dbPath = "/tmp/ads_cache.sqlite", + defaultCacheTtlSeconds = 600L, // 10 min + maxSizeMib = 20L // 20 MiB +) + +val telemetry = AdsClientTelemetry() + +val client = MozAdsClientBuilder() + .environment(MozAdsEnvironment.PROD) + .cacheConfig(cache) + .telemetry(telemetry) + .build() +``` + +Where `dbPath` represents the location of the SQLite file. This must be a file that the client has permission to write to. + +### Cache Invalidation + +**TTL-based expiry (automatic):** + +At the start of each send, the cache computes a cutoff from the current time minus the TTL and deletes rows older than that. This is a coarse, global freshness window that bounds how long entries can live. + +**Size-based trimming (automatic):** +After storing a cacheable miss, the cache enforces `maxSizeMib` by deleting the oldest rows until the total stored size is at or below the maximum allowed size of the cache. Due to the small size of items in the cache and the relatively short TTL, this behavior should be rare. + +**Manual clearing (explicit):** +The cache can be manually cleared by the client using the exposed `client.clearCache()` method. This clears _all_ objects in the cache. diff --git a/components/ads-client/docs/usage-swift.md b/components/ads-client/docs/usage-swift.md new file mode 100644 index 0000000000..840673c5b1 --- /dev/null +++ b/components/ads-client/docs/usage-swift.md @@ -0,0 +1,515 @@ +# Mozilla Ads Client (MAC) — Swift API Reference + +## Overview + +This document covers the full API reference for the `ads_client` component, with all examples and type definitions in Swift. +It includes every type and function exposed via UniFFI that is part of the public API surface. + +--- + +## `MozAdsClient` + +Top-level client object for requesting ads and recording lifecycle events. + +```swift +class MozAdsClient { + // No public properties — use MozAdsClientBuilder to create instances +} +``` + +#### Creating a Client + +Use the `MozAdsClientBuilder` to configure and create the client. The builder provides a fluent API for setting configuration options. + +```swift +let client = MozAdsClientBuilder() + .environment(environment: .prod) + .cacheConfig(cacheConfig: cache) + .telemetry(telemetry: telemetry) + .build() +``` + +#### Methods + +| Method | Return Type | Description | +| ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `clearCache()` | `Void` | Clears the client's HTTP cache. Throws on failure. | +| `recordClick(clickUrl: String)` | `Void` | Records a click using the provided callback URL (typically from `ad.callbacks.click`). | +| `recordImpression(impressionUrl: String)` | `Void` | Records an impression using the provided callback URL (typically from `ad.callbacks.impression`). | +| `reportAd(reportUrl: String)` | `Void` | Reports an ad using the provided callback URL (typically from `ad.callbacks.report`). | +| `requestImageAds(mozAdRequests: [MozAdsPlacementRequest], options: MozAdsRequestOptions?)` | `[String: MozAdsImage]` | Requests one image ad per placement. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a dictionary keyed by `placementId`. | +| `requestSpocAds(mozAdRequests: [MozAdsPlacementRequestWithCount], options: MozAdsRequestOptions?)` | `[String: [MozAdsSpoc]]` | Requests spoc ads per placement. Each placement request specifies its own count. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a dictionary keyed by `placementId`. | +| `requestTileAds(mozAdRequests: [MozAdsPlacementRequest], options: MozAdsRequestOptions?)` | `[String: MozAdsTile]` | Requests one tile ad per placement. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a dictionary keyed by `placementId`. | + +> **Notes** +> +> - We recommend that this client be initialized as a singleton or something similar so that multiple instances of the client do not exist at once. +> - Responses omit placements with no fill. Empty placements do not appear in the returned dictionaries. +> - The HTTP cache is internally managed. Configuration can be set with `MozAdsClientBuilder`. Per-request cache settings can be set with `MozAdsRequestOptions`. +> - If `cacheConfig` is `nil`, caching is disabled entirely. + +--- + +## `MozAdsClientBuilder` + +Builder for configuring and creating the ads client. Use the fluent builder pattern to set configuration options. + +```swift +class MozAdsClientBuilder { + func environment(environment: MozAdsEnvironment) -> MozAdsClientBuilder + func cacheConfig(cacheConfig: MozAdsCacheConfig) -> MozAdsClientBuilder + func telemetry(telemetry: MozAdsTelemetry) -> MozAdsClientBuilder + func build() -> MozAdsClient +} +``` + +#### Methods + +- **`MozAdsClientBuilder()`** - Creates a new builder with default values +- **`environment(environment: MozAdsEnvironment)`** - Sets the MARS environment (Prod, Staging, or Test) +- **`cacheConfig(cacheConfig: MozAdsCacheConfig)`** - Sets the cache configuration +- **`telemetry(telemetry: MozAdsTelemetry)`** - Sets the telemetry implementation +- **`build()`** - Builds and returns the configured client + +| Configuration | Type | Description | +| -------------- | --------------------- | ------------------------------------------------------------------------------------------------------ | +| `environment` | `MozAdsEnvironment` | Selects which MARS environment to connect to. Unless in a dev build, this value can only ever be Prod. Defaults to Prod. | +| `cacheConfig` | `MozAdsCacheConfig?` | Optional configuration for the internal cache. | +| `telemetry` | `MozAdsTelemetry?` | Optional telemetry instance for recording metrics. If not provided, a no-op implementation is used. | + +--- + +## `MozAdsTelemetry` + +Telemetry protocol for recording ads client metrics. You must provide an implementation of this protocol to the `MozAdsClientBuilder` to enable telemetry collection. If no telemetry instance is provided, a no-op implementation is used and no metrics will be recorded. + +```swift +protocol MozAdsTelemetry { + func recordBuildCacheError(label: String, value: String) + func recordClientError(label: String, value: String) + func recordClientOperationTotal(label: String) + func recordDeserializationError(label: String, value: String) + func recordHttpCacheOutcome(label: String, value: String) +} +``` + +#### Implementation Example + +```swift +import MozillaRustComponents +import Glean + +public final class AdsClientTelemetry: MozAdsTelemetry { + public func recordBuildCacheError(label: String, value: String) { + AdsClientMetrics.buildCacheError[label].set(value) + } + + public func recordClientError(label: String, value: String) { + AdsClientMetrics.clientError[label].set(value) + } + + public func recordClientOperationTotal(label: String) { + AdsClientMetrics.clientOperationTotal[label].add() + } + + public func recordDeserializationError(label: String, value: String) { + AdsClientMetrics.deserializationError[label].set(value) + } + + public func recordHttpCacheOutcome(label: String, value: String) { + AdsClientMetrics.httpCacheOutcome[label].set(value) + } +} +``` + +--- + +## `MozAdsCacheConfig` + +Describes the behavior and location of the on-disk HTTP cache. + +```swift +struct MozAdsCacheConfig { + let dbPath: String + let defaultCacheTtlSeconds: UInt64? + let maxSizeMib: UInt64? +} +``` + +| Field | Type | Description | +| --------------------------- | --------- | ------------------------------------------------------------------------------------ | +| `dbPath` | `String` | Path to the SQLite database file used for cache storage. Required to enable caching. | +| `defaultCacheTtlSeconds` | `UInt64?` | Default TTL for cached entries. If omitted, defaults to 300 seconds (5 minutes). | +| `maxSizeMib` | `UInt64?` | Maximum cache size. If omitted, defaults to 10 MiB. | + +**Defaults** + +- defaultCacheTtlSeconds: 300 seconds (5 min) +- maxSizeMib: 10 MiB + +#### Configuration Example + +```swift +let cache = MozAdsCacheConfig( + dbPath: "/tmp/ads_cache.sqlite", + defaultCacheTtlSeconds: 600, // 10 min + maxSizeMib: 20 // 20 MiB +) + +let telemetry = AdsClientTelemetry() + +let client = MozAdsClientBuilder() + .environment(environment: .prod) + .cacheConfig(cacheConfig: cache) + .telemetry(telemetry: telemetry) + .build() +``` + +--- + +## `MozAdsPlacementRequest` + +Describes a single ad placement to request from MARS. An array of these is required for the `requestImageAds` and `requestTileAds` methods on the client. + +```swift +struct MozAdsPlacementRequest { + let placementId: String + let iabContent: MozAdsIABContent? +} +``` + +| Field | Type | Description | +| -------------- | ------------------- | ------------------------------------------------------------------------------- | +| `placementId` | `String` | Unique identifier for the ad placement. Must be unique within one request call. | +| `iabContent` | `MozAdsIABContent?` | Optional IAB content classification for targeting. | + +**Validation Rules:** + +- `placementId` values must be unique per request. + +--- + +## `MozAdsPlacementRequestWithCount` + +Describes a single ad placement to request from MARS with a count parameter. An array of these is required for the `requestSpocAds` method on the client. + +```swift +struct MozAdsPlacementRequestWithCount { + let count: UInt32 + let placementId: String + let iabContent: MozAdsIABContent? +} +``` + +| Field | Type | Description | +| -------------- | ------------------- | ------------------------------------------------------------------------------- | +| `count` | `UInt32` | Number of spoc ads to request for this placement. | +| `placementId` | `String` | Unique identifier for the ad placement. Must be unique within one request call. | +| `iabContent` | `MozAdsIABContent?` | Optional IAB content classification for targeting. | + +**Validation Rules:** + +- `placementId` values must be unique per request. + +--- + +## `MozAdsRequestOptions` + +Options passed when making a single ad request. + +```swift +struct MozAdsRequestOptions { + let cachePolicy: MozAdsRequestCachePolicy? +} +``` + +| Field | Type | Description | +| -------------- | ----------------------------- | --------------------------------------------------------------------------------------------- | +| `cachePolicy` | `MozAdsRequestCachePolicy?` | Per-request caching policy. If `nil`, uses the client's default TTL with a `cacheFirst` mode. | + +--- + +## `MozAdsRequestCachePolicy` + +Defines how each request interacts with the cache. + +```swift +struct MozAdsRequestCachePolicy { + let mode: MozAdsCacheMode + let ttlSeconds: UInt64? +} +``` + +| Field | Type | Description | +| ------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `mode` | `MozAdsCacheMode` | Strategy for combining cache and network. Can be `.cacheFirst` or `.networkFirst`. | +| `ttlSeconds` | `UInt64?` | Optional per-request TTL override in seconds. `nil` uses the client default. `0` disables caching for this request. | + +#### Per-Request Cache Policy Override Example + +```swift +// Always fetch from network but only cache for 60 seconds +let options = MozAdsRequestOptions( + cachePolicy: MozAdsRequestCachePolicy(mode: .networkFirst, ttlSeconds: 60) +) + +// Use it when requesting ads +let placements = client.requestImageAds(configs, options: options) +``` + +--- + +## `MozAdsCacheMode` + +Determines how the cache is used during a request. + +```swift +enum MozAdsCacheMode { + case cacheFirst + case networkFirst +} +``` + +| Variant | Behavior | +| -------------- | -------------------------------------------------------------------------------------------------- | +| `.cacheFirst` | Check cache first, return cached response if found, otherwise make a network request and store it. | +| `.networkFirst`| Always fetch from network, then cache the result. | + +--- + +## `MozAdsImage` + +The image ad creative, callbacks, and metadata provided for each image ad returned from MARS. + +```swift +struct MozAdsImage { + let altText: String? + let blockKey: String + let callbacks: MozAdsCallbacks + let format: String + let imageUrl: String + let url: String +} +``` + +| Field | Type | Description | +| ----------- | ----------------- | ------------------------------------------- | +| `url` | `String` | Destination URL. | +| `imageUrl` | `String` | Creative asset URL. | +| `format` | `String` | Ad format e.g., `"skyscraper"`. | +| `blockKey` | `String` | The block key generated for the advertiser. | +| `altText` | `String?` | Alt text if available. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsSpoc` + +The spoc ad creative, callbacks, and metadata provided for each spoc ad returned from MARS. + +```swift +struct MozAdsSpoc { + let blockKey: String + let callbacks: MozAdsCallbacks + let caps: MozAdsSpocFrequencyCaps + let domain: String + let excerpt: String + let format: String + let imageUrl: String + let ranking: MozAdsSpocRanking + let sponsor: String + let sponsoredByOverride: String? + let title: String + let url: String +} +``` + +| Field | Type | Description | +| ----------------------- | ------------------------- | ------------------------------------------- | +| `url` | `String` | Destination URL. | +| `imageUrl` | `String` | Creative asset URL. | +| `format` | `String` | Ad format e.g., `"spoc"`. | +| `blockKey` | `String` | The block key generated for the advertiser. | +| `title` | `String` | Spoc ad title. | +| `excerpt` | `String` | Spoc ad excerpt/description. | +| `domain` | `String` | Domain of the spoc ad. | +| `sponsor` | `String` | Sponsor name. | +| `sponsoredByOverride` | `String?` | Optional override for sponsor name. | +| `caps` | `MozAdsSpocFrequencyCaps` | Frequency capping information. | +| `ranking` | `MozAdsSpocRanking` | Ranking and personalization information. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsTile` + +The tile ad creative, callbacks, and metadata provided for each tile ad returned from MARS. + +```swift +struct MozAdsTile { + let blockKey: String + let callbacks: MozAdsCallbacks + let format: String + let imageUrl: String + let name: String + let url: String +} +``` + +| Field | Type | Description | +| ----------- | ----------------- | ------------------------------------------- | +| `url` | `String` | Destination URL. | +| `imageUrl` | `String` | Creative asset URL. | +| `format` | `String` | Ad format e.g., `"tile"`. | +| `blockKey` | `String` | The block key generated for the advertiser. | +| `name` | `String` | Tile ad name. | +| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | + +--- + +## `MozAdsSpocFrequencyCaps` + +Frequency capping information for spoc ads. + +```swift +struct MozAdsSpocFrequencyCaps { + let capKey: String + let day: UInt32 +} +``` + +| Field | Type | Description | +| -------- | -------- | --------------------------------- | +| `capKey` | `String` | Frequency cap key identifier. | +| `day` | `UInt32` | Day number for the frequency cap. | + +--- + +## `MozAdsSpocRanking` + +Ranking and personalization information for spoc ads. + +```swift +struct MozAdsSpocRanking { + let priority: UInt32 + let personalizationModels: [String: UInt32] + let itemScore: Double +} +``` + +| Field | Type | Description | +| ------------------------ | ------------------ | ----------------------------- | +| `priority` | `UInt32` | Priority score for ranking. | +| `personalizationModels` | `[String: UInt32]` | Personalization model scores. | +| `itemScore` | `Double` | Overall item score. | + +--- + +## `MozAdsCallbacks` + +```swift +struct MozAdsCallbacks { + let click: String + let impression: String + let report: String? +} +``` + +| Field | Type | Description | +| ------------ | --------- | ------------------------ | +| `click` | `String` | Click callback URL. | +| `impression` | `String` | Impression callback URL. | +| `report` | `String?` | Report callback URL. | + +--- + +## `MozAdsIABContent` + +Provides IAB content classification context for a placement. + +```swift +struct MozAdsIABContent { + let taxonomy: MozAdsIABContentTaxonomy + let categoryIds: [String] +} +``` + +| Field | Type | Description | +| -------------- | -------------------------- | ------------------------------------- | +| `taxonomy` | `MozAdsIABContentTaxonomy` | IAB taxonomy version. | +| `categoryIds` | `[String]` | One or more IAB category identifiers. | + +--- + +## `MozAdsIABContentTaxonomy` + +The [IAB Content Taxonomy](https://www.iab.com/guidelines/content-taxonomy/) version to be used in the request. e.g `IAB-1.0` + +```swift +enum MozAdsIABContentTaxonomy { + case iab1_0 + case iab2_0 + case iab2_1 + case iab2_2 + case iab3_0 +} +``` + +> Note: The generated UniFFI bindings use lower camel-case for enum cases in Swift. + +--- + +## Internal Cache Behavior + +### Cache Overview + +The internal HTTP cache is a SQLite-backed key-value store layered over the HTTP request layer. +It reduces redundant network traffic and improves latency for repeated or identical ad requests. + +### Cache Lifecycle + +Each network response can be stored in the cache with an associated effective TTL, computed as: + +``` +effective_ttl = min(server_max_age, client_default_ttl, per_request_ttl) +``` + +where: + +- `server_max_age` comes from the HTTP `Cache-Control: max-age=N` header (if present), +- `client_default_ttl` is set in `MozAdsCacheConfig`, +- `per_request_ttl` is an optional override set in `MozAdsRequestCachePolicy`. + +If the effective TTL resolves to 0 seconds, the response is not cached. + +### Configuring The Cache + +```swift +let cache = MozAdsCacheConfig( + dbPath: "/tmp/ads_cache.sqlite", + defaultCacheTtlSeconds: 600, // 10 min + maxSizeMib: 20 // 20 MiB +) + +let telemetry = AdsClientTelemetry() + +let client = MozAdsClientBuilder() + .environment(environment: .prod) + .cacheConfig(cacheConfig: cache) + .telemetry(telemetry: telemetry) + .build() +``` + +Where `dbPath` represents the location of the SQLite file. This must be a file that the client has permission to write to. + +### Cache Invalidation + +**TTL-based expiry (automatic):** + +At the start of each send, the cache computes a cutoff from the current time minus the TTL and deletes rows older than that. This is a coarse, global freshness window that bounds how long entries can live. + +**Size-based trimming (automatic):** +After storing a cacheable miss, the cache enforces `maxSizeMib` by deleting the oldest rows until the total stored size is at or below the maximum allowed size of the cache. Due to the small size of items in the cache and the relatively short TTL, this behavior should be rare. + +**Manual clearing (explicit):** +The cache can be manually cleared by the client using the exposed `client.clearCache()` method. This clears _all_ objects in the cache. diff --git a/components/ads-client/docs/usage.md b/components/ads-client/docs/usage.md deleted file mode 100644 index 1fa79faed2..0000000000 --- a/components/ads-client/docs/usage.md +++ /dev/null @@ -1,571 +0,0 @@ -# Mozilla Ads Client (MAC) — UniFFI API Reference - -## Overview - -This document lists the Rust types and functions exposed via UniFFI by the `ads_client` component. -It only includes items that are part of the UniFFI surface. This document is aimed at users of the ads-client who want to know what is available to them. - ---- - -## `MozAdsClient` - -Top-level client object for requesting ads and recording lifecycle events. - -```rust -pub struct MozAdsClient { - ... // No public fields -} -``` - -#### Creating a Client - -Use the `MozAdsClientBuilder` to configure and create the client. The builder provides a fluent API for setting configuration options. - -**Swift:** -```swift -let client = MozAdsClientBuilder() - .environment(environment: .prod) - .cacheConfig(cacheConfig: cache) - .telemetry(telemetry: telemetry) - .build() -``` - -**Kotlin:** -```kotlin -val client = MozAdsClientBuilder() - .environment(MozAdsEnvironment.PROD) - .cacheConfig(cache) - .telemetry(telemetry) - .build() -``` - -**Rust:** -```rust -let client = MozAdsClientBuilder::new() - .environment(MozAdsEnvironment::Prod) - .cache_config(cache_config) - .build(); -``` - -#### Methods - -| Method | Return Type | Description | -| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `clear_cache(&self)` | `AdsClientApiResult<()>` | Clears the client's HTTP cache. Returns an error if clearing fails. | -| `record_click(&self, click_url: String)` | `AdsClientApiResult<()>` | Records a click using the provided callback URL (typically from `ad.callbacks.click`). | -| `record_impression(&self, impression_url: String)` | `AdsClientApiResult<()>` | Records an impression using the provided callback URL (typically from `ad.callbacks.impression`). | -| `report_ad(&self, report_url: String)` | `AdsClientApiResult<()>` | Reports an ad using the provided callback URL (typically from `ad.callbacks.report`). | -| `request_image_ads(&self, moz_ad_requests: Vec, options: Option)` | `AdsClientApiResult>` | Requests one image ad per placement. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a map keyed by `placement_id`. | -| `request_spoc_ads(&self, moz_ad_requests: Vec, options: Option)` | `AdsClientApiResult>>` | Requests spoc ads per placement. Each placement request specifies its own count. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a map keyed by `placement_id`. | -| `request_tile_ads(&self, moz_ad_requests: Vec, options: Option)` | `AdsClientApiResult>` | Requests one tile ad per placement. Optional `MozAdsRequestOptions` can adjust caching behavior. Returns a map keyed by `placement_id`. | - -> **Notes** -> -> - We recommend that this client be initialized as a singleton or something similar so that multiple instances of the client do not exist at once. -> - Responses omit placements with no fill. Empty placements do not appear in the returned maps. -> - The HTTP cache is internally managed. Configuration can be set with `MozAdsClientBuilder`. Per-request cache settings can be set with `MozAdsRequestOptions`. -> - If `cache_config` is `None`, caching is disabled entirely. - ---- - -## `MozAdsClientBuilder` - -Builder for configuring and creating the ads client. Use the fluent builder pattern to set configuration options. - -```rust -pub struct MozAdsClientBuilder -``` - -#### Methods - -- **`new() -> MozAdsClientBuilder`** - Creates a new builder with default values -- **`environment(self, environment: MozAdsEnvironment) -> Self`** - Sets the MARS environment (Prod, Staging, or Test) -- **`cache_config(self, cache_config: MozAdsCacheConfig) -> Self`** - Sets the cache configuration -- **`telemetry(self, telemetry: Arc) -> Self`** - Sets the telemetry implementation -- **`build(self) -> MozAdsClient`** - Builds and returns the configured client - -| Configuration | Type | Description | -| -------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------ | -| `environment` | `MozAdsEnvironment` | Selects which MARS environment to connect to. Unless in a dev build, this value can only ever be Prod. Defaults to Prod. | -| `cache_config` | `Option` | Optional configuration for the internal cache. | -| `telemetry` | `Option>` | Optional telemetry instance for recording metrics. If not provided, a no-op implementation is used. | - ---- - -## `MozAdsTelemetry` - -Telemetry interface for recording ads client metrics. You must provide an implementation of this interface to the `MozAdsClientBuilder` constructor to enable telemetry collection. If no telemetry instance is provided, a no-op implementation is used and no metrics will be recorded. - -```rust -pub trait MozAdsTelemetry: Send + Sync { - fn record_build_cache_error(&self, label: String, value: String); - fn record_client_error(&self, label: String, value: String); - fn record_client_operation_total(&self, label: String); - fn record_deserialization_error(&self, label: String, value: String); - fn record_http_cache_outcome(&self, label: String, value: String); -} -``` - -### Implementing Telemetry - -To enable telemetry collection, you need to implement the `MozAdsTelemetry` interface and provide an instance to the `MozAdsClientBuilder` constructor. The following examples show how to bind Glean metrics to the telemetry interface. - -#### Swift Example - -```swift -import MozillaRustComponents -import Glean - -public final class AdsClientTelemetry: MozAdsTelemetry { - public func recordBuildCacheError(label: String, value: String) { - AdsClientMetrics.buildCacheError[label].set(value) - } - - public func recordClientError(label: String, value: String) { - AdsClientMetrics.clientError[label].set(value) - } - - public func recordClientOperationTotal(label: String) { - AdsClientMetrics.clientOperationTotal[label].add() - } - - public func recordDeserializationError(label: String, value: String) { - AdsClientMetrics.deserializationError[label].set(value) - } - - public func recordHttpCacheOutcome(label: String, value: String) { - AdsClientMetrics.httpCacheOutcome[label].set(value) - } -} -``` - -#### Kotlin Example - -```kotlin -import mozilla.appservices.adsclient.MozAdsTelemetry -import org.mozilla.appservices.ads_client.GleanMetrics.AdsClient - -class AdsClientTelemetry : MozAdsTelemetry { - override fun recordBuildCacheError(label: String, value: String) { - AdsClient.buildCacheError[label].set(value) - } - - override fun recordClientError(label: String, value: String) { - AdsClient.clientError[label].set(value) - } - - override fun recordClientOperationTotal(label: String) { - AdsClient.clientOperationTotal[label].add() - } - - override fun recordDeserializationError(label: String, value: String) { - AdsClient.deserializationError[label].set(value) - } - - override fun recordHttpCacheOutcome(label: String, value: String) { - AdsClient.httpCacheOutcome[label].set(value) - } -} -``` - ---- - -## `MozAdsCacheConfig` - -Describes the behavior and location of the on-disk HTTP cache. - -```rust -pub struct MozAdsCacheConfig { - pub db_path: String, - pub default_cache_ttl_seconds: Option, - pub max_size_mib: Option, -} -``` - -| Field | Type | Description | -| --------------------------- | ------------- | ------------------------------------------------------------------------------------ | -| `db_path` | `String` | Path to the SQLite database file used for cache storage. Required to enable caching. | -| `default_cache_ttl_seconds` | `Option` | Default TTL for cached entries. If omitted, defaults to 300 seconds (5 minutes). | -| `max_size_mib` | `Option` | Maximum cache size. If omitted, defaults to 10 MiB. | - -**Defaults** - -- default_cache_ttl_seconds: 300 seconds (5 min) -- max_size_mib: 10 MiB - ---- - -## `MozAdsPlacementRequest` - -Describes a single ad placement to request from MARS. A vector of these are required for the `request_image_ads` and `request_tile_ads` methods on the client. - -```rust -pub struct MozAdsPlacementRequest { - pub placement_id: String, - pub iab_content: Option, -} -``` - -| Field | Type | Description | -| -------------- | -------------------------- | ------------------------------------------------------------------------------- | -| `placement_id` | `String` | Unique identifier for the ad placement. Must be unique within one request call. | -| `iab_content` | `Option` | Optional IAB content classification for targeting. | - -**Validation Rules:** - -- `placement_id` values must be unique per request. - ---- - -## `MozAdsPlacementRequestWithCount` - -Describes a single ad placement to request from MARS with a count parameter. A vector of these are required for the `request_spoc_ads` method on the client. - -```rust -pub struct MozAdsPlacementRequestWithCount { - pub count: u32, - pub placement_id: String, - pub iab_content: Option, -} -``` - -| Field | Type | Description | -| -------------- | -------------------------- | ------------------------------------------------------------------------------- | -| `count` | `u32` | Number of spoc ads to request for this placement. | -| `placement_id` | `String` | Unique identifier for the ad placement. Must be unique within one request call. | -| `iab_content` | `Option` | Optional IAB content classification for targeting. | - -**Validation Rules:** - -- `placement_id` values must be unique per request. - ---- - -## `MozAdsRequestOptions` - -Options passed when making a single ad request. - -```rust -pub struct MozAdsRequestOptions { - pub cache_policy: Option, -} -``` - -| Field | Type | Description | -| -------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------- | -| `cache_policy` | `Option` | Per-request caching policy. If `None`, uses the client's default TTL with a `CacheFirst` mode. | - ---- - -## `MozAdsRequestCachePolicy` - -Defines how each request interacts with the cache. - -```rust -pub struct MozAdsRequestCachePolicy { - pub mode: MozAdsCacheMode, - pub ttl_seconds: Option, -} -``` - -| Field | Type | Description | -| ------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `mode` | `MozAdsCacheMode` | Strategy for combining cache and network. Can be `CacheFirst` or `NetworkFirst`. | -| `ttl_seconds` | `Option` | Optional per-request TTL override in seconds. `None` uses the client default. `Some(0)` disables caching for this request. | - ---- - -## `MozAdsCacheMode` - -Determines how the cache is used during a request. - -```rust -pub enum MozAdsCacheMode { - CacheFirst, - NetworkFirst, -} -``` - -| Variant | Behavior | -| -------------- | -------------------------------------------------------------------------------------------------- | -| `CacheFirst` | Check cache first, return cached response if found, otherwise make a network request and store it. | -| `NetworkFirst` | Always fetch from network, then cache the result. | - ---- - -## `MozAdsImage` - -The image ad creative, callbacks, and metadata provided for each image ad returned from MARS. - -```rust -pub struct MozAdsImage { - pub alt_text: Option, - pub block_key: String, - pub callbacks: MozAdsCallbacks, - pub format: String, - pub image_url: String, - pub url: String, -} -``` - -| Field | Type | Description | -| ----------- | ----------------- | ------------------------------------------- | -| `url` | `String` | Destination URL. | -| `image_url` | `String` | Creative asset URL. | -| `format` | `String` | Ad format e.g., `"skyscraper"`. | -| `block_key` | `String` | The block key generated for the advertiser. | -| `alt_text` | `Option` | Alt text if available. | -| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | - ---- - -## `MozAdsSpoc` - -The spoc ad creative, callbacks, and metadata provided for each spoc ad returned from MARS. - -```rust -pub struct MozAdsSpoc { - pub block_key: String, - pub callbacks: MozAdsCallbacks, - pub caps: MozAdsSpocFrequencyCaps, - pub domain: String, - pub excerpt: String, - pub format: String, - pub image_url: String, - pub ranking: MozAdsSpocRanking, - pub sponsor: String, - pub sponsored_by_override: Option, - pub title: String, - pub url: String, -} -``` - -| Field | Type | Description | -| ----------------------- | ------------------------- | ------------------------------------------- | -| `url` | `String` | Destination URL. | -| `image_url` | `String` | Creative asset URL. | -| `format` | `String` | Ad format e.g., `"spoc"`. | -| `block_key` | `String` | The block key generated for the advertiser. | -| `title` | `String` | Spoc ad title. | -| `excerpt` | `String` | Spoc ad excerpt/description. | -| `domain` | `String` | Domain of the spoc ad. | -| `sponsor` | `String` | Sponsor name. | -| `sponsored_by_override` | `Option` | Optional override for sponsor name. | -| `caps` | `MozAdsSpocFrequencyCaps` | Frequency capping information. | -| `ranking` | `MozAdsSpocRanking` | Ranking and personalization information. | -| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | - ---- - -## `MozAdsTile` - -The tile ad creative, callbacks, and metadata provided for each tile ad returned from MARS. - -```rust -pub struct MozAdsTile { - pub block_key: String, - pub callbacks: MozAdsCallbacks, - pub format: String, - pub image_url: String, - pub name: String, - pub url: String, -} -``` - -| Field | Type | Description | -| ----------- | ----------------- | ------------------------------------------- | -| `url` | `String` | Destination URL. | -| `image_url` | `String` | Creative asset URL. | -| `format` | `String` | Ad format e.g., `"tile"`. | -| `block_key` | `String` | The block key generated for the advertiser. | -| `name` | `String` | Tile ad name. | -| `callbacks` | `MozAdsCallbacks` | Lifecycle callback endpoints. | - ---- - -## `MozAdsSpocFrequencyCaps` - -Frequency capping information for spoc ads. - -```rust -pub struct MozAdsSpocFrequencyCaps { - pub cap_key: String, - pub day: u32, -} -``` - -| Field | Type | Description | -| --------- | -------- | --------------------------------- | -| `cap_key` | `String` | Frequency cap key identifier. | -| `day` | `u32` | Day number for the frequency cap. | - ---- - -## `MozAdsSpocRanking` - -Ranking and personalization information for spoc ads. - -```rust -pub struct MozAdsSpocRanking { - pub priority: u32, - pub personalization_models: HashMap, - pub item_score: f64, -} -``` - -| Field | Type | Description | -| ------------------------ | ---------------------- | ----------------------------- | -| `priority` | `u32` | Priority score for ranking. | -| `personalization_models` | `HashMap` | Personalization model scores. | -| `item_score` | `f64` | Overall item score. | - ---- - -## `MozAdsCallbacks` - -```rust -pub struct MozAdsCallbacks { - pub click: Url, - pub impression: Url, - pub report: Option, -} -``` - -| Field | Type | Description | -| ------------ | ------------- | ------------------------ | -| `click` | `Url` | Click callback URL. | -| `impression` | `Url` | Impression callback URL. | -| `report` | `Option` | Report callback URL. | - ---- - -## `MozAdsIABContent` - -Provides IAB content classification context for a placement. - -```rust -pub struct MozAdsIABContent { - pub taxonomy: MozAdsIABContentTaxonomy, - pub category_ids: Vec, -} -``` - -| Field | Type | Description | -| -------------- | -------------------------- | ------------------------------------- | -| `taxonomy` | `MozAdsIABContentTaxonomy` | IAB taxonomy version. | -| `category_ids` | `Vec` | One or more IAB category identifiers. | - ---- - -## `MozAdsIABContentTaxonomy` - -The [IAB Content Taxonomy](https://www.iab.com/guidelines/content-taxonomy/) version to be used in the request. e.g `IAB-1.0` - -```rust -pub enum MozAdsIABContentTaxonomy { - IAB1_0, - IAB2_0, - IAB2_1, - IAB2_2, - IAB3_0, -} -``` - -> Note: The generated native bindings for the values may look different depending on the language (snake-case, camel case, etc.) as a result of UniFFI's formatting. - ---- - -## Internal Cache Behavior - -### Cache Overview - -The internal HTTP cache is a SQLite-backed key-value store layered over viaduct::Request::send(). -It reduces redundant network traffic and improves latency for repeated or identical ad requests. - -### Cache Lifecycle - -Each network response can be stored in the cache with an associated effective TTL, computed as: - -```rust -effective_ttl = min(server_max_age, client_default_ttl, per_request_ttl) -``` - -where: - -- `server_max_age` comes from the HTTP Cache-Control: max-age=N header (if present), -- `client_default_ttl` is set in `MozAdsCacheConfig`, -- `per_request_ttl` is an optional override set in `MozAdsRequestCachePolicy`. - -If the effective TTL resolves to 0 seconds, the response is not cached. - -### Configuring The Cache - -#### Example Client Configuration - -```swift -// Swift example -let cache = MozAdsCacheConfig( - dbPath: "/tmp/ads_cache.sqlite", - defaultCacheTtlSeconds: 600, // 10 min - maxSizeMib: 20 // 20 MiB -) - -let telemetry = AdsClientTelemetry() - -let client = MozAdsClientBuilder() - .environment(environment: .prod) - .cacheConfig(cacheConfig: cache) - .telemetry(telemetry: telemetry) - .build() -``` - -```kotlin -// Kotlin example -val cache = MozAdsCacheConfig( - dbPath = "/tmp/ads_cache.sqlite", - defaultCacheTtlSeconds = 600L, // 10 min - maxSizeMib = 20L // 20 MiB -) - -val telemetry = AdsClientTelemetry() - -val client = MozAdsClientBuilder() - .environment(MozAdsEnvironment.PROD) - .cacheConfig(cache) - .telemetry(telemetry) - .build() -``` - -Where `db_path` represents the location of the SQLite file. This must be a file that the client has permission to write to. - -#### Example Request Policy Override - -Assuming you have at least initialized the client with a `db_path`, individual requests can override caching behavior. However, recall the minimum TTL is always respected. So this override will only provide a new ttl floor. - -```rust -// Always fetch from network but only cache for 60 seconds -let options = MozAdsRequestOptions( - cachePolicy: MozAdsRequestCachePolicy(mode: .networkFirst, ttlSeconds: 60) -) - -// Use it when requesting ads -let placements = client.requestImageAds(configs, options: options) -``` - -### Cache Invalidation - -**TTL-based expiry (automatic):** - -At the start of each send, the cache computes a cutoff from chrono::Utc::now() - ttl and deletes rows older than that. This is a coarse, global freshness window that bounds how long entries can live. - -**Size-based trimming (automatic):** -After storing a cacheable miss, the cache enforces max_size by deleting the oldest rows until the total stored size is ≤ the maximum allowed size of the cache. Due to the small size of items in the cache and the relatively short TTL, this behavior should be rare. - -**Manual clearing (explicit):** -The cache can be manually cleared by the client using the exposed `client.clear_cache()` method. This clears _all_ objects in the cache. - ---- - -### Example Usage - -Under construction diff --git a/components/ads-client/integration-tests/tests/http_cache.rs b/components/ads-client/integration-tests/tests/http_cache.rs index 32a9f4889f..5b0a2b2024 100644 --- a/components/ads-client/integration-tests/tests/http_cache.rs +++ b/components/ads-client/integration-tests/tests/http_cache.rs @@ -6,9 +6,9 @@ use std::hash::{Hash, Hasher}; use std::time::Duration; -use ads_client::http_cache::{ByteSize, CacheMode, CacheOutcome, HttpCache, RequestCachePolicy}; +use ads_client::http_cache::{ByteSize, CacheOutcome, CachePolicy, HttpCache}; use mockito::mock; -use viaduct::Request; +use viaduct::{Client, ClientSettings, Request}; /// Test-only hashable wrapper around Request. #[derive(Clone)] @@ -49,6 +49,7 @@ fn test_cache_works_using_real_timeouts() { ], }))); + let client = Client::new(ClientSettings::default()); let test_ttl = 2; let _m1 = mock("POST", "/v1/ads") @@ -61,10 +62,10 @@ fn test_cache_works_using_real_timeouts() { // First call: miss -> store let (_, outcomes) = cache .send_with_policy( + &client, req.clone(), - &RequestCachePolicy { - mode: CacheMode::CacheFirst, - ttl_seconds: Some(test_ttl), + &CachePolicy::CacheFirst { + ttl: Some(Duration::from_secs(test_ttl)), }, ) .unwrap(); @@ -72,7 +73,7 @@ fn test_cache_works_using_real_timeouts() { // Second call: hit (no extra HTTP due to expect(1)) let (response, outcomes) = cache - .send_with_policy(req.clone(), &RequestCachePolicy::default()) + .send_with_policy(&client, req.clone(), &CachePolicy::default()) .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::Hit)); assert_eq!(response.status, 200); @@ -87,7 +88,7 @@ fn test_cache_works_using_real_timeouts() { // Third call: Miss due to timeout for the test_ttl duration std::thread::sleep(Duration::from_secs(test_ttl)); let (response, outcomes) = cache - .send_with_policy(req, &RequestCachePolicy::default()) + .send_with_policy(&client, req, &CachePolicy::default()) .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); assert_eq!(response.status, 200); diff --git a/components/ads-client/integration-tests/tests/mars.rs b/components/ads-client/integration-tests/tests/mars.rs index dd94c496b4..734a9b32b1 100644 --- a/components/ads-client/integration-tests/tests/mars.rs +++ b/components/ads-client/integration-tests/tests/mars.rs @@ -29,8 +29,8 @@ fn test_contract_image_prod() { let client = prod_client(); let result = client.request_image_ads( vec![MozAdsPlacementRequest { - placement_id: "mock_billboard_1".to_string(), iab_content: None, + placement_id: "mock_billboard_1".to_string(), }], None, ); @@ -52,9 +52,9 @@ fn test_contract_spoc_prod() { let client = prod_client(); let result = client.request_spoc_ads( vec![MozAdsPlacementRequestWithCount { - placement_id: "mock_spoc_1".to_string(), count: 3, iab_content: None, + placement_id: "mock_spoc_1".to_string(), }], None, ); @@ -73,8 +73,8 @@ fn test_contract_tile_prod() { let client = prod_client(); let result = client.request_tile_ads( vec![MozAdsPlacementRequest { - placement_id: "mock_tile_1".to_string(), iab_content: None, + placement_id: "mock_tile_1".to_string(), }], None, ); diff --git a/components/ads-client/src/client.rs b/components/ads-client/src/client.rs index c9331c4dfb..0756b9c6fd 100644 --- a/components/ads-client/src/client.rs +++ b/components/ads-client/src/client.rs @@ -26,7 +26,7 @@ impl ReportReason { } } } -use crate::http_cache::{HttpCache, RequestCachePolicy}; +use crate::http_cache::{CachePolicy, HttpCache}; use crate::mars::MARSClient; use crate::telemetry::Telemetry; use ad_request::{AdPlacementRequest, AdRequest}; @@ -51,8 +51,8 @@ where client: MARSClient, context_id_component: ContextIDComponent, environment: Environment, - telemetry: T, rotation_days: u8, + telemetry: T, } impl AdsClient @@ -96,11 +96,11 @@ where let client = MARSClient::new(http_cache, telemetry.clone()); let client = Self { - environment, - context_id_component, client, - telemetry: telemetry.clone(), + context_id_component, + environment, rotation_days, + telemetry: telemetry.clone(), }; telemetry.record(&ClientOperationEvent::New); return client; @@ -108,38 +108,74 @@ where let client = MARSClient::new(None, telemetry.clone()); let client = Self { - environment, - context_id_component, client, - telemetry: telemetry.clone(), + context_id_component, + environment, rotation_days, + telemetry: telemetry.clone(), }; telemetry.record(&ClientOperationEvent::New); client } - fn request_ads( - &self, - ad_placement_requests: Vec, - options: Option, - ) -> Result, RequestAdsError> - where - A: AdResponseValue, - { - let context_id = self.get_context_id()?; - let url = self.environment.into_url("ads"); - let ad_request = AdRequest::try_new(context_id, ad_placement_requests, url)?; - let cache_policy = options.unwrap_or_default(); - let (mut response, request_hash) = self.client.fetch_ads::(ad_request, &cache_policy)?; - response.add_request_hash_to_callbacks(&request_hash); - response.add_placement_info_to_report_callbacks(); - Ok(response) + pub fn clear_cache(&self) -> Result<(), HttpCacheError> { + self.client.clear_cache() + } + + pub fn get_context_id(&self) -> context_id::ApiResult { + self.context_id_component.request(self.rotation_days) + } + + pub fn record_click(&self, click_url: Url) -> Result<(), RecordClickError> { + // TODO: Re-enable cache invalidation behind a Nimbus experiment. + // The mobile team has requested this be temporarily disabled. + // let mut click_url = click_url.clone(); + // if let Some(request_hash) = pop_request_hash_from_url(&mut click_url) { + // let _ = self.client.invalidate_cache_by_hash(&request_hash); + // } + self.client + .record_click(click_url) + .inspect_err(|e| { + self.telemetry.record(e); + }) + .inspect(|_| { + self.telemetry.record(&ClientOperationEvent::RecordClick); + }) + } + + pub fn record_impression(&self, impression_url: Url) -> Result<(), RecordImpressionError> { + // TODO: Re-enable cache invalidation behind a Nimbus experiment. + // The mobile team has requested this be temporarily disabled. + // let mut impression_url = impression_url.clone(); + // if let Some(request_hash) = pop_request_hash_from_url(&mut impression_url) { + // let _ = self.client.invalidate_cache_by_hash(&request_hash); + // } + self.client + .record_impression(impression_url) + .inspect_err(|e| { + self.telemetry.record(e); + }) + .inspect(|_| { + self.telemetry + .record(&ClientOperationEvent::RecordImpression); + }) + } + + pub fn report_ad(&self, report_url: Url, reason: ReportReason) -> Result<(), ReportAdError> { + self.client + .report_ad(report_url, reason) + .inspect_err(|e| { + self.telemetry.record(e); + }) + .inspect(|_| { + self.telemetry.record(&ClientOperationEvent::ReportAd); + }) } pub fn request_image_ads( &self, ad_placement_requests: Vec, - options: Option, + options: Option, ) -> Result, RequestAdsError> { let response = self .request_ads::(ad_placement_requests, options) @@ -153,7 +189,7 @@ where pub fn request_spoc_ads( &self, ad_placement_requests: Vec, - options: Option, + options: Option, ) -> Result>, RequestAdsError> { let result = self.request_ads::(ad_placement_requests, options); result @@ -169,7 +205,7 @@ where pub fn request_tile_ads( &self, ad_placement_requests: Vec, - options: Option, + options: Option, ) -> Result, RequestAdsError> { let result = self.request_ads::(ad_placement_requests, options); result @@ -182,58 +218,22 @@ where }) } - pub fn record_impression(&self, impression_url: Url) -> Result<(), RecordImpressionError> { - // TODO: Re-enable cache invalidation behind a Nimbus experiment. - // The mobile team has requested this be temporarily disabled. - // let mut impression_url = impression_url.clone(); - // if let Some(request_hash) = pop_request_hash_from_url(&mut impression_url) { - // let _ = self.client.invalidate_cache_by_hash(&request_hash); - // } - self.client - .record_impression(impression_url) - .inspect_err(|e| { - self.telemetry.record(e); - }) - .inspect(|_| { - self.telemetry - .record(&ClientOperationEvent::RecordImpression); - }) - } - - pub fn record_click(&self, click_url: Url) -> Result<(), RecordClickError> { - // TODO: Re-enable cache invalidation behind a Nimbus experiment. - // The mobile team has requested this be temporarily disabled. - // let mut click_url = click_url.clone(); - // if let Some(request_hash) = pop_request_hash_from_url(&mut click_url) { - // let _ = self.client.invalidate_cache_by_hash(&request_hash); - // } - self.client - .record_click(click_url) - .inspect_err(|e| { - self.telemetry.record(e); - }) - .inspect(|_| { - self.telemetry.record(&ClientOperationEvent::RecordClick); - }) - } - - pub fn report_ad(&self, report_url: Url, reason: ReportReason) -> Result<(), ReportAdError> { - self.client - .report_ad(report_url, reason) - .inspect_err(|e| { - self.telemetry.record(e); - }) - .inspect(|_| { - self.telemetry.record(&ClientOperationEvent::ReportAd); - }) - } - - pub fn get_context_id(&self) -> context_id::ApiResult { - self.context_id_component.request(self.rotation_days) - } - - pub fn clear_cache(&self) -> Result<(), HttpCacheError> { - self.client.clear_cache() + fn request_ads( + &self, + ad_placement_requests: Vec, + options: Option, + ) -> Result, RequestAdsError> + where + A: AdResponseValue, + { + let context_id = self.get_context_id()?; + let url = self.environment.into_url("ads"); + let ad_request = AdRequest::try_new(context_id, ad_placement_requests, url)?; + let cache_policy = options.unwrap_or_default(); + let (mut response, request_hash) = self.client.fetch_ads::(ad_request, cache_policy)?; + response.add_request_hash_to_callbacks(&request_hash); + response.add_placement_info_to_report_callbacks(); + Ok(response) } } @@ -268,21 +268,21 @@ mod tests { Box::new(DefaultContextIdCallback), ); AdsClient { - environment: Environment::Test, - context_id_component, client, - telemetry: MozAdsTelemetryWrapper::noop(), + context_id_component, + environment: Environment::Test, rotation_days: DEFAULT_ROTATION_DAYS, + telemetry: MozAdsTelemetryWrapper::noop(), } } #[test] fn test_get_context_id() { let config = AdsClientConfig { - environment: Environment::Test, cache_config: None, - telemetry: MozAdsTelemetryWrapper::noop(), + environment: Environment::Test, rotation_days: None, + telemetry: MozAdsTelemetryWrapper::noop(), }; let client = AdsClient::new(config); let context_id = client.get_context_id().unwrap(); @@ -394,7 +394,7 @@ mod tests { ads_client .request_ads::( make_happy_placement_requests(), - Some(RequestCachePolicy::default()), + Some(CachePolicy::default()), ) .unwrap(); } diff --git a/components/ads-client/src/client/ad_request.rs b/components/ads-client/src/client/ad_request.rs index 031406d2cd..5e3d181123 100644 --- a/components/ads-client/src/client/ad_request.rs +++ b/components/ads-client/src/client/ad_request.rs @@ -48,9 +48,9 @@ impl AdRequest { }; let mut request = AdRequest { - url, - placements: vec![], context_id, + placements: vec![], + url, }; let mut used_placement_ids: HashSet = HashSet::new(); @@ -63,14 +63,14 @@ impl AdRequest { } request.placements.push(AdPlacementRequest { - placement: ad_placement_request.placement.clone(), - count: ad_placement_request.count, content: ad_placement_request .content .map(|iab_content| AdContentCategory { categories: iab_content.categories, taxonomy: iab_content.taxonomy, }), + count: ad_placement_request.count, + placement: ad_placement_request.placement.clone(), }); used_placement_ids.insert(ad_placement_request.placement.clone()); @@ -82,15 +82,15 @@ impl AdRequest { #[derive(Debug, Hash, PartialEq, Serialize)] pub struct AdPlacementRequest { - pub placement: String, - pub count: u32, pub content: Option, + pub count: u32, + pub placement: String, } #[derive(Debug, Deserialize, Hash, PartialEq, Serialize)] pub struct AdContentCategory { - pub taxonomy: IABContentTaxonomy, pub categories: Vec, + pub taxonomy: IABContentTaxonomy, } #[derive(Debug, Deserialize, Hash, PartialEq, Serialize)] @@ -121,12 +121,12 @@ mod tests { #[test] fn test_ad_placement_request_with_content_serialize() { let request = AdPlacementRequest { - placement: "example_placement".into(), - count: 1, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, categories: vec!["Technology".into(), "Programming".into()], + taxonomy: IABContentTaxonomy::IAB2_1, }), + count: 1, + placement: "example_placement".into(), }; let serialized = to_value(&request).unwrap(); @@ -171,20 +171,20 @@ mod tests { TEST_CONTEXT_ID.to_string(), vec![ AdPlacementRequest { - placement: "example_placement_1".to_string(), - count: 1, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, categories: vec!["entertainment".to_string()], + taxonomy: IABContentTaxonomy::IAB2_1, }), + count: 1, + placement: "example_placement_1".to_string(), }, AdPlacementRequest { - placement: "example_placement_2".to_string(), - count: 2, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, categories: vec![], + taxonomy: IABContentTaxonomy::IAB2_1, }), + count: 2, + placement: "example_placement_2".to_string(), }, ], url.clone(), @@ -192,26 +192,26 @@ mod tests { .unwrap(); let expected_request = AdRequest { - url, context_id: TEST_CONTEXT_ID.to_string(), placements: vec![ AdPlacementRequest { - placement: "example_placement_1".to_string(), - count: 1, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, categories: vec!["entertainment".to_string()], + taxonomy: IABContentTaxonomy::IAB2_1, }), + count: 1, + placement: "example_placement_1".to_string(), }, AdPlacementRequest { - placement: "example_placement_2".to_string(), - count: 2, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, categories: vec![], + taxonomy: IABContentTaxonomy::IAB2_1, }), + count: 2, + placement: "example_placement_2".to_string(), }, ], + url, }; assert_eq!(request, expected_request); @@ -224,20 +224,20 @@ mod tests { TEST_CONTEXT_ID.to_string(), vec![ AdPlacementRequest { - placement: "example_placement_1".to_string(), - count: 1, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, categories: vec!["entertainment".to_string()], + taxonomy: IABContentTaxonomy::IAB2_1, }), + count: 1, + placement: "example_placement_1".to_string(), }, AdPlacementRequest { - placement: "example_placement_1".to_string(), - count: 1, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB3_0, categories: vec![], + taxonomy: IABContentTaxonomy::IAB3_0, }), + count: 1, + placement: "example_placement_1".to_string(), }, ], url, @@ -259,9 +259,9 @@ mod tests { let url: Url = "https://example.com/ads".parse().unwrap(); let make_placements = || { vec![AdPlacementRequest { - placement: "tile_1".to_string(), - count: 1, content: None, + count: 1, + placement: "tile_1".to_string(), }] }; @@ -283,9 +283,9 @@ mod tests { let req1 = AdRequest::try_new( "same-id".to_string(), vec![AdPlacementRequest { - placement: "tile_1".to_string(), - count: 1, content: None, + count: 1, + placement: "tile_1".to_string(), }], url.clone(), ) @@ -294,9 +294,9 @@ mod tests { let req2 = AdRequest::try_new( "same-id".to_string(), vec![AdPlacementRequest { - placement: "tile_2".to_string(), - count: 3, content: None, + count: 3, + placement: "tile_2".to_string(), }], url, ) diff --git a/components/ads-client/src/client/config.rs b/components/ads-client/src/client/config.rs index af5206c979..8582ff864e 100644 --- a/components/ads-client/src/client/config.rs +++ b/components/ads-client/src/client/config.rs @@ -18,10 +18,10 @@ pub struct AdsClientConfig where T: Telemetry, { - pub environment: Environment, pub cache_config: Option, - pub telemetry: T, + pub environment: Environment, pub rotation_days: Option, + pub telemetry: T, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -34,15 +34,6 @@ pub enum Environment { } impl Environment { - fn base_url(self) -> Url { - match self { - Environment::Prod => MARS_API_ENDPOINT_PROD.clone(), - Environment::Staging => MARS_API_ENDPOINT_STAGING.clone(), - #[cfg(test)] - Environment::Test => Url::parse(&mockito::server_url()).unwrap(), - } - } - pub fn into_url(self, path: &str) -> Url { let mut base = self.base_url(); // Ensure the path has a trailing slash so that `join` appends @@ -53,6 +44,15 @@ impl Environment { base.join(path) .expect("joining a path to a valid base URL must succeed") } + + fn base_url(self) -> Url { + match self { + Environment::Prod => MARS_API_ENDPOINT_PROD.clone(), + Environment::Staging => MARS_API_ENDPOINT_STAGING.clone(), + #[cfg(test)] + Environment::Test => Url::parse(&mockito::server_url()).unwrap(), + } + } } #[derive(Clone, Debug)] diff --git a/components/ads-client/src/error.rs b/components/ads-client/src/error.rs index a041286186..87ad4fac10 100644 --- a/components/ads-client/src/error.rs +++ b/components/ads-client/src/error.rs @@ -8,9 +8,6 @@ use viaduct::Response; #[derive(Debug, thiserror::Error)] pub enum ComponentError { - #[error("Error requesting ads: {0}")] - RequestAds(#[from] RequestAdsError), - #[error("Error recording a click for a placement: {0}")] RecordClick(#[from] RecordClickError), @@ -19,6 +16,9 @@ pub enum ComponentError { #[error("Error reporting an ad: {0}")] ReportAd(#[from] ReportAdError), + + #[error("Error requesting ads: {0}")] + RequestAds(#[from] RequestAdsError), } #[derive(Debug, thiserror::Error)] @@ -35,26 +35,26 @@ pub enum RequestAdsError { #[derive(Debug, thiserror::Error)] pub enum BuildRequestError { - #[error("Could not build request with empty placement configs")] - EmptyConfig, - #[error("Duplicate placement_id found: {placement_id}. Placement_ids must be unique.")] DuplicatePlacementId { placement_id: String }, + + #[error("Could not build request with empty placement configs")] + EmptyConfig, } #[derive(Debug, thiserror::Error)] pub enum FetchAdsError { - #[error("URL parse error: {0}")] - UrlParse(#[from] url::ParseError), - - #[error("Error sending request: {0}")] - Request(#[from] viaduct::ViaductError), + #[error("Could not fetch ads, MARS responded with: {0}")] + HTTPError(#[from] HTTPError), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), - #[error("Could not fetch ads, MARS responded with: {0}")] - HTTPError(#[from] HTTPError), + #[error("Error sending request: {0}")] + Request(#[from] viaduct::ViaductError), + + #[error("URL parse error: {0}")] + UrlParse(#[from] url::ParseError), } #[derive(Debug, thiserror::Error)] diff --git a/components/ads-client/src/ffi.rs b/components/ads-client/src/ffi.rs index eb6e2fd8be..33fb45f1d2 100644 --- a/components/ads-client/src/ffi.rs +++ b/components/ads-client/src/ffi.rs @@ -16,7 +16,7 @@ use crate::client::AdsClient; use crate::client::ReportReason; use crate::error::ComponentError; use crate::ffi::telemetry::MozAdsTelemetryWrapper; -use crate::http_cache::{CacheMode, RequestCachePolicy}; +use crate::http_cache::CachePolicy; use crate::MozAdsClient; use error_support::{ErrorHandling, GetErrorHandling}; use parking_lot::Mutex; @@ -50,39 +50,30 @@ impl GetErrorHandling for ComponentError { } } -#[derive(uniffi::Record)] +#[derive(Default, uniffi::Record)] pub struct MozAdsRequestOptions { - pub cache_policy: Option, -} - -impl Default for MozAdsRequestOptions { - fn default() -> Self { - Self { - cache_policy: Some(MozAdsRequestCachePolicy { - mode: MozAdsCacheMode::default(), - ttl_seconds: None, - }), - } - } + pub cache_policy: Option, } #[derive(Clone, Debug, PartialEq, uniffi::Record)] pub struct MozAdsIABContent { - pub taxonomy: MozAdsIABContentTaxonomy, pub category_ids: Vec, + pub taxonomy: MozAdsIABContentTaxonomy, } #[derive(Clone, Debug, PartialEq, uniffi::Record)] pub struct MozAdsPlacementRequest { - pub placement_id: String, + #[uniffi(default = None)] pub iab_content: Option, + pub placement_id: String, } #[derive(Clone, Debug, PartialEq, uniffi::Record)] pub struct MozAdsPlacementRequestWithCount { pub count: u32, - pub placement_id: String, + #[uniffi(default = None)] pub iab_content: Option, + pub placement_id: String, } #[derive(Debug, PartialEq, uniffi::Record)] @@ -97,10 +88,10 @@ pub struct MozAdsClientBuilder(Mutex); #[derive(Default)] struct MozAdsClientBuilderInner { - environment: Option, cache_config: Option, - telemetry: Option>, + environment: Option, rotation_days: Option, + telemetry: Option>, } impl Default for MozAdsClientBuilder { @@ -116,9 +107,22 @@ impl MozAdsClientBuilder { Self::default() } - pub fn environment(self: Arc, environment: MozAdsEnvironment) -> Arc { - self.0.lock().environment = Some(environment); - self + pub fn build(&self) -> MozAdsClient { + let inner = self.0.lock(); + let client_config = AdsClientConfig { + cache_config: inner.cache_config.clone().map(Into::into), + environment: inner.environment.unwrap_or_default().into(), + rotation_days: inner.rotation_days, + telemetry: inner + .telemetry + .clone() + .map(MozAdsTelemetryWrapper::new) + .unwrap_or_else(MozAdsTelemetryWrapper::noop), + }; + let client = AdsClient::new(client_config); + MozAdsClient { + inner: Mutex::new(client), + } } pub fn cache_config(self: Arc, cache_config: MozAdsCacheConfig) -> Arc { @@ -126,8 +130,8 @@ impl MozAdsClientBuilder { self } - pub fn telemetry(self: Arc, telemetry: Arc) -> Arc { - self.0.lock().telemetry = Some(telemetry); + pub fn environment(self: Arc, environment: MozAdsEnvironment) -> Arc { + self.0.lock().environment = Some(environment); self } @@ -136,22 +140,9 @@ impl MozAdsClientBuilder { self } - pub fn build(&self) -> MozAdsClient { - let inner = self.0.lock(); - let client_config = AdsClientConfig { - environment: inner.environment.unwrap_or_default().into(), - cache_config: inner.cache_config.clone().map(Into::into), - telemetry: inner - .telemetry - .clone() - .map(MozAdsTelemetryWrapper::new) - .unwrap_or_else(MozAdsTelemetryWrapper::noop), - rotation_days: inner.rotation_days, - }; - let client = AdsClient::new(client_config); - MozAdsClient { - inner: Mutex::new(client), - } + pub fn telemetry(self: Arc, telemetry: Arc) -> Arc { + self.0.lock().telemetry = Some(telemetry); + self } } @@ -167,14 +158,16 @@ pub enum MozAdsEnvironment { #[derive(Clone, uniffi::Record)] pub struct MozAdsCacheConfig { pub db_path: String, + #[uniffi(default = None)] pub default_cache_ttl_seconds: Option, + #[uniffi(default = None)] pub max_size_mib: Option, } #[derive(Debug, PartialEq, uniffi::Record)] pub struct MozAdsContentCategory { - pub taxonomy: MozAdsIABContentTaxonomy, pub categories: Vec, + pub taxonomy: MozAdsIABContentTaxonomy, } #[derive(Clone, Copy, Debug, uniffi::Enum, PartialEq)] @@ -187,8 +180,9 @@ pub enum MozAdsIABContentTaxonomy { } #[derive(Clone, Copy, Debug, Default, uniffi::Record)] -pub struct MozAdsRequestCachePolicy { +pub struct MozAdsCachePolicy { pub mode: MozAdsCacheMode, + #[uniffi(default = None)] pub ttl_seconds: Option, } @@ -250,9 +244,9 @@ pub struct MozAdsSpocFrequencyCaps { #[derive(Debug, PartialEq, uniffi::Record)] pub struct MozAdsSpocRanking { - pub priority: u32, - pub personalization_models: std::collections::HashMap, pub item_score: f64, + pub personalization_models: std::collections::HashMap, + pub priority: u32, } #[derive(Debug, PartialEq, uniffi::Record)] @@ -297,9 +291,9 @@ impl From for MozAdsSpocFrequencyCaps { impl From for MozAdsSpocRanking { fn from(ranking: SpocRanking) -> Self { Self { - priority: ranking.priority, - personalization_models: ranking.personalization_models.unwrap_or_default(), item_score: ranking.item_score, + personalization_models: ranking.personalization_models.unwrap_or_default(), + priority: ranking.priority, } } } @@ -395,29 +389,12 @@ impl From for IABContentTaxonomy { } } -impl From for RequestCachePolicy { - fn from(policy: MozAdsRequestCachePolicy) -> Self { - Self { - mode: policy.mode.into(), - ttl_seconds: policy.ttl_seconds, - } - } -} - -impl From for MozAdsCacheMode { - fn from(mode: CacheMode) -> Self { - match mode { - CacheMode::CacheFirst => MozAdsCacheMode::CacheFirst, - CacheMode::NetworkFirst => MozAdsCacheMode::NetworkFirst, - } - } -} - -impl From for CacheMode { - fn from(mode: MozAdsCacheMode) -> Self { - match mode { - MozAdsCacheMode::CacheFirst => CacheMode::CacheFirst, - MozAdsCacheMode::NetworkFirst => CacheMode::NetworkFirst, +impl From for CachePolicy { + fn from(policy: MozAdsCachePolicy) -> Self { + let ttl = policy.ttl_seconds.map(std::time::Duration::from_secs); + match policy.mode { + MozAdsCacheMode::CacheFirst => CachePolicy::CacheFirst { ttl }, + MozAdsCacheMode::NetworkFirst => CachePolicy::NetworkFirst { ttl }, } } } @@ -425,19 +402,19 @@ impl From for CacheMode { impl From<&MozAdsIABContent> for AdContentCategory { fn from(content: &MozAdsIABContent) -> Self { Self { - taxonomy: content.taxonomy.into(), categories: content.category_ids.clone(), + taxonomy: content.taxonomy.into(), } } } -impl From for RequestCachePolicy { +impl From for CachePolicy { fn from(options: MozAdsRequestOptions) -> Self { options.cache_policy.map(Into::into).unwrap_or_default() } } -impl From> for RequestCachePolicy { +impl From> for CachePolicy { fn from(options: Option) -> Self { options.map(Into::into).unwrap_or_default() } @@ -456,9 +433,9 @@ impl From for AdsCacheConfig { impl From<&MozAdsPlacementRequest> for AdPlacementRequest { fn from(request: &MozAdsPlacementRequest) -> Self { Self { - placement: request.placement_id.clone(), - count: 1, content: request.iab_content.as_ref().map(Into::into), + count: 1, + placement: request.placement_id.clone(), } } } @@ -466,9 +443,9 @@ impl From<&MozAdsPlacementRequest> for AdPlacementRequest { impl From<&MozAdsPlacementRequestWithCount> for AdPlacementRequest { fn from(request: &MozAdsPlacementRequestWithCount) -> Self { Self { - placement: request.placement_id.clone(), - count: request.count, content: request.iab_content.as_ref().map(Into::into), + count: request.count, + placement: request.placement_id.clone(), } } } diff --git a/components/ads-client/src/http_cache.rs b/components/ads-client/src/http_cache.rs index b3492916e3..6ae09d67b3 100644 --- a/components/ads-client/src/http_cache.rs +++ b/components/ads-client/src/http_cache.rs @@ -13,7 +13,7 @@ mod store; use self::{builder::HttpCacheBuilder, cache_control::CacheControl, store::HttpCacheStore}; use std::hash::Hash; -use viaduct::{Request, Response}; +use viaduct::{Client, Request, Response}; pub use self::builder::HttpCacheBuilderError; pub use self::bytesize::ByteSize; @@ -25,17 +25,16 @@ use std::time::Duration; pub type HttpCacheSendResult = std::result::Result<(Response, Vec), viaduct::ViaductError>; -#[derive(Clone, Copy, Debug, Default)] -pub struct RequestCachePolicy { - pub mode: CacheMode, - pub ttl_seconds: Option, // optional client-defined ttl override +#[derive(Clone, Copy, Debug)] +pub enum CachePolicy { + CacheFirst { ttl: Option }, + NetworkFirst { ttl: Option }, } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum CacheMode { - #[default] - CacheFirst, - NetworkFirst, +impl Default for CachePolicy { + fn default() -> Self { + Self::CacheFirst { ttl: None } + } } #[derive(Debug, thiserror::Error)] @@ -59,9 +58,9 @@ pub enum CacheOutcome { } pub struct HttpCache> { + default_ttl: Duration, max_size: ByteSize, store: HttpCacheStore, - default_ttl: Duration, _phantom: std::marker::PhantomData, } @@ -84,22 +83,23 @@ impl> HttpCache { pub fn send_with_policy( &self, + client: &Client, item: T, - request_policy: &RequestCachePolicy, + cache_policy: &CachePolicy, ) -> HttpCacheSendResult { let mut outcomes = vec![]; let request_hash = RequestHash::new(&item); - let request: Request = item.into(); - let request_policy_ttl = match request_policy.ttl_seconds { - Some(s) => Duration::new(s, 0), - None => self.default_ttl, + let ttl = match cache_policy { + CachePolicy::CacheFirst { ttl: Some(d) } + | CachePolicy::NetworkFirst { ttl: Some(d) } => *d, + _ => self.default_ttl, }; if let Err(e) = self.store.delete_expired_entries() { outcomes.push(CacheOutcome::CleanupFailed(e.into())); } - if request_policy.mode == CacheMode::CacheFirst { + if matches!(cache_policy, CachePolicy::CacheFirst { .. }) { match self.store.lookup(&request_hash) { Ok(Some(response)) => { outcomes.push(CacheOutcome::Hit); @@ -108,7 +108,7 @@ impl> HttpCache { Err(e) => { outcomes.push(CacheOutcome::LookupFailed(e)); let (response, mut fetch_outcomes) = - self.fetch_and_cache(&request, &request_hash, &request_policy_ttl)?; + self.fetch_and_cache(client, item, &ttl)?; outcomes.append(&mut fetch_outcomes); return Ok((response, outcomes)); } @@ -116,37 +116,38 @@ impl> HttpCache { } } - let (response, mut fetch_outcomes) = - self.fetch_and_cache(&request, &request_hash, &request_policy_ttl)?; + let (response, mut fetch_outcomes) = self.fetch_and_cache(client, item, &ttl)?; outcomes.append(&mut fetch_outcomes); Ok((response, outcomes)) } - fn fetch_and_cache( + fn cache_object( &self, - request: &Request, request_hash: &RequestHash, - request_policy_ttl: &Duration, - ) -> HttpCacheSendResult { - let response = request.clone().send()?; + response: &Response, + ttl: &Duration, + ) -> Result<(), HttpCacheError> { + self.store.store_with_ttl(request_hash, response, ttl)?; + self.store.trim_to_max_size(self.max_size.as_u64() as i64)?; + Ok(()) + } + + fn fetch_and_cache(&self, client: &Client, item: T, ttl: &Duration) -> HttpCacheSendResult { + let request_hash = RequestHash::new(&item); + let request: Request = item.into(); + let response = client.send_sync(request)?; let cache_control = CacheControl::from(&response); let cache_outcome = if cache_control.should_cache() { - let response_ttl = match cache_control.max_age { - Some(s) => Duration::new(s, 0), - None => self.default_ttl, + let ttl = match cache_control.max_age { + Some(s) => cmp::min(*ttl, Duration::from_secs(s)), + None => *ttl, }; - // We respect the smallest ttl between the policy, default client value, or header - let final_ttl = cmp::min( - cmp::min(*request_policy_ttl, self.default_ttl), - response_ttl, - ); - - if final_ttl.as_secs() == 0 { + if ttl.is_zero() { return Ok((response, vec![CacheOutcome::NoCache])); } - match self.cache_object(request_hash, &response, &final_ttl) { + match self.cache_object(&request_hash, &response, &ttl) { Ok(()) => CacheOutcome::MissStored, Err(e) => CacheOutcome::StoreFailed(e), } @@ -156,17 +157,6 @@ impl> HttpCache { Ok((response, vec![cache_outcome])) } - - fn cache_object( - &self, - request_hash: &RequestHash, - response: &Response, - ttl: &Duration, - ) -> Result<(), HttpCacheError> { - self.store.store_with_ttl(request_hash, response, ttl)?; - self.store.trim_to_max_size(self.max_size.as_u64() as i64)?; - Ok(()) - } } #[cfg(test)] @@ -175,6 +165,11 @@ mod tests { use std::hash::{Hash, Hasher}; use super::*; + use viaduct::ClientSettings; + + fn make_client() -> Client { + Client::new(ClientSettings::default()) + } /// Test-only hashable wrapper around Request. /// Hashes method + url for cache key purposes. @@ -249,7 +244,7 @@ mod tests { cache .store - .store_with_ttl(&hash, &response, &Duration::new(300, 0)) + .store_with_ttl(&hash, &response, &Duration::from_secs(300)) .unwrap(); // Verify it's cached @@ -278,16 +273,17 @@ mod tests { let cache = make_cache(); let req = make_post_request(); + let client = make_client(); // First call: miss -> store let (_, outcomes) = cache - .send_with_policy(req.clone(), &RequestCachePolicy::default()) + .send_with_policy(&client, req.clone(), &CachePolicy::default()) .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); // Second call: hit (no extra HTTP request due to expect(1)) let (response, outcomes) = cache - .send_with_policy(req, &RequestCachePolicy::default()) + .send_with_policy(&client, req, &CachePolicy::default()) .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::Hit)); assert_eq!(response.status, 200); @@ -313,28 +309,21 @@ mod tests { let cache = make_cache(); let req = make_post_request(); + let client = make_client(); // First refresh: live -> MissStored let (_, outcomes) = cache .send_with_policy( + &client, req.clone(), - &RequestCachePolicy { - mode: CacheMode::NetworkFirst, - ttl_seconds: None, - }, + &CachePolicy::NetworkFirst { ttl: None }, ) .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); // Second refresh: live again (different body), still MissStored let (response, outcomes) = cache - .send_with_policy( - req, - &RequestCachePolicy { - mode: CacheMode::NetworkFirst, - ttl_seconds: None, - }, - ) + .send_with_policy(&client, req, &CachePolicy::NetworkFirst { ttl: None }) .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); assert_eq!(response.status, 200); @@ -354,9 +343,10 @@ mod tests { let cache = make_cache(); let req = make_post_request(); + let client = make_client(); let (_, outcomes) = cache - .send_with_policy(req.clone(), &RequestCachePolicy::default()) + .send_with_policy(&client, req.clone(), &CachePolicy::default()) .unwrap(); assert!(matches!( outcomes.last().unwrap(), @@ -371,7 +361,7 @@ mod tests { .expect(1) .create(); let (_, outcomes) = cache - .send_with_policy(req, &RequestCachePolicy::default()) + .send_with_policy(&client, req, &CachePolicy::default()) .unwrap(); // Either MissStored (if headers differ) or MissNotCacheable if still no-store assert!(matches!( @@ -395,13 +385,13 @@ mod tests { let cache = make_cache_with_ttl(300); let req = make_post_request(); let hash = RequestHash::new(&req); - let policy = RequestCachePolicy { - mode: CacheMode::CacheFirst, - ttl_seconds: Some(20), // 20 second ttl specified vs the cache's default of 300s + let policy = CachePolicy::CacheFirst { + ttl: Some(Duration::from_secs(20)), // 20 second ttl specified vs the cache's default of 300s }; + let client = make_client(); // Store ttl should resolve to 1s as specified by response headers - let (_, outcomes) = cache.send_with_policy(req, &policy).unwrap(); + let (_, outcomes) = cache.send_with_policy(&client, req, &policy).unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); // After ~>1s, cleanup should remove it @@ -425,13 +415,13 @@ mod tests { let cache = make_cache_with_ttl(60); let req = make_post_request(); let hash = RequestHash::new(&req); - let policy = RequestCachePolicy { - mode: CacheMode::CacheFirst, - ttl_seconds: Some(2), + let policy = CachePolicy::CacheFirst { + ttl: Some(Duration::from_secs(2)), }; + let client = make_client(); // Store with effective TTL = 2s - let (_, outcomes) = cache.send_with_policy(req, &policy).unwrap(); + let (_, outcomes) = cache.send_with_policy(&client, req, &policy).unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); // Not expired yet at ~1s @@ -460,11 +450,11 @@ mod tests { let cache = make_cache_with_ttl(2); let req = make_post_request(); let hash = RequestHash::new(&req); - // No request policy ttl - let policy = RequestCachePolicy::default(); - + let client = make_client(); // Store with effective TTL = 2s from client default - let (_, outcomes) = cache.send_with_policy(req, &policy).unwrap(); + let (_, outcomes) = cache + .send_with_policy(&client, req, &CachePolicy::default()) + .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); // Not expired at ~1s @@ -495,10 +485,11 @@ mod tests { let cache = make_cache_with_ttl(2); let req = make_post_request(); + let client = make_client(); // First call: miss -> store with 2s TTL let (_, outcomes) = cache - .send_with_policy(req.clone(), &RequestCachePolicy::default()) + .send_with_policy(&client, req.clone(), &CachePolicy::default()) .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); @@ -507,7 +498,7 @@ mod tests { // Second call: expired entry must be a miss, not a hit let (_, outcomes) = cache - .send_with_policy(req, &RequestCachePolicy::default()) + .send_with_policy(&client, req, &CachePolicy::default()) .unwrap(); assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored)); } @@ -530,12 +521,12 @@ mod tests { cache .store - .store_with_ttl(&hash1, &response, &Duration::new(300, 0)) + .store_with_ttl(&hash1, &response, &Duration::from_secs(300)) .unwrap(); cache .store - .store_with_ttl(&hash2, &response, &Duration::new(300, 0)) + .store_with_ttl(&hash2, &response, &Duration::from_secs(300)) .unwrap(); assert!(cache.store.lookup(&hash1).unwrap().is_some()); diff --git a/components/ads-client/src/http_cache/builder.rs b/components/ads-client/src/http_cache/builder.rs index 7cb22aada2..3d5bc44a62 100644 --- a/components/ads-client/src/http_cache/builder.rs +++ b/components/ads-client/src/http_cache/builder.rs @@ -118,9 +118,9 @@ impl> HttpCacheBuilder { let default_ttl = self.default_ttl.unwrap_or(DEFAULT_TTL); Ok(HttpCache { + default_ttl, max_size, store, - default_ttl, _phantom: std::marker::PhantomData, }) } @@ -135,9 +135,9 @@ impl> HttpCacheBuilder { let default_ttl = self.default_ttl.unwrap_or(DEFAULT_TTL); Ok(HttpCache { + default_ttl, max_size, store, - default_ttl, _phantom: std::marker::PhantomData, }) } diff --git a/components/ads-client/src/http_cache/store.rs b/components/ads-client/src/http_cache/store.rs index 74dab586f2..7ab9d5a6dd 100644 --- a/components/ads-client/src/http_cache/store.rs +++ b/components/ads-client/src/http_cache/store.rs @@ -340,7 +340,7 @@ mod tests { let hash = hash_for_request(&create_test_request("https://example.com/api", b"body")); let err = store - .store_with_ttl(&hash, &resp, &Duration::new(300, 0)) + .store_with_ttl(&hash, &resp, &Duration::from_secs(300)) .unwrap_err(); match err { rusqlite::Error::SqliteFailure(_, Some(msg)) => { @@ -359,7 +359,7 @@ mod tests { let hash = hash_for_request(&req); let resp = create_test_response(200, b"resp"); store - .store_with_ttl(&hash, &resp, &Duration::new(300, 0)) + .store_with_ttl(&hash, &resp, &Duration::from_secs(300)) .unwrap(); let err = store.trim_to_max_size(1).unwrap_err(); @@ -392,7 +392,7 @@ mod tests { let hash = hash_for_request(&req); let resp = create_test_response(200, b"X"); - let ttl = Duration::new(5, 0); + let ttl = Duration::from_secs(5); store.store_with_ttl(&hash, &resp, &ttl).unwrap(); let (cached_at, expires_at, ttl_seconds) = fetch_timestamps(&store, &hash); @@ -415,7 +415,7 @@ mod tests { let resp = create_test_response(200, b"Y"); store - .store_with_ttl(&hash, &resp, &Duration::new(300, 0)) + .store_with_ttl(&hash, &resp, &Duration::from_secs(300)) .unwrap(); let (c1, e1, t1) = fetch_timestamps(&store, &hash); assert_eq!(t1, 300); @@ -423,7 +423,7 @@ mod tests { store.get_clock().advance(3); store - .store_with_ttl(&hash, &resp, &Duration::new(1, 0)) + .store_with_ttl(&hash, &resp, &Duration::from_secs(1)) .unwrap(); let (c2, e2, t2) = fetch_timestamps(&store, &hash); assert_eq!(t2, 1); @@ -441,10 +441,10 @@ mod tests { let resp = create_test_response(200, b"Z"); store - .store_with_ttl(&hash_exp, &resp, &Duration::new(1, 0)) + .store_with_ttl(&hash_exp, &resp, &Duration::from_secs(1)) .unwrap(); store - .store_with_ttl(&hash_fresh, &resp, &Duration::new(10, 0)) + .store_with_ttl(&hash_fresh, &resp, &Duration::from_secs(10)) .unwrap(); assert!(store.lookup(&hash_exp).unwrap().is_some()); @@ -469,7 +469,7 @@ mod tests { let resp = create_test_response(200, b"W"); store - .store_with_ttl(&hash, &resp, &Duration::new(1, 0)) + .store_with_ttl(&hash, &resp, &Duration::from_secs(1)) .unwrap(); store.clock.advance(2); assert!(store.lookup(&hash).unwrap().is_some()); @@ -486,7 +486,7 @@ mod tests { let resp = create_test_response(200, b"0"); store - .store_with_ttl(&hash, &resp, &Duration::new(0, 0)) + .store_with_ttl(&hash, &resp, &Duration::from_secs(0)) .unwrap(); assert!(store.lookup(&hash).unwrap().is_some()); @@ -505,7 +505,7 @@ mod tests { let response = create_test_response(200, b"test response"); store - .store_with_ttl(&hash, &response, &Duration::new(300, 0)) + .store_with_ttl(&hash, &response, &Duration::from_secs(300)) .unwrap(); let retrieved = store.lookup(&hash).unwrap().unwrap(); @@ -522,7 +522,7 @@ mod tests { let response = create_test_response(200, b"test response"); store - .store_with_ttl(&hash, &response, &Duration::new(300, 0)) + .store_with_ttl(&hash, &response, &Duration::from_secs(300)) .unwrap(); let retrieved = store.lookup(&hash).unwrap().unwrap(); @@ -547,7 +547,7 @@ mod tests { let large_body = vec![0u8; 300]; let response = create_test_response(200, &large_body); store - .store_with_ttl(&hash, &response, &Duration::new(300, 0)) + .store_with_ttl(&hash, &response, &Duration::from_secs(300)) .unwrap(); } @@ -575,7 +575,7 @@ mod tests { .unwrap(); store - .store_with_ttl(&hash, &response, &Duration::new(300, 0)) + .store_with_ttl(&hash, &response, &Duration::from_secs(300)) .unwrap(); let retrieved = store.lookup(&hash).unwrap(); assert!(retrieved.is_some()); @@ -589,14 +589,14 @@ mod tests { let hash1 = hash_for_request(&request1); let response1 = create_test_response(200, b"test response 1"); store - .store_with_ttl(&hash1, &response1, &Duration::new(300, 0)) + .store_with_ttl(&hash1, &response1, &Duration::from_secs(300)) .unwrap(); let request2 = create_test_request("https://example.com/api2", b"test body 2"); let hash2 = hash_for_request(&request2); let response2 = create_test_response(200, b"test response 2"); store - .store_with_ttl(&hash2, &response2, &Duration::new(300, 0)) + .store_with_ttl(&hash2, &response2, &Duration::from_secs(300)) .unwrap(); assert!(store.lookup(&hash1).unwrap().is_some()); @@ -620,10 +620,10 @@ mod tests { let resp = create_test_response(200, b"resp"); store - .store_with_ttl(&hash1, &resp, &Duration::new(300, 0)) + .store_with_ttl(&hash1, &resp, &Duration::from_secs(300)) .unwrap(); store - .store_with_ttl(&hash2, &resp, &Duration::new(300, 0)) + .store_with_ttl(&hash2, &resp, &Duration::from_secs(300)) .unwrap(); assert!(store.lookup(&hash1).unwrap().is_some()); diff --git a/components/ads-client/src/lib.rs b/components/ads-client/src/lib.rs index 5d94c5d38c..756b617e7a 100644 --- a/components/ads-client/src/lib.rs +++ b/components/ads-client/src/lib.rs @@ -12,7 +12,7 @@ use url::Url as AdsClientUrl; use client::ad_request::AdPlacementRequest; use client::AdsClient; -use http_cache::RequestCachePolicy; +use http_cache::CachePolicy; mod client; mod error; @@ -51,7 +51,7 @@ impl MozAdsClient { ) -> AdsClientApiResult> { let inner = self.inner.lock(); let requests: Vec = moz_ad_requests.iter().map(|r| r.into()).collect(); - let cache_policy: RequestCachePolicy = options.into(); + let cache_policy: CachePolicy = options.into(); let response = inner .request_image_ads(requests, Some(cache_policy)) .map_err(ComponentError::RequestAds)?; @@ -66,7 +66,7 @@ impl MozAdsClient { ) -> AdsClientApiResult>> { let inner = self.inner.lock(); let requests: Vec = moz_ad_requests.iter().map(|r| r.into()).collect(); - let cache_policy: RequestCachePolicy = options.into(); + let cache_policy: CachePolicy = options.into(); let response = inner .request_spoc_ads(requests, Some(cache_policy)) .map_err(ComponentError::RequestAds)?; @@ -84,7 +84,7 @@ impl MozAdsClient { ) -> AdsClientApiResult> { let inner = self.inner.lock(); let requests: Vec = moz_ad_requests.iter().map(|r| r.into()).collect(); - let cache_policy: RequestCachePolicy = options.into(); + let cache_policy: CachePolicy = options.into(); let response = inner .request_tile_ads(requests, Some(cache_policy)) .map_err(ComponentError::RequestAds)?; diff --git a/components/ads-client/src/mars.rs b/components/ads-client/src/mars.rs index 1b9fcd6fc0..78ee723972 100644 --- a/components/ads-client/src/mars.rs +++ b/components/ads-client/src/mars.rs @@ -15,15 +15,16 @@ use crate::{ }, http_cache::{HttpCache, HttpCacheError, RequestHash}, telemetry::Telemetry, - RequestCachePolicy, + CachePolicy, }; use url::Url; -use viaduct::Request; +use viaduct::{Client, ClientSettings, Request}; pub struct MARSClient where T: Telemetry, { + http_client: Client, http_cache: Option>, telemetry: T, } @@ -34,21 +35,23 @@ where { pub fn new(http_cache: Option>, telemetry: T) -> Self { Self { + http_client: Client::new(ClientSettings::default()), http_cache, telemetry, } } - fn make_callback_request(&self, callback: Url) -> Result<(), CallbackRequestError> { - let request = Request::get(callback); - let response = request.send()?; - check_http_status_for_error(&response).map_err(Into::into) + pub fn clear_cache(&self) -> Result<(), HttpCacheError> { + if let Some(cache) = &self.http_cache { + cache.clear()?; + } + Ok(()) } pub fn fetch_ads( &self, ad_request: AdRequest, - cache_policy: &RequestCachePolicy, + cache_policy: CachePolicy, ) -> Result<(AdResponse, RequestHash), FetchAdsError> where A: AdResponseValue, @@ -56,7 +59,8 @@ where let request_hash = RequestHash::new(&ad_request); let response: AdResponse = if let Some(cache) = self.http_cache.as_ref() { - let (response, cache_outcomes) = cache.send_with_policy(ad_request, cache_policy)?; + let (response, cache_outcomes) = + cache.send_with_policy(&self.http_client, ad_request, &cache_policy)?; for outcome in &cache_outcomes { self.telemetry.record(outcome); } @@ -71,14 +75,6 @@ where Ok((response, request_hash)) } - pub fn record_impression(&self, callback: Url) -> Result<(), RecordImpressionError> { - Ok(self.make_callback_request(callback)?) - } - - pub fn record_click(&self, callback: Url) -> Result<(), RecordClickError> { - Ok(self.make_callback_request(callback)?) - } - // TODO: Remove this allow(dead_code) when cache invalidation is re-enabled behind Nimbus experiment #[allow(dead_code)] pub fn invalidate_cache_by_hash( @@ -91,6 +87,14 @@ where Ok(()) } + pub fn record_click(&self, callback: Url) -> Result<(), RecordClickError> { + Ok(self.make_callback_request(callback)?) + } + + pub fn record_impression(&self, callback: Url) -> Result<(), RecordImpressionError> { + Ok(self.make_callback_request(callback)?) + } + pub fn report_ad(&self, mut callback: Url, reason: ReportReason) -> Result<(), ReportAdError> { callback .query_pairs_mut() @@ -98,11 +102,10 @@ where Ok(self.make_callback_request(callback)?) } - pub fn clear_cache(&self) -> Result<(), HttpCacheError> { - if let Some(cache) = &self.http_cache { - cache.clear()?; - } - Ok(()) + fn make_callback_request(&self, callback: Url) -> Result<(), CallbackRequestError> { + let request = Request::get(callback); + let response = request.send()?; + check_http_status_for_error(&response).map_err(Into::into) } } @@ -179,7 +182,7 @@ mod tests { let ad_request = make_happy_ad_request(); - let result = client.fetch_ads::(ad_request, &RequestCachePolicy::default()); + let result = client.fetch_ads::(ad_request, CachePolicy::default()); assert!(result.is_ok()); let (response, _request_hash) = result.unwrap(); assert_eq!(expected_response, response); @@ -200,13 +203,13 @@ mod tests { // First call should be a miss then warm the cache let (response1, _request_hash1) = client - .fetch_ads::(make_happy_ad_request(), &RequestCachePolicy::default()) + .fetch_ads::(make_happy_ad_request(), CachePolicy::default()) .unwrap(); assert_eq!(response1, expected); // Second call should be a hit let (response2, _request_hash2) = client - .fetch_ads::(make_happy_ad_request(), &RequestCachePolicy::default()) + .fetch_ads::(make_happy_ad_request(), CachePolicy::default()) .unwrap(); assert_eq!(response2, expected); } diff --git a/components/ads-client/src/test_utils.rs b/components/ads-client/src/test_utils.rs index 6e5407da66..b9b9c14205 100644 --- a/components/ads-client/src/test_utils.rs +++ b/components/ads-client/src/test_utils.rs @@ -19,20 +19,20 @@ pub const TEST_CONTEXT_ID: &str = "00000000-0000-4000-8000-000000000001"; pub fn make_happy_placement_requests() -> Vec { vec![ AdPlacementRequest { - placement: "example_placement_1".to_string(), - count: 1, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, categories: vec!["entertainment".to_string()], + taxonomy: IABContentTaxonomy::IAB2_1, }), + count: 1, + placement: "example_placement_1".to_string(), }, AdPlacementRequest { - placement: "example_placement_2".to_string(), - count: 1, content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, categories: vec!["entertainment".to_string()], + taxonomy: IABContentTaxonomy::IAB2_1, }), + count: 1, + placement: "example_placement_2".to_string(), }, ] } @@ -43,14 +43,14 @@ pub fn make_happy_ad_request() -> AdRequest { TEST_CONTEXT_ID.to_string(), vec![ AdPlacementRequest { - placement: "example_placement_1".to_string(), - count: 1, content: None, + count: 1, + placement: "example_placement_1".to_string(), }, AdPlacementRequest { - placement: "example_placement_2".to_string(), - count: 1, content: None, + count: 1, + placement: "example_placement_2".to_string(), }, ], url,