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

prebid / Prebid.js / 16734543791

04 Aug 2025 09:22PM UTC coverage: 96.262% (+33.1%) from 63.163%
16734543791

Pull #13726

github

f6fa0b
web-flow
Merge branch 'master' into dependabot/npm_and_yarn/videojs-playlist-5.2.0
Pull Request #13726: Bump videojs-playlist from 5.1.2 to 5.2.0

39538 of 48585 branches covered (81.38%)

195470 of 203060 relevant lines covered (96.26%)

83.45 hits per line

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

89.07
/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

12
const consoleExists = Boolean(window.console);
4✔
13
const consoleLogExists = Boolean(consoleExists && window.console.log);
4✔
14
const consoleInfoExists = Boolean(consoleExists && window.console.info);
4✔
15
const consoleWarnExists = Boolean(consoleExists && window.console.warn);
4✔
16
const consoleErrorExists = Boolean(consoleExists && window.console.error);
4✔
17

18
let eventEmitter;
19
let windowDimensions;
20

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

26
function emitEvent(...args) {
27
  if (eventEmitter != null) {
6,563✔
28
    eventEmitter(...args);
6,563✔
29
  }
30
}
31

32
export const getWinDimensions = (function() {
4✔
33
  let lastCheckTimestamp;
34
  const CHECK_INTERVAL_MS = 20;
4✔
35
  return () => {
4✔
36
    if (!windowDimensions || !lastCheckTimestamp || (Date.now() - lastCheckTimestamp > CHECK_INTERVAL_MS)) {
3,222✔
37
      internal.resetWinDimensions();
278✔
38
      lastCheckTimestamp = Date.now();
278✔
39
    }
40
    return windowDimensions;
3,222✔
41
  }
42
})();
43

44
export function resetWinDimensions() {
45
  const top = canAccessWindowTop() ? internal.getWindowTop() : internal.getWindowSelf();
460✔
46

47
  windowDimensions = {
460✔
48
    screen: {
49
      width: top.screen?.width,
50
      height: top.screen?.height,
51
      availWidth: top.screen?.availWidth,
52
      availHeight: top.screen?.availHeight,
53
      colorDepth: top.screen?.colorDepth,
54
    },
55
    innerHeight: top.innerHeight,
56
    innerWidth: top.innerWidth,
57
    outerWidth: top.outerWidth,
58
    outerHeight: top.outerHeight,
59
    visualViewport: {
60
      height: top.visualViewport?.height,
61
      width: top.visualViewport?.width,
62
    },
63
    document: {
64
      documentElement: {
65
        clientWidth: top.document?.documentElement?.clientWidth,
66
        clientHeight: top.document?.documentElement?.clientHeight,
67
        scrollTop: top.document?.documentElement?.scrollTop,
68
        scrollLeft: top.document?.documentElement?.scrollLeft,
69
      },
70
      body: {
71
        scrollTop: document.body?.scrollTop,
72
        scrollLeft: document.body?.scrollLeft,
73
        clientWidth: document.body?.clientWidth,
74
        clientHeight: document.body?.clientHeight,
75
      },
76
    }
77
  };
78
}
79

80
// this allows stubbing of utility functions that are used internally by other utility functions
81
export const internal = {
4✔
82
  checkCookieSupport,
83
  createTrackPixelIframeHtml,
84
  getWindowSelf,
85
  getWindowTop,
86
  canAccessWindowTop,
87
  getWindowLocation,
88
  insertUserSyncIframe,
89
  insertElement,
90
  isFn,
91
  triggerPixel,
92
  logError,
93
  logWarn,
94
  logMessage,
95
  logInfo,
96
  parseQS,
97
  formatQS,
98
  deepEqual,
99
  resetWinDimensions
100
};
101

102
const prebidInternal = {};
4✔
103
/**
104
 * Returns object that is used as internal prebid namespace
105
 */
106
export function getPrebidInternal() {
107
  return prebidInternal;
×
108
}
109

110
/* utility method to get incremental integer starting from 1 */
111
var getIncrementalInteger = (function () {
4✔
112
  var count = 0;
4✔
113
  return function () {
4✔
114
    count++;
2,703✔
115
    return count;
2,703✔
116
  };
117
})();
118

119
// generate a random string (to be used as a dynamic JSONP callback)
120
export function getUniqueIdentifierStr() {
121
  return getIncrementalInteger() + Math.random().toString(16).substr(2);
2,703✔
122
}
123

124
/**
125
 * Returns a random v4 UUID of the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,
126
 * where each x is replaced with a random hexadecimal digit from 0 to f,
127
 * and y is replaced with a random hexadecimal digit from 8 to b.
128
 * https://gist.github.com/jed/982883 via node-uuid
129
 */
130
export function generateUUID(placeholder) {
131
  return placeholder
101,288✔
132
    ? (placeholder ^ _getRandomData() >> placeholder / 4).toString(16)
133
    : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID);
134
}
135

136
/**
137
 * Returns random data using the Crypto API if available and Math.random if not
138
 * Method is from https://gist.github.com/jed/982883 like generateUUID, direct link https://gist.github.com/jed/982883#gistcomment-45104
139
 */
140
function _getRandomData() {
141
  if (window && window.crypto && window.crypto.getRandomValues) {
98,123!
142
    return crypto.getRandomValues(new Uint8Array(1))[0] % 16;
98,123✔
143
  } else {
144
    return Math.random() * 16;
×
145
  }
146
}
147

148
export function getBidIdParameter(key, paramsObj) {
149
  return paramsObj?.[key] || '';
3,490✔
150
}
151

152
// parse a query string object passed in bid params
153
// bid params should be an object such as {key: "value", key1 : "value1"}
154
// aliases to formatQS
155
export function parseQueryStringParameters(queryObj) {
156
  let result = '';
78✔
157
  for (var k in queryObj) {
78✔
158
    if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; }
975✔
159
  }
160
  result = result.replace(/&$/, '');
78✔
161
  return result;
78✔
162
}
163

164
// transform an AdServer targeting bids into a query string to send to the adserver
165
export function transformAdServerTargetingObj(targeting) {
166
  // we expect to receive targeting for a single slot at a time
167
  if (targeting && Object.getOwnPropertyNames(targeting).length > 0) {
3✔
168
    return Object.keys(targeting)
2✔
169
      .map(key => `${key}=${encodeURIComponent(targeting[key])}`).join('&');
66✔
170
  } else {
171
    return '';
1✔
172
  }
173
}
174

