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

prebid / Prebid.js / 19950878469

05 Dec 2025 02:41AM UTC coverage: 96.212% (+0.003%) from 96.209%
19950878469

push

github

web-flow
PubMatic RTD Provider: Dynamic timeout plugin & code refactoring (#14104)

* PubMatic RTD Provider: Plugin based architectural changes

* PubMatic RTD Provider: Minor changes to the Plugins

* PubMatic RTD Provider: Moved configJsonManager to the RTD provider

* PubMatic RTD Provider: Move bidderOptimisation to one file

* Adding testcases for floorProvider.js for floorPlugin

* PubMatic RTD provider: Update endpoint to actual after testing

* Adding test cases for floorProvider

* Adding test cases for config

* PubMatic RTD provider: Handle the getTargetting effectively

* Pubmatic RTD Provider: Handle null case

* Adding test cases for floorProviders

* fixing linting issues

* Fixing linting and other errors

* RTD provider: Update pubmaticRtdProvider.js

* RTD Provider: Update pubmaticRtdProvider_spec.js

* RTD Provider: Dynamic Timeout Plugin

* RTD Provider: Dynamic Timeout Plugin Updated with new schema

* RTD Provider: Plugin changes

* RTD Provider: Dynamic Timeout fixes

* RTD Provider: Plugin changes

* RTD Provider: Plugin changes

* RTD Provider: Plugin changes

(cherry picked from commit d440ca6ae)

* RTD Provider: Plugin changes for Dynamic Timeout

* RTD PRovider: Test cases : PubmaticUTils test cases and

* RTD PRovider: Plugin Manager test cases

* RTD Provider: Lint issues and Spec removed

* RTD Provider: Dynamic TImeout Test cases

* Add unit test cases for unifiedPricingRule.js

* RTD Provider: Fixes for Floor and UPR working

* RTD Provider: test cases updated

* Dynamic timeout comments added

* Remove bidder optimization

* PubMatic RTD Provider: Handle Empty object case for floor data

* RTD Provider: Update the priority for dynamic timeout rules

* Dynamic timeout: handle default skipRate

* Dynamic Timeout - Handle Negative values gracefully with 500 Default threshold

* Review comments resolved

* Comments added

* Update pubmaticRtdProvider_Example.html

* Fix lint & test cases

* Update de... (continued)

53903 of 66028 branches covered (81.64%)

1396 of 1453 new or added lines in 15 files covered. (96.08%)

24 existing lines in 7 files now uncovered.

205952 of 214061 relevant lines covered (96.21%)

71.49 hits per line

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

90.21
/src/utils.js
1
import {config} from './config.js';
2

3
import {EVENTS} from './constants.js';
4
import {PbPromise} from './utils/promise.js';
5
import deepAccess from 'dlv/index.js';
6
import {isArray, isFn, isStr, isPlainObject} from './utils/objects.js';
7

8
export { deepAccess };
9
export { dset as deepSetValue } from 'dset';
10
export * from './utils/objects.js'
11
export {getWinDimensions, resetWinDimensions, getScreenOrientation} from './utils/winDimensions.js';
12
const consoleExists = Boolean(window.console);
8✔
13
const consoleLogExists = Boolean(consoleExists && window.console.log);
8✔
14
const consoleInfoExists = Boolean(consoleExists && window.console.info);
8✔
15
const consoleWarnExists = Boolean(consoleExists && window.console.warn);
8✔
16
const consoleErrorExists = Boolean(consoleExists && window.console.error);
8✔
17

18
let eventEmitter;
19

20
export function _setEventEmitter(emitFn) {
21
  // called from events.js - this hoop is to avoid circular imports
22
  eventEmitter = emitFn;
8✔
23
}
24

25
function emitEvent(...args) {
26
  if (eventEmitter != null) {
5,526✔
27
    eventEmitter(...args);
5,526✔
28
  }
29
}
30

31
// this allows stubbing of utility functions that are used internally by other utility functions
32
export const internal = {
8✔
33
  checkCookieSupport,
34
  createTrackPixelIframeHtml,
35
  getWindowSelf,
36
  getWindowTop,
37
  canAccessWindowTop,
38
  getWindowLocation,
39
  insertUserSyncIframe,
40
  insertElement,
41
  isFn,
42
  triggerPixel,
43
  logError,
44
  logWarn,
45
  logMessage,
46
  logInfo,
47
  parseQS,
48
  formatQS,
49
  deepEqual,
50
};
51

52
const prebidInternal = {};
8✔
53
/**
54
 * Returns object that is used as internal prebid namespace
55
 */
56
export function getPrebidInternal() {
57
  return prebidInternal;
×
58
}
59

60
/* utility method to get incremental integer starting from 1 */
61
var getIncrementalInteger = (function () {
8✔
62
  var count = 0;
8✔
63
  return function () {
8✔
64
    count++;
916✔
65
    return count;
916✔
66
  };
67
})();
68

69
// generate a random string (to be used as a dynamic JSONP callback)
70
export function getUniqueIdentifierStr() {
71
  return getIncrementalInteger() + Math.random().toString(16).substr(2);
916✔
72
}
73

74
/**
75
 * Returns a random v4 UUID of the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,
76
 * where each x is replaced with a random hexadecimal digit from 0 to f,
77
 * and y is replaced with a random hexadecimal digit from 8 to b.
78
 * https://gist.github.com/jed/982883 via node-uuid
79
 */
80
export function generateUUID(placeholder) {
81
  return placeholder
223,463✔
82
    ? (placeholder ^ _getRandomData() >> placeholder / 4).toString(16)
83
    : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID);
84
}
85

86
/**
87
 * Returns random data using the Crypto API if available and Math.random if not
88
 * Method is from https://gist.github.com/jed/982883 like generateUUID, direct link https://gist.github.com/jed/982883#gistcomment-45104
89
 */
90
function _getRandomData() {
91
  if (window && window.crypto && window.crypto.getRandomValues) {
216,480!
92
    return crypto.getRandomValues(new Uint8Array(1))[0] % 16;
216,480✔
93
  } else {
94
    return Math.random() * 16;
×
95
  }
96
}
97

98
export function getBidIdParameter(key, paramsObj) {
99
  return paramsObj?.[key] || '';
3,422✔
100
}
101

