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

bidViewablityIO Module: add new submodule for detecting viewability without ad server dependancies #7151

Merged
merged 7 commits into from
Aug 10, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
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();
var iframe = document.getElementById(adUnitCode);
var iframeDoc = iframe.contentWindow.document;
if (winner && winner.mediaType === 'banner') {
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);
jsut marked this conversation as resolved.
Show resolved Hide resolved
}

// 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`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is me being picky, but when I am testing / debugging pub pages I like when modules emit their name inside their console logs, makes it easy to filter.

You could wrap logMessage in your own function which just adds in MODULE_NAME on each message you send.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That doesn't sound picky, it sounds like an excellent practice.

}
}

// 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it would be better to never register for this event until the user enables the config.

So your init function could subscribe to setConfig and then once it is enabled, it then will add the event listener.

config.getConfig(MODULE_NAME, conf => {
   // if conf enabled then 
   events.on(EVENTS.AD_RENDER_SUCCEEDED, ({doc, bid, id}) => {
});

That way the event callback is not executing every single time this module is included.

Only when it is actually turned on.

I would also add the CLIENT_SUPPORTS_IO check inside of that config subscribe as well since no need to run it when it is not supported

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 just pushed a change that doesn't do the subscribe thing, then actually read all these words and found out that this subscribe thing is a thing. I'll have a look it doing it as you describe this afternoon probably.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok cool.

So basically if you do the following

config.getConfig(MODULE_NAME, CALLBACK_FUNCTION);

Prebid will execute your CALLBACK_FUNCTION each time pbjs.setConfig is called with your MODULE_NAME

So for your use case, we only want to register the event listener if the user / pub runs pbjs.setConfig and turns on bidViewability.

so your callback can do something like:

config.getConfig(MODULE_NAME, conf => {
   const  globalModuleConfig = conf.bidViewabilityIO;
   if (globalModuleConfig[CONFIG_ENABLED] && CLIENT_SUPPORTS_IO) {
      // register your event code
      events.on(EVENTS.AD_RENDER_SUCCEEDED, ({doc, bid, id}) => {
   }
});

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