175
/**
176
 * 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]]'
177
 */
178
export function sizesToSizeTuples(sizes) {
179
  if (typeof sizes === 'string') {
3,579✔
180
    // multiple sizes will be comma-separated
181
    return sizes
29✔
182
      .split(/\s*,\s*/)
183
      .map(sz => sz.match(/^(\d+)x(\d+)$/i))
32✔
184
      .filter(match => match)
32✔
185
      .map(([_, w, h]) => [parseInt(w, 10), parseInt(h, 10)])
26✔
186
  } else if (Array.isArray(sizes)) {
3,550✔
187
    if (isValidGPTSingleSize(sizes)) {
3,364✔
188
      return [sizes]
443✔
189
    }
190
    return sizes.filter(isValidGPTSingleSize);
2,921✔
191
  }
192
  return [];
186✔
193
}
194

195
/**
196
 * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']'
197
 * @param  {(Array.<number[]>|Array.<number>)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]]
198
 * @return {Array.<string>}  Array of strings like `["300x250"]` or `["300x250", "728x90"]`
199
 */
200
export function parseSizesInput(sizeObj) {
201
  return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString);
1,889✔
202
}
203

204
export function sizeTupleToSizeString(size) {
205
  return size[0] + 'x' + size[1]
3,283✔
206
}
207

208
// Parse a GPT style single size array, (i.e [300, 250])
209
// into an AppNexus style string, (i.e. 300x250)
210
export function parseGPTSingleSizeArray(singleSize) {
211
  if (isValidGPTSingleSize(singleSize)) {
32✔
212
    return sizeTupleToSizeString(singleSize);
22✔
213
  }
214
}
215

216
export function sizeTupleToRtbSize(size) {
217
  return {w: size[0], h: size[1]};
2,659✔
218
}
219

220
// Parse a GPT style single size array, (i.e [300, 250])
221
// into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250})
222
export function parseGPTSingleSizeArrayToRtbSize(singleSize) {
223
  if (isValidGPTSingleSize(singleSize)) {
306✔
224
    return sizeTupleToRtbSize(singleSize)
300✔
225
  }
226
}
227

228
function isValidGPTSingleSize(singleSize) {
229
  // if we aren't exactly 2 items in this array, it is invalid
230
  return isArray(singleSize) && singleSize.length === 2 && (!isNaN(singleSize[0]) && !isNaN(singleSize[1]));
8,898✔
231
}
232

233
export function getWindowTop() {
234
  return window.top;
3,127✔
235
}
236

237
export function getWindowSelf() {
238
  return window.self;
1,857✔
239
}
240

241
export function getWindowLocation() {
242
  return window.location;
1,897✔
243
}
244

245
export function getDocument() {
246
  return document;
392✔
247
}
248

249
export function canAccessWindowTop() {
250
  try {
521✔
251
    if (internal.getWindowTop().location.href) {
521✔
252
      return true;
438✔
253
    }
254
  } catch (e) {
255
    return false;
83✔
256
  }
257
}
258

259
/**
260
 * Wrappers to console.(log | info | warn | error). Takes N arguments, the same as the native methods
261
 */
262
// eslint-disable-next-line no-restricted-syntax
263
export function logMessage() {
264
  if (debugTurnedOn() && consoleLogExists) {
16,395✔
265
    // eslint-disable-next-line no-console
266
    console.log.apply(console, decorateLog(arguments, 'MESSAGE:'));
14✔
267
  }
268
}
269

270
// eslint-disable-next-line no-restricted-syntax
271
export function logInfo() {
272
  if (debugTurnedOn() && consoleInfoExists) {
12,213✔
273
    // eslint-disable-next-line no-console
274
    console.info.apply(console, decorateLog(arguments, 'INFO:'));
6✔
275
  }
276
}
277

278
// eslint-disable-next-line no-restricted-syntax
279
export function logWarn() {
280
  if (debugTurnedOn() && consoleWarnExists) {
4,618✔
281
    // eslint-disable-next-line no-console
282
    console.warn.apply(console, decorateLog(arguments, 'WARNING:'));
7✔
283
  }
284
  emitEvent(EVENTS.AUCTION_DEBUG, { type: 'WARNING', arguments: arguments });
4,618✔
285
}
286

287
// eslint-disable-next-line no-restricted-syntax
288
export function logError() {
289
  if (debugTurnedOn() && consoleErrorExists) {
1,945✔
290
    // eslint-disable-next-line no-console
291
    console.error.apply(console, decorateLog(arguments, 'ERROR:'));
3✔
292
  }
293
  emitEvent(EVENTS.AUCTION_DEBUG, { type: 'ERROR', arguments: arguments });
1,945✔
294
}
295

296
export function prefixLog(prefix) {
297
  function decorate(fn) {
298
    return function (...args) {
2,316✔
299
      fn(prefix, ...args);
1,056✔
300
    }
301
  }
302
  return {
150✔
303
    logError: decorate(logError),
304
    logWarn: decorate(logWarn),
305
    logMessage: decorate(logMessage),
306
    logInfo: decorate(logInfo),
307
  }
308
}
309

310
function decorateLog(args, prefix) {
311
  args = [].slice.call(args);
30✔
312
  const bidder = config.getCurrentBidder();
30✔
313

314
  prefix && args.unshift(prefix);
30✔
315
  if (bidder) {
30!
316
    args.unshift(label('#aaa'));
×
317
  }
318
  args.unshift(label('#3b88c3'));
30✔
319
  args.unshift('%cPrebid' + (bidder ? `%c${bidder}` : ''));
30!
320
  return args;
30✔
321

322
  function label(color) {
323
    return `display: inline-block; color: #fff; background: ${color}; padding: 1px 4px; border-radius: 3px;`
30✔
324
  }
325
}
326

327
export function hasConsoleLogger() {
328
  return consoleLogExists;
×
329
}
330

331
export function debugTurnedOn() {
332
  return !!config.getConfig('debug');
36,062✔
333
}
334

