Skip to content

Commit

Permalink
Add viewability to e-planning bid adapter (#4471)
Browse files Browse the repository at this point in the history
* add viewability e-planning bid adapter

* change viewability and spaces name

* fix in viewability

* fix in viewability
  • Loading branch information
fndigrazia authored and Mike Chowla committed Dec 10, 2019
1 parent df35628 commit a7c5b80
Show file tree
Hide file tree
Showing 2 changed files with 609 additions and 25 deletions.
263 changes: 243 additions & 20 deletions modules/eplanningBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ const NET_REVENUE = true;
const TTL = 120;
const NULL_SIZE = '1x1';
const FILE = 'file';
const STORAGE_RENDER_PREFIX = 'pbsr_';
const STORAGE_VIEW_PREFIX = 'pbvi_';

export const spec = {
code: BIDDER_CODE,

isBidRequestValid: function(bid) {
return Boolean(bid.params.ci) || Boolean(bid.params.t);
},
Expand All @@ -26,21 +29,26 @@ export const spec = {
let params;
const urlConfig = getUrlConfig(bidRequests);
const pcrs = getCharset();

const spaces = getSpaces(bidRequests);
if (urlConfig.t) {
url = urlConfig.isv + '/layers/t_pbjs_2.json';
params = {};
} else {
url = '//' + (urlConfig.sv || DEFAULT_SV) + '/hb/1/' + urlConfig.ci + '/' + dfpClientId + '/' + (utils.getTopWindowLocation().hostname || FILE) + '/' + sec;
const referrerUrl = utils.getTopWindowReferrer();
const spacesString = getSpacesString(bidRequests);

if (utils.hasLocalStorage()) {
registerViewabilityAllBids(bidRequests);
}

params = {
rnd: rnd,
e: spacesString,
e: spaces.str,
ur: utils.getTopWindowUrl() || FILE,
r: 'pbjs',
pbv: '$prebid.version$',
ncb: '1'
ncb: '1',
vs: spaces.vs
};

if (pcrs) {
Expand All @@ -56,7 +64,7 @@ export const spec = {
method: method,
url: url,
data: params,
adUnitToBidId: getBidIdMap(bidRequests),
adUnitToBidId: spaces.map,
};
},
interpretResponse: function(serverResponse, request) {
Expand Down Expand Up @@ -111,9 +119,6 @@ export const spec = {
},
}

function cleanName(name) {
return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)|:/g, '_').replace(/^_+|_+$/g, '');
}
function getUrlConfig(bidRequests) {
if (isTestRequest(bidRequests)) {
return getTestConfig(bidRequests.filter(br => br.params.t));
Expand All @@ -135,9 +140,12 @@ function getUrlConfig(bidRequests) {
return config;
}
function isTestRequest(bidRequests) {
let isTest = false;
bidRequests.forEach(bid => isTest = bid.params.t);
return isTest;
for (let i = 0; i < bidRequests.length; i++) {
if (bidRequests[i].params.t) {
return true;
}
}
return false;
}
function getTestConfig(bidRequests) {
let isv;
Expand All @@ -147,14 +155,55 @@ function getTestConfig(bidRequests) {
isv: '//' + (isv || DEFAULT_ISV)
};
}
function getSpacesString(bids) {
const spacesString = bids.map(bid =>
cleanName(bid.adUnitCode) + ':' + (bid.sizes && bid.sizes.length ? utils.parseSizesInput(bid.sizes).join(',') : NULL_SIZE)
).join('+');

return spacesString;
function getSize(bid, first) {
return bid.sizes && bid.sizes.length ? utils.parseSizesInput(first ? bid.sizes[0] : bid.sizes).join(',') : NULL_SIZE;
}

function getSpacesStruct(bids) {
let e = {};
bids.forEach(bid => {
let size = getSize(bid, true);
e[size] = e[size] ? e[size] : [];
e[size].push(bid);
});

return e;
}

function getSpaces(bidRequests) {
let spacesStruct = getSpacesStruct(bidRequests);
let es = {str: '', vs: '', map: {}};
es.str = Object.keys(spacesStruct).map(size => spacesStruct[size].map((bid, i) => {
es.vs += getVs(bid);
let name = getSize(bid, true) + '_' + i;
es.map[name] = bid.bidId;
return name + ':' + getSize(bid);
}).join('+')).join('+');
return es;
}

function getVs(bid) {
let s;
let vs = '';
if (utils.hasLocalStorage()) {
s = getViewabilityData(bid);
vs += s.render >= 4 ? s.ratio.toString(16) : 'F';
} else {
vs += 'F';
}
return vs;
}

function getViewabilityData(bid) {
let r = utils.getDataFromLocalStorage(STORAGE_RENDER_PREFIX + bid.adUnitCode) || 0;
let v = utils.getDataFromLocalStorage(STORAGE_VIEW_PREFIX + bid.adUnitCode) || 0;
let ratio = r > 0 ? (v / r) : 0;
return {
render: r,
ratio: window.parseInt(ratio * 10, 10)
};
}
function getCharset() {
try {
return window.top.document.charset || window.top.document.characterSet;
Expand All @@ -163,10 +212,184 @@ function getCharset() {
}
}

function getBidIdMap(bidRequests) {
let map = {};
bidRequests.forEach(bid => map[cleanName(bid.adUnitCode)] = bid.bidId);
return map;
function waitForElementsPresent(elements) {
const observer = new MutationObserver(function (mutationList, observer) {
if (mutationList && Array.isArray(mutationList)) {
mutationList.forEach(mr => {
if (mr && mr.addedNodes && Array.isArray(mr.addedNodes)) {
mr.addedNodes.forEach(ad => {
let index = elements.indexOf(ad.id);
if (index >= 0) {
registerViewability(ad);
elements.splice(index, 1);
if (!elements.length) {
observer.disconnect();
}
}
});
}
});
}
});
const config = {childList: true, subtree: true, characterData: true, attributes: true, attributeOldValue: true};
observer.observe(document.body, config);
}

function registerViewability(div) {
visibilityHandler({
name: div.id,
div: div
});
}

function registerViewabilityAllBids(bids) {
let elementsNotPresent = [];
bids.forEach(bid => {
let div = document.getElementById(bid.adUnitCode);
if (div) {
registerViewability(div);
} else {
elementsNotPresent.push(bid.adUnitCode);
}
});
if (elementsNotPresent.length) {
waitForElementsPresent(elementsNotPresent);
}
}

function getViewabilityTracker() {
let TIME_PARTITIONS = 5;
let VIEWABILITY_TIME = 1000;
let VIEWABILITY_MIN_RATIO = 0.5;
let publicApi;
let context;

function segmentIsOutsideTheVisibleRange(visibleRangeEnd, p1, p2) {
return p1 > visibleRangeEnd || p2 < 0;
}

function segmentBeginsBeforeTheVisibleRange(p1) {
return p1 < 0;
}

function segmentEndsAfterTheVisibleRange(visibleRangeEnd, p2) {
return p2 < visibleRangeEnd;
}

function axialVisibilityRatio(visibleRangeEnd, p1, p2) {
let visibilityRatio = 0;
if (!segmentIsOutsideTheVisibleRange(visibleRangeEnd, p1, p2)) {
if (segmentBeginsBeforeTheVisibleRange(p1)) {
visibilityRatio = p2 / (p2 - p1);
} else {
visibilityRatio = segmentEndsAfterTheVisibleRange(visibleRangeEnd, p2) ? 1 : (visibleRangeEnd - p1) / (p2 - p1);
}
}
return visibilityRatio;
}

function isNotHiddenByNonFriendlyIframe() {
return (window === window.top) || window.frameElement;
}

function defineContext(e) {
context = e && window.document.body.contains(e) ? window : (window.top.document.body.contains(e) ? top : undefined);
return context;
}

function getContext(e) {
return context;
}

function verticalVisibilityRatio(position) {
return axialVisibilityRatio(getContext().innerHeight, position.top, position.bottom);
}

function horizontalVisibilityRatio(position) {
return axialVisibilityRatio(getContext().innerWidth, position.left, position.right);
}

function itIsNotHiddenByBannerAreaPosition(e) {
let position = e.getBoundingClientRect();
return (verticalVisibilityRatio(position) * horizontalVisibilityRatio(position)) > VIEWABILITY_MIN_RATIO;
}

function itIsNotHiddenByDisplayStyleCascade(e) {
return e.offsetHeight > 0 && e.offsetWidth > 0;
}

function itIsNotHiddenByOpacityStyleCascade(e) {
let s = e.style;
let p = e.parentNode;
return !(s && parseFloat(s.opacity) === 0) && (!p || itIsNotHiddenByOpacityStyleCascade(p));
}

function itIsNotHiddenByVisibilityStyleCascade(e) {
return getContext().getComputedStyle(e).visibility !== 'hidden';
}

function itIsNotHiddenByTabFocus() {
return getContext().top.document.hasFocus();
}

function isDefined(e) {
return (e !== null) && (typeof e !== 'undefined');
}

function itIsNotHiddenByOrphanBranch() {
return isDefined(getContext());
}

function isContextInAnIframe() {
return isDefined(getContext().frameElement);
}

function processIntervalVisibilityStatus(elapsedVisibleIntervals, element, callback) {
let visibleIntervals = isVisible(element) ? (elapsedVisibleIntervals + 1) : 0;
if (visibleIntervals === TIME_PARTITIONS) {
callback();
} else {
setTimeout(processIntervalVisibilityStatus.bind(this, visibleIntervals, element, callback), VIEWABILITY_TIME / TIME_PARTITIONS);
}
}

function isVisible(element) {
defineContext(element);
return isNotHiddenByNonFriendlyIframe() &&
itIsNotHiddenByOrphanBranch() &&
itIsNotHiddenByTabFocus() &&
itIsNotHiddenByDisplayStyleCascade(element) &&
itIsNotHiddenByVisibilityStyleCascade(element) &&
itIsNotHiddenByOpacityStyleCascade(element) &&
itIsNotHiddenByBannerAreaPosition(element) &&
(!isContextInAnIframe() || isVisible(getContext().frameElement));
}

publicApi = {
isVisible: isVisible,
onView: processIntervalVisibilityStatus.bind(this, 0)
};

return publicApi;
};

function visibilityHandler(obj) {
if (obj.div) {
registerAuction(STORAGE_RENDER_PREFIX + obj.name);
getViewabilityTracker().onView(obj.div, registerAuction.bind(undefined, STORAGE_VIEW_PREFIX + obj.name));
}
}

function registerAuction(storageID) {
let value;
try {
value = utils.getDataFromLocalStorage(storageID);
value = value ? window.parseInt(value, 10) + 1 : 1;
utils.setDataInLocalStorage(storageID, value);
} catch (exc) {
return false;
}

return true;
}
registerBidder(spec);
Loading

0 comments on commit a7c5b80

Please sign in to comment.