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

Prebid Core: add documentResolver callback and allow the user to supply a different document object to render #8262

Merged
merged 11 commits into from
Apr 12, 2022

Conversation

olafbuitelaar
Copy link
Contributor

@olafbuitelaar olafbuitelaar commented Apr 5, 2022

Type of change

  • Bugfix
  • Feature
  • New bidder adapter
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Does this change affect user-facing APIs or examples documented on http://prebid.org?
  • Other

Description of change

This PR, allows the user to supply a different document object to render the ad/outstream video in. This can be usefull if the prebid instance is loaded in a different document/iframe, while the ad/video should be rendered in another iframe/document.
This implements this optional change also in the Appnexus bid adapter, and allows the Appnexus bid adapter to fail silently when certain elements don't exist.
The feature add's an optional contextResolver callback method to the config of the Renderer, which receives the current Bid, the document where prebid is loaded, and the document object which is passed to the renderAd method. And should return the document object in which it should render the ad

Configuration can be on the adunit or mediaType level.
For example;

mediaTypes: {
    video: {
        context: 'outstream',
        playerSize: [640, 480],
        mimes: ['video/mp4'],
        protocols: [1, 2, 3, 4, 5, 6, 7, 8],
        playbackmethod: [2],
        skip: 1,
        playback_method: ['auto_play_sound_off'],
        renderer:{                                        
            options:{                                            
                contextResolver:function(bid, ownDoc, renderDoc){                                                
                    return renderDoc;                                                
                }
            }                                        
        }
    }
}

* 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
* support user-provided renderer context
* handle when there is no div[id^='google_ads'] element
@ChrisHuie ChrisHuie requested a review from dgirardi April 6, 2022 16:37
@ChrisHuie ChrisHuie requested review from patmmccann and mmoschovas and removed request for patmmccann April 6, 2022 16:37
@ChrisHuie ChrisHuie changed the title Render context Prebid Core: add contextResolver callback and allow thte user to supply a different document object to render Apr 6, 2022
src/utils.js Outdated
if (!doc) {
return null;
}
return doc.defaultView || doc.parentWindow;
Copy link
Collaborator

Choose a reason for hiding this comment

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

where does parentWindow come from? I don't see it specified on MDN: https://developer.mozilla.org/en-US/docs/Web/API/Document

Copy link
Contributor Author

Choose a reason for hiding this comment

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

parentWindow is used on older IE browsers

Copy link
Collaborator

Choose a reason for hiding this comment

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

We do not support older IE browsers - in fact we don't support IE at all - many other things would not work. IMO it can be removed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. We shouldnt be adding support back in for old IE versions. Can the contents of this function also just be one ternary line saying return (doc) ? doc.defaultView : null