335
export const createIframe = (() => {
4✔
336
  const DEFAULTS = {
4✔
337
    border: '0px',
338
    hspace: '0',
339
    vspace: '0',
340
    marginWidth: '0',
341
    marginHeight: '0',
342
    scrolling: 'no',
343
    frameBorder: '0',
344
    allowtransparency: 'true'
345
  }
346
  return (doc, attrs, style = {}) => {
9✔
347
    const f = doc.createElement('iframe');
7✔
348
    Object.assign(f, Object.assign({}, DEFAULTS, attrs));
7✔
349
    Object.assign(f.style, style);
6✔
350
    return f;
6✔
351
  }
352
})();
353

354
export function createInvisibleIframe() {
355
  return createIframe(document, {
6✔
356
    id: getUniqueIdentifierStr(),
357
    width: 0,
358
    height: 0,
359
    src: 'about:blank'
360
  }, {
361
    display: 'none',
362
    height: '0px',
363
    width: '0px',
364
    border: '0px'
365
  });
366
}
367

368
/*
369
 *   Check if a given parameter name exists in query string
370
 *   and if it does return the value
371
 */
372
export function getParameterByName(name) {
373
  return parseQS(getWindowLocation().search)[name] || '';
1,737✔
374
}
375

376
/**
377
 * Return if the object is "empty";
378
 * this includes falsey, no keys, or no items at indices
379
 * @param {*} object object to test
380
 * @return {Boolean} if object is empty
381
 */
382
export function isEmpty(object) {
383
  if (!object) return true;
52,925✔
384
  if (isArray(object) || isStr(object)) {
47,982✔
385
    return !(object.length > 0);
21,657✔
386
  }
387
  return Object.keys(object).length <= 0;
26,325✔
388
}
389

390
/**
391
 * Return if string is empty, null, or undefined
392
 * @param str string to test
393
 * @returns {boolean} if string is empty
394
 */
395
export function isEmptyStr(str) {
396
  return isStr(str) && (!str || str.length === 0);
525✔
397
}
398

399
/**
400
 * Iterate object with the function
401
 * falls back to es5 `forEach`
402
 * @param {Array|Object} object
403
 * @param {Function} fn - The function to execute for each element. It receives three arguments: value, key, and the original object.
404
 * @returns {void}
405
 */
406
export function _each(object, fn) {
407
  if (isFn(object?.forEach)) return object.forEach(fn, this);
7,210✔
408
  Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k));
35,774✔
409
}
410

411
export function contains(a, obj) {
412
  return isFn(a?.includes) && a.includes(obj);
38✔
413
}
414

415
/**
416
 * Map an array or object into another array
417
 * given a function
418
 * @param {Array|Object} object
419
 * @param {Function} callback - The function to execute for each element. It receives three arguments: value, key, and the original object.
420
 * @return {Array}
421
 */
422
export function _map(object, callback) {
423
  if (isFn(object?.map)) return object.map(callback);
431✔
424
  return Object.entries(object || {}).map(([k, v]) => callback(v, k, object))
6,485✔
425
}
426

427
/*
428
* Inserts an element(elm) as targets child, by default as first child
429
* @param {HTMLElement} elm
430
* @param {HTMLElement} [doc]
431
* @param {HTMLElement} [target]
432
* @param {Boolean} [asLastChildChild]
433
* @return {HTML Element}
434
*/
435
export function insertElement(elm, doc, target, asLastChildChild) {
436
  doc = doc || document;
18✔
437
  let parentEl;
438
  if (target) {
18✔
439
    parentEl = doc.getElementsByTagName(target);
14✔
440
  } else {
441
    parentEl = doc.getElementsByTagName('head');
4✔
442
  }
443
  try {
18✔
444
    parentEl = parentEl.length ? parentEl : doc.getElementsByTagName('body');
18!
445
    if (parentEl.length) {
18✔
446
      parentEl = parentEl[0];
18✔
447
      const insertBeforeEl = asLastChildChild ? null : parentEl.firstChild;
18✔
448
      return parentEl.insertBefore(elm, insertBeforeEl);
18✔
449
    }
450
  } catch (e) {}
451
}
452

453
/**
454
 * Returns a promise that completes when the given element triggers a 'load' or 'error' DOM event, or when
455
 * `timeout` milliseconds have elapsed.
456
 *
457
 * @param {HTMLElement} element
458
 * @param {Number} [timeout]
459
 * @returns {Promise}
460
 */
461
export function waitForElementToLoad(element, timeout) {
462
  let timer = null;
3✔
463
  return new PbPromise((resolve) => {
3✔
464
    const onLoad = function() {
3✔
465
      element.removeEventListener('load', onLoad);
3✔
466
      element.removeEventListener('error', onLoad);
3✔
467
      if (timer != null) {
3✔
468
        window.clearTimeout(timer);
1✔
469
      }
470
      resolve();
3✔
471
    };
472
    element.addEventListener('load', onLoad);
3✔
473
    element.addEventListener('error', onLoad);
3✔
474
    if (timeout != null) {
3✔
475
      timer = window.setTimeout(onLoad, timeout);
1✔
476
    }
477
  });
478
}
479

480
/**
481
 * Inserts an image pixel with the specified `url` for cookie sync
482
 * @param {string} url URL string of the image pixel to load
483
 * @param  {function} [done] an optional exit callback, used when this usersync pixel is added during an async process
484
 * @param  {Number} [timeout] an optional timeout in milliseconds for the image to load before calling `done`
485
 */
486
export function triggerPixel(url, done, timeout) {
487
  const img = new Image();
65✔
488
  if (done && internal.isFn(done)) {
65!
489
    waitForElementToLoad(img, timeout).then(done);
×
490
  }
491
  img.src = url;
65✔
492
}
493

494
/**
495
 * Inserts an empty iframe with the specified `html`, primarily used for tracking purposes
496
 * (though could be for other purposes)
497
 * @param {string} htmlCode snippet of HTML code used for tracking purposes
498
 */
499
export function insertHtmlIntoIframe(htmlCode) {
500
  if (!htmlCode) {
×
501
    return;
×
502
  }
503
  const iframe = createInvisibleIframe();
×
504
  internal.insertElement(iframe, document, 'body');
×
505

506
  ((doc) => {
×
507
    doc.open();
×
508
    doc.write(htmlCode);
×
509
    doc.close();
×
510
  })(iframe.contentWindow.document);
511
}
512

513
/**
514
 * Inserts empty iframe with the specified `url` for cookie sync
515
 * @param  {string} url URL to be requested
516
 * @param  {function} [done] an optional exit callback, used when this usersync pixel is added during an async process
517
 * @param  {Number} [timeout] an optional timeout in milliseconds for the iframe to load before calling `done`
518
 */
