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

prebid / Prebid.js / #306

01 Jul 2025 06:03PM UTC coverage: 90.335% (-0.07%) from 90.409%
#306

push

travis-ci

prebidjs-release
Prebid 9.53.1 release

43587 of 54762 branches covered (79.59%)

64461 of 71358 relevant lines covered (90.33%)

148.96 hits per line

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

88.96
/src/utils.js
1
import {config} from './config.js';
2
import {klona} from 'klona/json';
3

4
import {EVENTS} from './constants.js';
5
import {PbPromise} from './utils/promise.js';
6
import {getGlobal} from './prebidGlobal.js';
7
import { default as deepAccess } from 'dlv/index.js';
8

9
export { deepAccess };
10
export { dset as deepSetValue } from 'dset';
11

12
var tStr = 'String';
4✔
13
var tFn = 'Function';
4✔
14
var tNumb = 'Number';
4✔
15
var tObject = 'Object';
4✔
16
var tBoolean = 'Boolean';
4✔
17
var toString = Object.prototype.toString;
4✔
18
let consoleExists = Boolean(window.console);
4✔
19
let consoleLogExists = Boolean(consoleExists && window.console.log);
4✔
20
let consoleInfoExists = Boolean(consoleExists && window.console.info);
4✔
21
let consoleWarnExists = Boolean(consoleExists && window.console.warn);
4✔
22
let consoleErrorExists = Boolean(consoleExists && window.console.error);
4✔
23

24
let eventEmitter;
25
let windowDimensions;
26

27
const pbjsInstance = getGlobal();
4✔
28

29
export function _setEventEmitter(emitFn) {
30
  // called from events.js - this hoop is to avoid circular imports
31
  eventEmitter = emitFn;
4✔
32
}
33

34
function emitEvent(...args) {
35
  if (eventEmitter != null) {
3,686!
36
    eventEmitter(...args);
3,686✔
37
  }
38
}
39

40
export const getWinDimensions = (function() {
4✔
41
  let lastCheckTimestamp;
42
  const CHECK_INTERVAL_MS = 20;
4✔
43
  return () => {
4✔
44
    if (!windowDimensions || !lastCheckTimestamp || (Date.now() - lastCheckTimestamp > CHECK_INTERVAL_MS)) {
3,238✔
45
      internal.resetWinDimensions();
223✔
46
      lastCheckTimestamp = Date.now();
223✔
47
    }
48
    return windowDimensions;
3,238✔
49
  }
50
})();
51

52
export function resetWinDimensions() {
53
  const top = canAccessWindowTop() ? internal.getWindowTop() : internal.getWindowSelf();
405✔
54

55
  windowDimensions = {
405✔
56
    screen: {
57
      width: top.screen?.width,
58
      height: top.screen?.height,
59
      availWidth: top.screen?.availWidth,
60
      availHeight: top.screen?.availHeight,
61
      colorDepth: top.screen?.colorDepth,
62
    },
63
    innerHeight: top.innerHeight,
64
    innerWidth: top.innerWidth,
65
    outerWidth: top.outerWidth,
66
    outerHeight: top.outerHeight,
67
    visualViewport: {
68
      height: top.visualViewport?.height,
69
      width: top.visualViewport?.width,
70
    },
71
    document: {
72
      documentElement: {
73
        clientWidth: top.document?.documentElement?.clientWidth,
74
        clientHeight: top.document?.documentElement?.clientHeight,
75
        scrollTop: top.document?.documentElement?.scrollTop,
76
        scrollLeft: top.document?.documentElement?.scrollLeft,
77
      },
78
      body: {
79
        scrollTop: document.body?.scrollTop,
80
        scrollLeft: document.body?.scrollLeft,
81
        clientWidth: document.body?.clientWidth,
82
        clientHeight: document.body?.clientHeight,
83
      },
84
    }
85
  };
86
}
87

88
// this allows stubbing of utility functions that are used internally by other utility functions
89
export const internal = {
4✔
90
  checkCookieSupport,
91
  createTrackPixelIframeHtml,
92
  getWindowSelf,
93
  getWindowTop,
94
  canAccessWindowTop,
95
  getWindowLocation,
96
  insertUserSyncIframe,
97
  insertElement,
98
  isFn,
99
  triggerPixel,
100
  logError,
101
  logWarn,
102
  logMessage,
103
  logInfo,
104
  parseQS,
105
  formatQS,
106
  deepEqual,
107
  resetWinDimensions
108
};
109

110
let prebidInternal = {};
4✔
111
/**
112
 * Returns object that is used as internal prebid namespace
113
 */
114
export function getPrebidInternal() {
115
  return prebidInternal;
×
116
}
117

118
/* utility method to get incremental integer starting from 1 */
119
var getIncrementalInteger = (function () {
4✔
120
  var count = 0;
4✔
121
  return function () {
4✔
122
    count++;
2,753✔
123
    return count;
2,753✔
124
  };
125
})();
126

127
// generate a random string (to be used as a dynamic JSONP callback)
128
export function getUniqueIdentifierStr() {
129
  return getIncrementalInteger() + Math.random().toString(16).substr(2);
2,753✔
130
}
131

132
/**
133
 * Returns a random v4 UUID of the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,
134
 * where each x is replaced with a random hexadecimal digit from 0 to f,
135
 * and y is replaced with a random hexadecimal digit from 8 to b.
136
 * https://gist.github.com/jed/982883 via node-uuid
137
 */
138
export function generateUUID(placeholder) {
139
  return placeholder
97,350✔
140
    ? (placeholder ^ _getRandomData() >> placeholder / 4).toString(16)
141
    : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, generateUUID);
142
}
143

144
/**
145
 * Returns random data using the Crypto API if available and Math.random if not
146
 * Method is from https://gist.github.com/jed/982883 like generateUUID, direct link https://gist.github.com/jed/982883#gistcomment-45104
147
 */
148
function _getRandomData() {
149
  if (window && window.crypto && window.crypto.getRandomValues) {
94,308!
150
    return crypto.getRandomValues(new Uint8Array(1))[0] % 16;
94,308✔
151
  } else {
152
    return Math.random() * 16;
×
153
  }
154
}
155

156
export function getBidIdParameter(key, paramsObj) {
157
  return paramsObj?.[key] || '';
3,662✔
158
}
159

160
// parse a query string object passed in bid params
161
// bid params should be an object such as {key: "value", key1 : "value1"}
162
// aliases to formatQS
163
export function parseQueryStringParameters(queryObj) {
164
  let result = '';
78✔
165
  for (var k in queryObj) {
78✔
166
    if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; }
975!
167
  }
