diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 948e1c31b17..1d69e69f555 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -22,6 +22,7 @@ import { logInfo, logMessage, logWarn, + mergeDeep, transformBidderParamKeywords, getWindowFromDocument } from '../src/utils.js'; @@ -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')) { @@ -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'); @@ -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); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 6f00d851a94..211c0d64e67 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -610,7 +610,7 @@ 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') @@ -618,10 +618,31 @@ describe('AppNexusAdapter', function () { .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([{ @@ -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(); @@ -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], { @@ -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' + } + } } } ); @@ -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' }]); });