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

[AD-1583] Allow publisher to specify tracking urls #8

Merged
merged 16 commits into from
Nov 3, 2021
53 changes: 51 additions & 2 deletions modules/videoModule/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import { allVideoEvents } from './constants/events.js';
import CONSTANTS from '../../src/constants.json';
import { videoCoreFactory } from './coreVideo.js';
import { coreAdServerFactory } from './adServer.js';
import find from 'core-js-pure/features/array/find.js';
import { vastXmlEditorFactory } from './shared/vastXmlEditor.js';

events.addEvents(allVideoEvents);

export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvents_, adServerCore_) {
export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvents_, adServerCore_, vastXmlEditor_) {
const videoCore = videoCore_;
const getConfig = getConfig_;
const pbGlobal = pbGlobal_;
const requestBids = pbGlobal.requestBids;
const pbEvents = pbEvents_;
const videoEvents = videoEvents_;
const adServerCore = adServerCore_;
const vastXmlEditor = vastXmlEditor_;

function init() {
getConfig('video', ({ video }) => {
Expand Down Expand Up @@ -42,6 +45,20 @@ export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvent
}
});
});

const cache = getConfig('cache');
if (!cache) {
return;
}

pbEvents.on(CONSTANTS.EVENTS.BID_ADJUSTMENT, function (bid) {
const adUnitCode = bid.adUnitCode;
const adUnit = find(pbGlobal.adUnits, adUnit => adUnitCode === adUnit.code);
const videoConfig = adUnit && adUnit.video;
const adServerConfig = videoConfig && videoConfig.adServer;
const trackingConfig = adServerConfig && adServerConfig.tracking;
addTrackingNodesToVastXml(bid, trackingConfig);
});
}

return { init };
Expand Down Expand Up @@ -81,12 +98,44 @@ export function PbVideo(videoCore_, getConfig_, pbGlobal_, pbEvents_, videoEvent
options.adXml = highestBid.vastXml;
videoCore.setAdTagUrl(adTagUrl, divId, options);
}

function addTrackingNodesToVastXml(bid, trackingConfig) {
if (!trackingConfig) {
return;
}

let { vastXml, vastUrl, adId } = bid;
let impressionUrl;
let impressionId;
let errorUrl;

const impressionTracking = trackingConfig.impression;
const errorTracking = trackingConfig.error;

if (impressionTracking) {
impressionUrl = impressionTracking.url;
impressionId = impressionTracking.id || adId + '-impression';
}

if (errorTracking) {
errorUrl = errorTracking.url;
}

if (vastXml) {
vastXml = vastXmlEditor.getVastXmlWithTrackingNodes(vastXml, impressionUrl, impressionId, errorUrl);
} else if (vastUrl) {
vastXml = vastXmlEditor.buildVastWrapper(adId, vastUrl, impressionUrl, impressionId, errorUrl);
}

bid.vastXml = vastXml;
}
}

export function pbVideoFactory() {
const videoCore = videoCoreFactory();
const adServerCore = coreAdServerFactory();
const pbVideo = PbVideo(videoCore, config.getConfig, $$PREBID_GLOBAL$$, events, allVideoEvents, adServerCore);
const vastXmlEditor = vastXmlEditorFactory();
const pbVideo = PbVideo(videoCore, config.getConfig, $$PREBID_GLOBAL$$, events, allVideoEvents, adServerCore, vastXmlEditor);
pbVideo.init();
return pbVideo;
}
76 changes: 76 additions & 0 deletions modules/videoModule/shared/vastXmlBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

export function buildVastWrapper(adId, adTagUrl, impressionUrl, impressionId, errorUrl) {
let wrapperBody = getAdSystemNode('Prebid org', $$PREBID_GLOBAL$$.version);

if (adTagUrl) {
wrapperBody += getAdTagUriNode(adTagUrl);
}

if (impressionUrl) {
wrapperBody += getImpressionNode(impressionUrl, impressionId);
}

if (errorUrl) {
wrapperBody += getErrorNode(errorUrl);
}

return getVastNode(getAdNode(getWrapperNode(wrapperBody), adId), '4.2');
}

