@bidkernel/analytics 0.4.0 → 0.6.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/analytics.global.js +1 -1
- package/dist/index.d.mts +46 -1
- package/dist/index.d.ts +46 -1
- package/dist/index.js +700 -25
- package/dist/index.mjs +700 -25
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -658,6 +658,11 @@ var MAX_QUEUE_SIZE = 200;
|
|
|
658
658
|
var MAX_SEND_BACKOFF_MS = 5 * 60 * 1e3;
|
|
659
659
|
var MAX_CONSECUTIVE_SEND_FAILURES = 10;
|
|
660
660
|
var MAX_PAYLOAD_BYTES = 32 * 1024;
|
|
661
|
+
var SAFE_CONTENT_TYPE = "text/plain";
|
|
662
|
+
var PENDING_BATCH_KEY_PREFIX = "_bidkernel_pending_";
|
|
663
|
+
var MAX_PENDING_BATCHES = 20;
|
|
664
|
+
var PENDING_BATCH_MAX_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
665
|
+
var PENDING_BATCH_CLEANUP_DELAY_MS = 1e4;
|
|
661
666
|
var EVENT_NAME_TO_TYPE = {
|
|
662
667
|
auctionStart: TraceEventType.AUCTION_START,
|
|
663
668
|
auctionEnd: TraceEventType.AUCTION_END,
|
|
@@ -673,6 +678,11 @@ var EVENT_NAME_TO_TYPE = {
|
|
|
673
678
|
timeInView: TraceEventType.TIME_IN_VIEW,
|
|
674
679
|
viewable: TraceEventType.VIEWABLE
|
|
675
680
|
};
|
|
681
|
+
var IMMEDIATE_FLUSH_TYPES = /* @__PURE__ */ new Set([
|
|
682
|
+
TraceEventType.IMPRESSION,
|
|
683
|
+
TraceEventType.BID_WIN,
|
|
684
|
+
TraceEventType.CLICK
|
|
685
|
+
]);
|
|
676
686
|
var SESSION_KEY = "_bidkernel_session";
|
|
677
687
|
var SESSION_TS_KEY = "_bidkernel_session_ts";
|
|
678
688
|
var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
|
|
@@ -688,6 +698,22 @@ function generateUUID() {
|
|
|
688
698
|
return v.toString(16);
|
|
689
699
|
});
|
|
690
700
|
}
|
|
701
|
+
function bytesToBase64(bytes) {
|
|
702
|
+
let binary = "";
|
|
703
|
+
const chunk = 8192;
|
|
704
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
705
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
706
|
+
}
|
|
707
|
+
return btoa(binary);
|
|
708
|
+
}
|
|
709
|
+
function base64ToBytes(b64) {
|
|
710
|
+
const binary = atob(b64);
|
|
711
|
+
const bytes = new Uint8Array(binary.length);
|
|
712
|
+
for (let i = 0; i < binary.length; i++) {
|
|
713
|
+
bytes[i] = binary.charCodeAt(i);
|
|
714
|
+
}
|
|
715
|
+
return bytes;
|
|
716
|
+
}
|
|
691
717
|
function extendSession() {
|
|
692
718
|
const now = Date.now();
|
|
693
719
|
inMemorySessionTs = now;
|
|
@@ -835,7 +861,8 @@ function parseBidDimensions(bid) {
|
|
|
835
861
|
height: Number.isFinite(bid?.height) ? bid.height : 0
|
|
836
862
|
};
|
|
837
863
|
}
|
|
838
|
-
var BidkernelPrebidAnalytics = class {
|
|
864
|
+
var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
|
|
865
|
+
static activeInstances = /* @__PURE__ */ new Set();
|
|
839
866
|
config;
|
|
840
867
|
queue = [];
|
|
841
868
|
errorCount = 0;
|
|
@@ -851,6 +878,18 @@ var BidkernelPrebidAnalytics = class {
|
|
|
851
878
|
consecutiveSendFailures = 0;
|
|
852
879
|
nextSendAllowedAt = 0;
|
|
853
880
|
replayedEventCount = 0;
|
|
881
|
+
immediateFlushScheduled = false;
|
|
882
|
+
// localStorage keys of exit batches this instance persisted, so a page that
|
|
883
|
+
// survives its own pagehide/hidden (bfcache restore, tab re-focus) can
|
|
884
|
+
// remove them instead of leaving them for a duplicate resend.
|
|
885
|
+
persistedBatchKeys = [];
|
|
886
|
+
persistedCleanupTimer = null;
|
|
887
|
+
// Viewability & refresh tracking
|
|
888
|
+
intersectionObserver = null;
|
|
889
|
+
slotViewabilityRecords = /* @__PURE__ */ new Map();
|
|
890
|
+
elementToSlotId = /* @__PURE__ */ new Map();
|
|
891
|
+
slotRefreshIndices = /* @__PURE__ */ new Map();
|
|
892
|
+
pendingThresholdListeners = /* @__PURE__ */ new Map();
|
|
854
893
|
constructor(config) {
|
|
855
894
|
this.config = {
|
|
856
895
|
endpoint: config.endpoint || "",
|
|
@@ -864,19 +903,36 @@ var BidkernelPrebidAnalytics = class {
|
|
|
864
903
|
auctionEnabled: config.auctionEnabled ?? true,
|
|
865
904
|
warningsEnabled: config.warningsEnabled ?? true,
|
|
866
905
|
errorsEnabled: config.errorsEnabled ?? true,
|
|
906
|
+
viewabilityEnabled: config.viewabilityEnabled ?? true,
|
|
867
907
|
logLevel: config.logLevel || "INFO",
|
|
868
908
|
pbjsGlobalName: config.pbjsGlobalName || "pbjs",
|
|
869
909
|
attachPbjsListeners: config.attachPbjsListeners ?? true
|
|
870
910
|
};
|
|
871
|
-
this.boundFlushBeacon = () =>
|
|
911
|
+
this.boundFlushBeacon = () => {
|
|
912
|
+
this.flushAllSlotsTimeInView();
|
|
913
|
+
this.flushBeacon();
|
|
914
|
+
};
|
|
872
915
|
this.boundVisibilityChange = () => this.handleVisibilityChange();
|
|
873
916
|
}
|
|
874
917
|
enable() {
|
|
875
918
|
if (this.isEnabled) return;
|
|
876
919
|
this.isEnabled = true;
|
|
920
|
+
_BidkernelPrebidAnalytics.activeInstances.add(this);
|
|
877
921
|
if (!this.config.endpoint) {
|
|
878
922
|
this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
|
|
879
923
|
}
|
|
924
|
+
if (this.config.viewabilityEnabled) {
|
|
925
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
926
|
+
this.intersectionObserver = new IntersectionObserver(this.handleIntersection.bind(this), {
|
|
927
|
+
threshold: [0.5]
|
|
928
|
+
});
|
|
929
|
+
for (const record of this.slotViewabilityRecords.values()) {
|
|
930
|
+
if (record.element) {
|
|
931
|
+
this.intersectionObserver.observe(record.element);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
880
936
|
if (this.config.attachPbjsListeners) {
|
|
881
937
|
const win = typeof window !== "undefined" ? window : {};
|
|
882
938
|
const pbjs = win[this.config.pbjsGlobalName] || {};
|
|
@@ -952,15 +1008,41 @@ var BidkernelPrebidAnalytics = class {
|
|
|
952
1008
|
document.addEventListener("visibilitychange", this.boundVisibilityChange);
|
|
953
1009
|
}
|
|
954
1010
|
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
1011
|
+
this.resendPersistedBatches();
|
|
955
1012
|
}
|
|
956
1013
|
}
|
|
957
1014
|
disable() {
|
|
958
1015
|
if (!this.isEnabled) return;
|
|
1016
|
+
this.flushAllSlotsTimeInView();
|
|
959
1017
|
this.isEnabled = false;
|
|
1018
|
+
_BidkernelPrebidAnalytics.activeInstances.delete(this);
|
|
1019
|
+
for (const record of this.slotViewabilityRecords.values()) {
|
|
1020
|
+
if (record.dwellTimer) {
|
|
1021
|
+
clearTimeout(record.dwellTimer);
|
|
1022
|
+
record.dwellTimer = null;
|
|
1023
|
+
record.dwellStartedAt = null;
|
|
1024
|
+
}
|
|
1025
|
+
for (const l of record.thresholdListeners) {
|
|
1026
|
+
if (l.timer) {
|
|
1027
|
+
clearTimeout(l.timer);
|
|
1028
|
+
l.timer = null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (this.intersectionObserver) {
|
|
1033
|
+
this.intersectionObserver.disconnect();
|
|
1034
|
+
this.intersectionObserver = null;
|
|
1035
|
+
}
|
|
1036
|
+
this.slotViewabilityRecords.clear();
|
|
1037
|
+
this.elementToSlotId.clear();
|
|
960
1038
|
if (this.flushTimer) {
|
|
961
1039
|
clearInterval(this.flushTimer);
|
|
962
1040
|
this.flushTimer = null;
|
|
963
1041
|
}
|
|
1042
|
+
if (this.persistedCleanupTimer) {
|
|
1043
|
+
clearTimeout(this.persistedCleanupTimer);
|
|
1044
|
+
this.persistedCleanupTimer = null;
|
|
1045
|
+
}
|
|
964
1046
|
if (typeof window !== "undefined") {
|
|
965
1047
|
window.removeEventListener("pagehide", this.boundFlushBeacon);
|
|
966
1048
|
if (typeof document !== "undefined") {
|
|
@@ -980,8 +1062,443 @@ var BidkernelPrebidAnalytics = class {
|
|
|
980
1062
|
this.flush();
|
|
981
1063
|
}
|
|
982
1064
|
handleVisibilityChange() {
|
|
983
|
-
if (typeof document
|
|
1065
|
+
if (typeof document === "undefined") return;
|
|
1066
|
+
if (document.visibilityState === "hidden") {
|
|
1067
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1068
|
+
for (const record of this.slotViewabilityRecords.values()) {
|
|
1069
|
+
if (record.inView && record.lastEnteredViewAt !== null) {
|
|
1070
|
+
record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
|
|
1071
|
+
record.lastEnteredViewAt = null;
|
|
1072
|
+
this.checkThresholdListeners(record);
|
|
1073
|
+
}
|
|
1074
|
+
if (record.dwellTimer) {
|
|
1075
|
+
clearTimeout(record.dwellTimer);
|
|
1076
|
+
record.dwellTimer = null;
|
|
1077
|
+
record.dwellStartedAt = null;
|
|
1078
|
+
}
|
|
1079
|
+
for (const l of record.thresholdListeners) {
|
|
1080
|
+
if (l.timer) {
|
|
1081
|
+
clearTimeout(l.timer);
|
|
1082
|
+
l.timer = null;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const durationMs = Math.round(record.accumulatedTimeInViewMs);
|
|
1086
|
+
if (durationMs > 0) {
|
|
1087
|
+
this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
|
|
1088
|
+
auctionId: record.auctionId,
|
|
1089
|
+
transactionId: record.transactionId,
|
|
1090
|
+
adUnitCode: record.adUnitCode,
|
|
1091
|
+
viewableDurationMs: durationMs,
|
|
1092
|
+
bid: record.bidPayload,
|
|
1093
|
+
metadata: {
|
|
1094
|
+
...record.metadata,
|
|
1095
|
+
refresh_index: String(record.refreshIndex)
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
984
1100
|
this.flushBeacon();
|
|
1101
|
+
} else if (document.visibilityState === "visible") {
|
|
1102
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1103
|
+
for (const record of this.slotViewabilityRecords.values()) {
|
|
1104
|
+
if (record.inView) {
|
|
1105
|
+
record.lastEnteredViewAt = now;
|
|
1106
|
+
if (!record.viewableFired && record.dwellTimer === null) {
|
|
1107
|
+
const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
|
|
1108
|
+
record.dwellStartedAt = now;
|
|
1109
|
+
record.dwellTimer = setTimeout(() => {
|
|
1110
|
+
this.handleDwellComplete(record);
|
|
1111
|
+
}, dwellTarget);
|
|
1112
|
+
}
|
|
1113
|
+
this.scheduleThresholdTimers(record);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
handleIntersection(entries) {
|
|
1119
|
+
for (const entry of entries) {
|
|
1120
|
+
let slotId = this.elementToSlotId.get(entry.target);
|
|
1121
|
+
if (!slotId) {
|
|
1122
|
+
for (const instance of _BidkernelPrebidAnalytics.activeInstances) {
|
|
1123
|
+
if (instance !== this && instance.elementToSlotId.has(entry.target)) {
|
|
1124
|
+
instance.handleIntersection([entry]);
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1131
|
+
if (!record) continue;
|
|
1132
|
+
const isViewableRatio = entry.isIntersecting && (entry.intersectionRatio === void 0 || entry.intersectionRatio >= 0.5);
|
|
1133
|
+
const isDocVisible = typeof document === "undefined" || document.visibilityState === "visible";
|
|
1134
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1135
|
+
if (isViewableRatio) {
|
|
1136
|
+
if (!record.inView) {
|
|
1137
|
+
record.inView = true;
|
|
1138
|
+
if (isDocVisible) {
|
|
1139
|
+
record.lastEnteredViewAt = now;
|
|
1140
|
+
if (!record.viewableFired && record.dwellTimer === null) {
|
|
1141
|
+
const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
|
|
1142
|
+
record.dwellStartedAt = now;
|
|
1143
|
+
record.dwellTimer = setTimeout(() => {
|
|
1144
|
+
this.handleDwellComplete(record);
|
|
1145
|
+
}, dwellTarget);
|
|
1146
|
+
}
|
|
1147
|
+
this.scheduleThresholdTimers(record);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
} else {
|
|
1151
|
+
if (record.inView) {
|
|
1152
|
+
if (record.lastEnteredViewAt !== null) {
|
|
1153
|
+
record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
|
|
1154
|
+
record.lastEnteredViewAt = null;
|
|
1155
|
+
}
|
|
1156
|
+
record.inView = false;
|
|
1157
|
+
this.checkThresholdListeners(record);
|
|
1158
|
+
}
|
|
1159
|
+
if (record.dwellTimer !== null) {
|
|
1160
|
+
clearTimeout(record.dwellTimer);
|
|
1161
|
+
record.dwellTimer = null;
|
|
1162
|
+
record.dwellStartedAt = null;
|
|
1163
|
+
}
|
|
1164
|
+
for (const l of record.thresholdListeners) {
|
|
1165
|
+
if (l.timer) {
|
|
1166
|
+
clearTimeout(l.timer);
|
|
1167
|
+
l.timer = null;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
handleDwellComplete(record) {
|
|
1174
|
+
record.dwellTimer = null;
|
|
1175
|
+
record.dwellStartedAt = null;
|
|
1176
|
+
if (!record.inView) return;
|
|
1177
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
1178
|
+
if (!record.viewableFired) {
|
|
1179
|
+
record.viewableFired = true;
|
|
1180
|
+
this.enqueue(TraceEventType.VIEWABLE, "viewable", {
|
|
1181
|
+
auctionId: record.auctionId,
|
|
1182
|
+
transactionId: record.transactionId,
|
|
1183
|
+
adUnitCode: record.adUnitCode,
|
|
1184
|
+
bid: record.bidPayload,
|
|
1185
|
+
metadata: {
|
|
1186
|
+
...record.metadata,
|
|
1187
|
+
refresh_index: String(record.refreshIndex)
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
scheduleThresholdTimers(record) {
|
|
1193
|
+
if (!record.inView || typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const currentTotal = this.getCurrentTimeInView(record);
|
|
1197
|
+
for (const listener of record.thresholdListeners) {
|
|
1198
|
+
if (listener.fired) continue;
|
|
1199
|
+
if (currentTotal >= listener.thresholdMs) {
|
|
1200
|
+
listener.fired = true;
|
|
1201
|
+
if (listener.timer) {
|
|
1202
|
+
clearTimeout(listener.timer);
|
|
1203
|
+
listener.timer = null;
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
listener.callback(record.slotId, currentTotal);
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
this.log("ERROR", "Error in threshold listener callback", e);
|
|
1209
|
+
}
|
|
1210
|
+
} else {
|
|
1211
|
+
if (listener.timer) {
|
|
1212
|
+
clearTimeout(listener.timer);
|
|
1213
|
+
}
|
|
1214
|
+
const remaining = listener.thresholdMs - currentTotal;
|
|
1215
|
+
listener.timer = setTimeout(() => {
|
|
1216
|
+
listener.timer = null;
|
|
1217
|
+
if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
|
|
1218
|
+
const nowTotal = this.getCurrentTimeInView(record);
|
|
1219
|
+
if (nowTotal >= listener.thresholdMs) {
|
|
1220
|
+
listener.fired = true;
|
|
1221
|
+
try {
|
|
1222
|
+
listener.callback(record.slotId, nowTotal);
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
this.log("ERROR", "Error in threshold listener callback", e);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}, remaining);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
checkThresholdListeners(record) {
|
|
1233
|
+
const currentTotal = this.getCurrentTimeInView(record);
|
|
1234
|
+
for (const listener of record.thresholdListeners) {
|
|
1235
|
+
if (!listener.fired && currentTotal >= listener.thresholdMs) {
|
|
1236
|
+
listener.fired = true;
|
|
1237
|
+
if (listener.timer) {
|
|
1238
|
+
clearTimeout(listener.timer);
|
|
1239
|
+
listener.timer = null;
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
listener.callback(record.slotId, currentTotal);
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
this.log("ERROR", "Error in threshold listener callback", e);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
getCurrentTimeInView(record) {
|
|
1250
|
+
let total = record.accumulatedTimeInViewMs;
|
|
1251
|
+
if (record.inView && record.lastEnteredViewAt !== null) {
|
|
1252
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1253
|
+
total += Math.max(0, now - record.lastEnteredViewAt);
|
|
1254
|
+
}
|
|
1255
|
+
return total;
|
|
1256
|
+
}
|
|
1257
|
+
flushSlotTimeInView(slotId) {
|
|
1258
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1259
|
+
if (!record) return;
|
|
1260
|
+
if (record.inView && record.lastEnteredViewAt !== null) {
|
|
1261
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1262
|
+
record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
|
|
1263
|
+
record.lastEnteredViewAt = null;
|
|
1264
|
+
}
|
|
1265
|
+
const durationMs = Math.round(record.accumulatedTimeInViewMs);
|
|
1266
|
+
if (durationMs > 0) {
|
|
1267
|
+
this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
|
|
1268
|
+
auctionId: record.auctionId,
|
|
1269
|
+
transactionId: record.transactionId,
|
|
1270
|
+
adUnitCode: record.adUnitCode,
|
|
1271
|
+
viewableDurationMs: durationMs,
|
|
1272
|
+
bid: record.bidPayload,
|
|
1273
|
+
metadata: {
|
|
1274
|
+
...record.metadata,
|
|
1275
|
+
refresh_index: String(record.refreshIndex)
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
record.accumulatedTimeInViewMs = 0;
|
|
1280
|
+
}
|
|
1281
|
+
flushAllSlotsTimeInView() {
|
|
1282
|
+
for (const slotId of Array.from(this.slotViewabilityRecords.keys())) {
|
|
1283
|
+
this.flushSlotTimeInView(slotId);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
observeSlot(element, slotId, options) {
|
|
1287
|
+
if (!this.config.viewabilityEnabled) return;
|
|
1288
|
+
let el = null;
|
|
1289
|
+
if (typeof element === "string") {
|
|
1290
|
+
if (typeof document !== "undefined") {
|
|
1291
|
+
try {
|
|
1292
|
+
el = document.querySelector(element) || document.getElementById(element);
|
|
1293
|
+
} catch {
|
|
1294
|
+
el = document.getElementById(element);
|
|
1295
|
+
}
|
|
1296
|
+
if (!el) {
|
|
1297
|
+
el = document.getElementById(element);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
} else {
|
|
1301
|
+
el = element;
|
|
1302
|
+
}
|
|
1303
|
+
const resolvedSlotId = slotId || options?.adUnitCode || (el ? el.id : "") || generateUUID();
|
|
1304
|
+
const existing = this.slotViewabilityRecords.get(resolvedSlotId);
|
|
1305
|
+
if (existing) {
|
|
1306
|
+
const isNewRefreshCycle = options?.refreshIndex === void 0 || options.refreshIndex > existing.refreshIndex;
|
|
1307
|
+
if (isNewRefreshCycle) {
|
|
1308
|
+
this.flushSlotTimeInView(resolvedSlotId);
|
|
1309
|
+
const nextRefreshIndex = options?.refreshIndex !== void 0 ? options.refreshIndex : (this.slotRefreshIndices.get(resolvedSlotId) ?? existing.refreshIndex) + 1;
|
|
1310
|
+
this.slotRefreshIndices.set(resolvedSlotId, nextRefreshIndex);
|
|
1311
|
+
if (options?.emitRefreshEvent !== false) {
|
|
1312
|
+
this.enqueue(TraceEventType.REFRESH, "refresh", {
|
|
1313
|
+
auctionId: options?.auctionId || existing.auctionId,
|
|
1314
|
+
transactionId: options?.transactionId || existing.transactionId,
|
|
1315
|
+
adUnitCode: resolvedSlotId,
|
|
1316
|
+
bid: options?.bid ?? existing.bidPayload,
|
|
1317
|
+
metadata: {
|
|
1318
|
+
...options?.metadata,
|
|
1319
|
+
refresh_index: String(nextRefreshIndex)
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
if (existing.dwellTimer) {
|
|
1324
|
+
clearTimeout(existing.dwellTimer);
|
|
1325
|
+
existing.dwellTimer = null;
|
|
1326
|
+
existing.dwellStartedAt = null;
|
|
1327
|
+
}
|
|
1328
|
+
for (const l of existing.thresholdListeners) {
|
|
1329
|
+
l.fired = false;
|
|
1330
|
+
if (l.timer) {
|
|
1331
|
+
clearTimeout(l.timer);
|
|
1332
|
+
l.timer = null;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
existing.viewableFired = false;
|
|
1336
|
+
existing.accumulatedTimeInViewMs = 0;
|
|
1337
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1338
|
+
existing.lastEnteredViewAt = existing.inView ? now : null;
|
|
1339
|
+
existing.refreshIndex = nextRefreshIndex;
|
|
1340
|
+
existing.auctionId = options?.auctionId || existing.auctionId;
|
|
1341
|
+
existing.transactionId = options?.transactionId || existing.transactionId;
|
|
1342
|
+
existing.mediaType = options?.mediaType || existing.mediaType || "banner";
|
|
1343
|
+
existing.bidPayload = options?.bid !== void 0 ? options.bid : existing.bidPayload;
|
|
1344
|
+
existing.metadata = options?.metadata ?? existing.metadata;
|
|
1345
|
+
if (existing.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
|
|
1346
|
+
const dwellTarget = existing.mediaType === "video" ? 2e3 : 1e3;
|
|
1347
|
+
existing.dwellStartedAt = now;
|
|
1348
|
+
existing.dwellTimer = setTimeout(() => {
|
|
1349
|
+
this.handleDwellComplete(existing);
|
|
1350
|
+
}, dwellTarget);
|
|
1351
|
+
this.scheduleThresholdTimers(existing);
|
|
1352
|
+
}
|
|
1353
|
+
} else {
|
|
1354
|
+
existing.auctionId = options?.auctionId || existing.auctionId;
|
|
1355
|
+
existing.transactionId = options?.transactionId || existing.transactionId;
|
|
1356
|
+
existing.mediaType = options?.mediaType || existing.mediaType;
|
|
1357
|
+
if (options?.bid !== void 0) existing.bidPayload = options.bid;
|
|
1358
|
+
if (options?.metadata !== void 0) existing.metadata = options.metadata;
|
|
1359
|
+
}
|
|
1360
|
+
if (el) {
|
|
1361
|
+
if (existing.element && existing.element !== el && this.intersectionObserver) {
|
|
1362
|
+
this.intersectionObserver.unobserve(existing.element);
|
|
1363
|
+
this.elementToSlotId.delete(existing.element);
|
|
1364
|
+
}
|
|
1365
|
+
existing.element = el;
|
|
1366
|
+
this.elementToSlotId.set(el, resolvedSlotId);
|
|
1367
|
+
this.intersectionObserver?.observe(el);
|
|
1368
|
+
}
|
|
1369
|
+
} else {
|
|
1370
|
+
const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
|
|
1371
|
+
this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
|
|
1372
|
+
const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
|
|
1373
|
+
this.pendingThresholdListeners.delete(resolvedSlotId);
|
|
1374
|
+
const record = {
|
|
1375
|
+
slotId: resolvedSlotId,
|
|
1376
|
+
element: el,
|
|
1377
|
+
adUnitCode: options?.adUnitCode || resolvedSlotId,
|
|
1378
|
+
auctionId: options?.auctionId || "",
|
|
1379
|
+
transactionId: options?.transactionId || "",
|
|
1380
|
+
mediaType: options?.mediaType || "banner",
|
|
1381
|
+
bidPayload: options?.bid,
|
|
1382
|
+
metadata: options?.metadata,
|
|
1383
|
+
refreshIndex: initialRefreshIndex,
|
|
1384
|
+
inView: false,
|
|
1385
|
+
lastEnteredViewAt: null,
|
|
1386
|
+
accumulatedTimeInViewMs: 0,
|
|
1387
|
+
viewableFired: false,
|
|
1388
|
+
dwellTimer: null,
|
|
1389
|
+
dwellStartedAt: null,
|
|
1390
|
+
thresholdListeners: listeners
|
|
1391
|
+
};
|
|
1392
|
+
this.slotViewabilityRecords.set(resolvedSlotId, record);
|
|
1393
|
+
if (el) {
|
|
1394
|
+
this.elementToSlotId.set(el, resolvedSlotId);
|
|
1395
|
+
this.intersectionObserver?.observe(el);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
unobserveSlot(slotId) {
|
|
1400
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1401
|
+
if (!record) return;
|
|
1402
|
+
this.flushSlotTimeInView(slotId);
|
|
1403
|
+
if (record.dwellTimer) {
|
|
1404
|
+
clearTimeout(record.dwellTimer);
|
|
1405
|
+
record.dwellTimer = null;
|
|
1406
|
+
record.dwellStartedAt = null;
|
|
1407
|
+
}
|
|
1408
|
+
for (const l of record.thresholdListeners) {
|
|
1409
|
+
if (l.timer) {
|
|
1410
|
+
clearTimeout(l.timer);
|
|
1411
|
+
l.timer = null;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
record.thresholdListeners = /* @__PURE__ */ new Set();
|
|
1415
|
+
if (record.element && this.intersectionObserver) {
|
|
1416
|
+
this.intersectionObserver.unobserve(record.element);
|
|
1417
|
+
this.elementToSlotId.delete(record.element);
|
|
1418
|
+
record.element = null;
|
|
1419
|
+
}
|
|
1420
|
+
record.inView = false;
|
|
1421
|
+
record.lastEnteredViewAt = null;
|
|
1422
|
+
}
|
|
1423
|
+
destroySlot(slotId) {
|
|
1424
|
+
this.unobserveSlot(slotId);
|
|
1425
|
+
this.slotViewabilityRecords.delete(slotId);
|
|
1426
|
+
this.slotRefreshIndices.delete(slotId);
|
|
1427
|
+
this.pendingThresholdListeners.delete(slotId);
|
|
1428
|
+
}
|
|
1429
|
+
onTimeInViewThreshold(slotId, thresholdMs, callback) {
|
|
1430
|
+
const listener = {
|
|
1431
|
+
thresholdMs,
|
|
1432
|
+
callback,
|
|
1433
|
+
timer: null,
|
|
1434
|
+
fired: false
|
|
1435
|
+
};
|
|
1436
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1437
|
+
if (record) {
|
|
1438
|
+
record.thresholdListeners.add(listener);
|
|
1439
|
+
const currentTotal = this.getCurrentTimeInView(record);
|
|
1440
|
+
if (currentTotal >= thresholdMs) {
|
|
1441
|
+
listener.fired = true;
|
|
1442
|
+
try {
|
|
1443
|
+
callback(slotId, currentTotal);
|
|
1444
|
+
} catch (e) {
|
|
1445
|
+
this.log("ERROR", "Error in threshold callback", e);
|
|
1446
|
+
}
|
|
1447
|
+
} else if (record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
|
|
1448
|
+
const remaining = thresholdMs - currentTotal;
|
|
1449
|
+
listener.timer = setTimeout(() => {
|
|
1450
|
+
listener.timer = null;
|
|
1451
|
+
if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
|
|
1452
|
+
const nowTotal = this.getCurrentTimeInView(record);
|
|
1453
|
+
if (nowTotal >= thresholdMs) {
|
|
1454
|
+
listener.fired = true;
|
|
1455
|
+
try {
|
|
1456
|
+
callback(slotId, nowTotal);
|
|
1457
|
+
} catch (e) {
|
|
1458
|
+
this.log("ERROR", "Error in threshold callback", e);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}, remaining);
|
|
1463
|
+
}
|
|
1464
|
+
} else {
|
|
1465
|
+
if (!this.pendingThresholdListeners.has(slotId)) {
|
|
1466
|
+
this.pendingThresholdListeners.set(slotId, /* @__PURE__ */ new Set());
|
|
1467
|
+
}
|
|
1468
|
+
this.pendingThresholdListeners.get(slotId).add(listener);
|
|
1469
|
+
}
|
|
1470
|
+
return () => {
|
|
1471
|
+
if (listener.timer) {
|
|
1472
|
+
clearTimeout(listener.timer);
|
|
1473
|
+
listener.timer = null;
|
|
1474
|
+
}
|
|
1475
|
+
if (record) {
|
|
1476
|
+
record.thresholdListeners.delete(listener);
|
|
1477
|
+
}
|
|
1478
|
+
const pending = this.pendingThresholdListeners.get(slotId);
|
|
1479
|
+
if (pending) {
|
|
1480
|
+
pending.delete(listener);
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
getViewabilityState(slotId) {
|
|
1485
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1486
|
+
if (!record) return void 0;
|
|
1487
|
+
const timeInViewMs = Math.round(this.getCurrentTimeInView(record));
|
|
1488
|
+
return {
|
|
1489
|
+
inView: record.inView,
|
|
1490
|
+
viewable: record.viewableFired,
|
|
1491
|
+
timeInViewMs,
|
|
1492
|
+
refreshIndex: record.refreshIndex
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
getRefreshIndex(slotId) {
|
|
1496
|
+
return this.slotRefreshIndices.get(slotId) ?? 0;
|
|
1497
|
+
}
|
|
1498
|
+
triggerSlotRefresh(slotId, options) {
|
|
1499
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1500
|
+
if (record && record.element) {
|
|
1501
|
+
this.observeSlot(record.element, slotId, options);
|
|
985
1502
|
}
|
|
986
1503
|
}
|
|
987
1504
|
trackRawEvent(level, eventName, data) {
|
|
@@ -996,6 +1513,8 @@ var BidkernelPrebidAnalytics = class {
|
|
|
996
1513
|
}
|
|
997
1514
|
navigate(pageviewId, pageUrl) {
|
|
998
1515
|
this.disable();
|
|
1516
|
+
this.slotRefreshIndices.clear();
|
|
1517
|
+
this.pendingThresholdListeners.clear();
|
|
999
1518
|
this.config.pageviewId = pageviewId || generateUUID();
|
|
1000
1519
|
if (pageUrl) {
|
|
1001
1520
|
try {
|
|
@@ -1170,23 +1689,81 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1170
1689
|
handleAdRenderSucceeded(data) {
|
|
1171
1690
|
if (this.isDuplicate("adRenderSucceeded", data)) return;
|
|
1172
1691
|
this.log("DEBUG", "adRenderSucceeded", data);
|
|
1173
|
-
const bid = data.bid || {};
|
|
1692
|
+
const bid = data.bid || data || {};
|
|
1693
|
+
const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
|
|
1694
|
+
const auctionId = bid.auctionId || data.auctionId || "";
|
|
1695
|
+
const transactionId = bid.transactionId || data.transactionId || "";
|
|
1696
|
+
const mediaType = bid.mediaType || "banner";
|
|
1697
|
+
const bidTrace = {
|
|
1698
|
+
bidder: bid.bidderCode || bid.bidder || "",
|
|
1699
|
+
cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
|
|
1700
|
+
currency: bid.originalCurrency ?? bid.currency ?? "USD",
|
|
1701
|
+
...parseBidDimensions(bid),
|
|
1702
|
+
dealId: bid.dealId || "",
|
|
1703
|
+
mediaType,
|
|
1704
|
+
latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
|
|
1705
|
+
advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
|
|
1706
|
+
creativeId: bid.creativeId || ""
|
|
1707
|
+
};
|
|
1708
|
+
const isRefresh = adUnitCode && (this.slotViewabilityRecords.has(adUnitCode) || this.slotRefreshIndices.has(adUnitCode) && this.slotRefreshIndices.get(adUnitCode) > 0);
|
|
1709
|
+
if (isRefresh) {
|
|
1710
|
+
this.flushSlotTimeInView(adUnitCode);
|
|
1711
|
+
const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
|
|
1712
|
+
this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
|
|
1713
|
+
this.enqueue(TraceEventType.REFRESH, "refresh", {
|
|
1714
|
+
auctionId,
|
|
1715
|
+
transactionId,
|
|
1716
|
+
adUnitCode,
|
|
1717
|
+
bid: bidTrace,
|
|
1718
|
+
metadata: { refresh_index: String(nextRefreshIndex) }
|
|
1719
|
+
});
|
|
1720
|
+
} else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
|
|
1721
|
+
this.slotRefreshIndices.set(adUnitCode, 0);
|
|
1722
|
+
}
|
|
1723
|
+
const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
|
|
1174
1724
|
this.enqueue(TraceEventType.IMPRESSION, "impression", {
|
|
1175
|
-
auctionId
|
|
1176
|
-
transactionId
|
|
1177
|
-
adUnitCode
|
|
1178
|
-
bid:
|
|
1179
|
-
|
|
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
|
-
}
|
|
1725
|
+
auctionId,
|
|
1726
|
+
transactionId,
|
|
1727
|
+
adUnitCode,
|
|
1728
|
+
bid: bidTrace,
|
|
1729
|
+
metadata: { refresh_index: String(currentRefreshIndex) }
|
|
1189
1730
|
});
|
|
1731
|
+
if (this.config.viewabilityEnabled && typeof document !== "undefined") {
|
|
1732
|
+
let el = null;
|
|
1733
|
+
const targetId = adUnitCode || data.adId || bid.adId;
|
|
1734
|
+
if (targetId) {
|
|
1735
|
+
el = document.getElementById(targetId);
|
|
1736
|
+
}
|
|
1737
|
+
if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
|
|
1738
|
+
try {
|
|
1739
|
+
const slots = window.googletag.pubads().getSlots();
|
|
1740
|
+
for (const slot of slots) {
|
|
1741
|
+
if (slot.getAdUnitPath?.() === adUnitCode || slot.getSlotElementId?.() === adUnitCode) {
|
|
1742
|
+
el = document.getElementById(slot.getSlotElementId());
|
|
1743
|
+
if (el) break;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
} catch {
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (!el && targetId) {
|
|
1750
|
+
try {
|
|
1751
|
+
el = document.querySelector(`[data-ad-slot="${targetId}"]`) || document.querySelector(`#${targetId}`);
|
|
1752
|
+
} catch {
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (el) {
|
|
1756
|
+
this.observeSlot(el, adUnitCode || targetId, {
|
|
1757
|
+
adUnitCode,
|
|
1758
|
+
auctionId,
|
|
1759
|
+
transactionId,
|
|
1760
|
+
mediaType,
|
|
1761
|
+
bid: bidTrace,
|
|
1762
|
+
refreshIndex: currentRefreshIndex,
|
|
1763
|
+
emitRefreshEvent: false
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1190
1767
|
}
|
|
1191
1768
|
isDuplicate(eventName, data) {
|
|
1192
1769
|
return this.deduper.isDuplicate(eventName, data);
|
|
@@ -1232,6 +1809,9 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1232
1809
|
if (data.gpid) {
|
|
1233
1810
|
metadata.gpid = String(data.gpid);
|
|
1234
1811
|
}
|
|
1812
|
+
if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
|
|
1813
|
+
metadata.refresh_index = String(data.refreshIndex);
|
|
1814
|
+
}
|
|
1235
1815
|
if (Object.keys(metadata).length > 0) {
|
|
1236
1816
|
protoEvent.metadata = metadata;
|
|
1237
1817
|
}
|
|
@@ -1241,6 +1821,12 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1241
1821
|
}
|
|
1242
1822
|
if (this.queue.length >= BATCH_SIZE) {
|
|
1243
1823
|
this.flush();
|
|
1824
|
+
} else if (IMMEDIATE_FLUSH_TYPES.has(type) && !this.immediateFlushScheduled) {
|
|
1825
|
+
this.immediateFlushScheduled = true;
|
|
1826
|
+
setTimeout(() => {
|
|
1827
|
+
this.immediateFlushScheduled = false;
|
|
1828
|
+
this.flush();
|
|
1829
|
+
}, 0);
|
|
1244
1830
|
}
|
|
1245
1831
|
}
|
|
1246
1832
|
shouldSample(type, level) {
|
|
@@ -1297,10 +1883,11 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1297
1883
|
events
|
|
1298
1884
|
};
|
|
1299
1885
|
}
|
|
1300
|
-
sendFetch(payload, useKeepalive = false) {
|
|
1886
|
+
sendFetch(payload, useKeepalive = false, onDelivered) {
|
|
1301
1887
|
const fetchOpts = {
|
|
1302
1888
|
method: "POST",
|
|
1303
|
-
|
|
1889
|
+
// Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
|
|
1890
|
+
headers: { "Content-Type": SAFE_CONTENT_TYPE },
|
|
1304
1891
|
body: payload.encoded
|
|
1305
1892
|
};
|
|
1306
1893
|
if (useKeepalive) {
|
|
@@ -1309,7 +1896,7 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1309
1896
|
fetch(payload.url, fetchOpts).then((res) => {
|
|
1310
1897
|
if (!res.ok) {
|
|
1311
1898
|
this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
|
|
1312
|
-
if (res.status >= 400 && res.status < 500) {
|
|
1899
|
+
if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 408) {
|
|
1313
1900
|
this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
|
|
1314
1901
|
return;
|
|
1315
1902
|
}
|
|
@@ -1317,11 +1904,19 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1317
1904
|
} else {
|
|
1318
1905
|
this.consecutiveSendFailures = 0;
|
|
1319
1906
|
this.nextSendAllowedAt = 0;
|
|
1907
|
+
if (onDelivered) onDelivered();
|
|
1320
1908
|
}
|
|
1321
1909
|
}).catch((err) => {
|
|
1322
1910
|
this.log("ERROR", "Failed to send batch", err);
|
|
1323
1911
|
this.handleSendFailure(payload.events);
|
|
1324
1912
|
});
|
|
1913
|
+
const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
|
|
1914
|
+
if (proc && typeof proc._tickCallback === "function") {
|
|
1915
|
+
try {
|
|
1916
|
+
proc._tickCallback();
|
|
1917
|
+
} catch {
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1325
1920
|
}
|
|
1326
1921
|
handleSendFailure(events) {
|
|
1327
1922
|
this.consecutiveSendFailures++;
|
|
@@ -1354,19 +1949,99 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1354
1949
|
let sent = false;
|
|
1355
1950
|
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
1356
1951
|
try {
|
|
1357
|
-
const blob = new Blob([payload.encoded], {
|
|
1358
|
-
type: "application/x-protobuf"
|
|
1359
|
-
});
|
|
1952
|
+
const blob = new Blob([payload.encoded], { type: SAFE_CONTENT_TYPE });
|
|
1360
1953
|
sent = navigator.sendBeacon(payload.url, blob);
|
|
1361
1954
|
} catch {
|
|
1362
1955
|
sent = false;
|
|
1363
1956
|
}
|
|
1364
1957
|
}
|
|
1365
1958
|
if (!sent) {
|
|
1366
|
-
this.
|
|
1959
|
+
const persistKey = this.persistBatch(payload.encoded);
|
|
1960
|
+
this.sendFetch(payload, true, () => this.removePersistedBatch(persistKey));
|
|
1367
1961
|
}
|
|
1368
1962
|
payload = this.drainBatch();
|
|
1369
1963
|
}
|
|
1964
|
+
this.schedulePersistedCleanup();
|
|
1965
|
+
}
|
|
1966
|
+
// --- Exit-batch persistence -----------------------------------------------
|
|
1967
|
+
persistBatch(encoded) {
|
|
1968
|
+
try {
|
|
1969
|
+
if (typeof localStorage === "undefined") return null;
|
|
1970
|
+
let pendingCount = 0;
|
|
1971
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
1972
|
+
const k = localStorage.key(i);
|
|
1973
|
+
if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
|
|
1974
|
+
}
|
|
1975
|
+
if (pendingCount >= MAX_PENDING_BATCHES) return null;
|
|
1976
|
+
const key = `${PENDING_BATCH_KEY_PREFIX}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
|
|
1977
|
+
localStorage.setItem(key, bytesToBase64(encoded));
|
|
1978
|
+
this.persistedBatchKeys.push(key);
|
|
1979
|
+
return key;
|
|
1980
|
+
} catch {
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
removePersistedBatch(key) {
|
|
1985
|
+
if (!key) return;
|
|
1986
|
+
try {
|
|
1987
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(key);
|
|
1988
|
+
} catch {
|
|
1989
|
+
}
|
|
1990
|
+
const idx = this.persistedBatchKeys.indexOf(key);
|
|
1991
|
+
if (idx !== -1) this.persistedBatchKeys.splice(idx, 1);
|
|
1992
|
+
}
|
|
1993
|
+
// A page that is still running PENDING_BATCH_CLEANUP_DELAY_MS after an exit
|
|
1994
|
+
// flush was never torn down, so its beacons have long since gone out; drop
|
|
1995
|
+
// the persisted copies rather than letting a later pageview resend them.
|
|
1996
|
+
schedulePersistedCleanup() {
|
|
1997
|
+
if (this.persistedBatchKeys.length === 0) return;
|
|
1998
|
+
if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
|
|
1999
|
+
this.persistedCleanupTimer = setTimeout(() => {
|
|
2000
|
+
this.persistedCleanupTimer = null;
|
|
2001
|
+
for (const key of [...this.persistedBatchKeys]) {
|
|
2002
|
+
this.removePersistedBatch(key);
|
|
2003
|
+
}
|
|
2004
|
+
}, PENDING_BATCH_CLEANUP_DELAY_MS);
|
|
2005
|
+
}
|
|
2006
|
+
// Resend batches a previous pageview persisted at exit but could not
|
|
2007
|
+
// confirm. Keys are claimed (removed) before sending so concurrent tabs
|
|
2008
|
+
// don't double-send; the batch bytes carry their original pageview, session,
|
|
2009
|
+
// and event timestamps, so late rows attribute correctly.
|
|
2010
|
+
resendPersistedBatches() {
|
|
2011
|
+
try {
|
|
2012
|
+
if (typeof localStorage === "undefined" || !this.config.endpoint) return;
|
|
2013
|
+
const keys = [];
|
|
2014
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
2015
|
+
const k = localStorage.key(i);
|
|
2016
|
+
if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) keys.push(k);
|
|
2017
|
+
}
|
|
2018
|
+
for (const key of keys) {
|
|
2019
|
+
let value = null;
|
|
2020
|
+
try {
|
|
2021
|
+
value = localStorage.getItem(key);
|
|
2022
|
+
localStorage.removeItem(key);
|
|
2023
|
+
} catch {
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
if (!value) continue;
|
|
2027
|
+
const ts = parseInt(key.slice(PENDING_BATCH_KEY_PREFIX.length), 10);
|
|
2028
|
+
if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
|
|
2029
|
+
let encoded;
|
|
2030
|
+
try {
|
|
2031
|
+
encoded = base64ToBytes(value);
|
|
2032
|
+
} catch {
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if (encoded.byteLength === 0) continue;
|
|
2036
|
+
this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
|
|
2037
|
+
this.sendFetch({
|
|
2038
|
+
url: `${this.config.endpoint}/${this.config.propertyId}`,
|
|
2039
|
+
encoded,
|
|
2040
|
+
events: []
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
1370
2045
|
}
|
|
1371
2046
|
log(level, msg, ...args) {
|
|
1372
2047
|
const levels = { DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 };
|