519
export function insertUserSyncIframe(url, done, timeout) {
520
  const iframeHtml = internal.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin');
1✔
521
  const div = document.createElement('div');
1✔
522
  div.innerHTML = iframeHtml;
1✔
523
  const iframe = div.firstChild;
1✔
524
  if (done && internal.isFn(done)) {
1!
525
    waitForElementToLoad(iframe, timeout).then(done);
×
526
  }
527
  internal.insertElement(iframe, document, 'html', true);
1✔
528
}
529

530
/**
531
 * Creates a snippet of HTML that retrieves the specified `url`
532
 * @param  {string} url URL to be requested
533
 * @param encode
534
 * @return {string}     HTML snippet that contains the img src = set to `url`
535
 */
536
export function createTrackPixelHtml(url, encode = encodeURI) {
97✔
537
  if (!url) {
97!
538
    return '';
×
539
  }
540

541
  const escapedUrl = encode(url);
97✔
542
  let img = '<div style="position:absolute;left:0px;top:0px;visibility:hidden;">';
97✔
543
  img += '<img src="' + escapedUrl + '"></div>';
97✔
544
  return img;
97✔
545
};
546

547
/**
548
 * encodeURI, but preserves macros of the form '${MACRO}' (e.g. '${AUCTION_PRICE}')
549
 * @param url
550
 * @return {string}
551
 */
552
export function encodeMacroURI(url) {
553
  const macros = Array.from(url.matchAll(/\$({[^}]+})/g)).map(match => match[1]);
48✔
554
  return macros.reduce((str, macro) => {
39✔
555
    return str.replace('$' + encodeURIComponent(macro), '$' + macro)
25✔
556
  }, encodeURI(url))
557
}
558

559
/**
560
 * Creates a snippet of Iframe HTML that retrieves the specified `url`
561
 * @param  {string} url plain URL to be requested
562
 * @param  {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true
563
 * @param  {string} sandbox string if provided the sandbox attribute will be included with the given value
564
 * @return {string}     HTML snippet that contains the iframe src = set to `url`
565
 */
566
export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') {
1!
567
  if (!url) {
1!
568
    return '';
×
569
  }
570
  if (encodeUri) {
1!
571
    url = encodeURI(url);
×
572
  }
573
  if (sandbox) {
1✔
574
    sandbox = `sandbox="${sandbox}"`;
1✔
575
  }
576

577
  return `<iframe ${sandbox} id="${getUniqueIdentifierStr()}"
1✔
578
      frameborder="0"
579
      allowtransparency="true"
580
      marginheight="0" marginwidth="0"
581
      width="0" hspace="0" vspace="0" height="0"
582
      style="height:0px;width:0px;display:none;"
583
      scrolling="no"
584
      src="${url}">
585
    </iframe>`;
586
}
587

588
export function uniques(value, index, arry) {
589
  return arry.indexOf(value) === index;
18,269✔
590
}
591

592
export function flatten(a, b) {
593
  return a.concat(b);
3,303✔
594
}
595

596
export function getBidRequest(id, bidderRequests) {
597
  if (!id) {
248✔
598
    return;
4✔
599
  }
600
  return bidderRequests.flatMap(br => br.bids)
329✔
601
    .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id))
361✔
602
}
603

604
export function getValue(obj, key) {
605
  return obj[key];
101✔
606
}
607

608
export function getBidderCodes(adUnits) {
609
  // this could memoize adUnits
610
  return adUnits.map(unit => unit.bids.map(bid => bid.bidder)
1,105✔
611
    .reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques);
1,105✔
612
}
613

614
export function isGptPubadsDefined() {
615
  if (window.googletag && isFn(window.googletag.pubads) && isFn(window.googletag.pubads().getSlots)) {
814✔
616
    return true;
793✔
617
  }
618
}
619

620
export function isApnGetTagDefined() {
621
  if (window.apntag && isFn(window.apntag.getTag)) {
10✔
622
    return true;
8✔
623
  }
624
}
625

626
export const sortByHighestCpm = (a, b) => {
4✔
627
  return b.cpm - a.cpm;
659✔
628
}
629

630
/**
631
 * Fisher–Yates shuffle
632
 * http://stackoverflow.com/a/6274398
633
 * https://bost.ocks.org/mike/shuffle/
634
 * istanbul ignore next
635
 */
636
export function shuffle(array) {
637
  let counter = array.length;
123✔
638

639
  // while there are elements in the array
640
  while (counter > 0) {
123✔
641
    // pick a random index
642
    const index = Math.floor(Math.random() * counter);
745✔
643

644
    // decrease counter by 1
645
    counter--;
745✔
646

647
    // and swap the last element with it
648
    const temp = array[counter];
745✔
649
    array[counter] = array[index];
745✔
650
    array[index] = temp;
745✔
651
  }
652

653
  return array;
123✔
654
}
655

656
export function inIframe() {
657
  try {
1,515✔
658
    return internal.getWindowSelf() !== internal.getWindowTop();
1,515✔
659
  } catch (e) {
660
    return true;
×
661
  }
662
}
663

664
/**
665
 * https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf
666
 */
667
export function isSafeFrameWindow() {
668
  if (!inIframe()) {
72!
669
    return false;
×
670
  }
671

672
  const ws = internal.getWindowSelf();
72✔
673
  return !!(ws.$sf && ws.$sf.ext);
72✔
674
}
675

676
/**
677
 * Returns the result of calling the function $sf.ext.geom() if it exists
678
 * @see https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf — 5.4 Function $sf.ext.geom
679
 * @returns {Object | undefined} geometric information about the container
680
 */
681
export function getSafeframeGeometry() {
682
  try {
4✔
683
    const ws = getWindowSelf();
4✔
684
    return (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : undefined;
4✔
685
  } catch (e) {
686
    logError('Error getting SafeFrame geometry', e);
×
687
    return undefined;
×
688
  }
689
}
690

691
export function isSafariBrowser() {
692
  return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent);
73✔
693
}
694

695
export function replaceMacros(str, subs) {
696
  if (!str) return;
157✔
697
  return Object.entries(subs).reduce((str, [key, val]) => {
131✔
698
    return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || '');
131✔
699
  }, str);