168
  result = result.replace(/&$/, '');
78✔
169
  return result;
78✔
170
}
171

172
// transform an AdServer targeting bids into a query string to send to the adserver
173
export function transformAdServerTargetingObj(targeting) {
174
  // we expect to receive targeting for a single slot at a time
175
  if (targeting && Object.getOwnPropertyNames(targeting).length > 0) {
3✔
176
    return Object.keys(targeting)
2✔
177
      .map(key => `${key}=${encodeURIComponent(targeting[key])}`).join('&');
66✔
178
  } else {
179
    return '';
1✔
180
  }
181
}
182

183
/**
184
 * 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]]'
185
 */
186
export function sizesToSizeTuples(sizes) {
187
  if (typeof sizes === 'string') {
3,519✔
188
    // multiple sizes will be comma-separated
189
    return sizes
30✔
190
      .split(/\s*,\s*/)
191
      .map(sz => sz.match(/^(\d+)x(\d+)$/i))
33✔
192
      .filter(match => match)
33✔
193
      .map(([_, w, h]) => [parseInt(w, 10), parseInt(h, 10)])
27✔
194
  } else if (Array.isArray(sizes)) {
3,489✔
195
    if (isValidGPTSingleSize(sizes)) {
3,303✔
196
      return [sizes]
435✔
197
    }
198
    return sizes.filter(isValidGPTSingleSize);
2,868✔
199
  }
200
  return [];
186✔
201
}
202

203
/**
204
 * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']'
205
 * @param  {(Array.<number[]>|Array.<number>)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]]
206
 * @return {Array.<string>}  Array of strings like `["300x250"]` or `["300x250", "728x90"]`
207
 */
208
export function parseSizesInput(sizeObj) {
209
  return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString);
1,884✔
210
}
211

212
export function sizeTupleToSizeString(size) {
213
  return size[0] + 'x' + size[1]
3,274✔
214
}
215

216
// Parse a GPT style single size array, (i.e [300, 250])
217
// into an AppNexus style string, (i.e. 300x250)
218
export function parseGPTSingleSizeArray(singleSize) {
219
  if (isValidGPTSingleSize(singleSize)) {
32✔
220
    return sizeTupleToSizeString(singleSize);
22✔
221
  }
222
}
223

224
export function sizeTupleToRtbSize(size) {
225
  return {w: size[0], h: size[1]};
2,577✔
226
}
227

228
// Parse a GPT style single size array, (i.e [300, 250])
229
// into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250})
230
export function parseGPTSingleSizeArrayToRtbSize(singleSize) {
231
  if (isValidGPTSingleSize(singleSize)) {
306✔
232
    return sizeTupleToRtbSize(singleSize)
300✔
233
  }
234
}
235

236
function isValidGPTSingleSize(singleSize) {
237
  // if we aren't exactly 2 items in this array, it is invalid
238
  return isArray(singleSize) && singleSize.length === 2 && (!isNaN(singleSize[0]) && !isNaN(singleSize[1]));
8,753✔
239
}
240

241
export function getWindowTop() {
242
  return window.top;
3,016✔
243
}
244

245
export function getWindowSelf() {
246
  return window.self;
1,849✔
247
}
248

249
export function getWindowLocation() {
250
  return window.location;
1,909✔
251
}
252

253
export function getDocument() {
254
  return document;
400✔
255
}
256

257
export function canAccessWindowTop() {
258
  try {
466✔
259
    if (internal.getWindowTop().location.href) {
466!
260
      return true;
383✔
261
    }
262
  } catch (e) {
263
    return false;
83✔
264
  }
265
}
266

267
/**
268
 * Wrappers to console.(log | info | warn | error). Takes N arguments, the same as the native methods
269
 */
270
export function logMessage() {
271
  if (debugTurnedOn() && consoleLogExists) {
13,388✔
272
    // eslint-disable-next-line no-console
273
    console.log.apply(console, decorateLog(arguments, 'MESSAGE:'));
7✔
274
  }
275
}
276

277
export function logInfo() {
278
  if (debugTurnedOn() && consoleInfoExists) {
11,575✔
279
    // eslint-disable-next-line no-console
280
    console.info.apply(console, decorateLog(arguments, 'INFO:'));
2✔
281
  }
282
}
283

284
export function logWarn() {
285
  if (debugTurnedOn() && consoleWarnExists) {
1,683!
286
    // eslint-disable-next-line no-console
287
    console.warn.apply(console, decorateLog(arguments, 'WARNING:'));
×
288
  }
289
  emitEvent(EVENTS.AUCTION_DEBUG, { type: 'WARNING', arguments: arguments });
1,683✔
290
}
291

292
export function logError() {
293
  if (debugTurnedOn() && consoleErrorExists) {
2,003✔
294
    // eslint-disable-next-line no-console
295
    console.error.apply(console, decorateLog(arguments, 'ERROR:'));
3✔
296
  }
297
  emitEvent(EVENTS.AUCTION_DEBUG, { type: 'ERROR', arguments: arguments });
2,003✔
298
}
299

300
export function prefixLog(prefix) {
301
  function decorate(fn) {
302
    return function (...args) {
600✔
303
      fn(prefix, ...args);
1,052✔
304
    }
305
  }
306
  return {
150✔
307
    logError: decorate(logError),
308
    logWarn: decorate(logWarn),
309
    logMessage: decorate(logMessage),
310
    logInfo: decorate(logInfo),
311
  }
312
}
313

314
function decorateLog(args, prefix) {
315
  args = [].slice.call(args);
12✔
316
  let bidder = config.getCurrentBidder();
12✔
317

318
  prefix && args.unshift(prefix);
12✔
319
  if (bidder) {
12!
320
    args.unshift(label('#aaa'));
×
321
  }
322
  args.unshift(label('#3b88c3'));
12✔
323
  args.unshift('%cPrebid' + (bidder ? `%c${bidder}` : ''));
12!
324
  return args;
12✔
325

326
  function label(color) {
327
    return `display: inline-block; color: #fff; background: ${color}; padding: 1px 4px; border-radius: 3px;`
12✔
328
  }
329
}
330

331
export function hasConsoleLogger() {
332
  return consoleLogExists;
×
333
}
334

335
export function debugTurnedOn() {
336
  return !!config.getConfig('debug');
29,555✔
337
}
338

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

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

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

380
/**
381
 * Return if the object is of the
382
 * given type.
383
 * @param {*} object to test
384
 * @param {String} _t type string (e.g., Array)
385
 * @return {Boolean} if object is of type _t
386
 */
