• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prebid / Prebid.js / 17552805458

08 Sep 2025 01:40PM UTC coverage: 96.25% (+0.003%) from 96.247%
17552805458

push

github

47e5f4
prebidjs-release
Increment version to 10.11.0-pre

39744 of 48861 branches covered (81.34%)

197557 of 205253 relevant lines covered (96.25%)

124.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.1
/modules/pubmaticRtdProvider.js
1
import { submodule } from '../src/hook.js';
1✔
2
import { logError, logInfo, isPlainObject, isEmpty, isFn, mergeDeep } from '../src/utils.js';
3
import { config as conf } from '../src/config.js';
4
import { getDeviceType as fetchDeviceType, getOS } from '../libraries/userAgentUtils/index.js';
5
import { getLowEntropySUA } from '../src/fpd/sua.js';
6
import { getGlobal } from '../src/prebidGlobal.js';
7
import { REJECTION_REASON } from '../src/constants.js';
8

9
/**
10
 * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule
11
 */
12

13
/**
14
 * This RTD module has a dependency on the priceFloors module.
15
 * We utilize the continueAuction function from the priceFloors module to incorporate price floors data into the current auction.
16
 */
17
import { continueAuction } from './priceFloors.js'; // eslint-disable-line prebid/validate-imports
18

19
export const CONSTANTS = Object.freeze({
1✔
20
  SUBMODULE_NAME: 'pubmatic',
21
  REAL_TIME_MODULE: 'realTimeData',
22
  LOG_PRE_FIX: 'PubMatic-Rtd-Provider: ',
23
  UTM: 'utm_',
24
  UTM_VALUES: {
25
    TRUE: '1',
26
    FALSE: '0'
27
  },
28
  TIME_OF_DAY_VALUES: {
29
    MORNING: 'morning',
30
    AFTERNOON: 'afternoon',
31
    EVENING: 'evening',
32
    NIGHT: 'night',
33
  },
34
  ENDPOINTS: {
35
    BASEURL: 'https://ads.pubmatic.com/AdServer/js/pwt',
36
    FLOORS: 'floors.json',
37
    CONFIGS: 'config.json'
38
  },
39
  BID_STATUS: {
40
    NOBID: 0,
41
    WON: 1,
42
    FLOORED: 2
43
  },
44
  MULTIPLIERS: {
45
    WIN: 1.0,
46
    FLOORED: 0.8,
47
    NOBID: 1.2
48
  },
49
  TARGETING_KEYS: {
50
    PM_YM_FLRS: 'pm_ym_flrs', // Whether RTD floor was applied
51
    PM_YM_FLRV: 'pm_ym_flrv', // Final floor value (after applying multiplier)
52
    PM_YM_BID_S: 'pm_ym_bid_s' // Bid status (0: No bid, 1: Won, 2: Floored)
53
  }
54
});
55

56
const BROWSER_REGEX_MAP = [
1✔
57
  { regex: /\b(?:crios)\/([\w.]+)/i, id: 1 }, // Chrome for iOS
58
  { regex: /(edg|edge)(?:e|ios|a)?(?:\/([\w.]+))?/i, id: 2 }, // Edge
59
  { regex: /(opera|opr)(?:.+version\/|[/ ]+)([\w.]+)/i, id: 3 }, // Opera
60
  { regex: /(?:ms|\()(ie) ([\w.]+)|(?:trident\/[\w.]+)/i, id: 4 }, // Internet Explorer
61
  { regex: /fxios\/([-\w.]+)/i, id: 5 }, // Firefox for iOS
62
  { regex: /((?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w.]+);)/i, id: 6 }, // Facebook In-App Browser
63
  { regex: / wv\).+(chrome)\/([\w.]+)/i, id: 7 }, // Chrome WebView
64
  { regex: /droid.+ version\/([\w.]+)\b.+(?:mobile safari|safari)/i, id: 8 }, // Android Browser
65
  { regex: /(chrome|crios)(?:\/v?([\w.]+))?\b/i, id: 9 }, // Chrome
66
  { regex: /version\/([\w.,]+) .*mobile\/\w+ (safari)/i, id: 10 }, // Safari Mobile
67
  { regex: /version\/([\w(.|,]+) .*(mobile ?safari|safari)/i, id: 11 }, // Safari
68
  { regex: /(firefox)\/([\w.]+)/i, id: 12 } // Firefox
69
];
70

71
export const defaultValueTemplate = {
1✔
72
  currency: 'USD',
73
  skipRate: 0,
74
  schema: {
75
    fields: ['mediaType', 'size']
76
  }
77
};
78

79
let initTime;
80
let _fetchFloorRulesPromise = null; let _fetchConfigPromise = null;
1✔
81
export let configMerged;
82
// configMerged is a reference to the function that can resolve configMergedPromise whenever we want
83
let configMergedPromise = new Promise((resolve) => { configMerged = resolve; });
1✔
84
export let _country;
85
// Store multipliers from floors.json, will use default values from CONSTANTS if not available
86
export let _multipliers = null;
1✔
87

88
// Use a private variable for profile configs
89
let _profileConfigs;
90
// Export getter and setter functions for _profileConfigs
91
export const getProfileConfigs = () => _profileConfigs;
23✔
92
export const setProfileConfigs = (configs) => { _profileConfigs = configs; };
19✔
93

94
// Waits for a given promise to resolve within a timeout
95
export function withTimeout(promise, ms) {
96
  let timeout;
97
  const timeoutPromise = new Promise((resolve) => {
8✔
98
    timeout = setTimeout(() => resolve(undefined), ms);
8✔
99
  });
100

101
  return Promise.race([promise.finally(() => clearTimeout(timeout)), timeoutPromise]);
8✔
102
}
103

104
// Utility Functions
105
export const getCurrentTimeOfDay = () => {
1✔
106
  const currentHour = new Date().getHours();
6✔
107

108
  return currentHour < 5 ? CONSTANTS.TIME_OF_DAY_VALUES.NIGHT
6✔
109
    : currentHour < 12 ? CONSTANTS.TIME_OF_DAY_VALUES.MORNING
5✔
110
      : currentHour < 17 ? CONSTANTS.TIME_OF_DAY_VALUES.AFTERNOON
3✔
111
        : currentHour < 19 ? CONSTANTS.TIME_OF_DAY_VALUES.EVENING
2✔
112
          : CONSTANTS.TIME_OF_DAY_VALUES.NIGHT;
113
}
114

115
export const getBrowserType = () => {
1✔
116
  const brandName = getLowEntropySUA()?.browsers
8✔
117
    ?.map(b => b.brand.toLowerCase())
×
118
    .join(' ') || '';
119
  const browserMatch = brandName ? BROWSER_REGEX_MAP.find(({ regex }) => regex.test(brandName)) : -1;
8!
120

121
  if (browserMatch?.id) return browserMatch.id.toString();
8!
122

123
  const userAgent = navigator?.userAgent;
8✔
124
  let browserIndex = userAgent == null ? -1 : 0;
8✔
125

126
  if (userAgent) {
8✔
127
    browserIndex = BROWSER_REGEX_MAP.find(({ regex }) => regex.test(userAgent))?.id || 0;
53✔
128
  }
129
  return browserIndex.toString();
8✔
130
}
131

132
// Find all bids for a specific ad unit
133
function findBidsForAdUnit(auction, code) {
134
  return auction?.bidsReceived?.filter(bid => bid.adUnitCode === code) || [];
10!
135
}
136

137
// Find rejected bids for a specific ad unit
138
function findRejectedBidsForAdUnit(auction, code) {
139
  if (!auction?.bidsRejected) return [];
7✔
140

141
  // If bidsRejected is an array
142
  if (Array.isArray(auction.bidsRejected)) {
5✔
143
    return auction.bidsRejected.filter(bid => bid.adUnitCode === code);
5✔
144
  }
145

146
  // If bidsRejected is an object mapping bidders to their rejected bids
147
  if (typeof auction.bidsRejected === 'object') {
×
148
    return Object.values(auction.bidsRejected)
×
149
      .filter(Array.isArray)
150
      .flatMap(bidderBids => bidderBids.filter(bid => bid.adUnitCode === code));
×
151
  }
152

153
  return [];
×
154
}
155

156
// Find a rejected bid due to price floor
157
function findRejectedFloorBid(rejectedBids) {
158
  return rejectedBids.find(bid => {
7✔
159
    return bid.rejectionReason === REJECTION_REASON.FLOOR_NOT_MET &&
1✔
160
      (bid.floorData?.floorValue && bid.cpm < bid.floorData.floorValue);
161
  });
162
}
163