700
}
701

702
export function replaceAuctionPrice(str, cpm) {
703
  return replaceMacros(str, {AUCTION_PRICE: cpm})
105✔
704
}
705

706
export function replaceClickThrough(str, clicktag) {
707
  if (!str || !clicktag || typeof clicktag !== 'string') return;
×
708
  return str.replace(/\${CLICKTHROUGH}/g, clicktag);
×
709
}
710

711
export function timestamp() {
712
  return new Date().getTime();
11,679✔
713
}
714

715
/**
716
 * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
717
 * @returns {number}
718
 */
719
export function getPerformanceNow() {
720
  return (window.performance && window.performance.now && window.performance.now()) || 0;
10,727✔
721
}
722

723
/**
724
 * Retuns the difference between `timing.domLoading` and `timing.navigationStart`.
725
 * This function uses the deprecated `Performance.timing` API and should be removed in future.
726
 * It has not been updated yet because it is still used in some modules.
727
 * @deprecated
728
 * @param {Window} w The window object used to perform the api call. default to window.self
729
 * @returns {number}
730
 */
731
export function getDomLoadingDuration(w) {
732
  let domLoadingDuration = -1;
17✔
733

734
  w = w || getWindowSelf();
17!
735

736
  const performance = w.performance;
17✔
737

738
  if (w.performance?.timing) {
17✔
739
    if (w.performance.timing.navigationStart > 0) {
17✔
740
      const val = performance.timing.domLoading - performance.timing.navigationStart;
14✔
741
      if (val > 0) {
14✔
742
        domLoadingDuration = val;
14✔
743
      }
744
    }
745
  }
746

747
  return domLoadingDuration;
17✔
748
}
749

750
/**
751
 * When the deviceAccess flag config option is false, no cookies should be read or set
752
 * @returns {boolean}
753
 */
754
export function hasDeviceAccess() {
755
  return config.getConfig('deviceAccess') !== false;
18,427✔
756
}
757

758
/**
759
 * @returns {(boolean|undefined)}
760
 */
761
export function checkCookieSupport() {
762
  // eslint-disable-next-line no-restricted-properties
763
  if (window.navigator.cookieEnabled || !!document.cookie.length) {
1,189!
764
    return true;
1,189✔
765
  }
766
}
767

768
/**
769
 * Given a function, return a function which only executes the original after
770
 * it's been called numRequiredCalls times.
771
 *
772
 * Note that the arguments from the previous calls will *not* be forwarded to the original function.
773
 * Only the final call's arguments matter.
774
 *
775
 * @param {function} func The function which should be executed, once the returned function has been executed
776
 *   numRequiredCalls times.
777
 * @param {number} numRequiredCalls The number of times which the returned function needs to be called before
778
 *   func is.
779
 */