387
export function isA(object, _t) {
388
  return toString.call(object) === '[object ' + _t + ']';
204,586✔
389
}
390

391
export function isFn(object) {
392
  return isA(object, tFn);
31,016✔
393
}
394

395
export function isStr(object) {
396
  return isA(object, tStr);
38,230✔
397
}
398

399
export const isArray = Array.isArray.bind(Array);
4✔
400

401
export function isNumber(object) {
402
  return isA(object, tNumb);
4,745✔
403
}
404

405
export function isPlainObject(object) {
406
  return isA(object, tObject);
129,934✔
407
}
408

409
export function isBoolean(object) {
410
  return isA(object, tBoolean);
652✔
411
}
412

413
/**
414
 * Return if the object is "empty";
415
 * this includes falsey, no keys, or no items at indices
416
 * @param {*} object object to test
417
 * @return {Boolean} if object is empty
418
 */
419
export function isEmpty(object) {
420
  if (!object) return true;
51,552✔
421
  if (isArray(object) || isStr(object)) {
47,939✔
422
    return !(object.length > 0);
21,838✔
423
  }
424
  return Object.keys(object).length <= 0;
26,101✔
425
}
426

427
/**
428
 * Return if string is empty, null, or undefined
429
 * @param str string to test
430
 * @returns {boolean} if string is empty
431
 */
432
export function isEmptyStr(str) {
433
  return isStr(str) && (!str || str.length === 0);
777✔
434
}
435

436
/**
437
 * Iterate object with the function
438
 * falls back to es5 `forEach`
439
 * @param {Array|Object} object
440
 * @param {Function} fn - The function to execute for each element. It receives three arguments: value, key, and the original object.
441
 * @returns {void}
442
 */
443
export function _each(object, fn) {
444
  if (isFn(object?.forEach)) return object.forEach(fn, this);
7,308✔
445
  Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k));
36,197✔
446
}
447

448
export function contains(a, obj) {
449
  return isFn(a?.includes) && a.includes(obj);
40✔
450
}
451

452
/**
453
 * Map an array or object into another array
454
 * given a function
455
 * @param {Array|Object} object
456
 * @param {Function} callback - The function to execute for each element. It receives three arguments: value, key, and the original object.
457
 * @return {Array}
458
 */
459
export function _map(object, callback) {
460
  if (isFn(object?.map)) return object.map(callback);
426✔
461
  return Object.entries(object || {}).map(([k, v]) => callback(v, k, object))
6,485✔
462
}
463

464
/*
465
* Inserts an element(elm) as targets child, by default as first child
466
* @param {HTMLElement} elm
467
* @param {HTMLElement} [doc]
468
* @param {HTMLElement} [target]
469
* @param {Boolean} [asLastChildChild]
470
* @return {HTML Element}
471
*/
472
export function insertElement(elm, doc, target, asLastChildChild) {
473
  doc = doc || document;
18✔
474
  let parentEl;
475
  if (target) {
18✔
476
    parentEl = doc.getElementsByTagName(target);
14✔
477
  } else {
478
    parentEl = doc.getElementsByTagName('head');
4✔
479
  }
480
  try {
18✔
481
    parentEl = parentEl.length ? parentEl : doc.getElementsByTagName('body');
18!
482
    if (parentEl.length) {
18!
483
      parentEl = parentEl[0];
18✔
484
      let insertBeforeEl = asLastChildChild ? null : parentEl.firstChild;
18✔
485
      return parentEl.insertBefore(elm, insertBeforeEl);
18✔
486
    }
487
  } catch (e) {}
488
}
489

490
/**
491
 * Returns a promise that completes when the given element triggers a 'load' or 'error' DOM event, or when
492
 * `timeout` milliseconds have elapsed.
493
 *
494
 * @param {HTMLElement} element
495
 * @param {Number} [timeout]
496
 * @returns {Promise}
497
 */
498
export function waitForElementToLoad(element, timeout) {
499
  let timer = null;
3✔
500
  return new PbPromise((resolve) => {
3✔
501
    const onLoad = function() {
3✔
502
      element.removeEventListener('load', onLoad);
3✔
503
      element.removeEventListener('error', onLoad);
3✔
504
      if (timer != null) {
3✔
505
        window.clearTimeout(timer);
1✔
506
      }
507
      resolve();
3✔
508
    };
509
    element.addEventListener('load', onLoad);
3✔
510
    element.addEventListener('error', onLoad);
3✔
511
    if (timeout != null) {
3✔
512
      timer = window.setTimeout(onLoad, timeout);
1✔
513
    }
514
  });
515
}
516

517
/**
518
 * Inserts an image pixel with the specified `url` for cookie sync
519
 * @param {string} url URL string of the image pixel to load
520
 * @param  {function} [done] an optional exit callback, used when this usersync pixel is added during an async process
521
 * @param  {Number} [timeout] an optional timeout in milliseconds for the image to load before calling `done`
522
 */
523
export function triggerPixel(url, done, timeout) {
524
  const img = new Image();
64✔
525
  if (done && internal.isFn(done)) {
64!
526
    waitForElementToLoad(img, timeout).then(done);
×
527
  }
528
  img.src = url;
64✔
529
}
530

531
/**
532
 * Inserts an empty iframe with the specified `html`, primarily used for tracking purposes
533
 * (though could be for other purposes)
534
 * @param {string} htmlCode snippet of HTML code used for tracking purposes
535
 */
536
export function insertHtmlIntoIframe(htmlCode) {
537
  if (!htmlCode) {
×
538
    return;
×
539
  }
540
  const iframe = createInvisibleIframe();
×
541
  internal.insertElement(iframe, document, 'body');
×
542

543
  ((doc) => {
×
544
    doc.open();
×
545
    doc.write(htmlCode);
×
546
    doc.close();
×
547
  })(iframe.contentWindow.document);
548
}
549

550
/**
551
 * Inserts empty iframe with the specified `url` for cookie sync
552
 * @param  {string} url URL to be requested
553
 * @param  {function} [done] an optional exit callback, used when this usersync pixel is added during an async process
554
 * @param  {Number} [timeout] an optional timeout in milliseconds for the iframe to load before calling `done`
555
 */
556
export function insertUserSyncIframe(url, done, timeout) {
557
  let iframeHtml = internal.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin');
1✔
558
  let div = document.createElement('div');
1✔
559
  div.innerHTML = iframeHtml;
1✔
560
  let iframe = div.firstChild;
1✔
561
  if (done && internal.isFn(done)) {
1!
562
    waitForElementToLoad(iframe, timeout).then(done);
×
563
  }
564
  internal.insertElement(iframe, document, 'html', true);
1✔
565
}
566