164
// Find the winning or highest bid for an ad unit
165
function findWinningBid(adUnitCode) {
166
  try {
7✔
167
    const pbjs = getGlobal();
7✔
168
    if (!pbjs?.getHighestCpmBids) return null;
7!
169

170
    const highestCpmBids = pbjs.getHighestCpmBids(adUnitCode);
7✔
171
    if (!highestCpmBids?.length) {
7✔
172
      logInfo(CONSTANTS.LOG_PRE_FIX, `No highest CPM bids found for ad unit: ${adUnitCode}`);
6✔
173
      return null;
6✔
174
    }
175

176
    const highestCpmBid = highestCpmBids[0];
1✔
177
    logInfo(CONSTANTS.LOG_PRE_FIX, `Found highest CPM bid using pbjs.getHighestCpmBids() for ad unit: ${adUnitCode}, CPM: ${highestCpmBid.cpm}`);
1✔
178
    return highestCpmBid;
1✔
179
  } catch (error) {
180
    logError(CONSTANTS.LOG_PRE_FIX, `Error finding highest CPM bid: ${error}`);
×
181
    return null;
×
182
  }
183
}
184

185
// Find floor value from bidder requests
186
function findFloorValueFromBidderRequests(auction, code) {
187
  if (!auction?.bidderRequests?.length) return 0;
5✔
188

189
  // Find all bids in bidder requests for this ad unit
190
  const bidsFromRequests = auction.bidderRequests
3✔
191
    .flatMap(request => request.bids || [])
3!
192
    .filter(bid => bid.adUnitCode === code);
3✔
193

194
  if (!bidsFromRequests.length) {
3!
195
    logInfo(CONSTANTS.LOG_PRE_FIX, `No bids found for ad unit: ${code}`);
×
196
    return 0;
×
197
  }
198

199
  const bidWithGetFloor = bidsFromRequests.find(bid => bid.getFloor);
3✔
200
  if (!bidWithGetFloor) {
3!
201
    logInfo(CONSTANTS.LOG_PRE_FIX, `No bid with getFloor method found for ad unit: ${code}`);
×
202
    return 0;
×
203
  }
204

205
  // Helper function to extract sizes with their media types from a source object
206
  const extractSizes = (source) => {
3✔
207
    if (!source) return null;
3!
208

209
    const result = [];
3✔
210

211
    // Extract banner sizes
212
    if (source.mediaTypes?.banner?.sizes) {
3✔
213
      source.mediaTypes.banner.sizes.forEach(size => {
3✔
214
        result.push({
4✔
215
          size,
216
          mediaType: 'banner'
217
        });
218
      });
219
    }
220

221
    // Extract video sizes
222
    if (source.mediaTypes?.video?.playerSize) {
3✔
223
      const playerSize = source.mediaTypes.video.playerSize;
1✔
224
      // Handle both formats: [[w, h]] and [w, h]
225
      const videoSizes = Array.isArray(playerSize[0]) ? playerSize : [playerSize];
1!
226

227
      videoSizes.forEach(size => {
1✔
228
        result.push({
1✔
229
          size,
230
          mediaType: 'video'
231
        });
232
      });
233
    }
234

235
    // Use general sizes as fallback if no specific media types found
236
    if (result.length === 0 && source.sizes) {
3!
237
      source.sizes.forEach(size => {
×
238
        result.push({
×
239
          size,
240
          mediaType: 'banner' // Default to banner for general sizes
241
        });
242
      });
243
    }
244

245
    return result.length > 0 ? result : null;
3!
246
  };
247

248
  // Try to get sizes from different sources in order of preference
249
  const adUnit = auction.adUnits?.find(unit => unit.code === code);
3✔
250
  let sizes = extractSizes(adUnit) || extractSizes(bidWithGetFloor);
3!
251

252
  // Handle fallback to wildcard size if no sizes found
253
  if (!sizes) {
3!
254
    sizes = [{ size: ['*', '*'], mediaType: 'banner' }];
×
255
    logInfo(CONSTANTS.LOG_PRE_FIX, `No sizes found, using wildcard size for ad unit: ${code}`);
×
256
  }
257

258
  // Try to get floor values for each size
259
  let minFloor = -1;
3✔
260

261
  for (const sizeObj of sizes) {
3✔
262
    // Extract size and mediaType from the object
263
    const { size, mediaType } = sizeObj;
5✔
264

265
    // Call getFloor with the appropriate media type
266
    const floorInfo = bidWithGetFloor.getFloor({
5✔
267
      currency: 'USD', // Default currency
268
      mediaType: mediaType, // Use the media type we extracted
269
      size: size
270
    });
271

272
    if (floorInfo?.floor && !isNaN(parseFloat(floorInfo.floor))) {
5✔
273
      const floorValue = parseFloat(floorInfo.floor);
5✔
274
      logInfo(CONSTANTS.LOG_PRE_FIX, `Floor value for ${mediaType} size ${size}: ${floorValue}`);
5✔
275

276
      // Update minimum floor value
277
      minFloor = minFloor === -1 ? floorValue : Math.min(minFloor, floorValue);
5✔
278
    }
279
  }
280

281
  if (minFloor !== -1) {
3✔
282
    logInfo(CONSTANTS.LOG_PRE_FIX, `Calculated minimum floor value ${minFloor} for ad unit: ${code}`);
3✔
283
    return minFloor;
3✔
284
  }
285

286
  logInfo(CONSTANTS.LOG_PRE_FIX, `No floor data found for ad unit: ${code}`);
×
287
  return 0;
×
288
}
289