src/Renderer.js Outdated
context = renderer.config.contextResolver(bid, document, doc);// a user provided callback, which should return a Document, and expect the parameters; bid, sourceDocument, renderDocument
}
if (!context) {
context = document;
Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO doc would be a better default; what is the reasoning for using document instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

document is the current default, changing it to doc would introduce a possible breaking change in the current behaviour. But generally i agree, in fact one of the main reasons this PR exists.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You're right - I misread and thought it would default to the document passed to renderAd. Nevermind.

@@ -590,6 +591,7 @@ function formatRequest(payload, bidderRequest) {
}

function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) {
debugger;
Copy link
Collaborator

Choose a reason for hiding this comment

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

ah-ehm.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

src/adloader.js Outdated
*/
export function loadExternalScript(url, moduleCode, callback) {
export function loadExternalScript(url, moduleCode, callback, context) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does this need to be aware of the context? (also, is context a good name for what is always a document?)
Would it work if instead of keeping track of context, we add an option to always run the callback, and let the caller decide if they need to run their logic again?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the renderer can load scripts, when the config contains a url property, like;

renderer: {
  url: 'https://cdn.adnxs.com/renderer/video/ANOutstreamVideo.js'
}

in which case the script should be loaded in the correct/desired document context.

I'm not sure if calling the callback always would be the best solution, since other modules also make use of this method.
Also this would push the (repeating) logic of caching or not, always to the callback.
However there is 1 advantage to that, since once a script is loaded, it could reference/re initiate the already loaded code in the new context. But this could also be implemented else if desired, like in a pre-render hook. Which would save a url load, if the script needs to be loaded in different document contexts.
Also one could say, you could simply rely on the caching behaviour of the browser.

Copy link
Collaborator

@dgirardi dgirardi Apr 7, 2022

Choose a reason for hiding this comment

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

I missed that you are using context.createScript - you are right. I have two suggestions:

  • change the name from context to something that's clearly a document (like doc)
  • use WeakMap to separate caches by document, e.g. const caches = new WeakMap(); const _requestCache = caches.get(doc) || caches.set(doc, {}).get(doc). It's a smaller change, easier to follow, and marginally more efficient.

@@ -1138,12 +1147,13 @@ function hideSASIframe(elementId) {
}
}

function outstreamRender(bid) {
function outstreamRender(bid, context) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this new context option needs support from individual adapters anyway,, it looks to me like you could reduce the scope of this change if you look for the document / context in here, without requiring it to be passed around as an argument. By that I mean look at this.options.context in here to see what document / window to use, leaving almost everything in core unchanged.

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 considered that, but only the fields url, renderder and config/options are passed allong, so adding another field would mean to make sure the new context field is always passed in all circumstances. having it a prop of config, ensures it's always passed.
While it's true Adapter should implement this to support it fully, and the ones i've checked it seems implementations vary a lot surrounding this subject.
The core part, for loading the url, when there is not an adapter specific renderer defined, requires this change to live in the core part.

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 I agree after giving it some more thought, if the idea is that the same renderer instance could be used on different documents. The only pushback I'll give on is the naming again - context sound a lot like object, while the arument is very well defined as a document. Same for contextResolver.

* use WeakMap for caching of url's and documents
* flipped key order of url cache
* added unit test
@olafbuitelaar
Copy link
Contributor Author

i've renamed all context's to doc's, implemented the WeakMap for the caching store, and added a unit test.

@olafbuitelaar olafbuitelaar changed the title Prebid Core: add contextResolver callback and allow thte user to supply a different document object to render Prebid Core: add contextResolver callback and allow the user to supply a different document object to render Apr 8, 2022
@olafbuitelaar olafbuitelaar changed the title Prebid Core: add contextResolver callback and allow the user to supply a different document object to render Prebid Core: add documentResolver callback and allow the user to supply a different document object to render Apr 8, 2022
function requestResource(tagSrc, callback) {
var jptScript = document.createElement('script');
function requestResource(tagSrc, callback, doc) {
if (!doc) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

here you check that doc and cacheObject are defined before using them. It doesn't hurt, except maybe for adding a couple bytes, but they should always be defined here - conceptually, if you're going to check for that, it would make more sense to throw an error when they are undefined (but I wouldn't do that either).

It's not that consequential so I don't mind if it stays this way, just noting it as an odd choice IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It depends, doc being not set defaults to the loaded document, which in theory does also happen down the line in insertElement and some other methods. But now it's specific from here on. But i do agree it could also be an Argument Exception if there is a need to be specific about this.
The cacheObject could theoeratically be gone for what ever reason it's resolved there. This could actually be an error, since the tag property won't be set, but the callback would flag the object as loaded, which is sort of inconsistent. But not failing there and just loading the script, without failing there is i think not that troublesome.

@@ -212,5 +212,20 @@ describe('Renderer', function () {
testRenderer.render()
expect(loadExternalScript.called).to.be.true;
});

it('call\'s documentResolver when configured', function () {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a case to adloater_spec to check that when the same script, but different document is passed to loadExternalScript, it does not hit the cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

if (el[0]) {
el[0].style.setProperty('display', 'none');
try {
var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']");
Copy link
Contributor

Choose a reason for hiding this comment

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

Since you are making changes here anyway, can this be updated to use a const instead of a var?

src/utils.js Outdated
if (!doc) {
return null;
}
return doc.defaultView || doc.parentWindow;
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. We shouldnt be adding support back in for old IE versions. Can the contents of this function also just be one ternary line saying return (doc) ? doc.defaultView : null

* drop IE support
@olafbuitelaar
Copy link
Contributor Author

i've made the requested amendments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants