• 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

78.04
/modules/mobkoiAnalyticsAdapter.js
1
import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
1✔
2
import adapterManager from '../src/adapterManager.js';
3
import { EVENTS } from '../src/constants.js';
4
import { ajax } from '../src/ajax.js';
5
import {
6
  logInfo,
7
  logWarn,
8
  logError,
9
  _each,
10
  pick,
11
  triggerPixel,
12
  debugTurnedOn,
13
  mergeDeep,
14
  isEmpty,
15
  deepClone,
16
  deepAccess,
17
} from '../src/utils.js';
18

19
const BIDDER_CODE = 'mobkoi';
1✔
20
const analyticsType = 'endpoint';
1✔
21
const GVL_ID = 898;
1✔
22
/**
23
 * !IMPORTANT: Must match the value in the mobkoiBidAdapter.js
24
 * The name of the parameter that the publisher can use to specify the ad server endpoint.
25
 */
26
const PARAM_NAME_AD_SERVER_BASE_URL = 'adServerBaseUrl';
1✔
27

28
/**
29
 * Order by events lifecycle
30
 */
31
const {
32
  // Order events
33
  AUCTION_INIT,
34
  BID_RESPONSE,
35
  AUCTION_END,
36
  AD_RENDER_SUCCEEDED,
37
  BID_WON,
38
  BIDDER_DONE,
39

40
  // Error events (Not in order)
41
  AUCTION_TIMEOUT,
42
  NO_BID,
43
  BID_REJECTED,
44
  BIDDER_ERROR,
45
  AD_RENDER_FAILED,
46
} = EVENTS;
1✔
47

48
const CUSTOM_EVENTS = {
1✔
49
  BID_LOSS: 'bidLoss',
50
};
51

52
export const DEBUG_EVENT_LEVELS = {
1✔
53
  info: 'info',
54
  warn: 'warn',
55
  error: 'error',
56
};
57

58
/**
59
 * Some fields contain large data that are not useful for debugging. This
60
 * constant contains the fields that should be omitted from the payload and in
61
 * error messages.
62
 */
63
const COMMON_FIELDS_TO_OMIT = ['ad', 'adm'];
1✔
64