102
// parse a query string object passed in bid params
103
// bid params should be an object such as {key: "value", key1 : "value1"}
104
// aliases to formatQS
105
export function parseQueryStringParameters(queryObj) {
106
  let result = '';
79✔
107
  for (var k in queryObj) {
79✔
108
    if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; }
959✔
109
  }
110
  result = result.replace(/&$/, '');
79✔
111
  return result;
79✔
112
}
113

114
// transform an AdServer targeting bids into a query string to send to the adserver
115
export function transformAdServerTargetingObj(targeting) {
116
  // we expect to receive targeting for a single slot at a time
117
  if (targeting && Object.getOwnPropertyNames(targeting).length > 0) {
3✔
118
    return Object.keys(targeting)
2✔
119
      .map(key => `${key}=${encodeURIComponent(targeting[key])}`).join('&');
67✔
120
  } else {
121
    return '';
1✔
122
  }
123
}
124

125
/**
126
 * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of width, height tuples `[[300, 250]]` or '[[300,250], [970,90]]'
127
 */
128
export function sizesToSizeTuples(sizes) {
129
  if (typeof sizes === 'string') {
3,908✔
130
    // multiple sizes will be comma-separated
131
    return sizes
32✔
132
      .split(/\s*,\s*/)
133
      .map(sz => sz.match(/^(\d+)x(\d+)$/i))
35✔
134
      .filter(match => match)
35✔
135
      .map(([_, w, h]) => [parseInt(w, 10), parseInt(h, 10)])
28✔
136
  } else if (Array.isArray(sizes)) {
3,876✔
137
    if (isValidGPTSingleSize(sizes)) {
3,683✔
138
      return [sizes]
453✔
139
    }
140
    return sizes.filter(isValidGPTSingleSize);
3,230✔
141
  }
142
  return [];
193✔
143
}
144

145
/**
146
 * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']'
147
 * @param  {(Array.<number[]>|Array.<number>)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]]
148
 * @return {Array.<string>}  Array of strings like `["300x250"]` or `["300x250", "728x90"]`
149
 */
150
export function parseSizesInput(sizeObj) {
151
  return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString);
2,011✔
152
}
153

154
export function sizeTupleToSizeString(size) {
155
  return size[0] + 'x' + size[1]
3,495✔
156
}
157

158
// Parse a GPT style single size array, (i.e [300, 250])
159
// into an AppNexus style string, (i.e. 300x250)
160
export function parseGPTSingleSizeArray(singleSize) {
161
  if (isValidGPTSingleSize(singleSize)) {
32✔
162
    return sizeTupleToSizeString(singleSize);
22✔
163
  }
164
}
165

166
export function sizeTupleToRtbSize(size) {
167
  return {w: size[0], h: size[1]};
2,947✔
168
}
169

170
// Parse a GPT style single size array, (i.e [300, 250])
171
// into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250})
172
export function parseGPTSingleSizeArrayToRtbSize(singleSize) {
173
  if (isValidGPTSingleSize(singleSize)) {
307✔
174
    return sizeTupleToRtbSize(singleSize)
301✔
175
  }
176
}
177

178
function isValidGPTSingleSize(singleSize) {
179
  // if we aren't exactly 2 items in this array, it is invalid
180
  return isArray(singleSize) && singleSize.length === 2 && (!isNaN(singleSize[0]) && !isNaN(singleSize[1]));
9,715✔
181
}
182

183
export function getWindowTop() {
184
  return window.top;
4,514✔
185
}
186

187
export function getWindowSelf() {
188
  return window.self;
1,991✔
189
}
190

191
export function getWindowLocation() {
192
  return window.location;
1,916✔
193
}
194

195
export function getDocument() {
196
  return document;
395✔
197
}
198

199
export function canAccessWindowTop() {
200
  try {
1,159✔
201
    if (internal.getWindowTop().location.href) {
1,159✔
202
      return true;
1,066✔
203
    }
204
  } catch (e) {
205
    return false;
93✔
206
  }
207
}
208

209
/**
210
 * Wrappers to console.(log | info | warn | error). Takes N arguments, the same as the native methods
211
 */
212
// eslint-disable-next-line no-restricted-syntax
213
export function logMessage() {
214
  if (debugTurnedOn() && consoleLogExists) {
15,184✔
215
    // eslint-disable-next-line no-console
216
    console.log.apply(console, decorateLog(arguments, 'MESSAGE:'));
22✔
217
  }
218
}
219

220
// eslint-disable-next-line no-restricted-syntax
221
export function logInfo() {
222
  if (debugTurnedOn() && consoleInfoExists) {
13,634✔
223
    // eslint-disable-next-line no-console
224
    console.info.apply(console, decorateLog(arguments, 'INFO:'));
3✔
225
  }
226
}
227

228
// eslint-disable-next-line no-restricted-syntax
229
export function logWarn() {
230
  if (debugTurnedOn() && consoleWarnExists) {
3,736!
231
    // eslint-disable-next-line no-console
UNCOV
232
    console.warn.apply(console, decorateLog(arguments, 'WARNING:'));
×
233
  }
234
  emitEvent(EVENTS.AUCTION_DEBUG, { type: 'WARNING', arguments: arguments });
3,736✔
235
}
236

237
// eslint-disable-next-line no-restricted-syntax
238
export function logError() {
239
  if (debugTurnedOn() && consoleErrorExists) {
1,790!
240
    // eslint-disable-next-line no-console
UNCOV
241
    console.error.apply(console, decorateLog(arguments, 'ERROR:'));
×
242
  }
243
  emitEvent(EVENTS.AUCTION_DEBUG, { type: 'ERROR', arguments: arguments });
1,790✔
244
}
245

246
export function prefixLog(prefix) {
247
  function decorate(fn) {
248
    return function (...args) {
2,796✔
249
      fn(prefix, ...args);
1,294✔
250
    }
251
  }
252
  return {
155✔
253
    logError: decorate(logError),
254
    logWarn: decorate(logWarn),
255
    logMessage: decorate(logMessage),
256
    logInfo: decorate(logInfo),
257
  }
258
}
259

260
function decorateLog(args, prefix) {
261
  args = [].slice.call(args);
25✔
262
  const bidder = config.getCurrentBidder();
25✔
263

264
  prefix && args.unshift(prefix);
25✔
265
  if (bidder) {
25!
266
    args.unshift(label('#aaa'));
×
267
  }
268
  args.unshift(label('#3b88c3'));
25✔
269
  args.unshift('%cPrebid' + (bidder ? `%c${bidder}` : ''));
25!
270
  return args;
25✔
271

272
  function label(color) {
273
    return `display: inline-block; color: #fff; background: ${color}; padding: 1px 4px; border-radius: 3px;`
25✔
274
  }
275
}
276