290
// Select multiplier based on priority order: floors.json → config.json → default
291
function selectMultiplier(multiplierKey, profileConfigs) {
292
  // Define sources in priority order
293
  const multiplierSources = [
7✔
294
    {
295
      name: 'config.json',
296
      getValue: () => {
297
        const configPath = profileConfigs?.plugins?.dynamicFloors?.pmTargetingKeys?.multiplier;
7✔
298
        const lowerKey = multiplierKey.toLowerCase();
7✔
299
        return configPath && lowerKey in configPath ? configPath[lowerKey] : null;
7✔
300
      }
301
    },
302
    {
303
      name: 'floor.json',
304
      getValue: () => _multipliers && multiplierKey in _multipliers ? _multipliers[multiplierKey] : null
4!
305
    },
306
    {
307
      name: 'default',
308
      getValue: () => CONSTANTS.MULTIPLIERS[multiplierKey]
4✔
309
    }
310
  ];
311

312
  // Find the first source with a non-null value
313
  for (const source of multiplierSources) {
7✔
314
    const value = source.getValue();
15✔
315
    if (value != null) {
15✔
316
      return { value, source: source.name };
7✔
317
    }
318
  }
319

320
  // Fallback (shouldn't happen due to default source)
321
  return { value: CONSTANTS.MULTIPLIERS[multiplierKey], source: 'default' };
×
322
}
323

324
// Identify winning bid scenario and return scenario data
325
function handleWinningBidScenario(winningBid, code) {
326
  return {
1✔
327
    scenario: 'winning',
328
    bidStatus: CONSTANTS.BID_STATUS.WON,
329
    baseValue: winningBid.cpm,
330
    multiplierKey: 'WIN',
331
    logMessage: `Bid won for ad unit: ${code}, CPM: ${winningBid.cpm}`
332
  };
333
}
334

335
// Identify rejected floor bid scenario and return scenario data
336
function handleRejectedFloorBidScenario(rejectedFloorBid, code) {
337
  const baseValue = rejectedFloorBid.floorData?.floorValue || 0;
1!
338
  return {
1✔
339
    scenario: 'rejected',
340
    bidStatus: CONSTANTS.BID_STATUS.FLOORED,
341
    baseValue,
342
    multiplierKey: 'FLOORED',
343
    logMessage: `Bid rejected due to price floor for ad unit: ${code}, Floor value: ${baseValue}, Bid CPM: ${rejectedFloorBid.cpm}`
344
  };
345
}
346

347
// Identify no bid scenario and return scenario data
348
function handleNoBidScenario(auction, code) {
349
  const baseValue = findFloorValueFromBidderRequests(auction, code);
5✔
350
  return {
5✔
351
    scenario: 'nobid',
352
    bidStatus: CONSTANTS.BID_STATUS.NOBID,
353
    baseValue,
354
    multiplierKey: 'NOBID',
355
    logMessage: `No bids for ad unit: ${code}, Floor value: ${baseValue}`
356
  };
357
}
358

