Skip to content

Commit

Permalink
Prebid core: accept and propagate AD_RENDER_FAILED / AD_RENDER_SUCCEE…
Browse files Browse the repository at this point in the history
…DED events from cross-origin ads (#7917)

* Prebid core: accept and propagate AD_RENDER_FAILED / AD_RENDER_SUCCEEDED events from cross-origin ads

This adds a new type of cross-origin message ('Prebid Event') to allow PUC and other cross-origin renderings to correctly report rendering result, and generates the appropriate events.

Addresses #7702

Related PUC changes: prebid/prebid-universal-creative#152
Documentation changes TBD

* Emit AD_RENDER_FAILED on x-origin communication errors

* Do not consider bid won if x-origin render fails

* Cleanup (address #7917 (review))
  • Loading branch information
dgirardi authored Jan 20, 2022
1 parent 1454067 commit 4ad4024
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 89 deletions.
79 changes: 53 additions & 26 deletions integrationExamples/gpt/x-domain/creative.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,40 @@
// this script can be returned by an ad server delivering a cross domain iframe, into which the
// creative will be rendered, e.g. DFP delivering a SafeFrame

let windowLocation = window.location;
var urlParser = document.createElement('a');
const windowLocation = window.location;
const urlParser = document.createElement('a');
urlParser.href = '%%PATTERN:url%%';
var publisherDomain = urlParser.protocol + '//' + urlParser.hostname;
const publisherDomain = urlParser.protocol + '//' + urlParser.hostname;
const adId = '%%PATTERN:hb_adid%%';

function renderAd(ev) {
var key = ev.message ? 'message' : 'data';
var adObject = {};
try {
adObject = JSON.parse(ev[key]);
} catch (e) {
return;
}
const key = ev.message ? 'message' : 'data';
let adObject = {};
try {
adObject = JSON.parse(ev[key]);
} catch (e) {
return;
}

var origin = ev.origin || ev.originalEvent.origin;
if (adObject.message && adObject.message === 'Prebid Response' &&
publisherDomain === origin &&
adObject.adId === '%%PATTERN:hb_adid%%' &&
(adObject.ad || adObject.adUrl)) {
var body = window.document.body;
var ad = adObject.ad;
var url = adObject.adUrl;
var width = adObject.width;
var height = adObject.height;
const origin = ev.origin || ev.originalEvent.origin;
if (adObject.message && adObject.message === 'Prebid Response' &&
publisherDomain === origin &&
adObject.adId === adId) {
try {
const body = window.document.body;
const ad = adObject.ad;
const url = adObject.adUrl;
const width = adObject.width;
const height = adObject.height;

if (adObject.mediaType === 'video') {
signalRenderResult(false, {
reason: 'preventWritingOnMainDocument',
message: `Cannot render video ad ${adId}`
});
console.log('Error trying to write ad.');
} else

if (ad) {
var frame = document.createElement('iframe');
} else if (ad) {
const frame = document.createElement('iframe');
frame.setAttribute('FRAMEBORDER', 0);
frame.setAttribute('SCROLLING', 'no');
frame.setAttribute('MARGINHEIGHT', 0);
Expand All @@ -46,18 +49,42 @@
frame.contentDocument.open();
frame.contentDocument.write(ad);
frame.contentDocument.close();
signalRenderResult(true);
} else if (url) {
body.insertAdjacentHTML('beforeend', '<IFRAME SRC="' + url + '" FRAMEBORDER="0" SCROLLING="no" MARGINHEIGHT="0" MARGINWIDTH="0" TOPMARGIN="0" LEFTMARGIN="0" ALLOWTRANSPARENCY="true" WIDTH="' + width + '" HEIGHT="' + height + '"></IFRAME>');
signalRenderResult(true);
} else {
console.log('Error trying to write ad. No ad for bid response id: ' + id);
signalRenderResult(false, {
reason: 'noAd',
message: `No ad for ${adId}`
});
console.log(`Error trying to write ad. No ad markup or adUrl for ${adId}`);
}
} catch (e) {
signalRenderResult(false, {reason: 'exception', message: e.message});
console.log(`Error in rendering ad`, e);
}
}

function signalRenderResult(success, {reason, message} = {}) {
const payload = {
message: 'Prebid Event',
adId,
event: success ? 'adRenderSucceeded' : 'adRenderFailed',
}
if (!success) {
payload.info = {reason, message};
}
ev.source.postMessage(JSON.stringify(payload), publisherDomain);
}

}


function requestAdFromPrebid() {
var message = JSON.stringify({
message: 'Prebid Request',
adId: '%%PATTERN:hb_adid%%'
adId
});
window.parent.postMessage(message, publisherDomain);
}
Expand Down
38 changes: 38 additions & 0 deletions src/adRendering.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {logError} from './utils.js';
import events from './events.js';
import CONSTANTS from './constants.json';

const {AD_RENDER_FAILED, AD_RENDER_SUCCEEDED} = CONSTANTS.EVENTS;

/**
* Emit the AD_RENDER_FAILED event.
*
* @param reason one of the values in CONSTANTS.AD_RENDER_FAILED_REASON
* @param message failure description
* @param bid? bid response object that failed to render
* @param id? adId that failed to render
*/
export function emitAdRenderFail({ reason, message, bid, id }) {
const data = { reason, message };
if (bid) data.bid = bid;
if (id) data.adId = id;

logError(message);
events.emit(AD_RENDER_FAILED, data);
}

/**
* Emit the AD_RENDER_SUCCEEDED event.
*
* @param doc document object that was used to `.write` the ad. Should be `null` if unavailable (e.g. for documents in
* a cross-origin frame).
* @param bid bid response object for the ad that was rendered
* @param id adId that was rendered.
*/
export function emitAdRenderSucceeded({ doc, bid, id }) {
const data = { doc };
if (bid) data.bid = bid;
if (id) data.adId = id;

events.emit(AD_RENDER_SUCCEEDED, data);
}
20 changes: 2 additions & 18 deletions src/prebid.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { adunitCounter } from './adUnits.js';
import { executeRenderer, isRendererRequired } from './Renderer.js';
import { createBid } from './bidfactory.js';
import { storageCallbacks } from './storageManager.js';
import { emitAdRenderSucceeded, emitAdRenderFail } from './adRendering.js';

const $$PREBID_GLOBAL$$ = getGlobal();
const CONSTANTS = require('./constants.json');
Expand All @@ -27,7 +28,7 @@ const events = require('./events.js');
const { triggerUserSyncs } = userSync;

/* private variables */
const { ADD_AD_UNITS, BID_WON, REQUEST_BIDS, SET_TARGETING, AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER } = CONSTANTS.EVENTS;
const { ADD_AD_UNITS, BID_WON, REQUEST_BIDS, SET_TARGETING, STALE_RENDER } = CONSTANTS.EVENTS;
const { PREVENT_WRITING_ON_MAIN_DOCUMENT, NO_AD, EXCEPTION, CANNOT_FIND_AD, MISSING_DOC_OR_ADID } = CONSTANTS.AD_RENDER_FAILED_REASON;

const eventValidators = {
Expand Down Expand Up @@ -385,23 +386,6 @@ $$PREBID_GLOBAL$$.setTargetingForAst = function (adUnitCodes) {
events.emit(SET_TARGETING, targeting.getAllTargeting());
};

function emitAdRenderFail({ reason, message, bid, id }) {
const data = { reason, message };
if (bid) data.bid = bid;
if (id) data.adId = id;

logError(message);
events.emit(AD_RENDER_FAILED, data);
}

function emitAdRenderSucceeded({ doc, bid, id }) {
const data = { doc };
if (bid) data.bid = bid;
if (id) data.adId = id;

events.emit(AD_RENDER_SUCCEEDED, data);
}

/**
* This function will check for presence of given node in given parent. If not present - will inject it.
* @param {Node} node node, whose existance is in question
Expand Down
153 changes: 111 additions & 42 deletions src/secureCreatives.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@
*/

import events from './events.js';
import { fireNativeTrackers, getAssetMessage, getAllAssetsMessage } from './native.js';
import {fireNativeTrackers, getAllAssetsMessage, getAssetMessage} from './native.js';
import constants from './constants.json';
import { logWarn, replaceAuctionPrice, deepAccess, isGptPubadsDefined, isApnGetTagDefined } from './utils.js';
import { auctionManager } from './auctionManager.js';
import {deepAccess, isApnGetTagDefined, isGptPubadsDefined, logError, logWarn, replaceAuctionPrice} from './utils.js';
import {auctionManager} from './auctionManager.js';
import find from 'core-js-pure/features/array/find.js';
import { isRendererRequired, executeRenderer } from './Renderer.js';
import {executeRenderer, isRendererRequired} from './Renderer.js';
import includes from 'core-js-pure/features/array/includes.js';
import { config } from './config.js';
import {config} from './config.js';
import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js';

const BID_WON = constants.EVENTS.BID_WON;
const STALE_RENDER = constants.EVENTS.STALE_RENDER;

const HANDLER_MAP = {
'Prebid Request': handleRenderRequest,
'Prebid Native': handleNativeRequest,
'Prebid Event': handleEventRequest,
}

export function listenMessagesFromCreative() {
window.addEventListener('message', receiveMessage, false);
}
Expand All @@ -29,52 +36,114 @@ export function receiveMessage(ev) {
return;
}

if (data && data.adId) {
if (data && data.adId && data.message) {
const adObject = find(auctionManager.getBidsReceived(), function (bid) {
return bid.adId === data.adId;
});
if (HANDLER_MAP.hasOwnProperty(data.message)) {
HANDLER_MAP[data.message](ev, data, adObject);
}
}
}

if (adObject && data.message === 'Prebid Request') {
if (adObject.status === constants.BID_STATUS.RENDERED) {
logWarn(`Ad id ${adObject.adId} has been rendered before`);
events.emit(STALE_RENDER, adObject);
if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) {
return;
}
}
function handleRenderRequest(ev, data, adObject) {
if (adObject == null) {
emitAdRenderFail({
reason: constants.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD,
message: `Cannot find ad '${data.adId}' for cross-origin render request`,
id: data.adId
});
return;
}
if (adObject.status === constants.BID_STATUS.RENDERED) {
logWarn(`Ad id ${adObject.adId} has been rendered before`);
events.emit(STALE_RENDER, adObject);
if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) {
return;
}
}

_sendAdToCreative(adObject, ev);
try {
_sendAdToCreative(adObject, ev);
} catch (e) {
emitAdRenderFail({
reason: constants.AD_RENDER_FAILED_REASON.EXCEPTION,
message: e.message,
id: data.adId,
bid: adObject
});
return;
}

// save winning bids
auctionManager.addWinningBid(adObject);
// save winning bids
auctionManager.addWinningBid(adObject);

events.emit(BID_WON, adObject);
}
events.emit(BID_WON, adObject);
}

// handle this script from native template in an ad server
// window.parent.postMessage(JSON.stringify({
// message: 'Prebid Native',
// adId: '%%PATTERN:hb_adid%%'
// }), '*');
if (adObject && data.message === 'Prebid Native') {
if (data.action === 'assetRequest') {
const message = getAssetMessage(data, adObject);
ev.source.postMessage(JSON.stringify(message), ev.origin);
} else if (data.action === 'allAssetRequest') {
const message = getAllAssetsMessage(data, adObject);
ev.source.postMessage(JSON.stringify(message), ev.origin);
} else if (data.action === 'resizeNativeHeight') {
adObject.height = data.height;
adObject.width = data.width;
resizeRemoteCreative(adObject);
} else {
const trackerType = fireNativeTrackers(data, adObject);
if (trackerType === 'click') { return; }

auctionManager.addWinningBid(adObject);
events.emit(BID_WON, adObject);
function handleNativeRequest(ev, data, adObject) {
// handle this script from native template in an ad server
// window.parent.postMessage(JSON.stringify({
// message: 'Prebid Native',
// adId: '%%PATTERN:hb_adid%%'
// }), '*');
if (adObject == null) {
logError(`Cannot find ad '${data.adId}' for x-origin event request`);
return;
}
switch (data.action) {
case 'assetRequest':
reply(getAssetMessage(data, adObject));
break;
case 'allAssetRequest':
reply(getAllAssetsMessage(data, adObject));
break;
case 'resizeNativeHeight':
adObject.height = data.height;
adObject.width = data.width;
resizeRemoteCreative(adObject);
break;
default:
const trackerType = fireNativeTrackers(data, adObject);
if (trackerType === 'click') {
return;
}
}
auctionManager.addWinningBid(adObject);
events.emit(BID_WON, adObject);
}

function reply(message) {
ev.source.postMessage(JSON.stringify(message), ev.origin);
}
}

function handleEventRequest(ev, data, adObject) {
if (adObject == null) {
logError(`Cannot find ad '${data.adId}' for x-origin event request`);
return;
}
if (adObject.status !== constants.BID_STATUS.RENDERED) {
logWarn(`Received x-origin event request without corresponding render request for ad '${data.adId}'`);
return;
}
switch (data.event) {
case constants.EVENTS.AD_RENDER_FAILED:
emitAdRenderFail({
bid: adObject,
id: data.adId,
reason: data.info.reason,
message: data.info.message
});
break;
case constants.EVENTS.AD_RENDER_SUCCEEDED:
emitAdRenderSucceeded({
doc: null,
bid: adObject,
id: data.adId
});
break;
default:
logError(`Received x-origin event request for unsupported event: '${data.event}' (adId: '${data.adId}')`)
}
}

Expand Down
Loading

0 comments on commit 4ad4024

Please sign in to comment.