Skip to content

Commit

Permalink
IAS RTD adapter: improve workflow (prebid#7431)
Browse files Browse the repository at this point in the history
  • Loading branch information
raguilar-ias authored and Chris Pabst committed Jan 10, 2022
1 parent 31d3f3c commit eabfa8a
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 73 deletions.
150 changes: 83 additions & 67 deletions modules/iasRtdProvider.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { isArray, getAdUnitSizes, getKeys, logError } from '../src/utils.js';
import { submodule } from '../src/hook.js';
import * as utils from '../src/utils.js';
import { ajax } from '../src/ajax.js';
import { getGlobal } from '../src/prebidGlobal.js';
import { ajaxBuilder } from '../src/ajax.js';

/** @type {string} */
const MODULE_NAME = 'realTimeData';
const SUBMODULE_NAME = 'ias';

let bidResponses = {};
const IAS_HOST = 'https://pixel.adsafeprotected.com/services/pub';
export let iasTargeting = {};
const BRAND_SAFETY_OBJECT_FIELD_NAME = 'brandSafety';
const FRAUD_FIELD_NAME = 'fr';
const SLOTS_OBJECT_FIELD_NAME = 'slots';
const CUSTOM_FIELD_NAME = 'custom';
const IAS_KW = 'ias-kw';

/**
* Module init
Expand All @@ -16,12 +21,17 @@ let bidResponses = {};
* @return {boolean}
*/
export function init(config, userConsent) {
const params = config.params;
if (!params || !params.pubId) {
utils.logError('missing pubId param for IAS provider');
return false;
}
return true;
}

function stringifySlotSizes(sizes) {
let result = '';
if (isArray(sizes)) {
if (utils.isArray(sizes)) {
result = sizes.reduce((acc, size) => {
acc.push(size.join('.'));
return acc;
Expand All @@ -31,13 +41,14 @@ function stringifySlotSizes(sizes) {
return result;
}

function stringifySlot(bidRequest, adUnitPath) {
const sizes = getAdUnitSizes(bidRequest);
function stringifySlot(bidRequest) {
const sizes = utils.getAdUnitSizes(bidRequest);
const id = bidRequest.code;
const ss = stringifySlotSizes(sizes);
const p = bidRequest.code;
const adSlot = utils.getGptSlotInfoForAdUnitCode(bidRequest.code);
const p = utils.isEmpty(adSlot) ? bidRequest.code : adSlot.gptSlot;
const slot = { id, ss, p };
const keyValues = getKeys(slot).map(function (key) {
const keyValues = utils.getKeys(slot).map(function (key) {
return [key, slot[key]].join(':');
});
return '{' + keyValues.join(',') + '}';
Expand All @@ -51,94 +62,99 @@ function stringifyScreenSize() {
return [(window.screen && window.screen.width) || -1, (window.screen && window.screen.height) || -1].join('.');
}

function getPageLevelKeywords(response) {
function formatTargetingData(adUnit) {
let result = {};
if (response.brandSafety) {
shallowMerge(result, response.brandSafety);
if (iasTargeting[BRAND_SAFETY_OBJECT_FIELD_NAME]) {
utils.mergeDeep(result, iasTargeting[BRAND_SAFETY_OBJECT_FIELD_NAME]);
}
if (iasTargeting[FRAUD_FIELD_NAME]) {
result[FRAUD_FIELD_NAME] = iasTargeting[FRAUD_FIELD_NAME];
}
if (iasTargeting[CUSTOM_FIELD_NAME] && IAS_KW in iasTargeting[CUSTOM_FIELD_NAME]) {
result[IAS_KW] = iasTargeting[CUSTOM_FIELD_NAME][IAS_KW];
}
if (iasTargeting[SLOTS_OBJECT_FIELD_NAME] && adUnit in iasTargeting[SLOTS_OBJECT_FIELD_NAME]) {
utils.mergeDeep(result, iasTargeting[SLOTS_OBJECT_FIELD_NAME][adUnit]);
}
result.fr = response.fr;
result.custom = response.custom;
return result;
}

function shallowMerge(dest, src) {
getKeys(src).reduce((dest, srcKey) => {
dest[srcKey] = src[srcKey];
return dest;
}, dest);
}

function getBidRequestData(reqBidsConfigObj, callback, config) {
const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits;
let isFinish = false;

const IAS_HOST = 'https://pixel.adsafeprotected.com/services/pub';
const { pubId, adUnitPath } = config.params;
const anId = pubId;
function constructQueryString(anId, adUnits) {
let queries = [];
queries.push(['anId', anId]);

queries = queries.concat(adUnits.reduce(function (acc, request) {
acc.push(['slot', stringifySlot(request, adUnitPath)]);
acc.push(['slot', stringifySlot(request)]);
return acc;
}, []));

queries.push(['wr', stringifyWindowSize()]);
queries.push(['sr', stringifyScreenSize()]);
queries.push(['url', encodeURIComponent(window.location.href)]);

const queryString = encodeURI(queries.map(qs => qs.join('=')).join('&'));

const ajax = ajaxBuilder();

ajax(`${IAS_HOST}?${queryString}`, {
success: function (response, request) {
if (!isFinish) {
if (request.status === 200) {
const iasResponse = JSON.parse(response);
const commonBidResponse = {};
shallowMerge(commonBidResponse, getPageLevelKeywords(iasResponse));
commonBidResponse.slots = iasResponse.slots;
bidResponses = commonBidResponse;
adUnits.forEach(adUnit => {
adUnit.bids.forEach(bid => {
const rtd = bid.rtd || {};
const iasRtd = {};
iasRtd[SUBMODULE_NAME] = Object.assign({}, rtd[SUBMODULE_NAME], bidResponses);
bid.rtd = Object.assign({}, rtd, iasRtd);
});
});
}
isFinish = true;
}
callback();
},
error: function () {
logError('failed to retrieve targeting information');
callback();
}
});
return encodeURI(queries.map(qs => qs.join('=')).join('&'));
}

function parseResponse(result) {
let iasResponse = {};
try {
iasResponse = JSON.parse(result);
} catch (err) {
utils.logError('error', err);
}
iasTargeting = iasResponse;
}

function getTargetingData(adUnits, config, userConsent) {
const targeting = {};
Object.keys(bidResponses).forEach(key => bidResponses[key] === undefined ? delete bidResponses[key] : {});
try {
adUnits.forEach(function(adUnit) {
targeting[adUnit] = bidResponses;
});
if (!utils.isEmpty(iasTargeting)) {
adUnits.forEach(function (adUnit) {
targeting[adUnit] = formatTargetingData(adUnit);
});
}
} catch (err) {
logError('error', err);
utils.logError('error', err);
}
utils.logInfo('IAS targeting', targeting);
return targeting;
}

export function getApiCallback() {
return {
success: function (response, req) {
if (req.status === 200) {
try {
parseResponse(response);
} catch (e) {
utils.logError('Unable to parse IAS response.', e);
}
}
},
error: function () {
utils.logError('failed to retrieve IAS data');
}
}
}

function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) {
const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits;
const { pubId } = config.params;
const queryString = constructQueryString(pubId, adUnits);
ajax(
`${IAS_HOST}?${queryString}`,
getApiCallback(),
undefined,
{ method: 'GET' }
);
}

/** @type {RtdSubmodule} */
export const iasSubModule = {
name: SUBMODULE_NAME,
init: init,
getBidRequestData: getBidRequestData,
getTargetingData: getTargetingData
getTargetingData: getTargetingData,
getBidRequestData: getBidRequestData
};

submodule(MODULE_NAME, iasSubModule);
77 changes: 71 additions & 6 deletions test/spec/modules/iasRtdProvider_spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { iasSubModule } from 'modules/iasRtdProvider.js';
import { iasSubModule, iasTargeting } from 'modules/iasRtdProvider.js';
import { expect } from 'chai';
import { server } from 'test/mocks/xhr.js';

const responseHeader = { 'Content-Type': 'application/json' };

describe('iasRtdProvider is a RTD provider that', function () {
it('has the correct module name', function () {
expect(iasSubModule.name).to.equal('ias');
Expand All @@ -10,8 +12,33 @@ describe('iasRtdProvider is a RTD provider that', function () {
it('exists', function () {
expect(iasSubModule.init).to.be.a('function');
});
it('returns true', function () {
expect(iasSubModule.init()).to.equal(true);
it('returns false missing config params', function () {
const config = {
name: 'ias',
waitForIt: true,
};
const value = iasSubModule.init(config);
expect(value).to.equal(false);
});
it('returns false missing pubId param', function () {
const config = {
name: 'ias',
waitForIt: true,
params: {}
};
const value = iasSubModule.init(config);
expect(value).to.equal(false);
});
it('returns false missing pubId param', function () {
const config = {
name: 'ias',
waitForIt: true,
params: {
pubId: '123456'
}
};
const value = iasSubModule.init(config);
expect(value).to.equal(true);
});
});
describe('has a method `getBidRequestData` that', function () {
Expand All @@ -30,10 +57,17 @@ describe('iasRtdProvider is a RTD provider that', function () {
const adUnitsOriginal = adUnits;
iasSubModule.getBidRequestData({ adUnits: adUnits }, callback, config);
request = server.requests[0];
server.respond();
request.respond(200, responseHeader, JSON.stringify(data));
expect(request.url).to.be.include(`https://pixel.adsafeprotected.com/services/pub?anId=1234`);
expect(adUnits).to.length(2);
expect(adUnits[0]).to.be.eq(adUnitsOriginal[0]);
const targetingKeys = Object.keys(iasTargeting);
const dataKeys = Object.keys(data);
expect(targetingKeys.length).to.equal(dataKeys.length);
expect(targetingKeys['fr']).to.be.eq(dataKeys['fr']);
expect(targetingKeys['brandSafety']).to.be.eq(dataKeys['brandSafety']);
expect(targetingKeys['ias-kw']).to.be.eq(dataKeys['ias-kw']);
expect(targetingKeys['slots']).to.be.eq(dataKeys['slots']);
});
});

Expand All @@ -42,10 +76,32 @@ describe('iasRtdProvider is a RTD provider that', function () {
expect(iasSubModule.getTargetingData).to.be.a('function');
});
it('invoke method', function () {
const targeting = iasSubModule.getTargetingData(adUnits, config);
expect(adUnits).to.length(2);
const targeting = iasSubModule.getTargetingData(adUnitsCode, config);
expect(adUnitsCode).to.length(2);
expect(targeting).to.be.not.null;
expect(targeting).to.be.not.empty;
expect(targeting['one-div-id']).to.be.not.null;
const targetingKeys = Object.keys(targeting['one-div-id']);
expect(targetingKeys.length).to.equal(10);
expect(targetingKeys['adt']).to.be.not.null;
expect(targetingKeys['alc']).to.be.not.null;
expect(targetingKeys['dlm']).to.be.not.null;
expect(targetingKeys['drg']).to.be.not.null;
expect(targetingKeys['hat']).to.be.not.null;
expect(targetingKeys['off']).to.be.not.null;
expect(targetingKeys['vio']).to.be.not.null;
expect(targetingKeys['fr']).to.be.not.null;
expect(targetingKeys['ias-kw']).to.be.not.null;
expect(targetingKeys['id']).to.be.not.null;
expect(targeting['one-div-id']['adt']).to.be.eq('veryLow');
expect(targeting['one-div-id']['alc']).to.be.eq('veryLow');
expect(targeting['one-div-id']['dlm']).to.be.eq('veryLow');
expect(targeting['one-div-id']['drg']).to.be.eq('veryLow');
expect(targeting['one-div-id']['hat']).to.be.eq('veryLow');
expect(targeting['one-div-id']['off']).to.be.eq('veryLow');
expect(targeting['one-div-id']['vio']).to.be.eq('veryLow');
expect(targeting['one-div-id']['fr']).to.be.eq('false');
expect(targeting['one-div-id']['id']).to.be.eq('4813f7a2-1f22-11ec-9bfd-0a1107f94461');
});
});
});
Expand All @@ -58,6 +114,8 @@ const config = {
}
};

const adUnitsCode = ['one-div-id', 'two-div-id'];

const adUnits = [
{
code: 'one-div-id',
Expand Down Expand Up @@ -87,3 +145,10 @@ const adUnits = [
}
}]
}];

const data = {
brandSafety: { adt: 'veryLow', alc: 'veryLow', dlm: 'veryLow', drg: 'veryLow', hat: 'veryLow', off: 'veryLow', vio: 'veryLow' },
custom: { 'ias-kw': ['IAS_5995_KW', 'IAS_7066_KW', 'IAS_7232_KW', 'IAS_7364_KW', 'IAS_3894_KW', 'IAS_6535_KW', 'IAS_6153_KW', 'IAS_5238_KW', 'IAS_7393_KW', 'IAS_1499_KW', 'IAS_7376_KW', 'IAS_1035_KW', 'IAS_6566_KW', 'IAS_1058_KW', 'IAS_11338_724_KW', 'IAS_7301_KW', 'IAS_15969_725_KW', 'IAS_6358_KW', 'IAS_710_KW', 'IAS_5445_KW', 'IAS_3822_KW', 'IAS_4901_KW', 'IAS_5806_KW', 'IAS_460_KW', 'IAS_11461_702_KW', 'IAS_5681_KW', 'IAS_17609_1240_KW', 'IAS_6634_KW', 'IAS_5597_KW'] },
fr: 'false',
slots: { 'one-div-id': { id: '4813f7a2-1f22-11ec-9bfd-0a1107f94461' } }
};

0 comments on commit eabfa8a

Please sign in to comment.