359
// Determine which scenario applies based on bid conditions
360
function determineScenario(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code) {
361
  return winningBid ? handleWinningBidScenario(winningBid, code)
7✔
362
    : rejectedFloorBid ? handleRejectedFloorBidScenario(rejectedFloorBid, code)
6✔
363
      : handleNoBidScenario(auction, code);
364
}
365

366
// Main function that determines bid status and calculates values
367
function determineBidStatusAndValues(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code) {
368
  const profileConfigs = getProfileConfigs();
7✔
369

370
  // Determine the scenario based on bid conditions
371
  const { bidStatus, baseValue, multiplierKey, logMessage } =
372
    determineScenario(winningBid, rejectedFloorBid, bidsForAdUnit, auction, code);
7✔
373

374
  // Select the appropriate multiplier
375
  const { value: multiplier, source } = selectMultiplier(multiplierKey, profileConfigs);
7✔
376
  logInfo(CONSTANTS.LOG_PRE_FIX, logMessage + ` (Using ${source} multiplier: ${multiplier})`);
7✔
377

378
  return { bidStatus, baseValue, multiplier };
7✔
379
}
380

381
// Getter Functions
382
export const getOs = () => getOS().toString();
1✔
383
export const getDeviceType = () => fetchDeviceType().toString();
1✔
384
export const getCountry = () => _country;
1✔
385
export const getBidder = (request) => request?.bidder;
4✔
386
export const getUtm = () => {
1✔
387
  const url = new URL(window.location?.href);
2✔
388
  const urlParams = new URLSearchParams(url?.search);
2✔
389
  return urlParams && urlParams.toString().includes(CONSTANTS.UTM) ? CONSTANTS.UTM_VALUES.TRUE : CONSTANTS.UTM_VALUES.FALSE;
2!
390
}
391

392
export const getFloorsConfig = (floorsData, profileConfigs) => {
1✔
393
  if (!isPlainObject(profileConfigs) || isEmpty(profileConfigs)) {
10✔
394
    logError(`${CONSTANTS.LOG_PRE_FIX} profileConfigs is not an object or is empty`);
4✔
395
    return undefined;
4✔
396
  }
397

398
  // Floor configs from adunit / setconfig
399
  const defaultFloorConfig = conf.getConfig('floors') ?? {};
6!
400
  if (defaultFloorConfig?.endpoint) {
6!
401
    delete defaultFloorConfig.endpoint;
×
402
  }
403
  // Plugin data from profile
404
  const dynamicFloors = profileConfigs?.plugins?.dynamicFloors;
6✔
405

406
  // If plugin disabled or config not present, return undefined
407
  if (!dynamicFloors?.enabled || !dynamicFloors?.config) {
6✔
408
    return undefined;
1✔
409
  }
410

411
  const config = { ...dynamicFloors.config };
5✔
412

413
  // default values provided by publisher on profile
414
  const defaultValues = config.defaultValues ?? {};
5✔
415
  // If floorsData is not present, use default values
416
  const finalFloorsData = floorsData ?? { ...defaultValueTemplate, values: { ...defaultValues } };
5✔
417

418
  delete config.defaultValues;
5✔
419
  // If skiprate is provided in configs, overwrite the value in finalFloorsData
420
  (config.skipRate !== undefined) && (finalFloorsData.skipRate = config.skipRate);
5✔
421

422
  // merge default configs from page, configs
423
  return {
5✔
424
    floors: {
425
      ...defaultFloorConfig,
426
      ...config,
427
      data: finalFloorsData,
428
      additionalSchemaFields: {
429
        deviceType: getDeviceType,
430
        timeOfDay: getCurrentTimeOfDay,
431
        browser: getBrowserType,
432
        os: getOs,
433
        utm: getUtm,
434
        country: getCountry,
435
        bidder: getBidder,
436
      },
437
    },
438
  };
439
};
440