277
export function hasConsoleLogger() {
278
  return consoleLogExists;
×
279
}
280

281
export function debugTurnedOn() {
282
  return !!config.getConfig('debug');
35,285✔
283
}
284

285
export const createIframe = (() => {
8✔
286
  const DEFAULTS = {
8✔
287
    border: '0px',
288
    hspace: '0',
289
    vspace: '0',
290
    marginWidth: '0',
291
    marginHeight: '0',
292
    scrolling: 'no',
293
    frameBorder: '0',
294
    allowtransparency: 'true'
295
  }
296
  return (doc, attrs, style = {}) => {
22✔
297
    const f = doc.createElement('iframe');
16✔
298
    Object.assign(f, Object.assign({}, DEFAULTS, attrs));
15✔
299
    Object.assign(f.style, style);
8✔
300
    return f;
7✔
301
  }
302
})();
303

304
export function createInvisibleIframe() {
305
  return createIframe(document, {
6✔
306
    id: getUniqueIdentifierStr(),
307
    width: 0,
308
    height: 0,
309
    src: 'about:blank'
310
  }, {
311
    display: 'none',
312
    height: '0px',
313
    width: '0px',
314
    border: '0px'
315
  });
316
}
317

318
/*
319
 *   Check if a given parameter name exists in query string
320
 *   and if it does return the value
321
 */
322
export function getParameterByName(name) {
323
  return parseQS(getWindowLocation().search)[name] || '';
1,756✔
324
}
325

326
/**
327
 * Return if the object is "empty";
328
 * this includes falsey, no keys, or no items at indices
329
 * @param {*} object object to test
330
 * @return {Boolean} if object is empty
331
 */
332
export function isEmpty(object) {
333
  if (!object) return true;
56,550✔
334
  if (isArray(object) || isStr(object)) {
51,244✔
335
    return !(object.length > 0);
23,461✔
336
  }
337
  return Object.keys(object).length <= 0;
27,783✔
338
}
339

340
/**
341
 * Return if string is empty, null, or undefined
342
 * @param str string to test
343
 * @returns {boolean} if string is empty
344
 */
345
export function isEmptyStr(str) {
346
  return isStr(str) && (!str || str.length === 0);
787✔
347
}
348

349
/**
350
 * Iterate object with the function
351
 * falls back to es5 `forEach`
352
 * @param {Array|Object} object
353
 * @param {Function} fn - The function to execute for each element. It receives three arguments: value, key, and the original object.
354
 * @returns {void}
355
 */
356
export function _each(object, fn) {
357
  if (isFn(object?.forEach)) return object.forEach(fn, this);
7,226✔
358
  Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k));
35,998✔
359
}
360

361
export function contains(a, obj) {
362
  return isFn(a?.includes) && a.includes(obj);
31✔
363
}
364

365
/**
366
 * Map an array or object into another array
367
 * given a function
368
 * @param {Array|Object} object
369
 * @param {Function} callback - The function to execute for each element. It receives three arguments: value, key, and the original object.
370
 * @return {Array}
371
 */
372
export function _map(object, callback) {
373
  if (isFn(object?.map)) return object.map(callback);
454✔
374
  return Object.entries(object || {}).map(([k, v]) => callback(v, k, object))
7,489✔
375
}
376

377
/*
378
* Inserts an element(elm) as targets child, by default as first child
379
* @param {HTMLElement} elm
380
* @param {HTMLElement} [doc]
381
* @param {HTMLElement} [target]
382
* @param {Boolean} [asLastChildChild]
383
* @return {HTML Element}
384
*/
385
export function insertElement(elm, doc, target, asLastChildChild) {
386
  doc = doc || document;
19✔
387
  let parentEl;
388
  if (target) {
19✔
389
    parentEl = doc.getElementsByTagName(target);
15✔
390
  } else {
391
    parentEl = doc.getElementsByTagName('head');
4✔
392
  }
393
  try {
19✔
394
    parentEl = parentEl.length ? parentEl : doc.getElementsByTagName('body');
19!
395
    if (parentEl.length) {
19✔
396
      parentEl = parentEl[0];
19✔
397
      const insertBeforeEl = asLastChildChild ? null : parentEl.firstChild;
19✔
398
      return parentEl.insertBefore(elm, insertBeforeEl);
19✔
399
    }
400
  } catch (e) {}
401
}
402

403
/**
404
 * Returns a promise that completes when the given element triggers a 'load' or 'error' DOM event, or when
405
 * `timeout` milliseconds have elapsed.
406
 *
407
 * @param {HTMLElement} element
408
 * @param {Number} [timeout]
409
 * @returns {Promise}
410
 */
411
export function waitForElementToLoad(element, timeout) {
412
  let timer = null;
3✔
413
  return new PbPromise((resolve) => {
3✔
414
    const onLoad = function() {
3✔
415
      element.removeEventListener('load', onLoad);
3✔
416
      element.removeEventListener('error', onLoad);
3✔
417
      if (timer != null) {
3✔
418
        window.clearTimeout(timer);
1✔
419
      }
420
      resolve();
3✔
421
    };
422
    element.addEventListener('load', onLoad);
3✔
423
    element.addEventListener('error', onLoad);
3✔
424
    if (timeout != null) {
3✔
425
      timer = window.setTimeout(onLoad, timeout);
1✔
426
    }
427
  });
428
}
429

430
/**
431
 * Inserts an image pixel with the specified `url` for cookie sync
432
 * @param {string} url URL string of the image pixel to load
433
 * @param  {function} [done] an optional exit callback, used when this usersync pixel is added during an async process
434
 * @param  {Number} [timeout] an optional timeout in milliseconds for the image to load before calling `done`
435
 */
436
export function triggerPixel(url, done, timeout) {
437
  const img = new Image();
68✔
438
  if (done && internal.isFn(done)) {
68!
439
    waitForElementToLoad(img, timeout).then(done);
×
440
  }
441
  img.src = url;
68✔
442
}
443

444
/**
445
 * Inserts an empty iframe with the specified `html`, primarily used for tracking purposes
446
 * (though could be for other purposes)
447
 * @param {string} htmlCode snippet of HTML code used for tracking purposes
448
 */
