Skip to content

Commit

Permalink
33Across: add viewability (prebid#3084)
Browse files Browse the repository at this point in the history
* check gdpr in buildRequest

* User sync based on whether gdpr applies or not

* check if consent data exists during user sync

* split user sync into further branches: 1) when gdpr does not apply 2) when consent data is unavailable

* contribute viewability to ttxRequest

* update tests

* remove window mock from tests

* use local variables

* introduce ServerRequestBuilder

* add withOptions() method to ServerRequestBuilder

* add semicolons

* sync up package-lock.json with upstream/master

* stub window.top in tests

* introduce getTopWindowSize() for test purpose

* reformat code

* add withSite() method to TtxRequestBuilder

add withSite() method to TtxRequestBuilder

* add isIframe() and _isViewabilityMeasurable()

* handle NON_MEASURABLE viewability in nested iframes

* consider page visibility, stub utils functions getWindowTop() and getWindowSelf()

* contribute viewability as 0 for inactive tab
  • Loading branch information
glebglushtsov authored and Pedro López Jiménez committed Mar 18, 2019
1 parent 3b804f2 commit 7815cdc
Show file tree
Hide file tree
Showing 2 changed files with 514 additions and 320 deletions.
165 changes: 148 additions & 17 deletions modules/33acrossBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { uniques } from 'src/utils';
import * as utils from 'src/utils';

const { registerBidder } = require('../src/adapters/bidderFactory');
const { config } = require('../src/config');

const BIDDER_CODE = '33across';
const END_POINT = 'https://ssc.33across.com/api/v1/hb';
const SYNC_ENDPOINT = 'https://de.tynt.com/deb/v2?m=xch&rt=html';

const adapterState = {};

const NON_MEASURABLE = 'nm';

// All this assumes that only one bid is ever returned by ttx
function _createBidResponse(response) {
return {
Expand All @@ -23,26 +27,45 @@ function _createBidResponse(response) {
}
}

function _isViewabilityMeasurable() {
return !_isIframe();
}

function _getViewability(element, topWin, { w, h } = {}) {
return utils.getWindowTop().document.visibilityState === 'visible'
? _getPercentInView(element, topWin, { w, h })
: 0;
}

// Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request
// NOTE: At this point, TTX only accepts request for a single impression
function _createServerRequest(bidRequest, gdprConsent) {
const ttxRequest = {};
const params = bidRequest.params;
const element = document.getElementById(bidRequest.adUnitCode);
const sizes = _transformSizes(bidRequest.sizes);
const minSize = _getMinSize(sizes);

const viewabilityAmount = _isViewabilityMeasurable()
? _getViewability(element, utils.getWindowTop(), minSize)
: NON_MEASURABLE;

const contributeViewability = ViewabilityContributor(viewabilityAmount);

/*
* Infer data for the request payload
*/
ttxRequest.imp = [];
ttxRequest.imp[0] = {
banner: {
format: bidRequest.sizes.map(_getFormatSize)
format: sizes.map(size => Object.assign(size, {ext: {}}))
},
ext: {
ttx: {
prod: params.productId
}
}
}
};
ttxRequest.site = { id: params.siteId };

// Go ahead send the bidId in request to 33exchange so it's kept track of in the bid response and
Expand All @@ -54,12 +77,12 @@ function _createServerRequest(bidRequest, gdprConsent) {
ext: {
consent: gdprConsent.consentString
}
}
};
ttxRequest.regs = {
ext: {
gdpr: (gdprConsent.gdprApplies === true) ? 1 : 0
}
}
};

// Finally, set the openRTB 'test' param if this is to be a test bid
if (params.test === 1) {
Expand All @@ -81,7 +104,7 @@ function _createServerRequest(bidRequest, gdprConsent) {
return {
'method': 'POST',
'url': url,
'data': JSON.stringify(ttxRequest),
'data': JSON.stringify(contributeViewability(ttxRequest)),
'options': options
}
}
Expand All @@ -97,11 +120,118 @@ function _createSync(siteId) {
}
}

