From 13b18fa8333f6541d738ccd5aa46b3036d13b1f3 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 20 Feb 2025 10:41:16 -0800 Subject: [PATCH] Core: suspend auctions during prerendering (#12763) * Core: suspend auctions during prerendering * Delay only auctions by default * add option to delay queue --------- Co-authored-by: Patrick McCann --- src/prebid.js | 10 +++--- src/utils/prerendering.js | 22 ++++++++++++ test/spec/unit/pbjs_api_spec.js | 27 ++++++++++---- test/spec/utils/prerendering_spec.js | 53 ++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 src/utils/prerendering.js create mode 100644 test/spec/utils/prerendering_spec.js diff --git a/src/prebid.js b/src/prebid.js index 4bfa7745680..684464eb2b1 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -50,6 +50,7 @@ import {getHighestCpm} from './utils/reducers.js'; import {ORTB_VIDEO_PARAMS, fillVideoDefaults, validateOrtbVideoFields} from './video.js'; import { ORTB_BANNER_PARAMS } from './banner.js'; import { BANNER, VIDEO } from './mediaTypes.js'; +import {delayIfPrerendering} from './utils/prerendering.js'; import { newBidder } from './adapters/bidderFactory.js'; const pbjsInstance = getGlobal(); @@ -570,13 +571,14 @@ pbjsInstance.requestBids = (function() { }) }, 'requestBids'); - return wrapHook(delegate, function requestBids(req = {}) { + return wrapHook(delegate, delayIfPrerendering(() => !config.getConfig('allowPrerendering'), function requestBids(req = {}) { // unlike the main body of `delegate`, this runs before any other hook has a chance to; // it's also not restricted in its return value in the way `async` hooks are. // if the request does not specify adUnits, clone the global adUnit array; // otherwise, if the caller goes on to use addAdUnits/removeAdUnits, any asynchronous logic // in any hook might see their effects. + let adUnits = req.adUnits || pbjsInstance.adUnits; req.adUnits = (isArray(adUnits) ? adUnits.slice() : [adUnits]); @@ -585,7 +587,7 @@ pbjsInstance.requestBids = (function() { req.defer = defer({promiseFactory: (r) => new Promise(r)}) delegate.call(this, req); return req.defer.promise; - }); + })); })(); export const startAuction = hook('async', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, ttlBuffer, adUnitCodes, labels, auctionId, ortb2Fragments, metrics, defer } = {}) { @@ -1026,13 +1028,13 @@ function processQueue(queue) { /** * @alias module:pbjs.processQueue */ -pbjsInstance.processQueue = function () { +pbjsInstance.processQueue = delayIfPrerendering(() => getGlobal().delayPrerendering, function () { pbjsInstance.que.push = pbjsInstance.cmd.push = quePush; insertLocatorFrame(); hook.ready(); processQueue(pbjsInstance.que); processQueue(pbjsInstance.cmd); -}; +}); /** * @alias module:pbjs.triggerBilling diff --git a/src/utils/prerendering.js b/src/utils/prerendering.js new file mode 100644 index 00000000000..b89b8d895eb --- /dev/null +++ b/src/utils/prerendering.js @@ -0,0 +1,22 @@ +import {logInfo} from '../utils.js'; + +/** + * Returns a wrapper around fn that delays execution until the page if activated, if it was prerendered and isDelayEnabled returns true. + * https://developer.chrome.com/docs/web-platform/prerender-pages + */ +export function delayIfPrerendering(isDelayEnabled, fn) { + return function () { + if (document.prerendering && isDelayEnabled()) { + const that = this; + const args = Array.from(arguments); + return new Promise((resolve) => { + document.addEventListener('prerenderingchange', () => { + logInfo(`Auctions were suspended while page was prerendering`) + resolve(fn.apply(that, args)) + }, {once: true}) + }) + } else { + return Promise.resolve(fn.apply(this, arguments)); + } + } +} diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 1cb87440e80..378f15e35bf 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -244,14 +244,27 @@ describe('Unit: Prebid Module', function () { }); ['cmd', 'que'].forEach(prop => { - it(`should patch ${prop}.push`, () => { - $$PREBID_GLOBAL$$[prop].push = false; - $$PREBID_GLOBAL$$.processQueue(); - let ran = false; - $$PREBID_GLOBAL$$[prop].push(() => { ran = true; }); - expect(ran).to.be.true; + describe(`using .${prop}`, () => { + let queue, ran; + beforeEach(() => { + ran = false; + queue = $$PREBID_GLOBAL$$[prop] = []; + }); + after(() => { + $$PREBID_GLOBAL$$.processQueue(); + }) + + function pushToQueue() { + queue.push(() => { ran = true }); + } + + it(`should patch .push`, () => { + $$PREBID_GLOBAL$$.processQueue(); + pushToQueue(); + expect(ran).to.be.true; + }); }) - }) + }); }) describe('and global adUnits', () => { diff --git a/test/spec/utils/prerendering_spec.js b/test/spec/utils/prerendering_spec.js new file mode 100644 index 00000000000..409edb93747 --- /dev/null +++ b/test/spec/utils/prerendering_spec.js @@ -0,0 +1,53 @@ +import {delayIfPrerendering} from '../../../src/utils/prerendering.js'; + +describe('delayIfPrerendering', () => { + let sandbox, enabled, ran; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + enabled = true; + ran = false; + }); + + afterEach(() => { + sandbox.restore(); + }) + + const delay = delayIfPrerendering(() => enabled, () => { + ran = true; + }) + + it('should not delay if page is not prerendering', () => { + delay(); + expect(ran).to.be.true; + }) + + describe('when page is prerendering', () => { + before(() => { + if (!('prerendering' in document)) { + document.prerendering = null; + after(() => { + delete document.prerendering; + }) + } + }) + beforeEach(() => { + sandbox.stub(document, 'prerendering').get(() => true); + }); + function prerenderingDone() { + document.dispatchEvent(new Event('prerenderingchange')); + } + + it('should run fn only after prerenderingchange event', async () => { + delay(); + expect(ran).to.be.false; + prerenderingDone(); + expect(ran).to.be.true; + }); + + it('should not delay if not enabled', () => { + enabled = false; + delay(); + expect(ran).to.be.true; + }) + }) +})