import { deepClone, isPlainObject, logError, shuffle, logMessage, triggerPixel, insertUserSyncIframe, isArray, logWarn, isStr, isSafariBrowser } from './utils.js'; import { config } from './config.js'; import { getCoreStorageManager } from './storageManager.js'; import {isActivityAllowed, registerActivityControl} from './activities/rules.js'; import {ACTIVITY_SYNC_USER} from './activities/activities.js'; import { ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE, ACTIVITY_PARAM_SYNC_TYPE, ACTIVITY_PARAM_SYNC_URL } from './activities/params.js'; import {MODULE_TYPE_BIDDER} from './activities/modules.js'; import {activityParams} from './activities/activityParams.js'; import type {BidderCode} from "./types/common.d.ts"; export type SyncType = 'image' | 'iframe'; type SyncConfig = { bidders: '*' | BidderCode[]; filter: 'include' | 'exclude' } type FilterSettings = {[K in SyncType | 'all']?: SyncConfig}; export interface UserSyncConfig { /** * Enable/disable the user syncing feature. Default: true. */ syncEnabled?: boolean; /** * Configure lists of adapters to include or exclude their user syncing based on the pixel type (image/iframe). */ filterSettings?: FilterSettings; /** * Number of registered syncs allowed per adapter. Default: 5. To allow all, set to 0. */ syncsPerBidder?: number; /** * Delay in milliseconds for user syncing (both bid adapter user sync pixels and userId module ID providers) * after the auction ends. Default: 3000. Ignored by the userId module if auctionDelay > 0. */ syncDelay?: number; /** * Delay in milliseconds of the auction to retrieve user ids via the userId module before the auction starts. * Continues auction once all IDs are retrieved or delay times out. Does not apply to bid adapter user sync pixels. Default: 0. */ auctionDelay?: number; /** * Enable/disable publisher to trigger user syncs by calling pbjs.triggerUserSyncs(). Default: false. */ enableOverride?: boolean; /** * Enable/disable registered syncs for aliased adapters. Default: false. */ aliasSyncEnabled?: boolean; } export const USERSYNC_DEFAULT_CONFIG: UserSyncConfig = { syncEnabled: true, filterSettings: { image: { bidders: '*', filter: 'include' } }, syncsPerBidder: 5, syncDelay: 3000, auctionDelay: 500 }; // Set userSync default values config.setDefaults({ 'userSync': deepClone(USERSYNC_DEFAULT_CONFIG) }); const storage = getCoreStorageManager('usersync'); /** * Factory function which creates a new UserSyncPool. * * @param {} deps Configuration options and dependencies which the * UserSync object needs in order to behave properly. */ export function newUserSync(deps) { const publicApi: any = {}; // A queue of user syncs for each adapter // Let getDefaultQueue() set the defaults let queue = getDefaultQueue(); // Whether or not user syncs have been trigger on this page load for a specific bidder const hasFiredBidder = new Set(); // How many bids for each adapter let numAdapterBids = {}; // for now - default both to false in case filterSettings config is absent/misconfigured const permittedPixels = { image: true, iframe: false }; // Use what is in config by default let usConfig = deps.config; // Update if it's (re)set config.getConfig('userSync', (conf) => { // Added this logic for https://github.com/prebid/Prebid.js/issues/4864 // if userSync.filterSettings does not contain image/all configs, merge in default image config to ensure image pixels are fired if (conf.userSync) { const fs = conf.userSync.filterSettings; if (isPlainObject(fs)) { if (!fs.image && !fs.all) { conf.userSync.filterSettings.image = { bidders: '*', filter: 'include' }; } } } usConfig = Object.assign(usConfig, conf.userSync); }); deps.regRule(ACTIVITY_SYNC_USER, 'userSync config', (params) => { if (!usConfig.syncEnabled) { return {allow: false, reason: 'syncs are disabled'} } if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_BIDDER) { const syncType = params[ACTIVITY_PARAM_SYNC_TYPE]; const bidder = params[ACTIVITY_PARAM_COMPONENT_NAME]; if (!publicApi.canBidderRegisterSync(syncType, bidder)) { return {allow: false, reason: `${syncType} syncs are not enabled for ${bidder}`} } } }); /** * @function getDefaultQueue * @summary Returns the default empty queue * @private * @return {object} A queue with no syncs */ function getDefaultQueue() { return { image: [], iframe: [] }; } /** * @function fireSyncs * @summary Trigger all user syncs in the queue * @private */ function fireSyncs() { if (!usConfig.syncEnabled || !deps.browserSupportsCookies) { return; } try { // Iframe syncs loadIframes(); // Image pixels fireImagePixels(); } catch (e) { return logError('Error firing user syncs', e); } // Reset the user sync queue queue = getDefaultQueue(); } function forEachFire(queue, fn) { // Randomize the order of the pixels before firing // This is to avoid giving any bidder who has registered multiple syncs // any preferential treatment and balancing them out shuffle(queue).forEach(fn); } /** * @function fireImagePixels * @summary Loops through user sync pixels and fires each one * @private */ function fireImagePixels() { if (!permittedPixels.image) { return; } forEachFire(queue.image, (sync) => { const [bidderName, trackingPixelUrl] = sync; logMessage(`Invoking image pixel user sync for bidder: ${bidderName}`); // Create image object and add the src url triggerPixel(trackingPixelUrl); }); } /** * @function loadIframes * @summary Loops through iframe syncs and loads an iframe element into the page * @private */ function loadIframes() { if (!(permittedPixels.iframe)) { return; } forEachFire(queue.iframe, (sync) => { const [bidderName, iframeUrl] = sync; logMessage(`Invoking iframe user sync for bidder: ${bidderName}`); // Insert iframe into DOM insertUserSyncIframe(iframeUrl); // for a bidder, if iframe sync is present then remove image pixel removeImagePixelsForBidder(queue, bidderName); }); } function removeImagePixelsForBidder(queue, iframeSyncBidderName) { queue.image = queue.image.filter(imageSync => { const imageSyncBidderName = imageSync[0]; return imageSyncBidderName !== iframeSyncBidderName }); } /** * @function incrementAdapterBids * @summary Increment the count of user syncs queue for the adapter * @private * @param {object} numAdapterBids The object contain counts for all adapters * @param {string} bidder The name of the bidder adding a sync * @returns {object} The updated version of numAdapterBids */ function incrementAdapterBids(numAdapterBids, bidder) { if (!numAdapterBids[bidder]) { numAdapterBids[bidder] = 1; } else { numAdapterBids[bidder] += 1; } return numAdapterBids; } /** * @function registerSync * @summary Add sync for this bidder to a queue to be fired later * @public * @param {string} type The type of the sync including image, iframe * @param {string} bidder The name of the adapter. e.g. "rubicon" * @param {string} url Either the pixel url or iframe url depending on the type * @example