From 95e400f222fd16fe117822e44b9f72a950b1fd39 Mon Sep 17 00:00:00 2001 From: Antonios Sarhanis Date: Thu, 6 Jun 2024 14:40:08 +1000 Subject: [PATCH] Update how key-values are passed into ad server --- modules/adnuntiusBidAdapter.js | 22 ++-- src/utils.js | 27 ++++ test/spec/modules/adnuntiusBidAdapter_spec.js | 115 +++++++++++++++--- test/spec/utils_spec.js | 38 ++++++ 4 files changed, 175 insertions(+), 27 deletions(-) diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index 189e2287571..52c5499b0b8 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -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'; @@ -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 () { @@ -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'); @@ -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 }; }) || []; @@ -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 } } @@ -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)); } } })(); diff --git a/src/utils.js b/src/utils.js index 2affc52ab8c..be22d4a82a9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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 diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index 4044e62280a..d0d16d0f399 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -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 @@ -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', @@ -592,6 +593,12 @@ 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: { @@ -599,7 +606,7 @@ describe('adnuntiusBidAdapter', function () { data: { '12345': 'true', '45678': 'true', - '9090': 'should-be-overwritten' + '9090': 'should-be-retained' } } } @@ -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'); }); @@ -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 () { diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index d7d7007674a..aaeeee48d83 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -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'),