449
export function insertHtmlIntoIframe(htmlCode) {
450
  if (!htmlCode) {
×
451
    return;
×
452
  }
453
  const iframe = createInvisibleIframe();
×
454
  internal.insertElement(iframe, document, 'body');
×
455

456
  ((doc) => {
×
457
    doc.open();
×
458
    doc.write(htmlCode);
×
459
    doc.close();
×
460
  })(iframe.contentWindow.document);
461
}
462

463
/**
464
 * Inserts empty iframe with the specified `url` for cookie sync
465
 * @param  {string} url URL to be requested
466
 * @param  {function} [done] an optional exit callback, used when this usersync pixel is added during an async process
467
 * @param  {Number} [timeout] an optional timeout in milliseconds for the iframe to load before calling `done`
468
 */
469
export function insertUserSyncIframe(url, done, timeout) {
470
  const iframeHtml = internal.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin');
1✔
471
  const div = document.createElement('div');
1✔
472
  div.innerHTML = iframeHtml;
1✔
473
  const iframe = div.firstChild;
1✔
474
  if (done && internal.isFn(done)) {
1!
475
    waitForElementToLoad(iframe, timeout).then(done);
×
476
  }
477
  internal.insertElement(iframe, document, 'html', true);
1✔
478
}
479

480
/**
481
 * Creates a snippet of HTML that retrieves the specified `url`
482
 * @param  {string} url URL to be requested
483
 * @param encode
484
 * @return {string}     HTML snippet that contains the img src = set to `url`
485
 */
486
export function createTrackPixelHtml(url, encode = encodeURI) {
98✔
487
  if (!url) {
98!
488
    return '';
×
489
  }
490

491
  const escapedUrl = encode(url);
98✔
492
  let img = '<div style="position:absolute;left:0px;top:0px;visibility:hidden;">';
98✔
493
  img += '<img src="' + escapedUrl + '"></div>';
98✔
494
  return img;
98✔
495
};
496

497
/**
498
 * encodeURI, but preserves macros of the form '${MACRO}' (e.g. '${AUCTION_PRICE}')
499
 * @param url
500
 * @return {string}
501
 */
502
export function encodeMacroURI(url) {
503
  const macros = Array.from(url.matchAll(/\$({[^}]+})/g)).map(match => match[1]);
53✔
504
  return macros.reduce((str, macro) => {
41✔
505
    return str.replace('$' + encodeURIComponent(macro), '$' + macro)
26✔
506
  }, encodeURI(url))
507
}
508

509
/**
510
 * Creates a snippet of Iframe HTML that retrieves the specified `url`
511
 * @param  {string} url plain URL to be requested
512
 * @param  {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true
513
 * @param  {string} sandbox string if provided the sandbox attribute will be included with the given value
514
 * @return {string}     HTML snippet that contains the iframe src = set to `url`
515
 */
516
export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') {
1!
517
  if (!url) {
1!
518
    return '';
×
519
  }
520
  if (encodeUri) {
1!
521
    url = encodeURI(url);
×
522
  }
523
  if (sandbox) {
1✔
524
    sandbox = `sandbox="${sandbox}"`;
1✔
525
  }
526

527
  return `<iframe ${sandbox} id="${getUniqueIdentifierStr()}"
1✔
528
      frameborder="0"
529
      allowtransparency="true"
530
      marginheight="0" marginwidth="0"
531
      width="0" hspace="0" vspace="0" height="0"
532
      style="height:0px;width:0px;display:none;"
533
      scrolling="no"
534
      src="${url}">
535
    </iframe>`;
536
}
537

538
export function uniques(value, index, arry) {
539
  return arry.indexOf(value) === index;
20,206✔
540
}
541

542
export function flatten(a, b) {
543
  return a.concat(b);
3,460✔
544
}
545

546
export function getBidRequest(id, bidderRequests) {
547
  if (!id) {
249✔
548
    return;
4✔
549
  }
550
  return bidderRequests.flatMap(br => br.bids)
331✔
551
    .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id))
362✔
552
}
553

554
export function getValue(obj, key) {
555
  return obj[key];
153✔
556
}
557

558
export function getBidderCodes(adUnits) {
559
  // this could memoize adUnits
560
  return adUnits.map(unit => unit.bids.map(bid => bid.bidder)
1,169✔
561
    .reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques);
1,169✔
562
}
563

564
export function isGptPubadsDefined() {
565
  if (window.googletag && isFn(window.googletag.pubads) && isFn(window.googletag.pubads().getSlots)) {
877✔
566
    return true;
857✔
567
  }
568
}
569

570
export function isApnGetTagDefined() {
571
  if (window.apntag && isFn(window.apntag.getTag)) {
5✔
572
    return true;
4✔
573
  }
574
}
575

576
export const sortByHighestCpm = (a, b) => {
8✔
577
  return b.cpm - a.cpm;
638✔
578
}
579

580
/**
581
 * Fisher–Yates shuffle
582
 * http://stackoverflow.com/a/6274398
583
 * https://bost.ocks.org/mike/shuffle/
584
 * istanbul ignore next
585
 */
586
export function shuffle(array) {
587
  let counter = array.length;
222✔
588

589
  // while there are elements in the array
590
  while (counter > 0) {
222✔
591
    // pick a random index
592
    const index = Math.floor(Math.random() * counter);
771✔
593

594
    // decrease counter by 1
595
    counter--;
771✔
596

597
    // and swap the last element with it
598
    const temp = array[counter];
771✔
599
    array[counter] = array[index];
771✔
600
    array[index] = temp;
771✔
601
  }
602

603
  return array;
222✔
604
}
605

606
export function inIframe() {
607
  try {
1,704✔
608
    return internal.getWindowSelf() !== internal.getWindowTop();
1,704✔
609
  } catch (e) {
610
    return true;
×
611
  }
612
}
613

614
/**
615
 * https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf
616
 */
617
export function isSafeFrameWindow() {
618
  if (!inIframe()) {
72!
619
    return false;
×
620
  }
621

622
  const ws = internal.getWindowSelf();
72✔
623
  return !!(ws.$sf && ws.$sf.ext);
72✔
624
}
625

626
/**
627
 * Returns the result of calling the function $sf.ext.geom() if it exists
628
 * @see https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf — 5.4 Function $sf.ext.geom
629
 * @returns {Object | undefined} geometric information about the container
630
 */