567
/**
568
 * Creates a snippet of HTML that retrieves the specified `url`
569
 * @param  {string} url URL to be requested
570
 * @param encode
571
 * @return {string}     HTML snippet that contains the img src = set to `url`
572
 */
573
export function createTrackPixelHtml(url, encode = encodeURI) {
65✔
574
  if (!url) {
96!
575
    return '';
×
576
  }
577

578
  let escapedUrl = encode(url);
96✔
579
  let img = '<div style="position:absolute;left:0px;top:0px;visibility:hidden;">';
96✔
580
  img += '<img src="' + escapedUrl + '"></div>';
96✔
581
  return img;
96✔
582
};
583

584
/**
585
 * encodeURI, but preserves macros of the form '${MACRO}' (e.g. '${AUCTION_PRICE}')
586
 * @param url
587
 * @return {string}
588
 */
589
export function encodeMacroURI(url) {
590
  const macros = Array.from(url.matchAll(/\$({[^}]+})/g)).map(match => match[1]);
44✔
591
  return macros.reduce((str, macro) => {
38✔
592
    return str.replace('$' + encodeURIComponent(macro), '$' + macro)
25✔
593
  }, encodeURI(url))
594
}
595

596
/**
597
 * Creates a snippet of Iframe HTML that retrieves the specified `url`
598
 * @param  {string} url plain URL to be requested
599
 * @param  {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true
600
 * @param  {string} sandbox string if provided the sandbox attribute will be included with the given value
601
 * @return {string}     HTML snippet that contains the iframe src = set to `url`
602
 */
603
export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') {
×
604
  if (!url) {
1!
605
    return '';
×
606
  }
607
  if (encodeUri) {
1!
608
    url = encodeURI(url);
×
609
  }
610
  if (sandbox) {
1!
611
    sandbox = `sandbox="${sandbox}"`;
1✔
612
  }
613

614
  return `<iframe ${sandbox} id="${getUniqueIdentifierStr()}"
1✔
615
      frameborder="0"
616
      allowtransparency="true"
617
      marginheight="0" marginwidth="0"
618
      width="0" hspace="0" vspace="0" height="0"
619
      style="height:0px;width:0px;display:none;"
620
      scrolling="no"
621
      src="${url}">
622
    </iframe>`;
623
}
624

625
export function uniques(value, index, arry) {
626
  return arry.indexOf(value) === index;
16,067✔
627
}
628

629
export function flatten(a, b) {
630
  return a.concat(b);
3,366✔
631
}
632

633
export function getBidRequest(id, bidderRequests) {
634
  if (!id) {
252✔
635
    return;
4✔
636
  }
637
  return bidderRequests.flatMap(br => br.bids)
333✔
638
    .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id))
365✔
639
}
640

641
export function getValue(obj, key) {
642
  return obj[key];
101✔
643
}
644

645
export function getBidderCodes(adUnits = pbjsInstance.adUnits) {
×
646
  // this could memoize adUnits
647
  return adUnits.map(unit => unit.bids.map(bid => bid.bidder)
1,126✔
648
    .reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques);
1,126✔
649
}
650

651
export function isGptPubadsDefined() {
652
  if (window.googletag && isFn(window.googletag.pubads) && isFn(window.googletag.pubads().getSlots)) {
808✔
653
    return true;
786✔
654
  }
655
}
656

657
export function isApnGetTagDefined() {
658
  if (window.apntag && isFn(window.apntag.getTag)) {
2!
659
    return true;
×
660
  }
661
}
662

663
export const sortByHighestCpm = (a, b) => {
4✔
664
  return b.cpm - a.cpm;
593✔
665
}
666

667
/**
668
 * Fisher–Yates shuffle
669
 * http://stackoverflow.com/a/6274398
670
 * https://bost.ocks.org/mike/shuffle/
671
 * istanbul ignore next
672
 */
673
export function shuffle(array) {
674
  let counter = array.length;
209✔
675

676
  // while there are elements in the array
677
  while (counter > 0) {
209✔
678
    // pick a random index
679
    let index = Math.floor(Math.random() * counter);
762✔
680

681
    // decrease counter by 1
682
    counter--;
762✔
683

684
    // and swap the last element with it
685
    let temp = array[counter];
762✔
686
    array[counter] = array[index];
762✔
687
    array[index] = temp;
762✔
688
  }
689

690
  return array;
209✔
691
}
692

693
export function deepClone(obj) {
694
  return klona(obj) || {};
9,206✔
695
}
696

697
export function inIframe() {
698
  try {
1,507✔
699
    return internal.getWindowSelf() !== internal.getWindowTop();
1,507✔
700
  } catch (e) {
701
    return true;
×
702
  }
703
}
704

705
/**
706
 * https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf
707
 */
708
export function isSafeFrameWindow() {
709
  if (!inIframe()) {
72!
710
    return false;
×
711
  }
712

713
  const ws = internal.getWindowSelf();
72✔
714
  return !!(ws.$sf && ws.$sf.ext);
72✔
715
}
716

717
/**
718
 * Returns the result of calling the function $sf.ext.geom() if it exists
719
 * @see https://iabtechlab.com/wp-content/uploads/2016/03/SafeFrames_v1.1_final.pdf — 5.4 Function $sf.ext.geom
720
 * @returns {Object | undefined} geometric information about the container
721
 */
722
export function getSafeframeGeometry() {
723
  try {
4✔
724
    const ws = getWindowSelf();
4✔
725
    return (typeof ws.$sf.ext.geom === 'function') ? ws.$sf.ext.geom() : undefined;
4✔
726
  } catch (e) {
727
    logError('Error getting SafeFrame geometry', e);
×
728
    return undefined;
×
729
  }
730
}
731

732
export function isSafariBrowser() {
733
  return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent);
72✔
734
}
735

736
export function replaceMacros(str, subs) {
737
  if (!str) return;
153✔
738
  return Object.entries(subs).reduce((str, [key, val]) => {
101✔
739
    return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || '');
127✔
740
  }, str);
741
}
742

743
export function replaceAuctionPrice(str, cpm) {
744
  return replaceMacros(str, {AUCTION_PRICE: cpm})
101✔
745
}
746

747
export function replaceClickThrough(str, clicktag) {
748
  if (!str || !clicktag || typeof clicktag !== 'string') return;
×
749
  return str.replace(/\${CLICKTHROUGH}/g, clicktag);
×
750
}
751

