Skip to content

Commit

Permalink
Add bidViewablityIO module
Browse files Browse the repository at this point in the history
- Emits a BID_VIEWABLE event for banner ads when a bid meets IAB
  viewable specifications, using the browsers IntersectionObserver API,
  if it is available
- adds the new module, markdown documentation, an integration example, and tests
  • Loading branch information
jsut committed Jul 8, 2021
1 parent 797c7ef commit b8ff1de
Show file tree
Hide file tree
Showing 4 changed files with 411 additions and 0 deletions.
136 changes: 136 additions & 0 deletions integrationExamples/postbid/bidViewabilityIO_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<html>
<head>
<script>
var pbjs = pbjs || {};
pbjs.que = pbjs.que || [];

(function() {
var pbjsEl = document.createElement("script");
pbjsEl.type = "text/javascript";
pbjsEl.async = true;
pbjsEl.src = '../../build/dev/prebid.js';
var pbjsTargetEl = document.getElementsByTagName("head")[0];
pbjsTargetEl.insertBefore(pbjsEl, pbjsTargetEl.firstChild);
})();

pbjs.que.push(function() {
var adUnits = [
{
code: 'regular_iframe',
mediaTypes: {
banner: {
sizes: [[300, 250]]
}
},
bids: [
{
bidder: 'appnexus',
params: {
placementId: 13144370
}
}
]
},
{
code: 'large_iframe',
mediaTypes: {
banner: {
sizes: [[970, 250]]
}
},
bids: [
{
bidder: 'appnexus',
params: {
placementId: 13144370
}
}
]
},
];

pbjs.setConfig({
bidderTimeout: 1000,
bidViewabilityIO: {
enabled: true,
}
});

pbjs.onEvent('adRenderSucceeded', ({bid}) => {
var p = document.createElement('p');
p.innerHTML = bid.adUnitCode + ' was rendered';
document.getElementById('notes').appendChild(p);
});

pbjs.onEvent('bidViewable', (bid) => {
var p = document.createElement('p');
p.innerHTML = bid.adUnitCode + ' was viewed';
document.getElementById('notes').appendChild(p);
});

pbjs.addAdUnits(adUnits);

pbjs.requestBids({
bidsBackHandler: function(bidResponses) {
Object.keys(bidResponses).forEach(adUnitCode => {
var highestCpmBids = pbjs.getHighestCpmBids(adUnitCode);
var winner = highestCpmBids.pop();
if (winner && winner.mediaType === 'banner') {
var iframe = document.getElementById(adUnitCode);
var iframeDoc = iframe.contentWindow.document;
pbjs.renderAd(iframeDoc, winner.adId);
} else if (winner) {
iframe.width = 300;
iframe.height = 300;
iframeDoc.write('<head></head><body>unsupported mediaType</body>');
iframeDoc.close();
} else {
iframe.width = 300;
iframe.height = 300;
iframeDoc.write('<head></head><body>no winner</body>');
iframeDoc.close();
}
});
}
})
});

</script>

</head>

<body>
<div id="notes" style="position: fixed; right: 0; width: 50%; height: 100%;"></div>

<div style="height: 100%"></div>

<iframe id='regular_iframe'
FRAMEBORDER="0"
SCROLLING="no"
MARGINHEIGHT="0"
MARGINWIDTH="0"
TOPMARGIN="0"
LEFTMARGIN="0"
ALLOWTRANSPARENCY="true"
WIDTH="0"
HEIGHT="0">
</iframe>

<div style="height: 100%"></div>

<iframe id='large_iframe'
FRAMEBORDER="0"
SCROLLING="no"
MARGINHEIGHT="0"
MARGINWIDTH="0"
TOPMARGIN="0"
LEFTMARGIN="0"
ALLOWTRANSPARENCY="true"
WIDTH="0"
HEIGHT="0">
</iframe>

<div style="height: 100%"></div>

</body>
</html>
85 changes: 85 additions & 0 deletions modules/bidViewabilityIO.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { config } from '../src/config.js';
import * as events from '../src/events.js';
import { EVENTS } from '../src/constants.json';
import { logMessage } from '../src/utils.js';

const MODULE_NAME = 'bidViewabilityIO';
const CONFIG_ENABLED = 'enabled';

// IAB numbers from: https://support.google.com/admanager/answer/4524488?hl=en
const IAB_VIEWABLE_DISPLAY_TIME = 1000;
const IAB_VIEWABLE_DISPLAY_LARGE_PX = 242000;
export const IAB_VIEWABLE_DISPLAY_THRESHOLD = 0.5
export const IAB_VIEWABLE_DISPLAY_LARGE_THRESHOLD = 0.3;