65
export class LocalContext {
66
  /**
67
   * A map of impression ID (ORTB terms) to BidContext object
68
   */
69
  bidContexts = {};
1✔
70

71
  /**
72
   * Shouldn't be accessed directly. Use getPayloadByImpId method instead.
73
   * Payload are indexed by impression ID.
74
   */
75
  _impressionPayloadCache = {
1✔
76
    // [impid]: { ... }
77
  };
78
  /**
79
   * The payload that is common to all bid contexts. The payload will be
80
   * submitted to the server along with the debug events.
81
   */
82
  getImpressionPayload(impid) {
83
    if (!impid) {
29!
84
      throw new Error(`Impression ID is required. Given: "${impid}".`);
×
85
    }
86

87
    return this._impressionPayloadCache[impid] || {};
29!
88
  }
89
  /**
90
   * Update the payload for all impressions. The new values will be merged to
91
   * the existing payload.
92
   * @param {*} subPayloads Object containing new values to be merged indexed by SUB_PAYLOAD_TYPES
93
   */
94
  mergeToAllImpressionsPayload(subPayloads) {
95
    // Create clone for each impression ID and update the payload cache
96
    _each(this.getAllBidderRequestImpIds(), currentImpid => {
29✔
97
      // Avoid modifying the original object
98
      const cloneSubPayloads = deepClone(subPayloads);
29✔
99

100
      // Initialise the payload cache if it doesn't exist
101
      if (!this._impressionPayloadCache[currentImpid]) {
29✔
102
        this._impressionPayloadCache[currentImpid] = {};
1✔
103
      }
104

105
      // Merge the new values to the existing payload
106
      utils.mergePayloadAndAddCustomFields(
29✔
107
        this._impressionPayloadCache[currentImpid],
108
        cloneSubPayloads,
109
        // Add the identity fields to all sub payloads
110
        {
111
          impid: currentImpid,
112
          publisherId: this.publisherId,
113
        }
114
      );
115
    });
116
  }
117

118
  /**
119
   * The Prebid auction object but only contains the key fields that we
120
   * interested in.
121
   */
122
  auction = null;
1✔
123

124
  /**
125
   * Auction.bidderRequests object
126
   */
127
  bidderRequests = null;
1✔
128

129
  get publisherId() {
130
    if (!this.bidderRequests) {
110!
131
      throw new Error('Bidder requests are not available. Accessing before assigning.');
×
132
    }
133
    return utils.getPublisherId(this.bidderRequests[0]);
110✔
134
  }
135

136
  get adServerBaseUrl() {
137
    if (
7!
138
      !Array.isArray(this.bidderRequests) &&
7!
139
      this.bidderRequests.length > 0
140
    ) {
141
      throw new Error('Bidder requests are not available. Accessing before assigning.' +
×
142
        JSON.stringify(this.bidderRequests, null, 2)
143
      );
144
    }
145

146
    return utils.getAdServerEndpointBaseUrl(this.bidderRequests[0]);
7✔
147
  }
148

149
  /**
150
   * Extract all impression IDs from all bid requests.
151
   */
152
  getAllBidderRequestImpIds() {
153
    if (!Array.isArray(this.bidderRequests)) {
58!
154
      return [];
×
155
    }
156
    return this.bidderRequests.flatMap(br => br.bids.map(bid => utils.getImpId(bid)));
58✔
157
  }
158

159
  /**
160
   * Cache the debug events that are common to all bid contexts.
161
   * When a new bid context is created, the events will be pushed to the new
162
   * context.
163
   */
164
  commonBidContextEvents = [];
1✔
165

166
  initialise(auction) {
167
    this.auction = pick(auction, ['auctionId', 'auctionEnd']);
12✔
168
    this.bidderRequests = auction.bidderRequests;
12✔
169
  }
170

171
  /**
172
   * Retrieve the BidContext object by the bid object. If the bid context is not
173
   * available, it will create a new one. The new bid context will returned.
174
   * @param {*} bid can be a prebid bid response or ortb bid response
175
   * @returns BidContext object
176
   */
177
  retrieveBidContext(bid) {
178
    const ortbId = (() => {
19✔
179
      try {
19✔
180
        const id = utils.getOrtbId(bid);
19✔
181
        if (!id) {
19!
182
          throw new Error(
×
183
            'ORTB ID is not available in the given bid object:' +
184
            JSON.stringify(utils.omitRecursive(bid, COMMON_FIELDS_TO_OMIT), null, 2));
185
        }
186
        return id;
19✔
187
      } catch (error) {
188
        throw new Error(
×
189
          'Failed to retrieve ORTB ID from bid object. Please ensure the given object contains an ORTB ID field.\n' +
190
          `Sub Error: ${error.message}`
191
        );
192
      }
193
    })();
194
    const bidContext = this.bidContexts[ortbId];
19✔
195

196
    if (bidContext) {
19✔
197
      return bidContext;
18✔
198
    }
199

200
    /**
201
     * Create a new context object and return it.
202
     */
203
    let newBidContext = new BidContext({
1✔
204
      localContext: this,
205
      prebidOrOrtbBidResponse: bid,
206
    });
207

208
    /**
209
     * Add the data that store in local context to the new bid context.
210
     */
211
    _each(
1✔
212
      this.commonBidContextEvents,
213
      event => newBidContext.pushEvent(
1✔
214
        {
215
          eventInstance: event,
216
          subPayloads: null, // Merge the payload later
217
        })
218
    );
219
    // Merge cached payloads to the new bid context
220
    newBidContext.mergePayload(this.getImpressionPayload(newBidContext.impid));
1✔
221

222
    this.bidContexts[ortbId] = newBidContext;
1✔
223
    return newBidContext;
1✔
224
  }
225

226
  /**
227
   * Immediately trigger the loss beacon for all bids (bid contexts) that haven't won the auction.
228
   */
229
  triggerAllLossBidLossBeacon() {
230
    _each(this.bidContexts, (bidContext) => {
12✔
231
      const { ortbBidResponse, bidWin, lurlTriggered } = bidContext;
12✔
232
      if (ortbBidResponse.lurl && !bidWin && !lurlTriggered) {
12✔
233
        logInfo('TriggerLossBeacon. impid:', ortbBidResponse.impid);
1✔
234
        utils.sendGetRequest(ortbBidResponse.lurl);
1✔
235
        // Update the flog. Don't wait for the response to continue to avoid race conditions
236
        bidContext.lurlTriggered = true;
1✔
237
      }
238
    });
239
  }
240

241
  /**
242
   * Push an debug event to all bid contexts. This is useful for events that are
243
   * related to all bids in the auction.
244
   * @param {Object} params Object containing the event details
245
   * @param {*} params.eventType Prebid event type or custom event type
246
   * @param {*} params.level Debug level of the event. It can be one of the following:
247
   * - info
248
   * - warn
249
   * - error
250
   * @param {*} params.timestamp Default to current timestamp if not provided.
251
   * @param {*} params.note Optional field. Additional information about the event.
252
   * @param {*} params.subPayloads Objects containing additional data that are
253
   * obtain from to the Prebid events indexed by SUB_PAYLOAD_TYPES.
254
   */
255
  pushEventToAllBidContexts({eventType, level, timestamp, note, subPayloads}) {
256
    // Create one event for each impression ID
257
    _each(this.getAllBidderRequestImpIds(), impid => {
29✔
258
      const eventClone = new Event({
29✔
259
        eventType,
260
        impid,
261
        publisherId: this.publisherId,
262
        level,
263
        timestamp,
264
        note,
265
      });
266
      // Save to the LocalContext
267
      this.commonBidContextEvents.push(eventClone);
29✔
268
      this.mergeToAllImpressionsPayload(subPayloads);
29✔
269
    });
270

271
    // If there are no bid contexts, push the event to the common events list
272
    if (isEmpty(this.bidContexts)) {
29✔
273
      this._commonBidContextEventsFlushed = false;
1✔
274
      return;
1✔
275
    }
276

277
    // Once the bid contexts are available, push the event to all bid contexts
278
    _each(this.bidContexts, (bidContext) => {
28✔
279
      bidContext.pushEvent({
28✔
280
        eventInstance: new Event({
281
          eventType,
282
          impid: bidContext.impid,
283
          publisherId: this.publisherId,
284
          level,
285
          timestamp,
286
          note,
287
        }),
288
        subPayloads: this.getImpressionPayload(bidContext.impid),
289
      });
290
    });
291
  }
292

293
  /**
294
   * A flag to indicate if the common events have been flushed to the server.
295
   * This is useful to avoid submitting the same events multiple times.
296
   */
297
  _commonBidContextEventsFlushed = false;
1✔
298

299
  /**
300
   * Flush all debug events in all bid contexts as well as the common events (in
301
   * Local Context) to the server.
302
   */
303
  async flushAllDebugEvents() {
304
    if (this.commonBidContextEvents.length < 0 && isEmpty(this.bidContexts)) {
7!
305
      logInfo('No debug events to flush');
×
306
      return;
×
307
    }
308

309
    const flushPromises = [];
7✔
310
    const debugEndpoint = `${this.adServerBaseUrl}/debug`;
7✔
311

312
    // If there are no bid contexts, and there are error events, submit the
313
    // common events to the server
314
    if (
7!
315
      isEmpty(this.bidContexts) &&
7!
316
      !this._commonBidContextEventsFlushed &&
317
      this.commonBidContextEvents.some(
318
        event => event.level === DEBUG_EVENT_LEVELS.error ||
×
319
          event.level === DEBUG_EVENT_LEVELS.warn
320
      )
321
    ) {
322
      logInfo('Flush common events to the server');
×
323
      const debugReports = this.bidderRequests.flatMap(currentBidderRequest => {
×
324
        return currentBidderRequest.bids.map(bid => {
×
325
          const impid = utils.getImpId(bid);
×
326
          return {
×
327
            impid: impid,
328
            events: this.commonBidContextEvents,
329
            bidWin: null,
330
            // Unroll the payload object to the top level to make it easier for
331
            // Grafana to process the data.
332
            ...this.getImpressionPayload(impid),
333
          };
334
        });
335
      });
336

337
      _each(debugReports, debugReport => {
×
338
        flushPromises.push(utils.postAjax(
×
339
          debugEndpoint,
340
          debugReport
341
        ));
342
      });
343

344
      this._commonBidContextEventsFlushed = true;
×
345
    }
346

347
    flushPromises.push(
7✔
348
      ...Object.values(this.bidContexts)
349
        .map(async (currentBidContext) => {
350
          logInfo('Flush bid context events to the server', currentBidContext);
7✔
351
          return utils.postAjax(
7✔
352
            debugEndpoint,
353
            {
354
              impid: currentBidContext.impid,
355
              bidWin: currentBidContext.bidWin,
356
              events: currentBidContext.events,
357
              // Unroll the payload object to the top level to make it easier for
358
              // Grafana to process the data.
359
              ...currentBidContext.subPayloads,
360
            }
361
          );
362
        }));
363

364
    await Promise.all(flushPromises);
7✔
365
  }
366
}
367

368
/**
369
 * Select key fields from the given object based on the object type. This is
370
 * useful for debugging to reduce the size of the API call payload.
371
 * @param {*} objType The custom type of the object. Return by determineObjType function.
372
 * @param {*} eventArgs The args object that is passed in to the event handler
373
 * or any supported object.
374
 * @returns the clone of the given object but only contains the key fields
375
 */