441
export const fetchData = async (publisherId, profileId, type) => {
1✔
442
  try {
16✔
443
    const endpoint = CONSTANTS.ENDPOINTS[type];
16✔
444
    const baseURL = (type == 'FLOORS') ? `${CONSTANTS.ENDPOINTS.BASEURL}/floors` : CONSTANTS.ENDPOINTS.BASEURL;
16✔
445
    const url = `${baseURL}/${publisherId}/${profileId}/${endpoint}`;
16✔
446
    const response = await fetch(url);
16✔
447

448
    if (!response.ok) {
8✔
449
      logError(`${CONSTANTS.LOG_PRE_FIX} Error while fetching ${type}: Not ok`);
2✔
450
      return;
2✔
451
    }
452

453
    if (type === "FLOORS") {
6✔
454
      const cc = response.headers?.get('country_code');
4✔
455
      _country = cc ? cc.split(',')?.map(code => code.trim())[0] : undefined;
4✔
456
    }
457

458
    const data = await response.json();
6✔
459

460
    // Extract multipliers from floors.json if available
461
    if (type === "FLOORS" && data && data.multiplier) {
4!
462
      // Map of source keys to destination keys
463
      const multiplierKeys = {
×
464
        'win': 'WIN',
465
        'floored': 'FLOORED',
466
        'nobid': 'NOBID'
467
      };
468

469
      // Initialize _multipliers and only add keys that exist in data.multiplier
470
      _multipliers = Object.entries(multiplierKeys)
×
471
        .reduce((acc, [srcKey, destKey]) => {
×
472
          if (srcKey in data.multiplier) {
×
473
            acc[destKey] = data.multiplier[srcKey];
×
474
          }
475
          return acc;
×
476
        }, {});
477

478
      logInfo(CONSTANTS.LOG_PRE_FIX, `Using multipliers from floors.json: ${JSON.stringify(_multipliers)}`);
×
479
    }
480

481
    return data;
4✔
482
  } catch (error) {
483
    logError(`${CONSTANTS.LOG_PRE_FIX} Error while fetching ${type}: ${error}`);
10✔
484
  }
485
};
486

487
/**
488
 * Initialize the Pubmatic RTD Module.
489
 * @param {Object} config
490
 * @param {Object} _userConsent
491
 * @returns {boolean}
492
 */
493
const init = (config, _userConsent) => {
1✔
494
  initTime = Date.now(); // Capture the initialization time
7✔
495
  let { publisherId, profileId } = config?.params || {};
7✔
496

497
  if (!publisherId || !profileId) {
7✔
498
    logError(`${CONSTANTS.LOG_PRE_FIX} ${!publisherId ? 'Missing publisher Id.' : 'Missing profile Id.'}`);
3✔
499
    return false;
3✔
500
  }
501

502
  publisherId = String(publisherId).trim();
4✔
503
  profileId = String(profileId).trim();
4✔
504

505
  if (!isFn(continueAuction)) {
4✔
506
    logError(`${CONSTANTS.LOG_PRE_FIX} continueAuction is not a function. Please ensure to add priceFloors module.`);
1✔
507
    return false;
1✔
508
  }
509

510
  _fetchFloorRulesPromise = fetchData(publisherId, profileId, "FLOORS");
3✔
511
  _fetchConfigPromise = fetchData(publisherId, profileId, "CONFIGS");
3✔
512

513
  _fetchConfigPromise.then(async (profileConfigs) => {
3✔
514
    const auctionDelay = conf?.getConfig('realTimeData')?.auctionDelay || 300;
3✔
515
    const maxWaitTime = 0.8 * auctionDelay;
3✔
516

517
    const elapsedTime = Date.now() - initTime;
3✔
518
    const remainingTime = Math.max(maxWaitTime - elapsedTime, 0);
3✔
519
    const floorsData = await withTimeout(_fetchFloorRulesPromise, remainingTime);
3✔
520

521
    // Store the profile configs globally
522
    setProfileConfigs(profileConfigs);
3✔
523

524
    const floorsConfig = getFloorsConfig(floorsData, profileConfigs);
3✔
525
    floorsConfig && conf?.setConfig(floorsConfig);
3!
526
    configMerged();
3✔
527
  });
528

529
  return true;
3✔
530
};
531

532
/**
533
 * @param {Object} reqBidsConfigObj
534
 * @param {function} callback
535
 */