631
export function getSafeframeGeometry() {
632
  try {
4✔
633
    const ws = getWindowSelf();
4✔
634
    return (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : undefined;
4✔
635
  } catch (e) {
636
    logError('Error getting SafeFrame geometry', e);
×
637
    return undefined;
×
638
  }
639
}
640

641
export function isSafariBrowser() {
642
  return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent);
73✔
643
}
644

645
export function replaceMacros(str, subs) {
646
  if (!str) return;
158✔
647
  return Object.entries(subs).reduce((str, [key, val]) => {
133✔
648
    return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || '');
133✔
649
  }, str);
650
}
651

652
export function replaceAuctionPrice(str, cpm) {
653
  return replaceMacros(str, {AUCTION_PRICE: cpm})
105✔
654
}
655

656
export function replaceClickThrough(str, clicktag) {
657
  if (!str || !clicktag || typeof clicktag !== 'string') return;
×
658
  return str.replace(/\${CLICKTHROUGH}/g, clicktag);
×
659
}
660

661
export function timestamp() {
662
  return new Date().getTime();
10,283✔
663
}
664

665
/**
666
 * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
667
 * @returns {number}
668
 */
669
export function getPerformanceNow() {
670
  return (window.performance && window.performance.now && window.performance.now()) || 0;
9,370✔
671
}
672

673
/**
674
 * Retuns the difference between `timing.domLoading` and `timing.navigationStart`.
675
 * This function uses the deprecated `Performance.timing` API and should be removed in future.
676
 * It has not been updated yet because it is still used in some modules.
677
 * @deprecated
678
 * @param {Window} w The window object used to perform the api call. default to window.self
679
 * @returns {number}
680
 */
681
export function getDomLoadingDuration(w) {
682
  let domLoadingDuration = -1;
17✔
683

684
  w = w || getWindowSelf();
17!
685

686
  const performance = w.performance;
17✔
687

688
  if (w.performance?.timing) {
17✔
689
    if (w.performance.timing.navigationStart > 0) {
17✔
690
      const val = performance.timing.domLoading - performance.timing.navigationStart;
14✔
691
      if (val > 0) {
14✔
692
        domLoadingDuration = val;
14✔
693
      }
694
    }
695
  }
696

697
  return domLoadingDuration;
17✔
698
}
699

700
/**
701
 * When the deviceAccess flag config option is false, no cookies should be read or set
702
 * @returns {boolean}
703
 */
704
export function hasDeviceAccess() {
705
  return config.getConfig('deviceAccess') !== false;
18,854✔
706
}
707

708
/**
709
 * @returns {(boolean|undefined)}
710
 */
711
export function checkCookieSupport() {
712
  // eslint-disable-next-line no-restricted-properties
713
  if (window.navigator.cookieEnabled || !!document.cookie.length) {
1,240!
714
    return true;
1,240✔
715
  }
716
}
717

718
/**
719
 * Given a function, return a function which only executes the original after
720
 * it's been called numRequiredCalls times.
721
 *
722
 * Note that the arguments from the previous calls will *not* be forwarded to the original function.
723
 * Only the final call's arguments matter.
724
 *
725
 * @param {function} func The function which should be executed, once the returned function has been executed
726
 *   numRequiredCalls times.
727
 * @param {number} numRequiredCalls The number of times which the returned function needs to be called before
728
 *   func is.
729
 */
730
export function delayExecution(func, numRequiredCalls) {
731
  if (numRequiredCalls < 1) {
351!
732
    throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`);
×
733
  }
734
  let numCalls = 0;
351✔
735
  return function () {
351✔
736
    numCalls++;
183✔
737
    if (numCalls === numRequiredCalls) {
183✔
738
      func.apply(this, arguments);
159✔
739
    }
740
  }
741
}
742

743
/**
744
 * https://stackoverflow.com/a/34890276/428704
745
 * @param {Array} xs
746
 * @param {string} key
747
 * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}}
748
 */
749
export function groupBy(xs, key) {
750
  return xs.reduce(function(rv, x) {
1,268✔
751
    (rv[x[key]] = rv[x[key]] || []).push(x);
1,470✔
752
    return rv;
1,470✔
753
  }, {});
754
}
755

756
/**
757
 * Validates an adunit's `mediaTypes` parameter
758
 * @param mediaTypes mediaTypes parameter to validate
759
 * @return If object is valid
760
 */
761
export function isValidMediaTypes(mediaTypes) {
762
  const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video', 'audio'];
1,045✔
763
  const SUPPORTED_STREAM_TYPES = ['instream', 'outstream', 'adpod'];
1,045✔
764

765
  const types = Object.keys(mediaTypes);
1,045✔
766

767
  if (!types.every(type => SUPPORTED_MEDIA_TYPES.includes(type))) {
1,047!
768
    return false;
×
769
  }
770

771
  if (FEATURES.VIDEO && mediaTypes.video && mediaTypes.video.context) {
1,045!
772
    return SUPPORTED_STREAM_TYPES.includes(mediaTypes.video.context);
×
773
  }
774

775
  return true;
1,045✔
776
}
777

778
/**
779
 * Returns user configured bidder params from adunit
780
 * @param {Object} adUnits
781
 * @param {string} adUnitCode code
782
 * @param {string} bidder code
783
 * @return {Array} user configured param for the given bidder adunit configuration
784
 */
785
export function getUserConfiguredParams(adUnits, adUnitCode, bidder) {
786
  return adUnits
51✔
787
    .filter(adUnit => adUnit.code === adUnitCode)
79✔
788
    .flatMap((adUnit) => adUnit.bids)
26✔
789
    .filter((bidderData) => bidderData.bidder === bidder)
172✔
790
    .map((bidderData) => bidderData.params || {});
25✔
791
}
792

793
export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode;
647✔
794

795
/**
796
 * Returns filter function to match adUnitCode in slot
797
 * @param slot GoogleTag slot
798
 * @return filter function
799
 */
800
export function isAdUnitCodeMatchingSlot(slot) {
801
  return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode);
222✔
802
}
803

804
/**
805
 * Constructs warning message for when unsupported bidders are dropped from an adunit
806
 * @param {Object} adUnit ad unit from which the bidder is being dropped
807
 * @param {string} bidder bidder code that is not compatible with the adUnit
808
 * @return {string} warning message to display when condition is met
809
 */
810
export function unsupportedBidderMessage(adUnit, bidder) {
811
  const mediaType = Object.keys(adUnit.mediaTypes || {'banner': 'banner'}).join(', ');
7!
812

813
  return `
7✔
814
    ${adUnit.code} is a ${mediaType} ad unit
815
    containing bidders that don't support ${mediaType}: ${bidder}.
816
    This bidder won't fetch demand.
817
  `;
818
}
819

820
/**
821
 * Returns a new object with undefined properties removed from given object
822
 * @param obj the object to clean
823
 */
824
export function cleanObj(obj) {
825
  return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined'))
318✔
826
}
827

828
/**
829
 * Create a new object with selected properties.  Also allows property renaming and transform functions.
830
 * @param obj the original object
831
 * @param properties An array of desired properties
832
 */
833
export function pick(obj, properties) {
834
  if (typeof obj !== 'object') {
1,972✔
835
    return {};
3✔
836
  }
837
  return properties.reduce((newObj, prop, i) => {
1,969✔
838
    if (typeof prop === 'function') {
22,654✔
839
      return newObj;
9,417✔
840
    }
841

842
    let newProp = prop;
13,237✔
843
    const match = prop.match(/^(.+?)\sas\s(.+?)$/i);
13,237✔
844

845
    if (match) {
13,237✔
846
      prop = match[1];
482✔
847
      newProp = match[2];
482✔
848
    }
849

850
    let value = obj[prop];
13,237✔
851
    if (typeof properties[i + 1] === 'function') {
13,237✔
852
      value = properties[i + 1](value, newObj);
9,417✔
853
    }
854
    if (typeof value !== 'undefined') {
13,237✔
855
      newObj[newProp] = value;
9,331✔
856
    }
857

858
    return newObj;
13,237✔
859
  }, {});
860
}
861

862
export function parseQS(query) {
863
  return !query ? {} : query
2,929✔
864
    .replace(/^\?/, '')
865
    .split('&')
866
    .reduce((acc, criteria) => {
867
      let [k, v] = criteria.split('=');
3,248✔
868
      if (/\[\]$/.test(k)) {
3,248!
869
        k = k.replace('[]', '');
×
870
        acc[k] = acc[k] || [];
×
871
        acc[k].push(v);
×
872
      } else {
873
        acc[k] = v || '';
3,248✔
874
      }
875
      return acc;
3,248✔
876
    }, {});
877
}
878

879
export function formatQS(query) {
880
  return Object
478✔
881
    .keys(query)
882
    .map(k => Array.isArray(query[k])
2,723✔
883
      ? query[k].map(v => `${k}[]=${v}`).join('&')
17✔
884
      : `${k}=${query[k]}`)
885
    .join('&');
886
}
887

888
export function parseUrl(url, options) {
889
  const parsed = document.createElement('a');
1,169✔
890
  if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) {
1,169✔
891
    parsed.href = url;
127✔
892
  } else {
893
    parsed.href = decodeURIComponent(url);
1,042✔
894
  }
895
  // in window.location 'search' is string, not object
896
  const qsAsString = (options && 'decodeSearchAsString' in options && options.decodeSearchAsString);
1,169✔
897
  return {
1,169✔
898
    href: parsed.href,
899
    protocol: (parsed.protocol || '').replace(/:$/, ''),
1,169!
900
    hostname: parsed.hostname,
901
    port: +parsed.port,
902
    pathname: parsed.pathname.replace(/^(?!\/)/, '/'),
903
    search: (qsAsString) ? parsed.search : internal.parseQS(parsed.search || ''),
2,380✔
904
    hash: (parsed.hash || '').replace(/^#/, ''),
2,329✔
905
    host: parsed.host || window.location.host
1,169!
906
  };
907
}
908

909
export function buildUrl(obj) {
910
  return (obj.protocol || 'http') + '://' +
403✔
911
    (obj.host ||
505✔
912
      obj.hostname + (obj.port ? `:${obj.port}` : '')) +
102✔
913
    (obj.pathname || '') +
404✔
914
    (obj.search ? `?${internal.formatQS(obj.search || '')}` : '') +
651!
915
    (obj.hash ? `#${obj.hash}` : '');
403✔
916
}
917

918
/**
919
 * This function deeply compares two objects checking for their equivalence.
920
 * @param {Object} obj1
921
 * @param {Object} obj2
922
 * @param {Object} [options] - Options for comparison.
923
 * @param {boolean} [options.checkTypes=false] - If set, two objects with identical properties but different constructors will *not* be considered equivalent.
924
 * @returns {boolean} - Returns `true` if the objects are equivalent, `false` otherwise.
925
 */
926
export function deepEqual(obj1, obj2, { checkTypes = false } = {}) {
1,507✔
927
  // Quick reference check
928
  if (obj1 === obj2) return true;
1,507✔
929

930
  // If either is null or not an object, do a direct equality check
931
  if (
1,031✔
932
    typeof obj1 !== 'object' || obj1 === null ||
2,625✔
933
    typeof obj2 !== 'object' || obj2 === null
934
  ) {
935
    return false;
502✔
936
  }
937
  // Cache the Array checks
938
  const isArr1 = Array.isArray(obj1);
529✔
939
  const isArr2 = Array.isArray(obj2);
529✔
940
  // Special case: both are arrays
941
  if (isArr1 && isArr2) {
529✔
942
    if (obj1.length !== obj2.length) return false;
98✔
943
    for (let i = 0; i < obj1.length; i++) {
91✔
944
      if (!deepEqual(obj1[i], obj2[i], { checkTypes })) {
114✔
945
        return false;
7✔
946
      }
947
    }
948
    return true;
84✔
949
  } else if (isArr1 || isArr2) {
431!
950
    return false;
×
951
  }
952

953
  // If we’re checking types, compare constructors (e.g., plain object vs. Date)
954
  if (checkTypes && obj1.constructor !== obj2.constructor) {
431✔
955
    return false;
1✔
956
  }
957

958
  // Compare object keys. Cache keys for both to avoid repeated calls.
959
  const keys1 = Object.keys(obj1);
430✔
960
  const keys2 = Object.keys(obj2);
430✔
961

962
  if (keys1.length !== keys2.length) return false;
430✔
963

964
  for (const key of keys1) {
421✔
965
    // If `obj2` doesn't have this key or sub-values aren't equal, bail out.
966
    if (!Object.prototype.hasOwnProperty.call(obj2, key)) {
463✔
967
      return false;
1✔
968
    }
969
    if (!deepEqual(obj1[key], obj2[key], { checkTypes })) {
462✔
970
      return false;
301✔
971
    }
972
  }
973

974
  return true;
119✔
975
}
976

977
export function mergeDeep(target, ...sources) {
978
  for (let i = 0; i < sources.length; i++) {
16,363✔
979
    const source = sources[i];
24,773!
980
    if (!isPlainObject(source)) {
24,773✔
981
      continue;
8,580✔
982
    }
983
    mergeDeepHelper(target, source);
16,193✔
984
  }
985
  return target;
16,363✔
986
}
987

988
function mergeDeepHelper(target, source) {
989
  // quick check
990
  if (!isPlainObject(target) || !isPlainObject(source)) {
34,184✔
991
    return;
36✔
992
  }
993

994
  const keys = Object.keys(source);
34,148✔
995
  for (let i = 0; i < keys.length; i++) {
34,148✔
996
    const key = keys[i];
52,130✔
997
    if (key === '__proto__' || key === 'constructor') {
52,130!
998
      continue;
×
999
    }
1000
    const val = source[key];
52,130✔
1001

1002
    if (isPlainObject(val)) {
52,130✔
1003
      if (!target[key]) {
17,991✔
1004
        target[key] = {};
16,445✔
1005
      }
1006
      mergeDeepHelper(target[key], val);
17,991✔
1007
    } else if (Array.isArray(val)) {
34,139✔
1008
      if (!Array.isArray(target[key])) {
4,671✔
1009
        target[key] = [...val];
2,970✔
1010
      } else {
1011
        // deduplicate
1012
        val.forEach(obj => {
1,701✔
1013
          if (!target[key].some(item => deepEqual(item, obj))) {
729✔
1014
            target[key].push(obj);
92✔
1015
          }
1016
        });
1017
      }
1018
    } else {
1019
      // direct assignment
1020
      target[key] = val;
29,468✔
1021
    }
1022
  }
1023
}
1024

1025
/**
1026
 * returns a hash of a string using a fast algorithm
1027
 * source: https://stackoverflow.com/a/52171480/845390
1028
 * @param str
1029
 * @param seed (optional)
1030
 * @returns {string}
1031
 */
1032
export function cyrb53Hash(str, seed = 0) {
162✔
1033
  // IE doesn't support imul
1034
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill
1035
  const imul = function(opA, opB) {
162✔
1036
    if (isFn(Math.imul)) {
12,384!
1037
      return Math.imul(opA, opB);
12,384✔
1038
    } else {
1039
      opB |= 0; // ensure that opB is an integer. opA will automatically be coerced.
×
1040
      // floating points give us 53 bits of precision to work with plus 1 sign bit
1041
      // automatically handled for our convienence:
1042
      // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001
1043
      //    0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
1044
      var result = (opA & 0x003fffff) * opB;
×
1045
      // 2. We can remove an integer coersion from the statement above because:
1046
      //    0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001
1047
      //    0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
1048
      if (opA & 0xffc00000) result += (opA & 0xffc00000) * opB | 0;
×
1049
      return result | 0;
×
1050
    }
1051
  };
1052

1053
  let h1 = 0xdeadbeef ^ seed;
162✔
1054
  let h2 = 0x41c6ce57 ^ seed;
162✔
1055
  for (let i = 0, ch; i < str.length; i++) {
162✔
1056
    ch = str.charCodeAt(i);
5,868✔
1057
    h1 = imul(h1 ^ ch, 2654435761);
5,868✔
1058
    h2 = imul(h2 ^ ch, 1597334677);
5,868✔
1059
  }
1060
  h1 = imul(h1 ^ (h1 >>> 16), 2246822507) ^ imul(h2 ^ (h2 >>> 13), 3266489909);
162✔
1061
  h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909);
162✔
1062
  return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString();
162✔
1063
}
1064

