Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Appnexus Bid Adapter: add support to read ortb2 keywords #8939

Merged
merged 1 commit into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 73 additions & 12 deletions modules/appnexusBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
logInfo,
logMessage,
logWarn,
mergeDeep,
transformBidderParamKeywords,
getWindowFromDocument
} from '../src/utils.js';
Expand Down Expand Up @@ -229,15 +230,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 @@ -843,13 +861,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 @@ -1247,4 +1277,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'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you consider gathering this

deepSetValue(bid, 'params.keywords.permutive', data.appnexus)
and
const writeToLegacySiteKeywords = LEGACY_SITE_KEYWORDS_BIDDERS.includes(bidder);
and
deepSetValue(bid, 'params.keywords.perid', audiences || []);
in scope for this change?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these rtd modules bypass the "normal" workflow that the data controller module is able to edit

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they're populating the values under the bid.params.keywords before our request goes out, it should be gathered automatically by the logic we have already? I'm not sure how else to pre-emptively gather the data from their systems.

If they want to change their setup to have the publisher populate the ortb2 keyword fields with their data, then I suppose their customizations shouldn't be needed (if that's what you meant by 'in scope for this change').

Copy link
Collaborator

@patmmccann patmmccann Sep 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These rtd modules typically write contextual segments to site.content.data and user segments to user.data. It is in those locations that the data controller module is able to edit the segments down if necessary. We believe, since the appnexus adapter does not support either of these inputs and people seem to be using params.keywords in their place, that appnexus adapter should be responsible for copying the segments from the standard locations to the keywords instead of these modules each having special appnexus handling. It is totally reasonable to consider that out of scope however, or perhaps it is easiest to combine in this change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as reference: #8614 (review)

Copy link
Collaborator Author

@jsnellbaker jsnellbaker Sep 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you transform the typical segment objects into key-value pairs; I'm looking at the samples in the first party data from the docs, and I don't see exactly how things should be pulled together?

Is this really something that would okay to implement for everyone who potentially has this data in these fields, as opposed to just doing it for these rtd modules?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it isn't obvious how this should be done, and each of these adapters seems to have made slightly different choices to support Xandr

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone change suggest a standard approach, we can take a look and consider. If it's going to take time, then maybe it can be a future PR and we just have this PR focus on the keywords fields specifically.

}]);

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