@bidkernel/analytics 0.6.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ __export(src_exports, {
24
24
  PrebidEventDeduper: () => PrebidEventDeduper,
25
25
  getPrebidEventKey: () => getPrebidEventKey,
26
26
  getbidkernel: () => getbidkernel,
27
+ isTrustedEndpoint: () => isTrustedEndpoint,
27
28
  registerPrebidAnalytics: () => registerPrebidAnalytics
28
29
  });
29
30
  module.exports = __toCommonJS(src_exports);
@@ -34,26 +35,33 @@ var TraceEventType = {
34
35
  TRACE_EVENT_TYPE_UNSPECIFIED: 0,
35
36
  AUCTION_START: 1,
36
37
  AUCTION_END: 2,
37
- BID_REQUEST: 3,
38
- BID_RESPONSE: 4,
39
- BID_WIN: 5,
40
- BID_TIMEOUT: 6,
38
+ BID_RESPONSE: 3,
39
+ BID_WIN: 4,
41
40
  /** IMPRESSION - ad rendered into DOM */
42
- IMPRESSION: 7,
41
+ IMPRESSION: 5,
43
42
  /** VIEWABLE - IAB viewability threshold met (boolean, once per render cycle) */
44
- VIEWABLE: 8,
43
+ VIEWABLE: 6,
45
44
  /** TIME_IN_VIEW - accumulated in-viewport duration (once per render cycle end) */
46
- TIME_IN_VIEW: 14,
47
- REFRESH: 9,
48
- ERROR: 10,
49
- SLOT_DEFINED: 11,
50
- SLOT_DESTROYED: 12,
45
+ TIME_IN_VIEW: 7,
46
+ REFRESH: 8,
47
+ ERROR: 9,
48
+ SLOT_DEFINED: 10,
49
+ SLOT_DESTROYED: 11,
51
50
  /** CLICK - ad click-through */
52
- CLICK: 13,
53
- /** NO_BID - bidder responded with no bid */
54
- NO_BID: 15,
51
+ CLICK: 12,
55
52
  /** AD_RENDER_FAILED - Prebid failed to render the winning ad */
56
- AD_RENDER_FAILED: 16,
53
+ AD_RENDER_FAILED: 13,
54
+ UNRECOGNIZED: -1
55
+ };
56
+ var BidderOutcome = {
57
+ BIDDER_OUTCOME_UNSPECIFIED: 0,
58
+ /** BIDDER_OUTCOME_BID - returned a bid (CPM > 0) */
59
+ BIDDER_OUTCOME_BID: 1,
60
+ BIDDER_OUTCOME_NO_BID: 2,
61
+ BIDDER_OUTCOME_TIMEOUT: 3,
62
+ BIDDER_OUTCOME_ERROR: 4,
63
+ /** BIDDER_OUTCOME_NO_RESPONSE - requested, nothing came back by auction end */
64
+ BIDDER_OUTCOME_NO_RESPONSE: 5,
57
65
  UNRECOGNIZED: -1
58
66
  };
59
67
  function createBaseTraceEventBatch() {
@@ -284,7 +292,8 @@ function createBaseTraceEvent() {
284
292
  namespace: "",
285
293
  eventId: "",
286
294
  metadata: {},
287
- viewableDurationMs: void 0
295
+ viewableDurationMs: void 0,
296
+ bidderOutcomes: []
288
297
  };
289
298
  }
290
299
  var TraceEvent = {
@@ -322,6 +331,11 @@ var TraceEvent = {
322
331
  if (message.viewableDurationMs !== void 0) {
323
332
  writer.uint32(89).double(message.viewableDurationMs);
324
333
  }
334
+ if (message.bidderOutcomes !== void 0 && message.bidderOutcomes.length !== 0) {
335
+ for (const v of message.bidderOutcomes) {
336
+ BidderOutcomeEntry.encode(v, writer.uint32(98).fork()).join();
337
+ }
338
+ }
325
339
  return writer;
326
340
  },
327
341
  decode(input, length) {
@@ -411,6 +425,16 @@ var TraceEvent = {
411
425
  message.viewableDurationMs = reader.double();
412
426
  continue;
413
427
  }
428
+ case 12: {
429
+ if (tag !== 98) {
430
+ break;
431
+ }
432
+ const el = BidderOutcomeEntry.decode(reader, reader.uint32());
433
+ if (el !== void 0) {
434
+ message.bidderOutcomes.push(el);
435
+ }
436
+ continue;
437
+ }
414
438
  }
415
439
  if ((tag & 7) === 4 || tag === 0) {
416
440
  break;
@@ -443,6 +467,7 @@ var TraceEvent = {
443
467
  {}
444
468
  );
445
469
  message.viewableDurationMs = object.viewableDurationMs ?? void 0;
470
+ message.bidderOutcomes = object.bidderOutcomes?.map((e) => BidderOutcomeEntry.fromPartial(e)) || [];
446
471
  return message;
447
472
  }
448
473
  };
@@ -498,6 +523,91 @@ var TraceEvent_MetadataEntry = {
498
523
  return message;
499
524
  }
500
525
  };