376
function pickKeyFields(objType, eventArgs) {
377
  switch (objType) {
70!
378
    case SUB_PAYLOAD_TYPES.AUCTION: {
379
      return pick(eventArgs, [
15✔
380
        'auctionId',
381
        'adUnitCodes',
382
        'auctionStart',
383
        'auctionEnd',
384
        'auctionStatus',
385
        'bidderRequestId',
386
        'timeout',
387
        'timestamp',
388
      ]);
389
    }
390
    case SUB_PAYLOAD_TYPES.BIDDER_REQUEST: {
391
      return pick(eventArgs, [
15✔
392
        'auctionId',
393
        'bidId',
394
        'bidderCode',
395
        'bidderRequestId',
396
        'timeout'
397
      ]);
398
    }
399
    case SUB_PAYLOAD_TYPES.ORTB_BID: {
400
      return pick(eventArgs, [
12✔
401
        'impid', 'id', 'price', 'cur', 'crid', 'cid', 'lurl', 'cpm'
402
      ]);
403
    }
404
    case SUB_PAYLOAD_TYPES.PREBID_RESPONSE_INTERPRETED: {
405
      return {
17✔
406
        ...pick(eventArgs, [
407
          'requestId',
408
          'creativeId',
409
          'cpm',
410
          'currency',
411
          'bidderCode',
412
          'adUnitCode',
413
          'ttl',
414
          'adId',
415
          'width',
416
          'height',
417
          'requestTimestamp',
418
          'responseTimestamp',
419
          'seatBidId',
420
          'statusMessage',
421
          'timeToRespond',
422
          'rejectionReason',
423
          'ortbId',
424
          'auctionId',
425
          'mediaType',
426
          'bidderRequestId',
427
        ]),
428
      };
429
    }
430
    case SUB_PAYLOAD_TYPES.PREBID_BID_REQUEST: {
431
      return {
×
432
        ...pick(eventArgs, [
433
          'bidderRequestId'
434
        ]),
435
        bids: eventArgs.bids.map(
436
          bid => pickKeyFields(SUB_PAYLOAD_TYPES.PREBID_RESPONSE_NOT_INTERPRETED, bid)
×
437
        ),
438
      };
439
    }
440
    case SUB_PAYLOAD_TYPES.AD_DOC_AND_PREBID_BID: {
441
      return {
3✔
442
        // bid: 'Not included to reduce payload size',
443
        doc: pick(eventArgs.doc, ['visibilityState', 'readyState', 'hidden']),
444
      };
445
    }
446
    case SUB_PAYLOAD_TYPES.AD_DOC_AND_PREBID_BID_WITH_ERROR: {
447
      return {
2✔
448
        // bid: 'Not included to reduce payload size',
449
        reason: eventArgs.reason,
450
        message: eventArgs.message,
451
        doc: pick(eventArgs.doc, ['visibilityState', 'readyState', 'hidden']),
452
      }
453
    }
454
    case SUB_PAYLOAD_TYPES.BIDDER_ERROR_ARGS: {
455
      return {
6✔
456
        bidderRequest: pickKeyFields(SUB_PAYLOAD_TYPES.BIDDER_REQUEST, eventArgs.bidderRequest),
457
        error: eventArgs.error?.toJSON ? eventArgs.error?.toJSON()
6!
458
          : (eventArgs.error || 'Failed to convert error object to JSON'),
6!
459
      };
460
    }
461
    default: {
462
      // Include the entire object for debugging
463
      return { eventArgs };
×
464
    }
465
  }
466
}
467