536
const getBidRequestData = (reqBidsConfigObj, callback) => {
1✔
537
  configMergedPromise.then(() => {
2✔
538
    const hookConfig = {
2✔
539
      reqBidsConfigObj,
540
      context: this,
541
      nextFn: () => true,
×
542
      haveExited: false,
543
      timer: null
544
    };
545
    continueAuction(hookConfig);
2✔
546
    if (_country) {
2!
547
      const ortb2 = {
×
548
        user: {
549
          ext: {
550
            ctr: _country,
551
          }
552
        }
553
      }
554

555
      mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, {
×
556
        [CONSTANTS.SUBMODULE_NAME]: ortb2
557
      });
558
    }
559
    callback();
2✔
560
  }).catch((error) => {
561
    logError(CONSTANTS.LOG_PRE_FIX, 'Error in updating floors :', error);
×
562
    callback();
×
563
  });
564
}
565

566
/**
567
 * Returns targeting data for ad units
568
 * @param {string[]} adUnitCodes - Ad unit codes
569
 * @param {Object} config - Module configuration
570
 * @param {Object} userConsent - User consent data
571
 * @param {Object} auction - Auction object
572
 * @return {Object} - Targeting data for ad units
573
 */
574
export const getTargetingData = (adUnitCodes, config, userConsent, auction) => {
1✔
575
  // Access the profile configs stored globally
576
  const profileConfigs = getProfileConfigs();
8✔
577

578
  // Return empty object if profileConfigs is undefined or pmTargetingKeys.enabled is explicitly set to false
579
  if (!profileConfigs || profileConfigs?.plugins?.dynamicFloors?.pmTargetingKeys?.enabled === false) {
8✔
580
    logInfo(`${CONSTANTS.LOG_PRE_FIX} pmTargetingKeys is disabled or profileConfigs is undefined`);
2✔
581
    return {};
2✔
582
  }
583

584
  // Helper to check if RTD floor is applied to a bid
585
  const isRtdFloorApplied = bid => bid.floorData?.floorProvider === "PM" && !bid.floorData.skipped;
11✔
586

587
  // Check if any bid has RTD floor applied
588
  const hasRtdFloorAppliedBid =
589
    auction?.adUnits?.some(adUnit => adUnit.bids?.some(isRtdFloorApplied)) ||
7✔
590
    auction?.bidsReceived?.some(isRtdFloorApplied);
591

592
  // Only log when RTD floor is applied
593
  if (hasRtdFloorAppliedBid) {
6✔
594
    logInfo(CONSTANTS.LOG_PRE_FIX, 'Setting targeting via getTargetingData:');
5✔
595
  }
596

597
  // Process each ad unit code
598
  const targeting = {};
6✔
599

600
  adUnitCodes.forEach(code => {
6✔
601
    targeting[code] = {};
9✔
602

603
    // For non-RTD floor applied cases, only set pm_ym_flrs to 0
604
    if (!hasRtdFloorAppliedBid) {
9✔
605
      targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRS] = 0;
2✔
606
      return;
2✔
607
    }
608

609
    // Find bids and determine status for RTD floor applied cases
610
    const bidsForAdUnit = findBidsForAdUnit(auction, code);
7✔
611
    const rejectedBidsForAdUnit = findRejectedBidsForAdUnit(auction, code);
7✔
612
    const rejectedFloorBid = findRejectedFloorBid(rejectedBidsForAdUnit);
7✔
613
    const winningBid = findWinningBid(code);
7✔
614

615
    // Determine bid status and values
616
    const { bidStatus, baseValue, multiplier } = determineBidStatusAndValues(
7✔
617
      winningBid,
618
      rejectedFloorBid,
619
      bidsForAdUnit,
620
      auction,
621
      code
622
    );
623

624
    // Set all targeting keys
625
    targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRS] = 1;
7✔
626
    targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_FLRV] = (baseValue * multiplier).toFixed(2);
7✔
627
    targeting[code][CONSTANTS.TARGETING_KEYS.PM_YM_BID_S] = bidStatus;
7✔
628
  });
629

630
  return targeting;
6✔
631
};
632

633
export const pubmaticSubmodule = {
1✔
634
  /**
635
   * used to link submodule with realTimeData
636
   * @type {string}
637
   */
638
  name: CONSTANTS.SUBMODULE_NAME,
639
  init,
640
  getBidRequestData,
641
  getTargetingData
642
};
643

644
export const registerSubModule = () => {
1✔
645
  submodule(CONSTANTS.REAL_TIME_MODULE, pubmaticSubmodule);
2✔
646
}
647

648
registerSubModule();
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc