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

PBS Bid adapter: timeout user syncs if they never load #7744

Merged
merged 3 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ let eidPermissions;
* @property {string} [adapter='prebidServer'] adapter code to use for S2S
* @property {boolean} [enabled=false] enables S2S bidding
* @property {number} [timeout=1000] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})`
* @property {number} [syncTimeout=1000] timeout for cookie sync iframe / image rendering
* @property {number} [maxBids=1]
* @property {AdapterOptions} [adapterOptions] adds arguments to resulting OpenRTB payload to Prebid Server
* @property {Object} [syncUrlModifier]
Expand All @@ -77,6 +78,7 @@ let eidPermissions;
*/
const s2sDefaultConfig = {
timeout: 1000,
syncTimeout: 1000,
maxBids: 1,
adapter: 'prebidServer',
adapterOptions: {},
Expand Down Expand Up @@ -274,11 +276,9 @@ function doAllSyncs(bidders, s2sConfig) {
*/
function doPreBidderSync(type, url, bidder, done, s2sConfig) {
if (s2sConfig.syncUrlModifier && typeof s2sConfig.syncUrlModifier[bidder] === 'function') {
const newSyncUrl = s2sConfig.syncUrlModifier[bidder](type, url, bidder);
doBidderSync(type, newSyncUrl, bidder, done)
} else {
doBidderSync(type, url, bidder, done)
url = s2sConfig.syncUrlModifier[bidder](type, url, bidder);
}
doBidderSync(type, url, bidder, done, s2sConfig.syncTimeout)
}

/**
Expand All @@ -288,17 +288,18 @@ function doPreBidderSync(type, url, bidder, done, s2sConfig) {
* @param {string} url the url to sync
* @param {string} bidder name of bidder doing sync for
* @param {function} done an exit callback; to signify this pixel has either: finished rendering or something went wrong
* @param {number} timeout: maximum time to wait for rendering in milliseconds
*/
function doBidderSync(type, url, bidder, done) {
function doBidderSync(type, url, bidder, done, timeout) {
if (!url) {
logError(`No sync url for bidder "${bidder}": ${url}`);
done();
} else if (type === 'image' || type === 'redirect') {
logMessage(`Invoking image pixel user sync for bidder: "${bidder}"`);
triggerPixel(url, done);
} else if (type == 'iframe') {
triggerPixel(url, done, timeout);
} else if (type === 'iframe') {
logMessage(`Invoking iframe user sync for bidder: "${bidder}"`);
insertUserSyncIframe(url, done);
insertUserSyncIframe(url, done, timeout);
} else {
logError(`User sync type "${type}" not supported for bidder: "${bidder}"`);
done();
Expand Down
41 changes: 34 additions & 7 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -482,16 +482,43 @@ export function insertElement(elm, doc, target, asLastChildChild) {
} catch (e) {}
}

/**
* Returns a promise that completes when the given element triggers a 'load' or 'error' DOM event, or when
* `timeout` milliseconds have elapsed.
*
* @param {HTMLElement} element
* @param {Number} [timeout]
* @returns {Promise}
*/
export function waitForElementToLoad(element, timeout) {
let timer = null;
return new Promise((resolve) => {
const onLoad = function() {
element.removeEventListener('load', onLoad);
element.removeEventListener('error', onLoad);
msm0504 marked this conversation as resolved.
Show resolved Hide resolved
if (timer != null) {
window.clearTimeout(timer);
}
resolve();
};
element.addEventListener('load', onLoad);
element.addEventListener('error', onLoad);
if (timeout != null) {
timer = window.setTimeout(onLoad, timeout);
}
});
}

/**
* Inserts an image pixel with the specified `url` for cookie sync
* @param {string} url URL string of the image pixel to load
* @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process
* @param {Number} [timeout] an optional timeout in milliseconds for the image to load before calling `done`
*/
export function triggerPixel(url, done) {
export function triggerPixel(url, done, timeout) {
const img = new Image();
if (done && internal.isFn(done)) {
img.addEventListener('load', done);
img.addEventListener('error', done);
waitForElementToLoad(img, timeout).then(done);
}
img.src = url;
}
Expand Down Expand Up @@ -539,18 +566,18 @@ export function insertHtmlIntoIframe(htmlCode) {
* @param {string} url URL to be requested
* @param {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true
* @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process
* @param {Number} [timeout] an optional timeout in milliseconds for the iframe to load before calling `done`
*/
export function insertUserSyncIframe(url, done) {
export function insertUserSyncIframe(url, done, timeout) {
let iframeHtml = internal.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin');
let div = document.createElement('div');
div.innerHTML = iframeHtml;
let iframe = div.firstChild;
if (done && internal.isFn(done)) {
iframe.addEventListener('load', done);
iframe.addEventListener('error', done);
waitForElementToLoad(iframe, timeout).then(done);
}
internal.insertElement(iframe, document, 'html', true);
};
}

/**
* Creates a snippet of HTML that retrieves the specified `url`
Expand Down
30 changes: 30 additions & 0 deletions test/spec/modules/prebidServerBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2420,6 +2420,36 @@ describe('S2S Adapter', function () {

utils.getBidRequest.restore();
});

describe('on sync requested with no cookie', () => {
let cfg, req, csRes;

beforeEach(() => {
cfg = utils.deepClone(CONFIG);
req = utils.deepClone(REQUEST);
cfg.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' };
req.s2sConfig = cfg;
config.setConfig({ s2sConfig: cfg });
csRes = utils.deepClone(RESPONSE_NO_COOKIE);
});

afterEach(() => {
resetSyncedStatus();
})

Object.entries({
iframe: () => utils.insertUserSyncIframe,
image: () => utils.triggerPixel,
}).forEach(([type, syncer]) => {
it(`passes timeout to ${type} syncs`, () => {
cfg.syncTimeout = 123;
csRes.bidder_status[0].usersync.type = type;
adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax);
server.requests[0].respond(200, {}, JSON.stringify(csRes));
expect(syncer().args[0]).to.include.members([123]);
});
});
});
});

describe('bid won events', function () {
Expand Down
38 changes: 38 additions & 0 deletions test/spec/utils_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getAdServerTargeting } from 'test/fixtures/fixtures.js';
import { expect } from 'chai';
import CONSTANTS from 'src/constants.json';
import * as utils from 'src/utils.js';
import {waitForElementToLoad} from 'src/utils.js';

var assert = require('assert');

Expand Down Expand Up @@ -1198,4 +1199,41 @@ describe('Utils', function () {
});
});
});

describe('waitForElementToLoad', () => {
let element;
let callbacks;

function callback() {
callbacks++;
}

function delay(delay = 0) {
return new Promise((resolve) => {
window.setTimeout(resolve, delay);
})
}

beforeEach(() => {
callbacks = 0;
element = window.document.createElement('div');
});

it('should respect timeout if set', () => {
waitForElementToLoad(element, 50).then(callback);
return delay(60).then(() => {
expect(callbacks).to.equal(1);
});
});

['load', 'error'].forEach((event) => {
it(`should complete on '${event} event'`, () => {
waitForElementToLoad(element).then(callback);
element.dispatchEvent(new Event(event));
return delay().then(() => {
expect(callbacks).to.equal(1);
})
});
});
});
});