752
export function timestamp() {
753
  return new Date().getTime();
8,775✔
754
}
755

756
/**
757
 * The returned value represents the time elapsed since the time origin. @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
758
 * @returns {number}
759
 */
760
export function getPerformanceNow() {
761
  return (window.performance && window.performance.now && window.performance.now()) || 0;
7,793✔
762
}
763

764
/**
765
 * Retuns the difference between `timing.domLoading` and `timing.navigationStart`.
766
 * This function uses the deprecated `Performance.timing` API and should be removed in future.
767
 * It has not been updated yet because it is still used in some modules.
768
 * @deprecated
769
 * @param {Window} w The window object used to perform the api call. default to window.self
770
 * @returns {number}
771
 */
772
export function getDomLoadingDuration(w) {
773
  let domLoadingDuration = -1;
17✔
774

775
  w = w || getWindowSelf();
17!
776

777
  const performance = w.performance;
17✔
778

779
  if (w.performance?.timing) {
17!
780
    if (w.performance.timing.navigationStart > 0) {
17✔
781
      const val = performance.timing.domLoading - performance.timing.navigationStart;
14✔
782
      if (val > 0) {
14!
783
        domLoadingDuration = val;
14✔
784
      }
785
    }
786
  }
787

788
  return domLoadingDuration;
17✔
789
}
790

791
/**
792
 * When the deviceAccess flag config option is false, no cookies should be read or set
793
 * @returns {boolean}
794
 */
795
export function hasDeviceAccess() {
796
  return config.getConfig('deviceAccess') !== false;
19,279✔
797
}
798

799
/**
800
 * @returns {(boolean|undefined)}
801
 */
802
export function checkCookieSupport() {
803
  // eslint-disable-next-line no-restricted-properties
804
  if (window.navigator.cookieEnabled || !!document.cookie.length) {
1,296!
805
    return true;
1,296✔
806
  }
807
}
808

809
/**
810
 * Given a function, return a function which only executes the original after
811
 * it's been called numRequiredCalls times.
812
 *
813
 * Note that the arguments from the previous calls will *not* be forwarded to the original function.
814
 * Only the final call's arguments matter.
815
 *
816
 * @param {function} func The function which should be executed, once the returned function has been executed
817
 *   numRequiredCalls times.
818
 * @param {number} numRequiredCalls The number of times which the returned function needs to be called before
819
 *   func is.
820
 */
