Skip to content

Commit

Permalink
Prebid Core: add documentResolver callback and allow the user to supp…
Browse files Browse the repository at this point in the history
…ly a different document object to render (prebid#8262)

* * allow Render to have a use resolvable document context
* cache url's per resolvable context
* pass options as config to Renderer
* pass renderDocument to render
* utility function to resolve the containing window object of a document

* * prefer mediaTypes.video.renderer.options over renderer.options
* support user-provided renderer context
* handle when there is no div[id^='google_ads'] element

* confics

* kick off circleci tests

* remove debugger

* * changed context to doc
* use WeakMap for caching of url's and documents
* flipped key order of url cache
* added unit test

* * added adLoader double document cache test

* * ensure call's are uneven

* marked doc parameter optional

* * change var to const
* drop IE support

Co-authored-by: Chris Huie <[email protected]>
  • Loading branch information
olafbuitelaar and ChrisHuie authored Apr 12, 2022
1 parent a6496f5 commit 2d14b7d
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 30 deletions.
23 changes: 16 additions & 7 deletions modules/appnexusBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
logInfo,
logMessage,
logWarn,
transformBidderParamKeywords
transformBidderParamKeywords,
getWindowFromDocument
} from '../src/utils.js';
import {Renderer} from '../src/Renderer.js';
import {config} from '../src/config.js';
Expand Down Expand Up @@ -703,7 +704,10 @@ function newBid(serverBid, rtbBid, bidderRequest) {

if (rtbBid.renderer_url) {
const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid);
const rendererOptions = deepAccess(videoBid, 'renderer.options');
let rendererOptions = deepAccess(videoBid, 'mediaTypes.video.renderer.options'); // mediaType definition has preference (shouldn't options be .config?)
if (!rendererOptions) {
rendererOptions = deepAccess(videoBid, 'renderer.options'); // second the adUnit definition has preference (shouldn't options be .config?)
}
bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions);
}
break;
Expand Down Expand Up @@ -1131,9 +1135,13 @@ function buildNativeRequest(params) {
* @param {string} elementId element id
*/
function hidedfpContainer(elementId) {
var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']");
if (el[0]) {
el[0].style.setProperty('display', 'none');
try {
const el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']");
if (el[0]) {
el[0].style.setProperty('display', 'none');
}
} catch (e) {
// element not found!
}
}

Expand All @@ -1149,12 +1157,13 @@ function hideSASIframe(elementId) {
}
}

function outstreamRender(bid) {
function outstreamRender(bid, doc) {
hidedfpContainer(bid.adUnitCode);
hideSASIframe(bid.adUnitCode);
// push to render queue because ANOutstreamVideo may not be loaded yet
bid.renderer.push(() => {
window.ANOutstreamVideo.renderAd({
const win = getWindowFromDocument(doc) || window;
win.ANOutstreamVideo.renderAd({
tagId: bid.adResponse.tag_id,
sizes: [bid.getSize().split('x')],
targetId: bid.adUnitCode, // target div id to render video
Expand Down
15 changes: 12 additions & 3 deletions src/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function Renderer(options) {
if (!isRendererPreferredFromAdUnit(adUnitCode)) {
// we expect to load a renderer url once only so cache the request to load script
this.cmd.unshift(runRender) // should render run first ?
loadExternalScript(url, moduleCode, this.callback);
loadExternalScript(url, moduleCode, this.callback, this.documentContext);
} else {
logWarn(`External Js not loaded by Renderer since renderer url and callback is already defined on adUnit ${adUnitCode}`);
runRender()
Expand Down Expand Up @@ -112,9 +112,18 @@ export function isRendererRequired(renderer) {
* Render the bid returned by the adapter
* @param {Object} renderer Renderer object installed by adapter
* @param {Object} bid Bid response
* @param {Document} doc context document of bid
*/
export function executeRenderer(renderer, bid) {
renderer.render(bid);
export function executeRenderer(renderer, bid, doc) {
let docContext = null;
if (renderer.config && renderer.config.documentResolver) {
docContext = renderer.config.documentResolver(bid, document, doc);// a user provided callback, which should return a Document, and expect the parameters; bid, sourceDocument, renderDocument
}
if (!docContext) {
docContext = document;
}
renderer.documentContext = docContext;
renderer.render(bid, renderer.documentContext);
}

function isRendererPreferredFromAdUnit(adUnitCode) {
Expand Down
56 changes: 39 additions & 17 deletions src/adloader.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {includes} from './polyfill.js';
import { logError, logWarn, insertElement } from './utils.js';

const _requestCache = {};
const _requestCache = new WeakMap();
// The below list contains modules or vendors whom Prebid allows to load external JS.
const _approvedLoadExternalJSList = [
'adloox',
Expand All @@ -18,9 +18,10 @@ const _approvedLoadExternalJSList = [
* Each unique URL will be loaded at most 1 time.
* @param {string} url the url to load
* @param {string} moduleCode bidderCode or module code of the module requesting this resource
* @param {function} [callback] callback function to be called after the script is loaded.
* @param {function} [callback] callback function to be called after the script is loaded
* @param {Document} [doc] the context document, in which the script will be loaded, defaults to loaded document
*/
export function loadExternalScript(url, moduleCode, callback) {
export function loadExternalScript(url, moduleCode, callback, doc) {
if (!moduleCode || !url) {
logError('cannot load external script without url and moduleCode');
return;
Expand All @@ -29,46 +30,60 @@ export function loadExternalScript(url, moduleCode, callback) {
logError(`${moduleCode} not whitelisted for loading external JavaScript`);
return;
}
if (!doc) {
doc = document; // provide a "valid" key for the WeakMap
}
// only load each asset once
if (_requestCache[url]) {
const storedCachedObject = getCacheObject(doc, url);
if (storedCachedObject) {
if (callback && typeof callback === 'function') {
if (_requestCache[url].loaded) {
if (storedCachedObject.loaded) {
// invokeCallbacks immediately
callback();
} else {
// queue the callback
_requestCache[url].callbacks.push(callback);
storedCachedObject.callbacks.push(callback);
}
}
return _requestCache[url].tag;
return storedCachedObject.tag;
}
_requestCache[url] = {
const cachedDocObj = _requestCache.get(doc) || {};
const cacheObject = {
loaded: false,
tag: null,
callbacks: []
};
cachedDocObj[url] = cacheObject;
_requestCache.set(doc, cachedDocObj);

if (callback && typeof callback === 'function') {
_requestCache[url].callbacks.push(callback);
cacheObject.callbacks.push(callback);
}

logWarn(`module ${moduleCode} is loading external JavaScript`);
return requestResource(url, function () {
_requestCache[url].loaded = true;
cacheObject.loaded = true;
try {
for (let i = 0; i < _requestCache[url].callbacks.length; i++) {
_requestCache[url].callbacks[i]();
for (let i = 0; i < cacheObject.callbacks.length; i++) {
cacheObject.callbacks[i]();
}
} catch (e) {
logError('Error executing callback', 'adloader.js:loadExternalScript', e);
}
});
}, doc);

function requestResource(tagSrc, callback) {
var jptScript = document.createElement('script');
function requestResource(tagSrc, callback, doc) {
if (!doc) {
doc = document;
}
var jptScript = doc.createElement('script');
jptScript.type = 'text/javascript';
jptScript.async = true;

_requestCache[url].tag = jptScript;
const cacheObject = getCacheObject(doc, url);
if (cacheObject) {
cacheObject.tag = jptScript;
}

if (jptScript.readyState) {
jptScript.onreadystatechange = function () {
Expand All @@ -86,8 +101,15 @@ export function loadExternalScript(url, moduleCode, callback) {
jptScript.src = tagSrc;

// add the new script tag to the page
insertElement(jptScript);
insertElement(jptScript, doc);

return jptScript;
}
function getCacheObject(doc, url) {
const cachedDocObj = _requestCache.get(doc);
if (cachedDocObj && cachedDocObj[url]) {
return cachedDocObj[url];
}
return null; // return new cache object?
}
};
3 changes: 2 additions & 1 deletion src/auction.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,8 @@ function getPreparedBidForAuction({adUnitCode, bid, auctionId}, {index = auction
}

if (renderer) {
bidObject.renderer = Renderer.install({ url: renderer.url });
// be aware, an adapter could already have installed the bidder, in which case this overwrite's the existing adapter
bidObject.renderer = Renderer.install({ url: renderer.url, config: renderer.options });// rename options to config, to make it consistent?
bidObject.renderer.setRender(renderer.render);
}

Expand Down
2 changes: 1 addition & 1 deletion src/prebid.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ $$PREBID_GLOBAL$$.renderAd = hook('async', function (doc, id, options) {
insertElement(creativeComment, doc, 'html');

if (isRendererRequired(renderer)) {
executeRenderer(renderer, bid);
executeRenderer(renderer, bid, doc);
reinjectNodeIfRemoved(creativeComment, doc, 'html');
emitAdRenderSucceeded({ doc, bid, id });
} else if ((doc === document && !inIframe()) || mediaType === 'video') {
Expand Down
9 changes: 9 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1355,3 +1355,12 @@ export function cyrb53Hash(str, seed = 0) {
h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909);
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString();
}

/**
* returns a window object, which holds the provided document or null
* @param {Document} doc
* @returns {Window}
*/
export function getWindowFromDocument(doc) {
return (doc) ? doc.defaultView : null;
}
30 changes: 30 additions & 0 deletions test/spec/adloader_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,35 @@ describe('adLoader', function () {
adLoader.loadExternalScript('someURL1', 'criteo', callback);
expect(utilsinsertElementStub.called).to.be.true;
});

it('requires a url to be included once per document', function () {
function getDocSpec() {
return {
createElement: function() {
return {

}
},
getElementsByTagName: function() {
return {
firstChild: {
insertBefore: function() {

}
}
}
}

}
}
const doc1 = getDocSpec();
const doc2 = getDocSpec();
adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc1);
adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc1);
adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc1);
adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc2);
adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc2);
expect(utilsinsertElementStub.callCount).to.equal(2);
});
});
});
17 changes: 16 additions & 1 deletion test/spec/renderer_spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { Renderer } from 'src/Renderer.js';
import { Renderer, executeRenderer } from 'src/Renderer.js';
import * as utils from 'src/utils.js';
import { loadExternalScript } from 'src/adloader.js';
require('test/mocks/adloaderStub.js');
Expand Down Expand Up @@ -212,5 +212,20 @@ describe('Renderer', function () {
testRenderer.render()
expect(loadExternalScript.called).to.be.true;
});

it('call\'s documentResolver when configured', function () {
const documentResolver = sinon.spy(function(bid, sDoc, tDoc) {
return document;
});

let testRenderer = Renderer.install({
url: 'https://httpbin.org/post',
config: { documentResolver: documentResolver }
});

executeRenderer(testRenderer, {}, {});

expect(documentResolver.called).to.be.true;
});
});
});

0 comments on commit 2d14b7d

Please sign in to comment.