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

prebid / Prebid.js / 18282755559

06 Oct 2025 01:37PM UTC coverage: 96.239% (-0.006%) from 96.245%
18282755559

push

github

web-flow
AdKernal tests: fix broken test related to DNT helper (#13973)

* Fix getDNT stubbing in utils and Adkernel tests

* Core: Proxy navigator DNT helper

* Core: Move DNT helper to dedicated library

* Core: Restore stub-friendly DNT re-export

51960 of 63588 branches covered (81.71%)

31 of 34 new or added lines in 10 files covered. (91.18%)

540 existing lines in 39 files now uncovered.

198985 of 206761 relevant lines covered (96.24%)

125.18 hits per line

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

94.64
/modules/adkernelBidAdapter.js
1
import { getDNT } from '../libraries/navigatorData/dnt.js';
1✔
2
import {
3
  _each,
4
  contains,
5
  createTrackPixelHtml,
6
  deepAccess,
7
  deepSetValue,
8
  getDefinedParams,
9
  isArray,
10
  isArrayOfNums,
11
  isEmpty,
12
  isNumber,
13
  isPlainObject,
14
  isStr,
15
  mergeDeep,
16
  parseGPTSingleSizeArrayToRtbSize,
17
  triggerPixel
18
} from '../src/utils.js';
19
import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
20
import {registerBidder} from '../src/adapters/bidderFactory.js';
21
import {config} from '../src/config.js';
22
import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js';
23
import {getBidFloor} from '../libraries/adkernelUtils/adkernelUtils.js'
24

25
/**
26
 * In case you're AdKernel whitelable platform's client who needs branded adapter to
27
 * work with Adkernel platform - DO NOT COPY THIS ADAPTER UNDER NEW NAME
28
 *
29
 * Please contact prebid@adkernel.com and we'll add your adapter as an alias
30
 * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
31
 * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest
32
 * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync
33
 */
34

35
const VIDEO_PARAMS = ['pos', 'context', 'placement', 'plcmt', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration',
1✔
36
  'startdelay', 'linearity', 'skip', 'skipmin', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackend', 'boxingallowed'];
37
const VIDEO_FPD = ['battr', 'pos'];
1✔
38
const NATIVE_FPD = ['battr', 'api'];
1✔
39
const BANNER_PARAMS = ['pos'];
1✔
40
const BANNER_FPD = ['btype', 'battr', 'pos', 'api'];
1✔
41
const VERSION = '1.8';
1✔
42
const SYNC_IFRAME = 1;
1✔
43
const SYNC_IMAGE = 2;
1✔
44
const SYNC_TYPES = {
1✔
45
  1: 'iframe',
46
  2: 'image'
47
};
48
const GVLID = 14;
1✔
49

50
const MULTI_FORMAT_SUFFIX = '__mf';
1✔
51
const MULTI_FORMAT_SUFFIX_BANNER = 'b' + MULTI_FORMAT_SUFFIX;
1✔
52
const MULTI_FORMAT_SUFFIX_VIDEO = 'v' + MULTI_FORMAT_SUFFIX;
1✔
53
const MULTI_FORMAT_SUFFIX_NATIVE = 'n' + MULTI_FORMAT_SUFFIX;
1✔
54

55
const MEDIA_TYPES = {
1✔
56
  BANNER: 1,
57
  VIDEO: 2,
58
  NATIVE: 4
59
};
60

61
/**
62
 * Adapter for requesting bids from AdKernel white-label display platform
63
 */
64
export const spec = {
1✔
65
  code: 'adkernel',
66
  gvlid: GVLID,
67
  aliases: [
68
    {code: 'headbidding'},
69
    {code: 'adsolut'},
70
    {code: 'oftmediahb'},
71
    {code: 'audiencemedia'},
72
    {code: 'waardex_ak'},
73
    {code: 'roqoon'},
74
    {code: 'adbite'},
75
    {code: 'houseofpubs'},
76
    {code: 'torchad'},
77
    {code: 'stringads'},
78
    {code: 'bcm'},
79
    {code: 'engageadx'},
80
    {code: 'converge', gvlid: 248},
81
    {code: 'adomega'},
82
    {code: 'denakop'},
83
    {code: 'rtbanalytica'},
84
    {code: 'unibots'},
85
    {code: 'ergadx'},
86
    {code: 'turktelekom'},
87
    {code: 'motionspots'},
88
    {code: 'sonic_twist'},
89
    {code: 'displayioads'},
90
    {code: 'rtbdemand_com'},
91
    {code: 'bidbuddy'},
92
    {code: 'didnadisplay'},
93
    {code: 'qortex'},
94
    {code: 'adpluto'},
95
    {code: 'headbidder'},
96
    {code: 'digiad'},
97
    {code: 'monetix'},
98
    {code: 'hyperbrainz'},
99
    {code: 'voisetech'},
100
    {code: 'global_sun'},
101
    {code: 'rxnetwork'},
102
    {code: 'revbid'},
103
    {code: 'spinx', gvlid: 1308},
104
    {code: 'oppamedia'},
105
    {code: 'pixelpluses', gvlid: 1209},
106
    {code: 'urekamedia'},
107
    {code: 'smartyexchange'},
108
    {code: 'infinety'}
109
  ],
110
  supportedMediaTypes: [BANNER, VIDEO, NATIVE],
111

112
  /**
113
   * Validates bid request for adunit
114
   * @param bidRequest {BidRequest}
115
   * @returns {boolean}
116
   */
117
  isBidRequestValid: function (bidRequest) {
118
    return 'params' in bidRequest &&
5✔
119
      typeof bidRequest.params.host !== 'undefined' &&
120
      'zoneId' in bidRequest.params &&
121
      !isNaN(Number(bidRequest.params.zoneId)) &&
122
      bidRequest.params.zoneId > 0 &&
123
      bidRequest.mediaTypes &&
124
      (bidRequest.mediaTypes.banner || bidRequest.mediaTypes.video ||
125
        (bidRequest.mediaTypes.native && validateNativeAdUnit(bidRequest.mediaTypes.native))
126
      );
127
  },
128

129
  /**
130
   * Builds http request for each unique combination of adkernel host/zone
131
   * @param bidRequests {BidRequest[]}
132
   * @param bidderRequest {BidderRequest}
133
   * @returns {ServerRequest[]}
134
   */
135
  buildRequests: function (bidRequests, bidderRequest) {
136
    const impGroups = groupImpressionsByHostZone(bidRequests, bidderRequest.refererInfo);
25✔
137
    const requests = [];
25✔
138
    const schain = bidRequests[0]?.ortb2?.source?.ext?.schain;
25!
139
    _each(impGroups, impGroup => {
25✔
140
      const {host, zoneId, imps} = impGroup;
27✔
141
      const request = buildRtbRequest(imps, bidderRequest, schain);
27✔
142
      requests.push({
27✔
143
        method: 'POST',
144
        url: `https://${host}/hb?zone=${zoneId}&v=${VERSION}`,
145
        data: JSON.stringify(request)
146
      });
147
    });
148
    return requests;
25✔
149
  },
150

151
  /**
152
   * Parse response from adkernel backend
153
   * @param serverResponse {ServerResponse}
154
   * @param serverRequest {ServerRequest}
155
   * @returns {Bid[]}
156
   */
157
  interpretResponse: function (serverResponse, serverRequest) {
158
    const response = serverResponse.body;
8✔
159
    if (!response.seatbid) {
8✔
160
      return [];
1✔
161
    }
162

163
    const rtbRequest = JSON.parse(serverRequest.data);
7✔
164
    const rtbBids = response.seatbid
7✔
165
      .map(seatbid => seatbid.bid)
7✔
166
      .reduce((a, b) => a.concat(b), []);
7✔
167

168
    return rtbBids.map(rtbBid => {
7✔
169
      const imp = ((rtbRequest.imp) || []).find(imp => imp.id === rtbBid.impid);
9!
170
      const prBid = {
8✔
171
        requestId: rtbBid.impid,
172
        cpm: rtbBid.price,
173
        creativeId: rtbBid.crid,
174
        currency: response.cur || 'USD',
13✔
175
        ttl: 360,
176
        netRevenue: true
177
      };
178
      if (prBid.requestId.endsWith(MULTI_FORMAT_SUFFIX)) {
8✔
179
        prBid.requestId = stripMultiformatSuffix(prBid.requestId);
2✔
180
      }
181
      if (rtbBid.mtype === MEDIA_TYPES.BANNER) {
8✔
182
        prBid.mediaType = BANNER;
3✔
183
        prBid.width = rtbBid.w;
3✔
184
        prBid.height = rtbBid.h;
3✔
185
        prBid.ad = formatAdMarkup(rtbBid);
3✔
186
      } else if (rtbBid.mtype === MEDIA_TYPES.VIDEO) {
5✔
187
        prBid.mediaType = VIDEO;
4✔
188
        if (rtbBid.adm) {
4✔
189
          prBid.vastXml = rtbBid.adm;
2✔
190
          if (rtbBid.nurl) {
2✔
191
            prBid.nurl = rtbBid.nurl;
2✔
192
          }
193
        } else {
194
          prBid.vastUrl = rtbBid.nurl;
2✔
195
        }
196
        prBid.width = imp.video.w;
4✔
197
        prBid.height = imp.video.h;
4✔
198
      } else if (rtbBid.mtype === MEDIA_TYPES.NATIVE) {
1✔
199
        prBid.mediaType = NATIVE;
1✔
200
        prBid.native = {
1✔
201
          ortb: buildNativeAd(rtbBid.adm)
202
        };
203
      }
204
      if (isStr(rtbBid.dealid)) {
8✔
205
        prBid.dealId = rtbBid.dealid;
2✔
206
      }
207
      if (isArray(rtbBid.adomain)) {
8✔
208
        deepSetValue(prBid, 'meta.advertiserDomains', rtbBid.adomain);
1✔
209
      }
210
      if (isArray(rtbBid.cat)) {
8✔
211
        deepSetValue(prBid, 'meta.secondaryCatIds', rtbBid.cat);
1✔
212
      }
213
      if (isPlainObject(rtbBid.ext)) {
8✔
214
        if (isNumber(rtbBid.ext.advertiser_id)) {
1✔
215
          deepSetValue(prBid, 'meta.advertiserId', rtbBid.ext.advertiser_id);
1✔
216
        }
217
        if (isStr(rtbBid.ext.advertiser_name)) {
1✔
218
          deepSetValue(prBid, 'meta.advertiserName', rtbBid.ext.advertiser_name);
1✔
219
        }
220
        if (isStr(rtbBid.ext.agency_name)) {
1✔
221
          deepSetValue(prBid, 'meta.agencyName', rtbBid.ext.agency_name);
1✔
222
        }
223
      }
224

225
      return prBid;
8✔
226
    });
227
  },
228

229
  /**
230
   * Extracts user-syncs information from server response
231
   * @param syncOptions {SyncOptions}
232
   * @param serverResponses {ServerResponse[]}
233
   * @returns {UserSync[]}
234
   */
235
  getUserSyncs: function (syncOptions, serverResponses) {
236
    if (!serverResponses || serverResponses.length === 0 || (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled)) {
4✔
237
      return [];
2✔
238
    }
239
    return serverResponses.filter(rsp => rsp.body && rsp.body.ext && rsp.body.ext.adk_usersync)
2✔
240
      .map(rsp => rsp.body.ext.adk_usersync)
2✔
241
      .reduce((a, b) => a.concat(b), [])
2✔
242
      .map(({url, type}) => ({type: SYNC_TYPES[type], url: url}));
2✔
243
  },
244

245
  /**
246
   * Handle bid win
247
   * @param bid {Bid}
248
   */
249
  onBidWon: function (bid) {
250
    if (bid.nurl) {
1✔
251
      triggerPixel(bid.nurl);
1✔
252
    }
253
  }
254
};
255

256
registerBidder(spec);
1✔
257

258
/**
259
 * Dispatch impressions by ad network host and zone
260
 * @param bidRequests {BidRequest[]}
261
 * @param refererInfo {refererInfo}
262
 */
263
function groupImpressionsByHostZone(bidRequests, refererInfo) {
264
  const secure = (refererInfo && refererInfo.page?.indexOf('https:') === 0);
25!
265
  return Object.values(
25✔
266
    bidRequests.map(bidRequest => buildImps(bidRequest, secure))
27✔
267
      .reduce((acc, curr, index) => {
268
        const bidRequest = bidRequests[index];
27✔
269
        const {zoneId, host} = bidRequest.params;
27✔
270
        const key = `${host}_${zoneId}`;
27✔
271
        acc[key] = acc[key] || {host: host, zoneId: zoneId, imps: []};
27✔
272
        acc[key].imps.push(...curr);
27✔
273
        return acc;
27✔
274
      }, {})
275
  );
276
}
277

278
/**
279
 *  Builds rtb imp object(s) for single adunit
280
 *  @param bidRequest {BidRequest}
281
 *  @param secure {boolean}
282
 */
283
function buildImps(bidRequest, secure) {
284
  const imp = {
27✔
285
    'id': bidRequest.bidId,
286
    'tagid': bidRequest.adUnitCode
287
  };
288
  if (secure) {
27✔
289
    imp.secure = bidRequest.ortb2Imp?.secure ?? 1;
27!
290
  }
291
  var sizes = [];
27✔
292
  const mediaTypes = bidRequest.mediaTypes;
27✔
293
  const isMultiformat = (~~!!mediaTypes?.banner + ~~!!mediaTypes?.video + ~~!!mediaTypes?.native) > 1;
27✔
294
  const result = [];
27✔
295
  let typedImp;
296

297
  if (mediaTypes?.banner) {
27✔
298
    if (isMultiformat) {
21✔
299
      typedImp = {...imp};
1✔
300
      typedImp.id = imp.id + MULTI_FORMAT_SUFFIX_BANNER;
1✔
301
    } else {
302
      typedImp = imp;
20✔
303
    }
304
    sizes = getAdUnitSizes(bidRequest);
21✔
305
    const pbBanner = mediaTypes.banner;
21✔
306
    typedImp.banner = {
21✔
307
      ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, BANNER_FPD),
308
      ...getDefinedParamsOrEmpty(pbBanner, BANNER_PARAMS),
309
      format: sizes.map(wh => parseGPTSingleSizeArrayToRtbSize(wh)),
39✔
310
      topframe: 0
311
    };
312
    initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : BANNER);
21✔
313
    result.push(typedImp);
21✔
314
  }
315

316
  if (mediaTypes?.video) {
27✔
317
    if (isMultiformat) {
5✔
318
      typedImp = {...imp};
1✔
319
      typedImp.id = typedImp.id + MULTI_FORMAT_SUFFIX_VIDEO;
1✔
320
    } else {
321
      typedImp = imp;
4✔
322
    }
323
    const pbVideo = mediaTypes.video;
5✔
324
    typedImp.video = {
5✔
325
      ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, VIDEO_FPD),
326
      ...getDefinedParamsOrEmpty(pbVideo, VIDEO_PARAMS)
327
    };
328
    if (pbVideo.playerSize) {
5!
329
      sizes = pbVideo.playerSize[0];
5✔
330
      typedImp.video = Object.assign(typedImp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {});
5!
331
    } else if (pbVideo.w && pbVideo.h) {
×
332
      typedImp.video.w = pbVideo.w;
×
UNCOV
333
      typedImp.video.h = pbVideo.h;
×
334
    }
335
    initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : VIDEO);
