@bidkernel/analytics 0.4.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
@@ -835,7 +835,8 @@ function parseBidDimensions(bid) {
835
835
  height: Number.isFinite(bid?.height) ? bid.height : 0
836
836
  };
837
837
  }
838
- var BidkernelPrebidAnalytics = class {
838
+ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
839
+ static activeInstances = /* @__PURE__ */ new Set();
839
840
  config;
840
841
  queue = [];
841
842
  errorCount = 0;
@@ -851,6 +852,12 @@ var BidkernelPrebidAnalytics = class {
851
852
  consecutiveSendFailures = 0;
852
853
  nextSendAllowedAt = 0;
853
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();
854
861
  constructor(config) {
855
862
  this.config = {
856
863
  endpoint: config.endpoint || "",
@@ -864,19 +871,36 @@ var BidkernelPrebidAnalytics = class {
864
871
  auctionEnabled: config.auctionEnabled ?? true,
865
872
  warningsEnabled: config.warningsEnabled ?? true,
866
873
  errorsEnabled: config.errorsEnabled ?? true,
874
+ viewabilityEnabled: config.viewabilityEnabled ?? true,
867
875
  logLevel: config.logLevel || "INFO",
868
876
  pbjsGlobalName: config.pbjsGlobalName || "pbjs",
869
877
  attachPbjsListeners: config.attachPbjsListeners ?? true
870
878
  };
871
- this.boundFlushBeacon = () => this.flushBeacon();
879
+ this.boundFlushBeacon = () => {
880
+ this.flushAllSlotsTimeInView();
881
+ this.flushBeacon();
882
+ };
872
883
  this.boundVisibilityChange = () => this.handleVisibilityChange();
873
884
  }
874
885
  enable() {
875
886
  if (this.isEnabled) return;
876
887
  this.isEnabled = true;
888
+ _BidkernelPrebidAnalytics.activeInstances.add(this);
877
889
  if (!this.config.endpoint) {
878
890
  this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
879
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
+ }
880
904
  if (this.config.attachPbjsListeners) {
881
905
  const win = typeof window !== "undefined" ? window : {};
882
906
  const pbjs = win[this.config.pbjsGlobalName] || {};
@@ -956,7 +980,28 @@ var BidkernelPrebidAnalytics = class {
956
980
  }
957
981
  disable() {
958
982
  if (!this.isEnabled) return;
983
+ this.flushAllSlotsTimeInView();
959
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();
960
1005
  if (this.flushTimer) {
961
1006
  clearInterval(this.flushTimer);
962
1007
  this.flushTimer = null;
@@ -980,8 +1025,443 @@ var BidkernelPrebidAnalytics = class {
980
1025
  this.flush();
981
1026
  }
982
1027
  handleVisibilityChange() {
983
- 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
+ }
984
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);
985
1465
  }
986
1466
  }
987
1467
  trackRawEvent(level, eventName, data) {
@@ -996,6 +1476,8 @@ var BidkernelPrebidAnalytics = class {
996
1476
  }
997
1477
  navigate(pageviewId, pageUrl) {
998
1478
  this.disable();
1479
+ this.slotRefreshIndices.clear();
1480
+ this.pendingThresholdListeners.clear();
999
1481
  this.config.pageviewId = pageviewId || generateUUID();
1000
1482
  if (pageUrl) {
1001
1483
  try {
@@ -1170,23 +1652,81 @@ var BidkernelPrebidAnalytics = class {
1170
1652
  handleAdRenderSucceeded(data) {
1171
1653
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1172
1654
  this.log("DEBUG", "adRenderSucceeded", data);
1173
- 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;
1174
1687
  this.enqueue(TraceEventType.IMPRESSION, "impression", {
1175
- auctionId: bid.auctionId || data.auctionId || "",
1176
- transactionId: bid.transactionId || data.transactionId || "",
1177
- adUnitCode: data.adUnitCode || bid.adUnitCode || "",
1178
- bid: {
1179
- bidder: bid.bidderCode || bid.bidder || "",
1180
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1181
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1182
- ...parseBidDimensions(bid),
1183
- dealId: bid.dealId || "",
1184
- mediaType: bid.mediaType || "banner",
1185
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1186
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1187
- creativeId: bid.creativeId || ""
1188
- }
1688
+ auctionId,
1689
+ transactionId,
1690
+ adUnitCode,
1691
+ bid: bidTrace,
1692
+ metadata: { refresh_index: String(currentRefreshIndex) }
1189
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
+ }
1190
1730
  }
1191
1731
  isDuplicate(eventName, data) {
1192
1732
  return this.deduper.isDuplicate(eventName, data);
@@ -1232,6 +1772,9 @@ var BidkernelPrebidAnalytics = class {
1232
1772
  if (data.gpid) {
1233
1773
  metadata.gpid = String(data.gpid);
1234
1774
  }
1775
+ if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
1776
+ metadata.refresh_index = String(data.refreshIndex);
1777
+ }
1235
1778
  if (Object.keys(metadata).length > 0) {
1236
1779
  protoEvent.metadata = metadata;
1237
1780
  }
@@ -1322,6 +1865,12 @@ var BidkernelPrebidAnalytics = class {
1322
1865
  this.log("ERROR", "Failed to send batch", err);
1323
1866
  this.handleSendFailure(payload.events);
1324
1867
  });
1868
+ if (typeof process !== "undefined" && typeof process._tickCallback === "function") {
1869
+ try {
1870
+ process._tickCallback();
1871
+ } catch {
1872
+ }
1873
+ }
1325
1874
  }
1326
1875
  handleSendFailure(events) {
1327
1876
  this.consecutiveSendFailures++;