@bidkernel/analytics 0.3.0 → 0.5.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.mjs CHANGED
@@ -627,6 +627,7 @@ var MAX_ERRORS_PER_SESSION = 50;
627
627
  var MAX_QUEUE_SIZE = 200;
628
628
  var MAX_SEND_BACKOFF_MS = 5 * 60 * 1e3;
629
629
  var MAX_CONSECUTIVE_SEND_FAILURES = 10;
630
+ var MAX_PAYLOAD_BYTES = 32 * 1024;
630
631
  var EVENT_NAME_TO_TYPE = {
631
632
  auctionStart: TraceEventType.AUCTION_START,
632
633
  auctionEnd: TraceEventType.AUCTION_END,
@@ -693,47 +694,73 @@ function getOrCreateSessionId() {
693
694
  inMemorySessionTs = now;
694
695
  return inMemorySessionId;
695
696
  }
697
+ function escapeKeyPart(str) {
698
+ return String(str ?? "").replace(/%/g, "%25").replace(/:/g, "%3A");
699
+ }
696
700
  function getPrebidEventKey(eventName, data) {
697
701
  switch (eventName) {
698
702
  case "auctionInit":
699
- return `auctionInit:${data?.auctionId || ""}`;
703
+ return `auctionInit:${escapeKeyPart(data?.auctionId)}`;
700
704
  case "auctionEnd":
701
- return `auctionEnd:${data?.auctionId || ""}`;
702
- case "bidRequested":
703
- return `bidRequested:${data?.auctionId || ""}:${data?.bidderCode || ""}:${Array.isArray(data?.bids) ? data.bids.map((b) => b?.bidId || b?.adUnitCode || "").join(",") : ""}`;
704
- case "bidResponse":
705
- return `bidResponse:${data?.auctionId || ""}:${data?.adUnitCode || ""}:${data?.bidderCode || data?.bidder || ""}:${data?.creativeId || data?.adId || data?.requestId || ""}:${data?.originalCpm ?? data?.cpm ?? ""}`;
706
- case "bidTimeout":
707
- if (Array.isArray(data)) {
708
- return "bidTimeout:" + data.map(
709
- (t) => `${t?.auctionId || ""}:${t?.bidder || t?.bidderCode || ""}:${t?.bidId || t?.adUnitCode || ""}`
710
- ).join(",");
711
- }
712
- return `bidTimeout:${data?.auctionId || ""}:${data?.bidder || data?.bidderCode || ""}:${data?.bidId || data?.adUnitCode || ""}`;
713
- case "bidWon":
714
- return `bidWon:${data?.auctionId || ""}:${data?.adUnitCode || ""}:${data?.bidderCode || data?.bidder || ""}:${data?.creativeId || data?.adId || data?.requestId || ""}:${data?.originalCpm ?? data?.cpm ?? ""}`;
705
+ return `auctionEnd:${escapeKeyPart(data?.auctionId)}`;
706
+ case "bidRequested": {
707
+ const bids = Array.isArray(data?.bids) ? data.bids.map((b) => escapeKeyPart(b?.bidId || b?.transactionId || b?.adUnitCode)).join(",") : "";
708
+ return `bidRequested:${escapeKeyPart(data?.auctionId)}:${escapeKeyPart(data?.bidderCode || data?.bidder)}:${bids}`;
709
+ }
710
+ case "bidResponse": {
711
+ const bid = data || {};
712
+ const uniqueBidId = bid.creativeId || bid.adId || bid.requestId || bid.bidId || "";
713
+ const price = bid.originalCpm ?? bid.cpm ?? "";
714
+ return `bidResponse:${escapeKeyPart(bid.auctionId)}:${escapeKeyPart(bid.adUnitCode)}:${escapeKeyPart(bid.bidderCode || bid.bidder)}:${escapeKeyPart(uniqueBidId)}:${escapeKeyPart(price)}`;
715
+ }
716
+ case "bidTimeout": {
717
+ const items = Array.isArray(data) ? data : data ? [data] : [];
718
+ return "bidTimeout:" + items.map(
719
+ (t) => `${escapeKeyPart(t?.auctionId)}:${escapeKeyPart(t?.bidderCode || t?.bidder)}:${escapeKeyPart(t?.bidId || t?.transactionId || t?.adUnitCode)}`
720
+ ).join(",");
721
+ }
722
+ case "bidWon": {
723
+ const bid = data || {};
724
+ const uniqueBidId = bid.creativeId || bid.adId || bid.requestId || bid.bidId || "";
725
+ const price = bid.originalCpm ?? bid.cpm ?? "";
726
+ return `bidWon:${escapeKeyPart(bid.auctionId)}:${escapeKeyPart(bid.adUnitCode)}:${escapeKeyPart(bid.bidderCode || bid.bidder)}:${escapeKeyPart(uniqueBidId)}:${escapeKeyPart(price)}`;
727
+ }
715
728
  case "noBid":
716
- return `noBid:${data?.auctionId || ""}:${data?.adUnitCode || ""}:${data?.bidderCode || data?.bidder || ""}:${data?.bidId || ""}`;
717
- case "adRenderFailed":
718
- return `adRenderFailed:${data?.bid?.auctionId || data?.auctionId || ""}:${data?.bid?.adUnitCode || data?.adUnitCode || ""}:${data?.bid?.bidderCode || data?.bid?.bidder || ""}:${data?.reason || ""}:${data?.message || ""}`;
719
- case "adRenderSucceeded":
720
- return `adRenderSucceeded:${data?.bid?.auctionId || data?.auctionId || ""}:${data?.bid?.adUnitCode || data?.adUnitCode || ""}:${data?.bid?.bidderCode || data?.bid?.bidder || ""}`;
729
+ return `noBid:${escapeKeyPart(data?.auctionId)}:${escapeKeyPart(data?.adUnitCode)}:${escapeKeyPart(data?.bidderCode || data?.bidder)}:${escapeKeyPart(data?.bidId || data?.transactionId)}`;
730
+ case "adRenderFailed": {
731
+ const bid = data?.bid || {};
732
+ return `adRenderFailed:${escapeKeyPart(bid.auctionId || data?.auctionId)}:${escapeKeyPart(bid.adUnitCode || data?.adUnitCode)}:${escapeKeyPart(bid.bidderCode || bid.bidder || data?.bidderCode || data?.bidder)}:${escapeKeyPart(data?.reason)}:${escapeKeyPart(data?.message)}`;
733
+ }
734
+ case "adRenderSucceeded": {
735
+ const bid = data?.bid || data || {};
736
+ const uniqueId = bid.creativeId || bid.adId || data?.adId || "";
737
+ const price = bid.originalCpm ?? bid.cpm ?? "";
738
+ return `adRenderSucceeded:${escapeKeyPart(bid.auctionId || data?.auctionId)}:${escapeKeyPart(bid.adUnitCode || data?.adUnitCode)}:${escapeKeyPart(bid.bidderCode || bid.bidder)}:${escapeKeyPart(uniqueId)}:${escapeKeyPart(price)}`;
739
+ }
721
740
  case "setTargeting":
722
- return `setTargeting:${Object.keys(data || {}).sort().join(",")}`;
741
+ return `setTargeting:${Object.keys(data || {}).sort().map(escapeKeyPart).join(",")}`;
723
742
  case "auctionDebug":
724
- return `auctionDebug:${data?.type || ""}:${String(data?.arguments?.[0] ?? "").slice(0, 50)}`;
743
+ return `auctionDebug:${escapeKeyPart(data?.type)}:${escapeKeyPart(String(data?.arguments?.[0] ?? "").slice(0, 50))}`;
725
744
  default:
726
745
  return "";
727
746
  }
728
747
  }
729
748
  var PrebidEventDeduper = class {
730
- seenObjects = /* @__PURE__ */ new WeakSet();
749
+ // Object identity is tracked per event name: Prebid passes the SAME bid
750
+ // object to bidResponse and later to bidWon, so a bare WeakSet would drop
751
+ // every bidWon as a duplicate of its own bidResponse.
752
+ seenObjects = /* @__PURE__ */ new WeakMap();
731
753
  seenKeys = /* @__PURE__ */ new Set();
732
754
  isDuplicate(eventName, data) {
733
755
  if (!data) return false;
734
756
  if (typeof data === "object") {
735
- if (this.seenObjects.has(data)) return true;
736
- this.seenObjects.add(data);
757
+ const seenEvents = this.seenObjects.get(data);
758
+ if (seenEvents) {
759
+ if (seenEvents.has(eventName)) return true;
760
+ seenEvents.add(eventName);
761
+ } else {
762
+ this.seenObjects.set(data, /* @__PURE__ */ new Set([eventName]));
763
+ }
737
764
  }
738
765
  const key = getPrebidEventKey(eventName, data);
739
766
  if (key) {
@@ -763,7 +790,23 @@ function parseSize(raw) {
763
790
  }
764
791
  return { width: 0, height: 0 };
765
792
  }
766
- var BidkernelPrebidAnalytics = class {
793
+ function parseBidDimensions(bid) {
794
+ if (Number.isFinite(bid?.width) && Number.isFinite(bid?.height)) {
795
+ return { width: bid.width, height: bid.height };
796
+ }
797
+ if (typeof bid?.size === "string") {
798
+ const match = bid.size.match(/^(\d+)x(\d+)$/);
799
+ if (match) {
800
+ return { width: Number(match[1]), height: Number(match[2]) };
801
+ }
802
+ }
803
+ return {
804
+ width: Number.isFinite(bid?.width) ? bid.width : 0,
805
+ height: Number.isFinite(bid?.height) ? bid.height : 0
806
+ };
807
+ }
808
+ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
809
+ static activeInstances = /* @__PURE__ */ new Set();
767
810
  config;
768
811
  queue = [];
769
812
  errorCount = 0;
@@ -778,6 +821,13 @@ var BidkernelPrebidAnalytics = class {
778
821
  deduper = new PrebidEventDeduper();
779
822
  consecutiveSendFailures = 0;
780
823
  nextSendAllowedAt = 0;
824
+ replayedEventCount = 0;
825
+ // Viewability & refresh tracking
826
+ intersectionObserver = null;
827
+ slotViewabilityRecords = /* @__PURE__ */ new Map();
828
+ elementToSlotId = /* @__PURE__ */ new Map();
829
+ slotRefreshIndices = /* @__PURE__ */ new Map();
830
+ pendingThresholdListeners = /* @__PURE__ */ new Map();
781
831
  constructor(config) {
782
832
  this.config = {
783
833
  endpoint: config.endpoint || "",
@@ -791,19 +841,36 @@ var BidkernelPrebidAnalytics = class {
791
841
  auctionEnabled: config.auctionEnabled ?? true,
792
842
  warningsEnabled: config.warningsEnabled ?? true,
793
843
  errorsEnabled: config.errorsEnabled ?? true,
844
+ viewabilityEnabled: config.viewabilityEnabled ?? true,
794
845
  logLevel: config.logLevel || "INFO",
795
846
  pbjsGlobalName: config.pbjsGlobalName || "pbjs",
796
847
  attachPbjsListeners: config.attachPbjsListeners ?? true
797
848
  };
798
- this.boundFlushBeacon = () => this.flushBeacon();
849
+ this.boundFlushBeacon = () => {
850
+ this.flushAllSlotsTimeInView();
851
+ this.flushBeacon();
852
+ };
799
853
  this.boundVisibilityChange = () => this.handleVisibilityChange();
800
854
  }
801
855
  enable() {
802
856
  if (this.isEnabled) return;
803
857
  this.isEnabled = true;
858
+ _BidkernelPrebidAnalytics.activeInstances.add(this);
804
859
  if (!this.config.endpoint) {
805
860
  this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
806
861
  }
862
+ if (this.config.viewabilityEnabled) {
863
+ if (typeof IntersectionObserver !== "undefined") {
864
+ this.intersectionObserver = new IntersectionObserver(this.handleIntersection.bind(this), {
865
+ threshold: [0.5]
866
+ });
867
+ for (const record of this.slotViewabilityRecords.values()) {
868
+ if (record.element) {
869
+ this.intersectionObserver.observe(record.element);
870
+ }
871
+ }
872
+ }
873
+ }
807
874
  if (this.config.attachPbjsListeners) {
808
875
  const win = typeof window !== "undefined" ? window : {};
809
876
  const pbjs = win[this.config.pbjsGlobalName] || {};
@@ -831,28 +898,32 @@ var BidkernelPrebidAnalytics = class {
831
898
  try {
832
899
  const pastEvents = pbjs.getEvents();
833
900
  if (Array.isArray(pastEvents)) {
834
- this.log(
835
- "DEBUG",
836
- `Replaying ${pastEvents.length} historical events from pbjs.getEvents()`
837
- );
838
- const handlerMap = {
839
- auctionInit: this.handleAuctionInit.bind(this),
840
- auctionEnd: this.handleAuctionEnd.bind(this),
841
- bidRequested: this.handleBidRequested.bind(this),
842
- bidResponse: this.handleBidResponse.bind(this),
843
- bidTimeout: this.handleBidTimeout.bind(this),
844
- bidWon: this.handleBidWon.bind(this),
845
- noBid: this.handleNoBid.bind(this),
846
- adRenderFailed: this.handleAdRenderFailed.bind(this),
847
- adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
848
- };
849
- for (const ev of pastEvents) {
850
- if (!ev) continue;
851
- const eventType = ev.eventType || ev.event || ev.name;
852
- const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
853
- const handler = handlerMap[eventType];
854
- if (handler) {
855
- handler(args);
901
+ const newPastEvents = pastEvents.slice(this.replayedEventCount);
902
+ this.replayedEventCount = pastEvents.length;
903
+ if (newPastEvents.length > 0) {
904
+ this.log(
905
+ "DEBUG",
906
+ `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
907
+ );
908
+ const handlerMap = {
909
+ auctionInit: this.handleAuctionInit.bind(this),
910
+ auctionEnd: this.handleAuctionEnd.bind(this),
911
+ bidRequested: this.handleBidRequested.bind(this),
912
+ bidResponse: this.handleBidResponse.bind(this),
913
+ bidTimeout: this.handleBidTimeout.bind(this),
914
+ bidWon: this.handleBidWon.bind(this),
915
+ noBid: this.handleNoBid.bind(this),
916
+ adRenderFailed: this.handleAdRenderFailed.bind(this),
917
+ adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
918
+ };
919
+ for (const ev of newPastEvents) {
920
+ if (!ev) continue;
921
+ const eventType = ev.eventType || ev.event || ev.name;
922
+ const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
923
+ const handler = handlerMap[eventType];
924
+ if (handler) {
925
+ handler(args);
926
+ }
856
927
  }
857
928
  }
858
929
  }
@@ -879,7 +950,28 @@ var BidkernelPrebidAnalytics = class {
879
950
  }
880
951
  disable() {
881
952
  if (!this.isEnabled) return;
953
+ this.flushAllSlotsTimeInView();
882
954
  this.isEnabled = false;
955
+ _BidkernelPrebidAnalytics.activeInstances.delete(this);
956
+ for (const record of this.slotViewabilityRecords.values()) {
957
+ if (record.dwellTimer) {
958
+ clearTimeout(record.dwellTimer);
959
+ record.dwellTimer = null;
960
+ record.dwellStartedAt = null;
961
+ }
962
+ for (const l of record.thresholdListeners) {
963
+ if (l.timer) {
964
+ clearTimeout(l.timer);
965
+ l.timer = null;
966
+ }
967
+ }
968
+ }
969
+ if (this.intersectionObserver) {
970
+ this.intersectionObserver.disconnect();
971
+ this.intersectionObserver = null;
972
+ }
973
+ this.slotViewabilityRecords.clear();
974
+ this.elementToSlotId.clear();
883
975
  if (this.flushTimer) {
884
976
  clearInterval(this.flushTimer);
885
977
  this.flushTimer = null;
@@ -903,8 +995,443 @@ var BidkernelPrebidAnalytics = class {
903
995
  this.flush();
904
996
  }
905
997
  handleVisibilityChange() {
906
- if (typeof document !== "undefined" && document.visibilityState === "hidden") {
998
+ if (typeof document === "undefined") return;
999
+ if (document.visibilityState === "hidden") {
1000
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1001
+ for (const record of this.slotViewabilityRecords.values()) {
1002
+ if (record.inView && record.lastEnteredViewAt !== null) {
1003
+ record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
1004
+ record.lastEnteredViewAt = null;
1005
+ this.checkThresholdListeners(record);
1006
+ }
1007
+ if (record.dwellTimer) {
1008
+ clearTimeout(record.dwellTimer);
1009
+ record.dwellTimer = null;
1010
+ record.dwellStartedAt = null;
1011
+ }
1012
+ for (const l of record.thresholdListeners) {
1013
+ if (l.timer) {
1014
+ clearTimeout(l.timer);
1015
+ l.timer = null;
1016
+ }
1017
+ }
1018
+ const durationMs = Math.round(record.accumulatedTimeInViewMs);
1019
+ if (durationMs > 0) {
1020
+ this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
1021
+ auctionId: record.auctionId,
1022
+ transactionId: record.transactionId,
1023
+ adUnitCode: record.adUnitCode,
1024
+ viewableDurationMs: durationMs,
1025
+ bid: record.bidPayload,
1026
+ metadata: {
1027
+ ...record.metadata,
1028
+ refresh_index: String(record.refreshIndex)
1029
+ }
1030
+ });
1031
+ }
1032
+ }
907
1033
  this.flushBeacon();
1034
+ } else if (document.visibilityState === "visible") {
1035
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1036
+ for (const record of this.slotViewabilityRecords.values()) {
1037
+ if (record.inView) {
1038
+ record.lastEnteredViewAt = now;
1039
+ if (!record.viewableFired && record.dwellTimer === null) {
1040
+ const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
1041
+ record.dwellStartedAt = now;
1042
+ record.dwellTimer = setTimeout(() => {
1043
+ this.handleDwellComplete(record);
1044
+ }, dwellTarget);
1045
+ }
1046
+ this.scheduleThresholdTimers(record);
1047
+ }
1048
+ }
1049
+ }
1050
+ }
1051
+ handleIntersection(entries) {
1052
+ for (const entry of entries) {
1053
+ let slotId = this.elementToSlotId.get(entry.target);
1054
+ if (!slotId) {
1055
+ for (const instance of _BidkernelPrebidAnalytics.activeInstances) {
1056
+ if (instance !== this && instance.elementToSlotId.has(entry.target)) {
1057
+ instance.handleIntersection([entry]);
1058
+ break;
1059
+ }
1060
+ }
1061
+ continue;
1062
+ }
1063
+ const record = this.slotViewabilityRecords.get(slotId);
1064
+ if (!record) continue;
1065
+ const isViewableRatio = entry.isIntersecting && (entry.intersectionRatio === void 0 || entry.intersectionRatio >= 0.5);
1066
+ const isDocVisible = typeof document === "undefined" || document.visibilityState === "visible";
1067
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1068
+ if (isViewableRatio) {
1069
+ if (!record.inView) {
1070
+ record.inView = true;
1071
+ if (isDocVisible) {
1072
+ record.lastEnteredViewAt = now;
1073
+ if (!record.viewableFired && record.dwellTimer === null) {
1074
+ const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
1075
+ record.dwellStartedAt = now;
1076
+ record.dwellTimer = setTimeout(() => {
1077
+ this.handleDwellComplete(record);
1078
+ }, dwellTarget);
1079
+ }
1080
+ this.scheduleThresholdTimers(record);
1081
+ }
1082
+ }
1083
+ } else {
1084
+ if (record.inView) {
1085
+ if (record.lastEnteredViewAt !== null) {
1086
+ record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
1087
+ record.lastEnteredViewAt = null;
1088
+ }
1089
+ record.inView = false;
1090
+ this.checkThresholdListeners(record);
1091
+ }
1092
+ if (record.dwellTimer !== null) {
1093
+ clearTimeout(record.dwellTimer);
1094
+ record.dwellTimer = null;
1095
+ record.dwellStartedAt = null;
1096
+ }
1097
+ for (const l of record.thresholdListeners) {
1098
+ if (l.timer) {
1099
+ clearTimeout(l.timer);
1100
+ l.timer = null;
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ }
1106
+ handleDwellComplete(record) {
1107
+ record.dwellTimer = null;
1108
+ record.dwellStartedAt = null;
1109
+ if (!record.inView) return;
1110
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
1111
+ if (!record.viewableFired) {
1112
+ record.viewableFired = true;
1113
+ this.enqueue(TraceEventType.VIEWABLE, "viewable", {
1114
+ auctionId: record.auctionId,
1115
+ transactionId: record.transactionId,
1116
+ adUnitCode: record.adUnitCode,
1117
+ bid: record.bidPayload,
1118
+ metadata: {
1119
+ ...record.metadata,
1120
+ refresh_index: String(record.refreshIndex)
1121
+ }
1122
+ });
1123
+ }
1124
+ }
1125
+ scheduleThresholdTimers(record) {
1126
+ if (!record.inView || typeof document !== "undefined" && document.visibilityState === "hidden") {
1127
+ return;
1128
+ }
1129
+ const currentTotal = this.getCurrentTimeInView(record);
1130
+ for (const listener of record.thresholdListeners) {
1131
+ if (listener.fired) continue;
1132
+ if (currentTotal >= listener.thresholdMs) {
1133
+ listener.fired = true;
1134
+ if (listener.timer) {
1135
+ clearTimeout(listener.timer);
1136
+ listener.timer = null;
1137
+ }
1138
+ try {
1139
+ listener.callback(record.slotId, currentTotal);
1140
+ } catch (e) {
1141
+ this.log("ERROR", "Error in threshold listener callback", e);
1142
+ }
1143
+ } else {
1144
+ if (listener.timer) {
1145
+ clearTimeout(listener.timer);
1146
+ }
1147
+ const remaining = listener.thresholdMs - currentTotal;
1148
+ listener.timer = setTimeout(() => {
1149
+ listener.timer = null;
1150
+ if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
1151
+ const nowTotal = this.getCurrentTimeInView(record);
1152
+ if (nowTotal >= listener.thresholdMs) {
1153
+ listener.fired = true;
1154
+ try {
1155
+ listener.callback(record.slotId, nowTotal);
1156
+ } catch (e) {
1157
+ this.log("ERROR", "Error in threshold listener callback", e);
1158
+ }
1159
+ }
1160
+ }
1161
+ }, remaining);
1162
+ }
1163
+ }
1164
+ }
1165
+ checkThresholdListeners(record) {
1166
+ const currentTotal = this.getCurrentTimeInView(record);
1167
+ for (const listener of record.thresholdListeners) {
1168
+ if (!listener.fired && currentTotal >= listener.thresholdMs) {
1169
+ listener.fired = true;
1170
+ if (listener.timer) {
1171
+ clearTimeout(listener.timer);
1172
+ listener.timer = null;
1173
+ }
1174
+ try {
1175
+ listener.callback(record.slotId, currentTotal);
1176
+ } catch (e) {
1177
+ this.log("ERROR", "Error in threshold listener callback", e);
1178
+ }
1179
+ }
1180
+ }
1181
+ }
1182
+ getCurrentTimeInView(record) {
1183
+ let total = record.accumulatedTimeInViewMs;
1184
+ if (record.inView && record.lastEnteredViewAt !== null) {
1185
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1186
+ total += Math.max(0, now - record.lastEnteredViewAt);
1187
+ }
1188
+ return total;
1189
+ }
1190
+ flushSlotTimeInView(slotId) {
1191
+ const record = this.slotViewabilityRecords.get(slotId);
1192
+ if (!record) return;
1193
+ if (record.inView && record.lastEnteredViewAt !== null) {
1194
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1195
+ record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
1196
+ record.lastEnteredViewAt = null;
1197
+ }
1198
+ const durationMs = Math.round(record.accumulatedTimeInViewMs);
1199
+ if (durationMs > 0) {
1200
+ this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
1201
+ auctionId: record.auctionId,
1202
+ transactionId: record.transactionId,
1203
+ adUnitCode: record.adUnitCode,
1204
+ viewableDurationMs: durationMs,
1205
+ bid: record.bidPayload,
1206
+ metadata: {
1207
+ ...record.metadata,
1208
+ refresh_index: String(record.refreshIndex)
1209
+ }
1210
+ });
1211
+ }
1212
+ record.accumulatedTimeInViewMs = 0;
1213
+ }
1214
+ flushAllSlotsTimeInView() {
1215
+ for (const slotId of Array.from(this.slotViewabilityRecords.keys())) {
1216
+ this.flushSlotTimeInView(slotId);
1217
+ }
1218
+ }
1219
+ observeSlot(element, slotId, options) {
1220
+ if (!this.config.viewabilityEnabled) return;
1221
+ let el = null;
1222
+ if (typeof element === "string") {
1223
+ if (typeof document !== "undefined") {
1224
+ try {
1225
+ el = document.querySelector(element) || document.getElementById(element);
1226
+ } catch {
1227
+ el = document.getElementById(element);
1228
+ }
1229
+ if (!el) {
1230
+ el = document.getElementById(element);
1231
+ }
1232
+ }
1233
+ } else {
1234
+ el = element;
1235
+ }
1236
+ const resolvedSlotId = slotId || options?.adUnitCode || (el ? el.id : "") || generateUUID();
1237
+ const existing = this.slotViewabilityRecords.get(resolvedSlotId);
1238
+ if (existing) {
1239
+ const isNewRefreshCycle = options?.refreshIndex === void 0 || options.refreshIndex > existing.refreshIndex;
1240
+ if (isNewRefreshCycle) {
1241
+ this.flushSlotTimeInView(resolvedSlotId);
1242
+ const nextRefreshIndex = options?.refreshIndex !== void 0 ? options.refreshIndex : (this.slotRefreshIndices.get(resolvedSlotId) ?? existing.refreshIndex) + 1;
1243
+ this.slotRefreshIndices.set(resolvedSlotId, nextRefreshIndex);
1244
+ if (options?.emitRefreshEvent !== false) {
1245
+ this.enqueue(TraceEventType.REFRESH, "refresh", {
1246
+ auctionId: options?.auctionId || existing.auctionId,
1247
+ transactionId: options?.transactionId || existing.transactionId,
1248
+ adUnitCode: resolvedSlotId,
1249
+ bid: options?.bid ?? existing.bidPayload,
1250
+ metadata: {
1251
+ ...options?.metadata,
1252
+ refresh_index: String(nextRefreshIndex)
1253
+ }
1254
+ });
1255
+ }
1256
+ if (existing.dwellTimer) {
1257
+ clearTimeout(existing.dwellTimer);
1258
+ existing.dwellTimer = null;
1259
+ existing.dwellStartedAt = null;
1260
+ }
1261
+ for (const l of existing.thresholdListeners) {
1262
+ l.fired = false;
1263
+ if (l.timer) {
1264
+ clearTimeout(l.timer);
1265
+ l.timer = null;
1266
+ }
1267
+ }
1268
+ existing.viewableFired = false;
1269
+ existing.accumulatedTimeInViewMs = 0;
1270
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1271
+ existing.lastEnteredViewAt = existing.inView ? now : null;
1272
+ existing.refreshIndex = nextRefreshIndex;
1273
+ existing.auctionId = options?.auctionId || existing.auctionId;
1274
+ existing.transactionId = options?.transactionId || existing.transactionId;
1275
+ existing.mediaType = options?.mediaType || existing.mediaType || "banner";
1276
+ existing.bidPayload = options?.bid !== void 0 ? options.bid : existing.bidPayload;
1277
+ existing.metadata = options?.metadata ?? existing.metadata;
1278
+ if (existing.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
1279
+ const dwellTarget = existing.mediaType === "video" ? 2e3 : 1e3;
1280
+ existing.dwellStartedAt = now;
1281
+ existing.dwellTimer = setTimeout(() => {
1282
+ this.handleDwellComplete(existing);
1283
+ }, dwellTarget);
1284
+ this.scheduleThresholdTimers(existing);
1285
+ }
1286
+ } else {
1287
+ existing.auctionId = options?.auctionId || existing.auctionId;
1288
+ existing.transactionId = options?.transactionId || existing.transactionId;
1289
+ existing.mediaType = options?.mediaType || existing.mediaType;
1290
+ if (options?.bid !== void 0) existing.bidPayload = options.bid;
1291
+ if (options?.metadata !== void 0) existing.metadata = options.metadata;
1292
+ }
1293
+ if (el) {
1294
+ if (existing.element && existing.element !== el && this.intersectionObserver) {
1295
+ this.intersectionObserver.unobserve(existing.element);
1296
+ this.elementToSlotId.delete(existing.element);
1297
+ }
1298
+ existing.element = el;
1299
+ this.elementToSlotId.set(el, resolvedSlotId);
1300
+ this.intersectionObserver?.observe(el);
1301
+ }
1302
+ } else {
1303
+ const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
1304
+ this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
1305
+ const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
1306
+ this.pendingThresholdListeners.delete(resolvedSlotId);
1307
+ const record = {
1308
+ slotId: resolvedSlotId,
1309
+ element: el,
1310
+ adUnitCode: options?.adUnitCode || resolvedSlotId,
1311
+ auctionId: options?.auctionId || "",
1312
+ transactionId: options?.transactionId || "",
1313
+ mediaType: options?.mediaType || "banner",
1314
+ bidPayload: options?.bid,
1315
+ metadata: options?.metadata,
1316
+ refreshIndex: initialRefreshIndex,
1317
+ inView: false,
1318
+ lastEnteredViewAt: null,
1319
+ accumulatedTimeInViewMs: 0,
1320
+ viewableFired: false,
1321
+ dwellTimer: null,
1322
+ dwellStartedAt: null,
1323
+ thresholdListeners: listeners
1324
+ };
1325
+ this.slotViewabilityRecords.set(resolvedSlotId, record);
1326
+ if (el) {
1327
+ this.elementToSlotId.set(el, resolvedSlotId);
1328
+ this.intersectionObserver?.observe(el);
1329
+ }
1330
+ }
1331
+ }
1332
+ unobserveSlot(slotId) {
1333
+ const record = this.slotViewabilityRecords.get(slotId);
1334
+ if (!record) return;
1335
+ this.flushSlotTimeInView(slotId);
1336
+ if (record.dwellTimer) {
1337
+ clearTimeout(record.dwellTimer);
1338
+ record.dwellTimer = null;
1339
+ record.dwellStartedAt = null;
1340
+ }
1341
+ for (const l of record.thresholdListeners) {
1342
+ if (l.timer) {
1343
+ clearTimeout(l.timer);
1344
+ l.timer = null;
1345
+ }
1346
+ }
1347
+ record.thresholdListeners = /* @__PURE__ */ new Set();
1348
+ if (record.element && this.intersectionObserver) {
1349
+ this.intersectionObserver.unobserve(record.element);
1350
+ this.elementToSlotId.delete(record.element);
1351
+ record.element = null;
1352
+ }
1353
+ record.inView = false;
1354
+ record.lastEnteredViewAt = null;
1355
+ }
1356
+ destroySlot(slotId) {
1357
+ this.unobserveSlot(slotId);
1358
+ this.slotViewabilityRecords.delete(slotId);
1359
+ this.slotRefreshIndices.delete(slotId);
1360
+ this.pendingThresholdListeners.delete(slotId);
1361
+ }
1362
+ onTimeInViewThreshold(slotId, thresholdMs, callback) {
1363
+ const listener = {
1364
+ thresholdMs,
1365
+ callback,
1366
+ timer: null,
1367
+ fired: false
1368
+ };
1369
+ const record = this.slotViewabilityRecords.get(slotId);
1370
+ if (record) {
1371
+ record.thresholdListeners.add(listener);
1372
+ const currentTotal = this.getCurrentTimeInView(record);
1373
+ if (currentTotal >= thresholdMs) {
1374
+ listener.fired = true;
1375
+ try {
1376
+ callback(slotId, currentTotal);
1377
+ } catch (e) {
1378
+ this.log("ERROR", "Error in threshold callback", e);
1379
+ }
1380
+ } else if (record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
1381
+ const remaining = thresholdMs - currentTotal;
1382
+ listener.timer = setTimeout(() => {
1383
+ listener.timer = null;
1384
+ if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
1385
+ const nowTotal = this.getCurrentTimeInView(record);
1386
+ if (nowTotal >= thresholdMs) {
1387
+ listener.fired = true;
1388
+ try {
1389
+ callback(slotId, nowTotal);
1390
+ } catch (e) {
1391
+ this.log("ERROR", "Error in threshold callback", e);
1392
+ }
1393
+ }
1394
+ }
1395
+ }, remaining);
1396
+ }
1397
+ } else {
1398
+ if (!this.pendingThresholdListeners.has(slotId)) {
1399
+ this.pendingThresholdListeners.set(slotId, /* @__PURE__ */ new Set());
1400
+ }
1401
+ this.pendingThresholdListeners.get(slotId).add(listener);
1402
+ }
1403
+ return () => {
1404
+ if (listener.timer) {
1405
+ clearTimeout(listener.timer);
1406
+ listener.timer = null;
1407
+ }
1408
+ if (record) {
1409
+ record.thresholdListeners.delete(listener);
1410
+ }
1411
+ const pending = this.pendingThresholdListeners.get(slotId);
1412
+ if (pending) {
1413
+ pending.delete(listener);
1414
+ }
1415
+ };
1416
+ }
1417
+ getViewabilityState(slotId) {
1418
+ const record = this.slotViewabilityRecords.get(slotId);
1419
+ if (!record) return void 0;
1420
+ const timeInViewMs = Math.round(this.getCurrentTimeInView(record));
1421
+ return {
1422
+ inView: record.inView,
1423
+ viewable: record.viewableFired,
1424
+ timeInViewMs,
1425
+ refreshIndex: record.refreshIndex
1426
+ };
1427
+ }
1428
+ getRefreshIndex(slotId) {
1429
+ return this.slotRefreshIndices.get(slotId) ?? 0;
1430
+ }
1431
+ triggerSlotRefresh(slotId, options) {
1432
+ const record = this.slotViewabilityRecords.get(slotId);
1433
+ if (record && record.element) {
1434
+ this.observeSlot(record.element, slotId, options);
908
1435
  }
909
1436
  }
910
1437
  trackRawEvent(level, eventName, data) {
@@ -919,6 +1446,8 @@ var BidkernelPrebidAnalytics = class {
919
1446
  }
920
1447
  navigate(pageviewId, pageUrl) {
921
1448
  this.disable();
1449
+ this.slotRefreshIndices.clear();
1450
+ this.pendingThresholdListeners.clear();
922
1451
  this.config.pageviewId = pageviewId || generateUUID();
923
1452
  if (pageUrl) {
924
1453
  try {
@@ -937,30 +1466,34 @@ var BidkernelPrebidAnalytics = class {
937
1466
  if (this.isDuplicate("auctionInit", data)) return;
938
1467
  this.log("DEBUG", "auctionInit", data);
939
1468
  this.enqueue(TraceEventType.AUCTION_START, "auctionStart", {
940
- auctionId: data.auctionId || ""
1469
+ auctionId: data.auctionId || "",
1470
+ transactionId: data.transactionId || ""
941
1471
  });
942
1472
  }
943
1473
  handleAuctionEnd(data) {
944
1474
  if (this.isDuplicate("auctionEnd", data)) return;
945
1475
  this.log("DEBUG", "auctionEnd", data);
946
1476
  this.enqueue(TraceEventType.AUCTION_END, "auctionEnd", {
947
- auctionId: data.auctionId || ""
1477
+ auctionId: data.auctionId || "",
1478
+ transactionId: data.transactionId || ""
948
1479
  });
949
1480
  }
950
1481
  handleBidRequested(data) {
951
1482
  if (this.isDuplicate("bidRequested", data)) return;
952
1483
  this.log("DEBUG", "bidRequested", data);
953
1484
  const auctionId = data.auctionId || "";
954
- const bidder = data.bidderCode || "";
1485
+ const bidder = data.bidderCode || data.bidder || "";
955
1486
  if (Array.isArray(data.bids)) {
956
1487
  data.bids.forEach((bid) => {
957
1488
  const mediaTypes = Object.keys(bid.mediaTypes || {});
958
1489
  const gpid = bid.ortb2Imp?.ext?.gpid || bid.gpid || data.gpid || "";
1490
+ const transactionId = bid.transactionId || bid.ortb2Imp?.id || data.transactionId || "";
959
1491
  if (mediaTypes.length === 0) {
960
1492
  const mediaType = bid.mediaType || "banner";
961
1493
  const { width, height } = parseSize(bid.sizes || bid.playerSize);
962
1494
  this.enqueue(TraceEventType.BID_REQUEST, "bidRequest", {
963
1495
  auctionId,
1496
+ transactionId,
964
1497
  adUnitCode: bid.adUnitCode || "",
965
1498
  gpid,
966
1499
  bid: {
@@ -984,6 +1517,7 @@ var BidkernelPrebidAnalytics = class {
984
1517
  const { width, height } = parseSize(rawSize);
985
1518
  this.enqueue(TraceEventType.BID_REQUEST, "bidRequest", {
986
1519
  auctionId,
1520
+ transactionId,
987
1521
  adUnitCode: bid.adUnitCode || "",
988
1522
  gpid,
989
1523
  bid: {
@@ -1002,13 +1536,13 @@ var BidkernelPrebidAnalytics = class {
1002
1536
  this.log("DEBUG", "bidResponse", data);
1003
1537
  this.enqueue(TraceEventType.BID_RESPONSE, "bidResponse", {
1004
1538
  auctionId: data.auctionId || "",
1539
+ transactionId: data.transactionId || "",
1005
1540
  adUnitCode: data.adUnitCode || "",
1006
1541
  bid: {
1007
1542
  bidder: data.bidderCode || data.bidder || "",
1008
1543
  cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1009
1544
  currency: data.originalCurrency ?? data.currency ?? "USD",
1010
- width: Number.isFinite(data.width) ? data.width : 0,
1011
- height: Number.isFinite(data.height) ? data.height : 0,
1545
+ ...parseBidDimensions(data),
1012
1546
  dealId: data.dealId || "",
1013
1547
  mediaType: data.mediaType || "banner",
1014
1548
  latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
@@ -1020,15 +1554,16 @@ var BidkernelPrebidAnalytics = class {
1020
1554
  handleBidTimeout(data) {
1021
1555
  if (this.isDuplicate("bidTimeout", data)) return;
1022
1556
  this.log("DEBUG", "bidTimeout", data);
1023
- if (Array.isArray(data)) {
1024
- data.forEach((t) => {
1025
- this.enqueue(TraceEventType.BID_TIMEOUT, "bidTimeout", {
1026
- auctionId: t.auctionId || "",
1027
- adUnitCode: t.adUnitCode || "",
1028
- bid: {
1029
- bidder: t.bidder || ""
1030
- }
1031
- });
1557
+ const items = Array.isArray(data) ? data : data ? [data] : [];
1558
+ for (const t of items) {
1559
+ this.enqueue(TraceEventType.BID_TIMEOUT, "bidTimeout", {
1560
+ auctionId: t.auctionId || "",
1561
+ transactionId: t.transactionId || "",
1562
+ adUnitCode: t.adUnitCode || "",
1563
+ bid: {
1564
+ bidder: t.bidderCode || t.bidder || "",
1565
+ latencyMs: Number.isFinite(t.timeout) ? t.timeout : 0
1566
+ }
1032
1567
  });
1033
1568
  }
1034
1569
  }
@@ -1037,13 +1572,13 @@ var BidkernelPrebidAnalytics = class {
1037
1572
  this.log("DEBUG", "bidWon", data);
1038
1573
  this.enqueue(TraceEventType.BID_WIN, "bidWon", {
1039
1574
  auctionId: data.auctionId || "",
1575
+ transactionId: data.transactionId || "",
1040
1576
  adUnitCode: data.adUnitCode || "",
1041
1577
  bid: {
1042
1578
  bidder: data.bidderCode || data.bidder || "",
1043
1579
  cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1044
1580
  currency: data.originalCurrency ?? data.currency ?? "USD",
1045
- width: Number.isFinite(data.width) ? data.width : 0,
1046
- height: Number.isFinite(data.height) ? data.height : 0,
1581
+ ...parseBidDimensions(data),
1047
1582
  dealId: data.dealId || "",
1048
1583
  mediaType: data.mediaType || "banner",
1049
1584
  latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
@@ -1057,6 +1592,7 @@ var BidkernelPrebidAnalytics = class {
1057
1592
  this.log("DEBUG", "noBid", data);
1058
1593
  this.enqueue(TraceEventType.NO_BID, "noBid", {
1059
1594
  auctionId: data.auctionId || "",
1595
+ transactionId: data.transactionId || "",
1060
1596
  adUnitCode: data.adUnitCode || "",
1061
1597
  bid: {
1062
1598
  bidder: data.bidderCode || data.bidder || ""
@@ -1066,16 +1602,19 @@ var BidkernelPrebidAnalytics = class {
1066
1602
  handleAdRenderFailed(data) {
1067
1603
  if (this.isDuplicate("adRenderFailed", data)) return;
1068
1604
  this.log("DEBUG", "adRenderFailed", data);
1605
+ const bid = data.bid || {};
1069
1606
  this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
1070
- auctionId: data.bid?.auctionId || "",
1071
- adUnitCode: data.bid?.adUnitCode || "",
1607
+ auctionId: bid.auctionId || data.auctionId || "",
1608
+ transactionId: bid.transactionId || data.transactionId || "",
1609
+ adUnitCode: bid.adUnitCode || data.adUnitCode || "",
1072
1610
  bid: {
1073
- bidder: data.bid?.bidderCode || data.bid?.bidder || ""
1611
+ bidder: bid.bidderCode || bid.bidder || data.bidderCode || data.bidder || ""
1074
1612
  },
1075
1613
  metadata: {
1076
1614
  reason: String(data.reason || ""),
1077
- message: String(data.message || "")
1078
- }
1615
+ message: String(data.message || data.error?.message || "")
1616
+ },
1617
+ error: data.error || (data.message ? new Error(String(data.message)) : void 0)
1079
1618
  });
1080
1619
  }
1081
1620
  // Prebid's adRenderSucceeded payload is { doc, bid, adId }: the winning bid
@@ -1083,23 +1622,81 @@ var BidkernelPrebidAnalytics = class {
1083
1622
  handleAdRenderSucceeded(data) {
1084
1623
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1085
1624
  this.log("DEBUG", "adRenderSucceeded", data);
1086
- const bid = data.bid || {};
1625
+ const bid = data.bid || data || {};
1626
+ const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
1627
+ const auctionId = bid.auctionId || data.auctionId || "";
1628
+ const transactionId = bid.transactionId || data.transactionId || "";
1629
+ const mediaType = bid.mediaType || "banner";
1630
+ const bidTrace = {
1631
+ bidder: bid.bidderCode || bid.bidder || "",
1632
+ cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1633
+ currency: bid.originalCurrency ?? bid.currency ?? "USD",
1634
+ ...parseBidDimensions(bid),
1635
+ dealId: bid.dealId || "",
1636
+ mediaType,
1637
+ latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1638
+ advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1639
+ creativeId: bid.creativeId || ""
1640
+ };
1641
+ const isRefresh = adUnitCode && (this.slotViewabilityRecords.has(adUnitCode) || this.slotRefreshIndices.has(adUnitCode) && this.slotRefreshIndices.get(adUnitCode) > 0);
1642
+ if (isRefresh) {
1643
+ this.flushSlotTimeInView(adUnitCode);
1644
+ const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
1645
+ this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
1646
+ this.enqueue(TraceEventType.REFRESH, "refresh", {
1647
+ auctionId,
1648
+ transactionId,
1649
+ adUnitCode,
1650
+ bid: bidTrace,
1651
+ metadata: { refresh_index: String(nextRefreshIndex) }
1652
+ });
1653
+ } else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
1654
+ this.slotRefreshIndices.set(adUnitCode, 0);
1655
+ }
1656
+ const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
1087
1657
  this.enqueue(TraceEventType.IMPRESSION, "impression", {
1088
- auctionId: bid.auctionId || "",
1089
- adUnitCode: data.adUnitCode || bid.adUnitCode || "",
1090
- bid: {
1091
- bidder: bid.bidderCode || bid.bidder || "",
1092
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1093
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1094
- width: Number.isFinite(bid.width) ? bid.width : 0,
1095
- height: Number.isFinite(bid.height) ? bid.height : 0,
1096
- dealId: bid.dealId || "",
1097
- mediaType: bid.mediaType || "banner",
1098
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1099
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1100
- creativeId: bid.creativeId || ""
1101
- }
1658
+ auctionId,
1659
+ transactionId,
1660
+ adUnitCode,
1661
+ bid: bidTrace,
1662
+ metadata: { refresh_index: String(currentRefreshIndex) }
1102
1663
  });
1664
+ if (this.config.viewabilityEnabled && typeof document !== "undefined") {
1665
+ let el = null;
1666
+ const targetId = adUnitCode || data.adId || bid.adId;
1667
+ if (targetId) {
1668
+ el = document.getElementById(targetId);
1669
+ }
1670
+ if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
1671
+ try {
1672
+ const slots = window.googletag.pubads().getSlots();
1673
+ for (const slot of slots) {
1674
+ if (slot.getAdUnitPath?.() === adUnitCode || slot.getSlotElementId?.() === adUnitCode) {
1675
+ el = document.getElementById(slot.getSlotElementId());
1676
+ if (el) break;
1677
+ }
1678
+ }
1679
+ } catch {
1680
+ }
1681
+ }
1682
+ if (!el && targetId) {
1683
+ try {
1684
+ el = document.querySelector(`[data-ad-slot="${targetId}"]`) || document.querySelector(`#${targetId}`);
1685
+ } catch {
1686
+ }
1687
+ }
1688
+ if (el) {
1689
+ this.observeSlot(el, adUnitCode || targetId, {
1690
+ adUnitCode,
1691
+ auctionId,
1692
+ transactionId,
1693
+ mediaType,
1694
+ bid: bidTrace,
1695
+ refreshIndex: currentRefreshIndex,
1696
+ emitRefreshEvent: false
1697
+ });
1698
+ }
1699
+ }
1103
1700
  }
1104
1701
  isDuplicate(eventName, data) {
1105
1702
  return this.deduper.isDuplicate(eventName, data);
@@ -1107,6 +1704,7 @@ var BidkernelPrebidAnalytics = class {
1107
1704
  enqueue(type, eventName, data, level) {
1108
1705
  if (!this.isEnabled) return;
1109
1706
  if (!this.shouldSample(type, level)) return;
1707
+ extendSession();
1110
1708
  const protoEvent = {
1111
1709
  eventId: generateUUID(),
1112
1710
  timestampMs: Date.now(),
@@ -1121,11 +1719,14 @@ var BidkernelPrebidAnalytics = class {
1121
1719
  protoEvent.bid = {
1122
1720
  bidder: data.bid.bidder || "",
1123
1721
  cpm: Number.isFinite(data.bid.cpm) ? data.bid.cpm : 0,
1124
- currency: data.bid.currency || "USD",
1722
+ // No defaults here: handlers with a priced bid (bidResponse, bidWon,
1723
+ // impression) set currency/mediaType themselves. Defaulting for the
1724
+ // rest would stamp fake "USD"/"banner" on noBid and bidRequest rows.
1725
+ currency: data.bid.currency || "",
1125
1726
  width: Number.isFinite(data.bid.width) ? data.bid.width : 0,
1126
1727
  height: Number.isFinite(data.bid.height) ? data.bid.height : 0,
1127
1728
  dealId: data.bid.dealId || "",
1128
- mediaType: data.bid.mediaType || "banner",
1729
+ mediaType: data.bid.mediaType || "",
1129
1730
  latencyMs: Number.isFinite(data.bid.latencyMs) ? data.bid.latencyMs : 0,
1130
1731
  advertiserDomain: data.bid.advertiserDomain || "",
1131
1732
  creativeId: data.bid.creativeId || ""
@@ -1141,6 +1742,9 @@ var BidkernelPrebidAnalytics = class {
1141
1742
  if (data.gpid) {
1142
1743
  metadata.gpid = String(data.gpid);
1143
1744
  }
1745
+ if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
1746
+ metadata.refresh_index = String(data.refreshIndex);
1747
+ }
1144
1748
  if (Object.keys(metadata).length > 0) {
1145
1749
  protoEvent.metadata = metadata;
1146
1750
  }
@@ -1170,14 +1774,13 @@ var BidkernelPrebidAnalytics = class {
1170
1774
  this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
1171
1775
  }
1172
1776
  }
1173
- // Drains the queue and returns the encoded batch plus target URL and event list,
1174
- // or null if there is nothing to send. Shared by flush() and flushBeacon().
1777
+ // Drains a batch (up to MAX_PAYLOAD_BYTES) from the queue.
1778
+ // Shared by flush() and flushBeacon().
1175
1779
  drainBatch() {
1176
1780
  if (this.queue.length === 0 || !this.config.endpoint) return null;
1177
- const events = this.queue;
1178
- this.queue = [];
1781
+ let events = this.queue;
1179
1782
  const domain = typeof window !== "undefined" ? window.location.hostname : "";
1180
- const batch = {
1783
+ const buildBatch = (evts) => ({
1181
1784
  propertyId: this.config.propertyId,
1182
1785
  pageviewId: this.config.pageviewId,
1183
1786
  sessionId: this.config.sessionId,
@@ -1187,25 +1790,42 @@ var BidkernelPrebidAnalytics = class {
1187
1790
  deviceType: this.config.deviceType,
1188
1791
  userId: this.config.userId,
1189
1792
  domain,
1190
- events
1191
- };
1793
+ events: evts
1794
+ });
1795
+ let encoded = TraceEventBatch.encode(
1796
+ buildBatch(events)
1797
+ ).finish();
1798
+ while (events.length > 1 && encoded.byteLength > MAX_PAYLOAD_BYTES) {
1799
+ events = events.slice(0, Math.floor(events.length / 2));
1800
+ encoded = TraceEventBatch.encode(
1801
+ buildBatch(events)
1802
+ ).finish();
1803
+ }
1804
+ this.queue = this.queue.slice(events.length);
1192
1805
  return {
1193
1806
  url: `${this.config.endpoint}/${this.config.propertyId}`,
1194
1807
  // protobufjs types finish() as Uint8Array<ArrayBufferLike>; the buffer is
1195
1808
  // always a plain ArrayBuffer, so narrow for fetch/Blob compatibility.
1196
- encoded: TraceEventBatch.encode(batch).finish(),
1809
+ encoded,
1197
1810
  events
1198
1811
  };
1199
1812
  }
1200
- sendFetch(payload) {
1201
- fetch(payload.url, {
1813
+ sendFetch(payload, useKeepalive = false) {
1814
+ const fetchOpts = {
1202
1815
  method: "POST",
1203
1816
  headers: { "Content-Type": "application/x-protobuf" },
1204
- body: payload.encoded,
1205
- keepalive: true
1206
- }).then((res) => {
1817
+ body: payload.encoded
1818
+ };
1819
+ if (useKeepalive) {
1820
+ fetchOpts.keepalive = true;
1821
+ }
1822
+ fetch(payload.url, fetchOpts).then((res) => {
1207
1823
  if (!res.ok) {
1208
1824
  this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
1825
+ if (res.status >= 400 && res.status < 500) {
1826
+ this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
1827
+ return;
1828
+ }
1209
1829
  this.handleSendFailure(payload.events);
1210
1830
  } else {
1211
1831
  this.consecutiveSendFailures = 0;
@@ -1215,6 +1835,12 @@ var BidkernelPrebidAnalytics = class {
1215
1835
  this.log("ERROR", "Failed to send batch", err);
1216
1836
  this.handleSendFailure(payload.events);
1217
1837
  });
1838
+ if (typeof process !== "undefined" && typeof process._tickCallback === "function") {
1839
+ try {
1840
+ process._tickCallback();
1841
+ } catch {
1842
+ }
1843
+ }
1218
1844
  }
1219
1845
  handleSendFailure(events) {
1220
1846
  this.consecutiveSendFailures++;
@@ -1234,24 +1860,31 @@ var BidkernelPrebidAnalytics = class {
1234
1860
  }
1235
1861
  flush() {
1236
1862
  if (Date.now() < this.nextSendAllowedAt) return;
1237
- const payload = this.drainBatch();
1238
- if (!payload) return;
1239
- this.sendFetch(payload);
1863
+ let payload = this.drainBatch();
1864
+ while (payload) {
1865
+ this.sendFetch(payload, false);
1866
+ if (Date.now() < this.nextSendAllowedAt) break;
1867
+ payload = this.drainBatch();
1868
+ }
1240
1869
  }
1241
1870
  flushBeacon() {
1242
- const payload = this.drainBatch();
1243
- if (!payload) return;
1244
- let sent = false;
1245
- if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
1246
- try {
1247
- const blob = new Blob([payload.encoded], { type: "application/x-protobuf" });
1248
- sent = navigator.sendBeacon(payload.url, blob);
1249
- } catch {
1250
- sent = false;
1871
+ let payload = this.drainBatch();
1872
+ while (payload) {
1873
+ let sent = false;
1874
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
1875
+ try {
1876
+ const blob = new Blob([payload.encoded], {
1877
+ type: "application/x-protobuf"
1878
+ });
1879
+ sent = navigator.sendBeacon(payload.url, blob);
1880
+ } catch {
1881
+ sent = false;
1882
+ }
1251
1883
  }
1252
- }
1253
- if (!sent) {
1254
- this.sendFetch(payload);
1884
+ if (!sent) {
1885
+ this.sendFetch(payload, true);
1886
+ }
1887
+ payload = this.drainBatch();
1255
1888
  }
1256
1889
  }
1257
1890
  log(level, msg, ...args) {