From f0852c0fc0c6906851c33642eb9d86664a6f0359 Mon Sep 17 00:00:00 2001 From: Vladimir Fedoseev Date: Mon, 21 Sep 2020 21:37:08 +0300 Subject: [PATCH 1/4] FID-162: Add Reconciliation RTD Provider --- .../reconciliationRtdProvider_example.html | 101 ++++++ modules/.submodules.json | 3 +- modules/reconciliationRtdProvider.js | 338 ++++++++++++++++++ modules/reconciliationRtdProvider.md | 49 +++ .../modules/reconciliationRtdProvider_spec.js | 135 +++++++ 5 files changed, 625 insertions(+), 1 deletion(-) create mode 100644 integrationExamples/gpt/reconciliationRtdProvider_example.html create mode 100644 modules/reconciliationRtdProvider.js create mode 100644 modules/reconciliationRtdProvider.md create mode 100644 test/spec/modules/reconciliationRtdProvider_spec.js diff --git a/integrationExamples/gpt/reconciliationRtdProvider_example.html b/integrationExamples/gpt/reconciliationRtdProvider_example.html new file mode 100644 index 00000000000..af414e0b055 --- /dev/null +++ b/integrationExamples/gpt/reconciliationRtdProvider_example.html @@ -0,0 +1,101 @@ + + + + + + + Reconciliation RTD Provider Example + + + + + + + + +
Div-1
+
+ +
+ + diff --git a/modules/.submodules.json b/modules/.submodules.json index 18e75dd1794..4635f1ba80d 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -24,6 +24,7 @@ "rtdModule": [ "browsiRtdProvider", "audigentRtdProvider", - "jwplayerRtdProvider" + "jwplayerRtdProvider", + "reconciliationRtdProvider" ] } diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js new file mode 100644 index 00000000000..d8cd759ece2 --- /dev/null +++ b/modules/reconciliationRtdProvider.js @@ -0,0 +1,338 @@ +/** + * This module adds reconciliation provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will add custom targetings to ad units + * The module will listen to post messages from rendered creatives with Reconciliation Tag + * The module will call tracking pixels to log info needed for reconciliation matching + * @module modules/reconciliationRtdProvider + * @requires module:modules/realTimeData + */ + +/** + * @typedef {Object} ModuleParams + * @property {string} publisherMemberId + * @property {?string} initUrl + * @property {?string} impressionUrl + * @property {?boolean} allowAccess + */ + +import { submodule } from '../src/hook.js'; +import { config } from '../src/config.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import * as utils from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +/** @type {string} */ +const SUBMODULE_NAME = 'reconciliation'; +/** @type {Object} */ +const MessageType = { + IMPRESSION_REQUEST: 'rsdk:impression:req', + IMPRESSION_RESPONSE: 'rsdk:impression:res', +}; +/** @type {ModuleParams} */ +const DEFAULT_PARAMS = { + initUrl: 'https://confirm.fiduciadlt.com/init', + impressionUrl: 'https://confirm.fiduciadlt.com/imp', + allowAccess: false, +}; +/** @type {ModuleParams} */ +let _moduleParams = {}; + +/** + * Handle postMesssage from ad creative, track impression + * and send response to reconciliation ad tag + * @param {Event} e + */ +function handleAdMessage(e) { + let data = {}; + let adUnitId = ''; + let adDeliveryId = ''; + + try { + data = JSON.parse(e.data); + } catch (e) { + return; + } + + if (data.type === MessageType.IMPRESSION_REQUEST) { + if (utils.isGptPubadsDefined()) { + // 1. Find the last iframed window before window.top where the tracker was injected + // (the tracker could be injected in nested iframes) + const adWin = getTopIFrameWin(e.source); + if (adWin && adWin !== window.top) { + // 2. Find the GPT slot for the iframed window + const adSlot = getSlotByWin(adWin); + // 3. Get AdUnit IDs for the selected slot + if (adSlot) { + adUnitId = adSlot.getAdUnitPath(); + adDeliveryId = adSlot.getTargeting('RSDK_ADID'); + adDeliveryId = adDeliveryId.length + ? adDeliveryId[0] + : utils.generateUUID(); + } + } + } + + // Call local impression callback + const args = Object.assign({}, data.args, { + publisherDomain: window.location.hostname, + publisherMemberId: _moduleParams.publisherMemberId, + adUnitId, + adDeliveryId, + }); + + utils.triggerPixel(`${_moduleParams.impressionUrl}?${stringify(args)}`); + + // Send response back to the Advertiser tag + let response = { + type: MessageType.IMPRESSION_RESPONSE, + id: data.id, + args: Object.assign( + { + publisherDomain: window.location.hostname, + }, + data.args + ), + }; + + // If access is allowed - add ad unit id to response + if (_moduleParams.allowAccess) { + Object.assign(response.args, { + adUnitId, + adDeliveryId, + }); + } + + e.source.postMessage(JSON.stringify(response), '*'); + } +} + +/** + * Get top iframe window for nested Window object + * - top + * -- iframe.window <-- top iframe window + * --- iframe.window + * ---- iframe.window <-- win + * + * @param {Window} win nested iframe window object + */ +export function getTopIFrameWin(win) { + if (!win) { + return null; + } + + try { + while (win.parent !== win.top) { + win = win.parent; + } + return win; + } catch (e) { + return null; + } +} + +/** + * get all slots on page + * @return {Object[]} slot GoogleTag slots + */ +function getAllSlots() { + return utils.isGptPubadsDefined() && window.googletag.pubads().getSlots(); +} + +/** + * get GPT slot by placement id + * @param {string} code placement id + * @return {?Object} + */ +function getSlotByCode(code) { + const slots = getAllSlots(); + if (!slots || !slots.length) { + return null; + } + return ( + find( + slots, + (s) => s.getSlotElementId() === code || s.getAdUnitPath() === code + ) || null + ); +} + +/** + * get GPT slot by iframe window + * @param {Window} win + * @return {?Object} + */ +function getSlotByWin(win) { + const slots = getAllSlots(); + + if (!slots || !slots.length) { + return null; + } + + return ( + find(slots, (s) => { + let slotElement = document.getElementById(s.getSlotElementId()); + + if (slotElement) { + let slotIframe = slotElement.querySelector('iframe'); + + if (slotIframe && slotIframe.contentWindow === win) { + return true; + } + } + + return false; + }) || null + ); +} + +/** + * serialize object and return query params string + * @param {Object} data + * @return {string} + */ +export function stringify(query) { + const parts = []; + + for (let key in query) { + if (query.hasOwnProperty(key)) { + let val = query[key]; + if (typeof query[key] !== 'object') { + parts.push(`${key}=${encodeURIComponent(val)}`); + } else { + parts.push(`${key}=${encodeURIComponent(stringify(val))}`); + } + } + } + return parts.join('&'); +} +/** + * Init Reconciliation post messages listeners to handle + * impressions messages from ad creative + */ +function initListeners() { + window.addEventListener('message', handleAdMessage, false); +} + +/** + * Send init event to log + * @param {Object} adUnitsDict + */ +function trackInit(adUnitsDict) { + const adUnits = Object.keys(adUnitsDict).map((k) => { + const adUnit = adUnitsDict[k]; + + return { + adUnitId: adUnit['RSDK_AUID'], + adDeliveryId: adUnit['RSDK_ADID'], + }; + }); + + track.trackPost( + _moduleParams.initUrl, + { + adUnits, + publisherDomain: window.location.hostname, + publisherMemberId: _moduleParams.publisherMemberId, + } + ); +} + +/** + * Track event via POST request + * wrap method to allow stubbing in tests + * @param {string} url + * @param {Object} data + */ +export const track = { + trackPost(url, data) { + const ajax = ajaxBuilder(); + + ajax( + url, + function() {}, + JSON.stringify(data), + { + method: 'POST', + } + ); + } +} + +/** + * Set custom targetings for provided adUnits + * call callback (onDone) when ready + * @param {adUnit[]} adUnits + * @param {function} onDone callback function + */ +function getReconciliationData(adUnits, onDone) { + let dataToReturn = adUnits.reduce((rp, cau) => { + const adUnitCode = cau && cau.code; + + if (!adUnitCode) { + return rp; + } + + const adSlot = getSlotByCode(adUnitCode); + rp[adUnitCode] = { + RSDK_ADID: + cau.transactionId || utils.generateUUID(), + RSDK_AUID: adSlot ? adSlot.getAdUnitPath() : adUnitCode, + }; + + return rp; + }, {}); + + // Track init event + trackInit(dataToReturn); + + return onDone(dataToReturn); +} + +/** @type {RtdSubmodule} */ +export const reconciliationSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: SUBMODULE_NAME, + /** + * get data and send back to realTimeData module + * @function + * @param {adUnit[]} adUnits + * @param {function} onDone + */ + getData: getReconciliationData, + init +}; + +function init(config, gdpr, usp) { + return true; +} + +export function beforeInit(config) { + const confListener = config.getConfig(MODULE_NAME, ({ realTimeData }) => { + try { + const params = + realTimeData.dataProviders && + realTimeData.dataProviders.filter( + (pr) => pr.name && pr.name.toLowerCase() === SUBMODULE_NAME + )[0].params; + _moduleParams = Object.assign({}, DEFAULT_PARAMS, params); + } catch (e) { + _moduleParams = {}; + } + + if (_moduleParams.publisherMemberId) { + confListener(); + initListeners(); + } else { + utils.logError('missing params for Reconciliation provider'); + } + }); +} + +submodule('realTimeData', reconciliationSubmodule); +beforeInit(config); diff --git a/modules/reconciliationRtdProvider.md b/modules/reconciliationRtdProvider.md new file mode 100644 index 00000000000..53883ad99eb --- /dev/null +++ b/modules/reconciliationRtdProvider.md @@ -0,0 +1,49 @@ +The purpose of this Real Time Data Provider is to allow publishers to match impressions accross the supply chain. + +**Reconciliation SDK** +The purpose of Reconciliation SDK module is to collect supply chain structure information and vendor-specific impression IDs from suppliers participating in ad creative delivery and report it to the Reconciliation Service, allowing publishers, advertisers and other supply chain participants to match and reconcile ad server, SSP, DSP and veritifation system log file records. Reconciliation SDK was created as part of TAG DLT initiative ( https://www.tagtoday.net/pressreleases/dlt_9_7_2020 ). + +**Usage for Publishers:** + +Compile the Reconciliation Provider into your Prebid build: + +`gulp build --modules=reconciliationRtdProvider` + +Add Reconciliation real time data provider configuration by setting up a Prebid Config: + +```javascript +const reconciliationDataProvider = { + name: "reconciliation", + params: { + publisherMemberId: "test_prebid_publisher", // required + allowAccess: true, //optional + } +}; + +pbjs.setConfig({ + ..., + realTimeData: { + dataProviders: [ + reconciliationDataProvider + ] + } +}); +``` + +where: +- `publisherMemberId` (required) - ID associated with the publisher +- `access` (optional) true/false - Whether ad markup will recieve Ad Unit Id's via Reconciliation Tag + +**Example:** + +To view an example: + +- in your cli run: + +`gulp serve --modules=reconciliationRtdProvider,appnexusBidAdapter` + +Your could also change 'appnexusBidAdapter' to another one. + +- in your browser, navigate to: + +`http://localhost:9999/integrationExamples/gpt/reconciliationRtdProvider_example.html` diff --git a/test/spec/modules/reconciliationRtdProvider_spec.js b/test/spec/modules/reconciliationRtdProvider_spec.js new file mode 100644 index 00000000000..56400e5f64f --- /dev/null +++ b/test/spec/modules/reconciliationRtdProvider_spec.js @@ -0,0 +1,135 @@ +import { reconciliationSubmodule, track } from 'modules/reconciliationRtdProvider.js'; +import { config } from 'src/config.js'; +import { makeSlot } from '../integration/faker/googletag.js'; + +describe('reconciliationRtdProvider', function () { + let trackPostStub; + + beforeEach(function () { + trackPostStub = sinon.stub(track, 'trackPost'); + }); + + afterEach(function () { + trackPostStub.restore(); + }); + + describe('reconciliationSubmodule', function () { + it('successfully instantiates', function () { + expect(reconciliationSubmodule.init()).to.equal(true); + }); + + describe('getData', function () { + it('should return data in proper format', function (done) { + const adUnit1 = { + code: '/adunit1', + transactionId: 'transactionId1' + }; + const adUnit2 = { + code: '/adunit1', + transactionId: 'transactionId2' + }; + + const expectedData = { + [adUnit1.code]: { + RSDK_AUID: adUnit1.code, + RSDK_ADID: adUnit1.transactionId + }, + [adUnit2.code]: { + RSDK_AUID: adUnit2.code, + RSDK_ADID: adUnit2.transactionId + } + }; + + reconciliationSubmodule.getData([adUnit1, adUnit2], onDone); + + function onDone(data) { + expect(data).to.eql(expectedData); + done(); + } + }); + + it('should generate deliveryId if transactionId is empty', function (done) { + const adUnit = { + code: '/adunit' + }; + + reconciliationSubmodule.getData([adUnit], onDone); + + function onDone(data) { + expect(data[adUnit.code].RSDK_AUID).to.eql(adUnit.code); + expect(data[adUnit.code].RSDK_ADID).to.be.a('string'); + done(); + } + }); + + it('should return unit path as adUnitId', function (done) { + const adUnitCode = '/adunit1'; + const adUnitId = 'ad1'; + const adUnit = { + code: adUnitId, + transactionId: 'transactionId1' + }; + const slot = makeSlot({ code: adUnitCode, divId: adUnitId }); + window.googletag.pubads().setSlots([slot]); + const expectedData = { + [adUnit.code]: { + RSDK_AUID: adUnitCode, + RSDK_ADID: adUnit.transactionId + } + }; + + reconciliationSubmodule.getData([adUnit], onDone); + function onDone(data) { + expect(data).to.eql(expectedData); + done(); + } + }); + }); + + describe('track events', function() { + const conf = { + 'realTimeData': { + 'dataProviders': [{ + 'name': 'reconciliation', + 'params': { + 'publisherMemberId': 'test_prebid_publisher' + }, + }] + } + }; + + beforeEach(function () { + config.setConfig(conf); + }); + + after(function () { + config.resetConfig(); + }); + + it('should track init event with data', function () { + const adUnit1 = { + code: '/adunit1', + transactionId: 'transactionId1' + }; + const expectedData = { + adUnits: [ + { + adUnitId: '/adunit1', + adDeliveryId: 'transactionId1' + } + ], + publisherMemberId: 'test_prebid_publisher' + }; + const onDone = sinon.spy(); + + reconciliationSubmodule.getData([adUnit1], onDone); + + expect(onDone.calledOnce).to.be.true; + expect(trackPostStub.calledOnce).to.be.true; + expect(trackPostStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/init'); + expect(trackPostStub.getCalls()[0].args[1].adUnits).to.eql(expectedData.adUnits); + expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); + }); + }); + }); +}); From a508a1421b96635770ff0196ae47e4c1edcf24dd Mon Sep 17 00:00:00 2001 From: Vladimir Fedoseev Date: Sun, 4 Oct 2020 20:53:52 +0300 Subject: [PATCH 2/4] FID-162: Update Reconciliation RTD Provider API --- modules/reconciliationRtdProvider.js | 63 +++------ .../modules/reconciliationRtdProvider_spec.js | 124 ++++-------------- 2 files changed, 45 insertions(+), 142 deletions(-) diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js index d8cd759ece2..ce7e0e9b679 100644 --- a/modules/reconciliationRtdProvider.js +++ b/modules/reconciliationRtdProvider.js @@ -17,15 +17,10 @@ */ import { submodule } from '../src/hook.js'; -import { config } from '../src/config.js'; import { ajaxBuilder } from '../src/ajax.js'; import * as utils from '../src/utils.js'; import find from 'core-js-pure/features/array/find.js'; -/** @type {string} */ -const MODULE_NAME = 'realTimeData'; -/** @type {string} */ -const SUBMODULE_NAME = 'reconciliation'; /** @type {Object} */ const MessageType = { IMPRESSION_REQUEST: 'rsdk:impression:req', @@ -263,22 +258,19 @@ export const track = { /** * Set custom targetings for provided adUnits - * call callback (onDone) when ready - * @param {adUnit[]} adUnits - * @param {function} onDone callback function + * @param {string[]} adUnitsCodes + * @return {Object} key-value object with custom targetings */ -function getReconciliationData(adUnits, onDone) { - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - +function getReconciliationData(adUnitsCodes) { + let dataToReturn = adUnitsCodes.reduce((rp, adUnitCode) => { if (!adUnitCode) { return rp; } const adSlot = getSlotByCode(adUnitCode); + rp[adUnitCode] = { - RSDK_ADID: - cau.transactionId || utils.generateUUID(), + RSDK_ADID: utils.generateUUID(), RSDK_AUID: adSlot ? adSlot.getAdUnitPath() : adUnitCode, }; @@ -288,7 +280,7 @@ function getReconciliationData(adUnits, onDone) { // Track init event trackInit(dataToReturn); - return onDone(dataToReturn); + return dataToReturn; } /** @type {RtdSubmodule} */ @@ -297,42 +289,25 @@ export const reconciliationSubmodule = { * used to link submodule with realTimeData * @type {string} */ - name: SUBMODULE_NAME, + name: 'reconciliation', /** * get data and send back to realTimeData module * @function - * @param {adUnit[]} adUnits - * @param {function} onDone + * @param {string[]} adUnitsCodes */ - getData: getReconciliationData, - init + getTargetingData: getReconciliationData, + init: init, }; -function init(config, gdpr, usp) { +function init(moduleConfig) { + const params = moduleConfig.params; + if (params && params.publisherMemberId) { + _moduleParams = Object.assign({}, DEFAULT_PARAMS, params); + initListeners(); + } else { + utils.logError('missing params for Browsi provider'); + } return true; } -export function beforeInit(config) { - const confListener = config.getConfig(MODULE_NAME, ({ realTimeData }) => { - try { - const params = - realTimeData.dataProviders && - realTimeData.dataProviders.filter( - (pr) => pr.name && pr.name.toLowerCase() === SUBMODULE_NAME - )[0].params; - _moduleParams = Object.assign({}, DEFAULT_PARAMS, params); - } catch (e) { - _moduleParams = {}; - } - - if (_moduleParams.publisherMemberId) { - confListener(); - initListeners(); - } else { - utils.logError('missing params for Reconciliation provider'); - } - }); -} - submodule('realTimeData', reconciliationSubmodule); -beforeInit(config); diff --git a/test/spec/modules/reconciliationRtdProvider_spec.js b/test/spec/modules/reconciliationRtdProvider_spec.js index 56400e5f64f..ce452c8e3b3 100644 --- a/test/spec/modules/reconciliationRtdProvider_spec.js +++ b/test/spec/modules/reconciliationRtdProvider_spec.js @@ -1,8 +1,16 @@ import { reconciliationSubmodule, track } from 'modules/reconciliationRtdProvider.js'; -import { config } from 'src/config.js'; import { makeSlot } from '../integration/faker/googletag.js'; -describe('reconciliationRtdProvider', function () { +describe('Reconciliation Real time data submodule', function () { + const conf = { + dataProviders: [{ + 'name': 'reconciliation', + 'params': { + 'publisherMemberId': 'test_prebid_publisher' + }, + }] + }; + let trackPostStub; beforeEach(function () { @@ -15,119 +23,39 @@ describe('reconciliationRtdProvider', function () { describe('reconciliationSubmodule', function () { it('successfully instantiates', function () { - expect(reconciliationSubmodule.init()).to.equal(true); + expect(reconciliationSubmodule.init(conf.dataProviders[0])).to.equal(true); }); describe('getData', function () { - it('should return data in proper format', function (done) { - const adUnit1 = { - code: '/adunit1', - transactionId: 'transactionId1' - }; - const adUnit2 = { - code: '/adunit1', - transactionId: 'transactionId2' - }; - - const expectedData = { - [adUnit1.code]: { - RSDK_AUID: adUnit1.code, - RSDK_ADID: adUnit1.transactionId - }, - [adUnit2.code]: { - RSDK_AUID: adUnit2.code, - RSDK_ADID: adUnit2.transactionId - } - }; - - reconciliationSubmodule.getData([adUnit1, adUnit2], onDone); - - function onDone(data) { - expect(data).to.eql(expectedData); - done(); - } - }); - - it('should generate deliveryId if transactionId is empty', function (done) { - const adUnit = { - code: '/adunit' - }; + it('should return data in proper format', function () { + makeSlot({code: '/reconciliationAdunit1', divId: 'reconciliationAd1'}); - reconciliationSubmodule.getData([adUnit], onDone); - - function onDone(data) { - expect(data[adUnit.code].RSDK_AUID).to.eql(adUnit.code); - expect(data[adUnit.code].RSDK_ADID).to.be.a('string'); - done(); - } + const targetingData = reconciliationSubmodule.getTargetingData(['/reconciliationAdunit1']); + expect(targetingData['/reconciliationAdunit1'].RSDK_AUID).to.eql('/reconciliationAdunit1'); + expect(targetingData['/reconciliationAdunit1'].RSDK_ADID).to.be.a('string'); }); - it('should return unit path as adUnitId', function (done) { - const adUnitCode = '/adunit1'; - const adUnitId = 'ad1'; - const adUnit = { - code: adUnitId, - transactionId: 'transactionId1' - }; - const slot = makeSlot({ code: adUnitCode, divId: adUnitId }); - window.googletag.pubads().setSlots([slot]); - const expectedData = { - [adUnit.code]: { - RSDK_AUID: adUnitCode, - RSDK_ADID: adUnit.transactionId - } - }; + it('should return unit path if called with divId', function () { + makeSlot({code: '/reconciliationAdunit2', divId: 'reconciliationAd2'}); - reconciliationSubmodule.getData([adUnit], onDone); - function onDone(data) { - expect(data).to.eql(expectedData); - done(); - } + const targetingData = reconciliationSubmodule.getTargetingData(['reconciliationAd2']); + expect(targetingData['reconciliationAd2'].RSDK_AUID).to.eql('/reconciliationAdunit2'); + expect(targetingData['reconciliationAd2'].RSDK_ADID).to.be.a('string'); }); }); describe('track events', function() { - const conf = { - 'realTimeData': { - 'dataProviders': [{ - 'name': 'reconciliation', - 'params': { - 'publisherMemberId': 'test_prebid_publisher' - }, - }] - } - }; - - beforeEach(function () { - config.setConfig(conf); - }); - - after(function () { - config.resetConfig(); - }); - it('should track init event with data', function () { - const adUnit1 = { - code: '/adunit1', - transactionId: 'transactionId1' - }; - const expectedData = { - adUnits: [ - { - adUnitId: '/adunit1', - adDeliveryId: 'transactionId1' - } - ], - publisherMemberId: 'test_prebid_publisher' + const adUnit = { + code: '/adunit' }; - const onDone = sinon.spy(); - reconciliationSubmodule.getData([adUnit1], onDone); + reconciliationSubmodule.getTargetingData([adUnit.code]); - expect(onDone.calledOnce).to.be.true; expect(trackPostStub.calledOnce).to.be.true; expect(trackPostStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/init'); - expect(trackPostStub.getCalls()[0].args[1].adUnits).to.eql(expectedData.adUnits); + expect(trackPostStub.getCalls()[0].args[1].adUnits[0].adUnitId).to.eql(adUnit.code); + expect(trackPostStub.getCalls()[0].args[1].adUnits[0].adDeliveryId).be.a('string'); expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); }); }); From 7681602118abd37a0768112fcd005e08ef27acd7 Mon Sep 17 00:00:00 2001 From: Vladimir Fedoseev Date: Fri, 16 Oct 2020 23:20:27 +0300 Subject: [PATCH 3/4] FID-162: Update getTargetingData method --- modules/reconciliationRtdProvider.js | 37 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js index ce7e0e9b679..0a3b9a0f29e 100644 --- a/modules/reconciliationRtdProvider.js +++ b/modules/reconciliationRtdProvider.js @@ -213,18 +213,9 @@ function initListeners() { /** * Send init event to log - * @param {Object} adUnitsDict + * @param {Array} adUnits */ -function trackInit(adUnitsDict) { - const adUnits = Object.keys(adUnitsDict).map((k) => { - const adUnit = adUnitsDict[k]; - - return { - adUnitId: adUnit['RSDK_AUID'], - adDeliveryId: adUnit['RSDK_ADID'], - }; - }); - +function trackInit(adUnits) { track.trackPost( _moduleParams.initUrl, { @@ -262,23 +253,31 @@ export const track = { * @return {Object} key-value object with custom targetings */ function getReconciliationData(adUnitsCodes) { - let dataToReturn = adUnitsCodes.reduce((rp, adUnitCode) => { + const dataToReturn = {}; + const adUnitsToTrack = []; + + adUnitsCodes.forEach((adUnitCode) => { if (!adUnitCode) { - return rp; + return; } const adSlot = getSlotByCode(adUnitCode); + const adUnitId = adSlot ? adSlot.getAdUnitPath() : adUnitCode; + const adDeliveryId = utils.generateUUID(); - rp[adUnitCode] = { - RSDK_ADID: utils.generateUUID(), - RSDK_AUID: adSlot ? adSlot.getAdUnitPath() : adUnitCode, + dataToReturn[adUnitCode] = { + RSDK_AUID: adUnitId, + RSDK_ADID: adDeliveryId, }; - return rp; + adUnitsToTrack.push({ + adUnitId, + adDeliveryId + }); }, {}); // Track init event - trackInit(dataToReturn); + trackInit(adUnitsToTrack); return dataToReturn; } @@ -305,7 +304,7 @@ function init(moduleConfig) { _moduleParams = Object.assign({}, DEFAULT_PARAMS, params); initListeners(); } else { - utils.logError('missing params for Browsi provider'); + utils.logError('missing params for Reconciliation provider'); } return true; } From 79fae4ce93fce330c5d028a35168b79af8693200 Mon Sep 17 00:00:00 2001 From: Vladimir Fedoseev Date: Sat, 17 Oct 2020 17:33:16 +0300 Subject: [PATCH 4/4] FID-162: Add tests --- modules/reconciliationRtdProvider.js | 14 +- .../modules/reconciliationRtdProvider_spec.js | 176 +++++++++++++++++- 2 files changed, 181 insertions(+), 9 deletions(-) diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js index 0a3b9a0f29e..20acb6a535a 100644 --- a/modules/reconciliationRtdProvider.js +++ b/modules/reconciliationRtdProvider.js @@ -78,7 +78,7 @@ function handleAdMessage(e) { adDeliveryId, }); - utils.triggerPixel(`${_moduleParams.impressionUrl}?${stringify(args)}`); + track.trackGet(_moduleParams.impressionUrl, args); // Send response back to the Advertiser tag let response = { @@ -112,14 +112,17 @@ function handleAdMessage(e) { * ---- iframe.window <-- win * * @param {Window} win nested iframe window object + * @param {Window} topWin top window */ -export function getTopIFrameWin(win) { +export function getTopIFrameWin(win, topWin) { + topWin = topWin || window; + if (!win) { return null; } try { - while (win.parent !== win.top) { + while (win.parent !== topWin) { win = win.parent; } return win; @@ -159,7 +162,7 @@ function getSlotByCode(code) { * @param {Window} win * @return {?Object} */ -function getSlotByWin(win) { +export function getSlotByWin(win) { const slots = getAllSlots(); if (!slots || !slots.length) { @@ -233,6 +236,9 @@ function trackInit(adUnits) { * @param {Object} data */ export const track = { + trackGet(url, data) { + utils.triggerPixel(`${url}?${stringify(data)}`); + }, trackPost(url, data) { const ajax = ajaxBuilder(); diff --git a/test/spec/modules/reconciliationRtdProvider_spec.js b/test/spec/modules/reconciliationRtdProvider_spec.js index ce452c8e3b3..8adca28248c 100644 --- a/test/spec/modules/reconciliationRtdProvider_spec.js +++ b/test/spec/modules/reconciliationRtdProvider_spec.js @@ -1,5 +1,12 @@ -import { reconciliationSubmodule, track } from 'modules/reconciliationRtdProvider.js'; +import { + reconciliationSubmodule, + track, + stringify, + getTopIFrameWin, + getSlotByWin +} from 'modules/reconciliationRtdProvider.js'; import { makeSlot } from '../integration/faker/googletag.js'; +import * as utils from 'src/utils.js'; describe('Reconciliation Real time data submodule', function () { const conf = { @@ -11,19 +18,38 @@ describe('Reconciliation Real time data submodule', function () { }] }; - let trackPostStub; + let trackPostStub, trackGetStub; beforeEach(function () { trackPostStub = sinon.stub(track, 'trackPost'); + trackGetStub = sinon.stub(track, 'trackGet'); }); afterEach(function () { trackPostStub.restore(); + trackGetStub.restore(); }); describe('reconciliationSubmodule', function () { - it('successfully instantiates', function () { - expect(reconciliationSubmodule.init(conf.dataProviders[0])).to.equal(true); + describe('initialization', function () { + let utilsLogErrorSpy; + + before(function () { + utilsLogErrorSpy = sinon.spy(utils, 'logError'); + }); + + after(function () { + utils.logError.restore(); + }); + + it('successfully instantiates', function () { + expect(reconciliationSubmodule.init(conf.dataProviders[0])).to.equal(true); + }); + + it('should log error if initializied without parameters', function () { + expect(reconciliationSubmodule.init({'name': 'reconciliation', 'params': {}})).to.equal(true); + expect(utilsLogErrorSpy.calledOnce).to.be.true; + }); }); describe('getData', function () { @@ -42,9 +68,16 @@ describe('Reconciliation Real time data submodule', function () { expect(targetingData['reconciliationAd2'].RSDK_AUID).to.eql('/reconciliationAdunit2'); expect(targetingData['reconciliationAd2'].RSDK_ADID).to.be.a('string'); }); + + it('should skip empty adUnit id', function () { + makeSlot({code: '/reconciliationAdunit3', divId: 'reconciliationAd3'}); + + const targetingData = reconciliationSubmodule.getTargetingData(['reconciliationAd3', '']); + expect(targetingData).to.have.all.keys('reconciliationAd3'); + }); }); - describe('track events', function() { + describe('track events', function () { it('should track init event with data', function () { const adUnit = { code: '/adunit' @@ -59,5 +92,138 @@ describe('Reconciliation Real time data submodule', function () { expect(trackPostStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); }); }); + + describe('stringify parameters', function () { + it('should return query for flat object', function () { + const parameters = { + adUnitId: '/adunit', + adDeliveryId: '12345' + }; + + expect(stringify(parameters)).to.eql('adUnitId=%2Fadunit&adDeliveryId=12345'); + }); + + it('should return query with nested parameters', function () { + const parameters = { + adUnitId: '/adunit', + adDeliveryId: '12345', + ext: { + adSize: '300x250', + adType: 'banner' + } + }; + + expect(stringify(parameters)).to.eql('adUnitId=%2Fadunit&adDeliveryId=12345&ext=adSize%3D300x250%26adType%3Dbanner'); + }); + }); + + describe('get topmost iframe', function () { + /** + * - top + * -- iframe.window <-- top iframe window + * --- iframe.window + * ---- iframe.window <-- win + */ + const mockFrameWin = (topWin, parentWin) => { + return { + top: topWin, + parent: parentWin + } + } + + it('should return null if called with null', function() { + expect(getTopIFrameWin(null)).to.be.null; + }); + + it('should return null if there is an error in frames chain', function() { + const topWin = {}; + const iframe1Win = mockFrameWin(topWin, null); // break chain + const iframe2Win = mockFrameWin(topWin, iframe1Win); + + expect(getTopIFrameWin(iframe1Win, topWin)).to.be.null; + }); + + it('should get the topmost iframe', function () { + const topWin = {}; + const iframe1Win = mockFrameWin(topWin, topWin); + const iframe2Win = mockFrameWin(topWin, iframe1Win); + + expect(getTopIFrameWin(iframe2Win, topWin)).to.eql(iframe1Win); + }); + }); + + describe('get slot by nested iframe window', function () { + it('should return the slot', function () { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAd'; + adSlotElement.appendChild(adSlotIframe); + document.body.appendChild(adSlotElement); + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + + expect(getSlotByWin(adSlotIframe.contentWindow)).to.eql(adSlot); + }); + + it('should return null if the slot is not found', function () { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAd'; + document.body.appendChild(adSlotElement); + document.body.appendChild(adSlotIframe); // iframe is not in ad slot + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + + expect(getSlotByWin(adSlotIframe.contentWindow)).to.be.null; + }); + }); + + describe('handle postMessage from Reconciliation Tag in ad iframe', function () { + it('should track impression pixel with parameters', function (done) { + const adSlotElement = document.createElement('div'); + const adSlotIframe = document.createElement('iframe'); + + adSlotElement.id = 'reconciliationAdMessage'; + adSlotElement.appendChild(adSlotIframe); + document.body.appendChild(adSlotElement); + + const adSlot = makeSlot({code: '/reconciliationAdunit', divId: adSlotElement.id}); + // Fix targeting methods + adSlot.targeting = {}; + adSlot.setTargeting = function(key, value) { + this.targeting[key] = [value]; + }; + adSlot.getTargeting = function(key) { + return this.targeting[key]; + }; + + adSlot.setTargeting('RSDK_AUID', '/reconciliationAdunit'); + adSlot.setTargeting('RSDK_ADID', '12345'); + adSlotIframe.contentDocument.open(); + adSlotIframe.contentDocument.write(``); + adSlotIframe.contentDocument.close(); + + setTimeout(() => { + expect(trackGetStub.calledOnce).to.be.true; + expect(trackGetStub.getCalls()[0].args[0]).to.eql('https://confirm.fiduciadlt.com/imp'); + expect(trackGetStub.getCalls()[0].args[1].adUnitId).to.eql('/reconciliationAdunit'); + expect(trackGetStub.getCalls()[0].args[1].adDeliveryId).to.eql('12345'); + expect(trackGetStub.getCalls()[0].args[1].sourceMemberId).to.eql('test_member_id'); ; + expect(trackGetStub.getCalls()[0].args[1].sourceImpressionId).to.eql('123'); ; + expect(trackGetStub.getCalls()[0].args[1].publisherMemberId).to.eql('test_prebid_publisher'); + done(); + }, 100); + }); + }); }); });