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

PBjs Core : add ability to inject tracking in video #10191

Merged
merged 22 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e407ca2
add vast impression tracking
matthieularere-msq Jul 7, 2023
14d1d3b
support additional context macro
matthieularere-msq Jul 7, 2023
643c74a
fix spaces and singlequotes
matthieularere-msq Jul 7, 2023
e70f141
remove 2494945CONTEXT2494945 macro
matthieularere-msq Jul 10, 2023
2019c74
remove CONTEXT macro
matthieularere-msq Jul 10, 2023
53f94b3
do not update vastImpUrl anymore
matthieularere-msq Jul 11, 2023
e182c18
add impression trackers in video cache
matthieularere-msq Jul 11, 2023
2587ba6
insert ony unique trackers
matthieularere-msq Jul 11, 2023
4f8086f
rename registerVastTrackers
matthieularere-msq Jul 11, 2023
94bbe1f
rename arrayVastTrackers
matthieularere-msq Jul 11, 2023
1934a1b
trackers object change
matthieularere-msq Jul 12, 2023
4a1eeaf
check modules are allowed to add trackers based on isActivityAllowed
matthieularere-msq Jul 12, 2023
1138443
rename validVastTracker and add line breaks
matthieularere-msq Jul 21, 2023
a7d72f5
removes duplicates verification in isValidVastTracker
matthieularere-msq Jul 21, 2023
2c18305
changes in wrapURI + typo fix
matthieularere-msq Jul 21, 2023
ecd61fe
requested changes
matthieularere-msq Aug 31, 2023
641c597
update function trackersToMap
matthieularere-msq Aug 31, 2023
cb593d5
using Set in trackers map
matthieularere-msq Aug 31, 2023
b74c5c6
changes suggested by dgirardi
matthieularere-msq Jan 19, 2024
8083d25
changes suggested by dgirardi
matthieularere-msq Jan 19, 2024
98f5d80
Update test/spec/video_spec.js
matthieularere-msq Jan 31, 2024
84d039c
add spaces
matthieularere-msq Jan 31, 2024
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
4 changes: 3 additions & 1 deletion src/auction.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import {config} from './config.js';
import {userSync} from './userSync.js';
import {hook} from './hook.js';
import {find, includes} from './polyfill.js';
import {OUTSTREAM} from './video.js';
import {OUTSTREAM, getVastTrackers, insertVastTrackers} from './video.js';
import {VIDEO} from './mediaTypes.js';
import {auctionManager} from './auctionManager.js';
import {bidderSettings} from './bidderSettings.js';
Expand Down Expand Up @@ -582,7 +582,9 @@ function tryAddVideoBid(auctionInstance, bidResponse, afterBidAdded, {index = au
}), 'video');
const context = videoMediaType && deepAccess(videoMediaType, 'context');
const useCacheKey = videoMediaType && deepAccess(videoMediaType, 'useCacheKey');
const [hasTrackers, vastTrackers] = getVastTrackers(bidResponse);
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's hard to work with functions that don't return standard types. In this case it appears to return an array where the first element is a bool, and the 2nd is an object.
Instead, why not return null when there are no trackers ? The type is then a nullable object.

