Skip to content

Commit

Permalink
Core: suspend auctions during prerendering (#12763)
Browse files Browse the repository at this point in the history
* Core: suspend auctions during prerendering

* Delay only auctions by default

* add option to delay queue

---------

Co-authored-by: Patrick McCann <[email protected]>
  • Loading branch information
dgirardi and patmmccann authored Feb 20, 2025
1 parent 8813b40 commit 13b18fa
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 11 deletions.
10 changes: 6 additions & 4 deletions src/prebid.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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]);

Expand All @@ -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 } = {}) {
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/utils/prerendering.js
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
27 changes: 20 additions & 7 deletions test/spec/unit/pbjs_api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
53 changes: 53 additions & 0 deletions test/spec/utils/prerendering_spec.js
Original file line number Diff line number Diff line change
@@ -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;
})
})
})

0 comments on commit 13b18fa

Please sign in to comment.