821
export function delayExecution(func, numRequiredCalls) {
822
  if (numRequiredCalls < 1) {
391!
823
    throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`);
×
824
  }
825
  let numCalls = 0;
391✔
826
  return function () {
391✔
827
    numCalls++;
186✔
828
    if (numCalls === numRequiredCalls) {
186✔
829
      func.apply(this, arguments);
162✔
830
    }
831
  }
832
}
833

834
/**
835
 * https://stackoverflow.com/a/34890276/428704
836
 * @param {Array} xs
837
 * @param {string} key
838
 * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}}
839
 */
840
export function groupBy(xs, key) {
841
  return xs.reduce(function(rv, x) {
1,119✔
842
    (rv[x[key]] = rv[x[key]] || []).push(x);
1,419✔
843
    return rv;
1,419✔
844
  }, {});
845
}
846

847
/**
848
 * Build an object consisting of only defined parameters to avoid creating an
849
 * object with defined keys and undefined values.
850
 * @param {Object} object The object to pick defined params out of
851
 * @param {string[]} params An array of strings representing properties to look for in the object
852
 * @returns {Object} An object containing all the specified values that are defined
853
 */
854
export function getDefinedParams(object, params) {
855
  return params
2,088✔
856
    .filter(param => object[param])
6,456✔
857
    .reduce((bid, param) => Object.assign(bid, { [param]: object[param] }), {});
1,393✔
858
}
859

860
/**
861
 * @typedef {Object} MediaTypes
862
 * @property {Object} banner banner configuration
863
 * @property {Object} native native configuration
864
 * @property {Object} video video configuration
865
 */
866

867
/**
868
 * Validates an adunit's `mediaTypes` parameter
869
 * @param {MediaTypes} mediaTypes mediaTypes parameter to validate
870
 * @return {boolean} If object is valid
871
 */
872
export function isValidMediaTypes(mediaTypes) {
873
  const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video'];
1,027✔
874
  const SUPPORTED_STREAM_TYPES = ['instream', 'outstream', 'adpod'];
1,027✔
875

876
  const types = Object.keys(mediaTypes);
1,027✔
877

878
  if (!types.every(type => SUPPORTED_MEDIA_TYPES.includes(type))) {
1,027!
879
    return false;
×
880
  }
881

882
  if (FEATURES.VIDEO && mediaTypes.video && mediaTypes.video.context) {
1,027!
883
    return SUPPORTED_STREAM_TYPES.includes(mediaTypes.video.context);
×
884
  }
885

886
  return true;
1,027✔
887
}
888

889
/**
890
 * Returns user configured bidder params from adunit
891
 * @param {Object} adUnits
892
 * @param {string} adUnitCode code
893
 * @param {string} bidder code
894
 * @return {Array} user configured param for the given bidder adunit configuration
895
 */
896
export function getUserConfiguredParams(adUnits, adUnitCode, bidder) {
897
  return adUnits
53✔
898
    .filter(adUnit => adUnit.code === adUnitCode)
81✔
899
    .flatMap((adUnit) => adUnit.bids)
28✔
900
    .filter((bidderData) => bidderData.bidder === bidder)
202✔
901
    .map((bidderData) => bidderData.params || {});
27✔
902
}
903

904
/**
905
 * Returns Do Not Track state
906
 */
907
export function getDNT() {
908
  return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1' || navigator.doNotTrack === 'yes';
783✔
909
}
910

911
export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode;
935✔
912

913
/**
914
 * Returns filter function to match adUnitCode in slot
915
 * @param {Object} slot GoogleTag slot
916
 * @return {function} filter function
917
 */
918
export function isAdUnitCodeMatchingSlot(slot) {
919
  return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode);
249✔
920
}
921

922
/**
923
 * Constructs warning message for when unsupported bidders are dropped from an adunit
924
 * @param {Object} adUnit ad unit from which the bidder is being dropped
925
 * @param {string} bidder bidder code that is not compatible with the adUnit
926
 * @return {string} warning message to display when condition is met
927
 */
928
export function unsupportedBidderMessage(adUnit, bidder) {
929
  const mediaType = Object.keys(adUnit.mediaTypes || {'banner': 'banner'}).join(', ');
7!
930

931
  return `
7✔
932
    ${adUnit.code} is a ${mediaType} ad unit
933
    containing bidders that don't support ${mediaType}: ${bidder}.
934
    This bidder won't fetch demand.
935
  `;
936
}
937

938
/**
939
 * Checks input is integer or not
940
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger
941
 * @param {*} value
942
 */
943
export const isInteger = Number.isInteger.bind(Number);
4✔
944

945
/**
946
 * Returns a new object with undefined properties removed from given object
947
 * @param obj the object to clean
948
 */
949
export function cleanObj(obj) {
950
  return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined'))
305✔
951
}
952

953
/**
954
 * Create a new object with selected properties.  Also allows property renaming and transform functions.
955
 * @param obj the original object
956
 * @param properties An array of desired properties
957
 */
958
export function pick(obj, properties) {
959
  if (typeof obj !== 'object') {
2,029✔
960
    return {};
3✔
961
  }
962
  return properties.reduce((newObj, prop, i) => {
2,026✔
963
    if (typeof prop === 'function') {
21,887✔
964
      return newObj;
8,892✔
965
    }
966

967
    let newProp = prop;
12,995✔
968
    let match = prop.match(/^(.+?)\sas\s(.+?)$/i);
12,995✔
969

970
    if (match) {
12,995✔
971
      prop = match[1];
486✔
972
      newProp = match[2];
486✔
973
    }
974

975
    let value = obj[prop];
12,995✔
976
    if (typeof properties[i + 1] === 'function') {
12,995✔
977
      value = properties[i + 1](value, newObj);
8,892✔
978
    }
979
    if (typeof value !== 'undefined') {
12,995✔
980
      newObj[newProp] = value;
9,123✔
981
    }
982

983
    return newObj;
12,995✔
984
  }, {});
985
}
986

987
export function isArrayOfNums(val, size) {
988
  return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v)));
1,353✔
989
}
990

991
export function parseQS(query) {
992
  return !query ? {} : query
2,923✔
993
    .replace(/^\?/, '')
994
    .split('&')
995
    .reduce((acc, criteria) => {
996
      let [k, v] = criteria.split('=');
3,324✔
997
      if (/\[\]$/.test(k)) {
3,324!
998
        k = k.replace('[]', '');
×
999
        acc[k] = acc[k] || [];
×
1000
        acc[k].push(v);
×
1001
      } else {
1002
        acc[k] = v || '';
3,324✔
1003
      }
1004
      return acc;
3,324✔
1005
    }, {});
1006
}
1007

1008
export function formatQS(query) {
1009
  return Object
480✔
1010
    .keys(query)
1011
    .map(k => Array.isArray(query[k])
2,678✔
1012
      ? query[k].map(v => `${k}[]=${v}`).join('&')
20✔
1013
      : `${k}=${query[k]}`)
1014
    .join('&');
1015
}
1016

1017
export function parseUrl(url, options) {
1018
  let parsed = document.createElement('a');
1,116✔
1019
  if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) {
1,116✔
1020
    parsed.href = url;
127✔
1021
  } else {
1022
    parsed.href = decodeURIComponent(url);
989✔
1023
  }
1024
  // in window.location 'search' is string, not object
1025
  let qsAsString = (options && 'decodeSearchAsString' in options && options.decodeSearchAsString);
1,116✔
1026
  return {
1,116✔
1027
    href: parsed.href,
1028
    protocol: (parsed.protocol || '').replace(/:$/, ''),
1,116!
1029
    hostname: parsed.hostname,
1030
    port: +parsed.port,
1031
    pathname: parsed.pathname.replace(/^(?!\/)/, '/'),
1032
    search: (qsAsString) ? parsed.search : internal.parseQS(parsed.search || ''),
2,320✔
1033
    hash: (parsed.hash || '').replace(/^#/, ''),
2,223✔
1034
    host: parsed.host || window.location.host
1,116!
1035
  };
1036
}
1037

1038
export function buildUrl(obj) {
1039
  return (obj.protocol || 'http') + '://' +
398✔
1040
    (obj.host ||
495✔
1041
      obj.hostname + (obj.port ? `:${obj.port}` : '')) +
97✔
1042
    (obj.pathname || '') +
399✔
1043
    (obj.search ? `?${internal.formatQS(obj.search || '')}` : '') +
644!
1044
    (obj.hash ? `#${obj.hash}` : '');
398✔
1045
}
1046

1047
/**
1048
 * This function deeply compares two objects checking for their equivalence.
1049
 * @param {Object} obj1
1050
 * @param {Object} obj2
1051
 * @param {Object} [options] - Options for comparison.
1052
 * @param {boolean} [options.checkTypes=false] - If set, two objects with identical properties but different constructors will *not* be considered equivalent.
1053
 * @returns {boolean} - Returns `true` if the objects are equivalent, `false` otherwise.
1054
 */
1055
export function deepEqual(obj1, obj2, { checkTypes = false } = {}) {
2,942✔
1056
  // Quick reference check
1057
  if (obj1 === obj2) return true;
1,939✔
1058

1059
  // If either is null or not an object, do a direct equality check
1060
  if (
934✔
1061
    typeof obj1 !== 'object' || obj1 === null ||
2,269✔
1062
    typeof obj2 !== 'object' || obj2 === null
1063
  ) {
1064
    return false;
489✔
1065
  }
1066
  // Cache the Array checks
1067
  const isArr1 = Array.isArray(obj1);
445✔
1068
  const isArr2 = Array.isArray(obj2);
445✔
1069
  // Special case: both are arrays
1070
  if (isArr1 && isArr2) {
445✔
1071
    if (obj1.length !== obj2.length) return false;
89✔
1072
    for (let i = 0; i < obj1.length; i++) {
59✔
1073
      if (!deepEqual(obj1[i], obj2[i], { checkTypes })) {
81✔
1074
        return false;
31✔
1075
      }
1076
    }
1077
    return true;
28✔
1078
  } else if (isArr1 || isArr2) {
356!
1079
    return false;
×
1080
  }
1081

1082
  // If we’re checking types, compare constructors (e.g., plain object vs. Date)
1083
  if (checkTypes && obj1.constructor !== obj2.constructor) {
356✔
1084
    return false;
1✔
1085
  }
1086

1087
  // Compare object keys. Cache keys for both to avoid repeated calls.
1088
  const keys1 = Object.keys(obj1);
355✔
1089
  const keys2 = Object.keys(obj2);
355✔
1090

1091
  if (keys1.length !== keys2.length) return false;
355✔
1092

1093
  for (const key of keys1) {
346✔
1094
    // If `obj2` doesn't have this key or sub-values aren't equal, bail out.
1095
    if (!Object.prototype.hasOwnProperty.call(obj2, key)) {
386!
1096
      return false;
×
1097
    }
1098
    if (!deepEqual(obj1[key], obj2[key], { checkTypes })) {
386✔
1099
      return false;
324✔
1100
    }
1101
  }
1102

1103
  return true;
22✔
1104
}
1105

1106
export function mergeDeep(target, ...sources) {
1107
  for (let i = 0; i < sources.length; i++) {
13,796✔
1108
    const source = sources[i];
19,282✔
1109
    if (!isPlainObject(source)) {
19,282✔
1110
      continue;
7,971✔
1111
    }
1112
    mergeDeepHelper(target, source);
11,311✔
1113
  }
1114
  return target;
13,796✔
1115
}
1116

1117
function mergeDeepHelper(target, source) {
1118
  // quick check
1119
  if (!isPlainObject(target) || !isPlainObject(source)) {
23,049✔
1120
    return;
37✔
1121
  }
1122

1123
  const keys = Object.keys(source);
23,012✔
1124
  for (let i = 0; i < keys.length; i++) {
23,012✔
1125
    const key = keys[i];
36,695✔
1126
    if (key === '__proto__' || key === 'constructor') {
36,695!
1127
      continue;
×
1128
    }
1129
    const val = source[key];
36,695✔
1130

1131
    if (isPlainObject(val)) {
36,695✔
1132
      if (!target[key]) {
11,738✔
1133
        target[key] = {};
10,588✔
1134
      }
1135
      mergeDeepHelper(target[key], val);
11,738✔
1136
    } else if (Array.isArray(val)) {
24,957✔
1137
      if (!Array.isArray(target[key])) {
2,613✔
1138
        target[key] = [...val];
2,381✔
1139
      } else {
1140
        // deduplicate
1141
        val.forEach(obj => {
232✔
1142
          if (!target[key].some(item => deepEqual(item, obj))) {
709✔
1143
            target[key].push(obj);
90✔
1144
          }
1145
        });
1146
      }
1147
    } else {
1148
      // direct assignment
1149
      target[key] = val;
22,344✔
1150
    }
1151
  }
1152
}
1153

1154
/**
1155
 * returns a hash of a string using a fast algorithm
1156
 * source: https://stackoverflow.com/a/52171480/845390
1157
 * @param str
1158
 * @param seed (optional)
1159
 * @returns {string}
1160
 */
1161
export function cyrb53Hash(str, seed = 0) {
132✔
1162
  // IE doesn't support imul
1163
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill
1164
  let imul = function(opA, opB) {
173✔
1165
    if (isFn(Math.imul)) {
13,572!
1166
      return Math.imul(opA, opB);
13,572✔
1167
    } else {
1168
      opB |= 0; // ensure that opB is an integer. opA will automatically be coerced.
×
1169
      // floating points give us 53 bits of precision to work with plus 1 sign bit
1170
      // automatically handled for our convienence:
1171
      // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001
1172
      //    0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
1173
      var result = (opA & 0x003fffff) * opB;
×
1174
      // 2. We can remove an integer coersion from the statement above because:
1175
      //    0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001
1176
      //    0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/
1177
      if (opA & 0xffc00000) result += (opA & 0xffc00000) * opB | 0;
×
1178
      return result | 0;
×
1179
    }
1180
  };
1181

1182
  let h1 = 0xdeadbeef ^ seed;
173✔
1183
  let h2 = 0x41c6ce57 ^ seed;
173✔
1184
  for (let i = 0, ch; i < str.length; i++) {
173✔
1185
    ch = str.charCodeAt(i);
6,440✔
1186
    h1 = imul(h1 ^ ch, 2654435761);
6,440✔
1187
    h2 = imul(h2 ^ ch, 1597334677);
6,440✔
1188
  }
1189
  h1 = imul(h1 ^ (h1 >>> 16), 2246822507) ^ imul(h2 ^ (h2 >>> 13), 3266489909);
173✔
1190
  h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909);
173✔
1191
  return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString();
173✔
1192
}
1193

