@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.mjs
CHANGED
|
@@ -628,6 +628,11 @@ var MAX_QUEUE_SIZE = 200;
|
|
|
628
628
|
var MAX_SEND_BACKOFF_MS = 5 * 60 * 1e3;
|
|
629
629
|
var MAX_CONSECUTIVE_SEND_FAILURES = 10;
|
|
630
630
|
var MAX_PAYLOAD_BYTES = 32 * 1024;
|
|
631
|
+
var SAFE_CONTENT_TYPE = "text/plain";
|
|
632
|
+
var PENDING_BATCH_KEY_PREFIX = "_bidkernel_pending_";
|
|
633
|
+
var MAX_PENDING_BATCHES = 20;
|
|
634
|
+
var PENDING_BATCH_MAX_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
635
|
+
var PENDING_BATCH_CLEANUP_DELAY_MS = 1e4;
|
|
631
636
|
var EVENT_NAME_TO_TYPE = {
|
|
632
637
|
auctionStart: TraceEventType.AUCTION_START,
|
|
633
638
|
auctionEnd: TraceEventType.AUCTION_END,
|
|
@@ -643,6 +648,11 @@ var EVENT_NAME_TO_TYPE = {
|
|
|
643
648
|
timeInView: TraceEventType.TIME_IN_VIEW,
|
|
644
649
|
viewable: TraceEventType.VIEWABLE
|
|
645
650
|
};
|
|
651
|
+
var IMMEDIATE_FLUSH_TYPES = /* @__PURE__ */ new Set([
|
|
652
|
+
TraceEventType.IMPRESSION,
|
|
653
|
+
TraceEventType.BID_WIN,
|
|
654
|
+
TraceEventType.CLICK
|
|
655
|
+
]);
|
|
646
656
|
var SESSION_KEY = "_bidkernel_session";
|
|
647
657
|
var SESSION_TS_KEY = "_bidkernel_session_ts";
|
|
648
658
|
var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
|
|
@@ -658,6 +668,22 @@ function generateUUID() {
|
|
|
658
668
|
return v.toString(16);
|
|
659
669
|
});
|
|
660
670
|
}
|
|
671
|
+
function bytesToBase64(bytes) {
|
|
672
|
+
let binary = "";
|
|
673
|
+
const chunk = 8192;
|
|
674
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
675
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
676
|
+
}
|
|
677
|
+
return btoa(binary);
|
|
678
|
+
}
|
|
679
|
+
function base64ToBytes(b64) {
|
|
680
|
+
const binary = atob(b64);
|
|
681
|
+
const bytes = new Uint8Array(binary.length);
|
|
682
|
+
for (let i = 0; i < binary.length; i++) {
|
|
683
|
+
bytes[i] = binary.charCodeAt(i);
|
|
684
|
+
}
|
|
685
|
+
return bytes;
|
|
686
|
+
}
|
|
661
687
|
function extendSession() {
|
|
662
688
|
const now = Date.now();
|
|
663
689
|
inMemorySessionTs = now;
|
|
@@ -805,7 +831,8 @@ function parseBidDimensions(bid) {
|
|
|
805
831
|
height: Number.isFinite(bid?.height) ? bid.height : 0
|
|
806
832
|
};
|
|
807
833
|
}
|
|
808
|
-
var BidkernelPrebidAnalytics = class {
|
|
834
|
+
var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
|
|
835
|
+
static activeInstances = /* @__PURE__ */ new Set();
|
|
809
836
|
config;
|
|
810
837
|
queue = [];
|
|
811
838
|
errorCount = 0;
|
|
@@ -821,6 +848,18 @@ var BidkernelPrebidAnalytics = class {
|
|
|
821
848
|
consecutiveSendFailures = 0;
|
|
822
849
|
nextSendAllowedAt = 0;
|
|
823
850
|
replayedEventCount = 0;
|
|
851
|
+
immediateFlushScheduled = false;
|
|
852
|
+
// localStorage keys of exit batches this instance persisted, so a page that
|
|
853
|
+
// survives its own pagehide/hidden (bfcache restore, tab re-focus) can
|
|
854
|
+
// remove them instead of leaving them for a duplicate resend.
|
|
855
|
+
persistedBatchKeys = [];
|
|
856
|
+
persistedCleanupTimer = null;
|
|
857
|
+
// Viewability & refresh tracking
|
|
858
|
+
intersectionObserver = null;
|
|
859
|
+
slotViewabilityRecords = /* @__PURE__ */ new Map();
|
|
860
|
+
elementToSlotId = /* @__PURE__ */ new Map();
|
|
861
|
+
slotRefreshIndices = /* @__PURE__ */ new Map();
|
|
862
|
+
pendingThresholdListeners = /* @__PURE__ */ new Map();
|
|
824
863
|
constructor(config) {
|
|
825
864
|
this.config = {
|
|
826
865
|
endpoint: config.endpoint || "",
|
|
@@ -834,19 +873,36 @@ var BidkernelPrebidAnalytics = class {
|
|
|
834
873
|
auctionEnabled: config.auctionEnabled ?? true,
|
|
835
874
|
warningsEnabled: config.warningsEnabled ?? true,
|
|
836
875
|
errorsEnabled: config.errorsEnabled ?? true,
|
|
876
|
+
viewabilityEnabled: config.viewabilityEnabled ?? true,
|
|
837
877
|
logLevel: config.logLevel || "INFO",
|
|
838
878
|
pbjsGlobalName: config.pbjsGlobalName || "pbjs",
|
|
839
879
|
attachPbjsListeners: config.attachPbjsListeners ?? true
|
|
840
880
|
};
|
|
841
|
-
this.boundFlushBeacon = () =>
|
|
881
|
+
this.boundFlushBeacon = () => {
|
|
882
|
+
this.flushAllSlotsTimeInView();
|
|
883
|
+
this.flushBeacon();
|
|
884
|
+
};
|
|
842
885
|
this.boundVisibilityChange = () => this.handleVisibilityChange();
|
|
843
886
|
}
|
|
844
887
|
enable() {
|
|
845
888
|
if (this.isEnabled) return;
|
|
846
889
|
this.isEnabled = true;
|
|
890
|
+
_BidkernelPrebidAnalytics.activeInstances.add(this);
|
|
847
891
|
if (!this.config.endpoint) {
|
|
848
892
|
this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
|
|
849
893
|
}
|
|
894
|
+
if (this.config.viewabilityEnabled) {
|
|
895
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
896
|
+
this.intersectionObserver = new IntersectionObserver(this.handleIntersection.bind(this), {
|
|
897
|
+
threshold: [0.5]
|
|
898
|
+
});
|
|
899
|
+
for (const record of this.slotViewabilityRecords.values()) {
|
|
900
|
+
if (record.element) {
|
|
901
|
+
this.intersectionObserver.observe(record.element);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
850
906
|
if (this.config.attachPbjsListeners) {
|
|
851
907
|
const win = typeof window !== "undefined" ? window : {};
|
|
852
908
|
const pbjs = win[this.config.pbjsGlobalName] || {};
|
|
@@ -922,15 +978,41 @@ var BidkernelPrebidAnalytics = class {
|
|
|
922
978
|
document.addEventListener("visibilitychange", this.boundVisibilityChange);
|
|
923
979
|
}
|
|
924
980
|
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
981
|
+
this.resendPersistedBatches();
|
|
925
982
|
}
|
|
926
983
|
}
|
|
927
984
|
disable() {
|
|
928
985
|
if (!this.isEnabled) return;
|
|
986
|
+
this.flushAllSlotsTimeInView();
|
|
929
987
|
this.isEnabled = false;
|
|
988
|
+
_BidkernelPrebidAnalytics.activeInstances.delete(this);
|
|
989
|
+
for (const record of this.slotViewabilityRecords.values()) {
|
|
990
|
+
if (record.dwellTimer) {
|
|
991
|
+
clearTimeout(record.dwellTimer);
|
|
992
|
+
record.dwellTimer = null;
|
|
993
|
+
record.dwellStartedAt = null;
|
|
994
|
+
}
|
|
995
|
+
for (const l of record.thresholdListeners) {
|
|
996
|
+
if (l.timer) {
|
|
997
|
+
clearTimeout(l.timer);
|
|
998
|
+
l.timer = null;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (this.intersectionObserver) {
|
|
1003
|
+
this.intersectionObserver.disconnect();
|
|
1004
|
+
this.intersectionObserver = null;
|
|
1005
|
+
}
|
|
1006
|
+
this.slotViewabilityRecords.clear();
|
|
1007
|
+
this.elementToSlotId.clear();
|
|
930
1008
|
if (this.flushTimer) {
|
|
931
1009
|
clearInterval(this.flushTimer);
|
|
932
1010
|
this.flushTimer = null;
|
|
933
1011
|
}
|
|
1012
|
+
if (this.persistedCleanupTimer) {
|
|
1013
|
+
clearTimeout(this.persistedCleanupTimer);
|
|
1014
|
+
this.persistedCleanupTimer = null;
|
|
1015
|
+
}
|
|
934
1016
|
if (typeof window !== "undefined") {
|
|
935
1017
|
window.removeEventListener("pagehide", this.boundFlushBeacon);
|
|
936
1018
|
if (typeof document !== "undefined") {
|
|
@@ -950,8 +1032,443 @@ var BidkernelPrebidAnalytics = class {
|
|
|
950
1032
|
this.flush();
|
|
951
1033
|
}
|
|
952
1034
|
handleVisibilityChange() {
|
|
953
|
-
if (typeof document
|
|
1035
|
+
if (typeof document === "undefined") return;
|
|
1036
|
+
if (document.visibilityState === "hidden") {
|
|
1037
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1038
|
+
for (const record of this.slotViewabilityRecords.values()) {
|
|
1039
|
+
if (record.inView && record.lastEnteredViewAt !== null) {
|
|
1040
|
+
record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
|
|
1041
|
+
record.lastEnteredViewAt = null;
|
|
1042
|
+
this.checkThresholdListeners(record);
|
|
1043
|
+
}
|
|
1044
|
+
if (record.dwellTimer) {
|
|
1045
|
+
clearTimeout(record.dwellTimer);
|
|
1046
|
+
record.dwellTimer = null;
|
|
1047
|
+
record.dwellStartedAt = null;
|
|
1048
|
+
}
|
|
1049
|
+
for (const l of record.thresholdListeners) {
|
|
1050
|
+
if (l.timer) {
|
|
1051
|
+
clearTimeout(l.timer);
|
|
1052
|
+
l.timer = null;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const durationMs = Math.round(record.accumulatedTimeInViewMs);
|
|
1056
|
+
if (durationMs > 0) {
|
|
1057
|
+
this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
|
|
1058
|
+
auctionId: record.auctionId,
|
|
1059
|
+
transactionId: record.transactionId,
|
|
1060
|
+
adUnitCode: record.adUnitCode,
|
|
1061
|
+
viewableDurationMs: durationMs,
|
|
1062
|
+
bid: record.bidPayload,
|
|
1063
|
+
metadata: {
|
|
1064
|
+
...record.metadata,
|
|
1065
|
+
refresh_index: String(record.refreshIndex)
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
954
1070
|
this.flushBeacon();
|
|
1071
|
+
} else if (document.visibilityState === "visible") {
|
|
1072
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1073
|
+
for (const record of this.slotViewabilityRecords.values()) {
|
|
1074
|
+
if (record.inView) {
|
|
1075
|
+
record.lastEnteredViewAt = now;
|
|
1076
|
+
if (!record.viewableFired && record.dwellTimer === null) {
|
|
1077
|
+
const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
|
|
1078
|
+
record.dwellStartedAt = now;
|
|
1079
|
+
record.dwellTimer = setTimeout(() => {
|
|
1080
|
+
this.handleDwellComplete(record);
|
|
1081
|
+
}, dwellTarget);
|
|
1082
|
+
}
|
|
1083
|
+
this.scheduleThresholdTimers(record);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
handleIntersection(entries) {
|
|
1089
|
+
for (const entry of entries) {
|
|
1090
|
+
let slotId = this.elementToSlotId.get(entry.target);
|
|
1091
|
+
if (!slotId) {
|
|
1092
|
+
for (const instance of _BidkernelPrebidAnalytics.activeInstances) {
|
|
1093
|
+
if (instance !== this && instance.elementToSlotId.has(entry.target)) {
|
|
1094
|
+
instance.handleIntersection([entry]);
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1101
|
+
if (!record) continue;
|
|
1102
|
+
const isViewableRatio = entry.isIntersecting && (entry.intersectionRatio === void 0 || entry.intersectionRatio >= 0.5);
|
|
1103
|
+
const isDocVisible = typeof document === "undefined" || document.visibilityState === "visible";
|
|
1104
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1105
|
+
if (isViewableRatio) {
|
|
1106
|
+
if (!record.inView) {
|
|
1107
|
+
record.inView = true;
|
|
1108
|
+
if (isDocVisible) {
|
|
1109
|
+
record.lastEnteredViewAt = now;
|
|
1110
|
+
if (!record.viewableFired && record.dwellTimer === null) {
|
|
1111
|
+
const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
|
|
1112
|
+
record.dwellStartedAt = now;
|
|
1113
|
+
record.dwellTimer = setTimeout(() => {
|
|
1114
|
+
this.handleDwellComplete(record);
|
|
1115
|
+
}, dwellTarget);
|
|
1116
|
+
}
|
|
1117
|
+
this.scheduleThresholdTimers(record);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
if (record.inView) {
|
|
1122
|
+
if (record.lastEnteredViewAt !== null) {
|
|
1123
|
+
record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
|
|
1124
|
+
record.lastEnteredViewAt = null;
|
|
1125
|
+
}
|
|
1126
|
+
record.inView = false;
|
|
1127
|
+
this.checkThresholdListeners(record);
|
|
1128
|
+
}
|
|
1129
|
+
if (record.dwellTimer !== null) {
|
|
1130
|
+
clearTimeout(record.dwellTimer);
|
|
1131
|
+
record.dwellTimer = null;
|
|
1132
|
+
record.dwellStartedAt = null;
|
|
1133
|
+
}
|
|
1134
|
+
for (const l of record.thresholdListeners) {
|
|
1135
|
+
if (l.timer) {
|
|
1136
|
+
clearTimeout(l.timer);
|
|
1137
|
+
l.timer = null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
handleDwellComplete(record) {
|
|
1144
|
+
record.dwellTimer = null;
|
|
1145
|
+
record.dwellStartedAt = null;
|
|
1146
|
+
if (!record.inView) return;
|
|
1147
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
1148
|
+
if (!record.viewableFired) {
|
|
1149
|
+
record.viewableFired = true;
|
|
1150
|
+
this.enqueue(TraceEventType.VIEWABLE, "viewable", {
|
|
1151
|
+
auctionId: record.auctionId,
|
|
1152
|
+
transactionId: record.transactionId,
|
|
1153
|
+
adUnitCode: record.adUnitCode,
|
|
1154
|
+
bid: record.bidPayload,
|
|
1155
|
+
metadata: {
|
|
1156
|
+
...record.metadata,
|
|
1157
|
+
refresh_index: String(record.refreshIndex)
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
scheduleThresholdTimers(record) {
|
|
1163
|
+
if (!record.inView || typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const currentTotal = this.getCurrentTimeInView(record);
|
|
1167
|
+
for (const listener of record.thresholdListeners) {
|
|
1168
|
+
if (listener.fired) continue;
|
|
1169
|
+
if (currentTotal >= listener.thresholdMs) {
|
|
1170
|
+
listener.fired = true;
|
|
1171
|
+
if (listener.timer) {
|
|
1172
|
+
clearTimeout(listener.timer);
|
|
1173
|
+
listener.timer = null;
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
listener.callback(record.slotId, currentTotal);
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
this.log("ERROR", "Error in threshold listener callback", e);
|
|
1179
|
+
}
|
|
1180
|
+
} else {
|
|
1181
|
+
if (listener.timer) {
|
|
1182
|
+
clearTimeout(listener.timer);
|
|
1183
|
+
}
|
|
1184
|
+
const remaining = listener.thresholdMs - currentTotal;
|
|
1185
|
+
listener.timer = setTimeout(() => {
|
|
1186
|
+
listener.timer = null;
|
|
1187
|
+
if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
|
|
1188
|
+
const nowTotal = this.getCurrentTimeInView(record);
|
|
1189
|
+
if (nowTotal >= listener.thresholdMs) {
|
|
1190
|
+
listener.fired = true;
|
|
1191
|
+
try {
|
|
1192
|
+
listener.callback(record.slotId, nowTotal);
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
this.log("ERROR", "Error in threshold listener callback", e);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}, remaining);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
checkThresholdListeners(record) {
|
|
1203
|
+
const currentTotal = this.getCurrentTimeInView(record);
|
|
1204
|
+
for (const listener of record.thresholdListeners) {
|
|
1205
|
+
if (!listener.fired && currentTotal >= listener.thresholdMs) {
|
|
1206
|
+
listener.fired = true;
|
|
1207
|
+
if (listener.timer) {
|
|
1208
|
+
clearTimeout(listener.timer);
|
|
1209
|
+
listener.timer = null;
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
listener.callback(record.slotId, currentTotal);
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
this.log("ERROR", "Error in threshold listener callback", e);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
getCurrentTimeInView(record) {
|
|
1220
|
+
let total = record.accumulatedTimeInViewMs;
|
|
1221
|
+
if (record.inView && record.lastEnteredViewAt !== null) {
|
|
1222
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1223
|
+
total += Math.max(0, now - record.lastEnteredViewAt);
|
|
1224
|
+
}
|
|
1225
|
+
return total;
|
|
1226
|
+
}
|
|
1227
|
+
flushSlotTimeInView(slotId) {
|
|
1228
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1229
|
+
if (!record) return;
|
|
1230
|
+
if (record.inView && record.lastEnteredViewAt !== null) {
|
|
1231
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1232
|
+
record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
|
|
1233
|
+
record.lastEnteredViewAt = null;
|
|
1234
|
+
}
|
|
1235
|
+
const durationMs = Math.round(record.accumulatedTimeInViewMs);
|
|
1236
|
+
if (durationMs > 0) {
|
|
1237
|
+
this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
|
|
1238
|
+
auctionId: record.auctionId,
|
|
1239
|
+
transactionId: record.transactionId,
|
|
1240
|
+
adUnitCode: record.adUnitCode,
|
|
1241
|
+
viewableDurationMs: durationMs,
|
|
1242
|
+
bid: record.bidPayload,
|
|
1243
|
+
metadata: {
|
|
1244
|
+
...record.metadata,
|
|
1245
|
+
refresh_index: String(record.refreshIndex)
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
record.accumulatedTimeInViewMs = 0;
|
|
1250
|
+
}
|
|
1251
|
+
flushAllSlotsTimeInView() {
|
|
1252
|
+
for (const slotId of Array.from(this.slotViewabilityRecords.keys())) {
|
|
1253
|
+
this.flushSlotTimeInView(slotId);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
observeSlot(element, slotId, options) {
|
|
1257
|
+
if (!this.config.viewabilityEnabled) return;
|
|
1258
|
+
let el = null;
|
|
1259
|
+
if (typeof element === "string") {
|
|
1260
|
+
if (typeof document !== "undefined") {
|
|
1261
|
+
try {
|
|
1262
|
+
el = document.querySelector(element) || document.getElementById(element);
|
|
1263
|
+
} catch {
|
|
1264
|
+
el = document.getElementById(element);
|
|
1265
|
+
}
|
|
1266
|
+
if (!el) {
|
|
1267
|
+
el = document.getElementById(element);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
} else {
|
|
1271
|
+
el = element;
|
|
1272
|
+
}
|
|
1273
|
+
const resolvedSlotId = slotId || options?.adUnitCode || (el ? el.id : "") || generateUUID();
|
|
1274
|
+
const existing = this.slotViewabilityRecords.get(resolvedSlotId);
|
|
1275
|
+
if (existing) {
|
|
1276
|
+
const isNewRefreshCycle = options?.refreshIndex === void 0 || options.refreshIndex > existing.refreshIndex;
|
|
1277
|
+
if (isNewRefreshCycle) {
|
|
1278
|
+
this.flushSlotTimeInView(resolvedSlotId);
|
|
1279
|
+
const nextRefreshIndex = options?.refreshIndex !== void 0 ? options.refreshIndex : (this.slotRefreshIndices.get(resolvedSlotId) ?? existing.refreshIndex) + 1;
|
|
1280
|
+
this.slotRefreshIndices.set(resolvedSlotId, nextRefreshIndex);
|
|
1281
|
+
if (options?.emitRefreshEvent !== false) {
|
|
1282
|
+
this.enqueue(TraceEventType.REFRESH, "refresh", {
|
|
1283
|
+
auctionId: options?.auctionId || existing.auctionId,
|
|
1284
|
+
transactionId: options?.transactionId || existing.transactionId,
|
|
1285
|
+
adUnitCode: resolvedSlotId,
|
|
1286
|
+
bid: options?.bid ?? existing.bidPayload,
|
|
1287
|
+
metadata: {
|
|
1288
|
+
...options?.metadata,
|
|
1289
|
+
refresh_index: String(nextRefreshIndex)
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
if (existing.dwellTimer) {
|
|
1294
|
+
clearTimeout(existing.dwellTimer);
|
|
1295
|
+
existing.dwellTimer = null;
|
|
1296
|
+
existing.dwellStartedAt = null;
|
|
1297
|
+
}
|
|
1298
|
+
for (const l of existing.thresholdListeners) {
|
|
1299
|
+
l.fired = false;
|
|
1300
|
+
if (l.timer) {
|
|
1301
|
+
clearTimeout(l.timer);
|
|
1302
|
+
l.timer = null;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
existing.viewableFired = false;
|
|
1306
|
+
existing.accumulatedTimeInViewMs = 0;
|
|
1307
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1308
|
+
existing.lastEnteredViewAt = existing.inView ? now : null;
|
|
1309
|
+
existing.refreshIndex = nextRefreshIndex;
|
|
1310
|
+
existing.auctionId = options?.auctionId || existing.auctionId;
|
|
1311
|
+
existing.transactionId = options?.transactionId || existing.transactionId;
|
|
1312
|
+
existing.mediaType = options?.mediaType || existing.mediaType || "banner";
|
|
1313
|
+
existing.bidPayload = options?.bid !== void 0 ? options.bid : existing.bidPayload;
|
|
1314
|
+
existing.metadata = options?.metadata ?? existing.metadata;
|
|
1315
|
+
if (existing.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
|
|
1316
|
+
const dwellTarget = existing.mediaType === "video" ? 2e3 : 1e3;
|
|
1317
|
+
existing.dwellStartedAt = now;
|
|
1318
|
+
existing.dwellTimer = setTimeout(() => {
|
|
1319
|
+
this.handleDwellComplete(existing);
|
|
1320
|
+
}, dwellTarget);
|
|
1321
|
+
this.scheduleThresholdTimers(existing);
|
|
1322
|
+
}
|
|
1323
|
+
} else {
|
|
1324
|
+
existing.auctionId = options?.auctionId || existing.auctionId;
|
|
1325
|
+
existing.transactionId = options?.transactionId || existing.transactionId;
|
|
1326
|
+
existing.mediaType = options?.mediaType || existing.mediaType;
|
|
1327
|
+
if (options?.bid !== void 0) existing.bidPayload = options.bid;
|
|
1328
|
+
if (options?.metadata !== void 0) existing.metadata = options.metadata;
|
|
1329
|
+
}
|
|
1330
|
+
if (el) {
|
|
1331
|
+
if (existing.element && existing.element !== el && this.intersectionObserver) {
|
|
1332
|
+
this.intersectionObserver.unobserve(existing.element);
|
|
1333
|
+
this.elementToSlotId.delete(existing.element);
|
|
1334
|
+
}
|
|
1335
|
+
existing.element = el;
|
|
1336
|
+
this.elementToSlotId.set(el, resolvedSlotId);
|
|
1337
|
+
this.intersectionObserver?.observe(el);
|
|
1338
|
+
}
|
|
1339
|
+
} else {
|
|
1340
|
+
const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
|
|
1341
|
+
this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
|
|
1342
|
+
const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
|
|
1343
|
+
this.pendingThresholdListeners.delete(resolvedSlotId);
|
|
1344
|
+
const record = {
|
|
1345
|
+
slotId: resolvedSlotId,
|
|
1346
|
+
element: el,
|
|
1347
|
+
adUnitCode: options?.adUnitCode || resolvedSlotId,
|
|
1348
|
+
auctionId: options?.auctionId || "",
|
|
1349
|
+
transactionId: options?.transactionId || "",
|
|
1350
|
+
mediaType: options?.mediaType || "banner",
|
|
1351
|
+
bidPayload: options?.bid,
|
|
1352
|
+
metadata: options?.metadata,
|
|
1353
|
+
refreshIndex: initialRefreshIndex,
|
|
1354
|
+
inView: false,
|
|
1355
|
+
lastEnteredViewAt: null,
|
|
1356
|
+
accumulatedTimeInViewMs: 0,
|
|
1357
|
+
viewableFired: false,
|
|
1358
|
+
dwellTimer: null,
|
|
1359
|
+
dwellStartedAt: null,
|
|
1360
|
+
thresholdListeners: listeners
|
|
1361
|
+
};
|
|
1362
|
+
this.slotViewabilityRecords.set(resolvedSlotId, record);
|
|
1363
|
+
if (el) {
|
|
1364
|
+
this.elementToSlotId.set(el, resolvedSlotId);
|
|
1365
|
+
this.intersectionObserver?.observe(el);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
unobserveSlot(slotId) {
|
|
1370
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1371
|
+
if (!record) return;
|
|
1372
|
+
this.flushSlotTimeInView(slotId);
|
|
1373
|
+
if (record.dwellTimer) {
|
|
1374
|
+
clearTimeout(record.dwellTimer);
|
|
1375
|
+
record.dwellTimer = null;
|
|
1376
|
+
record.dwellStartedAt = null;
|
|
1377
|
+
}
|
|
1378
|
+
for (const l of record.thresholdListeners) {
|
|
1379
|
+
if (l.timer) {
|
|
1380
|
+
clearTimeout(l.timer);
|
|
1381
|
+
l.timer = null;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
record.thresholdListeners = /* @__PURE__ */ new Set();
|
|
1385
|
+
if (record.element && this.intersectionObserver) {
|
|
1386
|
+
this.intersectionObserver.unobserve(record.element);
|
|
1387
|
+
this.elementToSlotId.delete(record.element);
|
|
1388
|
+
record.element = null;
|
|
1389
|
+
}
|
|
1390
|
+
record.inView = false;
|
|
1391
|
+
record.lastEnteredViewAt = null;
|
|
1392
|
+
}
|
|
1393
|
+
destroySlot(slotId) {
|
|
1394
|
+
this.unobserveSlot(slotId);
|
|
1395
|
+
this.slotViewabilityRecords.delete(slotId);
|
|
1396
|
+
this.slotRefreshIndices.delete(slotId);
|
|
1397
|
+
this.pendingThresholdListeners.delete(slotId);
|
|
1398
|
+
}
|
|
1399
|
+
onTimeInViewThreshold(slotId, thresholdMs, callback) {
|
|
1400
|
+
const listener = {
|
|
1401
|
+
thresholdMs,
|
|
1402
|
+
callback,
|
|
1403
|
+
timer: null,
|
|
1404
|
+
fired: false
|
|
1405
|
+
};
|
|
1406
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1407
|
+
if (record) {
|
|
1408
|
+
record.thresholdListeners.add(listener);
|
|
1409
|
+
const currentTotal = this.getCurrentTimeInView(record);
|
|
1410
|
+
if (currentTotal >= thresholdMs) {
|
|
1411
|
+
listener.fired = true;
|
|
1412
|
+
try {
|
|
1413
|
+
callback(slotId, currentTotal);
|
|
1414
|
+
} catch (e) {
|
|
1415
|
+
this.log("ERROR", "Error in threshold callback", e);
|
|
1416
|
+
}
|
|
1417
|
+
} else if (record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
|
|
1418
|
+
const remaining = thresholdMs - currentTotal;
|
|
1419
|
+
listener.timer = setTimeout(() => {
|
|
1420
|
+
listener.timer = null;
|
|
1421
|
+
if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
|
|
1422
|
+
const nowTotal = this.getCurrentTimeInView(record);
|
|
1423
|
+
if (nowTotal >= thresholdMs) {
|
|
1424
|
+
listener.fired = true;
|
|
1425
|
+
try {
|
|
1426
|
+
callback(slotId, nowTotal);
|
|
1427
|
+
} catch (e) {
|
|
1428
|
+
this.log("ERROR", "Error in threshold callback", e);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}, remaining);
|
|
1433
|
+
}
|
|
1434
|
+
} else {
|
|
1435
|
+
if (!this.pendingThresholdListeners.has(slotId)) {
|
|
1436
|
+
this.pendingThresholdListeners.set(slotId, /* @__PURE__ */ new Set());
|
|
1437
|
+
}
|
|
1438
|
+
this.pendingThresholdListeners.get(slotId).add(listener);
|
|
1439
|
+
}
|
|
1440
|
+
return () => {
|
|
1441
|
+
if (listener.timer) {
|
|
1442
|
+
clearTimeout(listener.timer);
|
|
1443
|
+
listener.timer = null;
|
|
1444
|
+
}
|
|
1445
|
+
if (record) {
|
|
1446
|
+
record.thresholdListeners.delete(listener);
|
|
1447
|
+
}
|
|
1448
|
+
const pending = this.pendingThresholdListeners.get(slotId);
|
|
1449
|
+
if (pending) {
|
|
1450
|
+
pending.delete(listener);
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
getViewabilityState(slotId) {
|
|
1455
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1456
|
+
if (!record) return void 0;
|
|
1457
|
+
const timeInViewMs = Math.round(this.getCurrentTimeInView(record));
|
|
1458
|
+
return {
|
|
1459
|
+
inView: record.inView,
|
|
1460
|
+
viewable: record.viewableFired,
|
|
1461
|
+
timeInViewMs,
|
|
1462
|
+
refreshIndex: record.refreshIndex
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
getRefreshIndex(slotId) {
|
|
1466
|
+
return this.slotRefreshIndices.get(slotId) ?? 0;
|
|
1467
|
+
}
|
|
1468
|
+
triggerSlotRefresh(slotId, options) {
|
|
1469
|
+
const record = this.slotViewabilityRecords.get(slotId);
|
|
1470
|
+
if (record && record.element) {
|
|
1471
|
+
this.observeSlot(record.element, slotId, options);
|
|
955
1472
|
}
|
|
956
1473
|
}
|
|
957
1474
|
trackRawEvent(level, eventName, data) {
|
|
@@ -966,6 +1483,8 @@ var BidkernelPrebidAnalytics = class {
|
|
|
966
1483
|
}
|
|
967
1484
|
navigate(pageviewId, pageUrl) {
|
|
968
1485
|
this.disable();
|
|
1486
|
+
this.slotRefreshIndices.clear();
|
|
1487
|
+
this.pendingThresholdListeners.clear();
|
|
969
1488
|
this.config.pageviewId = pageviewId || generateUUID();
|
|
970
1489
|
if (pageUrl) {
|
|
971
1490
|
try {
|
|
@@ -1140,23 +1659,81 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1140
1659
|
handleAdRenderSucceeded(data) {
|
|
1141
1660
|
if (this.isDuplicate("adRenderSucceeded", data)) return;
|
|
1142
1661
|
this.log("DEBUG", "adRenderSucceeded", data);
|
|
1143
|
-
const bid = data.bid || {};
|
|
1662
|
+
const bid = data.bid || data || {};
|
|
1663
|
+
const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
|
|
1664
|
+
const auctionId = bid.auctionId || data.auctionId || "";
|
|
1665
|
+
const transactionId = bid.transactionId || data.transactionId || "";
|
|
1666
|
+
const mediaType = bid.mediaType || "banner";
|
|
1667
|
+
const bidTrace = {
|
|
1668
|
+
bidder: bid.bidderCode || bid.bidder || "",
|
|
1669
|
+
cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
|
|
1670
|
+
currency: bid.originalCurrency ?? bid.currency ?? "USD",
|
|
1671
|
+
...parseBidDimensions(bid),
|
|
1672
|
+
dealId: bid.dealId || "",
|
|
1673
|
+
mediaType,
|
|
1674
|
+
latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
|
|
1675
|
+
advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
|
|
1676
|
+
creativeId: bid.creativeId || ""
|
|
1677
|
+
};
|
|
1678
|
+
const isRefresh = adUnitCode && (this.slotViewabilityRecords.has(adUnitCode) || this.slotRefreshIndices.has(adUnitCode) && this.slotRefreshIndices.get(adUnitCode) > 0);
|
|
1679
|
+
if (isRefresh) {
|
|
1680
|
+
this.flushSlotTimeInView(adUnitCode);
|
|
1681
|
+
const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
|
|
1682
|
+
this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
|
|
1683
|
+
this.enqueue(TraceEventType.REFRESH, "refresh", {
|
|
1684
|
+
auctionId,
|
|
1685
|
+
transactionId,
|
|
1686
|
+
adUnitCode,
|
|
1687
|
+
bid: bidTrace,
|
|
1688
|
+
metadata: { refresh_index: String(nextRefreshIndex) }
|
|
1689
|
+
});
|
|
1690
|
+
} else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
|
|
1691
|
+
this.slotRefreshIndices.set(adUnitCode, 0);
|
|
1692
|
+
}
|
|
1693
|
+
const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
|
|
1144
1694
|
this.enqueue(TraceEventType.IMPRESSION, "impression", {
|
|
1145
|
-
auctionId
|
|
1146
|
-
transactionId
|
|
1147
|
-
adUnitCode
|
|
1148
|
-
bid:
|
|
1149
|
-
|
|
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
|
-
}
|
|
1695
|
+
auctionId,
|
|
1696
|
+
transactionId,
|
|
1697
|
+
adUnitCode,
|
|
1698
|
+
bid: bidTrace,
|
|
1699
|
+
metadata: { refresh_index: String(currentRefreshIndex) }
|
|
1159
1700
|
});
|
|
1701
|
+
if (this.config.viewabilityEnabled && typeof document !== "undefined") {
|
|
1702
|
+
let el = null;
|
|
1703
|
+
const targetId = adUnitCode || data.adId || bid.adId;
|
|
1704
|
+
if (targetId) {
|
|
1705
|
+
el = document.getElementById(targetId);
|
|
1706
|
+
}
|
|
1707
|
+
if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
|
|
1708
|
+
try {
|
|
1709
|
+
const slots = window.googletag.pubads().getSlots();
|
|
1710
|
+
for (const slot of slots) {
|
|
1711
|
+
if (slot.getAdUnitPath?.() === adUnitCode || slot.getSlotElementId?.() === adUnitCode) {
|
|
1712
|
+
el = document.getElementById(slot.getSlotElementId());
|
|
1713
|
+
if (el) break;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
} catch {
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
if (!el && targetId) {
|
|
1720
|
+
try {
|
|
1721
|
+
el = document.querySelector(`[data-ad-slot="${targetId}"]`) || document.querySelector(`#${targetId}`);
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (el) {
|
|
1726
|
+
this.observeSlot(el, adUnitCode || targetId, {
|
|
1727
|
+
adUnitCode,
|
|
1728
|
+
auctionId,
|
|
1729
|
+
transactionId,
|
|
1730
|
+
mediaType,
|
|
1731
|
+
bid: bidTrace,
|
|
1732
|
+
refreshIndex: currentRefreshIndex,
|
|
1733
|
+
emitRefreshEvent: false
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1160
1737
|
}
|
|
1161
1738
|
isDuplicate(eventName, data) {
|
|
1162
1739
|
return this.deduper.isDuplicate(eventName, data);
|
|
@@ -1202,6 +1779,9 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1202
1779
|
if (data.gpid) {
|
|
1203
1780
|
metadata.gpid = String(data.gpid);
|
|
1204
1781
|
}
|
|
1782
|
+
if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
|
|
1783
|
+
metadata.refresh_index = String(data.refreshIndex);
|
|
1784
|
+
}
|
|
1205
1785
|
if (Object.keys(metadata).length > 0) {
|
|
1206
1786
|
protoEvent.metadata = metadata;
|
|
1207
1787
|
}
|
|
@@ -1211,6 +1791,12 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1211
1791
|
}
|
|
1212
1792
|
if (this.queue.length >= BATCH_SIZE) {
|
|
1213
1793
|
this.flush();
|
|
1794
|
+
} else if (IMMEDIATE_FLUSH_TYPES.has(type) && !this.immediateFlushScheduled) {
|
|
1795
|
+
this.immediateFlushScheduled = true;
|
|
1796
|
+
setTimeout(() => {
|
|
1797
|
+
this.immediateFlushScheduled = false;
|
|
1798
|
+
this.flush();
|
|
1799
|
+
}, 0);
|
|
1214
1800
|
}
|
|
1215
1801
|
}
|
|
1216
1802
|
shouldSample(type, level) {
|
|
@@ -1267,10 +1853,11 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1267
1853
|
events
|
|
1268
1854
|
};
|
|
1269
1855
|
}
|
|
1270
|
-
sendFetch(payload, useKeepalive = false) {
|
|
1856
|
+
sendFetch(payload, useKeepalive = false, onDelivered) {
|
|
1271
1857
|
const fetchOpts = {
|
|
1272
1858
|
method: "POST",
|
|
1273
|
-
|
|
1859
|
+
// Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
|
|
1860
|
+
headers: { "Content-Type": SAFE_CONTENT_TYPE },
|
|
1274
1861
|
body: payload.encoded
|
|
1275
1862
|
};
|
|
1276
1863
|
if (useKeepalive) {
|
|
@@ -1279,7 +1866,7 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1279
1866
|
fetch(payload.url, fetchOpts).then((res) => {
|
|
1280
1867
|
if (!res.ok) {
|
|
1281
1868
|
this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
|
|
1282
|
-
if (res.status >= 400 && res.status < 500) {
|
|
1869
|
+
if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 408) {
|
|
1283
1870
|
this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
|
|
1284
1871
|
return;
|
|
1285
1872
|
}
|
|
@@ -1287,11 +1874,19 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1287
1874
|
} else {
|
|
1288
1875
|
this.consecutiveSendFailures = 0;
|
|
1289
1876
|
this.nextSendAllowedAt = 0;
|
|
1877
|
+
if (onDelivered) onDelivered();
|
|
1290
1878
|
}
|
|
1291
1879
|
}).catch((err) => {
|
|
1292
1880
|
this.log("ERROR", "Failed to send batch", err);
|
|
1293
1881
|
this.handleSendFailure(payload.events);
|
|
1294
1882
|
});
|
|
1883
|
+
const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
|
|
1884
|
+
if (proc && typeof proc._tickCallback === "function") {
|
|
1885
|
+
try {
|
|
1886
|
+
proc._tickCallback();
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1295
1890
|
}
|
|
1296
1891
|
handleSendFailure(events) {
|
|
1297
1892
|
this.consecutiveSendFailures++;
|
|
@@ -1324,19 +1919,99 @@ var BidkernelPrebidAnalytics = class {
|
|
|
1324
1919
|
let sent = false;
|
|
1325
1920
|
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
1326
1921
|
try {
|
|
1327
|
-
const blob = new Blob([payload.encoded], {
|
|
1328
|
-
type: "application/x-protobuf"
|
|
1329
|
-
});
|
|
1922
|
+
const blob = new Blob([payload.encoded], { type: SAFE_CONTENT_TYPE });
|
|
1330
1923
|
sent = navigator.sendBeacon(payload.url, blob);
|
|
1331
1924
|
} catch {
|
|
1332
1925
|
sent = false;
|
|
1333
1926
|
}
|
|
1334
1927
|
}
|
|
1335
1928
|
if (!sent) {
|
|
1336
|
-
this.
|
|
1929
|
+
const persistKey = this.persistBatch(payload.encoded);
|
|
1930
|
+
this.sendFetch(payload, true, () => this.removePersistedBatch(persistKey));
|
|
1337
1931
|
}
|
|
1338
1932
|
payload = this.drainBatch();
|
|
1339
1933
|
}
|
|
1934
|
+
this.schedulePersistedCleanup();
|
|
1935
|
+
}
|
|
1936
|
+
// --- Exit-batch persistence -----------------------------------------------
|
|
1937
|
+
persistBatch(encoded) {
|
|
1938
|
+
try {
|
|
1939
|
+
if (typeof localStorage === "undefined") return null;
|
|
1940
|
+
let pendingCount = 0;
|
|
1941
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
1942
|
+
const k = localStorage.key(i);
|
|
1943
|
+
if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
|
|
1944
|
+
}
|
|
1945
|
+
if (pendingCount >= MAX_PENDING_BATCHES) return null;
|
|
1946
|
+
const key = `${PENDING_BATCH_KEY_PREFIX}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
|
|
1947
|
+
localStorage.setItem(key, bytesToBase64(encoded));
|
|
1948
|
+
this.persistedBatchKeys.push(key);
|
|
1949
|
+
return key;
|
|
1950
|
+
} catch {
|
|
1951
|
+
return null;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
removePersistedBatch(key) {
|
|
1955
|
+
if (!key) return;
|
|
1956
|
+
try {
|
|
1957
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(key);
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
1960
|
+
const idx = this.persistedBatchKeys.indexOf(key);
|
|
1961
|
+
if (idx !== -1) this.persistedBatchKeys.splice(idx, 1);
|
|
1962
|
+
}
|
|
1963
|
+
// A page that is still running PENDING_BATCH_CLEANUP_DELAY_MS after an exit
|
|
1964
|
+
// flush was never torn down, so its beacons have long since gone out; drop
|
|
1965
|
+
// the persisted copies rather than letting a later pageview resend them.
|
|
1966
|
+
schedulePersistedCleanup() {
|
|
1967
|
+
if (this.persistedBatchKeys.length === 0) return;
|
|
1968
|
+
if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
|
|
1969
|
+
this.persistedCleanupTimer = setTimeout(() => {
|
|
1970
|
+
this.persistedCleanupTimer = null;
|
|
1971
|
+
for (const key of [...this.persistedBatchKeys]) {
|
|
1972
|
+
this.removePersistedBatch(key);
|
|
1973
|
+
}
|
|
1974
|
+
}, PENDING_BATCH_CLEANUP_DELAY_MS);
|
|
1975
|
+
}
|
|
1976
|
+
// Resend batches a previous pageview persisted at exit but could not
|
|
1977
|
+
// confirm. Keys are claimed (removed) before sending so concurrent tabs
|
|
1978
|
+
// don't double-send; the batch bytes carry their original pageview, session,
|
|
1979
|
+
// and event timestamps, so late rows attribute correctly.
|
|
1980
|
+
resendPersistedBatches() {
|
|
1981
|
+
try {
|
|
1982
|
+
if (typeof localStorage === "undefined" || !this.config.endpoint) return;
|
|
1983
|
+
const keys = [];
|
|
1984
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
1985
|
+
const k = localStorage.key(i);
|
|
1986
|
+
if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) keys.push(k);
|
|
1987
|
+
}
|
|
1988
|
+
for (const key of keys) {
|
|
1989
|
+
let value = null;
|
|
1990
|
+
try {
|
|
1991
|
+
value = localStorage.getItem(key);
|
|
1992
|
+
localStorage.removeItem(key);
|
|
1993
|
+
} catch {
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
if (!value) continue;
|
|
1997
|
+
const ts = parseInt(key.slice(PENDING_BATCH_KEY_PREFIX.length), 10);
|
|
1998
|
+
if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
|
|
1999
|
+
let encoded;
|
|
2000
|
+
try {
|
|
2001
|
+
encoded = base64ToBytes(value);
|
|
2002
|
+
} catch {
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
if (encoded.byteLength === 0) continue;
|
|
2006
|
+
this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
|
|
2007
|
+
this.sendFetch({
|
|
2008
|
+
url: `${this.config.endpoint}/${this.config.propertyId}`,
|
|
2009
|
+
encoded,
|
|
2010
|
+
events: []
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
1340
2015
|
}
|
|
1341
2016
|
log(level, msg, ...args) {
|
|
1342
2017
|
const levels = { DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 };
|