diff --git a/modules/mobianRtdProvider.js b/modules/mobianRtdProvider.js index 02f2d1b83cf..01a0a5d93d1 100644 --- a/modules/mobianRtdProvider.js +++ b/modules/mobianRtdProvider.js @@ -37,15 +37,24 @@ import { setKeyValue } from '../libraries/gptUtils/gptUtils.js'; export const MOBIAN_URL = 'https://prebid.outcomes.net/api/prebid/v1/assessment/async'; +export const AP_VALUES = 'apValues'; +export const CATEGORIES = 'categories'; +export const EMOTIONS = 'emotions'; +export const GENRES = 'genres'; +export const RISK = 'risk'; +export const SENTIMENT = 'sentiment'; +export const THEMES = 'themes'; +export const TONES = 'tones'; + export const CONTEXT_KEYS = [ - 'apValues', - 'categories', - 'emotions', - 'genres', - 'risk', - 'sentiment', - 'themes', - 'tones' + AP_VALUES, + CATEGORIES, + EMOTIONS, + GENRES, + RISK, + SENTIMENT, + THEMES, + TONES ]; const AP_KEYS = ['a0', 'a1', 'p0', 'p1']; @@ -73,6 +82,24 @@ function makeMemoizedFetch() { export const getContextData = makeMemoizedFetch(); +const entriesToObjectReducer = (acc, [key, value]) => ({ ...acc, [key]: value }); + +export function makeContextDataToKeyValuesReducer(config) { + const { prefix } = config; + return function contextDataToKeyValuesReducer(keyValues, [key, value]) { + if (key === AP_VALUES) { + AP_KEYS.forEach((apKey) => { + if (!value?.[apKey]?.length) return; + keyValues.push([`${prefix}_ap_${apKey}`, value[apKey].map((v) => String(v))]); + }); + } + if (value?.length) { + keyValues.push([`${prefix}_${key}`, value]); + } + return keyValues; + } +} + export async function fetchContextData() { const pageUrl = encodeURIComponent(window.location.href); const requestUrl = `${MOBIAN_URL}?url=${pageUrl}`; @@ -101,33 +128,22 @@ export function getConfig(config) { } /** - * @param {MobianConfigParams} parsedConfig + * @param {MobianConfig} config * @param {MobianContextData} contextData - * @returns {function} */ -export function setTargeting(parsedConfig, contextData) { - const { publisherTargeting, prefix } = parsedConfig; +export function setTargeting(config, contextData) { logMessage('context', contextData); + const keyValues = Object.entries(contextData) + .filter(([key]) => config.publisherTargeting.includes(key)) + .reduce(makeContextDataToKeyValuesReducer(config), []) - CONTEXT_KEYS.forEach((key) => { - if (!publisherTargeting.includes(key)) return; - - if (key === 'apValues') { - AP_KEYS.forEach((apKey) => { - if (!contextData[key]?.[apKey]?.length) return; - logMessage(`${prefix}_ap_${apKey}`, contextData[key][apKey]); - setKeyValue(`${prefix}_ap_${apKey}`, contextData[key][apKey]); - }); - return; - } - - if (contextData[key]?.length) { - logMessage(`${prefix}_${key}`, contextData[key]); - setKeyValue(`${prefix}_${key}`, contextData[key]); - } - }); + keyValues.forEach(([key, value]) => setKeyValue(key, value)); } +/** + * @param {Object|string} contextData + * @returns {MobianContextData} + */ export function makeDataFromResponse(contextData) { const data = typeof contextData === 'string' ? safeJSONParse(contextData) : contextData; const results = data.results; @@ -135,50 +151,57 @@ export function makeDataFromResponse(contextData) { return {}; } return { - apValues: results.ap || {}, - categories: results.mobianContentCategories, - emotions: results.mobianEmotions, - genres: results.mobianGenres, - risk: results.mobianRisk || 'unknown', - sentiment: results.mobianSentiment || 'unknown', - themes: results.mobianThemes, - tones: results.mobianTones, + [AP_VALUES]: results.ap || {}, + [CATEGORIES]: results.mobianContentCategories, + [EMOTIONS]: results.mobianEmotions, + [GENRES]: results.mobianGenres, + [RISK]: results.mobianRisk || 'unknown', + [SENTIMENT]: results.mobianSentiment || 'unknown', + [THEMES]: results.mobianThemes, + [TONES]: results.mobianTones, }; } -export function extendBidRequestConfig(bidReqConfig, contextData) { +/** + * @param {Object} bidReqConfig + * @param {MobianContextData} contextData + * @param {MobianConfig} config + */ +export function extendBidRequestConfig(bidReqConfig, contextData, config) { logMessage('extendBidRequestConfig', bidReqConfig, contextData); const { site: ortb2Site } = bidReqConfig.ortb2Fragments.global; + const keyValues = Object.entries(contextData) + .filter(([key]) => config.advertiserTargeting.includes(key)) + .reduce(makeContextDataToKeyValuesReducer(config), []) + .reduce(entriesToObjectReducer, {}); ortb2Site.ext = ortb2Site.ext || {}; ortb2Site.ext.data = { ...(ortb2Site.ext.data || {}), - ...contextData + ...keyValues }; return bidReqConfig; } /** - * @param {MobianConfig} config + * @param {MobianConfig} rawConfig * @returns {boolean} */ -function init(config) { - logMessage('init', config); - - const parsedConfig = getConfig(config); - - if (parsedConfig.publisherTargeting.length) { - getContextData().then((contextData) => setTargeting(parsedConfig, contextData)); +function init(rawConfig) { + logMessage('init', rawConfig); + const config = getConfig(rawConfig); + if (config.publisherTargeting.length) { + getContextData().then((contextData) => setTargeting(config, contextData)); } - return true; } -function getBidRequestData(bidReqConfig, callback, config) { +function getBidRequestData(bidReqConfig, callback, rawConfig) { logMessage('getBidRequestData', bidReqConfig); - const { advertiserTargeting } = getConfig(config); + const config = getConfig(rawConfig); + const { advertiserTargeting } = config; if (!advertiserTargeting.length) { callback(); @@ -187,7 +210,7 @@ function getBidRequestData(bidReqConfig, callback, config) { getContextData() .then((contextData) => { - extendBidRequestConfig(bidReqConfig, contextData); + extendBidRequestConfig(bidReqConfig, contextData, config); }) .catch(() => {}) .finally(() => callback()); diff --git a/test/spec/modules/mobianRtdProvider_spec.js b/test/spec/modules/mobianRtdProvider_spec.js index 796a79e4e1c..0794e99151d 100644 --- a/test/spec/modules/mobianRtdProvider_spec.js +++ b/test/spec/modules/mobianRtdProvider_spec.js @@ -4,10 +4,19 @@ import * as ajax from 'src/ajax.js'; import * as gptUtils from 'libraries/gptUtils/gptUtils.js'; import { CONTEXT_KEYS, + AP_VALUES, + CATEGORIES, + EMOTIONS, + GENRES, + RISK, + SENTIMENT, + THEMES, + TONES, extendBidRequestConfig, fetchContextData, getConfig, getContextData, + makeContextDataToKeyValuesReducer, makeDataFromResponse, setTargeting, } from 'modules/mobianRtdProvider.js'; @@ -35,14 +44,29 @@ describe('Mobian RTD Submodule', function () { }); const mockContextData = { - apValues: { a0: [], a1: [2313, 12], p0: [1231231, 212], p1: [231, 419] }, - categories: [], - emotions: ['affection'], - genres: [], - risk: 'low', - sentiment: 'positive', - themes: [], - tones: [], + [AP_VALUES]: { a0: [], a1: [2313, 12], p0: [1231231, 212], p1: [231, 419] }, + [CATEGORIES]: [], + [EMOTIONS]: ['affection'], + [GENRES]: [], + [RISK]: 'low', + [SENTIMENT]: 'positive', + [THEMES]: [], + [TONES]: [], + } + + const mockKeyValues = { + 'mobian_ap_a1': ['2313', '12'], + 'mobian_ap_p0': ['1231231', '212'], + 'mobian_ap_p1': ['231', '419'], + 'mobian_emotions': ['affection'], + 'mobian_risk': 'low', + 'mobian_sentiment': 'positive', + } + + const mockConfig = { + prefix: 'mobian', + publisherTargeting: [AP_VALUES, EMOTIONS, RISK, SENTIMENT, THEMES, TONES, GENRES], + advertiserTargeting: [AP_VALUES, EMOTIONS, RISK, SENTIMENT, THEMES, TONES, GENRES], } beforeEach(function () { @@ -79,10 +103,6 @@ describe('Mobian RTD Submodule', function () { describe('makeDataFromResponse', function () { it('should format context data response', async function () { - ajaxStub = sinon.stub(ajax, 'ajaxBuilder').returns(function(url, callbacks) { - callbacks.success(mockResponse); - }); - const data = makeDataFromResponse(mockResponse); expect(data).to.deep.equal(mockContextData); }); @@ -103,14 +123,14 @@ describe('Mobian RTD Submodule', function () { it('should set targeting key-value pairs as per config', function () { const parsedConfig = { prefix: 'mobian', - publisherTargeting: ['apValues', 'emotions', 'risk', 'sentiment', 'themes', 'tones', 'genres'], + publisherTargeting: [AP_VALUES, EMOTIONS, RISK, SENTIMENT, THEMES, TONES, GENRES], }; setTargeting(parsedConfig, mockContextData); expect(setKeyValueSpy.callCount).to.equal(6); - expect(setKeyValueSpy.calledWith('mobian_ap_a1', [2313, 12])).to.equal(true); - expect(setKeyValueSpy.calledWith('mobian_ap_p0', [1231231, 212])).to.equal(true); - expect(setKeyValueSpy.calledWith('mobian_ap_p1', [231, 419])).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_ap_a1', ['2313', '12'])).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_ap_p0', ['1231231', '212'])).to.equal(true); + expect(setKeyValueSpy.calledWith('mobian_ap_p1', ['231', '419'])).to.equal(true); expect(setKeyValueSpy.calledWith('mobian_emotions', ['affection'])).to.equal(true); expect(setKeyValueSpy.calledWith('mobian_risk', 'low')).to.equal(true); expect(setKeyValueSpy.calledWith('mobian_sentiment', 'positive')).to.equal(true); @@ -124,7 +144,7 @@ describe('Mobian RTD Submodule', function () { it('should not set key-value pairs if context data is empty', function () { const parsedConfig = { prefix: 'mobian', - publisherTargeting: ['apValues', 'emotions', 'risk', 'sentiment', 'themes', 'tones', 'genres'], + publisherTargeting: [AP_VALUES, EMOTIONS, RISK, SENTIMENT, THEMES, TONES, GENRES], }; setTargeting(parsedConfig, {}); @@ -134,7 +154,7 @@ describe('Mobian RTD Submodule', function () { it('should only set key-value pairs for the keys specified in config', function () { const parsedConfig = { prefix: 'mobian', - publisherTargeting: ['emotions', 'risk'], + publisherTargeting: [EMOTIONS, RISK], }; setTargeting(parsedConfig, mockContextData); @@ -155,8 +175,8 @@ describe('Mobian RTD Submodule', function () { describe('extendBidRequestConfig', function () { it('should extend bid request config with context data', function () { - const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData); - expect(extendedConfig.ortb2Fragments.global.site.ext.data).to.deep.equal(mockContextData); + const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData, mockConfig); + expect(extendedConfig.ortb2Fragments.global.site.ext.data).to.deep.equal(mockKeyValues); }); it('should not override existing data', function () { @@ -164,17 +184,17 @@ describe('Mobian RTD Submodule', function () { existing: 'data' }; - const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData); + const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData, mockConfig); expect(extendedConfig.ortb2Fragments.global.site.ext.data).to.deep.equal({ existing: 'data', - ...mockContextData + ...mockKeyValues }); }); it('should create data object if missing', function () { delete bidReqConfig.ortb2Fragments.global.site.ext.data; - const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData); - expect(extendedConfig.ortb2Fragments.global.site.ext.data).to.deep.equal(mockContextData); + const extendedConfig = extendBidRequestConfig(bidReqConfig, mockContextData, mockConfig); + expect(extendedConfig.ortb2Fragments.global.site.ext.data).to.deep.equal(mockKeyValues); }); }); @@ -184,14 +204,14 @@ describe('Mobian RTD Submodule', function () { name: 'mobianBrandSafety', params: { prefix: 'mobiantest', - publisherTargeting: ['apValues'], - advertiserTargeting: ['emotions'], + publisherTargeting: [AP_VALUES], + advertiserTargeting: [EMOTIONS], } }); expect(config).to.deep.equal({ prefix: 'mobiantest', - publisherTargeting: ['apValues'], - advertiserTargeting: ['emotions'], + publisherTargeting: [AP_VALUES], + advertiserTargeting: [EMOTIONS], }); }); @@ -199,12 +219,12 @@ describe('Mobian RTD Submodule', function () { const config = getConfig({ name: 'mobianBrandSafety', params: { - publisherTargeting: ['apValues'], + publisherTargeting: [AP_VALUES], } }); expect(config).to.deep.equal({ prefix: 'mobian', - publisherTargeting: ['apValues'], + publisherTargeting: [AP_VALUES], advertiserTargeting: [], }); }); @@ -242,4 +262,20 @@ describe('Mobian RTD Submodule', function () { }); }); }); + + describe('makeContextDataToKeyValuesReducer', function () { + it('should format context data to key-value pairs', function () { + const config = getConfig({ + name: 'mobianBrandSafety', + params: { + prefix: 'mobian', + publisherTargeting: true, + advertiserTargeting: true, + } + }); + const keyValues = Object.entries(mockContextData).reduce(makeContextDataToKeyValuesReducer(config), []); + const keyValuesObject = Object.fromEntries(keyValues); + expect(keyValuesObject).to.deep.equal(mockKeyValues); + }); + }); });