Skip to content

Commit

Permalink
Prebid 8: pick keywords from FPD in addition to bid params (#9917)
Browse files Browse the repository at this point in the history
* extract appnexus-style keyword logic into its own library

* refactor appnexus clones to share keyword logic

* pick keywords from ortb2 in addition to bid params
  • Loading branch information
dgirardi authored May 25, 2023
1 parent 6d4d22d commit 5868d29
Show file tree
Hide file tree
Showing 22 changed files with 486 additions and 449 deletions.
106 changes: 106 additions & 0 deletions libraries/appnexusKeywords/anKeywords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {_each, getValueString, isArray, isStr, mergeDeep, isNumber} from '../../src/utils.js';
import {getAllOrtbKeywords} from '../keywords/keywords.js';

/**
* Converts an object of arrays (either strings or numbers) into an array of objects containing key and value properties
* normally read from bidder params
* eg { foo: ['bar', 'baz'], fizz: ['buzz'] }
* becomes [{ key: 'foo', value: ['bar', 'baz']}, {key: 'fizz', value: ['buzz']}]
* @param {Object} keywords object of arrays representing keyvalue pairs
* @param {string} paramName name of parent object (eg 'keywords') containing keyword data, used in error handling
* @returns {Array<{key, value}>}
*/
export function transformBidderParamKeywords(keywords, paramName = 'keywords') {
const arrs = [];

_each(keywords, (v, k) => {
if (isArray(v)) {
let values = [];
_each(v, (val) => {
val = getValueString(paramName + '.' + k, val);
if (val || val === '') {
values.push(val);
}
});
v = values;
} else {
v = getValueString(paramName + '.' + k, v);
if (isStr(v)) {
v = [v];
} else {
return;
} // unsuported types - don't send a key
}
v = v.filter(kw => kw !== '')
const entry = {key: k}
if (v.length > 0) {
entry.value = v;
}
arrs.push(entry);
});

return arrs;
}

// converts a comma separated list of keywords into the standard keyword object format used in appnexus bid params
// 'genre=rock,genre=pop,pets=dog,music' goes to { 'genre': ['rock', 'pop'], 'pets': ['dog'], 'music': [''] }
export function convertKeywordStringToANMap(keyStr) {
if (isStr(keyStr) && keyStr !== '') {
// will split based on commas and will eat white space before/after the comma
return convertKeywordsToANMap(keyStr.split(/\s*(?:,)\s*/));
} else {
return {}
}
}

/**
* @param {Array<String>} kwarray: keywords as an array of strings
* @return {{}} appnexus-style keyword map
*/
function convertKeywordsToANMap(kwarray) {
const result = {};
kwarray.forEach(kw => {
// if = exists, then split
if (kw.indexOf('=') !== -1) {
let kwPair = kw.split('=');
let key = kwPair[0];
let val = kwPair[1];

// then check for existing key in result > if so add value to the array > if not, add new key and create value array
if (result.hasOwnProperty(key)) {
result[key].push(val);
} else {
result[key] = [val];
}
} else {
if (!result.hasOwnProperty(kw)) {
result[kw] = [];
}
}
})
return result;
}

/**
* @param ortb2
* @return {{}} appnexus-style keyword map using all keywords contained in ortb2
*/
export function getANMapFromOrtb(ortb2) {
return convertKeywordsToANMap(getAllOrtbKeywords(ortb2));
}

export function getANKewyordParamFromMaps(...anKeywordMaps) {
return transformBidderParamKeywords(
mergeDeep(...anKeywordMaps.map(kwMap => Object.fromEntries(
Object.entries(kwMap || {})
.map(([k, v]) => [k, (isNumber(v) || isStr(v)) ? [v] : v])
)))
)
}

export function getANKeywordParam(ortb2, ...anKeywordsMaps) {
return getANKewyordParamFromMaps(
getANMapFromOrtb(ortb2),
...anKeywordsMaps
)
}
31 changes: 31 additions & 0 deletions libraries/keywords/keywords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {CLIENT_SECTIONS} from '../../src/fpd/oneClient.js';
import {deepAccess} from '../../src/utils.js';

const ORTB_KEYWORDS_PATHS = ['user.keywords'].concat(
CLIENT_SECTIONS.flatMap((prefix) => ['keywords', 'content.keywords'].map(suffix => `${prefix}.${suffix}`))
);

/**
* @param commaSeparatedKeywords: any number of either keyword arrays, or comma-separated keyword strings
* @returns an array with all unique keywords contained across all inputs
*/
export function mergeKeywords(...commaSeparatedKeywords) {
const keywords = new Set();
commaSeparatedKeywords
.filter(kwds => kwds)
.flatMap(kwds => Array.isArray(kwds) ? kwds : kwds.split(','))
.map(kw => kw.replace(/^\s*/, '').replace(/\s*$/, ''))
.filter(kw => kw)
.forEach(kw => keywords.add(kw));
return Array.from(keywords.keys());
}

/**
* Get an array with all keywords contained in an ortb2 object.
*/
export function getAllOrtbKeywords(ortb2, ...extraCommaSeparatedKeywords) {
return mergeKeywords(
...ORTB_KEYWORDS_PATHS.map(path => deepAccess(ortb2, path)),
...extraCommaSeparatedKeywords
)
}
3 changes: 2 additions & 1 deletion modules/adprimeBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js';
import { isFn, deepAccess, logMessage } from '../src/utils.js';
import { config } from '../src/config.js';
import { convertOrtbRequestToProprietaryNative } from '../src/native.js';
import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js';

const BIDDER_CODE = 'adprime';
const AD_URL = 'https://delta.adprime.com/pbjs';
Expand Down Expand Up @@ -128,7 +129,7 @@ export const spec = {
wPlayer: sizes ? sizes[0] : 0,
hPlayer: sizes ? sizes[1] : 0,
schain: bid.schain || {},
keywords: bid.params.keywords || [],
keywords: getAllOrtbKeywords(bidderRequest.ortb2, bid.params.keywords),
audiences: bid.params.audiences || [],
identeties,
bidFloor: getBidFloor(bid)
Expand Down
27 changes: 3 additions & 24 deletions modules/adrelevantisBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import {
isStr,
logError,
logMessage,
logWarn,
transformBidderParamKeywords
logWarn
} from '../src/utils.js';
import {config} from '../src/config.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
import {find, includes} from '../src/polyfill.js';
import {INSTREAM, OUTSTREAM} from '../src/video.js';
import { convertOrtbRequestToProprietaryNative } from '../src/native.js';
import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js';

const BIDDER_CODE = 'adrelevantis';
const URL = 'https://ssp.adrelevantis.com/prebid';
Expand Down Expand Up @@ -194,10 +194,6 @@ export const spec = {
params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false;
if (params.usePaymentRule) { delete params.usePaymentRule; }

if (isPopulatedArray(params.keywords)) {
params.keywords.forEach(deleteValues);
}

Object.keys(params).forEach(paramKey => {
let convertedKey = convertCamelToUnderscore(paramKey);
if (convertedKey !== paramKey) {
Expand All @@ -211,16 +207,6 @@ export const spec = {
}
};

function isPopulatedArray(arr) {
return !!(isArray(arr) && arr.length > 0);
}

function deleteValues(keyPairObj) {
if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') {
delete keyPairObj.value;
}
}

function formatRequest(payload, bidderRequest) {
let request = [];

Expand Down Expand Up @@ -475,14 +461,7 @@ function bidToTag(bid) {
if (bid.params.externalImpId) {
tag.external_imp_id = bid.params.externalImpId;
}
if (!isEmpty(bid.params.keywords)) {
let keywords = transformBidderParamKeywords(bid.params.keywords);

if (keywords.length > 0) {
keywords.forEach(deleteValues);
}
tag.keywords = keywords;
}
tag.keywords = getANKeywordParam(bid.ortb2, bid.params.keywords)
if (bid.params.category) {
tag.category = bid.params.category;
}
Expand Down
103 changes: 13 additions & 90 deletions modules/appnexusBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import {
logError,
logInfo,
logMessage,
logWarn,
mergeDeep,
transformBidderParamKeywords
logWarn
} from '../src/utils.js';
import {Renderer} from '../src/Renderer.js';
import {config} from '../src/config.js';
Expand All @@ -36,8 +34,14 @@ import {INSTREAM, OUTSTREAM} from '../src/video.js';
import {getStorageManager} from '../src/storageManager.js';
import {bidderSettings} from '../src/bidderSettings.js';
import {hasPurpose1Consent} from '../src/utils/gpdr.js';
import { convertOrtbRequestToProprietaryNative } from '../src/native.js';
import { APPNEXUS_CATEGORY_MAPPING } from '../libraries/categoryTranslationMapping/index.js';
import {convertOrtbRequestToProprietaryNative} from '../src/native.js';
import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js';
import {
convertKeywordStringToANMap,
getANKewyordParamFromMaps,
getANKeywordParam,
transformBidderParamKeywords
} from '../libraries/appnexusKeywords/anKeywords.js';

const BIDDER_CODE = 'appnexus';
const URL = 'https://ib.adnxs.com/ut/v3/prebid';
Expand Down Expand Up @@ -250,31 +254,12 @@ export const spec = {
payload.app = appIdObj;
}

function grabOrtb2Keywords(ortb2Obj) {
const fields = ['site.keywords', 'site.content.keywords', 'user.keywords', 'app.keywords', 'app.content.keywords'];
let result = [];

fields.forEach(path => {
let keyStr = deepAccess(ortb2Obj, path);
if (isStr(keyStr)) result.push(keyStr);
});
return result;
}

// grab the ortb2 keyword data (if it exists) and convert from the comma list string format to object format
let ortb2 = deepClone(bidderRequest && bidderRequest.ortb2);
let ortb2KeywordsObjList = grabOrtb2Keywords(ortb2).map(keyStr => convertStringToKeywordsObj(keyStr));

let anAuctionKeywords = deepClone(config.getConfig('appnexusAuctionKeywords')) || {};
// need to convert the string values into array of strings, to properly merge values with other existing keys later
Object.keys(anAuctionKeywords).forEach(k => { if (isStr(anAuctionKeywords[k]) || isNumber(anAuctionKeywords[k])) anAuctionKeywords[k] = [anAuctionKeywords[k]] });
// combine all sources of keywords (converted from string comma list to object format) into one object (that combines the values for shared keys)
let mergedAuctionKeywords = mergeDeep({}, anAuctionKeywords, ...ortb2KeywordsObjList);

// convert to final format used by adserver
let auctionKeywords = transformBidderParamKeywords(mergedAuctionKeywords);
let auctionKeywords = getANKeywordParam(ortb2, anAuctionKeywords)
if (auctionKeywords.length > 0) {
auctionKeywords.forEach(deleteValues);
payload.keywords = auctionKeywords;
}

Expand Down Expand Up @@ -468,10 +453,6 @@ export const spec = {
}, params);

if (isOpenRtb) {
if (isPopulatedArray(params.keywords)) {
params.keywords.forEach(deleteValues);
}

Object.keys(params).forEach(paramKey => {
let convertedKey = convertCamelToUnderscore(paramKey);
if (convertedKey !== paramKey) {
Expand All @@ -488,16 +469,6 @@ export const spec = {
}
};

function isPopulatedArray(arr) {
return !!(isArray(arr) && arr.length > 0);
}

function deleteValues(keyPairObj) {
if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') {
delete keyPairObj.value;
}
}

function strIsAppnexusViewabilityScript(str) {
if (!str || str === '') return false;

Expand Down Expand Up @@ -812,24 +783,9 @@ function bidToTag(bid) {
tag.external_imp_id = bid.params.external_imp_id;
}

let ortb2ImpKwStr = deepAccess(bid, 'ortb2Imp.ext.data.keywords');
if ((isStr(ortb2ImpKwStr) && ortb2ImpKwStr !== '') || !isEmpty(bid.params.keywords)) {
// convert ortb2 from comma list string format to bid param object format
let ortb2ImpKwObj = convertStringToKeywordsObj(ortb2ImpKwStr);

let bidParamsKwObj = (isPlainObject(bid.params.keywords)) ? deepClone(bid.params.keywords) : {};
// need to convert the string values into an array of strings, to properly merge values with other existing keys later
Object.keys(bidParamsKwObj).forEach(k => { if (isStr(bidParamsKwObj[k]) || isNumber(bidParamsKwObj[k])) bidParamsKwObj[k] = [bidParamsKwObj[k]] });

// combine both sources of keywords into one merged object (that combines the values for shared keys)
let keywordsObj = mergeDeep({}, bidParamsKwObj, ortb2ImpKwObj);

// convert to final format used by adserver
let keywordsUt = transformBidderParamKeywords(keywordsObj);
if (keywordsUt.length > 0) {
keywordsUt.forEach(deleteValues);
tag.keywords = keywordsUt;
}
const auKeywords = getANKewyordParamFromMaps(convertKeywordStringToANMap(deepAccess(bid, 'ortb2Imp.ext.data.keywords')), bid.params?.keywords);
if (auKeywords.length > 0) {
tag.keywords = auKeywords;
}

let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot');
Expand Down Expand Up @@ -1258,37 +1214,4 @@ function convertKeywordsToString(keywords) {
return result;
}

// converts a comma separated list of keywords into the standard keyword object format used in appnexus bid params
// 'genre=rock,genre=pop,pets=dog,music' goes to { 'genre': ['rock', 'pop'], 'pets': ['dog'], 'music': [''] }
function convertStringToKeywordsObj(keyStr) {
let result = {};

if (isStr(keyStr) && keyStr !== '') {
// will split based on commas and will eat white space before/after the comma
let keywordList = keyStr.split(/\s*(?:,)\s*/);
keywordList.forEach(kw => {
// if = exists, then split
if (kw.indexOf('=') !== -1) {
let kwPair = kw.split('=');
let key = kwPair[0];
let val = kwPair[1];

// then check for existing key in result > if so add value to the array > if not, add new key and create value array
if (result.hasOwnProperty(key)) {
result[key].push(val);
} else {
result[key] = [val];
}
} else {
// make a key with '' value; if key already exists > don't add
if (!result.hasOwnProperty(kw)) {
result[kw] = [''];
}
}
});
}

return result;
}

registerBidder(spec);
Loading

0 comments on commit 5868d29

Please sign in to comment.