export function getVastNode(body, vastVersion) {
return getNode('VAST', body, { version: vastVersion });
}

export function getAdNode(body, adId) {
return getNode('Ad', body, { id: adId });
}

export function getWrapperNode(body) {
return getNode('Wrapper', body);
}

export function getAdSystemNode(system, version) {
return getNode('AdSystem', system, { version });
}

export function getAdTagUriNode(adTagUrl) {
return getUrlNode('VASTAdTagURI', adTagUrl);
}

export function getImpressionNode(pingUrl, id) {
return getUrlNode('Impression', pingUrl, { id });
}

export function getErrorNode(pingUrl) {
return getUrlNode('Error', pingUrl);
}

// Helpers

function getUrlNode(labelName, url, attributes) {
const body = `<![CDATA[${url}]]>`;
return getNode(labelName, body, attributes);
}

function getNode(labelName, body, attributes) {
const openingLabel = getOpeningLabel(labelName, attributes);
return `<${openingLabel}>${body}</${labelName}>`;
}

/*
attributes is a KVP Object.
*/
function getOpeningLabel(name, attributes) {
if (!attributes) {
return name;
}

return Object.keys(attributes).reduce((label, key) => {
const value = attributes[key];
if (!value) {
return label;
}

return label + ` ${key}="${value}"`;
}, name);
}
98 changes: 98 additions & 0 deletions modules/videoModule/shared/vastXmlEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { getErrorNode, getImpressionNode, buildVastWrapper } from './vastXmlBuilder.js';

export const XML_MIME_TYPE = 'application/xml';

export function VastXmlEditor(xmlUtil_) {
const xmlUtil = xmlUtil_;

function getVastXmlWithTrackingNodes(vastXml, impressionUrl, impressionId, errorUrl) {
const impressionDoc = getImpressionDoc(impressionUrl, impressionId);
const errorDoc = getErrorDoc(errorUrl);
if (!impressionDoc && !errorDoc) {
return vastXml;
}

const vastXmlDoc = xmlUtil.parse(vastXml);
const nodes = vastXmlDoc.querySelectorAll('InLine,Wrapper');
const nodeCount = nodes.length;
for (let i = 0; i < nodeCount; i++) {
const node = nodes[i];
// A copy of the child is required until we reach the last node.
const requiresCopy = i < nodeCount - 1;
appendChild(node, impressionDoc, requiresCopy);
appendChild(node, errorDoc, requiresCopy);
}

return xmlUtil.serialize(vastXmlDoc);
}

return {
getVastXmlWithTrackingNodes,
buildVastWrapper
}

function getImpressionDoc(impressionUrl, impressionId) {
if (!impressionUrl) {
return;
}

const impressionNode = getImpressionNode(impressionUrl, impressionId);
return xmlUtil.parse(impressionNode);
}

function getErrorDoc(errorUrl) {
if (!errorUrl) {
return;
}

const errorNode = getErrorNode(errorUrl);
return xmlUtil.parse(errorNode);
}

function appendChild(node, child, copy) {
if (!child) {
return;
}

const doc = copy ? child.cloneNode(true) : child;
node.appendChild(doc.documentElement);
}
}

function XMLUtil() {
let parser;
let serializer;

function getParser() {
if (!parser) {
// DOMParser instantiation is costly; instantiate only once throughout Prebid lifecycle.
parser = new DOMParser();
}
return parser;
}

function getSerializer() {
if (!serializer) {
// XMLSerializer instantiation is costly; instantiate only once throughout Prebid lifecycle.
serializer = new XMLSerializer();
}
return serializer;
}

function parse(xmlString) {
return getParser().parseFromString(xmlString, XML_MIME_TYPE);
}

function serialize(xmlDoc) {
return getSerializer().serializeToString(xmlDoc);
}

return {
parse,
serialize
};
}

export function vastXmlEditorFactory() {
return VastXmlEditor(XMLUtil());
}
Loading