1194
/**
1195
 * returns the result of `JSON.parse(data)`, or undefined if that throws an error.
1196
 * @param data
1197
 * @returns {any}
1198
 */
1199
export function safeJSONParse(data) {
1200
  try {
137✔
1201
    return JSON.parse(data);
137✔
1202
  } catch (e) {}
1203
}
1204

1205
export function safeJSONEncode(data) {
1206
  try {
481✔
1207
    return JSON.stringify(data);
481✔
1208
  } catch (e) {
1209
    return '';
1✔
1210
  }
1211
}
1212

1213
/**
1214
 * Returns a memoized version of `fn`.
1215
 *
1216
 * @param fn
1217
 * @param key cache key generator, invoked with the same arguments passed to `fn`.
1218
 *        By default, the first argument is used as key.
1219
 * @return {function(): any}
1220
 */
1221
export function memoize(fn, key = function (arg) { return arg; }) {
1,328✔
1222
  const cache = new Map();
331✔
1223
  const memoized = function () {
331✔
1224
    const cacheKey = key.apply(this, arguments);
2,033✔
1225
    if (!cache.has(cacheKey)) {
2,033✔
1226
      cache.set(cacheKey, fn.apply(this, arguments));
869✔
1227
    }
1228
    return cache.get(cacheKey);
2,033✔
1229
  }
1230
  memoized.clear = cache.clear.bind(cache);
331✔
1231
  return memoized;
331✔
1232
}
1233

1234
/**
1235
 * Returns a Unix timestamp for given time value and unit.
1236
 * @param {number} timeValue numeric value, defaults to 0 (which means now)
1237
 * @param {string} timeUnit defaults to days (or 'd'), use 'm' for minutes. Any parameter that isn't 'd' or 'm' will return Date.now().
1238
 * @returns {number}
1239
 */
1240
export function getUnixTimestampFromNow(timeValue = 0, timeUnit = 'd') {
1,417✔
1241
  const acceptableUnits = ['m', 'd'];
723✔
1242
  if (acceptableUnits.indexOf(timeUnit) < 0) {
723✔
1243
    return Date.now();
1✔
1244
  }
1245
  const multiplication = timeValue / (timeUnit === 'm' ? 1440 : 1);
722✔
1246
  return Date.now() + (timeValue && timeValue > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0);
722✔
1247
}
1248

