From 1188738b84a9229589236b817a1869f0a7ce3249 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 13 Feb 2025 11:24:16 -0800 Subject: [PATCH 1/3] Core: suspend auctions during prerendering --- src/prebid.js | 32 +++++++++++-- test/spec/unit/pbjs_api_spec.js | 81 ++++++++++++++++++++++++++++++--- 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/prebid.js b/src/prebid.js index a536add9a96..3a74dcb614b 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -569,13 +569,17 @@ pbjsInstance.requestBids = (function() { }) }, 'requestBids'); - return wrapHook(delegate, function requestBids(req = {}) { + return wrapHook(delegate, delayIfPrerendering(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. + // Note that we delay this when prerendering because in some configurations requestBids may happen outside of + // pbjs.que (e.g. NPM consumers). + // 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]); @@ -584,7 +588,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 } = {}) { @@ -1018,16 +1022,36 @@ function processQueue(queue) { }); } +/** + * Returns a wrapper around fn that delays execution until the page if activated, if it was prerendered. + * https://developer.chrome.com/docs/web-platform/prerender-pages + */ +function delayIfPrerendering(fn) { + return function () { + if (document.prerendering && !(config.getConfig('allowPrerendering') || getGlobal().allowPrerendering)) { + const that = this; + const args = Array.from(arguments); + return new Promise((resolve) => { + document.addEventListener('prerenderingchange', () => { + logInfo(`Auctions are suspended while page is prerendering. Set $$PREBID_GLOBAL$$.allowPrerendering = true to allow auctions during prerendering.`) + resolve(fn.apply(that, args)) + }, {once: true}) + }) + } else { + return Promise.resolve(fn.apply(this, arguments)); + } + } +} /** * @alias module:pbjs.processQueue */ -pbjsInstance.processQueue = function () { +pbjsInstance.processQueue = delayIfPrerendering(function () { pbjsInstance.que.push = pbjsInstance.cmd.push = quePush; insertLocatorFrame(); hook.ready(); processQueue(pbjsInstance.que); processQueue(pbjsInstance.cmd); -}; +}); /** * @alias module:pbjs.triggerBilling diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index c86742d25a4..a1b95ef94e0 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -244,14 +244,81 @@ 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('when document 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 process queue only after prerenderingchange event', async () => { + pushToQueue(); + $$PREBID_GLOBAL$$.processQueue(); + pushToQueue(); + expect(ran).to.be.false; + prerenderingDone(); + expect(ran).to.be.true; + }); + + Object.entries({ + 'setConfig({allowPrerendering: true})': { + setup() { + $$PREBID_GLOBAL$$.setConfig({allowPrerendering: true}); + }, + teardown() { + $$PREBID_GLOBAL$$.setConfig({allowPrerendering: false}); + } + }, + '$$PREBID_GLOBAL$$.allowPrerendering = true': { + setup() { + $$PREBID_GLOBAL$$.allowPrerendering = true; + }, + teardown() { + delete $$PREBID_GLOBAL$$.allowPrerendering; + } + } + }).forEach(([t, {setup, teardown}]) => { + describe(`with ${t}`, () => { + beforeEach(setup); + afterEach(teardown); + it('should process immediately', () => { + pushToQueue(); + $$PREBID_GLOBAL$$.processQueue(); + expect(ran).to.be.true; + }) + }) + }) + }) }) - }) + }); }) describe('and global adUnits', () => { From 4ef3181a03a9a42cec0d437a929a26b51c5446fa Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 20 Feb 2025 08:48:43 -0800 Subject: [PATCH 2/3] Delay only auctions by default --- src/prebid.js | 30 +++------------- src/utils/prerendering.js | 22 ++++++++++++ test/spec/unit/pbjs_api_spec.js | 54 ---------------------------- test/spec/utils/prerendering_spec.js | 53 +++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 80 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 3a74dcb614b..385071e564c 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'; const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; @@ -569,13 +570,10 @@ pbjsInstance.requestBids = (function() { }) }, 'requestBids'); - return wrapHook(delegate, delayIfPrerendering(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. - // Note that we delay this when prerendering because in some configurations requestBids may happen outside of - // pbjs.que (e.g. NPM consumers). - // 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. @@ -1022,36 +1020,16 @@ function processQueue(queue) { }); } -/** - * Returns a wrapper around fn that delays execution until the page if activated, if it was prerendered. - * https://developer.chrome.com/docs/web-platform/prerender-pages - */ -function delayIfPrerendering(fn) { - return function () { - if (document.prerendering && !(config.getConfig('allowPrerendering') || getGlobal().allowPrerendering)) { - const that = this; - const args = Array.from(arguments); - return new Promise((resolve) => { - document.addEventListener('prerenderingchange', () => { - logInfo(`Auctions are suspended while page is prerendering. Set $$PREBID_GLOBAL$$.allowPrerendering = true to allow auctions during prerendering.`) - resolve(fn.apply(that, args)) - }, {once: true}) - }) - } else { - return Promise.resolve(fn.apply(this, arguments)); - } - } -} /** * @alias module:pbjs.processQueue */ -pbjsInstance.processQueue = delayIfPrerendering(function () { +pbjsInstance.processQueue = 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 a1b95ef94e0..60f39d2a6d7 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -263,60 +263,6 @@ describe('Unit: Prebid Module', function () { pushToQueue(); expect(ran).to.be.true; }); - - describe('when document 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 process queue only after prerenderingchange event', async () => { - pushToQueue(); - $$PREBID_GLOBAL$$.processQueue(); - pushToQueue(); - expect(ran).to.be.false; - prerenderingDone(); - expect(ran).to.be.true; - }); - - Object.entries({ - 'setConfig({allowPrerendering: true})': { - setup() { - $$PREBID_GLOBAL$$.setConfig({allowPrerendering: true}); - }, - teardown() { - $$PREBID_GLOBAL$$.setConfig({allowPrerendering: false}); - } - }, - '$$PREBID_GLOBAL$$.allowPrerendering = true': { - setup() { - $$PREBID_GLOBAL$$.allowPrerendering = true; - }, - teardown() { - delete $$PREBID_GLOBAL$$.allowPrerendering; - } - } - }).forEach(([t, {setup, teardown}]) => { - describe(`with ${t}`, () => { - beforeEach(setup); - afterEach(teardown); - it('should process immediately', () => { - pushToQueue(); - $$PREBID_GLOBAL$$.processQueue(); - expect(ran).to.be.true; - }) - }) - }) - }) }) }); }) 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; + }) + }) +}) From c58b4c967b9b27bdc4d55c22be433584969fd35b Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 20 Feb 2025 08:51:45 -0800 Subject: [PATCH 3/3] add option to delay queue --- src/prebid.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/prebid.js b/src/prebid.js index 385071e564c..3d80c2bd069 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -1023,13 +1023,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