1065
/**
1066
 * returns the result of `JSON.parse(data)`, or undefined if that throws an error.
1067
 * @param data
1068
 * @returns {any}
1069
 */
1070
export function safeJSONParse(data) {
1071
  try {
68✔
1072
    return JSON.parse(data);
68✔
1073
  } catch (e) {}
1074
}
1075

1076
export function safeJSONEncode(data) {
1077
  try {
542✔
1078
    return JSON.stringify(data);
542✔
1079
  } catch (e) {
1080
    return '';
1✔
1081
  }
1082
}
1083

1084
/**
1085
 * Returns a memoized version of `fn`.
1086
 *
1087
 * @param fn
1088
 * @param key cache key generator, invoked with the same arguments passed to `fn`.
1089
 *        By default, the first argument is used as key.
1090
 * @return {*}
1091
 */
1092
export function memoize(fn, key = function (arg) { return arg; }) {
2,470✔
1093
  const cache = new Map();
279✔
1094
  const memoized = function () {
279✔
1095
    const cacheKey = key.apply(this, arguments);
3,140✔
1096
    if (!cache.has(cacheKey)) {
3,140✔
1097
      cache.set(cacheKey, fn.apply(this, arguments));
760✔
1098
    }
1099
    return cache.get(cacheKey);
3,140✔
1100
  }
1101
  memoized.clear = cache.clear.bind(cache);
279✔
1102
  return memoized;
279✔
1103
}
1104

1105
/**
1106
 * Returns a Unix timestamp for given time value and unit.
1107
 * @param {number} timeValue numeric value, defaults to 0 (which means now)
1108
 * @param {string} timeUnit defaults to days (or 'd'), use 'm' for minutes. Any parameter that isn't 'd' or 'm' will return Date.now().
1109
 * @returns {number}
1110
 */
1111
export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') {
723✔
1112
  const acceptableUnits = ['m', 'd'];
723✔
1113
  if (acceptableUnits.indexOf(timeUnit) < 0) {
723✔
1114
    return Date.now();
1✔
1115
  }
1116
  const multiplication = timeValue / (timeUnit === 'm' ? 1440 : 1);
722✔
1117
  return Date.now() + (timeValue && timeValue > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0);
722✔
1118
}
1119

1120
/**
1121
 * Converts given object into an array, so {key: 1, anotherKey: 'fred', third: ['fred']} is turned
1122
 * into [{key: 1}, {anotherKey: 'fred'}, {third: ['fred']}]
1123
 * @param {Object} obj the object
1124
 * @returns {Array}
1125
 */
1126
export function convertObjectToArray(obj) {
1127
  return Object.keys(obj).map(key => {
10✔
1128
    return {[key]: obj[key]};
28✔
1129
  });
1130
}
1131

