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

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 prebid#7702

Related PUC changes: prebid/prebid-universal-creative#152
Documentation changes TBD
  • Loading branch information
dgirardi committed Jan 7, 2022
1 parent 30c6f40 commit cc39c00
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 87 deletions.
74 changes: 51 additions & 23 deletions integrationExamples/gpt/x-domain/creative.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,38 @@

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

urlParser.href = '%%PATTERN:url%%';
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;
}
let 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;
let origin = ev.origin || ev.originalEvent.origin;
if (adObject.message && adObject.message === 'Prebid Response' &&
publisherDomain === origin &&
adObject.adId === adId) {
try {
let body = win.document.body;
let ad = adObject.ad;
let url = adObject.adUrl;
let width = adObject.width;
let 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) {
} else if (ad) {
var frame = document.createElement('iframe');
frame.setAttribute('FRAMEBORDER', 0);
frame.setAttribute('SCROLLING', 'no');
Expand All @@ -46,18 +50,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
22 changes: 22 additions & 0 deletions src/adRendering.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {logError} from './utils.js';
import events from './events.js';
import CONSTANTS from './constants.json';

const {AD_RENDER_FAILED, AD_RENDER_SUCCEEDED} = CONSTANTS.EVENTS;

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);
}

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
139 changes: 96 additions & 43 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,98 @@ 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;
});
const handler = HANDLER_MAP[data.message];
if (handler != null) {
handler(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);
_sendAdToCreative(adObject, ev);

// 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;
}
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 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
16 changes: 13 additions & 3 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@ let consoleLogExists = Boolean(consoleExists && window.console.log);
let consoleInfoExists = Boolean(consoleExists && window.console.info);
let consoleWarnExists = Boolean(consoleExists && window.console.warn);
let consoleErrorExists = Boolean(consoleExists && window.console.error);
var events = require('./events.js');

const emitEvent = (function () {
// lazy load events to avoid circular import
let ev;
return function() {
if (ev == null) {
ev = require('./events.js');
}
return ev.emit.apply(ev, arguments);
}
})();

// this allows stubbing of utility functions that are used internally by other utility functions
export const internal = {
Expand Down Expand Up @@ -265,14 +275,14 @@ export function logWarn() {
if (debugTurnedOn() && consoleWarnExists) {
console.warn.apply(console, decorateLog(arguments, 'WARNING:'));
}
events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, {type: 'WARNING', arguments: arguments});
emitEvent(CONSTANTS.EVENTS.AUCTION_DEBUG, {type: 'WARNING', arguments: arguments});
}

export function logError() {
if (debugTurnedOn() && consoleErrorExists) {
console.error.apply(console, decorateLog(arguments, 'ERROR:'));
}
events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, {type: 'ERROR', arguments: arguments});
emitEvent(CONSTANTS.EVENTS.AUCTION_DEBUG, {type: 'ERROR', arguments: arguments});
}

function decorateLog(args, prefix) {
Expand Down
Loading

0 comments on commit cc39c00

Please sign in to comment.