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

Adnuntius Bid Adapter: standardise kv targeting value handling when sent to adserver #11721

Merged
merged 1 commit into from
Jun 9, 2024
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
22 changes: 11 additions & 11 deletions modules/adnuntiusBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import { isStr, deepAccess } from '../src/utils.js';
import {isStr, isEmpty, deepAccess, getUnixTimestampFromNow, convertObjectToArray} from '../src/utils.js';
import { config } from '../src/config.js';
import { getStorageManager } from '../src/storageManager.js';

Expand All @@ -19,10 +19,6 @@ const METADATA_KEY = 'adn.metaData';
const METADATA_KEY_SEPARATOR = '@@@';

export const misc = {
getUnixTimestamp: function (addDays, asMinutes) {
const multiplication = addDays / (asMinutes ? 1440 : 1);
return Date.now() + (addDays && addDays > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0);
}
};

const storageTool = (function () {
Expand Down Expand Up @@ -50,11 +46,11 @@ const storageTool = (function () {
if (datum.key === 'voidAuIds' && Array.isArray(datum.value)) {
return true;
}
return datum.key && datum.value && datum.exp && datum.exp > misc.getUnixTimestamp() && (!network || network === datum.network);
return datum.key && datum.value && datum.exp && datum.exp > getUnixTimestampFromNow() && (!network || network === datum.network);
}) : [];
const voidAuIdsEntry = filteredEntries.find(entry => entry.key === 'voidAuIds');
if (voidAuIdsEntry) {
const now = misc.getUnixTimestamp();
const now = getUnixTimestampFromNow();
voidAuIdsEntry.value = voidAuIdsEntry.value.filter(voidAuId => voidAuId.auId && voidAuId.exp > now);
if (!voidAuIdsEntry.value.length) {
filteredEntries = filteredEntries.filter(entry => entry.key !== 'voidAuIds');
Expand All @@ -73,7 +69,7 @@ const storageTool = (function () {
const notNewExistingAuIds = currentVoidAuIds.filter(auIdObj => {
return newAuIds.indexOf(auIdObj.value) < -1;
}) || [];
const oneDayFromNow = misc.getUnixTimestamp(1);
const oneDayFromNow = getUnixTimestampFromNow(1);
const apiIdsArray = newAuIds.map(auId => {
return { exp: oneDayFromNow, auId: auId };
}) || [];
Expand All @@ -86,7 +82,7 @@ const storageTool = (function () {
if (key !== 'voidAuIds') {
metaAsObj[key + METADATA_KEY_SEPARATOR + network] = {
value: apiRespMetadata[key],
exp: misc.getUnixTimestamp(100),
exp: getUnixTimestampFromNow(100),
network: network
}
}
Expand Down Expand Up @@ -201,10 +197,14 @@ const targetingTool = (function() {
},
mergeKvsFromOrtb: function(bidTargeting, bidderRequest) {
const kv = getKvsFromOrtb(bidderRequest || {});
if (!kv) {
if (isEmpty(kv)) {
return;
}
bidTargeting.kv = {...kv, ...bidTargeting.kv};
if (bidTargeting.kv && !Array.isArray(bidTargeting.kv)) {
bidTargeting.kv = convertObjectToArray(bidTargeting.kv);
}
bidTargeting.kv = bidTargeting.kv || [];
bidTargeting.kv = bidTargeting.kv.concat(convertObjectToArray(kv));
}
}
})();
Expand Down
27 changes: 27 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,33 @@ export function memoize(fn, key = function (arg) { return arg; }) {
return memoized;
}

/**
* Returns a Unix timestamp for given time value and unit.
* @param {number} timeValue numeric value, defaults to 0 (which means now)
* @param {string} timeUnit defaults to days (or 'd'), use 'm' for minutes. Any parameter that isn't 'd' or 'm' will return Date.now().
* @returns {number}
*/
export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') {
const acceptableUnits = ['m', 'd'];
if (acceptableUnits.indexOf(timeUnit) < 0) {
return Date.now();
}
const multiplication = timeValue / (timeUnit === 'm' ? 1440 : 1);
return Date.now() + (timeValue && timeValue > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0);
}

/**
* Converts given object into an array, so {key: 1, anotherKey: 'fred', third: ['fred']} is turned
* into [{key: 1}, {anotherKey: 'fred'}, {third: ['fred']}]
* @param {Object} obj the object
* @returns {Array}
*/
export function convertObjectToArray(obj) {
return Object.keys(obj).map(key => {
return {[key]: obj[key]};
});
}

/**
* Sets dataset attributes on a script
* @param {HTMLScriptElement} script
Expand Down
115 changes: 99 additions & 16 deletions test/spec/modules/adnuntiusBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { config } from 'src/config.js';
import * as utils from 'src/utils.js';
import { getStorageManager } from 'src/storageManager.js';
import { getGlobal } from '../../../src/prebidGlobal';
import {getUnixTimestampFromNow} from 'src/utils.js';

describe('adnuntiusBidAdapter', function () {
const URL = 'https://ads.adnuntius.delivery/i?tzo=';
const EURO_URL = 'https://europe.delivery.adnuntius.com/i?tzo=';
const usi = utils.generateUUID()

const meta = [{ key: 'valueless' }, { value: 'keyless' }, { key: 'voidAuIds' }, { key: 'voidAuIds', value: [{ auId: '11118b6bc', exp: misc.getUnixTimestamp() }, { exp: misc.getUnixTimestamp(1) }] }, { key: 'valid-withnetwork', value: 'also-valid-network', network: 'the-network', exp: misc.getUnixTimestamp(1) }, { key: 'valid', value: 'also-valid', exp: misc.getUnixTimestamp(1) }, { key: 'expired', value: 'fwefew', exp: misc.getUnixTimestamp() }, { key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp(), network: 'adnuntius' }, { key: 'usi', value: usi, exp: misc.getUnixTimestamp(100), network: 'adnuntius' }, { key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp() }]
const meta = [{ key: 'valueless' }, { value: 'keyless' }, { key: 'voidAuIds' }, { key: 'voidAuIds', value: [{ auId: '11118b6bc', exp: getUnixTimestampFromNow() }, { exp: getUnixTimestampFromNow(1) }] }, { key: 'valid-withnetwork', value: 'also-valid-network', network: 'the-network', exp: getUnixTimestampFromNow(1) }, { key: 'valid', value: 'also-valid', exp: getUnixTimestampFromNow(1) }, { key: 'expired', value: 'fwefew', exp: getUnixTimestampFromNow() }, { key: 'usi', value: 'should be skipped because timestamp', exp: getUnixTimestampFromNow(), network: 'adnuntius' }, { key: 'usi', value: usi, exp: getUnixTimestampFromNow(100), network: 'adnuntius' }, { key: 'usi', value: 'should be skipped because timestamp', exp: getUnixTimestampFromNow() }]
let storage;

// need this to make the restore work correctly -- something to do with stubbing static prototype methods
Expand Down Expand Up @@ -505,7 +506,7 @@ describe('adnuntiusBidAdapter', function () {
});

it('Test request changes for voided au ids', function () {
storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{ key: 'voidAuIds', value: [{ auId: '11118b6bc', exp: misc.getUnixTimestamp(1) }, { auId: '0000000000000023', exp: misc.getUnixTimestamp(1) }] }]));
storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{ key: 'voidAuIds', value: [{ auId: '11118b6bc', exp: getUnixTimestampFromNow(1) }, { auId: '0000000000000023', exp: getUnixTimestampFromNow(1) }] }]));
const bRequests = bidderRequests.concat([{
bidId: 'adn-11118b6bc',
bidder: 'adnuntius',
Expand Down Expand Up @@ -592,14 +593,20 @@ describe('adnuntiusBidAdapter', function () {
delete bidderRequests[0].params.targeting;
});

function countMatches(actualArray, expectedValue) {
return actualArray.filter(val => {
return JSON.stringify(val) === JSON.stringify(expectedValue);
}).length;
}

it('should pass site data ext as key values to ad server', function () {
const ortb2 = {
site: {
ext: {
data: {
'12345': 'true',
'45678': 'true',
'9090': 'should-be-overwritten'
'9090': 'should-be-retained'
}
}
}
Expand All @@ -614,26 +621,102 @@ describe('adnuntiusBidAdapter', function () {
expect(request.length).to.equal(1);
expect(request[0]).to.have.property('url')
const data = JSON.parse(request[0].data);
expect(data.adUnits[0].kv['12345']).to.equal('true');
expect(data.adUnits[0].kv['45678']).to.equal('true');
expect(data.adUnits[0].kv['9090'][0]).to.equal('take it over');
expect(data.adUnits[0].kv['merge'][0]).to.equal('this');
expect(countMatches(data.adUnits[0].kv, {'9090': ['take it over']})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'merge': ['this']})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'9090': 'should-be-retained'})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'45678': 'true'})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'12345': 'true'})).to.equal(1);
expect(data.adUnits[0].kv.length).to.equal(5);

delete bidderRequests[0].params.targeting;
});

it('should skip passing site data ext if missing', function () {
it('should pass site data ext as key values to ad server with targeting in different format', function () {
const ortb2 = {
site: {
ext: {
data: {
'12345': 'true',
'45678': 'true',
'9090': 'should-be-retained'
}
}
}
};
bidderRequests[0].params.targeting = {
kv: [
{'merge': ['this']},
{'9090': ['take it over']}
]
};
const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 }));
expect(request.length).to.equal(1);
expect(request[0]).to.have.property('url')
const data = JSON.parse(request[0].data);
expect(countMatches(data.adUnits[0].kv, {'9090': ['take it over']})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'merge': ['this']})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'9090': 'should-be-retained'})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'45678': 'true'})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'12345': 'true'})).to.equal(1);
expect(data.adUnits[0].kv.length).to.equal(5);

delete bidderRequests[0].params.targeting;
});

it('should pass site data ext as key values to ad server even if no kv targeting specified in params.targeting', function () {
const ortb2 = {
site: {
ext: {
data: {
'12345': 'true',
'45678': 'true',
'9090': 'should-be-retained'
}
}
}
};
const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 }));
expect(request.length).to.equal(1);
expect(request[0]).to.have.property('url')
const data = JSON.parse(request[0].data);
expect(countMatches(data.adUnits[0].kv, {'9090': 'should-be-retained'})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'45678': 'true'})).to.equal(1);
expect(countMatches(data.adUnits[0].kv, {'12345': 'true'})).to.equal(1);
expect(data.adUnits[0].kv.length).to.equal(3);

delete bidderRequests[0].params.targeting;
});

it('should skip passing site ext if missing', function () {
const ortb2 = {
site: {
ext: {
}
}
};

delete bidderRequests[0].params.targeting;
const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 }));
expect(request.length).to.equal(1);
expect(request[0]).to.have.property('url');
const data = JSON.parse(request[0].data);
expect(data.adUnits[0]).to.not.have.property('kv');
});

it('should skip passing site ext data if missing', function () {
const ortb2 = {
site: {
ext: {
data: {}
}
}
};

delete bidderRequests[0].params.targeting;
const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidderRequests, { ortb2 }));
expect(request.length).to.equal(1);
expect(request[0]).to.have.property('url');
const data = JSON.parse(request[0].data);
expect(data.adUnits[0]).to.not.have.property('kv');
});

Expand Down Expand Up @@ -985,30 +1068,30 @@ describe('adnuntiusBidAdapter', function () {
const usiEntry = results.find(entry => entry.key === 'usi' && entry.network === 'some-network-id');
expect(usiEntry.key).to.equal('usi');
expect(usiEntry.value).to.equal('from-api-server dude');
expect(usiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90));
expect(usiEntry.exp).to.be.greaterThan(getUnixTimestampFromNow(90));
expect(usiEntry.network).to.equal('some-network-id')

const voidAuIdsEntry = results.find(entry => entry.key === 'voidAuIds');
expect(voidAuIdsEntry.key).to.equal('voidAuIds');
expect(voidAuIdsEntry.exp).to.equal(undefined);
expect(voidAuIdsEntry.value[0].auId).to.equal('00000000000abcde');
expect(voidAuIdsEntry.value[0].exp).to.be.greaterThan(misc.getUnixTimestamp());
expect(voidAuIdsEntry.value[0].exp).to.be.lessThan(misc.getUnixTimestamp(2));
expect(voidAuIdsEntry.value[0].exp).to.be.greaterThan(getUnixTimestampFromNow());
expect(voidAuIdsEntry.value[0].exp).to.be.lessThan(getUnixTimestampFromNow(2));
expect(voidAuIdsEntry.value[1].auId).to.equal('00000000000fffff');
expect(voidAuIdsEntry.value[1].exp).to.be.greaterThan(misc.getUnixTimestamp());
expect(voidAuIdsEntry.value[1].exp).to.be.lessThan(misc.getUnixTimestamp(2));
expect(voidAuIdsEntry.value[1].exp).to.be.greaterThan(getUnixTimestampFromNow());
expect(voidAuIdsEntry.value[1].exp).to.be.lessThan(getUnixTimestampFromNow(2));

const validEntry = results.find(entry => entry.key === 'valid');
expect(validEntry.key).to.equal('valid');
expect(validEntry.value).to.equal('also-valid');
expect(validEntry.exp).to.be.greaterThan(misc.getUnixTimestamp());
expect(validEntry.exp).to.be.lessThan(misc.getUnixTimestamp(2));
expect(validEntry.exp).to.be.greaterThan(getUnixTimestampFromNow());
expect(validEntry.exp).to.be.lessThan(getUnixTimestampFromNow(2));

const randomApiEntry = results.find(entry => entry.key === 'randomApiKey');
expect(randomApiEntry.key).to.equal('randomApiKey');
expect(randomApiEntry.value).to.equal('randomApiValue');
expect(randomApiEntry.network).to.equal('some-network-id');
expect(randomApiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90));
expect(randomApiEntry.exp).to.be.greaterThan(getUnixTimestampFromNow(90));
});

it('should not process valid response when passed alt bidder that is an adndeal', function () {
Expand Down
38 changes: 38 additions & 0 deletions test/spec/utils_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,44 @@ describe('Utils', function () {
});
});

describe('getUnixTimestampFromNow', () => {
it('correctly obtains unix timestamp', () => {
const nowValue = new Date('2024-01-01').valueOf();
sinon.stub(Date, 'now').returns(nowValue);
let val = utils.getUnixTimestampFromNow();
expect(val).equal(nowValue);

val = utils.getUnixTimestampFromNow(1);
expect(val).equal(nowValue + (1000 * 60 * 60 * 24));

val = utils.getUnixTimestampFromNow(1, 'd');
expect(val).equal(nowValue + (1000 * 60 * 60 * 24));

val = utils.getUnixTimestampFromNow(1, 'm');
expect(val).equal(nowValue + (1000 * 60 * 60 * 24 / 1440));

val = utils.getUnixTimestampFromNow(2, 'm');
expect(val).equal(nowValue + (1000 * 60 * 60 * 24 * 2 / 1440));

// any value that isn't 'm' or 'd' gets treated as Date.now();
val = utils.getUnixTimestampFromNow(10, 'o');
expect(val).equal(nowValue);
});
});

describe('convertObjectToArray', () => {
it('correctly converts object to array', () => {
const obj = {key: 1, anotherKey: 'fred', third: ['fred'], fourth: {sub: {obj: 'test'}}};
const array = utils.convertObjectToArray(obj);

expect(JSON.stringify(array[0])).equal(JSON.stringify({'key': 1}))
expect(JSON.stringify(array[1])).equal(JSON.stringify({'anotherKey': 'fred'}))
expect(JSON.stringify(array[2])).equal(JSON.stringify({'third': ['fred']}))
expect(JSON.stringify(array[3])).equal(JSON.stringify({'fourth': {sub: {obj: 'test'}}}));
expect(array.length).to.equal(4);
});
});

describe('setScriptAttributes', () => {
it('correctly adds attributes from an object', () => {
const script = document.createElement('script'),
Expand Down