5✔
336
    result.push(typedImp);
5✔
337
  }
338

339
  if (mediaTypes?.native) {
27✔
340
    if (isMultiformat) {
2!
341
      typedImp = {...imp};
×
UNCOV
342
      typedImp.id = typedImp.id + MULTI_FORMAT_SUFFIX_NATIVE;
×
343
    } else {
344
      typedImp = imp;
2✔
345
    }
346
    typedImp.native = {
2✔
347
      ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, NATIVE_FPD),
348
      request: JSON.stringify(bidRequest.nativeOrtbRequest)
349
    };
350
    initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : NATIVE);
2!
351
    result.push(typedImp);
2✔
352
  }
353
  return result;
27✔
354
}
355

356
function initImpBidfloor(imp, bid, sizes, mediaType) {
357
  const bidfloor = getBidFloor(bid, mediaType, sizes);
28✔
358
  if (bidfloor) {
28✔
359
    imp.bidfloor = bidfloor;
1✔
360
  }
361
}
362

363
function getDefinedParamsOrEmpty(object, params) {
364
  if (object === undefined) {
54✔
365
    return {};
11✔
366
  }
367
  return getDefinedParams(object, params);
43✔
368
}
369

370
/**
371
 * Checks if configuration allows specified sync method
372
 * @param syncRule {Object}
373
 * @param bidderCode {string}
374
 * @returns {boolean}
375
 */
