Skip to content

Commit

Permalink
appnexus bid adapter - add support to read ortb2 keywords (prebid#8939)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsnellbaker authored and jorgeluisrocha committed May 18, 2023
1 parent 3388784 commit 9b0f6c1
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 18 deletions.
85 changes: 73 additions & 12 deletions modules/appnexusBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
logInfo,
logMessage,
logWarn,
mergeDeep,
transformBidderParamKeywords,
getWindowFromDocument
} from '../src/utils.js';
Expand Down Expand Up @@ -230,15 +231,32 @@ export const spec = {
payload.app = appIdObj;
}

let auctionKeywords = config.getConfig('appnexusAuctionKeywords');
if (isPlainObject(auctionKeywords)) {
let aucKeywords = transformBidderParamKeywords(auctionKeywords);
function grabOrtb2Keywords(ortb2Obj) {
const fields = ['site.keywords', 'site.content.keywords', 'user.keywords', 'app.keywords', 'app.content.keywords'];
let result = [];

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

payload.keywords = aucKeywords;
// 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 mergedAuctionKeywrds = mergeDeep({}, anAuctionKeywords, ...ortb2KeywordsObjList);

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

if (config.getConfig('adpod.brandCategoryExclusion')) {
Expand Down Expand Up @@ -763,13 +781,25 @@ 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);
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;
}
tag.keywords = keywords;
}

let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot');
Expand Down Expand Up @@ -1167,4 +1197,35 @@ 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 = {};

// 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);
67 changes: 61 additions & 6 deletions test/spec/modules/appnexusBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,18 +610,39 @@ describe('AppNexusAdapter', function () {
config.getConfig.restore();
});

it('adds auction level keywords to request when set', function () {
it('adds auction level keywords and ortb2 keywords to request when set', function () {
let bidRequest = Object.assign({}, bidRequests[0]);
sinon
.stub(config, 'getConfig')
.withArgs('appnexusAuctionKeywords')
.returns({
gender: 'm',
music: ['rock', 'pop'],
test: ''
test: '',
tools: 'power'
});

const request = spec.buildRequests([bidRequest]);
const bidderRequest = {
ortb2: {
site: {
keywords: 'power tools, drills, tools=industrial',
content: {
keywords: 'video, source=streaming'
}
},
user: {
keywords: 'tools=home,renting'
},
app: {
keywords: 'app=iphone 11',
content: {
keywords: 'appcontent=home repair, dyi'
}
}
}
};

const request = spec.buildRequests([bidRequest], bidderRequest);
const payload = JSON.parse(request.data);

expect(payload.keywords).to.deep.equal([{
Expand All @@ -632,6 +653,28 @@ describe('AppNexusAdapter', function () {
'value': ['rock', 'pop']
}, {
'key': 'test'
}, {
'key': 'tools',
'value': ['power', 'industrial', 'home']
}, {
'key': 'power tools'
}, {
'key': 'drills'
}, {
'key': 'video'
}, {
'key': 'source',
'value': ['streaming']
}, {
'key': 'renting'
}, {
'key': 'app',
'value': ['iphone 11']
}, {
'key': 'appcontent',
'value': ['home repair']
}, {
'key': 'dyi'
}]);

config.getConfig.restore();
Expand Down Expand Up @@ -714,7 +757,7 @@ describe('AppNexusAdapter', function () {
});
}

it('should convert keyword params to proper form and attaches to request', function () {
it('should convert keyword params and adUnit ortb2 keywords to proper form and attaches to request', function () {
let bidRequest = Object.assign({},
bidRequests[0],
{
Expand All @@ -730,6 +773,13 @@ describe('AppNexusAdapter', function () {
emptyArr: [''],
badValue: { 'foo': 'bar' } // should be dropped
}
},
ortb2Imp: {
ext: {
data: {
keywords: 'ortb2=yes,ortb2test, multiValMixed=4, singleValNum=456'
}
}
}
}
);
Expand All @@ -748,14 +798,19 @@ describe('AppNexusAdapter', function () {
'value': ['5']
}, {
'key': 'multiValMixed',
'value': ['value1', '2', 'value3']
'value': ['value1', '2', 'value3', '4']
}, {
'key': 'singleValNum',
'value': ['123']
'value': ['123', '456']
}, {
'key': 'emptyStr'
}, {
'key': 'emptyArr'
}, {
'key': 'ortb2',
'value': ['yes']
}, {
'key': 'ortb2test'
}]);
});

Expand Down

0 comments on commit 9b0f6c1

Please sign in to comment.