if (vastTrackers) { 
    bidResponse.vastXml = insertVastTrackers(vastTrackers, bidResponse.vastXml); 
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


if (hasTrackers) { bidResponse.vastXml = insertVastTrackers(vastTrackers, bidResponse.vastXml); }
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's make this dependent on configuration as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

something like that to add in config ?
pbjs.setConfig({ videoTrackers: true });
If so, shouldn't it be configuration dependant here as well in videoCache.js ?
if (Array.isArray(arrayVastTrackers) && arrayVastTrackers.length == 2 && arrayVastTrackers[1].hasOwnProperty('impressions')) {
What default value should be set in config.js ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @patmmccann , can you confirm me that I understood what you asked for so I can make the changes ? Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any feedback on this please ?

if (config.getConfig('cache.url') && (useCacheKey || context !== OUTSTREAM)) {
if (!bidResponse.videoCacheKey || config.getConfig('cache.ignoreBidderCacheKey')) {
addBid = false;
Expand Down
46 changes: 46 additions & 0 deletions src/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const videoBidder = bid => includes(adapterManager.videoAdapters, bid.bid
export const hasNonVideoBidder = adUnit =>
adUnit.bids.filter(bid => !videoBidder(bid)).length;

let vastTrackers = [];

/**
* @typedef {object} VideoBid
* @property {string} adId id of the bid
Expand Down Expand Up @@ -64,3 +66,47 @@ export const checkVideoBidSetup = hook('sync', function(bid, adUnit, videoMediaT

return true;
}, 'checkVideoBidSetup');

export function registerVASTTrackers(tracker) {
dgirardi marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit

Suggested change
export function registerVASTTrackers(tracker) {
export function registerVastTrackers(tracker) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

vastTrackers.push(tracker);
}

export function insertVastTrackers(trackers, vastXml) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this logic already exists in the Video Module Library. Why not reuse existing code ?
See getVastXmlWithTracking https://github.com/prebid/Prebid.js/blob/master/libraries/video/shared/vastXmlEditor.js#L8

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't aware of this library. I would say not reuse this existing code avoids to run querySelectorAll and loop on it for each tracker to insert.

const doc = new DOMParser().parseFromString(vastXml, 'text/xml');
karimMourra marked this conversation as resolved.
Show resolved Hide resolved
const wrappers = doc.querySelectorAll('VAST Ad Wrapper, VAST Ad InLine');
try {
if (wrappers.length) {
wrappers.forEach(wrapper => {
trackers['impressions'].forEach(trackingUrl => {
const impression = doc.createElement('Impression');
impression.appendChild(doc.createCDATASection(trackingUrl));
wrapper.appendChild(impression)
});
});
vastXml = new XMLSerializer().serializeToString(doc);
karimMourra marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (error) {
logError('an error happened trying to insert trackers in vastXml');
}
return vastXml;
}

export function getVastTrackers(bid) {
let hasTrackers = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

this implementation needs to be cleaned, it's not easy to follow and is difficult to manage. A few things that could help:

  • split into functions that do one thing and can be reused - (Single Responsibility Principle)
  • work with simple data structures: it's cumbersome to work with trackers which is an object where each value is an array; instead you should work with arrays, and then set those arrays to a trackers map at the end.
  • remove flags - flags are a nuisance to maintain as the code evolves, plus they are unnecessary. If at the end of the function, the impression array is empty, you can return null.

Lastly, the interface is hard to work with. I have to look at the implementation to understand what it's returning.
Instead, it would be easier if it returned a simpler data structure, such as a nullable object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Latest commit made changes which should match those you asked for. Can you tell me if there is something else you would like to see modify here or if I didn't understood what you requests ? I was not quite sure about the trackers map you mentioned. Thanks

let trackers = {'impressions': []};
vastTrackers.forEach(func => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

what are the elements in the vastTrackers array ? They are functions that when given a bid as an arg return what ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

here is an example of implementation at the analytic module side :

registerVASTTrackers(function(bidResponse) {
      return {
        'impressions': [`https://tracking.mydomain.com/vast?cpm=${bidResponse.cpm}`]
      };
    });

The function registered in vastTrackers returns an object with as many tracking url needed for each events. Currently only impression is handled, but doing it this way make it possible to extend to other vast tracking such as start, quartiles, completion...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's also the reason why getVastTrackers returns the kind of structure its trackers variable is.

let tmpTrackers = func(bid);
for (const key in tmpTrackers) {
if (key in trackers && Array.isArray(tmpTrackers[key])) {
// only add not existing trackers
tmpTrackers[key].forEach(item => {
if (!trackers[key].includes(item)) {
trackers[key].push(item);
hasTrackers = true;
}
});
}
}
});
return [hasTrackers, trackers];
}
14 changes: 10 additions & 4 deletions src/videoCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import {ajaxBuilder} from './ajax.js';
import {config} from './config.js';
import {auctionManager} from './auctionManager.js';
import {getVastTrackers} from './video.js';

/**
* Might be useful to be configurable in the future
Expand Down Expand Up @@ -42,17 +43,22 @@ const ttlBufferInSeconds = 15;
* @param {string} impUrl An impression tracker URL for the delivery of the video ad
* @return A VAST URL which loads XML from the given URI.
*/
function wrapURI(uri, impUrl) {
function wrapURI(uri, impUrl, arrayVastTrackers) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

no need to include the type in the name

Suggested change
function wrapURI(uri, impUrl, arrayVastTrackers) {
function wrapURI(uri, impUrl, vastTrackers) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

// Technically, this is vulnerable to cross-script injection by sketchy vastUrl bids.
// We could make sure it's a valid URI... but since we're loading VAST XML from the
// URL they provide anyway, that's probably not a big deal.
let vastImp = (impUrl) ? `<![CDATA[${impUrl}]]>` : ``;
let impressions = (impUrl) ? `<Impression><![CDATA[${impUrl}]]></Impression>` : ``;
if (Array.isArray(arrayVastTrackers) && arrayVastTrackers.length == 2 && arrayVastTrackers[1].hasOwnProperty('impressions')) {
arrayVastTrackers[1]['impressions'].forEach(trackingImp => {
impressions += `<Impression><![CDATA[${trackingImp}]]></Impression>`;
});
}
return `<VAST version="3.0">
<Ad>
<Wrapper>
<AdSystem>prebid.org wrapper</AdSystem>
<VASTAdTagURI><![CDATA[${uri}]]></VASTAdTagURI>
<Impression>${vastImp}</Impression>
${impressions}
<Creatives></Creatives>
</Wrapper>
</Ad>
Expand All @@ -67,7 +73,7 @@ function wrapURI(uri, impUrl) {
* @param index
*/
function toStorageRequest(bid, {index = auctionManager.index} = {}) {
const vastValue = bid.vastXml ? bid.vastXml : wrapURI(bid.vastUrl, bid.vastImpUrl);
const vastValue = bid.vastXml ? bid.vastXml : wrapURI(bid.vastUrl, bid.vastImpUrl, getVastTrackers(bid));
const auction = index.getAuction(bid);
const ttlWithBuffer = Number(bid.ttl) + ttlBufferInSeconds;
let payload = {
Expand Down
36 changes: 35 additions & 1 deletion test/spec/videoCache_spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import chai from 'chai';
import { registerVASTTrackers } from 'src/video.js';
import { getCacheUrl, store } from 'src/videoCache.js';
import { config } from 'src/config.js';
import { server } from 'test/mocks/xhr.js';
Expand Down Expand Up @@ -127,7 +128,7 @@ describe('The video cache', function () {
<Wrapper>
<AdSystem>prebid.org wrapper</AdSystem>
<VASTAdTagURI><![CDATA[my-mock-url.com]]></VASTAdTagURI>
<Impression></Impression>

<Creatives></Creatives>
</Wrapper>
</Ad>
Expand All @@ -149,6 +150,39 @@ describe('The video cache', function () {
assertRequestMade({ vastUrl: 'my-mock-url.com', vastImpUrl: 'imptracker.com', ttl: 25 }, expectedValue)
});

it('should include additional impressions trackers on top of vastImpUrl when they exist', function() {
registerVASTTrackers(function(bidResponse) {
return {
'impressions': [`https://vasttracking.mydomain.com/vast?cpm=${bidResponse.cpm}`]
};
});
const expectedValue = `<VAST version="3.0">
<Ad>
<Wrapper>
<AdSystem>prebid.org wrapper</AdSystem>
<VASTAdTagURI><![CDATA[my-mock-url.com]]></VASTAdTagURI>
<Impression><![CDATA[imptracker.com]]></Impression><Impression><![CDATA[https://vasttracking.mydomain.com/vast?cpm=1.2]]></Impression>
<Creatives></Creatives>
</Wrapper>
</Ad>
</VAST>`;
assertRequestMade({ vastUrl: 'my-mock-url.com', vastImpUrl: 'imptracker.com', ttl: 25, cpm: 1.2 }, expectedValue)
});

it('should include only additional impressions trackers when they exist', function() {
const expectedValue = `<VAST version="3.0">
<Ad>
<Wrapper>
<AdSystem>prebid.org wrapper</AdSystem>
<VASTAdTagURI><![CDATA[my-mock-url.com]]></VASTAdTagURI>
<Impression><![CDATA[https://vasttracking.mydomain.com/vast?cpm=1.2]]></Impression>
<Creatives></Creatives>
</Wrapper>
</Ad>
</VAST>`;
assertRequestMade({ vastUrl: 'my-mock-url.com', ttl: 25, cpm: 1.2 }, expectedValue)
});

it('should make the expected request when store() is called on an ad with vastXml', function () {
const vastXml = '<VAST version="3.0"></VAST>';
assertRequestMade({ vastXml: vastXml, ttl: 25 }, vastXml);
Expand Down
22 changes: 21 additions & 1 deletion test/spec/video_spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isValidVideoBid } from 'src/video.js';
import { isValidVideoBid, registerVASTTrackers, insertVastTrackers, getVastTrackers } from 'src/video.js';
import {hook} from '../../src/hook.js';
import {stubAuctionIndex} from '../helpers/indexStub.js';

Expand Down Expand Up @@ -100,4 +100,24 @@ describe('video.js', function () {
const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})});
expect(valid).to.equal(false);
});

it('insert into tracker list', function() {
registerVASTTrackers(function(bidResponse) {
return {
'impressions': [`https://vasttracking.mydomain.com/vast?cpm=${bidResponse.cpm}`]
};
});
const [hasTrackers, trackers] = getVastTrackers({'cpm': 1.0});
expect(hasTrackers).to.equal(true);
expect(trackers).to.have.property('impressions');
expect(trackers.impressions.length).to.equal(1);
expect(trackers.impressions[0]).to.equal('https://vasttracking.mydomain.com/vast?cpm=1');
});

it('insert trackers in vastXml', function() {
const [hasTrackers, trackers] = getVastTrackers({'cpm': 1.0});
let vastXml = '<VAST><Ad><Wrapper></Wrapper></Ad></VAST>';
vastXml = insertVastTrackers(trackers, vastXml);
expect(vastXml).to.equal('<VAST><Ad><Wrapper><Impression><![CDATA[https://vasttracking.mydomain.com/vast?cpm=1]]></Impression></Wrapper></Ad></VAST>');
});
});