376
function isSyncMethodAllowed(syncRule, bidderCode) {
377
  if (!syncRule) {
7✔
378
    return false;
2✔
379
  }
380
  const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode];
5✔
381
  const rule = syncRule.filter === 'include';
5✔
382
  return contains(bidders, bidderCode) === rule;
5✔
383
}
384

385
/**
386
 * Get preferred user-sync method based on publisher configuration
387
 * @param bidderCode {string}
388
 * @returns {number|undefined}
389
 */
390
function getAllowedSyncMethod(bidderCode) {
391
  if (!config.getConfig('userSync.syncEnabled')) {
27✔
392
    return;
24✔
393
  }
394
  const filterConfig = config.getConfig('userSync.filterSettings');
3✔
395
  if (isSyncMethodAllowed(filterConfig.all, bidderCode) || isSyncMethodAllowed(filterConfig.iframe, bidderCode)) {
3✔
396
    return SYNC_IFRAME;
1✔
397
  } else if (isSyncMethodAllowed(filterConfig.image, bidderCode)) {
2✔
398
    return SYNC_IMAGE;
1✔
399
  }
400
}
401

402
/**
403
 * Create device object from fpd and host-collected data
404
 * @param fpd {Object}
405
 * @returns {{device: Object}}
406
 */
407
function makeDevice(fpd) {
408
  const device = mergeDeep({
27✔
409
    'ip': 'caller',
410
    'ipv6': 'caller',
411
    'ua': 'caller',
412
    'js': 1,
413
    'language': getLanguage()
414
  }, fpd.device || {});
54✔
415
  if (getDNT()) {
27✔
416
    device.dnt = 1;
26✔
417
  }
418
  return {device: device};
27✔
419
}
420