const CLIENT_SUPPORTS_IO = window.IntersectionObserver && window.IntersectionObserverEntry && window.IntersectionObserverEntry.prototype &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype;

const supportedMediaTypes = [
'banner'
];

export let isSupportedMediaType = (bid) => {
return supportedMediaTypes.includes(bid.mediaType);
}

// returns options for the iO that detects if the ad is viewable
export let getViewableOptions = (bid) => {
if (bid.mediaType === 'banner') {
return {
root: null,
rootMargin: '0px',
threshold: bid.width * bid.height > IAB_VIEWABLE_DISPLAY_LARGE_PX ? IAB_VIEWABLE_DISPLAY_LARGE_THRESHOLD : IAB_VIEWABLE_DISPLAY_THRESHOLD
}
}
}

// markViewed returns a function what will be executed when an ad satisifes the viewable iO
export let markViewed = (bid, entry, observer) => {
return () => {
observer.unobserve(entry.target);
events.emit(EVENTS.BID_VIEWABLE, bid);
logMessage(`id: ${entry.target.getAttribute('id')} code: ${bid.adUnitCode} was viewed`);
}
}

// viewCallbackFactory creates the callback used by the viewable IntersectionObserver.
// When an ad comes into view, it sets a timeout for a function to be executed
// when that ad would be considered viewed per the IAB specs. The bid that was rendered
// is passed into the factory, so it can pass it into markViewed, so that it can be included
// in the BID_VIEWABLE event data. If the ad leaves view before the timer goes off, the setTimeout
// is cancelled, an the bid will not be marked as viewed. There's probably some kind of race-ish
// thing going on between IO and setTimeout but this isn't going to be perfect, it's just going to
// be pretty good.
export let viewCallbackFactory = (bid) => {
return (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
logMessage(`viewable timer starting for id: ${entry.target.getAttribute('id')} code: ${bid.adUnitCode}`);
entry.target.view_tracker = setTimeout(markViewed(bid, entry, observer), IAB_VIEWABLE_DISPLAY_TIME);
} else {
logMessage(`id: ${entry.target.getAttribute('id')} code: ${bid.adUnitCode} is out of view`);
if (entry.target.view_tracker) {
clearTimeout(entry.target.view_tracker);
logMessage(`viewable timer stopped for id: ${entry.target.getAttribute('id')} code: ${bid.adUnitCode}`);
}
}
});
};
};

export let init = () => {
events.on(EVENTS.AD_RENDER_SUCCEEDED, ({doc, bid, id}) => {
// read the config for the module
const globalModuleConfig = config.getConfig(MODULE_NAME) || {};
// do nothing if module-config.enabled is not set to true
// this way we are adding a way for bidders to know (using pbjs.getConfig('bidViewability').enabled === true) whether this module is added in build and is enabled
if (globalModuleConfig[CONFIG_ENABLED] && CLIENT_SUPPORTS_IO && isSupportedMediaType(bid)) {
let viewable = new IntersectionObserver(viewCallbackFactory(bid), getViewableOptions(bid));
let element = document.getElementById(bid.adUnitCode);
viewable.observe(element);
}
});
}

init()
41 changes: 41 additions & 0 deletions modules/bidViewabilityIO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Overview

Module Name: bidViewabilityIO

Purpose: Emit a BID_VIEWABLE event when a bid becomes viewable using the browsers IntersectionObserver API

Maintainer: adam.prime@alum.utoronto.ca

# Description
- This module will trigger a BID_VIEWABLE event which other modules, adapters or publisher code can use to get a sense of viewability
- You can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewabilityIO')```
- Viewability, as measured by this module is not perfect, nor should it be expected to be.
- The module does not require any specific ad server, or an adserver at all.

# Limitations

- Currently only supports the banner mediaType
- Assumes that the adUnitCode of the ad is also the id attribute of the element that the ad is rendered into.
- Does not make any attempt to ensure that the ad inside that element is itself visible. It assumes that the publisher is operating in good faith.

# Params
- enabled [required] [type: boolean, default: false], when set to true, the module will emit BID_VIEWABLE when applicable

# Example of consuming BID_VIEWABLE event
```
pbjs.onEvent('bidViewable', function(bid){
console.log('got bid details in bidViewable event', bid);
});
```

# Example of using config
```
pbjs.setConfig({
bidViewability: {
enabled: true,
}
});
```

An example implmentation without an ad server can be found in integrationExamples/postbid/bidViewabilityIO_example.html
Loading

0 comments on commit b8ff1de

Please sign in to comment.