780
export function delayExecution(func, numRequiredCalls) {
781
  if (numRequiredCalls < 1) {
385!
782
    throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`);
×
783
  }
784
  let numCalls = 0;
385✔
785
  return function () {
385✔
786
    numCalls++;
183✔
787
    if (numCalls === numRequiredCalls) {
183✔
788
      func.apply(this, arguments);
159✔
789
    }
790
  }
791
}
792

793
/**
794
 * https://stackoverflow.com/a/34890276/428704
795
 * @param {Array} xs
796
 * @param {string} key
797
 * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}}
798
 */
799
export function groupBy(xs, key) {
800
  return xs.reduce(function(rv, x) {
1,124✔
801
    (rv[x[key]] = rv[x[key]] || []).push(x);
1,714✔
802
    return rv;
1,714✔
803
  }, {});
804
}
805

806
/**
807
 * Validates an adunit's `mediaTypes` parameter
808
 * @param mediaTypes mediaTypes parameter to validate
809
 * @return If object is valid
810
 */
811
export function isValidMediaTypes(mediaTypes) {
812
  const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video', 'audio'];
1,006✔
813
  const SUPPORTED_STREAM_TYPES = ['instream', 'outstream', 'adpod'];
1,006✔
814

815
  const types = Object.keys(mediaTypes);
1,006✔
816

817
  if (!types.every(type => SUPPORTED_MEDIA_TYPES.includes(type))) {
1,006!
818
    return false;
×
819
  }
820

821
  if (FEATURES.VIDEO && mediaTypes.video && mediaTypes.video.context) {
1,006!
822
    return SUPPORTED_STREAM_TYPES.includes(mediaTypes.video.context);
×
823
  }
824

825
  return true;
1,006✔
826
}
827

828
/**
829
 * Returns user configured bidder params from adunit
830
 * @param {Object} adUnits
831
 * @param {string} adUnitCode code
832
 * @param {string} bidder code
833
 * @return {Array} user configured param for the given bidder adunit configuration
834
 */
835
export function getUserConfiguredParams(adUnits, adUnitCode, bidder) {
836
  return adUnits
340✔
837
    .filter(adUnit => adUnit.code === adUnitCode)
655✔
838
    .flatMap((adUnit) => adUnit.bids)
315✔
839
    .filter((bidderData) => bidderData.bidder === bidder)
3,523✔
840
    .map((bidderData) => bidderData.params || {});
314✔
841
}
842

843
/**
844
 * Returns Do Not Track state
845
 */
846
export function getDNT() {
847
  return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1' || navigator.doNotTrack === 'yes';
725✔
848
}
849

850
export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode;
905✔
851

852
/**
853
 * Returns filter function to match adUnitCode in slot
854
 * @param slot GoogleTag slot
855
 * @return filter function
856
 */
857
export function isAdUnitCodeMatchingSlot(slot) {
858
  return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode);
218✔
859
}
860

861
/**
862
 * Constructs warning message for when unsupported bidders are dropped from an adunit
863
 * @param {Object} adUnit ad unit from which the bidder is being dropped
864
 * @param {string} bidder bidder code that is not compatible with the adUnit
865
 * @return {string} warning message to display when condition is met
866
 */
867
export function unsupportedBidderMessage(adUnit, bidder) {
868
  const mediaType = Object.keys(adUnit.mediaTypes || {'banner': 'banner'}).join(', ');
6!
869

870
  return `
6✔
871
    ${adUnit.code} is a ${mediaType} ad unit
872
    containing bidders that don't support ${mediaType}: ${bidder}.
873
    This bidder won't fetch demand.
874
  `;
875
}
876

877
/**
878
 * Returns a new object with undefined properties removed from given object
879
 * @param obj the object to clean
880
 */
881
export function cleanObj(obj) {
882
  return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined'))
312✔
883
}
884

885
/**
886
 * Create a new object with selected properties.  Also allows property renaming and transform functions.
887
 * @param obj the original object
888
 * @param properties An array of desired properties
889
 */
890
export function pick(obj, properties) {
891
  if (typeof obj !== 'object') {
1,984✔
892
    return {};
3✔
893
  }
894
  return properties.reduce((newObj, prop, i) => {
1,981✔
895
    if (typeof prop === 'function') {
21,232✔
896
      return newObj;
8,574✔
897
    }
898

899
    let newProp = prop;
12,658✔
900
    const match = prop.match(/^(.+?)\sas\s(.+?)$/i);
12,658✔
901

902
    if (match) {
12,658✔
903
      prop = match[1];
486✔
904
      newProp = match[2];
486✔
905
    }
906

907
    let value = obj[prop];
12,658✔
908
    if (typeof properties[i + 1] === 'function') {
12,658✔
909
      value = properties[i + 1](value, newObj);
8,574✔
910
    }
911
    if (typeof value !== 'undefined') {
12,658✔
912
      newObj[newProp] = value;
8,832✔
913
    }
914

915
    return newObj;
12,658✔
916
  }, {});
917
}
918

919
export function parseQS(query) {
920
  return !query ? {} : query
2,898✔
921
    .replace(/^\?/, '')
922
    .split('&')
923
    .reduce((acc, criteria) => {
924
      let [k, v] = criteria.split('=');
3,271✔
925
      if (/\[\]$/.test(k)) {
3,271!
926
        k = k.replace('[]', '');
×
927
        acc[k] = acc[k] || [];
×
928
        acc[k].push(v);
×
929
      } else {
930
        acc[k] = v || '';
3,271✔
931
      }
932
      return acc;
3,271✔
933
    }, {});
934
}
935

936
export function formatQS(query) {
937
  return Object
469✔
938
    .keys(query)
939
    .map(k => Array.isArray(query[k])
2,621✔
940
      ? query[k].map(v => `${k}[]=${v}`).join('&')
19✔
941
      : `${k}=${query[k]}`)
942
    .join('&');
943
}
944

945
export function parseUrl(url, options) {
946
  const parsed = document.createElement('a');
1,121✔
947
  if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) {
1,121✔
948
    parsed.href = url;
127✔
949
  } else {
950
    parsed.href = decodeURIComponent(url);
994✔
951
  }
952
  // in window.location 'search' is string, not object
953
  const qsAsString = (options && 'decodeSearchAsString' in options && options.decodeSearchAsString);
1,121✔
954
  return {
1,121✔
955
    href: parsed.href,
956
    protocol: (parsed.protocol || '').replace(/:$/, ''),
1,121!
957
    hostname: parsed.hostname,
958
    port: +parsed.port,
959
    pathname: parsed.pathname.replace(/^(?!\/)/, '/'),
960
    search: (qsAsString) ? parsed.search : internal.parseQS(parsed.search || ''),
2,319✔
961
    hash: (parsed.hash || '').replace(/^#/, ''),
2,233✔
962
    host: parsed.host || window.location.host
1,121!
963
  };
964
}
965

966
export function buildUrl(obj) {
967
  return (obj.protocol || 'http') + '://' +
398✔
968
    (obj.host ||
495✔
969
      obj.hostname + (obj.port ? `:${obj.port}` : '')) +
97✔
970
    (obj.pathname || '') +
399✔
971
    (obj.search ? `?${internal.formatQS(obj.search || '')}` : '') +
644!
972
    (obj.hash ? `#${obj.hash}` : '');
398✔
973
}
974

975
/**
976
 * This function deeply compares two objects checking for their equivalence.
977
 * @param {Object} obj1
978
 * @param {Object} obj2
979
 * @param {Object} [options] - Options for comparison.
980
 * @param {boolean} [options.checkTypes=false] - If set, two objects with identical properties but different constructors will *not* be considered equivalent.
981
 * @returns {boolean} - Returns `true` if the objects are equivalent, `false` otherwise.
982
 */
983
export function deepEqual(obj1, obj2, { checkTypes = false } = {}) {
1,944✔
984
  // Quick reference check
985
  if (obj1 === obj2) return true;
1,944✔
986

987
  // If either is null or not an object, do a direct equality check
988
  if (
937✔
989
    typeof obj1 !== 'object' || obj1 === null ||
2,272✔
990
    typeof obj2 !== 'object' || obj2 === null
991
  ) {
992
    return false;
492✔
993
  }
994
  // Cache the Array checks
995
  const isArr1 = Array.isArray(obj1);
445✔
996
  const isArr2 = Array.isArray(obj2);
445✔
997
  // Special case: both are arrays
998
  if (isArr1 && isArr2) {
445✔
999
    if (obj1.length !== obj2.length) return false;
89✔
1000
    for (let i = 0; i < obj1.length; i++) {
59✔
1001
      if (!deepEqual(obj1[i], obj2[i], { checkTypes })) {
81✔
1002
        return false;
31✔
1003
      }
1004
    }
1005
    return true;
28✔
1006
  } else if (isArr1 || isArr2) {
356!
1007
    return false;
×
1008
  }
1009

1010
  // If we’re checking types, compare constructors (e.g., plain object vs. Date)
1011
  if (checkTypes && obj1.constructor !== obj2.constructor) {
356✔
1012
    return false;
1✔
1013
  }
1014

1015
  // Compare object keys. Cache keys for both to avoid repeated calls.
1016
  const keys1 = Object.keys(obj1);
355✔
1017
  const keys2 = Object.keys(obj2);
355✔
1018

1019
  if (keys1.length !== keys2.length) return false;
355✔
1020

1021
  for (const key of keys1) {
348✔
1022
    // If `obj2` doesn't have this key or sub-values aren't equal, bail out.
1023
    if (!Object.prototype.hasOwnProperty.call(obj2, key)) {
388!
1024
      return false;
×
1025
    }
1026
    if (!deepEqual(obj1[key], obj2[key], { checkTypes })) {
388✔
1027
      return false;
324✔
1028
    }
1029
  }
1030

1031
  return true;
24✔
1032
}
1033

1034
export function mergeDeep(target, ...sources) {
1035
  for (let i = 0; i < sources.length; i++) {
13,906✔
1036
    const source = sources[i];
19,590!
1037
    if (!isPlainObject(source)) {
19,590✔
1038
      continue;
7,986✔
1039
    }
1040
    mergeDeepHelper(target, source);
11,604✔
1041
  }
1042
  return target;
13,906✔
1043
}
1044

1045
function mergeDeepHelper(target, source) {
1046
  // quick check
1047
  if (!isPlainObject(target) || !isPlainObject(source)) {
23,360✔
1048
    return;
37✔
1049
  }
1050

1051
  const keys = Object.keys(source);
23,323✔
1052
  for (let i = 0; i < keys.length; i++) {
23,323✔
1053
    const key = keys[i];
35,153✔
1054
    if (key === '__proto__' || key === 'constructor') {
35,153!
1055
      continue;
×
1056
    }
1057
    const val = source[key];
35,153✔
1058

1059
    if (isPlainObject(val)) {
35,153✔
1060
      if (!target[key]) {
11,756✔
1061
        target[key] = {};
10,574✔
1062
      }
1063
      mergeDeepHelper(target[key], val);
11,756✔
1064
    } else if (Array.isArray(val)) {
23,397✔
1065
      if (!Array.isArray(target[key])) {
2,452✔
1066
        target[key] = [...val];
2,222✔
1067
      } else {
1068
        // deduplicate
1069
        val.forEach(obj => {
230✔
1070
          if (!target[key].some(item => deepEqual(item, obj))) {
707✔
1071
            target[key].push(obj);
88✔
1072
          }
1073
        });
1074
      }
1075
    } else {
1076
      // direct assignment
1077
      target[key] = val;
20,945✔
1078
    }
1079
  }
1080
}
1081

1082
/**
1083
 * returns a hash of a string using a fast algorithm
1084
 * source: https://stackoverflow.com/a/52171480/845390
1085
 * @param str
1086
 * @param seed (optional)
1087
 * @returns {string}
1088
 */
1089
export function cyrb53Hash(str, seed = 0) {
162✔
1090
  // IE doesn't support imul
1091
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill
1092
  const imul = function(opA, opB) {
162✔
1093
    if (isFn(Math.imul)) {
12,384!
1094
      return Math.imul(opA, opB);
12,384✔
1095
    } else {
1096
      opB |= 0; // ensure that opB is an integer. opA will automatically be coerced.
×
1097
      // floating points give us 53 bits of precision to work with plus 1 sign bit
1098
      // automatically handled for our convienence:
1099
      // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001
1100
      //    0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
1101
      var result = (opA & 0x003fffff) * opB;
×
1102
      // 2. We can remove an integer coersion from the statement above because:
1103
      //    0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001
1104
      //    0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
1105
      if (opA & 0xffc00000) result += (opA & 0xffc00000) * opB | 0;
×
1106
      return result | 0;
×
1107
    }
1108
  };
1109

1110
  let h1 = 0xdeadbeef ^ seed;
162✔
1111
  let h2 = 0x41c6ce57 ^ seed;
162✔
1112
  for (let i = 0, ch; i < str.length; i++) {
162✔
1113
    ch = str.charCodeAt(i);
5,868✔
1114
    h1 = imul(h1 ^ ch, 2654435761);
5,868✔
1115
    h2 = imul(h2 ^ ch, 1597334677);
5,868✔
1116
  }
1117
  h1 = imul(h1 ^ (h1 >>> 16), 2246822507) ^ imul(h2 ^ (h2 >>> 13), 3266489909);
162✔
1118
  h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909);
162✔
1119
  return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString();
162✔
1120
}
1121