1132
/**
1133
 * Sets dataset attributes on a script
1134
 * @param {HTMLScriptElement} script
1135
 * @param {object} attributes
1136
 */
1137
export function setScriptAttributes(script, attributes) {
1138
  Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v))
5✔
1139
}
1140

1141
/**
1142
 * Perform a binary search for `el` on an ordered array `arr`.
1143
 *
1144
 * @returns the lowest nonnegative integer I that satisfies:
1145
 *   key(arr[i]) >= key(el) for each i between I and arr.length
1146
 *
1147
 *   (if one or more matches are found for `el`, returns the index of the first;
1148
 *   if the element is not found, return the index of the first element that's greater;
1149
 *   if no greater element exists, return `arr.length`)
1150
 */
1151
export function binarySearch(arr, el, key = (el) => el) {
55✔
1152
  let left = 0;
55✔
1153
  let right = arr.length && arr.length - 1;
55✔
1154
  const target = key(el);
55✔
1155
  while (right - left > 1) {
55✔
1156
    const middle = left + Math.round((right - left) / 2);
14✔
1157
    if (target > key(arr[middle])) {
14✔
1158
      left = middle;
7✔
1159
    } else {
1160
      right = middle;
7✔
1161
    }
1162
  }
1163
  while (arr.length > left && target > key(arr[left])) {
55✔
1164
    left++;
16✔
1165
  }
1166
  return left;
55✔
1167
}
1168

1169
/**
1170
 * Checks if an object has non-serializable properties.
1171
 * Non-serializable properties are functions and RegExp objects.
1172
 *
1173
 * @param {Object} obj - The object to check.
1174
 * @param {Set} checkedObjects - A set of properties that have already been checked.
1175
 * @returns {boolean} - Returns true if the object has non-serializable properties, false otherwise.
1176
 */
1177
export function hasNonSerializableProperty(obj, checkedObjects = new Set()) {
109✔
1178
  for (const key in obj) {
109✔
1179
    const value = obj[key];
349✔
1180
    const type = typeof value;
349✔
1181

1182
    if (
349✔
1183
      value === undefined ||
3,125✔
1184
      type === 'function' ||
1185
      type === 'symbol' ||
1186
      value instanceof RegExp ||
1187
      value instanceof Map ||
1188
      value instanceof Set ||
1189
      value instanceof Date ||
1190
      (value !== null && type === 'object' && value.hasOwnProperty('toJSON'))
1191
    ) {
1192
      return true;
11✔
1193
    }
1194
    if (value !== null && type === 'object' && value.constructor === Object) {
338✔
1195
      if (checkedObjects.has(value)) {
12!
1196
        // circular reference, means we have a non-serializable property
1197
        return true;
×
1198
      }
1199
      checkedObjects.add(value);
12✔
1200
      if (hasNonSerializableProperty(value, checkedObjects)) {
12✔
1201
        return true;
8✔
1202
      }
1203
    }
1204
  }
1205
  return false;
90✔
1206
}
1207

1208
/**
1209
 * Returns the value of a nested property in an array of objects.
1210
 *
1211
 * @param {Array} collection - Array of objects.
1212
 * @param {String} key - Key of nested property.
1213
 * @returns {any|undefined} - Value of nested property.
1214
 */
1215
export function setOnAny(collection, key) {
1216
  for (let i = 0, result; i < collection.length; i++) {
651✔
1217
    result = deepAccess(collection[i], key);
812✔
1218
    if (result) {
812✔
1219
      return result;
90✔
1220
    }
1221
  }
1222
  return undefined;
561✔
1223
}
1224

1225
export function extractDomainFromHost(pageHost) {
1226
  let domain = null;
19✔
1227
  try {
19✔
1228
    const domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost);
19✔
1229
    if (domains != null && domains.length > 0) {
19✔
1230
      domain = domains[0];
19✔
1231
      for (let i = 1; i < domains.length; i++) {
19✔
1232
        if (domains[i].length > domain.length) {
19!
1233
          domain = domains[i];
×
1234
        }
1235
      }
1236
    }
1237
  } catch (e) {
1238
    domain = null;
×
1239
  }
1240
  return domain;
19✔
1241
}
1242

1243
export function triggerNurlWithCpm(bid, cpm) {
1244
  if (isStr(bid.nurl) && bid.nurl !== '') {
2✔
1245
    bid.nurl = bid.nurl.replace(
2✔
1246
      /\${AUCTION_PRICE}/,
1247
      cpm
1248
    );
1249
    triggerPixel(bid.nurl);
2✔
1250
  }
1251
}
1252

1253
// To ensure that isGzipCompressionSupported() doesn’t become an overhead, we have used memoization to cache the result after the first execution.
1254
// This way, even if the function is called multiple times, it will only perform the actual check once and return the cached result in subsequent calls.
1255
export const isGzipCompressionSupported = (function () {
8✔
1256
  let cachedResult; // Store the result
1257

1258
  return function () {
8✔
1259
    if (cachedResult !== undefined) {
3✔
1260
      return cachedResult; // Return cached result if already computed
2✔
1261
    }
1262

1263
    try {
1✔
1264
      if (typeof window.CompressionStream === 'undefined') {
1!
1265
        cachedResult = false;
×
1266
      } else {
1267
        (() => new window.CompressionStream('gzip'))();
1✔
1268
        cachedResult = true;
1✔
1269
      }
1270
    } catch (error) {
1271
      cachedResult = false;
×
1272
    }
1273

1274
    return cachedResult;
1✔
1275
  };
1276
})();
1277

1278
// Make sure to use isGzipCompressionSupported before calling this function
1279
export async function compressDataWithGZip(data) {
1280
  if (typeof data !== 'string') { // TextEncoder (below) expects a string
3✔
1281
    data = JSON.stringify(data);
1✔
1282
  }
1283

1284
  const encoder = new TextEncoder();
3✔
1285
  const encodedData = encoder.encode(data);
3✔
1286
  const compressedStream = new Blob([encodedData])
3✔
1287
    .stream()
1288
    .pipeThrough(new window.CompressionStream('gzip'));
1289

1290
  const compressedBlob = await new Response(compressedStream).blob();
3✔
1291
  const compressedArrayBuffer = await compressedBlob.arrayBuffer();
3✔
1292
  return new Uint8Array(compressedArrayBuffer);
3✔
1293
}
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