526
+ function createBaseBidderOutcomeEntry() {
527
+ return { bidder: "", adUnitCode: "", mediaType: "", outcome: 0, latencyMs: 0 };
528
+ }
529
+ var BidderOutcomeEntry = {
530
+ encode(message, writer = new import_wire.BinaryWriter()) {
531
+ if (message.bidder !== void 0 && message.bidder !== "") {
532
+ writer.uint32(10).string(message.bidder);
533
+ }
534
+ if (message.adUnitCode !== void 0 && message.adUnitCode !== "") {
535
+ writer.uint32(18).string(message.adUnitCode);
536
+ }
537
+ if (message.mediaType !== void 0 && message.mediaType !== "") {
538
+ writer.uint32(26).string(message.mediaType);
539
+ }
540
+ if (message.outcome !== void 0 && message.outcome !== 0) {
541
+ writer.uint32(32).int32(message.outcome);
542
+ }
543
+ if (message.latencyMs !== void 0 && message.latencyMs !== 0) {
544
+ writer.uint32(41).double(message.latencyMs);
545
+ }
546
+ return writer;
547
+ },
548
+ decode(input, length) {
549
+ const reader = input instanceof import_wire.BinaryReader ? input : new import_wire.BinaryReader(input);
550
+ const end = length === void 0 ? reader.len : reader.pos + length;
551
+ const message = createBaseBidderOutcomeEntry();
552
+ while (reader.pos < end) {
553
+ const tag = reader.uint32();
554
+ switch (tag >>> 3) {
555
+ case 1: {
556
+ if (tag !== 10) {
557
+ break;
558
+ }
559
+ message.bidder = reader.string();
560
+ continue;
561
+ }
562
+ case 2: {
563
+ if (tag !== 18) {
564
+ break;
565
+ }
566
+ message.adUnitCode = reader.string();
567
+ continue;
568
+ }
569
+ case 3: {
570
+ if (tag !== 26) {
571
+ break;
572
+ }
573
+ message.mediaType = reader.string();
574
+ continue;
575
+ }
576
+ case 4: {
577
+ if (tag !== 32) {
578
+ break;
579
+ }
580
+ message.outcome = reader.int32();
581
+ continue;
582
+ }
583
+ case 5: {
584
+ if (tag !== 41) {
585
+ break;
586
+ }
587
+ message.latencyMs = reader.double();
588
+ continue;
589
+ }
590
+ }
591
+ if ((tag & 7) === 4 || tag === 0) {
592
+ break;
593
+ }
594
+ reader.skip(tag & 7);
595
+ }
596
+ return message;
597
+ },
598
+ create(base) {
599
+ return BidderOutcomeEntry.fromPartial(base ?? {});
600
+ },
601
+ fromPartial(object) {
602
+ const message = createBaseBidderOutcomeEntry();
603
+ message.bidder = object.bidder ?? "";
604
+ message.adUnitCode = object.adUnitCode ?? "";
605
+ message.mediaType = object.mediaType ?? "";
606
+ message.outcome = object.outcome ?? 0;
607
+ message.latencyMs = object.latencyMs ?? 0;
608
+ return message;
609
+ }
610
+ };
501
611
  function createBaseBidTrace() {
502
612
  return {
503
613
  bidder: "",
@@ -658,6 +768,8 @@ var MAX_QUEUE_SIZE = 200;
658
768
  var MAX_SEND_BACKOFF_MS = 5 * 60 * 1e3;
659
769
  var MAX_CONSECUTIVE_SEND_FAILURES = 10;
660
770
  var MAX_PAYLOAD_BYTES = 32 * 1024;
771
+ var MAX_METADATA_VALUE_CHARS = 1024;
772
+ var MAX_METADATA_ENTRIES = 16;
661
773
  var SAFE_CONTENT_TYPE = "text/plain";
662
774
  var PENDING_BATCH_KEY_PREFIX = "_bidkernel_pending_";
663
775
  var MAX_PENDING_BATCHES = 20;
@@ -666,11 +778,8 @@ var PENDING_BATCH_CLEANUP_DELAY_MS = 1e4;
666
778
  var EVENT_NAME_TO_TYPE = {
667
779
  auctionStart: TraceEventType.AUCTION_START,
668
780
  auctionEnd: TraceEventType.AUCTION_END,
669
- bidRequest: TraceEventType.BID_REQUEST,
670
781
  bidResponse: TraceEventType.BID_RESPONSE,
671
- bidTimeout: TraceEventType.BID_TIMEOUT,
672
782
  bidWin: TraceEventType.BID_WIN,
673
- noBid: TraceEventType.NO_BID,
674
783
  adRenderFailed: TraceEventType.AD_RENDER_FAILED,
675
784
  click: TraceEventType.CLICK,
676
785
  impression: TraceEventType.IMPRESSION,
@@ -683,6 +792,10 @@ var IMMEDIATE_FLUSH_TYPES = /* @__PURE__ */ new Set([
683
792
  TraceEventType.BID_WIN,
684
793
  TraceEventType.CLICK
685
794
  ]);
795
+ var CACHED_BID_TTL_MS = 30 * 60 * 1e3;
796
+ var MAX_CACHED_BID_KEYS = 200;
797
+ var MAX_IMPRESSION_KEYS = 1e3;
798
+ var MAX_TRACKED_AUCTIONS = 50;
686
799
  var SESSION_KEY = "_bidkernel_session";
687
800
  var SESSION_TS_KEY = "_bidkernel_session_ts";
688
801
  var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
@@ -714,6 +827,46 @@ function base64ToBytes(b64) {
714
827
  }
715
828
  return bytes;
716
829
  }
830
+ function isTrustedEndpoint(raw) {
831
+ if (!raw) return false;
832
+ let url;
833
+ try {
834
+ url = new URL(raw);
835
+ } catch {
836
+ return false;
837
+ }
838
+ if (url.username || url.password) return false;
839
+ if (url.protocol === "https:") return true;
840
+ if (url.protocol === "http:") {
841
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
842
+ }
843
+ return false;
844
+ }
845
+ function boundMetadata(metadata) {
846
+ const bounded = {};
847
+ let count = 0;
848
+ for (const key of Object.keys(metadata)) {
849
+ if (count >= MAX_METADATA_ENTRIES) break;
850
+ const value = metadata[key];
851
+ if (value === void 0 || value === null) continue;
852
+ const str = String(value);
853
+ bounded[key] = str.length > MAX_METADATA_VALUE_CHARS ? `${str.slice(0, MAX_METADATA_VALUE_CHARS)}...[truncated]` : str;
854
+ count++;
855
+ }
856
+ return bounded;
857
+ }
858
+ function isLegacyPendingKey(key) {
859
+ const parts = key.slice(PENDING_BATCH_KEY_PREFIX.length).split("_");
860
+ return parts.length === 2 && /^\d+$/.test(parts[0]);
861
+ }
862
+ function sameEndpointOrigin(a, b) {
863
+ if (!a || !b) return false;
864
+ try {
865
+ return new URL(a).origin === new URL(b).origin;
866
+ } catch {
867
+ return false;
868
+ }
869
+ }
717
870
  function extendSession() {
718
871
  const now = Date.now();
719
872
  inMemorySessionTs = now;
@@ -830,22 +983,6 @@ var PrebidEventDeduper = class {
830
983
  return false;
831
984
  }
832
985
  };
833
- function parseSize(raw) {
834
- if (!raw) return { width: 0, height: 0 };
835
- let cur = raw;
836
- while (Array.isArray(cur) && cur.length > 0 && Array.isArray(cur[0])) {
837
- cur = cur[0];
838
- }
839
- if (Array.isArray(cur) && cur.length >= 2) {
840
- const w = Number(cur[0]);
841
- const h = Number(cur[1]);
842
- return {
843
- width: Number.isFinite(w) ? w : 0,
844
- height: Number.isFinite(h) ? h : 0
845
- };
846
- }
847
- return { width: 0, height: 0 };
848
- }
849
986
  function parseBidDimensions(bid) {
850
987
  if (Number.isFinite(bid?.width) && Number.isFinite(bid?.height)) {
851
988
  return { width: bid.width, height: bid.height };
@@ -889,10 +1026,21 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
889
1026
  slotViewabilityRecords = /* @__PURE__ */ new Map();
890
1027
  elementToSlotId = /* @__PURE__ */ new Map();
891
1028
  slotRefreshIndices = /* @__PURE__ */ new Map();
1029
+ slotLastAuctionIds = /* @__PURE__ */ new Map();
892
1030
  pendingThresholdListeners = /* @__PURE__ */ new Map();
1031
+ // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
1032
+ slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
1033
+ // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
1034
+ cachedWinningBids = /* @__PURE__ */ new Map();
1035
+ // Active video player attachment cleanup routines
1036
+ videoDetachCleanups = /* @__PURE__ */ new Set();
1037
+ // Compacted bidder participation map per auction: auctionId -> Map<`${bidder}:${adUnitCode}`, BidderOutcomeEntry>
1038
+ auctionBidderOutcomes = /* @__PURE__ */ new Map();
893
1039
  constructor(config) {
1040
+ const requestedEndpoint = config.endpoint || "";
1041
+ const endpoint = !requestedEndpoint || isTrustedEndpoint(requestedEndpoint) ? requestedEndpoint : "";
894
1042
  this.config = {
895
- endpoint: config.endpoint || "",
1043
+ endpoint,
896
1044
  propertyId: config.propertyId || "",
897
1045
  pageviewId: config.pageviewId || generateUUID(),
898
1046
  sessionId: config.sessionId || getOrCreateSessionId(),
@@ -908,6 +1056,12 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
908
1056
  pbjsGlobalName: config.pbjsGlobalName || "pbjs",
909
1057
  attachPbjsListeners: config.attachPbjsListeners ?? true
910
1058
  };
1059
+ if (requestedEndpoint && !endpoint) {
1060
+ this.log(
1061
+ "ERROR",
1062
+ `Rejected untrusted analytics endpoint ${requestedEndpoint}: must be an absolute https:// URL without embedded credentials. Events will not be transmitted.`
1063
+ );
1064
+ }
911
1065
  this.boundFlushBeacon = () => {
912
1066
  this.flushAllSlotsTimeInView();
913
1067
  this.flushBeacon();
@@ -1035,6 +1189,17 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1035
1189
  }
1036
1190
  this.slotViewabilityRecords.clear();
1037
1191
  this.elementToSlotId.clear();
1192
+ for (const cleanup of this.videoDetachCleanups) {
1193
+ try {
1194
+ cleanup();
1195
+ } catch {
1196
+ }
1197
+ }
1198
+ this.videoDetachCleanups.clear();
1199
+ this.cachedWinningBids.clear();
1200
+ this.slotEmittedImpressionKeys.clear();
1201
+ this.slotLastAuctionIds.clear();
1202
+ this.auctionBidderOutcomes.clear();
1038
1203
  if (this.flushTimer) {
1039
1204
  clearInterval(this.flushTimer);
1040
1205
  this.flushTimer = null;
@@ -1357,6 +1522,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1357
1522
  if (options?.bid !== void 0) existing.bidPayload = options.bid;
1358
1523
  if (options?.metadata !== void 0) existing.metadata = options.metadata;
1359
1524
  }
1525
+ if (options?.auctionId) {
1526
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1527
+ }
1360
1528
  if (el) {
1361
1529
  if (existing.element && existing.element !== el && this.intersectionObserver) {
1362
1530
  this.intersectionObserver.unobserve(existing.element);
@@ -1369,6 +1537,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1369
1537
  } else {
1370
1538
  const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
1371
1539
  this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
1540
+ if (options?.auctionId) {
1541
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1542
+ }
1372
1543
  const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
1373
1544
  this.pendingThresholdListeners.delete(resolvedSlotId);
1374
1545
  const record = {
@@ -1424,7 +1595,551 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1424
1595
  this.unobserveSlot(slotId);
1425
1596
  this.slotViewabilityRecords.delete(slotId);
1426
1597
  this.slotRefreshIndices.delete(slotId);
1598
+ this.slotLastAuctionIds.delete(slotId);
1427
1599
  this.pendingThresholdListeners.delete(slotId);
1600
+ const prefix = `${escapeKeyPart(slotId)}:`;
1601
+ for (const key of Array.from(this.slotEmittedImpressionKeys)) {
1602
+ if (key === slotId || key.startsWith(prefix)) {
1603
+ this.slotEmittedImpressionKeys.delete(key);
1604
+ }
1605
+ }
1606
+ const cached = this.cachedWinningBids.get(slotId);
1607
+ if (cached) this.deleteCachedBidEntry(cached);
1608
+ }
1609
+ // --- Bounded caches -------------------------------------------------------
1610
+ /**
1611
+ * Reads a cached winning bid, treating an entry past CACHED_BID_TTL_MS as
1612
+ * absent and dropping it. Every read goes through here so a stale bid can
1613
+ * never be married to a much later render.
1614
+ */
1615
+ getCachedBid(key) {
1616
+ const entry = this.cachedWinningBids.get(key);
1617
+ if (!entry) return void 0;
1618
+ if (Date.now() - entry.timestamp > CACHED_BID_TTL_MS) {
1619
+ this.deleteCachedBidEntry(entry);
1620
+ return void 0;
1621
+ }
1622
+ return entry;
1623
+ }
1624
+ /** Removes every alias pointing at one entry. Aliases are stored by identity. */
1625
+ deleteCachedBidEntry(entry) {
1626
+ for (const [key, value] of Array.from(this.cachedWinningBids)) {
1627
+ if (value === entry) this.cachedWinningBids.delete(key);
1628
+ }
1629
+ }
1630
+ /**
1631
+ * Drops expired entries, then evicts oldest-first until the key cap holds.
1632
+ * Map iterates in insertion order and one win's aliases are written
1633
+ * consecutively, so eviction takes a whole bid's aliases together.
1634
+ */
1635
+ pruneCachedWinningBids() {
1636
+ const now = Date.now();
1637
+ for (const [key, entry] of Array.from(this.cachedWinningBids)) {
1638
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) this.cachedWinningBids.delete(key);
1639
+ }
1640
+ while (this.cachedWinningBids.size > MAX_CACHED_BID_KEYS) {
1641
+ const oldest = this.cachedWinningBids.keys().next();
1642
+ if (oldest.done) break;
1643
+ this.cachedWinningBids.delete(oldest.value);
1644
+ }
1645
+ }
1646
+ /** Records a dedup key, evicting oldest-first at the cap. */
1647
+ addImpressionKey(key) {
1648
+ if (this.slotEmittedImpressionKeys.has(key)) return;
1649
+ while (this.slotEmittedImpressionKeys.size >= MAX_IMPRESSION_KEYS) {
1650
+ const oldest = this.slotEmittedImpressionKeys.values().next();
1651
+ if (oldest.done) break;
1652
+ this.slotEmittedImpressionKeys.delete(oldest.value);
1653
+ }
1654
+ this.slotEmittedImpressionKeys.add(key);
1655
+ }
1656
+ /**
1657
+ * The bidder-outcome map for an auction, creating it if absent. Evicts the
1658
+ * oldest auction at the cap: without this, any auction whose auctionEnd never
1659
+ * fires (abandoned, navigated away from mid-flight) leaks its map forever.
1660
+ */
1661
+ getOrCreateAuctionOutcomes(auctionId) {
1662
+ let auctionMap = this.auctionBidderOutcomes.get(auctionId);
1663
+ if (!auctionMap) {
1664
+ auctionMap = /* @__PURE__ */ new Map();
1665
+ this.auctionBidderOutcomes.set(auctionId, auctionMap);
1666
+ }
1667
+ while (this.auctionBidderOutcomes.size > MAX_TRACKED_AUCTIONS) {
1668
+ const oldest = this.auctionBidderOutcomes.keys().next();
1669
+ if (oldest.done || oldest.value === auctionId) break;
1670
+ this.auctionBidderOutcomes.delete(oldest.value);
1671
+ }
1672
+ return auctionMap;
1673
+ }
1674
+ hasImpressionEmitted(slotId, refreshIndex, auctionId, creativeId) {
1675
+ const rIndex = refreshIndex ?? this.slotRefreshIndices.get(slotId) ?? 0;
1676
+ const baseSlotKey = `${escapeKeyPart(slotId)}:${rIndex}`;
1677
+ if (creativeId) {
1678
+ if (auctionId) {
1679
+ return this.slotEmittedImpressionKeys.has(
1680
+ `${baseSlotKey}:auc:${escapeKeyPart(auctionId)}:${escapeKeyPart(creativeId)}`
1681
+ ) || this.slotEmittedImpressionKeys.has(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1682
+ }
1683
+ return this.slotEmittedImpressionKeys.has(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1684
+ }
1685
+ if (auctionId) {
1686
+ return this.slotEmittedImpressionKeys.has(`${baseSlotKey}:auc:${escapeKeyPart(auctionId)}`);
1687
+ }
1688
+ return this.slotEmittedImpressionKeys.has(baseSlotKey);
1689
+ }
1690
+ markImpressionEmitted(slotId, refreshIndex, auctionId, creativeId) {
1691
+ const baseSlotKey = `${escapeKeyPart(slotId)}:${refreshIndex}`;
1692
+ this.addImpressionKey(baseSlotKey);
1693
+ if (creativeId) {
1694
+ this.addImpressionKey(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1695
+ }
1696
+ if (auctionId) {
1697
+ this.addImpressionKey(`${baseSlotKey}:auc:${escapeKeyPart(auctionId)}`);
1698
+ if (creativeId) {
1699
+ this.addImpressionKey(
1700
+ `${baseSlotKey}:auc:${escapeKeyPart(auctionId)}:${escapeKeyPart(creativeId)}`
1701
+ );
1702
+ }
1703
+ }
1704
+ }
1705
+ recordImpression(slotId, options) {
1706
+ if (!this.isEnabled) return false;
1707
+ const resolvedSlotId = slotId || options?.adUnitCode || "";
1708
+ const adUnitCode = options?.adUnitCode || resolvedSlotId;
1709
+ const cached = (options?.auctionId && resolvedSlotId ? this.getCachedBid(`${options.auctionId}:${resolvedSlotId}`) : void 0) || (options?.auctionId && adUnitCode ? this.getCachedBid(`${options.auctionId}:${adUnitCode}`) : void 0) || (resolvedSlotId ? this.getCachedBid(resolvedSlotId) : void 0) || (adUnitCode ? this.getCachedBid(adUnitCode) : void 0);
1710
+ const rawBid = options?.bid || cached?.rawBid || {};
1711
+ const auctionId = options?.auctionId || rawBid.auctionId || cached?.auctionId || "";
1712
+ const transactionId = options?.transactionId || rawBid.transactionId || cached?.transactionId || "";
1713
+ const creativeId = rawBid.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || "";
1714
+ const mediaType = options?.mediaType || rawBid.mediaType || cached?.bidTrace?.mediaType || "banner";
1715
+ const targetSlot = resolvedSlotId || adUnitCode;
1716
+ const rec = (resolvedSlotId ? this.slotViewabilityRecords.get(resolvedSlotId) : void 0) || (adUnitCode ? this.slotViewabilityRecords.get(adUnitCode) : void 0);
1717
+ const lastAuctionId = targetSlot ? this.slotLastAuctionIds.get(targetSlot) || (rec ? rec.auctionId : void 0) : void 0;
1718
+ const isDifferentAuction = Boolean(
1719
+ targetSlot && auctionId && lastAuctionId && lastAuctionId !== auctionId && options?.refreshIndex === void 0
1720
+ );
1721
+ const bidTrace = {
1722
+ bidder: rawBid.bidderCode || rawBid.bidder || cached?.bidTrace?.bidder || "",
1723
+ cpm: Number.isFinite(rawBid.originalCpm) ? rawBid.originalCpm : Number.isFinite(rawBid.cpm) ? rawBid.cpm : cached?.bidTrace?.cpm ?? 0,
1724
+ currency: rawBid.originalCurrency ?? rawBid.currency ?? cached?.bidTrace?.currency ?? "USD",
1725
+ ...parseBidDimensions(rawBid).width ? parseBidDimensions(rawBid) : cached?.bidTrace ? { width: cached.bidTrace.width, height: cached.bidTrace.height } : parseBidDimensions(rawBid),
1726
+ dealId: rawBid.dealId || cached?.bidTrace?.dealId || "",
1727
+ mediaType,
1728
+ latencyMs: Number.isFinite(rawBid.timeToRespond) ? rawBid.timeToRespond : cached?.bidTrace?.latencyMs ?? 0,
1729
+ advertiserDomain: rawBid.meta?.advertiserDomains?.[0] || cached?.bidTrace?.advertiserDomain || "",
1730
+ creativeId: rawBid.creativeId || cached?.bidTrace?.creativeId || ""
1731
+ };
1732
+ if (isDifferentAuction && targetSlot) {
1733
+ this.flushSlotTimeInView(targetSlot);
1734
+ const nextRefreshIndex = (this.slotRefreshIndices.get(targetSlot) ?? (rec ? rec.refreshIndex : 0)) + 1;
1735
+ this.slotRefreshIndices.set(targetSlot, nextRefreshIndex);
1736
+ this.slotLastAuctionIds.set(targetSlot, auctionId);
1737
+ if (rec) {
1738
+ rec.refreshIndex = nextRefreshIndex;
1739
+ rec.auctionId = auctionId;
1740
+ rec.transactionId = transactionId;
1741
+ rec.bidPayload = bidTrace;
1742
+ rec.viewableFired = false;
1743
+ rec.accumulatedTimeInViewMs = 0;
1744
+ }
1745
+ this.enqueue(TraceEventType.REFRESH, "refresh", {
1746
+ auctionId,
1747
+ transactionId,
1748
+ adUnitCode: targetSlot,
1749
+ bid: bidTrace,
1750
+ metadata: {
1751
+ ...options?.metadata,
1752
+ refresh_index: String(nextRefreshIndex)
1753
+ }
1754
+ });
1755
+ } else if (targetSlot) {
1756
+ if (auctionId) {
1757
+ this.slotLastAuctionIds.set(targetSlot, auctionId);
1758
+ }
1759
+ if (!this.slotRefreshIndices.has(targetSlot)) {
1760
+ this.slotRefreshIndices.set(targetSlot, 0);
1761
+ }
1762
+ }
1763
+ const currentRefreshIndex = options?.refreshIndex ?? (resolvedSlotId ? this.slotRefreshIndices.get(resolvedSlotId) ?? (adUnitCode ? this.slotRefreshIndices.get(adUnitCode) : void 0) ?? (rec ? rec.refreshIndex : 0) : 0);
1764
+ const alreadyEmitted = resolvedSlotId && this.hasImpressionEmitted(resolvedSlotId, currentRefreshIndex, auctionId, creativeId) || adUnitCode && adUnitCode !== resolvedSlotId && this.hasImpressionEmitted(adUnitCode, currentRefreshIndex, auctionId, creativeId);
1765
+ if (alreadyEmitted) {
1766
+ this.log(
1767
+ "DEBUG",
1768
+ `Impression already emitted for slot ${resolvedSlotId || adUnitCode} in cycle ${currentRefreshIndex}`
1769
+ );
1770
+ return false;
1771
+ }
1772
+ if (resolvedSlotId)
1773
+ this.markImpressionEmitted(resolvedSlotId, currentRefreshIndex, auctionId, creativeId);
1774
+ if (adUnitCode && adUnitCode !== resolvedSlotId)
1775
+ this.markImpressionEmitted(adUnitCode, currentRefreshIndex, auctionId, creativeId);
1776
+ this.enqueue(TraceEventType.IMPRESSION, "impression", {
1777
+ auctionId,
1778
+ transactionId,
1779
+ adUnitCode,
1780
+ bid: bidTrace,
1781
+ metadata: {
1782
+ ...options?.metadata,
1783
+ refresh_index: String(currentRefreshIndex)
1784
+ }
1785
+ });
1786
+ if (rec) {
1787
+ if (!rec.bidPayload && bidTrace) rec.bidPayload = bidTrace;
1788
+ if (!rec.auctionId && auctionId) rec.auctionId = auctionId;
1789
+ if (!rec.transactionId && transactionId) rec.transactionId = transactionId;
1790
+ }
1791
+ return true;
1792
+ }
1793
+ /**
1794
+ * Bridges Google IMA SDK AdsManager events to Bidkernel analytics.
1795
+ * Defers IMPRESSION emission until AdEvent.STARTED (or IMPRESSION),
1796
+ * tracks milestones (FIRST_QUARTILE, MIDPOINT, THIRD_QUARTILE, COMPLETE),
1797
+ * and handles AD_ERROR.
1798
+ */
1799
+ attachImaAdsManager(adsManager, options) {
1800
+ if (!adsManager || typeof adsManager.addEventListener !== "function") {
1801
+ this.log("WARN", "Invalid AdsManager passed to attachImaAdsManager");
1802
+ return () => {
1803
+ };
1804
+ }
1805
+ const slotId = options?.slotId || options?.adUnitCode || "video";
1806
+ const adUnitCode = options?.adUnitCode || slotId;
1807
+ const auctionId = options?.auctionId || "";
1808
+ const transactionId = options?.transactionId || "";
1809
+ const getWinningBid = () => {
1810
+ return (auctionId ? this.getCachedBid(`${auctionId}:${adUnitCode}`) : void 0) || (auctionId && slotId !== adUnitCode ? this.getCachedBid(`${auctionId}:${slotId}`) : void 0) || this.getCachedBid(adUnitCode) || (slotId !== adUnitCode ? this.getCachedBid(slotId) : void 0);
1811
+ };
1812
+ const onAdStartedOrImpression = (event) => {
1813
+ this.log("DEBUG", "IMA AdEvent.STARTED / IMPRESSION received", event);
1814
+ const ad = typeof event?.getAd === "function" ? event.getAd() : event?.ad;
1815
+ const adData = {};
1816
+ if (ad) {
1817
+ adData.creativeId = (typeof ad.getCreativeId === "function" ? ad.getCreativeId() : ad.creativeId) || "";
1818
+ adData.adId = (typeof ad.getAdId === "function" ? ad.getAdId() : ad.id) || "";
1819
+ adData.title = (typeof ad.getTitle === "function" ? ad.getTitle() : ad.title) || "";
1820
+ adData.duration = typeof ad.getDuration === "function" ? ad.getDuration() : ad.duration;
1821
+ adData.advertiserName = typeof ad.getAdvertiserName === "function" ? ad.getAdvertiserName() : "";
1822
+ }
1823
+ const cached = getWinningBid();
1824
+ const mergedBid = {
1825
+ ...cached?.rawBid || options?.bid,
1826
+ mediaType: "video",
1827
+ creativeId: adData.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || ""
1828
+ };
1829
+ const emitted = this.recordImpression(slotId, {
1830
+ adUnitCode,
1831
+ auctionId: auctionId || cached?.auctionId,
1832
+ transactionId: transactionId || cached?.transactionId,
1833
+ mediaType: "video",
1834
+ bid: mergedBid,
1835
+ metadata: {
1836
+ ...options?.metadata,
1837
+ media_type: "video",
1838
+ ...adData.title ? { ad_title: adData.title } : {},
1839
+ ...Number.isFinite(adData.duration) ? { ad_duration_s: String(adData.duration) } : {}
1840
+ }
1841
+ });
1842
+ if (emitted && options?.onImpression) {
1843
+ try {
1844
+ options.onImpression(slotId, { ad, bid: mergedBid });
1845
+ } catch (e) {
1846
+ this.log("ERROR", "Error in onImpression callback", e);
1847
+ }
1848
+ }
1849
+ };
1850
+ const onMilestone = (milestone) => {
1851
+ this.log("DEBUG", `IMA AdEvent milestone: ${milestone}`);
1852
+ if (options?.onMilestone) {
1853
+ try {
1854
+ options.onMilestone(milestone, slotId);
1855
+ } catch (e) {
1856
+ this.log("ERROR", "Error in onMilestone callback", e);
1857
+ }
1858
+ }
1859
+ };
1860
+ const onAdClick = (event) => {
1861
+ this.log("DEBUG", "IMA AdEvent.CLICK received", event);
1862
+ const cached = getWinningBid();
1863
+ this.enqueue(TraceEventType.CLICK, "click", {
1864
+ auctionId: auctionId || cached?.auctionId,
1865
+ transactionId: transactionId || cached?.transactionId,
1866
+ adUnitCode,
1867
+ bid: cached?.bidTrace || options?.bid,
1868
+ metadata: {
1869
+ ...options?.metadata,
1870
+ media_type: "video"
1871
+ }
1872
+ });
1873
+ };
1874
+ const onAdError = (event) => {
1875
+ this.log("WARN", "IMA AdErrorEvent received", event);
1876
+ const err = typeof event?.getError === "function" ? event.getError() : event?.error || event;
1877
+ const msg = (err && typeof err.getMessage === "function" ? err.getMessage() : err?.message) || String(err || "IMA ad error");
1878
+ const code = (err && typeof err.getErrorCode === "function" ? err.getErrorCode() : err?.code) || "";
1879
+ const cached = getWinningBid();
1880
+ this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
1881
+ auctionId: auctionId || cached?.auctionId,
1882
+ transactionId: transactionId || cached?.transactionId,
1883
+ adUnitCode,
1884
+ bid: cached?.bidTrace || options?.bid,
1885
+ metadata: {
1886
+ ...options?.metadata,
1887
+ reason: "ima_ad_error",
1888
+ message: msg,
1889
+ ...code ? { ima_error_code: String(code) } : {}
1890
+ },
1891
+ error: typeof err === "object" ? err : new Error(msg)
1892
+ });
1893
+ if (options?.onError) {
1894
+ try {
1895
+ options.onError(err);
1896
+ } catch (e) {
1897
+ this.log("ERROR", "Error in onError callback", e);
1898
+ }
1899
+ }
1900
+ };
1901
+ const googleIma = typeof window !== "undefined" ? window.google?.ima : void 0;
1902
+ const adEventType = googleIma?.AdEvent?.Type || {};
1903
+ const adErrorEventType = googleIma?.AdErrorEvent?.Type || {};
1904
+ const listeners = [
1905
+ { type: adEventType.STARTED || "started", handler: onAdStartedOrImpression },
1906
+ { type: adEventType.IMPRESSION || "impression", handler: onAdStartedOrImpression },
1907
+ {
1908
+ type: adEventType.FIRST_QUARTILE || "firstQuartile",
1909
+ handler: () => onMilestone("firstQuartile")
1910
+ },
1911
+ { type: adEventType.MIDPOINT || "midpoint", handler: () => onMilestone("midpoint") },
1912
+ {
1913
+ type: adEventType.THIRD_QUARTILE || "thirdQuartile",
1914
+ handler: () => onMilestone("thirdQuartile")
1915
+ },
1916
+ { type: adEventType.COMPLETE || "complete", handler: () => onMilestone("complete") },
1917
+ { type: adEventType.CLICK || "click", handler: onAdClick },
1918
+ { type: adErrorEventType.AD_ERROR || "adError", handler: onAdError }
1919
+ ];
1920
+ for (const { type, handler } of listeners) {
1921
+ try {
1922
+ adsManager.addEventListener(type, handler);
1923
+ } catch {
1924
+ }
1925
+ }
1926
+ const cleanup = () => {
1927
+ for (const { type, handler } of listeners) {
1928
+ try {
1929
+ adsManager.removeEventListener(type, handler);
1930
+ } catch {
1931
+ }
1932
+ }
1933
+ this.videoDetachCleanups.delete(cleanup);
1934
+ };
1935
+ this.videoDetachCleanups.add(cleanup);
1936
+ return cleanup;
1937
+ }
1938
+ /**
1939
+ * Attaches render hooks and viewability tracking to a video player.
1940
+ * Supports HTML5 <video> elements, container elements, or Google IMA AdsManager.
1941
+ */
1942
+ attachVideoPlayer(target, options) {
1943
+ if (!target) {
1944
+ this.log("WARN", "attachVideoPlayer called with null/undefined target");
1945
+ return () => {
1946
+ };
1947
+ }
1948
+ let el = null;
1949
+ if (typeof target === "string") {
1950
+ if (typeof document !== "undefined") {
1951
+ try {
1952
+ el = document.querySelector(target) || document.getElementById(target);
1953
+ } catch {
1954
+ el = document.getElementById(target);
1955
+ }
1956
+ }
1957
+ } else if (typeof HTMLElement !== "undefined" && target instanceof HTMLElement) {
1958
+ el = target;
1959
+ } else if (target && typeof target === "object" && target.nodeType === 1) {
1960
+ el = target;
1961
+ } else if (
1962
+ // Check if target is an IMA AdsManager
1963
+ typeof target.addEventListener === "function" && (typeof target.init === "function" || typeof target.start === "function" || typeof target.getCuePoints === "function" || typeof target.getVolume === "function" || target.__imaAdsManager)
1964
+ ) {
1965
+ return this.attachImaAdsManager(target, options);
1966
+ } else if (target && typeof target.addEventListener === "function") {
1967
+ el = target;
1968
+ }
1969
+ const slotId = options?.slotId || options?.adUnitCode || (el ? el.id : "") || "video";
1970
+ const adUnitCode = options?.adUnitCode || slotId;
1971
+ const auctionId = options?.auctionId || "";
1972
+ const transactionId = options?.transactionId || "";
1973
+ const getWinningBid = () => {
1974
+ return (auctionId ? this.getCachedBid(`${auctionId}:${adUnitCode}`) : void 0) || (auctionId && slotId !== adUnitCode ? this.getCachedBid(`${auctionId}:${slotId}`) : void 0) || this.getCachedBid(adUnitCode) || (slotId !== adUnitCode ? this.getCachedBid(slotId) : void 0);
1975
+ };
1976
+ let videoEl = null;
1977
+ if (el) {
1978
+ if (el.tagName && el.tagName.toLowerCase() === "video") {
1979
+ videoEl = el;
1980
+ } else {
1981
+ videoEl = el.querySelector("video");
1982
+ }
1983
+ }
1984
+ const quartilesFired = {
1985
+ q1: false,
1986
+ q2: false,
1987
+ q3: false,
1988
+ q4: false
1989
+ };
1990
+ const triggerPlaybackImpression = (activeVideo) => {
1991
+ const vEl = activeVideo || videoEl || (el?.querySelector ? el.querySelector("video") : null);
1992
+ const cached = getWinningBid();
1993
+ const mergedBid = {
1994
+ ...cached?.rawBid || options?.bid,
1995
+ mediaType: "video"
1996
+ };
1997
+ const emitted = this.recordImpression(slotId, {
1998
+ adUnitCode,
1999
+ auctionId: auctionId || cached?.auctionId,
2000
+ transactionId: transactionId || cached?.transactionId,
2001
+ mediaType: "video",
2002
+ bid: mergedBid,
2003
+ metadata: {
2004
+ ...options?.metadata,
2005
+ media_type: "video",
2006
+ ...vEl && Number.isFinite(vEl.duration) ? { video_duration_s: String(Math.round(vEl.duration)) } : {}
2007
+ }
2008
+ });
2009
+ if (emitted && options?.onImpression) {
2010
+ try {
2011
+ options.onImpression(slotId, { element: vEl || el, bid: mergedBid });
2012
+ } catch (e) {
2013
+ this.log("ERROR", "Error in onImpression callback", e);
2014
+ }
2015
+ }
2016
+ };
2017
+ const handlePlaying = (e) => {
2018
+ this.log("DEBUG", "HTML5 video playing event received");
2019
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2020
+ triggerPlaybackImpression(targetVideo);
2021
+ };
2022
+ const handleTimeUpdate = (e) => {
2023
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2024
+ if (targetVideo && targetVideo.currentTime > 0) {
2025
+ triggerPlaybackImpression(targetVideo);
2026
+ if (Number.isFinite(targetVideo.duration) && targetVideo.duration > 0) {
2027
+ const progress = targetVideo.currentTime / targetVideo.duration;
2028
+ if (progress >= 0.25 && !quartilesFired.q1) {
2029
+ quartilesFired.q1 = true;
2030
+ options?.onMilestone?.("firstQuartile", slotId);
2031
+ }
2032
+ if (progress >= 0.5 && !quartilesFired.q2) {
2033
+ quartilesFired.q2 = true;
2034
+ options?.onMilestone?.("midpoint", slotId);
2035
+ }
2036
+ if (progress >= 0.75 && !quartilesFired.q3) {
2037
+ quartilesFired.q3 = true;
2038
+ options?.onMilestone?.("thirdQuartile", slotId);
2039
+ }
2040
+ }
2041
+ }
2042
+ };
2043
+ const handleEnded = () => {
2044
+ if (!quartilesFired.q4) {
2045
+ quartilesFired.q4 = true;
2046
+ options?.onMilestone?.("complete", slotId);
2047
+ }
2048
+ };
2049
+ const handleError = (e) => {
2050
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2051
+ const err = targetVideo?.error || videoEl?.error;
2052
+ const cached = getWinningBid();
2053
+ const msg = err ? `HTML5 video error code ${err.code}: ${err.message}` : "Video playback error";
2054
+ this.log("WARN", msg);
2055
+ this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
2056
+ auctionId: auctionId || cached?.auctionId,
2057
+ transactionId: transactionId || cached?.transactionId,
2058
+ adUnitCode,
2059
+ bid: cached?.bidTrace || options?.bid,
2060
+ metadata: {
2061
+ ...options?.metadata,
2062
+ reason: "html5_video_error",
2063
+ message: msg,
2064
+ ...err?.code ? { video_error_code: String(err.code) } : {}
2065
+ },
2066
+ error: err ? new Error(msg) : void 0
2067
+ });
2068
+ if (options?.onError) {
2069
+ try {
2070
+ options.onError(err);
2071
+ } catch (e2) {
2072
+ this.log("ERROR", "Error in onError callback", e2);
2073
+ }
2074
+ }
2075
+ };
2076
+ const attachListenersToVideo = (targetVideo) => {
2077
+ targetVideo.addEventListener("playing", handlePlaying);
2078
+ targetVideo.addEventListener("play", handlePlaying);
2079
+ targetVideo.addEventListener("timeupdate", handleTimeUpdate);
2080
+ targetVideo.addEventListener("ended", handleEnded);
2081
+ targetVideo.addEventListener("error", handleError);
2082
+ };
2083
+ const removeListenersFromVideo = (targetVideo) => {
2084
+ targetVideo.removeEventListener("playing", handlePlaying);
2085
+ targetVideo.removeEventListener("play", handlePlaying);
2086
+ targetVideo.removeEventListener("timeupdate", handleTimeUpdate);
2087
+ targetVideo.removeEventListener("ended", handleEnded);
2088
+ targetVideo.removeEventListener("error", handleError);
2089
+ };
2090
+ let mutationObserver = null;
2091
+ if (el && el !== videoEl) {
2092
+ el.addEventListener("playing", handlePlaying, true);
2093
+ el.addEventListener("play", handlePlaying, true);
2094
+ el.addEventListener("timeupdate", handleTimeUpdate, true);
2095
+ el.addEventListener("ended", handleEnded, true);
2096
+ el.addEventListener("error", handleError, true);
2097
+ }
2098
+ if (videoEl) {
2099
+ attachListenersToVideo(videoEl);
2100
+ }
2101
+ if (el && el.tagName?.toLowerCase() !== "video" && typeof MutationObserver !== "undefined") {
2102
+ mutationObserver = new MutationObserver(() => {
2103
+ const found = el.querySelector("video");
2104
+ if (found && found !== videoEl) {
2105
+ if (videoEl) removeListenersFromVideo(videoEl);
2106
+ videoEl = found;
2107
+ if (videoEl) attachListenersToVideo(videoEl);
2108
+ }
2109
+ });
2110
+ mutationObserver.observe(el, { childList: true, subtree: true });
2111
+ }
2112
+ const cachedInitial = getWinningBid();
2113
+ if (options?.trackViewability !== false && el) {
2114
+ this.observeSlot(el, slotId, {
2115
+ adUnitCode,
2116
+ auctionId: auctionId || cachedInitial?.auctionId,
2117
+ transactionId: transactionId || cachedInitial?.transactionId,
2118
+ mediaType: "video",
2119
+ bid: cachedInitial?.bidTrace || options?.bid,
2120
+ metadata: options?.metadata,
2121
+ emitRefreshEvent: false
2122
+ });
2123
+ }
2124
+ const cleanup = () => {
2125
+ if (mutationObserver) {
2126
+ mutationObserver.disconnect();
2127
+ mutationObserver = null;
2128
+ }
2129
+ if (el && el !== videoEl) {
2130
+ el.removeEventListener("playing", handlePlaying, true);
2131
+ el.removeEventListener("play", handlePlaying, true);
2132
+ el.removeEventListener("timeupdate", handleTimeUpdate, true);
2133
+ el.removeEventListener("ended", handleEnded, true);
2134
+ el.removeEventListener("error", handleError, true);
2135
+ }
2136
+ if (videoEl) {
2137
+ removeListenersFromVideo(videoEl);
2138
+ }
2139
+ this.videoDetachCleanups.delete(cleanup);
2140
+ };
2141
+ this.videoDetachCleanups.add(cleanup);
2142
+ return cleanup;
1428
2143
  }
1429
2144
  onTimeInViewThreshold(slotId, thresholdMs, callback) {
1430
2145
  const listener = {
@@ -1492,6 +2207,10 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1492
2207
  refreshIndex: record.refreshIndex
1493
2208
  };
1494
2209
  }
2210
+ /** The validated ingest endpoint in use, or "" when none passed validation. */
2211
+ getEndpoint() {
2212
+ return this.config.endpoint;
2213
+ }
1495
2214
  getRefreshIndex(slotId) {
1496
2215
  return this.slotRefreshIndices.get(slotId) ?? 0;
1497
2216
  }
@@ -1532,17 +2251,29 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1532
2251
  handleAuctionInit(data) {
1533
2252
  if (this.isDuplicate("auctionInit", data)) return;
1534
2253
  this.log("DEBUG", "auctionInit", data);
2254
+ const auctionId = data.auctionId || "";
2255
+ if (auctionId) {
2256
+ this.auctionBidderOutcomes.delete(auctionId);
2257
+ this.getOrCreateAuctionOutcomes(auctionId);
2258
+ }
1535
2259
  this.enqueue(TraceEventType.AUCTION_START, "auctionStart", {
1536
- auctionId: data.auctionId || "",
2260
+ auctionId,
1537
2261
  transactionId: data.transactionId || ""
1538
2262
  });
1539
2263
  }
1540
2264
  handleAuctionEnd(data) {
1541
2265
  if (this.isDuplicate("auctionEnd", data)) return;
1542
2266
  this.log("DEBUG", "auctionEnd", data);
2267
+ const auctionId = data.auctionId || "";
2268
+ const outcomesMap = auctionId ? this.auctionBidderOutcomes.get(auctionId) : void 0;
2269
+ const bidderOutcomes = outcomesMap ? Array.from(outcomesMap.values()) : [];
2270
+ if (auctionId) {
2271
+ this.auctionBidderOutcomes.delete(auctionId);
2272
+ }
1543
2273
  this.enqueue(TraceEventType.AUCTION_END, "auctionEnd", {
1544
- auctionId: data.auctionId || "",
1545
- transactionId: data.transactionId || ""
2274
+ auctionId,
2275
+ transactionId: data.transactionId || "",
2276
+ bidderOutcomes
1546
2277
  });
1547
2278
  }
1548
2279
  handleBidRequested(data) {
@@ -1550,121 +2281,170 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1550
2281
  this.log("DEBUG", "bidRequested", data);
1551
2282
  const auctionId = data.auctionId || "";
1552
2283
  const bidder = data.bidderCode || data.bidder || "";
2284
+ if (!auctionId || !bidder) return;
2285
+ const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
1553
2286
  if (Array.isArray(data.bids)) {
1554
2287
  data.bids.forEach((bid) => {
2288
+ const adUnitCode = bid.adUnitCode || "";
1555
2289
  const mediaTypes = Object.keys(bid.mediaTypes || {});
1556
- const gpid = bid.ortb2Imp?.ext?.gpid || bid.gpid || data.gpid || "";
1557
- const transactionId = bid.transactionId || bid.ortb2Imp?.id || data.transactionId || "";
1558
- if (mediaTypes.length === 0) {
1559
- const mediaType = bid.mediaType || "banner";
1560
- const { width, height } = parseSize(bid.sizes || bid.playerSize);
1561
- this.enqueue(TraceEventType.BID_REQUEST, "bidRequest", {
1562
- auctionId,
1563
- transactionId,
1564
- adUnitCode: bid.adUnitCode || "",
1565
- gpid,
1566
- bid: {
1567
- bidder,
1568
- mediaType,
1569
- width,
1570
- height
1571
- }
2290
+ const mediaType = mediaTypes.length > 0 ? mediaTypes[0] : bid.mediaType || "banner";
2291
+ const key = `${bidder}:${adUnitCode}`;
2292
+ if (!auctionMap.has(key)) {
2293
+ auctionMap.set(key, {
2294
+ bidder,
2295
+ adUnitCode,
2296
+ mediaType,
2297
+ outcome: BidderOutcome.BIDDER_OUTCOME_NO_RESPONSE,
2298
+ latencyMs: 0
1572
2299
  });
1573
- return;
1574
2300
  }
1575
- mediaTypes.forEach((mediaType) => {
1576
- let rawSize = null;
1577
- if (mediaType === "banner") {
1578
- rawSize = bid.mediaTypes?.banner?.sizes || bid.sizes;
1579
- } else if (mediaType === "video") {
1580
- rawSize = bid.mediaTypes?.video?.playerSize || bid.mediaTypes?.video?.sizes || bid.playerSize;
1581
- } else {
1582
- rawSize = bid.mediaTypes?.[mediaType]?.sizes;
1583
- }
1584
- const { width, height } = parseSize(rawSize);
1585
- this.enqueue(TraceEventType.BID_REQUEST, "bidRequest", {
1586
- auctionId,
1587
- transactionId,
1588
- adUnitCode: bid.adUnitCode || "",
1589
- gpid,
1590
- bid: {
1591
- bidder,
1592
- mediaType,
1593
- width,
1594
- height
1595
- }
1596
- });
1597
- });
1598
2301
  });
1599
2302
  }
1600
2303
  }
1601
2304
  handleBidResponse(data) {
1602
2305
  if (this.isDuplicate("bidResponse", data)) return;
1603
2306
  this.log("DEBUG", "bidResponse", data);
1604
- this.enqueue(TraceEventType.BID_RESPONSE, "bidResponse", {
1605
- auctionId: data.auctionId || "",
1606
- transactionId: data.transactionId || "",
1607
- adUnitCode: data.adUnitCode || "",
1608
- bid: {
1609
- bidder: data.bidderCode || data.bidder || "",
1610
- cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1611
- currency: data.originalCurrency ?? data.currency ?? "USD",
1612
- ...parseBidDimensions(data),
1613
- dealId: data.dealId || "",
1614
- mediaType: data.mediaType || "banner",
1615
- latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
1616
- advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
1617
- creativeId: data.creativeId || ""
2307
+ const auctionId = data.auctionId || "";
2308
+ const bidder = data.bidderCode || data.bidder || "";
2309
+ const adUnitCode = data.adUnitCode || "";
2310
+ const cpm = Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0;
2311
+ const latencyMs = Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0;
2312
+ const mediaType = data.mediaType || "banner";
2313
+ if (auctionId && bidder) {
2314
+ const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
2315
+ const key = `${bidder}:${adUnitCode}`;
2316
+ const entry = auctionMap.get(key);
2317
+ const outcome = cpm > 0 ? BidderOutcome.BIDDER_OUTCOME_BID : BidderOutcome.BIDDER_OUTCOME_NO_BID;
2318
+ if (entry) {
2319
+ entry.outcome = outcome;
2320
+ entry.latencyMs = latencyMs;
2321
+ if (mediaType) entry.mediaType = mediaType;
2322
+ } else {
2323
+ auctionMap.set(key, {
2324
+ bidder,
2325
+ adUnitCode,
2326
+ mediaType,
2327
+ outcome,
2328
+ latencyMs
2329
+ });
1618
2330
  }
1619
- });
2331
+ }
2332
+ if (cpm > 0) {
2333
+ this.enqueue(TraceEventType.BID_RESPONSE, "bidResponse", {
2334
+ auctionId,
2335
+ transactionId: data.transactionId || "",
2336
+ adUnitCode,
2337
+ bid: {
2338
+ bidder,
2339
+ cpm,
2340
+ currency: data.originalCurrency ?? data.currency ?? "USD",
2341
+ ...parseBidDimensions(data),
2342
+ dealId: data.dealId || "",
2343
+ mediaType,
2344
+ latencyMs,
2345
+ advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
2346
+ creativeId: data.creativeId || ""
2347
+ }
2348
+ });
2349
+ }
1620
2350
  }
1621
2351
  handleBidTimeout(data) {
1622
2352
  if (this.isDuplicate("bidTimeout", data)) return;
1623
2353
  this.log("DEBUG", "bidTimeout", data);
1624
2354
  const items = Array.isArray(data) ? data : data ? [data] : [];
1625
2355
  for (const t of items) {
1626
- this.enqueue(TraceEventType.BID_TIMEOUT, "bidTimeout", {
1627
- auctionId: t.auctionId || "",
1628
- transactionId: t.transactionId || "",
1629
- adUnitCode: t.adUnitCode || "",
1630
- bid: {
1631
- bidder: t.bidderCode || t.bidder || "",
1632
- latencyMs: Number.isFinite(t.timeout) ? t.timeout : 0
2356
+ const auctionId = t.auctionId || "";
2357
+ const bidder = t.bidderCode || t.bidder || "";
2358
+ const adUnitCode = t.adUnitCode || "";
2359
+ const latencyMs = Number.isFinite(t.timeout) ? t.timeout : 0;
2360
+ if (auctionId && bidder) {
2361
+ const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
2362
+ const key = `${bidder}:${adUnitCode}`;
2363
+ const entry = auctionMap.get(key);
2364
+ if (entry) {
2365
+ entry.outcome = BidderOutcome.BIDDER_OUTCOME_TIMEOUT;
2366
+ if (latencyMs > 0) entry.latencyMs = latencyMs;
2367
+ } else {
2368
+ auctionMap.set(key, {
2369
+ bidder,
2370
+ adUnitCode,
2371
+ mediaType: "banner",
2372
+ outcome: BidderOutcome.BIDDER_OUTCOME_TIMEOUT,
2373
+ latencyMs
2374
+ });
1633
2375
  }
1634
- });
2376
+ }
1635
2377
  }
1636
2378
  }
1637
2379
  handleBidWon(data) {
1638
2380
  if (this.isDuplicate("bidWon", data)) return;
1639
2381
  this.log("DEBUG", "bidWon", data);
1640
- this.enqueue(TraceEventType.BID_WIN, "bidWon", {
1641
- auctionId: data.auctionId || "",
1642
- transactionId: data.transactionId || "",
1643
- adUnitCode: data.adUnitCode || "",
1644
- bid: {
1645
- bidder: data.bidderCode || data.bidder || "",
1646
- cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1647
- currency: data.originalCurrency ?? data.currency ?? "USD",
1648
- ...parseBidDimensions(data),
1649
- dealId: data.dealId || "",
1650
- mediaType: data.mediaType || "banner",
1651
- latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
1652
- advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
1653
- creativeId: data.creativeId || ""
2382
+ const auctionId = data.auctionId || "";
2383
+ const transactionId = data.transactionId || "";
2384
+ const adUnitCode = data.adUnitCode || data.adId || "";
2385
+ const bidTrace = {
2386
+ bidder: data.bidderCode || data.bidder || "",
2387
+ cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
2388
+ currency: data.originalCurrency ?? data.currency ?? "USD",
2389
+ ...parseBidDimensions(data),
2390
+ dealId: data.dealId || "",
2391
+ mediaType: data.mediaType || "banner",
2392
+ latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
2393
+ advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
2394
+ creativeId: data.creativeId || ""
2395
+ };
2396
+ if (adUnitCode) {
2397
+ const cachedEntry = {
2398
+ bidTrace,
2399
+ auctionId,
2400
+ transactionId,
2401
+ adUnitCode,
2402
+ rawBid: data,
2403
+ timestamp: Date.now()
2404
+ };
2405
+ this.cachedWinningBids.set(adUnitCode, cachedEntry);
2406
+ if (data.adId && data.adId !== adUnitCode) {
2407
+ this.cachedWinningBids.set(data.adId, cachedEntry);
1654
2408
  }
2409
+ if (auctionId) {
2410
+ this.cachedWinningBids.set(`${auctionId}:${adUnitCode}`, cachedEntry);
2411
+ if (data.adId && data.adId !== adUnitCode) {
2412
+ this.cachedWinningBids.set(`${auctionId}:${data.adId}`, cachedEntry);
2413
+ }
2414
+ }
2415
+ this.pruneCachedWinningBids();
2416
+ }
2417
+ this.enqueue(TraceEventType.BID_WIN, "bidWon", {
2418
+ auctionId,
2419
+ transactionId,
2420
+ adUnitCode,
2421
+ bid: bidTrace
1655
2422
  });
1656
2423
  }
1657
2424
  handleNoBid(data) {
1658
2425
  if (this.isDuplicate("noBid", data)) return;
1659
2426
  this.log("DEBUG", "noBid", data);
1660
- this.enqueue(TraceEventType.NO_BID, "noBid", {
1661
- auctionId: data.auctionId || "",
1662
- transactionId: data.transactionId || "",
1663
- adUnitCode: data.adUnitCode || "",
1664
- bid: {
1665
- bidder: data.bidderCode || data.bidder || ""
2427
+ const auctionId = data.auctionId || "";
2428
+ const bidder = data.bidderCode || data.bidder || "";
2429
+ const adUnitCode = data.adUnitCode || "";
2430
+ if (auctionId && bidder) {
2431
+ const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
2432
+ const key = `${bidder}:${adUnitCode}`;
2433
+ const entry = auctionMap.get(key);
2434
+ if (entry) {
2435
+ if (entry.outcome === BidderOutcome.BIDDER_OUTCOME_NO_RESPONSE) {
2436
+ entry.outcome = BidderOutcome.BIDDER_OUTCOME_NO_BID;
2437
+ }
2438
+ } else {
2439
+ auctionMap.set(key, {
2440
+ bidder,
2441
+ adUnitCode,
2442
+ mediaType: "banner",
2443
+ outcome: BidderOutcome.BIDDER_OUTCOME_NO_BID,
2444
+ latencyMs: 0
2445
+ });
1666
2446
  }
1667
- });
2447
+ }
1668
2448
  }
1669
2449
  handleAdRenderFailed(data) {
1670
2450
  if (this.isDuplicate("adRenderFailed", data)) return;
@@ -1690,44 +2470,81 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1690
2470
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1691
2471
  this.log("DEBUG", "adRenderSucceeded", data);
1692
2472
  const bid = data.bid || data || {};
1693
- const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
2473
+ const adUnitCode = data.adUnitCode || bid.adUnitCode || data.adId || bid.adId || "";
1694
2474
  const auctionId = bid.auctionId || data.auctionId || "";
1695
2475
  const transactionId = bid.transactionId || data.transactionId || "";
1696
2476
  const mediaType = bid.mediaType || "banner";
2477
+ const slotKey = data.adUnitCode || bid.adUnitCode || "";
2478
+ const altKey = data.adId || bid.adId || "";
2479
+ const cached = (auctionId && slotKey ? this.getCachedBid(`${auctionId}:${slotKey}`) : void 0) || (auctionId && altKey ? this.getCachedBid(`${auctionId}:${altKey}`) : void 0) || (slotKey ? this.getCachedBid(slotKey) : void 0) || (altKey ? this.getCachedBid(altKey) : void 0);
1697
2480
  const bidTrace = {
1698
- bidder: bid.bidderCode || bid.bidder || "",
1699
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1700
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1701
- ...parseBidDimensions(bid),
1702
- dealId: bid.dealId || "",
1703
- mediaType,
1704
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1705
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1706
- creativeId: bid.creativeId || ""
2481
+ bidder: bid.bidderCode || bid.bidder || cached?.bidTrace?.bidder || "",
2482
+ cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : cached?.bidTrace?.cpm ?? 0,
2483
+ currency: bid.originalCurrency ?? bid.currency ?? cached?.bidTrace?.currency ?? "USD",
2484
+ ...parseBidDimensions(bid).width ? parseBidDimensions(bid) : cached?.bidTrace ? { width: cached.bidTrace.width, height: cached.bidTrace.height } : parseBidDimensions(bid),
2485
+ dealId: bid.dealId || cached?.bidTrace?.dealId || "",
2486
+ mediaType: mediaType || cached?.bidTrace?.mediaType || "banner",
2487
+ latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : cached?.bidTrace?.latencyMs ?? 0,
2488
+ advertiserDomain: bid.meta?.advertiserDomains?.[0] || cached?.bidTrace?.advertiserDomain || "",
2489
+ creativeId: bid.creativeId || cached?.bidTrace?.creativeId || ""
1707
2490
  };
1708
- const isRefresh = adUnitCode && (this.slotViewabilityRecords.has(adUnitCode) || this.slotRefreshIndices.has(adUnitCode) && this.slotRefreshIndices.get(adUnitCode) > 0);
2491
+ const resolvedAuctionId = auctionId || cached?.auctionId || "";
2492
+ const resolvedTransactionId = transactionId || cached?.transactionId || "";
2493
+ const record = adUnitCode ? this.slotViewabilityRecords.get(adUnitCode) : void 0;
2494
+ const isDifferentAuction = record && resolvedAuctionId && record.auctionId && record.auctionId !== resolvedAuctionId;
2495
+ const isRefresh = Boolean(adUnitCode && record && isDifferentAuction);
1709
2496
  if (isRefresh) {
1710
2497
  this.flushSlotTimeInView(adUnitCode);
1711
- const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
2498
+ const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0)) + 1;
1712
2499
  this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
2500
+ this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
2501
+ if (record) {
2502
+ record.refreshIndex = nextRefreshIndex;
2503
+ record.auctionId = resolvedAuctionId;
2504
+ record.transactionId = resolvedTransactionId;
2505
+ record.bidPayload = bidTrace;
2506
+ record.viewableFired = false;
2507
+ record.accumulatedTimeInViewMs = 0;
2508
+ }
1713
2509
  this.enqueue(TraceEventType.REFRESH, "refresh", {
1714
- auctionId,
1715
- transactionId,
2510
+ auctionId: resolvedAuctionId,
2511
+ transactionId: resolvedTransactionId,
1716
2512
  adUnitCode,
1717
2513
  bid: bidTrace,
1718
2514
  metadata: { refresh_index: String(nextRefreshIndex) }
1719
2515
  });
1720
- } else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
1721
- this.slotRefreshIndices.set(adUnitCode, 0);
2516
+ } else if (adUnitCode) {
2517
+ if (resolvedAuctionId) {
2518
+ this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
2519
+ }
2520
+ if (!this.slotRefreshIndices.has(adUnitCode)) {
2521
+ this.slotRefreshIndices.set(adUnitCode, 0);
2522
+ }
1722
2523
  }
1723
- const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
1724
- this.enqueue(TraceEventType.IMPRESSION, "impression", {
1725
- auctionId,
1726
- transactionId,
2524
+ const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0) : 0;
2525
+ const alreadyEmitted = adUnitCode ? this.hasImpressionEmitted(
1727
2526
  adUnitCode,
1728
- bid: bidTrace,
1729
- metadata: { refresh_index: String(currentRefreshIndex) }
1730
- });
2527
+ currentRefreshIndex,
2528
+ resolvedAuctionId,
2529
+ bidTrace.creativeId
2530
+ ) : false;
2531
+ if (!alreadyEmitted) {
2532
+ if (adUnitCode) {
2533
+ this.markImpressionEmitted(
2534
+ adUnitCode,
2535
+ currentRefreshIndex,
2536
+ resolvedAuctionId,
2537
+ bidTrace.creativeId
2538
+ );
2539
+ }
2540
+ this.enqueue(TraceEventType.IMPRESSION, "impression", {
2541
+ auctionId: resolvedAuctionId,
2542
+ transactionId: resolvedTransactionId,
2543
+ adUnitCode,
2544
+ bid: bidTrace,
2545
+ metadata: { refresh_index: String(currentRefreshIndex) }
2546
+ });
2547
+ }
1731
2548
  if (this.config.viewabilityEnabled && typeof document !== "undefined") {
1732
2549
  let el = null;
1733
2550
  const targetId = adUnitCode || data.adId || bid.adId;
@@ -1748,15 +2565,20 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1748
2565
  }
1749
2566
  if (!el && targetId) {
1750
2567
  try {
1751
- el = document.querySelector(`[data-ad-slot="${targetId}"]`) || document.querySelector(`#${targetId}`);
2568
+ for (const candidate of Array.from(document.querySelectorAll("[data-ad-slot]"))) {
2569
+ if (candidate.getAttribute("data-ad-slot") === targetId) {
2570
+ el = candidate;
2571
+ break;
2572
+ }
2573
+ }
1752
2574
  } catch {
1753
2575
  }
1754
2576
  }
1755
2577
  if (el) {
1756
2578
  this.observeSlot(el, adUnitCode || targetId, {
1757
2579
  adUnitCode,
1758
- auctionId,
1759
- transactionId,
2580
+ auctionId: resolvedAuctionId,
2581
+ transactionId: resolvedTransactionId,
1760
2582
  mediaType,
1761
2583
  bid: bidTrace,
1762
2584
  refreshIndex: currentRefreshIndex,
@@ -1802,6 +2624,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1802
2624
  if (data.viewableDurationMs !== void 0) {
1803
2625
  protoEvent.viewableDurationMs = Number.isFinite(data.viewableDurationMs) ? data.viewableDurationMs : 0;
1804
2626
  }
2627
+ if (data.bidderOutcomes && Array.isArray(data.bidderOutcomes)) {
2628
+ protoEvent.bidderOutcomes = data.bidderOutcomes;
2629
+ }
1805
2630
  const metadata = { ...data.metadata };
1806
2631
  if (data.error !== void 0 && data.error !== null) {
1807
2632
  metadata.error = typeof data.error === "object" && data.error.message ? String(data.error.message) : String(data.error);
@@ -1812,8 +2637,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1812
2637
  if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
1813
2638
  metadata.refresh_index = String(data.refreshIndex);
1814
2639
  }
1815
- if (Object.keys(metadata).length > 0) {
1816
- protoEvent.metadata = metadata;
2640
+ const boundedMetadata = boundMetadata(metadata);
2641
+ if (Object.keys(boundedMetadata).length > 0) {
2642
+ protoEvent.metadata = boundedMetadata;
1817
2643
  }
1818
2644
  this.queue.push(protoEvent);
1819
2645
  if (this.queue.length > MAX_QUEUE_SIZE) {
@@ -1851,7 +2677,6 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1851
2677
  // Shared by flush() and flushBeacon().
1852
2678
  drainBatch() {
1853
2679
  if (this.queue.length === 0 || !this.config.endpoint) return null;
1854
- let events = this.queue;
1855
2680
  const domain = typeof window !== "undefined" ? window.location.hostname : "";
1856
2681
  const buildBatch = (evts) => ({
1857
2682
  propertyId: this.config.propertyId,
@@ -1865,25 +2690,37 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1865
2690
  domain,
1866
2691
  events: evts
1867
2692
  });
1868
- let encoded = TraceEventBatch.encode(
1869
- buildBatch(events)
1870
- ).finish();
1871
- while (events.length > 1 && encoded.byteLength > MAX_PAYLOAD_BYTES) {
1872
- events = events.slice(0, Math.floor(events.length / 2));
1873
- encoded = TraceEventBatch.encode(
2693
+ for (; ; ) {
2694
+ if (this.queue.length === 0) return null;
2695
+ let events = this.queue;
2696
+ let encoded = TraceEventBatch.encode(
1874
2697
  buildBatch(events)
1875
2698
  ).finish();
2699
+ while (events.length > 1 && encoded.byteLength > MAX_PAYLOAD_BYTES) {
2700
+ events = events.slice(0, Math.floor(events.length / 2));
2701
+ encoded = TraceEventBatch.encode(
2702
+ buildBatch(events)
2703
+ ).finish();
2704
+ }
2705
+ if (encoded.byteLength > MAX_PAYLOAD_BYTES) {
2706
+ const [dropped] = this.queue.splice(0, 1);
2707
+ this.log(
2708
+ "WARN",
2709
+ `Dropping oversized event ${dropped?.eventName || dropped?.eventId} (${encoded.byteLength} bytes exceeds the ${MAX_PAYLOAD_BYTES}-byte cap on its own)`
2710
+ );
2711
+ continue;
2712
+ }
2713
+ this.queue = this.queue.slice(events.length);
2714
+ return {
2715
+ url: `${this.config.endpoint}/${this.config.propertyId}`,
2716
+ // protobufjs types finish() as Uint8Array<ArrayBufferLike>; the buffer is
2717
+ // always a plain ArrayBuffer, so narrow for fetch/Blob compatibility.
2718
+ encoded,
2719
+ events
2720
+ };
1876
2721
  }
1877
- this.queue = this.queue.slice(events.length);
1878
- return {
1879
- url: `${this.config.endpoint}/${this.config.propertyId}`,
1880
- // protobufjs types finish() as Uint8Array<ArrayBufferLike>; the buffer is
1881
- // always a plain ArrayBuffer, so narrow for fetch/Blob compatibility.
1882
- encoded,
1883
- events
1884
- };
1885
2722
  }
1886
- sendFetch(payload, useKeepalive = false, onDelivered) {
2723
+ sendFetch(payload, useKeepalive = false, onDelivered, isReplay = false) {
1887
2724
  const fetchOpts = {
1888
2725
  method: "POST",
1889
2726
  // Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
@@ -1900,15 +2737,17 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1900
2737
  this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
1901
2738
  return;
1902
2739
  }
1903
- this.handleSendFailure(payload.events);
2740
+ if (!isReplay) this.handleSendFailure(payload.events);
1904
2741
  } else {
1905
- this.consecutiveSendFailures = 0;
1906
- this.nextSendAllowedAt = 0;
2742
+ if (!isReplay) {
2743
+ this.consecutiveSendFailures = 0;
2744
+ this.nextSendAllowedAt = 0;
2745
+ }
1907
2746
  if (onDelivered) onDelivered();
1908
2747
  }
1909
2748
  }).catch((err) => {
1910
2749
  this.log("ERROR", "Failed to send batch", err);
1911
- this.handleSendFailure(payload.events);
2750
+ if (!isReplay) this.handleSendFailure(payload.events);
1912
2751
  });
1913
2752
  const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
1914
2753
  if (proc && typeof proc._tickCallback === "function") {
@@ -1964,16 +2803,23 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1964
2803
  this.schedulePersistedCleanup();
1965
2804
  }
1966
2805
  // --- Exit-batch persistence -----------------------------------------------
2806
+ // Key prefix for this instance's property. encodeURIComponent keeps the
2807
+ // property segment free of the "_" the timestamp suffix is parsed against,
2808
+ // so a property ID containing one cannot forge another property's prefix.
2809
+ pendingKeyPrefix() {
2810
+ return `${PENDING_BATCH_KEY_PREFIX}${encodeURIComponent(this.config.propertyId)}_`;
2811
+ }
1967
2812
  persistBatch(encoded) {
1968
2813
  try {
1969
2814
  if (typeof localStorage === "undefined") return null;
2815
+ if (!this.config.propertyId) return null;
1970
2816
  let pendingCount = 0;
1971
2817
  for (let i = 0; i < localStorage.length; i++) {
1972
2818
  const k = localStorage.key(i);
1973
2819
  if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
1974
2820
  }
1975
2821
  if (pendingCount >= MAX_PENDING_BATCHES) return null;
1976
- const key = `${PENDING_BATCH_KEY_PREFIX}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
2822
+ const key = `${this.pendingKeyPrefix()}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
1977
2823
  localStorage.setItem(key, bytesToBase64(encoded));
1978
2824
  this.persistedBatchKeys.push(key);
1979
2825
  return key;
@@ -1998,22 +2844,39 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1998
2844
  if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
1999
2845
  this.persistedCleanupTimer = setTimeout(() => {
2000
2846
  this.persistedCleanupTimer = null;
2001
- for (const key of [...this.persistedBatchKeys]) {
2847
+ for (const key of Array.from(this.persistedBatchKeys)) {
2002
2848
  this.removePersistedBatch(key);
2003
2849
  }
2004
2850
  }, PENDING_BATCH_CLEANUP_DELAY_MS);
2005
2851
  }
2006
2852
  // Resend batches a previous pageview persisted at exit but could not
2007
- // confirm. Keys are claimed (removed) before sending so concurrent tabs
2008
- // don't double-send; the batch bytes carry their original pageview, session,
2009
- // and event timestamps, so late rows attribute correctly.
2853
+ // confirm. Only this property's own keys are claimed: batches belonging to
2854
+ // another property sharing this origin are left for that property's next
2855
+ // pageview, since resending them here would file them under this property.
2856
+ // Keys are claimed (removed) before sending so concurrent tabs don't
2857
+ // double-send; the batch bytes carry their original pageview, session, and
2858
+ // event timestamps, so late rows attribute correctly.
2010
2859
  resendPersistedBatches() {
2011
2860
  try {
2012
2861
  if (typeof localStorage === "undefined" || !this.config.endpoint) return;
2862
+ if (!this.config.propertyId) return;
2863
+ const ownPrefix = this.pendingKeyPrefix();
2013
2864
  const keys = [];
2865
+ const legacyKeys = [];
2014
2866
  for (let i = 0; i < localStorage.length; i++) {
2015
2867
  const k = localStorage.key(i);
2016
- if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) keys.push(k);
2868
+ if (!k || !k.startsWith(PENDING_BATCH_KEY_PREFIX)) continue;
2869
+ if (k.startsWith(ownPrefix)) {
2870
+ keys.push(k);
2871
+ } else if (isLegacyPendingKey(k)) {
2872
+ legacyKeys.push(k);
2873
+ }
2874
+ }
2875
+ for (const key of legacyKeys) {
2876
+ try {
2877
+ localStorage.removeItem(key);
2878
+ } catch {
2879
+ }
2017
2880
  }
2018
2881
  for (const key of keys) {
2019
2882
  let value = null;
@@ -2024,7 +2887,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2024
2887
  continue;
2025
2888
  }
2026
2889
  if (!value) continue;
2027
- const ts = parseInt(key.slice(PENDING_BATCH_KEY_PREFIX.length), 10);
2890
+ const ts = parseInt(key.slice(ownPrefix.length), 10);
2028
2891
  if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
2029
2892
  let encoded;
2030
2893
  try {
@@ -2034,11 +2897,16 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2034
2897
  }
2035
2898
  if (encoded.byteLength === 0) continue;
2036
2899
  this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
2037
- this.sendFetch({
2038
- url: `${this.config.endpoint}/${this.config.propertyId}`,
2039
- encoded,
2040
- events: []
2041
- });
2900
+ this.sendFetch(
2901
+ {
2902
+ url: `${this.config.endpoint}/${this.config.propertyId}`,
2903
+ encoded,
2904
+ events: []
2905
+ },
2906
+ false,
2907
+ void 0,
2908
+ true
2909
+ );
2042
2910
  }
2043
2911
  } catch {
2044
2912
  }
@@ -2063,11 +2931,13 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2063
2931
  }
2064
2932
  }
2065
2933
  };
2066
- function extractAnalyticsOptions(config) {
2934
+ function extractAnalyticsOptions(config, allowProviderless = true) {
2067
2935
  if (!config) return null;
2068
2936
  if (Array.isArray(config)) {
2069
- const matched = config.find((item) => item?.provider === "bidkernel") ?? config.find((item) => item && typeof item === "object" && item.provider === void 0);
2070
- return matched ? extractAnalyticsOptions(matched) : null;
2937
+ const matched = config.find((item) => item?.provider === "bidkernel") ?? (allowProviderless ? config.find(
2938
+ (item) => item && typeof item === "object" && item.provider === void 0
2939
+ ) : void 0);
2940
+ return matched ? extractAnalyticsOptions(matched, allowProviderless) : null;
2071
2941
  }
2072
2942
  if (typeof config === "object") {
2073
2943
  if (config.options && typeof config.options === "object") {
@@ -2109,18 +2979,27 @@ function registerPrebidAnalytics(pbjsGlobalName = "pbjs", defaults) {
2109
2979
  win[pbjsGlobalName] = win[pbjsGlobalName] || {};
2110
2980
  const pbjs = win[pbjsGlobalName];
2111
2981
  const enableInstance = (options) => {
2112
- if (win._bidkernelPrebidAnalytics) {
2982
+ const previous = win._bidkernelPrebidAnalytics;
2983
+ const pinnedEndpoint = previous?.getEndpoint?.() || "";
2984
+ const merged = {
2985
+ pbjsGlobalName,
2986
+ ...defaults,
2987
+ ...options
2988
+ };
2989
+ if (pinnedEndpoint && !sameEndpointOrigin(merged.endpoint || "", pinnedEndpoint)) {
2990
+ console.warn(
2991
+ `[bidkernel] Ignoring analytics endpoint change to ${merged.endpoint || "(empty)"}: this page is pinned to ${pinnedEndpoint}. Reload to change the ingest endpoint.`
2992
+ );
2993
+ merged.endpoint = pinnedEndpoint;
2994
+ }
2995
+ if (previous) {
2113
2996
  try {
2114
- win._bidkernelPrebidAnalytics.disable();
2997
+ previous.disable();
2115
2998
  } catch (e) {
2116
2999
  console.warn("[bidkernel] Failed to disable previous analytics instance:", e);
2117
3000
  }
2118
3001
  }
2119
- const analytics = new BidkernelPrebidAnalytics({
2120
- pbjsGlobalName,
2121
- ...defaults,
2122
- ...options
2123
- });
3002
+ const analytics = new BidkernelPrebidAnalytics(merged);
2124
3003
  analytics.enable();
2125
3004
  win._bidkernelPrebidAnalytics = analytics;
2126
3005
  return analytics;
@@ -2131,7 +3010,7 @@ function registerPrebidAnalytics(pbjsGlobalName = "pbjs", defaults) {
2131
3010
  pbjs.adapterManager.registerAnalyticsAdapter({
2132
3011
  adapter: {
2133
3012
  enableAnalytics: (config) => {
2134
- const options = extractAnalyticsOptions(config);
3013
+ const options = extractAnalyticsOptions(config, !Array.isArray(config));
2135
3014
  if (options) {
2136
3015
  enableInstance(options);
2137
3016
  }
@@ -2168,7 +3047,28 @@ function getbidkernel(alias = "bidkernel") {
2168
3047
  }
2169
3048
  const win = window;
2170
3049
  win[alias] = win[alias] || { q: [] };
2171
- return win[alias];
3050
+ const sdk = win[alias];
3051
+ if (!sdk.attachVideoPlayer) {
3052
+ sdk.attachVideoPlayer = (target, options) => {
3053
+ const inst = win._bidkernelPrebidAnalytics;
3054
+ return inst?.attachVideoPlayer ? inst.attachVideoPlayer(target, options) : () => {
3055
+ };
3056
+ };
3057
+ }
3058
+ if (!sdk.attachImaAdsManager) {
3059
+ sdk.attachImaAdsManager = (adsManager, options) => {
3060
+ const inst = win._bidkernelPrebidAnalytics;
3061
+ return inst?.attachImaAdsManager ? inst.attachImaAdsManager(adsManager, options) : () => {
3062
+ };
3063
+ };
3064
+ }
3065
+ if (!sdk.recordImpression) {
3066
+ sdk.recordImpression = (slotId, options) => {
3067
+ const inst = win._bidkernelPrebidAnalytics;
3068
+ return inst?.recordImpression ? inst.recordImpression(slotId, options) : false;
3069
+ };
3070
+ }
3071
+ return sdk;
2172
3072
  }
2173
3073
  // Annotate the CommonJS export names for ESM import in node:
2174
3074
  0 && (module.exports = {
@@ -2176,5 +3076,6 @@ function getbidkernel(alias = "bidkernel") {
2176
3076
  PrebidEventDeduper,
2177
3077
  getPrebidEventKey,
2178
3078
  getbidkernel,
3079
+ isTrustedEndpoint,
2179
3080
  registerPrebidAnalytics
2180
3081
  });