1122
/**
1123
 * returns the result of `JSON.parse(data)`, or undefined if that throws an error.
1124
 * @param data
1125
 * @returns {any}
1126
 */
1127
export function safeJSONParse(data) {
1128
  try {
137✔
1129
    return JSON.parse(data);
137✔
1130
  } catch (e) {}
1131
}
1132

1133
export function safeJSONEncode(data) {
1134
  try {
481✔
1135
    return JSON.stringify(data);
481✔
1136
  } catch (e) {
1137
    return '';
1✔
1138
  }
1139
}
1140

1141
/**
1142
 * Returns a memoized version of `fn`.
1143
 *
1144
 * @param fn
1145
 * @param key cache key generator, invoked with the same arguments passed to `fn`.
1146
 *        By default, the first argument is used as key.
1147
 * @return {*}
1148
 */
1149
export function memoize(fn, key = function (arg) { return arg; }) {
1,324✔
1150
  const cache = new Map();
327✔
1151
  const memoized = function () {
327✔
1152
    const cacheKey = key.apply(this, arguments);
2,019✔
1153
    if (!cache.has(cacheKey)) {
2,019✔
1154
      cache.set(cacheKey, fn.apply(this, arguments));
866✔
1155
    }
1156
    return cache.get(cacheKey);
2,019✔
1157
  }
1158
  memoized.clear = cache.clear.bind(cache);
327✔
1159
  return memoized;
327✔
1160
}
1161

1162
/**
1163
 * Returns a Unix timestamp for given time value and unit.
1164
 * @param {number} timeValue numeric value, defaults to 0 (which means now)
1165
 * @param {string} timeUnit defaults to days (or 'd'), use 'm' for minutes. Any parameter that isn't 'd' or 'm' will return Date.now().
1166
 * @returns {number}
1167
 */
1168
export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') {
723✔
1169
  const acceptableUnits = ['m', 'd'];
723✔
1170
  if (acceptableUnits.indexOf(timeUnit) < 0) {
723✔
1171
    return Date.now();
1✔
1172
  }
1173
  const multiplication = timeValue / (timeUnit === 'm' ? 1440 : 1);
722✔
1174
  return Date.now() + (timeValue && timeValue > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0);
722✔
1175
}
1176

1177
/**
1178
 * Converts given object into an array, so {key: 1, anotherKey: 'fred', third: ['fred']} is turned
1179
 * into [{key: 1}, {anotherKey: 'fred'}, {third: ['fred']}]
1180
 * @param {Object} obj the object
1181
 * @returns {Array}
1182
 */
1183
export function convertObjectToArray(obj) {
1184
  return Object.keys(obj).map(key => {
10✔
1185
    return {[key]: obj[key]};
28✔
1186
  });
1187
}
1188