421
/**
422
 * Create site or app description object
423
 * @param bidderRequest {BidderRequest}
424
 * @param fpd {Object}
425
 * @returns {{site: Object}|{app: Object}}
426
 */
427
function makeSiteOrApp(bidderRequest, fpd) {
428
  const {refererInfo} = bidderRequest;
27✔
429
  const appConfig = config.getConfig('app');
27✔
430
  if (isEmpty(appConfig)) {
27!
431
    return {site: createSite(refererInfo, fpd)}
27✔
432
  } else {
UNCOV
433
    return {app: appConfig};
×
434
  }
435
}
436

437
/**
438
 * Create user description object
439
 * @param bidderRequest {BidderRequest}
440
 * @param fpd {Object}
441
 * @returns {{user: Object} | undefined}
442
 */
443
function makeUser(bidderRequest, fpd) {
444
  const {gdprConsent} = bidderRequest;
27✔
445
  const user = fpd.user || {};
27✔
446
  if (gdprConsent && gdprConsent.consentString !== undefined) {
27✔
447
    deepSetValue(user, 'ext.consent', gdprConsent.consentString);
1✔
448
  }
449
  const eids = getExtendedUserIds(bidderRequest);
27✔
450
  if (eids) {
27✔
451
    deepSetValue(user, 'ext.eids', eids);
1✔
452
  }
453
  if (!isEmpty(user)) {
27✔
454
    return {user: user};
2✔
455
  }
456
}
457

