@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.js CHANGED
@@ -657,6 +657,7 @@ var MAX_ERRORS_PER_SESSION = 50;
657
657
  var MAX_QUEUE_SIZE = 200;
658
658
  var MAX_SEND_BACKOFF_MS = 5 * 60 * 1e3;
659
659
  var MAX_CONSECUTIVE_SEND_FAILURES = 10;
660
+ var MAX_PAYLOAD_BYTES = 32 * 1024;
660
661
  var EVENT_NAME_TO_TYPE = {
661
662
  auctionStart: TraceEventType.AUCTION_START,
662
663
  auctionEnd: TraceEventType.AUCTION_END,
@@ -723,47 +724,73 @@ function getOrCreateSessionId() {
723
724
  inMemorySessionTs = now;
724
725
  return inMemorySessionId;
725
726
  }
727
+ function escapeKeyPart(str) {
728
+ return String(str ?? "").replace(/%/g, "%25").replace(/:/g, "%3A");
729
+ }
726
730
  function getPrebidEventKey(eventName, data) {
727
731
  switch (eventName) {
728
732
  case "auctionInit":
729
- return `auctionInit:${data?.auctionId || ""}`;
733
+ return `auctionInit:${escapeKeyPart(data?.auctionId)}`;
730
734
  case "auctionEnd":
731
- return `auctionEnd:${data?.auctionId || ""}`;
732
- case "bidRequested":
733
- return `bidRequested:${data?.auctionId || ""}:${data?.bidderCode || ""}:${Array.isArray(data?.bids) ? data.bids.map((b) => b?.bidId || b?.adUnitCode || "").join(",") : ""}`;
734
- case "bidResponse":
735
- return `bidResponse:${data?.auctionId || ""}:${data?.adUnitCode || ""}:${data?.bidderCode || data?.bidder || ""}:${data?.creativeId || data?.adId || data?.requestId || ""}:${data?.originalCpm ?? data?.cpm ?? ""}`;
736
- case "bidTimeout":
737
- if (Array.isArray(data)) {
738
- return "bidTimeout:" + data.map(
739
- (t) => `${t?.auctionId || ""}:${t?.bidder || t?.bidderCode || ""}:${t?.bidId || t?.adUnitCode || ""}`
740
- ).join(",");
741
- }
742
- return `bidTimeout:${data?.auctionId || ""}:${data?.bidder || data?.bidderCode || ""}:${data?.bidId || data?.adUnitCode || ""}`;
743
- case "bidWon":
744
- return `bidWon:${data?.auctionId || ""}:${data?.adUnitCode || ""}:${data?.bidderCode || data?.bidder || ""}:${data?.creativeId || data?.adId || data?.requestId || ""}:${data?.originalCpm ?? data?.cpm ?? ""}`;
735
+ return `auctionEnd:${escapeKeyPart(data?.auctionId)}`;
736
+ case "bidRequested": {
737
+ const bids = Array.isArray(data?.bids) ? data.bids.map((b) => escapeKeyPart(b?.bidId || b?.transactionId || b?.adUnitCode)).join(",") : "";
738
+ return `bidRequested:${escapeKeyPart(data?.auctionId)}:${escapeKeyPart(data?.bidderCode || data?.bidder)}:${bids}`;
739
+ }
740
+ case "bidResponse": {
741
+ const bid = data || {};
742
+ const uniqueBidId = bid.creativeId || bid.adId || bid.requestId || bid.bidId || "";
743
+ const price = bid.originalCpm ?? bid.cpm ?? "";
744
+ return `bidResponse:${escapeKeyPart(bid.auctionId)}:${escapeKeyPart(bid.adUnitCode)}:${escapeKeyPart(bid.bidderCode || bid.bidder)}:${escapeKeyPart(uniqueBidId)}:${escapeKeyPart(price)}`;
745
+ }
746
+ case "bidTimeout": {
747
+ const items = Array.isArray(data) ? data : data ? [data] : [];
748
+ return "bidTimeout:" + items.map(
749
+ (t) => `${escapeKeyPart(t?.auctionId)}:${escapeKeyPart(t?.bidderCode || t?.bidder)}:${escapeKeyPart(t?.bidId || t?.transactionId || t?.adUnitCode)}`
750
+ ).join(",");
751
+ }
752
+ case "bidWon": {
753
+ const bid = data || {};
754
+ const uniqueBidId = bid.creativeId || bid.adId || bid.requestId || bid.bidId || "";
755
+ const price = bid.originalCpm ?? bid.cpm ?? "";
756
+ return `bidWon:${escapeKeyPart(bid.auctionId)}:${escapeKeyPart(bid.adUnitCode)}:${escapeKeyPart(bid.bidderCode || bid.bidder)}:${escapeKeyPart(uniqueBidId)}:${escapeKeyPart(price)}`;
757
+ }
745
758
  case "noBid":
746
- return `noBid:${data?.auctionId || ""}:${data?.adUnitCode || ""}:${data?.bidderCode || data?.bidder || ""}:${data?.bidId || ""}`;
747
- case "adRenderFailed":
748
- return `adRenderFailed:${data?.bid?.auctionId || data?.auctionId || ""}:${data?.bid?.adUnitCode || data?.adUnitCode || ""}:${data?.bid?.bidderCode || data?.bid?.bidder || ""}:${data?.reason || ""}:${data?.message || ""}`;
749
- case "adRenderSucceeded":
750
- return `adRenderSucceeded:${data?.bid?.auctionId || data?.auctionId || ""}:${data?.bid?.adUnitCode || data?.adUnitCode || ""}:${data?.bid?.bidderCode || data?.bid?.bidder || ""}`;
759
+ return `noBid:${escapeKeyPart(data?.auctionId)}:${escapeKeyPart(data?.adUnitCode)}:${escapeKeyPart(data?.bidderCode || data?.bidder)}:${escapeKeyPart(data?.bidId || data?.transactionId)}`;
760
+ case "adRenderFailed": {
761
+ const bid = data?.bid || {};
762
+ 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)}`;
763
+ }
764
+ case "adRenderSucceeded": {
765
+ const bid = data?.bid || data || {};
766
+ const uniqueId = bid.creativeId || bid.adId || data?.adId || "";
767
+ const price = bid.originalCpm ?? bid.cpm ?? "";
768
+ return `adRenderSucceeded:${escapeKeyPart(bid.auctionId || data?.auctionId)}:${escapeKeyPart(bid.adUnitCode || data?.adUnitCode)}:${escapeKeyPart(bid.bidderCode || bid.bidder)}:${escapeKeyPart(uniqueId)}:${escapeKeyPart(price)}`;
769
+ }
751
770
  case "setTargeting":
752
- return `setTargeting:${Object.keys(data || {}).sort().join(",")}`;
771
+ return `setTargeting:${Object.keys(data || {}).sort().map(escapeKeyPart).join(",")}`;
753
772
  case "auctionDebug":
754
- return `auctionDebug:${data?.type || ""}:${String(data?.arguments?.[0] ?? "").slice(0, 50)}`;
773
+ return `auctionDebug:${escapeKeyPart(data?.type)}:${escapeKeyPart(String(data?.arguments?.[0] ?? "").slice(0, 50))}`;
755
774
  default:
756
775
  return "";
757
776
  }
758
777
  }
759
778
  var PrebidEventDeduper = class {
760
- seenObjects = /* @__PURE__ */ new WeakSet();
779
+ // Object identity is tracked per event name: Prebid passes the SAME bid
780
+ // object to bidResponse and later to bidWon, so a bare WeakSet would drop
781
+ // every bidWon as a duplicate of its own bidResponse.
782
+ seenObjects = /* @__PURE__ */ new WeakMap();
761
783
  seenKeys = /* @__PURE__ */ new Set();
762
784
  isDuplicate(eventName, data) {
763
785
  if (!data) return false;
764
786
  if (typeof data === "object") {
765
- if (this.seenObjects.has(data)) return true;
766
- this.seenObjects.add(data);
787
+ const seenEvents = this.seenObjects.get(data);
788
+ if (seenEvents) {
789
+ if (seenEvents.has(eventName)) return true;
790
+ seenEvents.add(eventName);
791
+ } else {
792
+ this.seenObjects.set(data, /* @__PURE__ */ new Set([eventName]));
793
+ }
767
794
  }
768
795
  const key = getPrebidEventKey(eventName, data);
769
796
  if (key) {
@@ -793,7 +820,23 @@ function parseSize(raw) {
793
820
  }
794
821
  return { width: 0, height: 0 };
795
822
  }
796
- var BidkernelPrebidAnalytics = class {
823
+ function parseBidDimensions(bid) {
824
+ if (Number.isFinite(bid?.width) && Number.isFinite(bid?.height)) {
825
+ return { width: bid.width, height: bid.height };
826
+ }
827
+ if (typeof bid?.size === "string") {
828
+ const match = bid.size.match(/^(\d+)x(\d+)$/);
829
+ if (match) {
830
+ return { width: Number(match[1]), height: Number(match[2]) };
831
+ }
832
+ }
833
+ return {
834
+ width: Number.isFinite(bid?.width) ? bid.width : 0,
835
+ height: Number.isFinite(bid?.height) ? bid.height : 0
836
+ };
837
+ }
838
+ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
839
+ static activeInstances = /* @__PURE__ */ new Set();
797
840
  config;
798
841
  queue = [];
799
842
  errorCount = 0;
@@ -808,6 +851,13 @@ var BidkernelPrebidAnalytics = class {
808
851
  deduper = new PrebidEventDeduper();
809
852
  consecutiveSendFailures = 0;
810
853
  nextSendAllowedAt = 0;
854
+ replayedEventCount = 0;
855
+ // Viewability & refresh tracking
856
+ intersectionObserver = null;
857
+ slotViewabilityRecords = /* @__PURE__ */ new Map();
858
+ elementToSlotId = /* @__PURE__ */ new Map();
859
+ slotRefreshIndices = /* @__PURE__ */ new Map();
860
+ pendingThresholdListeners = /* @__PURE__ */ new Map();
811
861
  constructor(config) {
812
862
  this.config = {
813
863
  endpoint: config.endpoint || "",
@@ -821,19 +871,36 @@ var BidkernelPrebidAnalytics = class {
821
871
  auctionEnabled: config.auctionEnabled ?? true,
822
872
  warningsEnabled: config.warningsEnabled ?? true,
823
873
  errorsEnabled: config.errorsEnabled ?? true,
874
+ viewabilityEnabled: config.viewabilityEnabled ?? true,
824
875
  logLevel: config.logLevel || "INFO",
825
876
  pbjsGlobalName: config.pbjsGlobalName || "pbjs",
826
877
  attachPbjsListeners: config.attachPbjsListeners ?? true
827
878
  };
828
- this.boundFlushBeacon = () => this.flushBeacon();
879
+ this.boundFlushBeacon = () => {
880
+ this.flushAllSlotsTimeInView();
881
+ this.flushBeacon();
882
+ };
829
883
  this.boundVisibilityChange = () => this.handleVisibilityChange();
830
884
  }
831
885
  enable() {
832
886
  if (this.isEnabled) return;
833
887
  this.isEnabled = true;
888
+ _BidkernelPrebidAnalytics.activeInstances.add(this);
834
889
  if (!this.config.endpoint) {
835
890
  this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
836
891
  }
892
+ if (this.config.viewabilityEnabled) {
893
+ if (typeof IntersectionObserver !== "undefined") {
894
+ this.intersectionObserver = new IntersectionObserver(this.handleIntersection.bind(this), {
895
+ threshold: [0.5]
896
+ });
897
+ for (const record of this.slotViewabilityRecords.values()) {
898
+ if (record.element) {
899
+ this.intersectionObserver.observe(record.element);
900
+ }
901
+ }
902
+ }
903
+ }
837
904
  if (this.config.attachPbjsListeners) {
838
905
  const win = typeof window !== "undefined" ? window : {};
839
906
  const pbjs = win[this.config.pbjsGlobalName] || {};
@@ -861,28 +928,32 @@ var BidkernelPrebidAnalytics = class {
861
928
  try {
862
929
  const pastEvents = pbjs.getEvents();
863
930
  if (Array.isArray(pastEvents)) {
864
- this.log(
865
- "DEBUG",
866
- `Replaying ${pastEvents.length} historical events from pbjs.getEvents()`
867
- );
868
- const handlerMap = {
869
- auctionInit: this.handleAuctionInit.bind(this),
870
- auctionEnd: this.handleAuctionEnd.bind(this),
871
- bidRequested: this.handleBidRequested.bind(this),
872
- bidResponse: this.handleBidResponse.bind(this),
873
- bidTimeout: this.handleBidTimeout.bind(this),
874
- bidWon: this.handleBidWon.bind(this),
875
- noBid: this.handleNoBid.bind(this),
876
- adRenderFailed: this.handleAdRenderFailed.bind(this),
877
- adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
878
- };
879
- for (const ev of pastEvents) {
880
- if (!ev) continue;
881
- const eventType = ev.eventType || ev.event || ev.name;
882
- const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
883
- const handler = handlerMap[eventType];
884
- if (handler) {
885
- handler(args);
931
+ const newPastEvents = pastEvents.slice(this.replayedEventCount);
932
+ this.replayedEventCount = pastEvents.length;
933
+ if (newPastEvents.length > 0) {
934
+ this.log(
935
+ "DEBUG",
936
+ `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
937
+ );
938
+ const handlerMap = {
939
+ auctionInit: this.handleAuctionInit.bind(this),
940
+ auctionEnd: this.handleAuctionEnd.bind(this),
941
+ bidRequested: this.handleBidRequested.bind(this),
942
+ bidResponse: this.handleBidResponse.bind(this),
943
+ bidTimeout: this.handleBidTimeout.bind(this),
944
+ bidWon: this.handleBidWon.bind(this),
945
+ noBid: this.handleNoBid.bind(this),
946
+ adRenderFailed: this.handleAdRenderFailed.bind(this),
947
+ adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
948
+ };
949
+ for (const ev of newPastEvents) {
950
+ if (!ev) continue;
951
+ const eventType = ev.eventType || ev.event || ev.name;
952
+ const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
953
+ const handler = handlerMap[eventType];
954
+ if (handler) {
955
+ handler(args);
956
+ }
886
957
  }
887
958
  }
