From db010a446514f97b09bf5cde1f1c3838ca934127 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Fri, 20 Mar 2026 02:16:55 -0700 Subject: [PATCH 01/12] adds agentic audiences --- modules/agenticAudienceAdapter.js | 120 ++++++++++++++++++++++++++++++ modules/agenticAudienceAdapter.md | 78 +++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 modules/agenticAudienceAdapter.js create mode 100644 modules/agenticAudienceAdapter.md diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js new file mode 100644 index 00000000000..e81bc64eee4 --- /dev/null +++ b/modules/agenticAudienceAdapter.js @@ -0,0 +1,120 @@ +/** + * Agentic Audience Adapter – injects Agentic Audiences (vector-based) signals into the OpenRTB request. + * See: https://github.com/IABTechLab/agentic-audiences + * + * The {@link module:modules/realTimeData} module is required + * @module modules/agenticAudienceAdapter + * @requires module:modules/realTimeData + */ + +import { identity } from 'lodash'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { logInfo, logError } from '../src/utils.js'; + +/** + * @typedef {import('./rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +const REAL_TIME_MODULE = 'realTimeData'; +const MODULE_NAME = 'agenticAudience'; +export const DEFAULT_STORAGE_KEY = '_agentic_audience_'; + +export const storage = getStorageManager({ + moduleType: MODULE_TYPE_RTD, + moduleName: MODULE_NAME, +}); + +function dataFromLocalStorage(key) { + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(key) : null; +} + +function dataFromCookie(key) { + return storage.cookiesAreEnabled() ? storage.getCookie(key) : null; +} + +/** + * @param {Object} config + * @param {Object} userConsent + * @returns {boolean} + */ +function init(config, userConsent) { + return true; +} + +/** + * @param {Object} reqBidsConfigObj + * @param {function} callback + * @param {Object} config + * @param {Object} userConsent + */ +function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + const entries = []; + + if (defaultEntries = getEntries(DEFAULT_STORAGE_KEY)) { + defaultEntries.forEach(entry => { entries.push(entry); }); + } + + const configParams = (config && config.params) ? config.params : {}; + const providers = Object.keys(configParams['providers']); + + for (let i = 0; i < providers.length; i++) { + const provider = providers[i]; + const providerParams = configParams['providers'][provider]; + const providerEntries = getEntries(providerParams['storageKey']); + providerEntries.forEach(entry => { entries.push(entry); }); + } + + const updated = { + user: { + data: { + name: 'agentic-audiences.org', + segment: entries + } + } + }; + + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, updated); + callback(); +} + +function tryParse(data) { + try { + return JSON.parse(atob(data)); + } catch (error) { + logInfo(error); + return null; + } +} + +function getEntries(key) { + const storedData = dataFromLocalStorage(key) || dataFromCookie(key); + + if (!storedData || typeof storedData != 'string') { + return []; + } + + const parsed = tryParse(storedData); + + if (!parsed || typeof parsed != 'object') { + return []; + } + + return parsed['entries'].map(entry => ({ + ver: entry['ver'], + vector: entry['vector'], + model: entry['model'], + mdimension: entry['mdimension'], + type: entry['type'] + })); +} + +/** @type {RtdSubmodule} */ +export const agenticAudienceAdapterSubmodule = { + name: MODULE_NAME, + init, + getBidRequestData +}; + +submodule(REAL_TIME_MODULE, agenticAudienceAdapterSubmodule); diff --git a/modules/agenticAudienceAdapter.md b/modules/agenticAudienceAdapter.md new file mode 100644 index 00000000000..4a9be3a5f40 --- /dev/null +++ b/modules/agenticAudienceAdapter.md @@ -0,0 +1,78 @@ +# Agentic Audience Adapter + +## Overview + +| | | +|:---|:---| +| Module Name | Agentic Audience Adapter | +| Module Type | RTD Provider | +| Module Code | agenticAudienceAdapter | + +## Description + +This RTD module injects Agentic Audiences (vector-based) signals into the OpenRTB bid request. Agentic Audiences is an open standard by [IABTechLab](https://github.com/IABTechLab/agentic-audiences) for exchanging semantic embeddings—identity, contextual, and reinforcement signals—in a privacy-preserving, interoperable format. + +The module reads agentic audience data from browser storage (localStorage or cookie) and adds it to `user.data` as segment extensions for downstream bidders. + +## Usage + +### Build + +``` +gulp build --modules="rtdModule,agenticAudienceAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite. + +### Configuration + +Configure the module as part of `realTimeData.dataProviders`: + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'agenticAudience', + waitForIt: true, + params: { + providers: { + liveRamp: { + storageKey: '_lr_agentic_audience_' + } + } + } + }] + } +}); +``` + +### Parameters + +| Name | Type | Description | +|:-----|:-----|:------------| +| name | String | RTD submodule name. Always `'agenticAudience'` | +| waitForIt | Boolean | Set to true to delay auction until module responds | +| params.providers | Object | Provider-specific config. Each key (e.g. `liveRamp`) defines a provider with its own storage. | +| params.providers.{provider}.storageKey | String | Storage key for that provider (e.g. `_lr_agentic_audience_` for LiveRamp). | + +## Storage + +The module reads agentic audience data from browser storage (localStorage or cookie). It first reads from the default key `_agentic_audience_`, then from each provider's `storageKey` defined under `params.providers`. + +Data must be base64-encoded JSON with an `entries` array. Each entry must include: + +| Field | Type | Description | +|:------|:-----|:------------| +| ver | string | Specification version | +| vector | number[] | Vector embedding (float array) | +| model | string | Model identifier (e.g. `sbert-mini-ctx-001`) | +| mdimension | number | Vector dimension | +| type | number[] | Embedding type(s): identity, contextual, reinforcement | + +These fields align with the [Agentic Audiences OpenRTB Segment extension](https://github.com/IABTechLab/agentic-audiences). + +## References + +- [IABTechLab Agentic Audiences](https://github.com/IABTechLab/agentic-audiences) +- [Agentic Audiences in OpenRTB](https://github.com/IABTechLab/agentic-audiences) (Segment extension proposal) From 10439f1cba5f2fa4fce37f5e79c6bc61a0c63f58 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Fri, 20 Mar 2026 12:30:32 -0700 Subject: [PATCH 02/12] fixes typo and updates docs --- modules/agenticAudienceAdapter.js | 2 +- modules/agenticAudienceAdapter.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index e81bc64eee4..5959e053cc5 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -105,7 +105,7 @@ function getEntries(key) { ver: entry['ver'], vector: entry['vector'], model: entry['model'], - mdimension: entry['mdimension'], + dimension: entry['dimension'], type: entry['type'] })); } diff --git a/modules/agenticAudienceAdapter.md b/modules/agenticAudienceAdapter.md index 4a9be3a5f40..b7460b42e84 100644 --- a/modules/agenticAudienceAdapter.md +++ b/modules/agenticAudienceAdapter.md @@ -60,14 +60,16 @@ pbjs.setConfig({ The module reads agentic audience data from browser storage (localStorage or cookie). It first reads from the default key `_agentic_audience_`, then from each provider's `storageKey` defined under `params.providers`. -Data must be base64-encoded JSON with an `entries` array. Each entry must include: +**Encoding:** Data stored in cookie or localStorage **must be base64-encoded**. The module decodes the stored value and parses it as JSON. The decoded JSON (unencoded data) is what gets injected into the bid request and sent over the wire to bidders—not the base64 string. + +**Format:** The decoded JSON must contain an `entries` array. Each entry must include: | Field | Type | Description | |:------|:-----|:------------| | ver | string | Specification version | | vector | number[] | Vector embedding (float array) | | model | string | Model identifier (e.g. `sbert-mini-ctx-001`) | -| mdimension | number | Vector dimension | +| dimension | number | Vector dimension | | type | number[] | Embedding type(s): identity, contextual, reinforcement | These fields align with the [Agentic Audiences OpenRTB Segment extension](https://github.com/IABTechLab/agentic-audiences). From 9d09c9ab6c8d494e48a5bef7820d2b93c8fad2c0 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Fri, 20 Mar 2026 12:43:14 -0700 Subject: [PATCH 03/12] fixes data object --- modules/agenticAudienceAdapter.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index 5959e053cc5..8ce7482a89e 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -68,10 +68,12 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { const updated = { user: { - data: { - name: 'agentic-audiences.org', - segment: entries - } + data: [ + { + name: 'agentic-audiences.org', + segment: entries + } + ] } }; From 5637f67d3263632d69739a304ff458e4b3a36d08 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Fri, 20 Mar 2026 13:59:48 -0700 Subject: [PATCH 04/12] adds example --- modules/agenticAudienceAdapter.md | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/modules/agenticAudienceAdapter.md b/modules/agenticAudienceAdapter.md index b7460b42e84..09b99aa9e38 100644 --- a/modules/agenticAudienceAdapter.md +++ b/modules/agenticAudienceAdapter.md @@ -74,6 +74,76 @@ The module reads agentic audience data from browser storage (localStorage or coo These fields align with the [Agentic Audiences OpenRTB Segment extension](https://github.com/IABTechLab/agentic-audiences). +## Example OpenRTB user object + +The module injects agentic audience entries into `user.data`. Entries from the default storage key and all configured providers (e.g. LiveRamp, Optable) are merged into a single `segment` array under one Data object. + +### Single provider (LiveRamp only) + +```json +{ + "user": { + "data": [ + { + "name": "agentic-audiences.org", + "segment": [ + { + "ver": "1.0", + "vector": [0.1, -0.2, 0.3], + "model": "sbert-mini-ctx-001", + "dimension": 3, + "type": [1, 2] + } + ] + } + ] + } +} +``` + +### Multiple providers (LiveRamp and Optable) + +When configured with both LiveRamp and Optable, entries from both storage keys are combined into one `segment` array: + +```json +{ + "user": { + "data": [ + { + "name": "agentic-audiences.org", + "segment": [ + { + "ver": "1.0", + "vector": [0.1, -0.2, 0.3], + "model": "sbert-mini-ctx-001", + "dimension": 3, + "type": [1] + }, + { + "ver": "1.0", + "vector": [0.5, 0.6, -0.1], + "model": "optable-embed-v1", + "dimension": 3, + "type": [2] + } + ] + } + ] + } +} +``` + +Configuration for the multi-provider example: + +```javascript +params: { + providers: { + liveRamp: { storageKey: '_lr_agentic_audience_' }, + optable: { storageKey: '_optable_agentic_audience_' } + } +} +``` + ## References - [IABTechLab Agentic Audiences](https://github.com/IABTechLab/agentic-audiences) From d9afb514df7f4d70215446e3c42fb4511219dc12 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Sat, 21 Mar 2026 13:48:34 -0700 Subject: [PATCH 05/12] uses multiple data entries --- modules/agenticAudienceAdapter.js | 65 +++++++++++++++++++------------ modules/agenticAudienceAdapter.md | 32 ++++++++++----- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index 8ce7482a89e..774b192cd67 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -7,11 +7,10 @@ * @requires module:modules/realTimeData */ -import { identity } from 'lodash'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; -import { logInfo, logError } from '../src/utils.js'; +import { logInfo, mergeDeep } from '../src/utils.js'; /** * @typedef {import('./rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -19,7 +18,11 @@ import { logInfo, logError } from '../src/utils.js'; const REAL_TIME_MODULE = 'realTimeData'; const MODULE_NAME = 'agenticAudience'; -export const DEFAULT_STORAGE_KEY = '_agentic_audience_'; + +export const DEFAULT_PROVIDERS = { + liveRamp: { storageKey: '_lr_agentic_audience_' }, + optable: { storageKey: '_optable_agentic_audience_' } +}; export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, @@ -40,6 +43,10 @@ function dataFromCookie(key) { * @returns {boolean} */ function init(config, userConsent) { + const providers = config?.params?.providers ?? DEFAULT_PROVIDERS; + if (!providers || typeof providers !== 'object' || Object.keys(providers).length === 0) { + return false; + } return true; } @@ -50,30 +57,40 @@ function init(config, userConsent) { * @param {Object} userConsent */ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - const entries = []; - - if (defaultEntries = getEntries(DEFAULT_STORAGE_KEY)) { - defaultEntries.forEach(entry => { entries.push(entry); }); + const configParams = config?.params || {}; + const providers = configParams.providers ?? DEFAULT_PROVIDERS; + if (!providers || typeof providers !== 'object') { + callback(); + return; } - const configParams = (config && config.params) ? config.params : {}; - const providers = Object.keys(configParams['providers']); + const data = []; + const providerKeys = Object.keys(providers); + + for (let i = 0; i < providerKeys.length; i++) { + const provider = providerKeys[i]; + const providerParams = providers[provider]; + const storageKey = providerParams && providerParams.storageKey; + if (!storageKey) continue; + + const providerEntries = getEntries(storageKey); - for (let i = 0; i < providers.length; i++) { - const provider = providers[i]; - const providerParams = configParams['providers'][provider]; - const providerEntries = getEntries(providerParams['storageKey']); - providerEntries.forEach(entry => { entries.push(entry); }); + if (providerEntries && providerEntries.length > 0) { + data.push({ + name: provider, + segment: providerEntries + }); + } + } + + if (data.length === 0) { + callback(); + return; } const updated = { user: { - data: [ - { - name: 'agentic-audiences.org', - segment: entries - } - ] + data } }; @@ -93,17 +110,17 @@ function tryParse(data) { function getEntries(key) { const storedData = dataFromLocalStorage(key) || dataFromCookie(key); - if (!storedData || typeof storedData != 'string') { + if (!storedData || typeof storedData !== 'string') { return []; } const parsed = tryParse(storedData); - - if (!parsed || typeof parsed != 'object') { + + if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.entries)) { return []; } - return parsed['entries'].map(entry => ({ + return parsed.entries.map(entry => ({ ver: entry['ver'], vector: entry['vector'], model: entry['model'], diff --git a/modules/agenticAudienceAdapter.md b/modules/agenticAudienceAdapter.md index 09b99aa9e38..63ddf661634 100644 --- a/modules/agenticAudienceAdapter.md +++ b/modules/agenticAudienceAdapter.md @@ -53,12 +53,21 @@ pbjs.setConfig({ |:-----|:-----|:------------| | name | String | RTD submodule name. Always `'agenticAudience'` | | waitForIt | Boolean | Set to true to delay auction until module responds | -| params.providers | Object | Provider-specific config. Each key (e.g. `liveRamp`) defines a provider with its own storage. | -| params.providers.{provider}.storageKey | String | Storage key for that provider (e.g. `_lr_agentic_audience_` for LiveRamp). | +| params.providers | Object | Provider-specific config. Each key (e.g. `liveRamp`) defines a provider with its own storage. Optional; when omitted, `DEFAULT_PROVIDERS` (LiveRamp and Optable with default storage keys) is used. | +| params.providers.{provider}.storageKey | String | Storage key for that provider. Defaults: LiveRamp `_lr_agentic_audience_`, Optable `_optable_agentic_audience_`. | + +## Default providers + +When `params.providers` is not configured, the module uses `DEFAULT_PROVIDERS`: + +| Provider | Default storage key | +|:---------|:-------------------| +| liveRamp | `_lr_agentic_audience_` | +| optable | `_optable_agentic_audience_` | ## Storage -The module reads agentic audience data from browser storage (localStorage or cookie). It first reads from the default key `_agentic_audience_`, then from each provider's `storageKey` defined under `params.providers`. +The module reads agentic audience data from browser storage (localStorage or cookie) using each provider's `storageKey` from `params.providers` or `DEFAULT_PROVIDERS`. **Encoding:** Data stored in cookie or localStorage **must be base64-encoded**. The module decodes the stored value and parses it as JSON. The decoded JSON (unencoded data) is what gets injected into the bid request and sent over the wire to bidders—not the base64 string. @@ -76,7 +85,7 @@ These fields align with the [Agentic Audiences OpenRTB Segment extension](https: ## Example OpenRTB user object -The module injects agentic audience entries into `user.data`. Entries from the default storage key and all configured providers (e.g. LiveRamp, Optable) are merged into a single `segment` array under one Data object. +The module injects agentic audience entries into `user.data`. Each configured provider gets its own Data object with `name` set to the provider key and `segment` containing that provider's entries. ### Single provider (LiveRamp only) @@ -85,7 +94,7 @@ The module injects agentic audience entries into `user.data`. Entries from the d "user": { "data": [ { - "name": "agentic-audiences.org", + "name": "liveRamp", "segment": [ { "ver": "1.0", @@ -103,14 +112,14 @@ The module injects agentic audience entries into `user.data`. Entries from the d ### Multiple providers (LiveRamp and Optable) -When configured with both LiveRamp and Optable, entries from both storage keys are combined into one `segment` array: +When configured with both LiveRamp and Optable, each provider gets its own Data object: ```json { "user": { "data": [ { - "name": "agentic-audiences.org", + "name": "liveRamp", "segment": [ { "ver": "1.0", @@ -118,7 +127,12 @@ When configured with both LiveRamp and Optable, entries from both storage keys a "model": "sbert-mini-ctx-001", "dimension": 3, "type": [1] - }, + } + ] + }, + { + "name": "optable", + "segment": [ { "ver": "1.0", "vector": [0.5, 0.6, -0.1], @@ -133,7 +147,7 @@ When configured with both LiveRamp and Optable, entries from both storage keys a } ``` -Configuration for the multi-provider example: +Configuration for the multi-provider example (or omit `params` to use `DEFAULT_PROVIDERS`): ```javascript params: { From ca0e24eb049aef899aadf187c46b098a38432fc5 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Sat, 21 Mar 2026 15:01:23 -0700 Subject: [PATCH 06/12] adds agentic audiences spec --- modules/agenticAudienceAdapter.js | 23 +- modules/agenticAudienceAdapter.md | 8 +- .../modules/agenticAudienceAdapter_spec.js | 298 ++++++++++++++++++ 3 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 test/spec/modules/agenticAudienceAdapter_spec.js diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index 774b192cd67..c46ca24bbf6 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -7,6 +7,7 @@ * @requires module:modules/realTimeData */ +import snakeCase from 'lodash/snakeCase'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -42,12 +43,17 @@ function dataFromCookie(key) { * @param {Object} userConsent * @returns {boolean} */ +function getProviders(config) { + const configuredProviders = config?.params?.providers; + return { + ...DEFAULT_PROVIDERS, + ...(configuredProviders && typeof configuredProviders === 'object' ? configuredProviders : {}) + }; +} + function init(config, userConsent) { - const providers = config?.params?.providers ?? DEFAULT_PROVIDERS; - if (!providers || typeof providers !== 'object' || Object.keys(providers).length === 0) { - return false; - } - return true; + const providers = getProviders(config); + return Object.keys(providers).length > 0; } /** @@ -57,9 +63,8 @@ function init(config, userConsent) { * @param {Object} userConsent */ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - const configParams = config?.params || {}; - const providers = configParams.providers ?? DEFAULT_PROVIDERS; - if (!providers || typeof providers !== 'object') { + const providers = getProviders(config); + if (!providers || Object.keys(providers).length === 0) { callback(); return; } @@ -77,7 +82,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { if (providerEntries && providerEntries.length > 0) { data.push({ - name: provider, + name: snakeCase(provider), segment: providerEntries }); } diff --git a/modules/agenticAudienceAdapter.md b/modules/agenticAudienceAdapter.md index 63ddf661634..90fe5cf3bf9 100644 --- a/modules/agenticAudienceAdapter.md +++ b/modules/agenticAudienceAdapter.md @@ -53,7 +53,7 @@ pbjs.setConfig({ |:-----|:-----|:------------| | name | String | RTD submodule name. Always `'agenticAudience'` | | waitForIt | Boolean | Set to true to delay auction until module responds | -| params.providers | Object | Provider-specific config. Each key (e.g. `liveRamp`) defines a provider with its own storage. Optional; when omitted, `DEFAULT_PROVIDERS` (LiveRamp and Optable with default storage keys) is used. | +| params.providers | Object | Provider-specific config. Optional; when omitted, `DEFAULT_PROVIDERS` (LiveRamp and Optable) is used. When provided, it is **merged** with defaults so you can override individual providers (e.g. change LiveRamp's storageKey) or add new ones. | | params.providers.{provider}.storageKey | String | Storage key for that provider. Defaults: LiveRamp `_lr_agentic_audience_`, Optable `_optable_agentic_audience_`. | ## Default providers @@ -85,7 +85,7 @@ These fields align with the [Agentic Audiences OpenRTB Segment extension](https: ## Example OpenRTB user object -The module injects agentic audience entries into `user.data`. Each configured provider gets its own Data object with `name` set to the provider key and `segment` containing that provider's entries. +The module injects agentic audience entries into `user.data`. Each configured provider gets its own Data object with `name` set to the provider key in snake_case (e.g. `liveRamp` → `live_ramp`) and `segment` containing that provider's entries. ### Single provider (LiveRamp only) @@ -94,7 +94,7 @@ The module injects agentic audience entries into `user.data`. Each configured pr "user": { "data": [ { - "name": "liveRamp", + "name": "live_ramp", "segment": [ { "ver": "1.0", @@ -119,7 +119,7 @@ When configured with both LiveRamp and Optable, each provider gets its own Data "user": { "data": [ { - "name": "liveRamp", + "name": "live_ramp", "segment": [ { "ver": "1.0", diff --git a/test/spec/modules/agenticAudienceAdapter_spec.js b/test/spec/modules/agenticAudienceAdapter_spec.js new file mode 100644 index 00000000000..bc744574256 --- /dev/null +++ b/test/spec/modules/agenticAudienceAdapter_spec.js @@ -0,0 +1,298 @@ +import { + agenticAudienceAdapterSubmodule, + DEFAULT_PROVIDERS, + storage +} from 'modules/agenticAudienceAdapter.js'; + +describe('agenticAudienceAdapter', function () { + let sandbox; + let reqBidsConfigObj; + let storageGetLocalStub; + let storageGetCookieStub; + let storageLocalEnabledStub; + let storageCookiesEnabledStub; + + const validEntry = { + ver: '1.0', + vector: [0.1, -0.2, 0.3], + model: 'sbert-mini-ctx-001', + dimension: 3, + type: [1, 2] + }; + + const encodeData = (obj) => btoa(JSON.stringify(obj)); + + beforeEach(function () { + sandbox = sinon.createSandbox(); + reqBidsConfigObj = { ortb2Fragments: { global: {} } }; + storageGetLocalStub = sandbox.stub(storage, 'getDataFromLocalStorage'); + storageGetCookieStub = sandbox.stub(storage, 'getCookie'); + storageLocalEnabledStub = sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + storageCookiesEnabledStub = sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('init', function () { + it('returns true when params.providers is configured', function () { + const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + }); + + it('returns true when params is omitted (uses DEFAULT_PROVIDERS)', function () { + const config = {}; + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + }); + + it('returns true when params.providers is undefined (uses DEFAULT_PROVIDERS)', function () { + const config = { params: {} }; + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + }); + + it('returns true when params.providers is empty object (falls back to DEFAULT_PROVIDERS)', function () { + const config = { params: { providers: {} } }; + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + }); + + it('returns true when params.providers is null (falls back to DEFAULT_PROVIDERS)', function () { + const config = { params: { providers: null } }; + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + }); + + it('uses params.providers to override DEFAULT_PROVIDERS when passed', function () { + const config = { params: { providers: { customProvider: { storageKey: '_custom_key_' } } } }; + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + }); + }); + + describe('getBidRequestData', function () { + it('calls callback when DEFAULT_PROVIDERS have no data in storage', function () { + const config = {}; + const callback = sinon.spy(); + storageGetLocalStub.returns(null); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(callback.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global.user).to.be.undefined; + }); + + it('merges params.providers with DEFAULT_PROVIDERS; custom provider adds to defaults', function () { + const config = { params: { providers: { customProvider: { storageKey: '_custom_agentic_' } } } }; + const callback = sinon.spy(); + storageGetLocalStub.withArgs('_custom_agentic_').returns(encodeData({ entries: [validEntry] })); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('custom_provider'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); + }); + + it('overrides default provider storageKey when passed in params.providers', function () { + const config = { params: { providers: { liveRamp: { storageKey: '_custom_lr_key_' } } } }; + const callback = sinon.spy(); + storageGetLocalStub.withArgs('_custom_lr_key_').returns(encodeData({ entries: [validEntry] })); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); + }); + + it('calls callback and does not inject when storage has no data', function () { + const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const callback = sinon.spy(); + storageGetLocalStub.returns(null); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(callback.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global.user).to.be.undefined; + }); + + it('injects user.data from LiveRamp when storage has valid base64 entries', function () { + const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const callback = sinon.spy(); + const storedData = encodeData({ entries: [validEntry] }); + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(storedData); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(callback.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); + }); + + it('injects user.data from multiple providers (LiveRamp and Optable)', function () { + const config = { + params: { + providers: { + liveRamp: { storageKey: '_lr_agentic_audience_' }, + optable: { storageKey: '_optable_agentic_audience_' } + } + } + }; + const callback = sinon.spy(); + const liveRampEntry = { ...validEntry, model: 'sbert-mini-ctx-001' }; + const optableEntry = { ...validEntry, vector: [0.5, 0.6, -0.1], model: 'optable-embed-v1', type: [2] }; + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liveRampEntry] })); + storageGetLocalStub.withArgs('_optable_agentic_audience_').returns(encodeData({ entries: [optableEntry] })); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(callback.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(2); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.deep.equal({ + name: 'live_ramp', + segment: [liveRampEntry] + }); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[1]).to.deep.equal({ + name: 'optable', + segment: [optableEntry] + }); + }); + + it('uses DEFAULT_PROVIDERS when params.providers is omitted', function () { + const config = { params: {} }; + const callback = sinon.spy(); + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + }); + + it('skips provider when storageKey is missing', function () { + const config = { + params: { + providers: { + liveRamp: { storageKey: '_lr_agentic_audience_' }, + badProvider: {} + } + } + }; + const callback = sinon.spy(); + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + }); + + it('does not inject when stored data has empty entries array', function () { + const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const callback = sinon.spy(); + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [] })); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(callback.calledOnce).to.be.true; + expect(reqBidsConfigObj.ortb2Fragments.global.user).to.be.undefined; + }); + + it('reads from cookie when localStorage returns null', function () { + const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const callback = sinon.spy(); + storageGetLocalStub.returns(null); + storageGetCookieStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); + }); + }); + + describe('DEFAULT_PROVIDERS', function () { + it('includes liveRamp and optable with expected storage keys', function () { + expect(DEFAULT_PROVIDERS.liveRamp.storageKey).to.equal('_lr_agentic_audience_'); + expect(DEFAULT_PROVIDERS.optable.storageKey).to.equal('_optable_agentic_audience_'); + }); + }); + + describe('generates valid OpenRTB user object', function () { + it('produces valid OpenRTB user object for single provider', function () { + const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const callback = sinon.spy(); + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + const expectedUser = { + user: { + data: [ + { + name: 'live_ramp', + segment: [ + { + ver: '1.0', + vector: [0.1, -0.2, 0.3], + model: 'sbert-mini-ctx-001', + dimension: 3, + type: [1, 2] + } + ] + } + ] + } + }; + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(expectedUser); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.be.an('array'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.have.property('name', 'live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.have.property('segment'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment[0]).to.have.keys('ver', 'vector', 'model', 'dimension', 'type'); + }); + + it('produces valid OpenRTB user object for multiple providers', function () { + const config = { + params: { + providers: { + liveRamp: { storageKey: '_lr_agentic_audience_' }, + optable: { storageKey: '_optable_agentic_audience_' } + } + } + }; + const callback = sinon.spy(); + const liveRampEntry = { ver: '1.0', vector: [0.1, -0.2, 0.3], model: 'sbert-mini-ctx-001', dimension: 3, type: [1] }; + const optableEntry = { ver: '1.0', vector: [0.5, 0.6, -0.1], model: 'optable-embed-v1', dimension: 3, type: [2] }; + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liveRampEntry] })); + storageGetLocalStub.withArgs('_optable_agentic_audience_').returns(encodeData({ entries: [optableEntry] })); + storageGetCookieStub.returns(null); + + agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); + + const expectedUser = { + user: { + data: [ + { name: 'live_ramp', segment: [liveRampEntry] }, + { name: 'optable', segment: [optableEntry] } + ] + } + }; + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(expectedUser); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(2); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[1].name).to.equal('optable'); + reqBidsConfigObj.ortb2Fragments.global.user.data.forEach((dataObj) => { + expect(dataObj).to.have.keys('name', 'segment'); + expect(dataObj.segment).to.be.an('array'); + dataObj.segment.forEach((seg) => { + expect(seg).to.have.keys('ver', 'vector', 'model', 'dimension', 'type'); + }); + }); + }); + }); +}); From 4c05e7103a8b5d2fef93c6b94ac49bbbd925733f Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Sat, 21 Mar 2026 15:46:35 -0700 Subject: [PATCH 07/12] uses snakecase --- modules/agenticAudienceAdapter.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index c46ca24bbf6..f40b8339c03 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -7,7 +7,6 @@ * @requires module:modules/realTimeData */ -import snakeCase from 'lodash/snakeCase'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -30,6 +29,11 @@ export const storage = getStorageManager({ moduleName: MODULE_NAME, }); +function toSnakeCase(str) { + if (typeof str !== 'string') return str; + return str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`).replace(/^_/, ''); +} + function dataFromLocalStorage(key) { return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(key) : null; } @@ -82,7 +86,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { if (providerEntries && providerEntries.length > 0) { data.push({ - name: snakeCase(provider), + name: toSnakeCase(provider), segment: providerEntries }); } @@ -114,7 +118,7 @@ function tryParse(data) { function getEntries(key) { const storedData = dataFromLocalStorage(key) || dataFromCookie(key); - + if (!storedData || typeof storedData !== 'string') { return []; } From e6bb76d351119ca50ddbf97342f066e74d0a9d25 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Sat, 21 Mar 2026 18:00:51 -0700 Subject: [PATCH 08/12] cleans up implementation --- modules/agenticAudienceAdapter.js | 36 +--- modules/agenticAudienceAdapter.md | 164 ------------------ .../modules/agenticAudienceAdapter_spec.js | 93 ++++------ 3 files changed, 45 insertions(+), 248 deletions(-) delete mode 100644 modules/agenticAudienceAdapter.md diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index f40b8339c03..fbb020de532 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -19,21 +19,11 @@ import { logInfo, mergeDeep } from '../src/utils.js'; const REAL_TIME_MODULE = 'realTimeData'; const MODULE_NAME = 'agenticAudience'; -export const DEFAULT_PROVIDERS = { - liveRamp: { storageKey: '_lr_agentic_audience_' }, - optable: { storageKey: '_optable_agentic_audience_' } -}; - export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME, }); -function toSnakeCase(str) { - if (typeof str !== 'string') return str; - return str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`).replace(/^_/, ''); -} - function dataFromLocalStorage(key) { return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(key) : null; } @@ -42,22 +32,12 @@ function dataFromCookie(key) { return storage.cookiesAreEnabled() ? storage.getCookie(key) : null; } -/** - * @param {Object} config - * @param {Object} userConsent - * @returns {boolean} - */ -function getProviders(config) { - const configuredProviders = config?.params?.providers; - return { - ...DEFAULT_PROVIDERS, - ...(configuredProviders && typeof configuredProviders === 'object' ? configuredProviders : {}) - }; -} - function init(config, userConsent) { - const providers = getProviders(config); - return Object.keys(providers).length > 0; + const providers = config?.params?.providers; + if (!providers || typeof providers !== 'object' || Object.keys(providers).length === 0) { + return false; + } + return true; } /** @@ -67,8 +47,8 @@ function init(config, userConsent) { * @param {Object} userConsent */ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - const providers = getProviders(config); - if (!providers || Object.keys(providers).length === 0) { + const providers = config?.params?.providers; + if (!providers || typeof providers !== 'object' || Object.keys(providers).length === 0) { callback(); return; } @@ -86,7 +66,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { if (providerEntries && providerEntries.length > 0) { data.push({ - name: toSnakeCase(provider), + name: provider, segment: providerEntries }); } diff --git a/modules/agenticAudienceAdapter.md b/modules/agenticAudienceAdapter.md deleted file mode 100644 index 90fe5cf3bf9..00000000000 --- a/modules/agenticAudienceAdapter.md +++ /dev/null @@ -1,164 +0,0 @@ -# Agentic Audience Adapter - -## Overview - -| | | -|:---|:---| -| Module Name | Agentic Audience Adapter | -| Module Type | RTD Provider | -| Module Code | agenticAudienceAdapter | - -## Description - -This RTD module injects Agentic Audiences (vector-based) signals into the OpenRTB bid request. Agentic Audiences is an open standard by [IABTechLab](https://github.com/IABTechLab/agentic-audiences) for exchanging semantic embeddings—identity, contextual, and reinforcement signals—in a privacy-preserving, interoperable format. - -The module reads agentic audience data from browser storage (localStorage or cookie) and adds it to `user.data` as segment extensions for downstream bidders. - -## Usage - -### Build - -``` -gulp build --modules="rtdModule,agenticAudienceAdapter,..." -``` - -> Note that the global RTD module, `rtdModule`, is a prerequisite. - -### Configuration - -Configure the module as part of `realTimeData.dataProviders`: - -```javascript -pbjs.setConfig({ - realTimeData: { - auctionDelay: 300, - dataProviders: [{ - name: 'agenticAudience', - waitForIt: true, - params: { - providers: { - liveRamp: { - storageKey: '_lr_agentic_audience_' - } - } - } - }] - } -}); -``` - -### Parameters - -| Name | Type | Description | -|:-----|:-----|:------------| -| name | String | RTD submodule name. Always `'agenticAudience'` | -| waitForIt | Boolean | Set to true to delay auction until module responds | -| params.providers | Object | Provider-specific config. Optional; when omitted, `DEFAULT_PROVIDERS` (LiveRamp and Optable) is used. When provided, it is **merged** with defaults so you can override individual providers (e.g. change LiveRamp's storageKey) or add new ones. | -| params.providers.{provider}.storageKey | String | Storage key for that provider. Defaults: LiveRamp `_lr_agentic_audience_`, Optable `_optable_agentic_audience_`. | - -## Default providers - -When `params.providers` is not configured, the module uses `DEFAULT_PROVIDERS`: - -| Provider | Default storage key | -|:---------|:-------------------| -| liveRamp | `_lr_agentic_audience_` | -| optable | `_optable_agentic_audience_` | - -## Storage - -The module reads agentic audience data from browser storage (localStorage or cookie) using each provider's `storageKey` from `params.providers` or `DEFAULT_PROVIDERS`. - -**Encoding:** Data stored in cookie or localStorage **must be base64-encoded**. The module decodes the stored value and parses it as JSON. The decoded JSON (unencoded data) is what gets injected into the bid request and sent over the wire to bidders—not the base64 string. - -**Format:** The decoded JSON must contain an `entries` array. Each entry must include: - -| Field | Type | Description | -|:------|:-----|:------------| -| ver | string | Specification version | -| vector | number[] | Vector embedding (float array) | -| model | string | Model identifier (e.g. `sbert-mini-ctx-001`) | -| dimension | number | Vector dimension | -| type | number[] | Embedding type(s): identity, contextual, reinforcement | - -These fields align with the [Agentic Audiences OpenRTB Segment extension](https://github.com/IABTechLab/agentic-audiences). - -## Example OpenRTB user object - -The module injects agentic audience entries into `user.data`. Each configured provider gets its own Data object with `name` set to the provider key in snake_case (e.g. `liveRamp` → `live_ramp`) and `segment` containing that provider's entries. - -### Single provider (LiveRamp only) - -```json -{ - "user": { - "data": [ - { - "name": "live_ramp", - "segment": [ - { - "ver": "1.0", - "vector": [0.1, -0.2, 0.3], - "model": "sbert-mini-ctx-001", - "dimension": 3, - "type": [1, 2] - } - ] - } - ] - } -} -``` - -### Multiple providers (LiveRamp and Optable) - -When configured with both LiveRamp and Optable, each provider gets its own Data object: - -```json -{ - "user": { - "data": [ - { - "name": "live_ramp", - "segment": [ - { - "ver": "1.0", - "vector": [0.1, -0.2, 0.3], - "model": "sbert-mini-ctx-001", - "dimension": 3, - "type": [1] - } - ] - }, - { - "name": "optable", - "segment": [ - { - "ver": "1.0", - "vector": [0.5, 0.6, -0.1], - "model": "optable-embed-v1", - "dimension": 3, - "type": [2] - } - ] - } - ] - } -} -``` - -Configuration for the multi-provider example (or omit `params` to use `DEFAULT_PROVIDERS`): - -```javascript -params: { - providers: { - liveRamp: { storageKey: '_lr_agentic_audience_' }, - optable: { storageKey: '_optable_agentic_audience_' } - } -} -``` - -## References - -- [IABTechLab Agentic Audiences](https://github.com/IABTechLab/agentic-audiences) -- [Agentic Audiences in OpenRTB](https://github.com/IABTechLab/agentic-audiences) (Segment extension proposal) diff --git a/test/spec/modules/agenticAudienceAdapter_spec.js b/test/spec/modules/agenticAudienceAdapter_spec.js index bc744574256..cd7bf477baa 100644 --- a/test/spec/modules/agenticAudienceAdapter_spec.js +++ b/test/spec/modules/agenticAudienceAdapter_spec.js @@ -1,6 +1,5 @@ import { agenticAudienceAdapterSubmodule, - DEFAULT_PROVIDERS, storage } from 'modules/agenticAudienceAdapter.js'; @@ -36,39 +35,39 @@ describe('agenticAudienceAdapter', function () { }); describe('init', function () { - it('returns true when params.providers is configured', function () { - const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + it('returns true when params.providers is configured with at least one provider', function () { + const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); }); - it('returns true when params is omitted (uses DEFAULT_PROVIDERS)', function () { + it('returns false when params is omitted', function () { const config = {}; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(false); }); - it('returns true when params.providers is undefined (uses DEFAULT_PROVIDERS)', function () { + it('returns false when params.providers is undefined', function () { const config = { params: {} }; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(false); }); - it('returns true when params.providers is empty object (falls back to DEFAULT_PROVIDERS)', function () { + it('returns false when params.providers is empty object', function () { const config = { params: { providers: {} } }; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(false); }); - it('returns true when params.providers is null (falls back to DEFAULT_PROVIDERS)', function () { + it('returns false when params.providers is null', function () { const config = { params: { providers: null } }; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(false); }); - it('uses params.providers to override DEFAULT_PROVIDERS when passed', function () { + it('returns true when custom provider is passed', function () { const config = { params: { providers: { customProvider: { storageKey: '_custom_key_' } } } }; expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); }); }); describe('getBidRequestData', function () { - it('calls callback when DEFAULT_PROVIDERS have no data in storage', function () { + it('calls callback and does not inject when params.providers is omitted', function () { const config = {}; const callback = sinon.spy(); storageGetLocalStub.returns(null); @@ -80,7 +79,7 @@ describe('agenticAudienceAdapter', function () { expect(reqBidsConfigObj.ortb2Fragments.global.user).to.be.undefined; }); - it('merges params.providers with DEFAULT_PROVIDERS; custom provider adds to defaults', function () { + it('injects user.data from custom provider when configured', function () { const config = { params: { providers: { customProvider: { storageKey: '_custom_agentic_' } } } }; const callback = sinon.spy(); storageGetLocalStub.withArgs('_custom_agentic_').returns(encodeData({ entries: [validEntry] })); @@ -88,24 +87,24 @@ describe('agenticAudienceAdapter', function () { agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('custom_provider'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('customProvider'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); }); - it('overrides default provider storageKey when passed in params.providers', function () { - const config = { params: { providers: { liveRamp: { storageKey: '_custom_lr_key_' } } } }; + it('uses custom storageKey when passed in params.providers', function () { + const config = { params: { providers: { liveramp: { storageKey: '_custom_lr_key_' } } } }; const callback = sinon.spy(); storageGetLocalStub.withArgs('_custom_lr_key_').returns(encodeData({ entries: [validEntry] })); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); }); it('calls callback and does not inject when storage has no data', function () { - const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; const callback = sinon.spy(); storageGetLocalStub.returns(null); storageGetCookieStub.returns(null); @@ -117,7 +116,7 @@ describe('agenticAudienceAdapter', function () { }); it('injects user.data from LiveRamp when storage has valid base64 entries', function () { - const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; const callback = sinon.spy(); const storedData = encodeData({ entries: [validEntry] }); storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(storedData); @@ -127,7 +126,7 @@ describe('agenticAudienceAdapter', function () { expect(callback.calledOnce).to.be.true; expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); }); @@ -135,15 +134,15 @@ describe('agenticAudienceAdapter', function () { const config = { params: { providers: { - liveRamp: { storageKey: '_lr_agentic_audience_' }, + liveramp: { storageKey: '_lr_agentic_audience_' }, optable: { storageKey: '_optable_agentic_audience_' } } } }; const callback = sinon.spy(); - const liveRampEntry = { ...validEntry, model: 'sbert-mini-ctx-001' }; + const liverampEntry = { ...validEntry, model: 'sbert-mini-ctx-001' }; const optableEntry = { ...validEntry, vector: [0.5, 0.6, -0.1], model: 'optable-embed-v1', type: [2] }; - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liveRampEntry] })); + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liverampEntry] })); storageGetLocalStub.withArgs('_optable_agentic_audience_').returns(encodeData({ entries: [optableEntry] })); storageGetCookieStub.returns(null); @@ -152,8 +151,8 @@ describe('agenticAudienceAdapter', function () { expect(callback.calledOnce).to.be.true; expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(2); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.deep.equal({ - name: 'live_ramp', - segment: [liveRampEntry] + name: 'liveramp', + segment: [liverampEntry] }); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[1]).to.deep.equal({ name: 'optable', @@ -161,22 +160,11 @@ describe('agenticAudienceAdapter', function () { }); }); - it('uses DEFAULT_PROVIDERS when params.providers is omitted', function () { - const config = { params: {} }; - const callback = sinon.spy(); - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); - storageGetCookieStub.returns(null); - - agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); - }); - it('skips provider when storageKey is missing', function () { const config = { params: { providers: { - liveRamp: { storageKey: '_lr_agentic_audience_' }, + liveramp: { storageKey: '_lr_agentic_audience_' }, badProvider: {} } } @@ -188,11 +176,11 @@ describe('agenticAudienceAdapter', function () { agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); }); it('does not inject when stored data has empty entries array', function () { - const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; const callback = sinon.spy(); storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [] })); storageGetCookieStub.returns(null); @@ -204,7 +192,7 @@ describe('agenticAudienceAdapter', function () { }); it('reads from cookie when localStorage returns null', function () { - const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; const callback = sinon.spy(); storageGetLocalStub.returns(null); storageGetCookieStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); @@ -215,16 +203,9 @@ describe('agenticAudienceAdapter', function () { }); }); - describe('DEFAULT_PROVIDERS', function () { - it('includes liveRamp and optable with expected storage keys', function () { - expect(DEFAULT_PROVIDERS.liveRamp.storageKey).to.equal('_lr_agentic_audience_'); - expect(DEFAULT_PROVIDERS.optable.storageKey).to.equal('_optable_agentic_audience_'); - }); - }); - describe('generates valid OpenRTB user object', function () { it('produces valid OpenRTB user object for single provider', function () { - const config = { params: { providers: { liveRamp: { storageKey: '_lr_agentic_audience_' } } } }; + const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; const callback = sinon.spy(); storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); storageGetCookieStub.returns(null); @@ -235,7 +216,7 @@ describe('agenticAudienceAdapter', function () { user: { data: [ { - name: 'live_ramp', + name: 'liveramp', segment: [ { ver: '1.0', @@ -251,7 +232,7 @@ describe('agenticAudienceAdapter', function () { }; expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(expectedUser); expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.be.an('array'); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.have.property('name', 'live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.have.property('name', 'liveramp'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.have.property('segment'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment[0]).to.have.keys('ver', 'vector', 'model', 'dimension', 'type'); }); @@ -260,15 +241,15 @@ describe('agenticAudienceAdapter', function () { const config = { params: { providers: { - liveRamp: { storageKey: '_lr_agentic_audience_' }, + liveramp: { storageKey: '_lr_agentic_audience_' }, optable: { storageKey: '_optable_agentic_audience_' } } } }; const callback = sinon.spy(); - const liveRampEntry = { ver: '1.0', vector: [0.1, -0.2, 0.3], model: 'sbert-mini-ctx-001', dimension: 3, type: [1] }; + const liverampEntry = { ver: '1.0', vector: [0.1, -0.2, 0.3], model: 'sbert-mini-ctx-001', dimension: 3, type: [1] }; const optableEntry = { ver: '1.0', vector: [0.5, 0.6, -0.1], model: 'optable-embed-v1', dimension: 3, type: [2] }; - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liveRampEntry] })); + storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liverampEntry] })); storageGetLocalStub.withArgs('_optable_agentic_audience_').returns(encodeData({ entries: [optableEntry] })); storageGetCookieStub.returns(null); @@ -277,14 +258,14 @@ describe('agenticAudienceAdapter', function () { const expectedUser = { user: { data: [ - { name: 'live_ramp', segment: [liveRampEntry] }, + { name: 'liveramp', segment: [liverampEntry] }, { name: 'optable', segment: [optableEntry] } ] } }; expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(expectedUser); expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(2); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('live_ramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[1].name).to.equal('optable'); reqBidsConfigObj.ortb2Fragments.global.user.data.forEach((dataObj) => { expect(dataObj).to.have.keys('name', 'segment'); From a632c758fceb87d911551c988528014e4e7e2b54 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Wed, 1 Apr 2026 01:32:08 -0700 Subject: [PATCH 09/12] updates to latest extension --- modules/agenticAudienceAdapter.js | 45 +++-- .../modules/agenticAudienceAdapter_spec.js | 156 +++++++++++------- 2 files changed, 133 insertions(+), 68 deletions(-) diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index fbb020de532..90411428040 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -1,6 +1,9 @@ /** * Agentic Audience Adapter – injects Agentic Audiences (vector-based) signals into the OpenRTB request. - * See: https://github.com/IABTechLab/agentic-audiences + * Conforms to the OpenRTB community extension: + * {@link https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/agentic-audiences.md Agentic Audiences in OpenRTB} + * + * Context: {@link https://github.com/IABTechLab/agentic-audiences IABTechLab Agentic Audiences} * * The {@link module:modules/realTimeData} module is required * @module modules/agenticAudienceAdapter @@ -32,6 +35,28 @@ function dataFromCookie(key) { return storage.cookiesAreEnabled() ? storage.getCookie(key) : null; } +/** + * Map a stored entry to an OpenRTB Segment (Agentic Audiences): id, name, ext.{ver, vector, dimension, model, type} + * Assumes storage matches the intended shape; fields are copied without validation or coercion. + * @param {Object} entry - Raw entry from storage `entries` array + * @returns {Object|null} + */ +export function mapEntryToOpenRtbSegment(entry) { + if (entry == null || typeof entry !== 'object') return null; + + return { + id: entry.id, + name: entry.name, + ext: { + ver: entry.ver, + vector: entry.vector, + dimension: entry.dimension, + model: entry.model, + type: entry.type + } + }; +} + function init(config, userConsent) { const providers = config?.params?.providers; if (!providers || typeof providers !== 'object' || Object.keys(providers).length === 0) { @@ -62,12 +87,12 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { const storageKey = providerParams && providerParams.storageKey; if (!storageKey) continue; - const providerEntries = getEntries(storageKey); + const segments = getSegmentsForStorageKey(storageKey); - if (providerEntries && providerEntries.length > 0) { + if (segments && segments.length > 0) { data.push({ name: provider, - segment: providerEntries + segment: segments }); } } @@ -96,7 +121,7 @@ function tryParse(data) { } } -function getEntries(key) { +function getSegmentsForStorageKey(key) { const storedData = dataFromLocalStorage(key) || dataFromCookie(key); if (!storedData || typeof storedData !== 'string') { @@ -109,13 +134,9 @@ function getEntries(key) { return []; } - return parsed.entries.map(entry => ({ - ver: entry['ver'], - vector: entry['vector'], - model: entry['model'], - dimension: entry['dimension'], - type: entry['type'] - })); + return parsed.entries + .map(entry => mapEntryToOpenRtbSegment(entry)) + .filter(seg => seg != null); } /** @type {RtdSubmodule} */ diff --git a/test/spec/modules/agenticAudienceAdapter_spec.js b/test/spec/modules/agenticAudienceAdapter_spec.js index cd7bf477baa..a3c44e3de4e 100644 --- a/test/spec/modules/agenticAudienceAdapter_spec.js +++ b/test/spec/modules/agenticAudienceAdapter_spec.js @@ -1,8 +1,20 @@ import { agenticAudienceAdapterSubmodule, + mapEntryToOpenRtbSegment, storage } from 'modules/agenticAudienceAdapter.js'; +/** Test fixture: OpenRTB Float32 LE base64 (module expects pre-encoded storage only). */ +function vectorBase64Fixture(arr) { + const buffer = new ArrayBuffer(arr.length * 4); + const view = new DataView(buffer); + arr.forEach((x, i) => view.setFloat32(i * 4, x, true)); + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); + return btoa(binary); +} + describe('agenticAudienceAdapter', function () { let sandbox; let reqBidsConfigObj; @@ -13,7 +25,7 @@ describe('agenticAudienceAdapter', function () { const validEntry = { ver: '1.0', - vector: [0.1, -0.2, 0.3], + vector: vectorBase64Fixture([0.1, -0.2, 0.3]), model: 'sbert-mini-ctx-001', dimension: 3, type: [1, 2] @@ -34,6 +46,58 @@ describe('agenticAudienceAdapter', function () { sandbox.restore(); }); + describe('mapEntryToOpenRtbSegment', function () { + it('maps stored Base64 vector to Segment unchanged', function () { + const seg = mapEntryToOpenRtbSegment(validEntry); + expect(seg.id).to.be.undefined; + expect(seg.name).to.be.undefined; + expect(seg.ext.ver).to.equal('1.0'); + expect(seg.ext.vector).to.equal(validEntry.vector); + expect(seg.ext.dimension).to.equal(3); + expect(seg.ext.model).to.equal('sbert-mini-ctx-001'); + expect(seg.ext.type).to.deep.equal([1, 2]); + }); + + it('passes vector through without coercion (e.g. array storage)', function () { + const arr = [0.1, 0.2, 0.3]; + const seg = mapEntryToOpenRtbSegment({ ...validEntry, vector: arr }); + expect(seg.ext.vector).to.equal(arr); + }); + + it('passes type through without normalizing number to array', function () { + const seg = mapEntryToOpenRtbSegment({ ...validEntry, type: 1 }); + expect(seg.ext.type).to.equal(1); + }); + + it('uses custom id and name when provided', function () { + const seg = mapEntryToOpenRtbSegment({ + ...validEntry, + id: 'seg-1', + name: 'identity-contextual' + }); + expect(seg.id).to.equal('seg-1'); + expect(seg.name).to.equal('identity-contextual'); + }); + + it('returns null only for non-object entry', function () { + expect(mapEntryToOpenRtbSegment(null)).to.equal(null); + expect(mapEntryToOpenRtbSegment(undefined)).to.equal(null); + }); + + it('maps empty object to segment with id, name, and ext fields undefined', function () { + const seg = mapEntryToOpenRtbSegment({}); + expect(seg.id).to.be.undefined; + expect(seg.name).to.be.undefined; + expect(seg.ext).to.deep.equal({ + ver: undefined, + vector: undefined, + dimension: undefined, + model: undefined, + type: undefined + }); + }); + }); + describe('init', function () { it('returns true when params.providers is configured with at least one provider', function () { const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; @@ -88,7 +152,9 @@ describe('agenticAudienceAdapter', function () { agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('customProvider'); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([ + mapEntryToOpenRtbSegment(validEntry) + ]); }); it('uses custom storageKey when passed in params.providers', function () { @@ -100,7 +166,9 @@ describe('agenticAudienceAdapter', function () { agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([ + mapEntryToOpenRtbSegment(validEntry) + ]); }); it('calls callback and does not inject when storage has no data', function () { @@ -115,7 +183,7 @@ describe('agenticAudienceAdapter', function () { expect(reqBidsConfigObj.ortb2Fragments.global.user).to.be.undefined; }); - it('injects user.data from LiveRamp when storage has valid base64 entries', function () { + it('injects user.data from liveramp when storage has valid base64 entries', function () { const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; const callback = sinon.spy(); const storedData = encodeData({ entries: [validEntry] }); @@ -127,23 +195,25 @@ describe('agenticAudienceAdapter', function () { expect(callback.calledOnce).to.be.true; expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([ + mapEntryToOpenRtbSegment(validEntry) + ]); }); - it('injects user.data from multiple providers (LiveRamp and Optable)', function () { + it('injects user.data from multiple providers (liveramp and raptive)', function () { const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' }, - optable: { storageKey: '_optable_agentic_audience_' } + raptive: { storageKey: '_raptive_agentic_audience_' } } } }; const callback = sinon.spy(); const liverampEntry = { ...validEntry, model: 'sbert-mini-ctx-001' }; - const optableEntry = { ...validEntry, vector: [0.5, 0.6, -0.1], model: 'optable-embed-v1', type: [2] }; + const raptiveEntry = { ...validEntry, vector: vectorBase64Fixture([0.5, 0.6, -0.1]), model: 'raptive-embed-v1', type: [2] }; storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liverampEntry] })); - storageGetLocalStub.withArgs('_optable_agentic_audience_').returns(encodeData({ entries: [optableEntry] })); + storageGetLocalStub.withArgs('_raptive_agentic_audience_').returns(encodeData({ entries: [raptiveEntry] })); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); @@ -152,11 +222,11 @@ describe('agenticAudienceAdapter', function () { expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(2); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.deep.equal({ name: 'liveramp', - segment: [liverampEntry] + segment: [mapEntryToOpenRtbSegment(liverampEntry)] }); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[1]).to.deep.equal({ - name: 'optable', - segment: [optableEntry] + name: 'raptive', + segment: [mapEntryToOpenRtbSegment(raptiveEntry)] }); }); @@ -199,12 +269,14 @@ describe('agenticAudienceAdapter', function () { agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([validEntry]); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([ + mapEntryToOpenRtbSegment(validEntry) + ]); }); }); - describe('generates valid OpenRTB user object', function () { - it('produces valid OpenRTB user object for single provider', function () { + describe('generates valid OpenRTB user object (Agentic Audiences extension)', function () { + it('produces valid structure for single provider', function () { const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; const callback = sinon.spy(); storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); @@ -212,66 +284,38 @@ describe('agenticAudienceAdapter', function () { agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - const expectedUser = { - user: { - data: [ - { - name: 'liveramp', - segment: [ - { - ver: '1.0', - vector: [0.1, -0.2, 0.3], - model: 'sbert-mini-ctx-001', - dimension: 3, - type: [1, 2] - } - ] - } - ] - } - }; - expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(expectedUser); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.be.an('array'); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.have.property('name', 'liveramp'); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.have.property('segment'); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment[0]).to.have.keys('ver', 'vector', 'model', 'dimension', 'type'); + const seg = reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment[0]; + expect(seg).to.have.keys('id', 'name', 'ext'); + expect(seg.ext).to.have.keys('ver', 'vector', 'dimension', 'model', 'type'); + expect(seg.ext.vector).to.equal(validEntry.vector); + expect(seg).to.deep.equal(mapEntryToOpenRtbSegment(validEntry)); }); - it('produces valid OpenRTB user object for multiple providers', function () { + it('produces valid structure for multiple providers', function () { const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' }, - optable: { storageKey: '_optable_agentic_audience_' } + raptive: { storageKey: '_raptive_agentic_audience_' } } } }; const callback = sinon.spy(); - const liverampEntry = { ver: '1.0', vector: [0.1, -0.2, 0.3], model: 'sbert-mini-ctx-001', dimension: 3, type: [1] }; - const optableEntry = { ver: '1.0', vector: [0.5, 0.6, -0.1], model: 'optable-embed-v1', dimension: 3, type: [2] }; + const liverampEntry = { ver: '1.0', vector: vectorBase64Fixture([0.1, -0.2, 0.3]), model: 'sbert-mini-ctx-001', dimension: 3, type: [1] }; + const raptiveEntry = { ver: '1.0', vector: vectorBase64Fixture([0.5, 0.6, -0.1]), model: 'raptive-embed-v1', dimension: 3, type: [2] }; storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liverampEntry] })); - storageGetLocalStub.withArgs('_optable_agentic_audience_').returns(encodeData({ entries: [optableEntry] })); + storageGetLocalStub.withArgs('_raptive_agentic_audience_').returns(encodeData({ entries: [raptiveEntry] })); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - const expectedUser = { - user: { - data: [ - { name: 'liveramp', segment: [liverampEntry] }, - { name: 'optable', segment: [optableEntry] } - ] - } - }; - expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(expectedUser); expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(2); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[1].name).to.equal('optable'); reqBidsConfigObj.ortb2Fragments.global.user.data.forEach((dataObj) => { expect(dataObj).to.have.keys('name', 'segment'); expect(dataObj.segment).to.be.an('array'); - dataObj.segment.forEach((seg) => { - expect(seg).to.have.keys('ver', 'vector', 'model', 'dimension', 'type'); + dataObj.segment.forEach((segment) => { + expect(segment).to.have.keys('id', 'name', 'ext'); + expect(segment.ext).to.have.keys('ver', 'vector', 'dimension', 'model', 'type'); }); }); }); From fd5db4d7e01a5bc1cb227ce34250847e6af3ec22 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Wed, 1 Apr 2026 09:19:00 -0700 Subject: [PATCH 10/12] adds vendorless id --- modules/agenticAudienceAdapter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index 90411428040..e8060b6045f 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -14,6 +14,7 @@ import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; import { logInfo, mergeDeep } from '../src/utils.js'; +import {VENDORLESS_GVLID} from '../src/consentHandler.js'; /** * @typedef {import('./rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -142,6 +143,7 @@ function getSegmentsForStorageKey(key) { /** @type {RtdSubmodule} */ export const agenticAudienceAdapterSubmodule = { name: MODULE_NAME, + gvlid: VENDORLESS_GVLID, init, getBidRequestData }; From 21ebd4dfc05bf4b51633e449422992e68d629ce7 Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Mon, 6 Apr 2026 12:08:58 -0700 Subject: [PATCH 11/12] adds to submodules.json --- modules/.submodules.json | 1 + modules/agenticAudienceAdapter.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/.submodules.json b/modules/.submodules.json index c0e972f3c5d..c5b3ea9f9c6 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -73,6 +73,7 @@ "adlooxRtdProvider", "adlaneRtdProvider", "adnuntiusRtdProvider", + "agenticAudienceAdapter", "airgridRtdProvider", "akamaiDapRtdProvider", "anonymisedRtdProvider", diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index e8060b6045f..bb9d372b7ec 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -14,7 +14,7 @@ import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; import { logInfo, mergeDeep } from '../src/utils.js'; -import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +import { VENDORLESS_GVLID } from '../src/consentHandler.js'; /** * @typedef {import('./rtdModule/index.js').RtdSubmodule} RtdSubmodule From 4c4cc16ed8cf8ad076ba2f7bac6723609bd4e9cc Mon Sep 17 00:00:00 2001 From: Miguel Morales Date: Tue, 14 Apr 2026 15:09:43 -0700 Subject: [PATCH 12/12] remove gvlid and multiple providers --- modules/agenticAudienceAdapter.js | 64 +++--- .../modules/agenticAudienceAdapter_spec.js | 205 ++++-------------- 2 files changed, 75 insertions(+), 194 deletions(-) diff --git a/modules/agenticAudienceAdapter.js b/modules/agenticAudienceAdapter.js index bb9d372b7ec..96b2152fcc4 100644 --- a/modules/agenticAudienceAdapter.js +++ b/modules/agenticAudienceAdapter.js @@ -6,6 +6,11 @@ * Context: {@link https://github.com/IABTechLab/agentic-audiences IABTechLab Agentic Audiences} * * The {@link module:modules/realTimeData} module is required + * + * Injects one OpenRTB `Data` object into `user.data` (`name` = submodule id, `segment[]` from storage). + * Each segment has optional `id`/`name` and `ext.aa` with `ver`, `vector`, `dimension`, `model`, `type`. + * Storage is read from the default key (see `DEFAULT_STORAGE_KEY` export) unless `params.storageKey` is set. + * * @module modules/agenticAudienceAdapter * @requires module:modules/realTimeData */ @@ -14,7 +19,6 @@ import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; import { logInfo, mergeDeep } from '../src/utils.js'; -import { VENDORLESS_GVLID } from '../src/consentHandler.js'; /** * @typedef {import('./rtdModule/index.js').RtdSubmodule} RtdSubmodule @@ -23,6 +27,9 @@ import { VENDORLESS_GVLID } from '../src/consentHandler.js'; const REAL_TIME_MODULE = 'realTimeData'; const MODULE_NAME = 'agenticAudience'; +/** @type {string} Default localStorage / cookie key when `params.storageKey` is omitted. */ +export const DEFAULT_STORAGE_KEY = '_agentic_audience_'; + export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME, @@ -37,7 +44,7 @@ function dataFromCookie(key) { } /** - * Map a stored entry to an OpenRTB Segment (Agentic Audiences): id, name, ext.{ver, vector, dimension, model, type} + * Map a stored entry to an OpenRTB Segment (Agentic Audiences): id, name, ext.aa.{ver, vector, dimension, model, type} * Assumes storage matches the intended shape; fields are copied without validation or coercion. * @param {Object} entry - Raw entry from storage `entries` array * @returns {Object|null} @@ -49,20 +56,18 @@ export function mapEntryToOpenRtbSegment(entry) { id: entry.id, name: entry.name, ext: { - ver: entry.ver, - vector: entry.vector, - dimension: entry.dimension, - model: entry.model, - type: entry.type + aa: { + ver: entry.ver, + vector: entry.vector, + dimension: entry.dimension, + model: entry.model, + type: entry.type + } } }; } function init(config, userConsent) { - const providers = config?.params?.providers; - if (!providers || typeof providers !== 'object' || Object.keys(providers).length === 0) { - return false; - } return true; } @@ -73,39 +78,25 @@ function init(config, userConsent) { * @param {Object} userConsent */ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - const providers = config?.params?.providers; - if (!providers || typeof providers !== 'object' || Object.keys(providers).length === 0) { - callback(); - return; - } - - const data = []; - const providerKeys = Object.keys(providers); - - for (let i = 0; i < providerKeys.length; i++) { - const provider = providerKeys[i]; - const providerParams = providers[provider]; - const storageKey = providerParams && providerParams.storageKey; - if (!storageKey) continue; + const customKey = config?.params?.storageKey; + const storageKey = + typeof customKey === 'string' && customKey.length > 0 ? customKey : DEFAULT_STORAGE_KEY; - const segments = getSegmentsForStorageKey(storageKey); - - if (segments && segments.length > 0) { - data.push({ - name: provider, - segment: segments - }); - } - } + const segments = getSegmentsForStorageKey(storageKey); - if (data.length === 0) { + if (!segments || segments.length === 0) { callback(); return; } const updated = { user: { - data + data: [ + { + name: MODULE_NAME, + segment: segments + } + ] } }; @@ -143,7 +134,6 @@ function getSegmentsForStorageKey(key) { /** @type {RtdSubmodule} */ export const agenticAudienceAdapterSubmodule = { name: MODULE_NAME, - gvlid: VENDORLESS_GVLID, init, getBidRequestData }; diff --git a/test/spec/modules/agenticAudienceAdapter_spec.js b/test/spec/modules/agenticAudienceAdapter_spec.js index a3c44e3de4e..0e062938dd5 100644 --- a/test/spec/modules/agenticAudienceAdapter_spec.js +++ b/test/spec/modules/agenticAudienceAdapter_spec.js @@ -1,5 +1,6 @@ import { agenticAudienceAdapterSubmodule, + DEFAULT_STORAGE_KEY, mapEntryToOpenRtbSegment, storage } from 'modules/agenticAudienceAdapter.js'; @@ -51,22 +52,22 @@ describe('agenticAudienceAdapter', function () { const seg = mapEntryToOpenRtbSegment(validEntry); expect(seg.id).to.be.undefined; expect(seg.name).to.be.undefined; - expect(seg.ext.ver).to.equal('1.0'); - expect(seg.ext.vector).to.equal(validEntry.vector); - expect(seg.ext.dimension).to.equal(3); - expect(seg.ext.model).to.equal('sbert-mini-ctx-001'); - expect(seg.ext.type).to.deep.equal([1, 2]); + expect(seg.ext.aa.ver).to.equal('1.0'); + expect(seg.ext.aa.vector).to.equal(validEntry.vector); + expect(seg.ext.aa.dimension).to.equal(3); + expect(seg.ext.aa.model).to.equal('sbert-mini-ctx-001'); + expect(seg.ext.aa.type).to.deep.equal([1, 2]); }); it('passes vector through without coercion (e.g. array storage)', function () { const arr = [0.1, 0.2, 0.3]; const seg = mapEntryToOpenRtbSegment({ ...validEntry, vector: arr }); - expect(seg.ext.vector).to.equal(arr); + expect(seg.ext.aa.vector).to.equal(arr); }); it('passes type through without normalizing number to array', function () { const seg = mapEntryToOpenRtbSegment({ ...validEntry, type: 1 }); - expect(seg.ext.type).to.equal(1); + expect(seg.ext.aa.type).to.equal(1); }); it('uses custom id and name when provided', function () { @@ -89,170 +90,84 @@ describe('agenticAudienceAdapter', function () { expect(seg.id).to.be.undefined; expect(seg.name).to.be.undefined; expect(seg.ext).to.deep.equal({ - ver: undefined, - vector: undefined, - dimension: undefined, - model: undefined, - type: undefined + aa: { + ver: undefined, + vector: undefined, + dimension: undefined, + model: undefined, + type: undefined + } }); }); }); describe('init', function () { - it('returns true when params.providers is configured with at least one provider', function () { - const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); - }); - - it('returns false when params is omitted', function () { - const config = {}; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(false); - }); - - it('returns false when params.providers is undefined', function () { - const config = { params: {} }; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(false); - }); - - it('returns false when params.providers is empty object', function () { - const config = { params: { providers: {} } }; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(false); - }); - - it('returns false when params.providers is null', function () { - const config = { params: { providers: null } }; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(false); - }); - - it('returns true when custom provider is passed', function () { - const config = { params: { providers: { customProvider: { storageKey: '_custom_key_' } } } }; - expect(agenticAudienceAdapterSubmodule.init(config)).to.equal(true); + it('returns true regardless of params', function () { + expect(agenticAudienceAdapterSubmodule.init({})).to.equal(true); + expect(agenticAudienceAdapterSubmodule.init({ params: { storageKey: '_custom_' } })).to.equal(true); }); }); describe('getBidRequestData', function () { - it('calls callback and does not inject when params.providers is omitted', function () { + it('uses default storage key when params omitted', function () { const config = {}; const callback = sinon.spy(); - storageGetLocalStub.returns(null); + storageGetLocalStub.withArgs(DEFAULT_STORAGE_KEY).returns(encodeData({ entries: [validEntry] })); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); expect(callback.calledOnce).to.be.true; - expect(reqBidsConfigObj.ortb2Fragments.global.user).to.be.undefined; - }); - - it('injects user.data from custom provider when configured', function () { - const config = { params: { providers: { customProvider: { storageKey: '_custom_agentic_' } } } }; - const callback = sinon.spy(); - storageGetLocalStub.withArgs('_custom_agentic_').returns(encodeData({ entries: [validEntry] })); - storageGetCookieStub.returns(null); - - agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('customProvider'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('agenticAudience'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([ mapEntryToOpenRtbSegment(validEntry) ]); }); - it('uses custom storageKey when passed in params.providers', function () { - const config = { params: { providers: { liveramp: { storageKey: '_custom_lr_key_' } } } }; + it('uses params.storageKey when provided', function () { + const config = { params: { storageKey: '_custom_agentic_' } }; const callback = sinon.spy(); - storageGetLocalStub.withArgs('_custom_lr_key_').returns(encodeData({ entries: [validEntry] })); + storageGetLocalStub.withArgs('_custom_agentic_').returns(encodeData({ entries: [validEntry] })); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('agenticAudience'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([ mapEntryToOpenRtbSegment(validEntry) ]); }); - it('calls callback and does not inject when storage has no data', function () { - const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; + it('falls back to default key when storageKey is empty string', function () { + const config = { params: { storageKey: '' } }; const callback = sinon.spy(); - storageGetLocalStub.returns(null); + storageGetLocalStub.withArgs(DEFAULT_STORAGE_KEY).returns(encodeData({ entries: [validEntry] })); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - expect(callback.calledOnce).to.be.true; - expect(reqBidsConfigObj.ortb2Fragments.global.user).to.be.undefined; - }); - - it('injects user.data from liveramp when storage has valid base64 entries', function () { - const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; - const callback = sinon.spy(); - const storedData = encodeData({ entries: [validEntry] }); - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(storedData); - storageGetCookieStub.returns(null); - - agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - - expect(callback.calledOnce).to.be.true; - expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.deep.equal([ mapEntryToOpenRtbSegment(validEntry) ]); }); - it('injects user.data from multiple providers (liveramp and raptive)', function () { - const config = { - params: { - providers: { - liveramp: { storageKey: '_lr_agentic_audience_' }, - raptive: { storageKey: '_raptive_agentic_audience_' } - } - } - }; + it('calls callback and does not inject when storage has no data', function () { + const config = {}; const callback = sinon.spy(); - const liverampEntry = { ...validEntry, model: 'sbert-mini-ctx-001' }; - const raptiveEntry = { ...validEntry, vector: vectorBase64Fixture([0.5, 0.6, -0.1]), model: 'raptive-embed-v1', type: [2] }; - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liverampEntry] })); - storageGetLocalStub.withArgs('_raptive_agentic_audience_').returns(encodeData({ entries: [raptiveEntry] })); + storageGetLocalStub.withArgs(DEFAULT_STORAGE_KEY).returns(null); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); expect(callback.calledOnce).to.be.true; - expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(2); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0]).to.deep.equal({ - name: 'liveramp', - segment: [mapEntryToOpenRtbSegment(liverampEntry)] - }); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[1]).to.deep.equal({ - name: 'raptive', - segment: [mapEntryToOpenRtbSegment(raptiveEntry)] - }); - }); - - it('skips provider when storageKey is missing', function () { - const config = { - params: { - providers: { - liveramp: { storageKey: '_lr_agentic_audience_' }, - badProvider: {} - } - } - }; - const callback = sinon.spy(); - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); - storageGetCookieStub.returns(null); - - agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - - expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); - expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal('liveramp'); + expect(reqBidsConfigObj.ortb2Fragments.global.user).to.be.undefined; }); it('does not inject when stored data has empty entries array', function () { - const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; + const config = {}; const callback = sinon.spy(); - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [] })); + storageGetLocalStub.withArgs(DEFAULT_STORAGE_KEY).returns(encodeData({ entries: [] })); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); @@ -262,10 +177,10 @@ describe('agenticAudienceAdapter', function () { }); it('reads from cookie when localStorage returns null', function () { - const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; + const config = {}; const callback = sinon.spy(); storageGetLocalStub.returns(null); - storageGetCookieStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); + storageGetCookieStub.withArgs(DEFAULT_STORAGE_KEY).returns(encodeData({ entries: [validEntry] })); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); @@ -276,48 +191,24 @@ describe('agenticAudienceAdapter', function () { }); describe('generates valid OpenRTB user object (Agentic Audiences extension)', function () { - it('produces valid structure for single provider', function () { - const config = { params: { providers: { liveramp: { storageKey: '_lr_agentic_audience_' } } } }; + it('produces valid structure under user.data[0]', function () { + const config = {}; const callback = sinon.spy(); - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [validEntry] })); + storageGetLocalStub.withArgs(DEFAULT_STORAGE_KEY).returns(encodeData({ entries: [validEntry] })); storageGetCookieStub.returns(null); agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - const seg = reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment[0]; + expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(1); + const dataObj = reqBidsConfigObj.ortb2Fragments.global.user.data[0]; + expect(dataObj).to.have.keys('name', 'segment'); + expect(dataObj.name).to.equal('agenticAudience'); + const seg = dataObj.segment[0]; expect(seg).to.have.keys('id', 'name', 'ext'); - expect(seg.ext).to.have.keys('ver', 'vector', 'dimension', 'model', 'type'); - expect(seg.ext.vector).to.equal(validEntry.vector); + expect(seg.ext).to.have.keys('aa'); + expect(seg.ext.aa).to.have.keys('ver', 'vector', 'dimension', 'model', 'type'); + expect(seg.ext.aa.vector).to.equal(validEntry.vector); expect(seg).to.deep.equal(mapEntryToOpenRtbSegment(validEntry)); }); - - it('produces valid structure for multiple providers', function () { - const config = { - params: { - providers: { - liveramp: { storageKey: '_lr_agentic_audience_' }, - raptive: { storageKey: '_raptive_agentic_audience_' } - } - } - }; - const callback = sinon.spy(); - const liverampEntry = { ver: '1.0', vector: vectorBase64Fixture([0.1, -0.2, 0.3]), model: 'sbert-mini-ctx-001', dimension: 3, type: [1] }; - const raptiveEntry = { ver: '1.0', vector: vectorBase64Fixture([0.5, 0.6, -0.1]), model: 'raptive-embed-v1', dimension: 3, type: [2] }; - storageGetLocalStub.withArgs('_lr_agentic_audience_').returns(encodeData({ entries: [liverampEntry] })); - storageGetLocalStub.withArgs('_raptive_agentic_audience_').returns(encodeData({ entries: [raptiveEntry] })); - storageGetCookieStub.returns(null); - - agenticAudienceAdapterSubmodule.getBidRequestData(reqBidsConfigObj, callback, config); - - expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.have.length(2); - reqBidsConfigObj.ortb2Fragments.global.user.data.forEach((dataObj) => { - expect(dataObj).to.have.keys('name', 'segment'); - expect(dataObj.segment).to.be.an('array'); - dataObj.segment.forEach((segment) => { - expect(segment).to.have.keys('id', 'name', 'ext'); - expect(segment.ext).to.have.keys('ver', 'vector', 'dimension', 'model', 'type'); - }); - }); - }); }); });