458
/**
459
 * Create privacy regulations object
460
 * @param bidderRequest {BidderRequest}
461
 * @returns {{regs: Object} | undefined}
462
 */
463
function makeRegulations(bidderRequest) {
464
  const {gdprConsent, uspConsent, gppConsent} = bidderRequest;
27✔
465
  const regs = {};
27✔
466
  if (gdprConsent) {
27✔
467
    if (gdprConsent.gdprApplies !== undefined) {
2✔
468
      deepSetValue(regs, 'regs.ext.gdpr', ~~gdprConsent.gdprApplies);
2✔
469
    }
470
  }
471
  if (gppConsent) {
27✔
472
    deepSetValue(regs, 'regs.gpp', gppConsent.gppString);
1✔
473
    deepSetValue(regs, 'regs.gpp_sid', gppConsent.applicableSections);
1✔
474
  }
475
  if (uspConsent) {
27✔
476
    deepSetValue(regs, 'regs.ext.us_privacy', uspConsent);
1✔
477
  }
478
  if (config.getConfig('coppa')) {
27✔
479
    deepSetValue(regs, 'regs.coppa', 1);
1✔
480
  }
481
  if (!isEmpty(regs)) {
27✔
482
    return regs;
3✔
483
  }
484
}
485

486
/**
487
 * Create top-level request object
488
 * @param bidderRequest {BidderRequest}
489
 * @param imps {Object} Impressions
490
 * @param fpd {Object} First party data
491
 * @returns
492
 */
493
function makeBaseRequest(bidderRequest, imps, fpd) {
494
  const request = {
27✔
495
    'id': bidderRequest.bidderRequestId,
496
    'imp': imps,
497
    'at': 1,
498
    'tmax': parseInt(bidderRequest.timeout)
499
  };
500
  if (!isEmpty(fpd.bcat)) {
27!
UNCOV
501
    request.bcat = fpd.bcat;
×
502
  }
503
  if (!isEmpty(fpd.badv)) {
27!
UNCOV
504
    request.badv = fpd.badv;
×
505
  }
506
  return request;
27✔
507
}
508