888
959
  }
@@ -909,7 +980,28 @@ var BidkernelPrebidAnalytics = class {
909
980
  }
910
981
  disable() {
911
982
  if (!this.isEnabled) return;
983
+ this.flushAllSlotsTimeInView();
912
984
  this.isEnabled = false;
985
+ _BidkernelPrebidAnalytics.activeInstances.delete(this);
986
+ for (const record of this.slotViewabilityRecords.values()) {
987
+ if (record.dwellTimer) {
988
+ clearTimeout(record.dwellTimer);
989
+ record.dwellTimer = null;
990
+ record.dwellStartedAt = null;
991
+ }
992
+ for (const l of record.thresholdListeners) {
993
+ if (l.timer) {
994
+ clearTimeout(l.timer);
995
+ l.timer = null;
996
+ }
997
+ }
998
+ }
999
+ if (this.intersectionObserver) {
1000
+ this.intersectionObserver.disconnect();
1001
+ this.intersectionObserver = null;
1002
+ }
1003
+ this.slotViewabilityRecords.clear();
1004
+ this.elementToSlotId.clear();
913
1005
  if (this.flushTimer) {
914
1006
  clearInterval(this.flushTimer);
915
1007
  this.flushTimer = null;
@@ -933,8 +1025,443 @@ var BidkernelPrebidAnalytics = class {
933
1025
  this.flush();
934
1026
  }
935
1027
  handleVisibilityChange() {
936
- if (typeof document !== "undefined" && document.visibilityState === "hidden") {
1028
+ if (typeof document === "undefined") return;
1029
+ if (document.visibilityState === "hidden") {
1030
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1031
+ for (const record of this.slotViewabilityRecords.values()) {
1032
+ if (record.inView && record.lastEnteredViewAt !== null) {
1033
+ record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
1034
+ record.lastEnteredViewAt = null;
1035
+ this.checkThresholdListeners(record);
1036
+ }
1037
+ if (record.dwellTimer) {
1038
+ clearTimeout(record.dwellTimer);
1039
+ record.dwellTimer = null;
1040
+ record.dwellStartedAt = null;
1041
+ }
1042
+ for (const l of record.thresholdListeners) {
1043
+ if (l.timer) {
1044
+ clearTimeout(l.timer);
1045
+ l.timer = null;
1046
+ }
1047
+ }
1048
+ const durationMs = Math.round(record.accumulatedTimeInViewMs);
1049
+ if (durationMs > 0) {
1050
+ this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
1051
+ auctionId: record.auctionId,
1052
+ transactionId: record.transactionId,
1053
+ adUnitCode: record.adUnitCode,
1054
+ viewableDurationMs: durationMs,
1055
+ bid: record.bidPayload,
1056
+ metadata: {
1057
+ ...record.metadata,
1058
+ refresh_index: String(record.refreshIndex)
1059
+ }
1060
+ });
1061
+ }
1062
+ }
937
1063
  this.flushBeacon();
1064
+ } else if (document.visibilityState === "visible") {
1065
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1066
+ for (const record of this.slotViewabilityRecords.values()) {
1067
+ if (record.inView) {
1068
+ record.lastEnteredViewAt = now;
1069
+ if (!record.viewableFired && record.dwellTimer === null) {
1070
+ const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
1071
+ record.dwellStartedAt = now;
1072
+ record.dwellTimer = setTimeout(() => {
1073
+ this.handleDwellComplete(record);
1074
+ }, dwellTarget);
1075
+ }
1076
+ this.scheduleThresholdTimers(record);
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ handleIntersection(entries) {
1082
+ for (const entry of entries) {
1083
+ let slotId = this.elementToSlotId.get(entry.target);
1084
+ if (!slotId) {
1085
+ for (const instance of _BidkernelPrebidAnalytics.activeInstances) {
1086
+ if (instance !== this && instance.elementToSlotId.has(entry.target)) {
1087
+ instance.handleIntersection([entry]);
1088
+ break;
1089
+ }
1090
+ }
1091
+ continue;
1092
+ }
1093
+ const record = this.slotViewabilityRecords.get(slotId);
1094
+ if (!record) continue;
1095
+ const isViewableRatio = entry.isIntersecting && (entry.intersectionRatio === void 0 || entry.intersectionRatio >= 0.5);
1096
+ const isDocVisible = typeof document === "undefined" || document.visibilityState === "visible";
1097
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1098
+ if (isViewableRatio) {
1099
+ if (!record.inView) {
1100
+ record.inView = true;
1101
+ if (isDocVisible) {
1102
+ record.lastEnteredViewAt = now;
1103
+ if (!record.viewableFired && record.dwellTimer === null) {
1104
+ const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
1105
+ record.dwellStartedAt = now;
1106
+ record.dwellTimer = setTimeout(() => {
1107
+ this.handleDwellComplete(record);
1108
+ }, dwellTarget);
1109
+ }
1110
+ this.scheduleThresholdTimers(record);
1111
+ }
1112
+ }
1113
+ } else {
1114
+ if (record.inView) {
1115
+ if (record.lastEnteredViewAt !== null) {
1116
+ record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
1117
+ record.lastEnteredViewAt = null;
1118
+ }
1119
+ record.inView = false;
1120
+ this.checkThresholdListeners(record);
1121
+ }
1122
+ if (record.dwellTimer !== null) {
1123
+ clearTimeout(record.dwellTimer);
1124
+ record.dwellTimer = null;
1125
+ record.dwellStartedAt = null;
1126
+ }
1127
+ for (const l of record.thresholdListeners) {
1128
+ if (l.timer) {
1129
+ clearTimeout(l.timer);
1130
+ l.timer = null;
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ }
1136
+ handleDwellComplete(record) {
1137
+ record.dwellTimer = null;
1138
+ record.dwellStartedAt = null;
1139
+ if (!record.inView) return;
1140
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
1141
+ if (!record.viewableFired) {
1142
+ record.viewableFired = true;
1143
+ this.enqueue(TraceEventType.VIEWABLE, "viewable", {
1144
+ auctionId: record.auctionId,
1145
+ transactionId: record.transactionId,
1146
+ adUnitCode: record.adUnitCode,
1147
+ bid: record.bidPayload,
1148
+ metadata: {
1149
+ ...record.metadata,
1150
+ refresh_index: String(record.refreshIndex)
1151
+ }
1152
+ });
1153
+ }
1154
+ }
1155
+ scheduleThresholdTimers(record) {
1156
+ if (!record.inView || typeof document !== "undefined" && document.visibilityState === "hidden") {
1157
+ return;
1158
+ }
1159
+ const currentTotal = this.getCurrentTimeInView(record);
1160
+ for (const listener of record.thresholdListeners) {
1161
+ if (listener.fired) continue;
1162
+ if (currentTotal >= listener.thresholdMs) {
1163
+ listener.fired = true;
1164
+ if (listener.timer) {
1165
+ clearTimeout(listener.timer);
1166
+ listener.timer = null;
1167
+ }
1168
+ try {
1169
+ listener.callback(record.slotId, currentTotal);
1170
+ } catch (e) {
1171
+ this.log("ERROR", "Error in threshold listener callback", e);
1172
+ }
1173
+ } else {
1174
+ if (listener.timer) {
1175
+ clearTimeout(listener.timer);
1176
+ }
1177
+ const remaining = listener.thresholdMs - currentTotal;
1178
+ listener.timer = setTimeout(() => {
1179
+ listener.timer = null;
1180
+ if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
1181
+ const nowTotal = this.getCurrentTimeInView(record);
1182
+ if (nowTotal >= listener.thresholdMs) {
1183
+ listener.fired = true;
1184
+ try {
1185
+ listener.callback(record.slotId, nowTotal);
1186
+ } catch (e) {
1187
+ this.log("ERROR", "Error in threshold listener callback", e);
1188
+ }
1189
+ }
1190
+ }
1191
+ }, remaining);
1192
+ }
1193
+ }
1194
+ }
1195
+ checkThresholdListeners(record) {
1196
+ const currentTotal = this.getCurrentTimeInView(record);
1197
+ for (const listener of record.thresholdListeners) {
1198
+ if (!listener.fired && currentTotal >= listener.thresholdMs) {
1199
+ listener.fired = true;
1200
+ if (listener.timer) {
1201
+ clearTimeout(listener.timer);
1202
+ listener.timer = null;
1203
+ }
1204
+ try {
1205
+ listener.callback(record.slotId, currentTotal);
1206
+ } catch (e) {
1207
+ this.log("ERROR", "Error in threshold listener callback", e);
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ getCurrentTimeInView(record) {
1213
+ let total = record.accumulatedTimeInViewMs;
1214
+ if (record.inView && record.lastEnteredViewAt !== null) {
1215
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1216
+ total += Math.max(0, now - record.lastEnteredViewAt);
1217
+ }
1218
+ return total;
1219
+ }
1220
+ flushSlotTimeInView(slotId) {
1221
+ const record = this.slotViewabilityRecords.get(slotId);
1222
+ if (!record) return;
1223
+ if (record.inView && record.lastEnteredViewAt !== null) {
1224
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1225
+ record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
1226
+ record.lastEnteredViewAt = null;
1227
+ }
1228
+ const durationMs = Math.round(record.accumulatedTimeInViewMs);
1229
+ if (durationMs > 0) {
1230
+ this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
1231
+ auctionId: record.auctionId,
1232
+ transactionId: record.transactionId,
1233
+ adUnitCode: record.adUnitCode,
1234
+ viewableDurationMs: durationMs,
1235
+ bid: record.bidPayload,
1236
+ metadata: {
1237
+ ...record.metadata,
1238
+ refresh_index: String(record.refreshIndex)
1239
+ }
1240
+ });
1241
+ }
1242
+ record.accumulatedTimeInViewMs = 0;
1243
+ }
1244
+ flushAllSlotsTimeInView() {
1245
+ for (const slotId of Array.from(this.slotViewabilityRecords.keys())) {
1246
+ this.flushSlotTimeInView(slotId);
1247
+ }
1248
+ }
1249
+ observeSlot(element, slotId, options) {
1250
+ if (!this.config.viewabilityEnabled) return;
1251
+ let el = null;
1252
+ if (typeof element === "string") {
1253
+ if (typeof document !== "undefined") {
1254
+ try {
1255
+ el = document.querySelector(element) || document.getElementById(element);
1256
+ } catch {
1257
+ el = document.getElementById(element);
1258
+ }
1259
+ if (!el) {
1260
+ el = document.getElementById(element);
1261
+ }
1262
+ }
1263
+ } else {
1264
+ el = element;
1265
+ }
1266
+ const resolvedSlotId = slotId || options?.adUnitCode || (el ? el.id : "") || generateUUID();
1267
+ const existing = this.slotViewabilityRecords.get(resolvedSlotId);
1268
+ if (existing) {
1269
+ const isNewRefreshCycle = options?.refreshIndex === void 0 || options.refreshIndex > existing.refreshIndex;
1270
+ if (isNewRefreshCycle) {
1271
+ this.flushSlotTimeInView(resolvedSlotId);
1272
+ const nextRefreshIndex = options?.refreshIndex !== void 0 ? options.refreshIndex : (this.slotRefreshIndices.get(resolvedSlotId) ?? existing.refreshIndex) + 1;
1273
+ this.slotRefreshIndices.set(resolvedSlotId, nextRefreshIndex);
1274
+ if (options?.emitRefreshEvent !== false) {
1275
+ this.enqueue(TraceEventType.REFRESH, "refresh", {
1276
+ auctionId: options?.auctionId || existing.auctionId,
1277
+ transactionId: options?.transactionId || existing.transactionId,
1278
+ adUnitCode: resolvedSlotId,
1279
+ bid: options?.bid ?? existing.bidPayload,
1280
+ metadata: {
1281
+ ...options?.metadata,
1282
+ refresh_index: String(nextRefreshIndex)
1283
+ }
1284
+ });
1285
+ }
1286
+ if (existing.dwellTimer) {
1287
+ clearTimeout(existing.dwellTimer);
1288
+ existing.dwellTimer = null;
1289
+ existing.dwellStartedAt = null;
1290
+ }
1291
+ for (const l of existing.thresholdListeners) {
1292
+ l.fired = false;
1293
+ if (l.timer) {
1294
+ clearTimeout(l.timer);
1295
+ l.timer = null;
1296
+ }
1297
+ }
1298
+ existing.viewableFired = false;
1299
+ existing.accumulatedTimeInViewMs = 0;
1300
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1301
+ existing.lastEnteredViewAt = existing.inView ? now : null;
1302
+ existing.refreshIndex = nextRefreshIndex;
1303
+ existing.auctionId = options?.auctionId || existing.auctionId;
1304
+ existing.transactionId = options?.transactionId || existing.transactionId;
1305
+ existing.mediaType = options?.mediaType || existing.mediaType || "banner";
1306
+ existing.bidPayload = options?.bid !== void 0 ? options.bid : existing.bidPayload;
1307
+ existing.metadata = options?.metadata ?? existing.metadata;
1308
+ if (existing.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
1309
+ const dwellTarget = existing.mediaType === "video" ? 2e3 : 1e3;
1310
+ existing.dwellStartedAt = now;
1311
+ existing.dwellTimer = setTimeout(() => {
1312
+ this.handleDwellComplete(existing);
1313
+ }, dwellTarget);
1314
+ this.scheduleThresholdTimers(existing);
1315
+ }
1316
+ } else {
1317
+ existing.auctionId = options?.auctionId || existing.auctionId;
1318
+ existing.transactionId = options?.transactionId || existing.transactionId;
1319
+ existing.mediaType = options?.mediaType || existing.mediaType;
1320
+ if (options?.bid !== void 0) existing.bidPayload = options.bid;
1321
+ if (options?.metadata !== void 0) existing.metadata = options.metadata;
1322
+ }
1323
+ if (el) {
1324
+ if (existing.element && existing.element !== el && this.intersectionObserver) {
1325
+ this.intersectionObserver.unobserve(existing.element);
1326
+ this.elementToSlotId.delete(existing.element);
1327
+ }
1328
+ existing.element = el;
1329
+ this.elementToSlotId.set(el, resolvedSlotId);
1330
+ this.intersectionObserver?.observe(el);
1331
+ }
1332
+ } else {
1333
+ const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
1334
+ this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
1335
+ const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
1336
+ this.pendingThresholdListeners.delete(resolvedSlotId);
1337
+ const record = {
1338
+ slotId: resolvedSlotId,
1339
+ element: el,
1340
+ adUnitCode: options?.adUnitCode || resolvedSlotId,
1341
+ auctionId: options?.auctionId || "",
1342
+ transactionId: options?.transactionId || "",
1343
+ mediaType: options?.mediaType || "banner",
1344
+ bidPayload: options?.bid,
1345
+ metadata: options?.metadata,
1346
+ refreshIndex: initialRefreshIndex,
1347
+ inView: false,
1348
+ lastEnteredViewAt: null,
1349
+ accumulatedTimeInViewMs: 0,
1350
+ viewableFired: false,
1351
+ dwellTimer: null,
1352
+ dwellStartedAt: null,
1353
+ thresholdListeners: listeners
1354
+ };
1355
+ this.slotViewabilityRecords.set(resolvedSlotId, record);
1356
+ if (el) {
1357
+ this.elementToSlotId.set(el, resolvedSlotId);
1358
+ this.intersectionObserver?.observe(el);
1359
+ }
1360
+ }
1361
+ }
1362
+ unobserveSlot(slotId) {
1363
+ const record = this.slotViewabilityRecords.get(slotId);
1364
+ if (!record) return;
1365
+ this.flushSlotTimeInView(slotId);
1366
+ if (record.dwellTimer) {
1367
+ clearTimeout(record.dwellTimer);
1368
+ record.dwellTimer = null;
1369
+ record.dwellStartedAt = null;
1370
+ }
1371
+ for (const l of record.thresholdListeners) {
1372
+ if (l.timer) {
1373
+ clearTimeout(l.timer);
1374
+ l.timer = null;
1375
+ }
1376
+ }
1377
+ record.thresholdListeners = /* @__PURE__ */ new Set();
1378
+ if (record.element && this.intersectionObserver) {
1379
+ this.intersectionObserver.unobserve(record.element);
1380
+ this.elementToSlotId.delete(record.element);
1381
+ record.element = null;
1382
+ }
1383
+ record.inView = false;
1384
+ record.lastEnteredViewAt = null;
1385
+ }
1386
+ destroySlot(slotId) {
1387
+ this.unobserveSlot(slotId);
1388
+ this.slotViewabilityRecords.delete(slotId);
1389
+ this.slotRefreshIndices.delete(slotId);
1390
+ this.pendingThresholdListeners.delete(slotId);
1391
+ }
1392
+ onTimeInViewThreshold(slotId, thresholdMs, callback) {
1393
+ const listener = {
1394
+ thresholdMs,
1395
+ callback,
1396
+ timer: null,
1397
+ fired: false
1398
+ };
1399
+ const record = this.slotViewabilityRecords.get(slotId);
1400
+ if (record) {
1401
+ record.thresholdListeners.add(listener);
1402
+ const currentTotal = this.getCurrentTimeInView(record);
1403
+ if (currentTotal >= thresholdMs) {
1404
+ listener.fired = true;
1405
+ try {
1406
+ callback(slotId, currentTotal);
1407
+ } catch (e) {
1408
+ this.log("ERROR", "Error in threshold callback", e);
1409
+ }
1410
+ } else if (record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
1411
+ const remaining = thresholdMs - currentTotal;
1412
+ listener.timer = setTimeout(() => {
1413
+ listener.timer = null;
1414
+ if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
1415
+ const nowTotal = this.getCurrentTimeInView(record);
1416
+ if (nowTotal >= thresholdMs) {
1417
+ listener.fired = true;
1418
+ try {
1419
+ callback(slotId, nowTotal);
1420
+ } catch (e) {
1421
+ this.log("ERROR", "Error in threshold callback", e);
1422
+ }
1423
+ }
1424
+ }
1425
+ }, remaining);
1426
+ }
1427
+ } else {
1428
+ if (!this.pendingThresholdListeners.has(slotId)) {
1429
+ this.pendingThresholdListeners.set(slotId, /* @__PURE__ */ new Set());
1430
+ }
1431
+ this.pendingThresholdListeners.get(slotId).add(listener);
1432
+ }
1433
+ return () => {
1434
+ if (listener.timer) {
1435
+ clearTimeout(listener.timer);
1436
+ listener.timer = null;
1437
+ }
1438
+ if (record) {
1439
+ record.thresholdListeners.delete(listener);
1440
+ }
1441
+ const pending = this.pendingThresholdListeners.get(slotId);
1442
+ if (pending) {
1443
+ pending.delete(listener);
1444
+ }
1445
+ };
1446
+ }
1447
+ getViewabilityState(slotId) {
1448
+ const record = this.slotViewabilityRecords.get(slotId);
1449
+ if (!record) return void 0;
1450
+ const timeInViewMs = Math.round(this.getCurrentTimeInView(record));
1451
+ return {
1452
+ inView: record.inView,
1453
+ viewable: record.viewableFired,
1454
+ timeInViewMs,
1455
+ refreshIndex: record.refreshIndex
1456
+ };
1457
+ }
1458
+ getRefreshIndex(slotId) {
1459
+ return this.slotRefreshIndices.get(slotId) ?? 0;
1460
+ }
1461
+ triggerSlotRefresh(slotId, options) {
1462
+ const record = this.slotViewabilityRecords.get(slotId);
1463
+ if (record && record.element) {
1464
+ this.observeSlot(record.element, slotId, options);
938
1465
  }
939
1466
  }
940
1467
  trackRawEvent(level, eventName, data) {
@@ -949,6 +1476,8 @@ var BidkernelPrebidAnalytics = class {
949
1476
  }
950
1477
  navigate(pageviewId, pageUrl) {
951
1478
  this.disable();
1479
+ this.slotRefreshIndices.clear();
1480
+ this.pendingThresholdListeners.clear();
952
1481
  this.config.pageviewId = pageviewId || generateUUID();
953
1482
  if (pageUrl) {
954
1483
  try {
@@ -967,30 +1496,34 @@ var BidkernelPrebidAnalytics = class {
967
1496
  if (this.isDuplicate("auctionInit", data)) return;
968
1497
  this.log("DEBUG", "auctionInit", data);
969
1498
  this.enqueue(TraceEventType.AUCTION_START, "auctionStart", {
970
- auctionId: data.auctionId || ""
1499
+ auctionId: data.auctionId || "",
1500
+ transactionId: data.transactionId || ""
971
1501
  });
972
1502
  }
973
1503
  handleAuctionEnd(data) {
974
1504
  if (this.isDuplicate("auctionEnd", data)) return;
975
1505
  this.log("DEBUG", "auctionEnd", data);
976
1506
  this.enqueue(TraceEventType.AUCTION_END, "auctionEnd", {
977
- auctionId: data.auctionId || ""
1507
+ auctionId: data.auctionId || "",
1508
+ transactionId: data.transactionId || ""
978
1509
  });
979
1510
  }
980
1511
  handleBidRequested(data) {
981
1512
  if (this.isDuplicate("bidRequested", data)) return;
982
1513
  this.log("DEBUG", "bidRequested", data);
983
1514
  const auctionId = data.auctionId || "";
984
- const bidder = data.bidderCode || "";
1515
+ const bidder = data.bidderCode || data.bidder || "";
985
1516
  if (Array.isArray(data.bids)) {
986
1517
  data.bids.forEach((bid) => {
987
1518
  const mediaTypes = Object.keys(bid.mediaTypes || {});
988
1519
  const gpid = bid.ortb2Imp?.ext?.gpid || bid.gpid || data.gpid || "";
1520
+ const transactionId = bid.transactionId || bid.ortb2Imp?.id || data.transactionId || "";
989
1521
  if (mediaTypes.length === 0) {
990
1522
  const mediaType = bid.mediaType || "banner";
991
1523
  const { width, height } = parseSize(bid.sizes || bid.playerSize);
992
1524
  this.enqueue(TraceEventType.BID_REQUEST, "bidRequest", {
993
1525
  auctionId,
1526
+ transactionId,
994
1527
  adUnitCode: bid.adUnitCode || "",
995
1528
  gpid,
996
1529
  bid: {
@@ -1014,6 +1547,7 @@ var BidkernelPrebidAnalytics = class {
1014
1547
  const { width, height } = parseSize(rawSize);
1015
1548
  this.enqueue(TraceEventType.BID_REQUEST, "bidRequest", {
1016
1549
  auctionId,
1550
+ transactionId,
1017
1551
  adUnitCode: bid.adUnitCode || "",
1018
1552
  gpid,
1019
1553
  bid: {
@@ -1032,13 +1566,13 @@ var BidkernelPrebidAnalytics = class {
1032
1566
  this.log("DEBUG", "bidResponse", data);
1033
1567
  this.enqueue(TraceEventType.BID_RESPONSE, "bidResponse", {
1034
1568
  auctionId: data.auctionId || "",
1569
+ transactionId: data.transactionId || "",
1035
1570
  adUnitCode: data.adUnitCode || "",
1036
1571
  bid: {
1037
1572
  bidder: data.bidderCode || data.bidder || "",
1038
1573
  cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1039
1574
  currency: data.originalCurrency ?? data.currency ?? "USD",
1040
- width: Number.isFinite(data.width) ? data.width : 0,
1041
- height: Number.isFinite(data.height) ? data.height : 0,
1575
+ ...parseBidDimensions(data),
1042
1576
  dealId: data.dealId || "",
1043
1577
  mediaType: data.mediaType || "banner",
1044
1578
  latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
@@ -1050,15 +1584,16 @@ var BidkernelPrebidAnalytics = class {
1050
1584
  handleBidTimeout(data) {
1051
1585
  if (this.isDuplicate("bidTimeout", data)) return;
1052
1586
  this.log("DEBUG", "bidTimeout", data);
1053
- if (Array.isArray(data)) {
1054
- data.forEach((t) => {
1055
- this.enqueue(TraceEventType.BID_TIMEOUT, "bidTimeout", {
1056
- auctionId: t.auctionId || "",
1057
- adUnitCode: t.adUnitCode || "",
1058
- bid: {
1059
- bidder: t.bidder || ""
1060
- }
1061
- });
1587
+ const items = Array.isArray(data) ? data : data ? [data] : [];
1588
+ for (const t of items) {
1589
+ this.enqueue(TraceEventType.BID_TIMEOUT, "bidTimeout", {
1590
+ auctionId: t.auctionId || "",
1591
+ transactionId: t.transactionId || "",
1592
+ adUnitCode: t.adUnitCode || "",
1593
+ bid: {
1594
+ bidder: t.bidderCode || t.bidder || "",
1595
+ latencyMs: Number.isFinite(t.timeout) ? t.timeout : 0
1596
+ }
1062
1597
  });
1063
1598
  }
1064
1599
  }
@@ -1067,13 +1602,13 @@ var BidkernelPrebidAnalytics = class {
1067
1602
  this.log("DEBUG", "bidWon", data);
1068
1603
  this.enqueue(TraceEventType.BID_WIN, "bidWon", {
1069
1604
  auctionId: data.auctionId || "",
1605
+ transactionId: data.transactionId || "",
1070
1606
  adUnitCode: data.adUnitCode || "",
1071
1607
  bid: {
1072
1608
  bidder: data.bidderCode || data.bidder || "",
1073
1609
  cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1074
1610
  currency: data.originalCurrency ?? data.currency ?? "USD",
1075
- width: Number.isFinite(data.width) ? data.width : 0,
1076
- height: Number.isFinite(data.height) ? data.height : 0,
1611
+ ...parseBidDimensions(data),
1077
1612
  dealId: data.dealId || "",
1078
1613
  mediaType: data.mediaType || "banner",
1079
1614
  latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
@@ -1087,6 +1622,7 @@ var BidkernelPrebidAnalytics = class {
1087
1622
  this.log("DEBUG", "noBid", data);
1088
1623
  this.enqueue(TraceEventType.NO_BID, "noBid", {
1089
1624
  auctionId: data.auctionId || "",
1625
+ transactionId: data.transactionId || "",
1090
1626
  adUnitCode: data.adUnitCode || "",
1091
1627
  bid: {
1092
1628
  bidder: data.bidderCode || data.bidder || ""
@@ -1096,16 +1632,19 @@ var BidkernelPrebidAnalytics = class {
1096
1632
  handleAdRenderFailed(data) {
1097
1633
  if (this.isDuplicate("adRenderFailed", data)) return;
1098
1634
  this.log("DEBUG", "adRenderFailed", data);
1635
+ const bid = data.bid || {};
1099
1636
  this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
1100
- auctionId: data.bid?.auctionId || "",
1101
- adUnitCode: data.bid?.adUnitCode || "",
1637
+ auctionId: bid.auctionId || data.auctionId || "",
1638
+ transactionId: bid.transactionId || data.transactionId || "",
1639
+ adUnitCode: bid.adUnitCode || data.adUnitCode || "",
1102
1640
  bid: {
1103
- bidder: data.bid?.bidderCode || data.bid?.bidder || ""
1641
+ bidder: bid.bidderCode || bid.bidder || data.bidderCode || data.bidder || ""
1104
1642
  },
1105
1643
  metadata: {
1106
1644
  reason: String(data.reason || ""),
1107
- message: String(data.message || "")
1108
- }
1645
+ message: String(data.message || data.error?.message || "")
1646
+ },
1647
+ error: data.error || (data.message ? new Error(String(data.message)) : void 0)
1109
1648
  });
1110
1649
  }
1111
1650
  // Prebid's adRenderSucceeded payload is { doc, bid, adId }: the winning bid
@@ -1113,23 +1652,81 @@ var BidkernelPrebidAnalytics = class {
1113
1652
  handleAdRenderSucceeded(data) {
1114
1653
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1115
1654
  this.log("DEBUG", "adRenderSucceeded", data);
1116
- const bid = data.bid || {};
1655
+ const bid = data.bid || data || {};
1656
+ const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
1657
+ const auctionId = bid.auctionId || data.auctionId || "";
1658
+ const transactionId = bid.transactionId || data.transactionId || "";
1659
+ const mediaType = bid.mediaType || "banner";
1660
+ const bidTrace = {
1661
+ bidder: bid.bidderCode || bid.bidder || "",
1662
+ cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1663
+ currency: bid.originalCurrency ?? bid.currency ?? "USD",
1664
+ ...parseBidDimensions(bid),
1665
+ dealId: bid.dealId || "",
1666
+ mediaType,
1667
+ latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1668
+ advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1669
+ creativeId: bid.creativeId || ""
1670
+ };
1671
+ const isRefresh = adUnitCode && (this.slotViewabilityRecords.has(adUnitCode) || this.slotRefreshIndices.has(adUnitCode) && this.slotRefreshIndices.get(adUnitCode) > 0);
1672
+ if (isRefresh) {
1673
+ this.flushSlotTimeInView(adUnitCode);
1674
+ const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
1675
+ this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
1676
+ this.enqueue(TraceEventType.REFRESH, "refresh", {
1677
+ auctionId,
1678
+ transactionId,
1679
+ adUnitCode,
1680
+ bid: bidTrace,
1681
+ metadata: { refresh_index: String(nextRefreshIndex) }
1682
+ });
1683
+ } else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
1684
+ this.slotRefreshIndices.set(adUnitCode, 0);
1685
+ }
1686
+ const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
1117
1687
  this.enqueue(TraceEventType.IMPRESSION, "impression", {
1118
- auctionId: bid.auctionId || "",
1119
- adUnitCode: data.adUnitCode || bid.adUnitCode || "",
1120
- bid: {
1121
- bidder: bid.bidderCode || bid.bidder || "",
1122
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1123
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1124
- width: Number.isFinite(bid.width) ? bid.width : 0,
1125
- height: Number.isFinite(bid.height) ? bid.height : 0,
1126
- dealId: bid.dealId || "",
1127
- mediaType: bid.mediaType || "banner",
1128
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1129
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1130
- creativeId: bid.creativeId || ""
1131
- }
1688
+ auctionId,
1689
+ transactionId,
1690
+ adUnitCode,
1691
+ bid: bidTrace,
1692
+ metadata: { refresh_index: String(currentRefreshIndex) }
1132
1693
  });
1694
+ if (this.config.viewabilityEnabled && typeof document !== "undefined") {
1695
+ let el = null;
1696
+ const targetId = adUnitCode || data.adId || bid.adId;
1697
+ if (targetId) {
1698
+ el = document.getElementById(targetId);
1699
+ }
1700
+ if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
1701
+ try {
1702
+ const slots = window.googletag.pubads().getSlots();
1703
+ for (const slot of slots) {
1704
+ if (slot.getAdUnitPath?.() === adUnitCode || slot.getSlotElementId?.() === adUnitCode) {
1705
+ el = document.getElementById(slot.getSlotElementId());
1706
+ if (el) break;
1707
+ }
1708
+ }
1709
+ } catch {
1710
+ }
1711
+ }
1712
+ if (!el && targetId) {
1713
+ try {
1714
+ el = document.querySelector(`[data-ad-slot="${targetId}"]`) || document.querySelector(`#${targetId}`);
1715
+ } catch {
1716
+ }
1717
+ }
1718
+ if (el) {
1719
+ this.observeSlot(el, adUnitCode || targetId, {
1720
+ adUnitCode,
1721
+ auctionId,
1722
+ transactionId,
1723
+ mediaType,
1724
+ bid: bidTrace,
1725
+ refreshIndex: currentRefreshIndex,
1726
+ emitRefreshEvent: false
1727
+ });
1728
+ }
1729
+ }
1133
1730
  }
1134
1731
  isDuplicate(eventName, data) {
1135
1732
  return this.deduper.isDuplicate(eventName, data);
@@ -1137,6 +1734,7 @@ var BidkernelPrebidAnalytics = class {
1137
1734
  enqueue(type, eventName, data, level) {
1138
1735
  if (!this.isEnabled) return;
1139
1736
  if (!this.shouldSample(type, level)) return;
1737
+ extendSession();
1140
1738
  const protoEvent = {
1141
1739
  eventId: generateUUID(),
1142
1740
  timestampMs: Date.now(),
@@ -1151,11 +1749,14 @@ var BidkernelPrebidAnalytics = class {
1151
1749
  protoEvent.bid = {
1152
1750
  bidder: data.bid.bidder || "",
1153
1751
  cpm: Number.isFinite(data.bid.cpm) ? data.bid.cpm : 0,
1154
- currency: data.bid.currency || "USD",
1752
+ // No defaults here: handlers with a priced bid (bidResponse, bidWon,
1753
+ // impression) set currency/mediaType themselves. Defaulting for the
1754
+ // rest would stamp fake "USD"/"banner" on noBid and bidRequest rows.
1755
+ currency: data.bid.currency || "",
1155
1756
  width: Number.isFinite(data.bid.width) ? data.bid.width : 0,
1156
1757
  height: Number.isFinite(data.bid.height) ? data.bid.height : 0,
1157
1758
  dealId: data.bid.dealId || "",
1158
- mediaType: data.bid.mediaType || "banner",
1759
+ mediaType: data.bid.mediaType || "",
1159
1760
  latencyMs: Number.isFinite(data.bid.latencyMs) ? data.bid.latencyMs : 0,
1160
1761
  advertiserDomain: data.bid.advertiserDomain || "",
1161
1762
  creativeId: data.bid.creativeId || ""
@@ -1171,6 +1772,9 @@ var BidkernelPrebidAnalytics = class {
1171
1772
  if (data.gpid) {
1172
1773
  metadata.gpid = String(data.gpid);
1173
1774
  }
1775
+ if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
1776
+ metadata.refresh_index = String(data.refreshIndex);
1777
+ }
1174
1778
  if (Object.keys(metadata).length > 0) {
1175
1779
  protoEvent.metadata = metadata;
1176
1780
  }
@@ -1200,14 +1804,13 @@ var BidkernelPrebidAnalytics = class {
1200
1804
  this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
1201
1805
  }
1202
1806
  }
1203
- // Drains the queue and returns the encoded batch plus target URL and event list,
1204
- // or null if there is nothing to send. Shared by flush() and flushBeacon().
1807
+ // Drains a batch (up to MAX_PAYLOAD_BYTES) from the queue.
1808
+ // Shared by flush() and flushBeacon().
1205
1809
  drainBatch() {
1206
1810
  if (this.queue.length === 0 || !this.config.endpoint) return null;
1207
- const events = this.queue;
1208
- this.queue = [];
1811
+ let events = this.queue;
1209
1812
  const domain = typeof window !== "undefined" ? window.location.hostname : "";
1210
- const batch = {
1813
+ const buildBatch = (evts) => ({
1211
1814
  propertyId: this.config.propertyId,
1212
1815
  pageviewId: this.config.pageviewId,
1213
1816
  sessionId: this.config.sessionId,
@@ -1217,25 +1820,42 @@ var BidkernelPrebidAnalytics = class {
1217
1820
  deviceType: this.config.deviceType,
1218
1821
  userId: this.config.userId,
1219
1822
  domain,
1220
- events
1221
- };
1823
+ events: evts
1824
+ });
1825
+ let encoded = TraceEventBatch.encode(
1826
+ buildBatch(events)
1827
+ ).finish();
1828
+ while (events.length > 1 && encoded.byteLength > MAX_PAYLOAD_BYTES) {
1829
+ events = events.slice(0, Math.floor(events.length / 2));
1830
+ encoded = TraceEventBatch.encode(
1831
+ buildBatch(events)
1832
+ ).finish();
1833
+ }
1834
+ this.queue = this.queue.slice(events.length);
1222
1835
  return {
1223
1836
  url: `${this.config.endpoint}/${this.config.propertyId}`,
1224
1837
  // protobufjs types finish() as Uint8Array<ArrayBufferLike>; the buffer is
1225
1838
  // always a plain ArrayBuffer, so narrow for fetch/Blob compatibility.
1226
- encoded: TraceEventBatch.encode(batch).finish(),
1839
+ encoded,
1227
1840
  events
1228
1841
  };
1229
1842
  }
1230
- sendFetch(payload) {
1231
- fetch(payload.url, {
1843
+ sendFetch(payload, useKeepalive = false) {
1844
+ const fetchOpts = {
1232
1845
  method: "POST",
1233
1846
  headers: { "Content-Type": "application/x-protobuf" },
1234
- body: payload.encoded,
1235
- keepalive: true
1236
- }).then((res) => {
1847
+ body: payload.encoded
1848
+ };
1849
+ if (useKeepalive) {
1850
+ fetchOpts.keepalive = true;
1851
+ }
1852
+ fetch(payload.url, fetchOpts).then((res) => {
1237
1853
  if (!res.ok) {
1238
1854
  this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
1855
+ if (res.status >= 400 && res.status < 500) {
1856
+ this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
1857
+ return;
1858
+ }
1239
1859
  this.handleSendFailure(payload.events);
1240
1860
  } else {
1241
1861
  this.consecutiveSendFailures = 0;
@@ -1245,6 +1865,12 @@ var BidkernelPrebidAnalytics = class {
1245
1865
  this.log("ERROR", "Failed to send batch", err);
1246
1866
  this.handleSendFailure(payload.events);
1247
1867
  });
1868
+ if (typeof process !== "undefined" && typeof process._tickCallback === "function") {
1869
+ try {
1870
+ process._tickCallback();
1871
+ } catch {
1872
+ }
1873
+ }
1248
1874
  }
1249
1875
  handleSendFailure(events) {
1250
1876
  this.consecutiveSendFailures++;
@@ -1264,24 +1890,31 @@ var BidkernelPrebidAnalytics = class {
1264
1890
  }
1265
1891
  flush() {
1266
1892
  if (Date.now() < this.nextSendAllowedAt) return;
1267
- const payload = this.drainBatch();
1268
- if (!payload) return;
1269
- this.sendFetch(payload);
1893
+ let payload = this.drainBatch();
1894
+ while (payload) {
1895
+ this.sendFetch(payload, false);
1896
+ if (Date.now() < this.nextSendAllowedAt) break;
1897
+ payload = this.drainBatch();
1898
+ }
1270
1899
  }
1271
1900
  flushBeacon() {
1272
- const payload = this.drainBatch();
1273
- if (!payload) return;
1274
- let sent = false;
1275
- if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
1276
- try {
1277
- const blob = new Blob([payload.encoded], { type: "application/x-protobuf" });
1278
- sent = navigator.sendBeacon(payload.url, blob);
1279
- } catch {
1280
- sent = false;
1901
+ let payload = this.drainBatch();
1902
+ while (payload) {
1903
+ let sent = false;
1904
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
1905
+ try {
1906
+ const blob = new Blob([payload.encoded], {
1907
+ type: "application/x-protobuf"
1908
+ });
1909
+ sent = navigator.sendBeacon(payload.url, blob);
1910
+ } catch {
1911
+ sent = false;
1912
+ }
1281
1913
  }
1282
- }
1283
- if (!sent) {
1284
- this.sendFetch(payload);
1914
+ if (!sent) {
1915
+ this.sendFetch(payload, true);
1916
+ }
1917
+ payload = this.drainBatch();
1285
1918
  }
1286
1919
  }
1287
1920
  log(level, msg, ...args) {