468
let mobkoiAnalytics = Object.assign(adapter({analyticsType}), {
1✔
469
  localContext: new LocalContext(),
470
  async track({
471
    eventType,
472
    args: prebidEventArgs
473
  }) {
474
    try {
53✔
475
      switch (eventType) {
53!
476
        case AUCTION_INIT: {
477
          utils.logTrackEvent(eventType, prebidEventArgs);
13✔
478
          const argsType = utils.determineObjType(prebidEventArgs);
13✔
479
          const auction = prebidEventArgs;
12✔
480
          this.localContext.initialise(auction);
12✔
481
          this.localContext.pushEventToAllBidContexts({
12✔
482
            eventType,
483
            level: DEBUG_EVENT_LEVELS.info,
484
            timestamp: auction.timestamp,
485
            subPayloads: {
486
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
487
            }
488
          });
489
          break;
12✔
490
        }
491
        case BID_RESPONSE: {
492
          utils.logTrackEvent(eventType, prebidEventArgs);
12✔
493
          const argsType = utils.determineObjType(prebidEventArgs);
12✔
494
          const prebidBid = prebidEventArgs;
12✔
495
          const bidContext = this.localContext.retrieveBidContext(prebidBid);
12✔
496
          bidContext.pushEvent({
12✔
497
            eventInstance: new Event({
498
              eventType,
499
              impid: bidContext.impid,
500
              publisherId: this.localContext.publisherId,
501
              level: DEBUG_EVENT_LEVELS.info,
502
              timestamp: prebidEventArgs.timestamp || Date.now(),
12!
503
            }),
504
            subPayloads: {
505
              [argsType]: pickKeyFields(argsType, prebidEventArgs),
506
              [SUB_PAYLOAD_TYPES.ORTB_BID]: pickKeyFields(SUB_PAYLOAD_TYPES.ORTB_BID, prebidEventArgs.ortbBidResponse),
507
            }
508
          });
509
          break;
12✔
510
        }
511
        case BID_WON: {
512
          utils.logTrackEvent(eventType, prebidEventArgs);
5✔
513
          const argsType = utils.determineObjType(prebidEventArgs);
5✔
514
          const prebidBid = prebidEventArgs;
5✔
515
          if (utils.isMobkoiBid(prebidBid)) {
5!
516
            this.localContext.retrieveBidContext(prebidBid).bidWin = true;
×
517
          }
518
          // Notify the server that the bidding results.
519
          this.localContext.triggerAllLossBidLossBeacon();
5✔
520
          // Append the bid win/loss event to all bid contexts
521
          _each(this.localContext.bidContexts, (currentBidContext) => {
5✔
522
            currentBidContext.pushEvent({
5✔
523
              eventInstance: new Event({
524
                eventType: currentBidContext.bidWin ? eventType : CUSTOM_EVENTS.BID_LOSS,
5!
525
                impid: currentBidContext.impid,
526
                publisherId: this.localContext.publisherId,
527
                level: DEBUG_EVENT_LEVELS.info,
528
                timestamp: prebidEventArgs.timestamp || Date.now(),
5!
529
              }),
530
              subPayloads: {
531
                [argsType]: pickKeyFields(argsType, prebidEventArgs),
532
              }
533
            });
534
          });
535
          break;
5✔
536
        }
537
        case AUCTION_TIMEOUT:
538
          utils.logTrackEvent(eventType, prebidEventArgs);
2✔
539
          const argsType = utils.determineObjType(prebidEventArgs);
2✔
540
          const auction = prebidEventArgs;
2✔
541
          this.localContext.pushEventToAllBidContexts({
2✔
542
            eventType,
543
            level: DEBUG_EVENT_LEVELS.error,
544
            timestamp: auction.timestamp,
545
            subPayloads: {
546
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
547
            }
548
          });
549
          break;
2✔
550
        case NO_BID: {
551
          utils.logTrackEvent(eventType, prebidEventArgs);
2✔
552
          const argsType = utils.determineObjType(prebidEventArgs);
2✔
553
          this.localContext.pushEventToAllBidContexts({
2✔
554
            eventType,
555
            level: DEBUG_EVENT_LEVELS.warn,
556
            timestamp: prebidEventArgs.timestamp || Date.now(),
2!
557
            subPayloads: {
558
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
559
            }
560
          });
561
          break;
2✔
562
        }
563
        case BID_REJECTED: {
564
          utils.logTrackEvent(eventType, prebidEventArgs);
2✔
565
          const argsType = utils.determineObjType(prebidEventArgs);
2✔
566
          const prebidBid = prebidEventArgs;
2✔
567
          const bidContext = this.localContext.retrieveBidContext(prebidBid);
2✔
568
          bidContext.pushEvent({
2✔
569
            eventInstance: new Event({
570
              eventType,
571
              impid: bidContext.impid,
572
              publisherId: this.localContext.publisherId,
573
              level: DEBUG_EVENT_LEVELS.error,
574
              timestamp: prebidEventArgs.timestamp || Date.now(),
2!
575
              note: prebidEventArgs.rejectionReason,
576
            }),
577
            subPayloads: {
578
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
579
            }
580
          });
581
          break;
2✔
582
        };
583
        case BIDDER_ERROR: {
584
          utils.logTrackEvent(eventType, prebidEventArgs)
2✔
585
          const argsType = utils.determineObjType(prebidEventArgs);
2✔
586
          this.localContext.pushEventToAllBidContexts({
2✔
587
            eventType,
588
            level: DEBUG_EVENT_LEVELS.warn,
589
            timestamp: prebidEventArgs.timestamp || Date.now(),
2!
590
            subPayloads: {
591
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
592
            }
593
          });
594
          break;
2✔
595
        }
596
        case AD_RENDER_FAILED: {
597
          utils.logTrackEvent(eventType, prebidEventArgs);
2✔
598
          const argsType = utils.determineObjType(prebidEventArgs);
2✔
599
          const {bid: prebidBid} = prebidEventArgs;
2✔
600
          const bidContext = this.localContext.retrieveBidContext(prebidBid);
2✔
601
          bidContext.pushEvent({
2✔
602
            eventInstance: new Event({
603
              eventType,
604
              impid: bidContext.impid,
605
              publisherId: this.localContext.publisherId,
606
              level: DEBUG_EVENT_LEVELS.error,
607
              timestamp: prebidEventArgs.timestamp || Date.now(),
4✔
608
            }),
609
            subPayloads: {
610
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
611
            }
612
          });
613
          break;
2✔
614
        }
615
        case AD_RENDER_SUCCEEDED: {
616
          utils.logTrackEvent(eventType, prebidEventArgs);
3✔
617
          const argsType = utils.determineObjType(prebidEventArgs);
3✔
618
          const prebidBid = prebidEventArgs.bid;
3✔
619
          const bidContext = this.localContext.retrieveBidContext(prebidBid);
3✔
620
          bidContext.pushEvent({
3✔
621
            eventInstance: new Event({
622
              eventType,
623
              impid: bidContext.impid,
624
              publisherId: this.localContext.publisherId,
625
              level: DEBUG_EVENT_LEVELS.info,
626
              timestamp: prebidEventArgs.timestamp || Date.now(),
6✔
627
            }),
628
            subPayloads: {
629
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
630
            }
631
          });
632
          break;
3✔
633
        }
634
        case AUCTION_END: {
635
          utils.logTrackEvent(eventType, prebidEventArgs);
3✔
636
          const argsType = utils.determineObjType(prebidEventArgs);
3✔
637
          const auction = prebidEventArgs;
3✔
638
          this.localContext.pushEventToAllBidContexts({
3✔
639
            eventType,
640
            level: DEBUG_EVENT_LEVELS.info,
641
            timestamp: auction.timestamp,
642
            subPayloads: {
643
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
644
            }
645
          });
646
          break;
3✔
647
        }
648
        case BIDDER_DONE: {
649
          utils.logTrackEvent(eventType, prebidEventArgs)
7✔
650
          const argsType = utils.determineObjType(prebidEventArgs);
7✔
651
          this.localContext.pushEventToAllBidContexts({
7✔
652
            eventType,
653
            level: DEBUG_EVENT_LEVELS.info,
654
            timestamp: prebidEventArgs.timestamp || Date.now(),
7!
655
            subPayloads: {
656
              [argsType]: pickKeyFields(argsType, prebidEventArgs)
657
            }
658
          });
659
          this.localContext.triggerAllLossBidLossBeacon();
7✔
660
          await this.localContext.flushAllDebugEvents();
7✔
661
          break;
2✔
662
        }
663
        default:
664
          // Do nothing in other events
665
          break;
×
666
      }
667
    } catch (error) {
668
      // If there is an unexpected error, such as a syntax error, we log
669
      // log the error and submit the error to the server for debugging.
670
      this.localContext.pushEventToAllBidContexts({
1✔
671
        eventType,
672
        level: DEBUG_EVENT_LEVELS.error,
673
        timestamp: prebidEventArgs.timestamp || Date.now(),
1!
674
        note: 'Error occurred when processing this event.',
675
        subPayloads: {
676
          // Include the entire object for debugging
677
          [`errorInEvent_${eventType}`]: {
678
            // Some fields contain large data. Omits them to reduce API call payload size
679
            eventArgs: utils.omitRecursive(prebidEventArgs, COMMON_FIELDS_TO_OMIT),
680
            error: error.message,
681
          }
682
        }
683
      });
684
      // Throw the error to skip the current Prebid event
685
      throw error;
1✔
686
    }
687
  }
688
});
689

690
// save the base class function
691
mobkoiAnalytics.originEnableAnalytics = mobkoiAnalytics.enableAnalytics;
1✔
692

693
// override enableAnalytics so we can get access to the config passed in from the page
694
mobkoiAnalytics.enableAnalytics = function (config) {
1✔
695
  mobkoiAnalytics.originEnableAnalytics(config); // call the base class function
×
696
};
697

698
adapterManager.registerAnalyticsAdapter({
1✔
699
  adapter: mobkoiAnalytics,
700
  code: BIDDER_CODE,
701
  gvlid: GVL_ID
702
});
703

704
export default mobkoiAnalytics;
705

