@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.mjs CHANGED
@@ -805,7 +805,8 @@ function parseBidDimensions(bid) {
805
805
  height: Number.isFinite(bid?.height) ? bid.height : 0
806
806
  };
807
807
  }
808
- var BidkernelPrebidAnalytics = class {
808
+ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
809
+ static activeInstances = /* @__PURE__ */ new Set();
809
810
  config;
810
811
  queue = [];
811
812
  errorCount = 0;
@@ -821,6 +822,12 @@ var BidkernelPrebidAnalytics = class {
821
822
  consecutiveSendFailures = 0;
822
823
  nextSendAllowedAt = 0;
823
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();
824
831
  constructor(config) {
825
832
  this.config = {
826
833
  endpoint: config.endpoint || "",
@@ -834,19 +841,36 @@ var BidkernelPrebidAnalytics = class {
834
841
  auctionEnabled: config.auctionEnabled ?? true,
835
842
  warningsEnabled: config.warningsEnabled ?? true,
836
843
  errorsEnabled: config.errorsEnabled ?? true,
844
+ viewabilityEnabled: config.viewabilityEnabled ?? true,
837
845
  logLevel: config.logLevel || "INFO",
838
846
  pbjsGlobalName: config.pbjsGlobalName || "pbjs",
839
847
  attachPbjsListeners: config.attachPbjsListeners ?? true
840
848
  };
841
- this.boundFlushBeacon = () => this.flushBeacon();
849
+ this.boundFlushBeacon = () => {
850
+ this.flushAllSlotsTimeInView();
851
+ this.flushBeacon();
852
+ };
842
853
  this.boundVisibilityChange = () => this.handleVisibilityChange();
843
854
  }
844
855
  enable() {
845
856
  if (this.isEnabled) return;
846
857
  this.isEnabled = true;
858
+ _BidkernelPrebidAnalytics.activeInstances.add(this);
847
859
  if (!this.config.endpoint) {
848
860
  this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
849
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
+ }
850
874
  if (this.config.attachPbjsListeners) {
851
875
  const win = typeof window !== "undefined" ? window : {};
852
876
  const pbjs = win[this.config.pbjsGlobalName] || {};
@@ -926,7 +950,28 @@ var BidkernelPrebidAnalytics = class {
926
950
  }
927
951
  disable() {
928
952
  if (!this.isEnabled) return;
953
+ this.flushAllSlotsTimeInView();
929
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();
930
975
  if (this.flushTimer) {
931
976
  clearInterval(this.flushTimer);
932
977
  this.flushTimer = null;
@@ -950,8 +995,443 @@ var BidkernelPrebidAnalytics = class {
950
995
  this.flush();
951
996
  }
952
997
  handleVisibilityChange() {
953
- 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
+ }
954
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);
955
1435
  }
956
1436
  }
957
1437
  trackRawEvent(level, eventName, data) {
@@ -966,6 +1446,8 @@ var BidkernelPrebidAnalytics = class {
966
1446
  }
967
1447
  navigate(pageviewId, pageUrl) {
968
1448
  this.disable();
1449
+ this.slotRefreshIndices.clear();
1450
+ this.pendingThresholdListeners.clear();
969
1451
  this.config.pageviewId = pageviewId || generateUUID();
970
1452
  if (pageUrl) {
971
1453
  try {
@@ -1140,23 +1622,81 @@ var BidkernelPrebidAnalytics = class {
1140
1622
  handleAdRenderSucceeded(data) {
1141
1623
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1142
1624
  this.log("DEBUG", "adRenderSucceeded", data);
1143
- 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;
1144
1657
  this.enqueue(TraceEventType.IMPRESSION, "impression", {
1145
- auctionId: bid.auctionId || data.auctionId || "",
1146
- transactionId: bid.transactionId || data.transactionId || "",
1147
- adUnitCode: data.adUnitCode || bid.adUnitCode || "",
1148
- bid: {
1149
- bidder: bid.bidderCode || bid.bidder || "",
1150
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1151
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1152
- ...parseBidDimensions(bid),
1153
- dealId: bid.dealId || "",
1154
- mediaType: bid.mediaType || "banner",
1155
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1156
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1157
- creativeId: bid.creativeId || ""
1158
- }
1658
+ auctionId,
1659
+ transactionId,
1660
+ adUnitCode,
1661
+ bid: bidTrace,
1662
+ metadata: { refresh_index: String(currentRefreshIndex) }
1159
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
+ }
1160
1700
  }
1161
1701
  isDuplicate(eventName, data) {
1162
1702
  return this.deduper.isDuplicate(eventName, data);
@@ -1202,6 +1742,9 @@ var BidkernelPrebidAnalytics = class {
1202
1742
  if (data.gpid) {
1203
1743
  metadata.gpid = String(data.gpid);
1204
1744
  }
1745
+ if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
1746
+ metadata.refresh_index = String(data.refreshIndex);
1747
+ }
1205
1748
  if (Object.keys(metadata).length > 0) {
1206
1749
  protoEvent.metadata = metadata;
1207
1750
  }
@@ -1292,6 +1835,12 @@ var BidkernelPrebidAnalytics = class {
1292
1835
  this.log("ERROR", "Failed to send batch", err);
1293
1836
  this.handleSendFailure(payload.events);
1294
1837
  });
1838
+ if (typeof process !== "undefined" && typeof process._tickCallback === "function") {
1839
+ try {
1840
+ process._tickCallback();
1841
+ } catch {
1842
+ }
1843
+ }
1295
1844
  }
1296
1845
  handleSendFailure(events) {
1297
1846
  this.consecutiveSendFailures++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bidkernel/analytics",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Bidkernel auction analytics: a standalone Prebid.js analytics adapter, plus TypeScript bindings for the Bidkernel ad SDK.",
5
5
  "keywords": [
6
6
  "advertising",