function _getFormatSize(sizeArr) {
function _getSize(size) {
return {
w: sizeArr[0],
h: sizeArr[1],
ext: {}
w: parseInt(size[0], 10),
h: parseInt(size[1], 10)
}
}

function _getMinSize(sizes) {
return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min);
}

function _getBoundingBox(element, { w, h } = {}) {
let { width, height, left, top, right, bottom } = element.getBoundingClientRect();

if ((width === 0 || height === 0) && w && h) {
width = w;
height = h;
right = left + w;
bottom = top + h;
}

return { width, height, left, top, right, bottom };
}

function _transformSizes(sizes) {
if (utils.isArray(sizes) && sizes.length === 2 && !utils.isArray(sizes[0])) {
return [_getSize(sizes)];
}

return sizes.map(_getSize);
}

function _getIntersectionOfRects(rects) {
const bbox = {
left: rects[0].left,
right: rects[0].right,
top: rects[0].top,
bottom: rects[0].bottom
};

for (let i = 1; i < rects.length; ++i) {
bbox.left = Math.max(bbox.left, rects[i].left);
bbox.right = Math.min(bbox.right, rects[i].right);

if (bbox.left >= bbox.right) {
return null;
}

bbox.top = Math.max(bbox.top, rects[i].top);
bbox.bottom = Math.min(bbox.bottom, rects[i].bottom);

if (bbox.top >= bbox.bottom) {
return null;
}
}

bbox.width = bbox.right - bbox.left;
bbox.height = bbox.bottom - bbox.top;

return bbox;
}

function _getPercentInView(element, topWin, { w, h } = {}) {
const elementBoundingBox = _getBoundingBox(element, { w, h });

// Obtain the intersection of the element and the viewport
const elementInViewBoundingBox = _getIntersectionOfRects([ {
left: 0,
top: 0,
right: topWin.innerWidth,
bottom: topWin.innerHeight
}, elementBoundingBox ]);

let elementInViewArea, elementTotalArea;

if (elementInViewBoundingBox !== null) {
// Some or all of the element is in view
elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height;
elementTotalArea = elementBoundingBox.width * elementBoundingBox.height;

return ((elementInViewArea / elementTotalArea) * 100);
}

// No overlap between element and the viewport; therefore, the element
// lies completely out of view
return 0;
}

/**
* Viewability contribution to request..
*/
function ViewabilityContributor(viewabilityAmount) {
function contributeViewability(ttxRequest) {
const req = Object.assign({}, ttxRequest);
const imp = req.imp = req.imp.map(impItem => Object.assign({}, impItem));
const banner = imp[0].banner = Object.assign({}, imp[0].banner);
const ext = banner.ext = Object.assign({}, banner.ext);
const ttx = ext.ttx = Object.assign({}, ext.ttx);

ttx.viewability = { amount: isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount) };

return req;
}

return contributeViewability;
}

function _isIframe() {
try {
return utils.getWindowSelf() !== utils.getWindowTop();
} catch (e) {
return true;
}
}

Expand All @@ -122,9 +252,9 @@ function isBidRequestValid(bid) {
// - the server, at this point, also doesn't need the consent string to handle gdpr compliance. So passing
// value whether set or not, for the sake of future dev.
function buildRequests(bidRequests, bidderRequest) {
const gdprConsent = Object.assign({ consentString: undefined, gdprApplies: false }, bidderRequest && bidderRequest.gdprConsent)
const gdprConsent = Object.assign({ consentString: undefined, gdprApplies: false }, bidderRequest && bidderRequest.gdprConsent);

adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(uniques);
adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(utils.uniques);

return bidRequests.map((req) => {
return _createServerRequest(req, gdprConsent);
Expand Down Expand Up @@ -153,14 +283,15 @@ function getUserSyncs(syncOptions, responses, gdprConsent) {
}
}

const spec = {
export const spec = {
NON_MEASURABLE,

code: BIDDER_CODE,

isBidRequestValid,
buildRequests,
interpretResponse,
getUserSyncs
}
getUserSyncs,
};

registerBidder(spec);

module.exports = spec;
Loading

0 comments on commit 7815cdc

Please sign in to comment.