509
/**
510
 * Initialize sync capabilities
511
 * @param bidderRequest {BidderRequest}
512
 */
513
function makeSyncInfo(bidderRequest) {
514
  const {bidderCode} = bidderRequest;
27✔
515
  const syncMethod = getAllowedSyncMethod(bidderCode);
27✔
516
  if (syncMethod) {
27✔
517
    const res = {};
2✔
518
    deepSetValue(res, 'ext.adk_usersync', syncMethod);
2✔
519
    return res;
2✔
520
  }
521
}
522

523
/**
524
 * Builds complete rtb request
525
 * @param imps {Object} Collection of rtb impressions
526
 * @param bidderRequest {BidderRequest}
527
 * @param schain {Object=} Supply chain config
528
 * @return {Object} Complete rtb request
529
 */
530
function buildRtbRequest(imps, bidderRequest, schain) {
531
  const fpd = bidderRequest.ortb2 || {};
27✔
532

533
  const req = mergeDeep(
27✔
534
    makeBaseRequest(bidderRequest, imps, fpd),
535
    makeDevice(fpd),
536
    makeSiteOrApp(bidderRequest, fpd),
537
    makeUser(bidderRequest, fpd),
538
    makeRegulations(bidderRequest),
539
    makeSyncInfo(bidderRequest)
540
  );
541
  if (schain) {
27!
UNCOV
542
    deepSetValue(req, 'source.ext.schain', schain);
×
543
  }
544
  return req;
27✔
545
}
546

547
/**
548
 * Get browser language
549
 * @returns {String}
550
 */
551
function getLanguage() {
552
  const language = navigator.language ? 'language' : 'userLanguage';
27!
553
  return navigator[language].split('-')[0];
27✔
554
}
555

556
/**
557
 * Creates site description object
558
 */
559
function createSite(refInfo, fpd) {
560
  const site = {
27✔
561
    'domain': refInfo.domain,
562
    'page': refInfo.page
563
  };
564
  mergeDeep(site, fpd.site);
27✔
565
  if (refInfo.ref != null) {
27!
UNCOV
566
    site.ref = refInfo.ref;
×
567
  } else {
568
    delete site.ref;
27✔
569
  }
570
  return site;
27✔
571
}
572

573
function getExtendedUserIds(bidderRequest) {
574
  const eids = deepAccess(bidderRequest, 'bids.0.userIdAsEids');
27✔
575
  if (isArray(eids)) {
27✔
576
    return eids;
1✔
577
  }
578
}
579

580
/**
581
 *  Format creative with optional nurl call
582
 *  @param bid rtb Bid object
583
 */
584
function formatAdMarkup(bid) {
585
  let adm = bid.adm;
3✔
586
  if ('nurl' in bid) {
3✔
587
    adm += createTrackPixelHtml(`${bid.nurl}&px=1`);
3✔
588
  }
589
  return adm;
3✔
590
}
591

592
/**
593
 * Basic validates to comply with platform requirements
594
 */
595
function validateNativeAdUnit(adUnit) {
596
  return validateNativeImageSize(adUnit.image) && validateNativeImageSize(adUnit.icon) &&
1✔
597
    !deepAccess(adUnit, 'privacyLink.required') && // not supported yet
598
    !deepAccess(adUnit, 'privacyIcon.required'); // not supported yet
599
}
600

601
/**
602
 * Validates image asset size definition
603
 */
604
function validateNativeImageSize(img) {
605
  if (!img) {
2!
UNCOV
606
    return true;
×
607
  }
608
  if (img.sizes) {
2✔
609
    return isArrayOfNums(img.sizes, 2);
1✔
610
  }
611
  if (isArray(img.aspect_ratios)) {
1✔
612
    return img.aspect_ratios.length > 0 && img.aspect_ratios[0].min_height && img.aspect_ratios[0].min_width;
1✔
613
  }
UNCOV
614
  return true;
×
615
}
616

617
/**
618
 * Creates native ad for native 1.2 response
619
 */
620
function buildNativeAd(adm) {
621
  let resp = JSON.parse(adm);
1✔
622
  // temporary workaround for top-level native object wrapper
623
  if ('native' in resp) {
1✔
624
    resp = resp.native;
1✔
625
  }
626
  return resp;
1✔
627
}
628

629
function stripMultiformatSuffix(impid) {
630
  return impid.substr(0, impid.length - MULTI_FORMAT_SUFFIX.length - 1);
2✔
631
}
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