706
class BidContext {
707
  /**
708
   * The impression ID (ORTB term) of the bid. This ID is initialised in Prebid
709
   * bid requests. The ID is reserved in requests and responses but have
710
   * different names from object to object.
711
   */
712
  get impid() {
713
    if (this.ortbBidResponse) {
194!
714
      return this.ortbBidResponse.impid;
194✔
715
    } else if (this.prebidBidResponse) {
×
716
      return this.prebidBidResponse.requestId;
×
717
    } else if (this.prebidBidRequest) {
×
718
      return this.prebidBidRequest.bidId;
×
719
    } else if (
×
720
      this.subPayloads &&
×
721
      utils.getImpId(this.subPayloads)
722
    ) {
723
      return utils.getImpId(this.subPayloads);
×
724
    } else {
725
      throw new Error('ORTB bid response and Prebid bid response are not available for extracting Impression ID');
×
726
    }
727
  }
728

729
  /**
730
   * ORTB ID generated by Ad Server
731
   */
732
  get ortbId() {
733
    if (this.ortbBidResponse) {
×
734
      return utils.getOrtbId(this.ortbBidResponse);
×
735
    } else if (this.prebidBidResponse) {
×
736
      return utils.getOrtbId(this.prebidBidResponse);
×
737
    } else if (this.subPayloads) {
×
738
      return utils.getOrtbId(this.subPayloads);
×
739
    } else {
740
      throw new Error('ORTB bid response and Prebid bid response are not available for extracting ORTB ID');
×
741
    }
742
  };
743

744
  get publisherId() {
745
    if (this.prebidBidRequest) {
53!
746
      return utils.getPublisherId(this.prebidBidRequest);
53✔
747
    } else {
748
      throw new Error('ORTB bid response and Prebid bid response are not available for extracting Publisher ID');
×
749
    }
750
  }
751

752
  /**
753
   * The prebid bid request object before converted to ORTB request in our
754
   * custom adapter.
755
   */
756
  get prebidBidRequest() {
757
    if (!this.prebidBidResponse) {
106!
758
      throw new Error('Prebid bid response is not available. Accessing before assigning.');
×
759
    }
760

761
    return this.localContext.bidderRequests.flatMap(br => br.bids)
106✔
762
      .find(bidRequest => bidRequest.bidId === this.prebidBidResponse.requestId);
106✔
763
  }
764

765
  /**
766
   * To avoid overriding the subPayloads object, we merge the new values to the
767
   * existing subPayloads object.
768
   */
769
  _subPayloads = null;
1✔
770
  /**
771
   * A group of payloads that are useful for debugging. The payloads are indexed
772
   * by SUB_PAYLOAD_TYPES.
773
   */
774
  get subPayloads() {
775
    return this._subPayloads;
7✔
776
  }
777
  /**
778
   * To avoid overriding the subPayloads object, we merge the new values to the
779
   * existing subPayloads object. Identity fields will automatically added to the
780
   * new values.
781
   * @param {*} newSubPayloads Object containing new values to be merged
782
   */
783
  mergePayload(newSubPayloads) {
784
    utils.mergePayloadAndAddCustomFields(
53✔
785
      this._subPayloads,
786
      newSubPayloads,
787
      // Add the identity fields to all sub payloads
788
      {
789
        impid: this.impid,
790
        publisherId: this.publisherId,
791
      }
792
    );
793
  }
794

795
  /**
796
   * The prebid bid response object after converted from ORTB response in our
797
   * custom adapter.
798
   */
799
  prebidBidResponse = null;
1✔
800

801
  /**
802
   * The raw ORTB bid response object from the server.
803
   */
804
  ortbBidResponse = null;
1✔
805

806
  /**
807
   * A flag to indicate if the bid has won the auction. It only updated to true
808
   * if the winning bid is from Mobkoi in the BID_WON event.
809
   */
810
  bidWin = false;
1✔
811

812
  /**
813
   * A flag to indicate if the loss beacon has been triggered.
814
   */
815
  lurlTriggered = false;
1✔
816

817
  /**
818
   * A list of DebugEvent objects
819
   */
820
  events = [];
1✔
821

822
  /**
823
   * Keep the reference of LocalContext object for easy accessing data.
824
   */
825
  localContext = null;
1✔
826

827
  /**
828
   * A object to store related data of a bid for easy access.
829
   * i.e. bid request and bid response.
830
   * @param {*} param0
831
   */
832
  constructor({
833
    localContext,
834
    prebidOrOrtbBidResponse: bidResponse,
835
  }) {
836
    this.localContext = localContext;
1✔
837
    this._subPayloads = {};
1✔
838

839
    if (!bidResponse) {
1!
840
      throw new Error('prebidOrOrtbBidResponse field is required');
×
841
    }
842

843
    const objType = utils.determineObjType(bidResponse);
1✔
844
    if (![SUB_PAYLOAD_TYPES.ORTB_BID, SUB_PAYLOAD_TYPES.PREBID_RESPONSE_INTERPRETED].includes(objType)) {
1!
845
      throw new Error(
×
846
        'Unable to create a new Bid Context as the given object is not a bid response object. ' +
847
        'Expect a Prebid Bid Object or ORTB Bid Object. Given object:\n' +
848
        JSON.stringify(utils.omitRecursive(bidResponse, COMMON_FIELDS_TO_OMIT), null, 2)
849
      );
850
    }
851

852
    if (objType === SUB_PAYLOAD_TYPES.ORTB_BID) {
1!
853
      this.ortbBidResponse = bidResponse;
×
854
      this.prebidBidResponse = null;
×
855
    } else if (objType === SUB_PAYLOAD_TYPES.PREBID_RESPONSE_INTERPRETED) {
1!
856
      this.ortbBidResponse = bidResponse.ortbBidResponse;
1✔
857
      this.prebidBidResponse = bidResponse;
1✔
858
    } else {
859
      throw new Error('Expect a Prebid Bid Object or ORTB Bid Object. Given object:\n' +
×
860
        JSON.stringify(utils.omitRecursive(bidResponse, COMMON_FIELDS_TO_OMIT), null, 2));
861
    }
862
  }
863

864
  /**
865
   * Push a debug event to the context which will be submitted to the server for debugging.
866
   * @param {Object} params Object containing the following properties:
867
   * @param {Event} params.eventInstance - DebugEvent object. If it does not contain the same impid as the BidContext, the event will be ignored.
868
   * @param {Object|null} params.subPayloads - Object containing various payloads obtained from the Prebid Event args. The payloads will be merged into the existing subPayloads.
869
   */
870
  pushEvent({eventInstance, subPayloads}) {
871
    if (!(eventInstance instanceof Event)) {
53!
872
      throw new Error('bugEvent must be an instance of DebugEvent');
×
873
    }
874
    if (eventInstance.impid != this.impid) {
53!
875
      // Ignore the event if the impression ID is not matched.
876
      return;
×
877
    }
878
    // Accept only object or null
879
    if (subPayloads !== null && typeof subPayloads !== 'object') {
53!
880
      throw new Error('subPayloads must be an object or null');
×
881
    }
882

883
    this.events.push(eventInstance);
53✔
884

885
    if (subPayloads !== null) {
53✔
886
      this.mergePayload(subPayloads);
52✔
887
    }
888
  }
889
}
890