1249
/**
1250
 * Converts given object into an array, so {key: 1, anotherKey: 'fred', third: ['fred']} is turned
1251
 * into [{key: 1}, {anotherKey: 'fred'}, {third: ['fred']}]
1252
 * @param {Object} obj the object
1253
 * @returns {Array}
1254
 */
1255
export function convertObjectToArray(obj) {
1256
  return Object.keys(obj).map(key => {
10✔
1257
    return {[key]: obj[key]};
28✔
1258
  });
1259
}
1260

1261
/**
1262
 * Sets dataset attributes on a script
1263
 * @param {HTMLScriptElement} script
1264
 * @param {object} attributes
1265
 */
1266
export function setScriptAttributes(script, attributes) {
1267
  Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v))
5✔
1268
}
1269

1270
/**
1271
 * Perform a binary search for `el` on an ordered array `arr`.
1272
 *
1273
 * @returns the lowest nonnegative integer I that satisfies:
1274
 *   key(arr[i]) >= key(el) for each i between I and arr.length
1275
 *
1276
 *   (if one or more matches are found for `el`, returns the index of the first;
1277
 *   if the element is not found, return the index of the first element that's greater;
1278
 *   if no greater element exists, return `arr.length`)
1279
 */
1280
export function binarySearch(arr, el, key = (el) => el) {
40✔
1281
  let left = 0;
54✔
1282
  let right = arr.length && arr.length - 1;
54✔
1283
  const target = key(el);
54✔
1284
  while (right - left > 1) {
54✔
1285
    const middle = left + Math.round((right - left) / 2);
14✔
1286
    if (target > key(arr[middle])) {
14✔
1287
      left = middle;
7✔
1288
    } else {
1289
      right = middle;
7✔
1290
    }
1291
  }
1292
  while (arr.length > left && target > key(arr[left])) {
54✔
1293
    left++;
16✔
1294
  }
1295
  return left;
54✔
1296
}
1297

1298
/**
1299
 * Checks if an object has non-serializable properties.
1300
 * Non-serializable properties are functions and RegExp objects.
1301
 *
1302
 * @param {Object} obj - The object to check.
1303
 * @param {Set} checkedObjects - A set of properties that have already been checked.
1304
 * @returns {boolean} - Returns true if the object has non-serializable properties, false otherwise.
1305
 */
1306
export function hasNonSerializableProperty(obj, checkedObjects = new Set()) {
100✔
1307
  for (const key in obj) {
112✔
1308
    const value = obj[key];
367✔
1309
    const type = typeof value;
367✔
1310

1311
    if (
367✔
1312
      value === undefined ||
3,305✔
1313
      type === 'function' ||
1314
      type === 'symbol' ||
1315
      value instanceof RegExp ||
1316
      value instanceof Map ||
1317
      value instanceof Set ||
1318
      value instanceof Date ||
1319
      (value !== null && type === 'object' && value.hasOwnProperty('toJSON'))
1320
    ) {
1321
      return true;
11✔
1322
    }
1323
    if (value !== null && type === 'object' && value.constructor === Object) {
356✔
1324
      if (checkedObjects.has(value)) {
12!
1325
        // circular reference, means we have a non-serializable property
1326
        return true;
×
1327
      }
1328
      checkedObjects.add(value);
12✔
1329
      if (hasNonSerializableProperty(value, checkedObjects)) {
12✔
1330
        return true;
8✔
1331
      }
1332
    }
1333
  }
1334
  return false;
93✔
1335
}
1336

1337
/**
1338
 * Returns the value of a nested property in an array of objects.
1339
 *
1340
 * @param {Array} collection - Array of objects.
1341
 * @param {String} key - Key of nested property.
1342
 * @returns {any|undefined} - Value of nested property.
1343
 */
1344
export function setOnAny(collection, key) {
1345
  for (let i = 0, result; i < collection.length; i++) {
657✔
1346
    result = deepAccess(collection[i], key);
818✔
1347
    if (result) {
818✔
1348
      return result;
90✔
1349
    }
1350
  }
1351
  return undefined;
567✔
1352
}
1353

1354
export function extractDomainFromHost(pageHost) {
1355
  let domain = null;
19✔
1356
  try {
19✔
1357
    let domains = /[-\w]+\.([-\w]+|[-\w]{3,}|[-\w]{1,3}\.[-\w]{2})$/i.exec(pageHost);
19✔
1358
    if (domains != null && domains.length > 0) {
19!
1359
      domain = domains[0];
19✔
1360
      for (let i = 1; i < domains.length; i++) {
19✔
1361
        if (domains[i].length > domain.length) {
19!
1362
          domain = domains[i];
×
1363
        }
1364
      }
1365
    }
1366
  } catch (e) {
1367
    domain = null;
×
1368
  }
1369
  return domain;
19✔
1370
}
1371

1372
export function triggerNurlWithCpm(bid, cpm) {
1373
  if (isStr(bid.nurl) && bid.nurl !== '') {
2!
1374
    bid.nurl = bid.nurl.replace(
2✔
1375
      /\${AUCTION_PRICE}/,
1376
      cpm
1377
    );
1378
    triggerPixel(bid.nurl);
2✔
1379
  }
1380
}
1381

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

1387
  return function () {
4✔
1388
    if (cachedResult !== undefined) {
×
1389
      return cachedResult; // Return cached result if already computed
×
1390
    }
1391

1392
    try {
×
1393
      if (typeof window.CompressionStream === 'undefined') {
×
1394
        cachedResult = false;
×
1395
      } else {
1396
        (() => new window.CompressionStream('gzip'))();
×
1397
        cachedResult = true;
×
1398
      }
1399
    } catch (error) {
1400
      cachedResult = false;
×
1401
    }
1402

1403
    return cachedResult;
×
1404
  };
1405
})();
1406

1407
// Make sure to use isGzipCompressionSupported before calling this function
1408
export async function compressDataWithGZip(data) {
1409
  if (typeof data !== 'string') { // TextEncoder (below) expects a string
2✔
1410
    data = JSON.stringify(data);
1✔
1411
  }
1412

1413
  const encoder = new TextEncoder();
2✔
1414
  const encodedData = encoder.encode(data);
2✔
1415
  const compressedStream = new Blob([encodedData])
2✔
1416
    .stream()
1417
    .pipeThrough(new window.CompressionStream('gzip'));
1418

1419
  const compressedBlob = await new Response(compressedStream).blob();
2✔
1420
  const compressedArrayBuffer = await compressedBlob.arrayBuffer();
2✔
1421
  return new Uint8Array(compressedArrayBuffer);
2✔
1422
}
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