1189
/**
1190
 * Sets dataset attributes on a script
1191
 * @param {HTMLScriptElement} script
1192
 * @param {object} attributes
1193
 */
1194
export function setScriptAttributes(script, attributes) {
1195
  Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v))
5✔
1196
}
1197

1198
/**
1199
 * Perform a binary search for `el` on an ordered array `arr`.
1200
 *
1201
 * @returns the lowest nonnegative integer I that satisfies:
1202
 *   key(arr[i]) >= key(el) for each i between I and arr.length
1203
 *
1204
 *   (if one or more matches are found for `el`, returns the index of the first;
1205
 *   if the element is not found, return the index of the first element that's greater;
1206
 *   if no greater element exists, return `arr.length`)
1207
 */
1208
export function binarySearch(arr, el, key = (el) => el) {
54✔
1209
  let left = 0;
53✔
1210
  let right = arr.length && arr.length - 1;
53✔
1211
  const target = key(el);
53✔
1212
  while (right - left > 1) {
53✔
1213
    const middle = left + Math.round((right - left) / 2);
14✔
1214
    if (target > key(arr[middle])) {
14✔
1215
      left = middle;
7✔
1216
    } else {
1217
      right = middle;
7✔
1218
    }
1219
  }
1220
  while (arr.length > left && target > key(arr[left])) {
53✔
1221
    left++;
16✔
1222
  }
1223
  return left;
53✔
1224
}
1225

1226
/**
1227
 * Checks if an object has non-serializable properties.
1228
 * Non-serializable properties are functions and RegExp objects.
1229
 *
1230
 * @param {Object} obj - The object to check.
1231
 * @param {Set} checkedObjects - A set of properties that have already been checked.
1232
 * @returns {boolean} - Returns true if the object has non-serializable properties, false otherwise.
1233
 */
1234
export function hasNonSerializableProperty(obj, checkedObjects = new Set()) {
115✔
1235
  for (const key in obj) {
115✔
1236
    const value = obj[key];
380✔
1237
    const type = typeof value;
380✔
1238

1239
    if (
380✔
1240
      value === undefined ||
3,398✔
1241
      type === 'function' ||
1242
      type === 'symbol' ||
1243
      value instanceof RegExp ||
1244
      value instanceof Map ||
1245
      value instanceof Set ||
1246
      value instanceof Date ||
1247
      (value !== null && type === 'object' && value.hasOwnProperty('toJSON'))
1248
    ) {
1249
      return true;
14✔
1250
    }
1251
    if (value !== null && type === 'object' && value.constructor === Object) {
366✔
1252
      if (checkedObjects.has(value)) {
12!
1253
        // circular reference, means we have a non-serializable property
1254
        return true;
×
1255
      }
1256
      checkedObjects.add(value);
12✔
1257
      if (hasNonSerializableProperty(value, checkedObjects)) {
12✔
1258
        return true;
8✔
1259
      }
1260
    }
1261
  }
1262
  return false;
93✔
1263
}
1264

1265
/**
1266
 * Returns the value of a nested property in an array of objects.
1267
 *
1268
 * @param {Array} collection - Array of objects.
1269
 * @param {String} key - Key of nested property.
1270
 * @returns {any|undefined} - Value of nested property.
1271
 */
1272
export function setOnAny(collection, key) {
1273
  for (let i = 0, result; i < collection.length; i++) {
651✔
1274
    result = deepAccess(collection[i], key);
812✔
1275
    if (result) {
812✔
1276
      return result;
90✔
1277
    }
1278
  }
1279
  return undefined;
561✔
1280
}
1281

1282
export function extractDomainFromHost(pageHost) {
1283
  let domain = null;
19✔
1284
  try {
19✔
1285
    const domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost);
19✔
1286
    if (domains != null && domains.length > 0) {
19✔
1287
      domain = domains[0];
19✔
1288
      for (let i = 1; i < domains.length; i++) {
19✔
1289
        if (domains[i].length > domain.length) {
19!
1290
          domain = domains[i];
×
1291
        }
1292
      }
1293
    }
1294
  } catch (e) {
1295
    domain = null;
×
1296
  }
1297
  return domain;
19✔
1298
}
1299

1300
export function triggerNurlWithCpm(bid, cpm) {
1301
  if (isStr(bid.nurl) && bid.nurl !== '') {
2✔
1302
    bid.nurl = bid.nurl.replace(
2✔
1303
      /\${AUCTION_PRICE}/,
1304
      cpm
1305
    );
1306
    triggerPixel(bid.nurl);
2✔
1307
  }
1308
}
1309

1310
// To ensure that isGzipCompressionSupported() doesn’t become an overhead, we have used memoization to cache the result after the first execution.
1311
// 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.
1312
export const isGzipCompressionSupported = (function () {
4✔
1313
  let cachedResult; // Store the result
1314

1315
  return function () {
4✔
1316
    if (cachedResult !== undefined) {
×
1317
      return cachedResult; // Return cached result if already computed
×
1318
    }
1319

1320
    try {
×
1321
      if (typeof window.CompressionStream === 'undefined') {
×
1322
        cachedResult = false;
×
1323
      } else {
1324
        (() => new window.CompressionStream('gzip'))();
×
1325
        cachedResult = true;
×
1326
      }
1327
    } catch (error) {
1328
      cachedResult = false;
×
1329
    }
1330

1331
    return cachedResult;
×
1332
  };
1333
})();
1334

1335
// Make sure to use isGzipCompressionSupported before calling this function
1336
export async function compressDataWithGZip(data) {
1337
  if (typeof data !== 'string') { // TextEncoder (below) expects a string
2✔
1338
    data = JSON.stringify(data);
1✔
1339
  }
1340

1341
  const encoder = new TextEncoder();
2✔
1342
  const encodedData = encoder.encode(data);
2✔
1343
  const compressedStream = new Blob([encodedData])
2✔
1344
    .stream()
1345
    .pipeThrough(new window.CompressionStream('gzip'));
1346

1347
  const compressedBlob = await new Response(compressedStream).blob();
2✔
1348
  const compressedArrayBuffer = await compressedBlob.arrayBuffer();
2✔
1349
  return new Uint8Array(compressedArrayBuffer);
2✔
1350
}
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

© 2025 Coveralls, Inc