891
/**
892
 * A class to represent an event happened in the bid processing lifecycle.
893
 */
894
class Event {
895
  /**
896
   * Impression ID must set before appending to event lists.
897
   */
898
  impid = null;
81✔
899

900
  /**
901
   * Publisher ID. It is a unique identifier for the publisher.
902
   */
903
  publisherId = null;
81✔
904

905
  /**
906
   * Prebid Event Type or Custom Event Type
907
   */
908
  eventType = null;
81✔
909
  /**
910
   * Debug level of the event. It can be one of the following:
911
   * - info
912
   * - warn
913
   * - error
914
   */
915
  level = null;
81✔
916
  /**
917
   * Timestamp of the event. It represents the time when the event occurred.
918
   */
919
  timestamp = null;
81✔
920

921
  constructor({eventType, impid, publisherId, level, timestamp, note = undefined}) {
79✔
922
    if (!eventType) {
81!
923
      throw new Error('eventType is required');
×
924
    }
925
    if (!impid) {
81!
926
      throw new Error(`Impression ID is required. Given: "${impid}"`);
×
927
    }
928

929
    if (typeof publisherId !== 'string') {
81!
930
      throw new Error(`Publisher ID must be a string. Given: "${publisherId}"`);
×
931
    }
932

933
    if (!DEBUG_EVENT_LEVELS[level]) {
81!
934
      throw new Error(`Event level must be one of ${Object.keys(DEBUG_EVENT_LEVELS).join(', ')}. Given: "${level}"`);
×
935
    }
936
    if (typeof timestamp !== 'number') {
81!
937
      throw new Error('Timestamp must be a number');
×
938
    }
939
    this.eventType = eventType;
81✔
940
    this.impid = impid;
81✔
941
    this.publisherId = publisherId;
81✔
942
    this.level = level;
81✔
943
    this.timestamp = timestamp;
81✔
944
    if (note) {
81✔
945
      this.note = note;
2✔
946
    }
947

948
    if (
81!
949
      debugTurnedOn() &&
81!
950
      (
951
        level === DEBUG_EVENT_LEVELS.error ||
952
        level === DEBUG_EVENT_LEVELS.warn
953
      )) {
954
      logWarn(`New Debug Event - Type: ${eventType} Level: ${level}.`);
×
955
    }
956
  }
957
}
958

959
/**
960
 * Various types of payloads that are submitted to the server for debugging.
961
 * Mostly they are obtain from the Prebid event args.
962
 */
963
export const SUB_PAYLOAD_TYPES = {
1✔
964
  AUCTION: 'prebid_auction',
965
  BIDDER_REQUEST: 'bidder_request',
966
  ORTB_BID: 'ortb_bid',
967
  PREBID_RESPONSE_INTERPRETED: 'prebid_bid_interpreted',
968
  PREBID_RESPONSE_NOT_INTERPRETED: 'prebid_bid_not_interpreted',
969
  PREBID_BID_REQUEST: 'prebid_bid_request',
970
  AD_DOC_AND_PREBID_BID: 'ad_doc_and_prebid_bid',
971
  AD_DOC_AND_PREBID_BID_WITH_ERROR: 'ad_doc_and_prebid_bid_with_error',
972
  BIDDER_ERROR_ARGS: 'bidder_error_args',
973
};
974

975
/**
976
 * Fields that are unique to objects used to identify the sub-payload type.
977
 */
978
export const SUB_PAYLOAD_UNIQUE_FIELDS_LOOKUP = {
1✔
979
  [SUB_PAYLOAD_TYPES.AUCTION]: ['auctionStatus'],
980
  [SUB_PAYLOAD_TYPES.BIDDER_REQUEST]: ['bidderRequestId'],
981
  [SUB_PAYLOAD_TYPES.ORTB_BID]: ['adm', 'impid'],
982
  [SUB_PAYLOAD_TYPES.PREBID_RESPONSE_INTERPRETED]: ['requestId', 'ortbBidResponse'],
983
  [SUB_PAYLOAD_TYPES.PREBID_RESPONSE_NOT_INTERPRETED]: ['requestId'], // This must be paste under PREBID_RESPONSE_INTERPRETED
984
  [SUB_PAYLOAD_TYPES.PREBID_BID_REQUEST]: ['bidId'],
985
  [SUB_PAYLOAD_TYPES.AD_DOC_AND_PREBID_BID]: ['doc', 'bid'],
986
  [SUB_PAYLOAD_TYPES.AD_DOC_AND_PREBID_BID_WITH_ERROR]: ['bid', 'reason', 'message'],
987
  [SUB_PAYLOAD_TYPES.BIDDER_ERROR_ARGS]: ['error', 'bidderRequest'],
988
};
989

990
/**
991
 * Required fields for the sub payloads. The property value defines the type of the required field.
992
 */
993
const PAYLOAD_REQUIRED_FIELDS = {
1✔
994
  impid: 'string',
995
  publisherId: 'string',
996
}
997

998
export const utils = {
1✔
999
  /**
1000
   * Make a POST request to the given URL with the given data.
1001
   * @param {*} url
1002
   * @param {*} data JSON data
1003
   * @returns
1004
   */
1005
  postAjax: async function (url, data) {
1006
    return new Promise((resolve, reject) => {
5✔
1007
      try {
5✔
1008
        logInfo('postAjax:', url, data);
5✔
1009
        ajax(url, resolve, JSON.stringify(data), {
5✔
1010
          contentType: 'application/json',
1011
          method: 'POST',
1012
          withCredentials: false, // No user-specific data is tied to the request
1013
          referrerPolicy: 'unsafe-url',
1014
          crossOrigin: true
1015
        });
1016
      } catch (error) {
1017
        reject(new Error(
×
1018
          `Failed to make post request to endpoint "${url}". With data: ` +
1019
          JSON.stringify(utils.omitRecursive(data, COMMON_FIELDS_TO_OMIT), null, 2),
1020
          { error: error.message }
1021
        ));
1022
      }
1023
    });
1024
  },
1025

1026
  /**
1027
   * Make a GET request to the given URL. If the request fails, it will fall back
1028
   * to AJAX request.
1029
   * @param {*} url URL with the query string
1030
   * @returns
1031
   */
1032
  sendGetRequest: async function(url) {
1033
    return new Promise((resolve, reject) => {
×
1034
      try {
×
1035
        logInfo('triggerPixel', url);
×
1036
        triggerPixel(url, resolve);
×
1037
      } catch (error) {
1038
        try {
×
1039
          logWarn(`triggerPixel failed. URL: (${url}) Falling back to ajax. Error: `, error);
×
1040
          ajax(url, resolve, null, {
×
1041
            contentType: 'application/json',
1042
            method: 'GET',
1043
            withCredentials: false, // No user-specific data is tied to the request
1044
            referrerPolicy: 'unsafe-url',
1045
            crossOrigin: true
1046
          });
1047
        } catch (error) {
1048
          // If failed with both methods, reject the promise
1049
          reject(error);
×
1050
        }
1051
      }
1052
    });
1053
  },
1054

1055
  /**
1056
   * Check if the given Prebid bid is from Mobkoi.
1057
   * @param {*} prebidBid
1058
   * @returns
1059
   */
1060
  isMobkoiBid: function (prebidBid) {
1061
    return prebidBid && prebidBid.bidderCode === BIDDER_CODE;
7✔
1062
  },
1063

1064
  /**
1065
   * !IMPORTANT: Make sure the implementation of this function matches utils.getOrtbId in
1066
   * mobkoiAnalyticsAdapter.js.
1067
   * We use the bidderRequestId as the ortbId. We could do so because we only
1068
   * make one ORTB request per Prebid Bidder Request.
1069
   * The ID field named differently when the value passed on to different contexts.
1070
   * @param {*} bid Prebid Bidder Request Object or Prebid Bid Response/Request
1071
   * or ORTB Request/Response Object
1072
   * @returns {string} The ORTB ID
1073
   * @throws {Error} If the ORTB ID cannot be found in the given object.
1074
   */
1075
  getOrtbId(bid) {
1076
    const ortbId =
1077
      // called bidderRequestId in Prebid Request
1078
      bid.bidderRequestId ||
24✔
1079
      // called seatBidId in Prebid Bid Response Object
1080
      bid.seatBidId ||
1081
      // called ortbId in Interpreted Prebid Response Object
1082
      bid.ortbId ||
1083
      // called id in ORTB object
1084
      (Object.hasOwn(bid, 'imp') && bid.id);
1085

1086
    if (!ortbId) {
24✔
1087
      throw new Error(
1✔
1088
        'Failed to obtain ORTB ID from the given object. Given object:\n' +
1089
        JSON.stringify(utils.omitRecursive(bid, COMMON_FIELDS_TO_OMIT), null, 2)
1090
      );
1091
    }
1092

1093
    return ortbId;
23✔
1094
  },
1095

1096
  /**
1097
   * Impression ID is named differently in different objects. This function will
1098
   * return the impression ID from the given bid object.
1099
   * @param {*} bid ORTB bid response or Prebid bid response or Prebid bid request
1100
   * @returns string | null
1101
   */
1102
  getImpId: function (bid) {
1103
    return (bid && (bid.impid || bid.requestId || bid.bidId)) || null;
62✔
1104
  },
1105

1106
  /**
1107
   * !IMPORTANT: Make sure the implementation of this function matches utils.getPublisherId in
1108
   * both adapters.
1109
   * Extract the publisher ID from the given object.
1110
   * @param {*} bid Prebid Bidder Request Object or Prebid Bid Response/Request
1111
   * or ORTB Request/Response Object
1112
   * @returns string
1113
   * @throws {Error} If the publisher ID is not found in the given object.
1114
   */
1115
  getPublisherId: function (bid) {
1116
    const ortbPath = 'site.publisher.id';
165✔
1117
    const prebidPath = `ortb2.${ortbPath}`;
165✔
1118

1119
    const publisherId =
1120
      deepAccess(bid, prebidPath) ||
165✔
1121
      deepAccess(bid, ortbPath);
1122

1123
    if (!publisherId) {
165✔
1124
      throw new Error(
1✔
1125
        'Failed to obtain publisher ID from the given object. ' +
1126
        `Please set it via the "${prebidPath}" field with pbjs.setBidderConfig.\n` +
1127
        'Given object:\n' +
1128
        JSON.stringify(bid, null, 2)
1129
      );
1130
    }
1131

1132
    return publisherId;
164✔
1133
  },
1134

1135
  /**
1136
   * !IMPORTANT: Make sure the implementation of this function matches getAdServerEndpointBaseUrl
1137
   * in both adapters.
1138
   * Obtain the Ad Server Base URL from the given Prebid object.
1139
   * @param {*} bid Prebid Bidder Request Object or Prebid Bid Response/Request
1140
   * or ORTB Request/Response Object
1141
   * @returns {string} The Ad Server Base URL
1142
   * @throws {Error} If the ORTB ID cannot be found in the given
1143
   */
1144
  getAdServerEndpointBaseUrl (bid) {
1145
    const path = `site.publisher.ext.${PARAM_NAME_AD_SERVER_BASE_URL}`;
9✔
1146
    const preBidPath = `ortb2.${path}`;
9✔
1147

1148
    const adServerBaseUrl =
1149
      // For Prebid Bid objects
1150
      deepAccess(bid, preBidPath) ||
9✔
1151
      // For ORTB objects
1152
      deepAccess(bid, path);
1153

1154
    if (!adServerBaseUrl) {
9✔
1155
      throw new Error('Failed to find the Ad Server Base URL in the given object. ' +
1✔
1156
        `Please set it via the "${preBidPath}" field with pbjs.setBidderConfig.\n` +
1157
        'Given Object:\n' +
1158
        JSON.stringify(bid, null, 2)
1159
      );
1160
    }
1161

1162
    return adServerBaseUrl;
8✔
1163
  },
1164

1165
  logTrackEvent: function (eventType, eventArgs) {
1166
    if (!debugTurnedOn()) {
53!
1167
      return;
53✔
1168
    }
1169
    const argsType = (() => {
×
1170
      try {
×
1171
        return utils.determineObjType(eventArgs);
×
1172
      } catch (error) {
1173
        logError(`Error when logging track event: [${eventType}]\n`, error);
×
1174
        return 'Unknown';
×
1175
      }
1176
    })();
1177
    logInfo(`Track event: [${eventType}]. Args Type Determination: ${argsType}`, eventArgs);
×
1178
  },
1179

1180
  /**
1181
   * Determine the type of the given object based on the object's fields.
1182
   * This is useful for identifying the type of object that is passed in to the
1183
   * handler functions.
1184
   * @param {*} eventArgs
1185
   * @returns string
1186
   */
1187
  determineObjType: function (eventArgs) {
1188
    if (typeof eventArgs !== 'object' || eventArgs === null) {
69✔
1189
      throw new Error(
5✔
1190
        'determineObjType: Expect an object. Given object is not an object or null. Given object:' +
1191
        JSON.stringify(utils.omitRecursive(eventArgs, COMMON_FIELDS_TO_OMIT), null, 2)
1192
      );
1193
    }
1194

1195
    let objType = null;
64✔
1196
    for (const type of Object.values(SUB_PAYLOAD_TYPES)) {
64✔
1197
      const identifyFields = SUB_PAYLOAD_UNIQUE_FIELDS_LOOKUP[type];
259✔
1198
      if (!identifyFields) {
259!
1199
        throw new Error(
×
1200
          `Identify fields for type "${type}" is not defined in COMMON_OBJECT_UNIT_FIELDS.`
1201
        );
1202
      }
1203

1204
      // If all fields are available in the object, then it's the type we are looking for
1205
      if (identifyFields.every(field => eventArgs.hasOwnProperty(field))) {
297✔
1206
        objType = type;
62✔
1207
        break;
62✔
1208
      }
1209
    }
1210

1211
    if (!objType) {
64✔
1212
      throw new Error(
2✔
1213
        'Unable to determine track args type. Please update COMMON_OBJECT_UNIT_FIELDS for the new object type.\n' +
1214
        'Given object:\n' +
1215
        JSON.stringify(utils.omitRecursive(eventArgs, COMMON_FIELDS_TO_OMIT), null, 2)
1216
      );
1217
    }
1218

1219
    return objType;
62✔
1220
  },
1221

1222
  /**
1223
   * Merge a Payload object with new values. The payload object must be in
1224
   * specific format where root level keys are SUB_PAYLOAD_TYPES values and the
1225
   * property values must be an object of the given type.
1226
   * @param {*} targetPayload
1227
   * @param {*} newSubPayloads
1228
   * @param {*} customFields Custom fields that are required for the sub payloads.
1229
   */
1230
  mergePayloadAndAddCustomFields: function (targetPayload, newSubPayloads, customFields = undefined) {
×
1231
    if (typeof targetPayload !== 'object') {
82!
1232
      throw new Error('Target must be an object');
×
1233
    }
1234

1235
    if (typeof newSubPayloads !== 'object') {
82!
1236
      throw new Error('New values must be an object');
×
1237
    }
1238

1239
    // Ensure all the required custom fields are available
1240
    if (customFields) {
82!
1241
      _each(customFields, (fieldType, fieldName) => {
82✔
1242
        if (fieldType === 'string' && typeof newSubPayloads[fieldName] !== 'string') {
164!
1243
          throw new Error(
×
1244
            `Field "${fieldName}" must be a string. Given: ${newSubPayloads[fieldName]}`
1245
          );
1246
        }
1247
      });
1248
    }
1249

1250
    mergeDeep(targetPayload, newSubPayloads);
82✔
1251

1252
    // Add the custom fields to the sub-payloads just added to the target payload
1253
    if (customFields) {
82!
1254
      utils.addCustomFieldsToSubPayloads(targetPayload, customFields);
82✔
1255
    }
1256
  },
1257

1258
  /**
1259
   * Should not use this function directly. Use mergePayloadAndCustomFields
1260
   * instead. This function add custom fields to the sub-payloads. The provided
1261
   * custom fields will be validated.
1262
   * @param {*} subPayloads A group of payloads that are useful for debugging. Indexed by SUB_PAYLOAD_TYPES.
1263
   * @param {*} customFields Custom fields that are required for the sub
1264
   * payloads. Fields are defined in PAYLOAD_REQUIRED_FIELDS.
1265
   */
1266
  addCustomFieldsToSubPayloads: function (subPayloads, customFields) {
1267
    _each(subPayloads, (currentSubPayload, subPayloadType) => {
82✔
1268
      if (!Object.values(SUB_PAYLOAD_TYPES).includes(subPayloadType)) {
348✔
1269
        return;
2✔
1270
      }
1271

1272
      // Add the custom fields to the sub-payloads
1273
      mergeDeep(currentSubPayload, customFields);
346✔
1274
    });
1275

1276
    // Before leaving the function, validate the payload to ensure all
1277
    // required fields are available.
1278
    utils.validateSubPayloads(subPayloads);
82✔
1279
  },
1280

1281
  /**
1282
   * Recursively omit the given keys from the object.
1283
   * @param {*} subPayloads - The payload objects index by their payload types.
1284
   * @throws {Error} - If the given object is not valid.
1285
   */
1286
  validateSubPayloads: function (subPayloads) {
1287
    _each(subPayloads, (currentSubPayload, subPayloadType) => {
84✔
1288
      if (!Object.values(SUB_PAYLOAD_TYPES).includes(subPayloadType)) {
350✔
1289
        return;
2✔
1290
      }
1291

1292
      const validationErrors = [];
348✔
1293
      // Validate the required fields
1294
      _each(PAYLOAD_REQUIRED_FIELDS, (requiredFieldType, requiredFieldName) => {
348✔
1295
        // eslint-disable-next-line valid-typeof
1296
        if (typeof currentSubPayload[requiredFieldName] !== requiredFieldType) {
696✔
1297
          validationErrors.push(new Error(
2✔
1298
            `Field "${requiredFieldName}" in "${subPayloadType}" must be a ${requiredFieldType}. Given: ${currentSubPayload[requiredFieldName]}`
1299
          ));
1300
        }
1301
      });
1302

1303
      if (validationErrors.length > 0) {
348✔
1304
        throw new Error(
1✔
1305
          `Validation failed for "${subPayloadType}".\n` +
1306
          `Object: ${JSON.stringify(utils.omitRecursive(currentSubPayload, COMMON_FIELDS_TO_OMIT), null, 2)}\n` +
1307
          validationErrors.map(error => `Error: ${error.message}`).join('\n')
2✔
1308
        );
1309
      }
1310
    });
1311
  },
1312

1313
  /**
1314
   * Recursively omit the given keys from the object.
1315
   * @param {*} obj - The object to process.
1316
   * @param {Array} keysToOmit - The keys to omit from the object.
1317
   * @param {*} [placeholder='OMITTED'] - The placeholder value to use for omitted keys.
1318
   * @returns {Object} - A clone of the given object with the specified keys omitted.
1319
   */
1320
  omitRecursive: function (obj, keysToOmit, placeholder = 'OMITTED') {
10✔
1321
    return Object.keys(obj).reduce((acc, currentKey) => {
56✔
1322
      // If the current key is in the keys to omit, replace the value with the placeholder
1323
      if (keysToOmit.includes(currentKey)) {
106!
1324
        acc[currentKey] = placeholder;
×
1325
        return acc;
×
1326
      }
1327

1328
      // If the current value is an object and not null, recursively omit keys
1329
      if (typeof obj[currentKey] === 'object' && obj[currentKey] !== null) {
106✔
1330
        acc[currentKey] = utils.omitRecursive(obj[currentKey], keysToOmit, placeholder);
46✔
1331
      } else {
1332
        // Otherwise, directly assign the value to the accumulator object
1333
        acc[currentKey] = obj[currentKey];
60✔
1334
      }
1335
      return acc;
106✔
1336
    }, {});
1337
  }
1338
};
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