@bidkernel/analytics 0.12.0 → 0.17.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
@@ -511,28 +511,6 @@ var TraceEventBatch = {
511
511
  reader.skip(tag & 7);
512
512
  }
513
513
  return message;
514
- },
515
- create(base) {
516
- return TraceEventBatch.fromPartial(base ?? {});
517
- },
518
- fromPartial(object) {
519
- const message = createBaseTraceEventBatch();
520
- message.propertyId = object.propertyId ?? "";
521
- message.pageviewId = object.pageviewId ?? "";
522
- message.sessionId = object.sessionId ?? "";
523
- message.pageUrl = object.pageUrl ?? "";
524
- message.country = object.country ?? "";
525
- message.region = object.region ?? "";
526
- message.deviceType = object.deviceType ?? "";
527
- message.userId = object.userId ?? "";
528
- message.domain = object.domain ?? "";
529
- message.events = object.events?.map((e) => TraceEvent.fromPartial(e)) || [];
530
- message.browser = object.browser ?? "";
531
- message.deviceManufacturer = object.deviceManufacturer ?? "";
532
- message.deviceModel = object.deviceModel ?? "";
533
- message.city = object.city ?? "";
534
- message.postalCode = object.postalCode ?? "";
535
- return message;
536
514
  }
537
515
  };
538
516
  function createBaseTraceEvent() {
@@ -697,33 +675,6 @@ var TraceEvent = {
697
675
  reader.skip(tag & 7);
698
676
  }
699
677
  return message;
700
- },
701
- create(base) {
702
- return TraceEvent.fromPartial(base ?? {});
703
- },
704
- fromPartial(object) {
705
- const message = createBaseTraceEvent();
706
- message.timestampMs = object.timestampMs ?? 0;
707
- message.type = object.type ?? 0;
708
- message.eventName = object.eventName ?? "";
709
- message.auctionId = object.auctionId ?? "";
710
- message.transactionId = object.transactionId ?? "";
711
- message.adUnitCode = object.adUnitCode ?? "";
712
- message.bid = object.bid !== void 0 && object.bid !== null ? BidTrace.fromPartial(object.bid) : void 0;
713
- message.namespace = object.namespace ?? "";
714
- message.eventId = object.eventId ?? "";
715
- message.metadata = globalThis.Object.entries(object.metadata ?? {}).reduce(
716
- (acc, [key, value]) => {
717
- if (value !== void 0) {
718
- acc[key] = globalThis.String(value);
719
- }
720
- return acc;
721
- },
722
- {}
723
- );
724
- message.viewableDurationMs = object.viewableDurationMs ?? void 0;
725
- message.bidderOutcomes = object.bidderOutcomes?.map((e) => BidderOutcomeEntry.fromPartial(e)) || [];
726
- return message;
727
678
  }
728
679
  };
729
680
  function createBaseTraceEvent_MetadataEntry() {
@@ -767,15 +718,6 @@ var TraceEvent_MetadataEntry = {
767
718
  reader.skip(tag & 7);
768
719
  }
769
720
  return message;
770
- },
771
- create(base) {
772
- return TraceEvent_MetadataEntry.fromPartial(base ?? {});
773
- },
774
- fromPartial(object) {
775
- const message = createBaseTraceEvent_MetadataEntry();
776
- message.key = object.key ?? "";
777
- message.value = object.value ?? "";
778
- return message;
779
721
  }
780
722
  };
781
723
  function createBaseBidderOutcomeEntry() {
@@ -849,18 +791,6 @@ var BidderOutcomeEntry = {
849
791
  reader.skip(tag & 7);
850
792
  }
851
793
  return message;
852
- },
853
- create(base) {
854
- return BidderOutcomeEntry.fromPartial(base ?? {});
855
- },
856
- fromPartial(object) {
857
- const message = createBaseBidderOutcomeEntry();
858
- message.bidder = object.bidder ?? "";
859
- message.adUnitCode = object.adUnitCode ?? "";
860
- message.mediaType = object.mediaType ?? "";
861
- message.outcome = object.outcome ?? 0;
862
- message.latencyMs = object.latencyMs ?? 0;
863
- return message;
864
794
  }
865
795
  };
866
796
  function createBaseBidTrace() {
@@ -995,23 +925,6 @@ var BidTrace = {
995
925
  reader.skip(tag & 7);
996
926
  }
997
927
  return message;
998
- },
999
- create(base) {
1000
- return BidTrace.fromPartial(base ?? {});
1001
- },
1002
- fromPartial(object) {
1003
- const message = createBaseBidTrace();
1004
- message.bidder = object.bidder ?? "";
1005
- message.cpm = object.cpm ?? 0;
1006
- message.currency = object.currency ?? "";
1007
- message.width = object.width ?? 0;
1008
- message.height = object.height ?? 0;
1009
- message.dealId = object.dealId ?? "";
1010
- message.mediaType = object.mediaType ?? "";
1011
- message.latencyMs = object.latencyMs ?? 0;
1012
- message.advertiserDomain = object.advertiserDomain ?? "";
1013
- message.creativeId = object.creativeId ?? "";
1014
- return message;
1015
928
  }
1016
929
  };
1017
930
 
@@ -1052,11 +965,13 @@ var CACHED_BID_TTL_MS = 30 * 60 * 1e3;
1052
965
  var MAX_CACHED_BID_KEYS = 200;
1053
966
  var MAX_IMPRESSION_KEYS = 1e3;
1054
967
  var MAX_TRACKED_AUCTIONS = 50;
968
+ var CLICK_DEDUP_MS = 1e3;
1055
969
  var SESSION_KEY = "_bidkernel_session";
1056
970
  var SESSION_TS_KEY = "_bidkernel_session_ts";
1057
971
  var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
1058
972
  var inMemorySessionId = null;
1059
973
  var inMemorySessionTs = 0;
974
+ var storageAllowed = true;
1060
975
  function generateUUID() {
1061
976
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1062
977
  return crypto.randomUUID();
@@ -1123,30 +1038,43 @@ function sameEndpointOrigin(a, b) {
1123
1038
  return false;
1124
1039
  }
1125
1040
  }
1126
- function extendSession() {
1127
- const now = Date.now();
1128
- inMemorySessionTs = now;
1041
+ var SESSION_PERSIST_INTERVAL_MS = 60 * 1e3;
1042
+ var persistedSessionTs = 0;
1043
+ function persistSessionTs(now) {
1129
1044
  try {
1130
1045
  if (typeof localStorage !== "undefined") {
1131
1046
  localStorage.setItem(SESSION_TS_KEY, now.toString());
1047
+ persistedSessionTs = now;
1132
1048
  }
1133
1049
  } catch {
1134
1050
  }
1135
1051
  }
1052
+ function extendSession() {
1053
+ const now = Date.now();
1054
+ inMemorySessionTs = now;
1055
+ if (!storageAllowed) return;
1056
+ if (now - persistedSessionTs < SESSION_PERSIST_INTERVAL_MS) return;
1057
+ persistSessionTs(now);
1058
+ }
1136
1059
  function getOrCreateSessionId() {
1137
1060
  const now = Date.now();
1061
+ const memoryFresh = !!inMemorySessionId && !!inMemorySessionTs && now - inMemorySessionTs <= THIRTY_MINUTES_MS;
1138
1062
  try {
1139
- if (typeof localStorage !== "undefined") {
1063
+ if (storageAllowed && typeof localStorage !== "undefined") {
1140
1064
  const storedId = localStorage.getItem(SESSION_KEY);
1141
1065
  const tsStr = localStorage.getItem(SESSION_TS_KEY);
1142
1066
  const storedTs = tsStr ? parseInt(tsStr, 10) || 0 : 0;
1143
1067
  if (storedId && storedTs && now - storedTs <= THIRTY_MINUTES_MS) {
1144
- localStorage.setItem(SESSION_TS_KEY, now.toString());
1068
+ persistSessionTs(now);
1069
+ inMemorySessionId = storedId;
1070
+ inMemorySessionTs = now;
1145
1071
  return storedId;
1146
1072
  }
1147
- const newId = generateUUID();
1073
+ const newId = memoryFresh ? inMemorySessionId : generateUUID();
1148
1074
  localStorage.setItem(SESSION_KEY, newId);
1149
- localStorage.setItem(SESSION_TS_KEY, now.toString());
1075
+ persistSessionTs(now);
1076
+ inMemorySessionId = newId;
1077
+ inMemorySessionTs = now;
1150
1078
  return newId;
1151
1079
  }
1152
1080
  } catch {
@@ -1254,557 +1182,334 @@ function parseBidDimensions(bid) {
1254
1182
  height: Number.isFinite(bid?.height) ? bid.height : 0
1255
1183
  };
1256
1184
  }
1257
- var adsLoaderListenerMap = /* @__PURE__ */ new WeakMap();
1258
- function hookAdsManagerLoadedEvent(loadedEventCtor) {
1259
- if (!loadedEventCtor) return;
1260
- try {
1261
- const loadedProto = loadedEventCtor.prototype;
1262
- if (loadedProto && typeof loadedProto.getAdsManager === "function" && !loadedProto.getAdsManager.__bidkernelHooked) {
1263
- const originalGetAdsManager = loadedProto.getAdsManager;
1264
- const hookedGetAdsManager = function(...args) {
1265
- const adsManager = originalGetAdsManager.apply(this, args);
1266
- if (adsManager) {
1267
- for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
1268
- try {
1269
- instance.attachImaAdsManager(adsManager);
1270
- } catch {
1271
- }
1272
- }
1273
- }
1274
- return adsManager;
1275
- };
1276
- hookedGetAdsManager.__bidkernelHooked = true;
1277
- loadedProto.getAdsManager = hookedGetAdsManager;
1185
+ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1186
+ static activeInstances = /* @__PURE__ */ new Set();
1187
+ static getActiveInstances() {
1188
+ return _BidkernelPrebidAnalytics.activeInstances;
1189
+ }
1190
+ /**
1191
+ * Allows or forbids localStorage writes for every instance on the page.
1192
+ * Collection and delivery are unaffected: only the persisted session id,
1193
+ * its timestamp and exit batches are gated.
1194
+ */
1195
+ static setStorageAllowed(allowed) {
1196
+ storageAllowed = allowed;
1197
+ persistedSessionTs = 0;
1198
+ }
1199
+ static isStorageAllowed() {
1200
+ return storageAllowed;
1201
+ }
1202
+ config;
1203
+ queue = [];
1204
+ errorCount = 0;
1205
+ flushTimer = null;
1206
+ boundFlushBeacon;
1207
+ boundVisibilityChange;
1208
+ isEnabled = false;
1209
+ boundPbjsHandlers = [];
1210
+ // Captured at enable()/navigate() so events queued before an SPA route
1211
+ // change flush with the page they occurred on, not the new URL.
1212
+ pageUrl = "";
1213
+ deduper = new PrebidEventDeduper();
1214
+ consecutiveSendFailures = 0;
1215
+ nextSendAllowedAt = 0;
1216
+ replayedEventCount = 0;
1217
+ revenueFlushTimer = null;
1218
+ // localStorage keys of exit batches this instance persisted, so a page that
1219
+ // survives its own pagehide/hidden (bfcache restore, tab re-focus) can
1220
+ // remove them instead of leaving them for a duplicate resend.
1221
+ persistedBatchKeys = [];
1222
+ persistedCleanupTimer = null;
1223
+ // Viewability & refresh tracking
1224
+ intersectionObserver = null;
1225
+ slotViewabilityRecords = /* @__PURE__ */ new Map();
1226
+ elementToSlotId = /* @__PURE__ */ new Map();
1227
+ slotRefreshIndices = /* @__PURE__ */ new Map();
1228
+ slotLastAuctionIds = /* @__PURE__ */ new Map();
1229
+ pendingThresholdListeners = /* @__PURE__ */ new Map();
1230
+ pendingViewableListeners = /* @__PURE__ */ new Map();
1231
+ // Element a subscriber named for a slot (a sticky bar or interstitial that
1232
+ // renders outside the slot element); wins over document.getElementById.
1233
+ slotElements = /* @__PURE__ */ new Map();
1234
+ clickDetachCleanups = /* @__PURE__ */ new Set();
1235
+ lastClickAt = /* @__PURE__ */ new Map();
1236
+ // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
1237
+ slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
1238
+ // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
1239
+ cachedWinningBids = /* @__PURE__ */ new Map();
1240
+ // Active video player attachment cleanup routines (registered by ./video)
1241
+ videoDetachCleanups = /* @__PURE__ */ new Set();
1242
+ // Compacted bidder participation map per auction: auctionId -> Map<`${bidder}:${adUnitCode}`, BidderOutcomeEntry>
1243
+ auctionBidderOutcomes = /* @__PURE__ */ new Map();
1244
+ logger;
1245
+ constructor(config) {
1246
+ if (config.storageAllowed !== void 0) {
1247
+ _BidkernelPrebidAnalytics.setStorageAllowed(config.storageAllowed);
1278
1248
  }
1279
- } catch {
1249
+ const requestedEndpoint = config.endpoint || "";
1250
+ const endpoint = !requestedEndpoint || isTrustedEndpoint(requestedEndpoint) ? requestedEndpoint : "";
1251
+ this.config = {
1252
+ endpoint,
1253
+ propertyId: config.propertyId || "",
1254
+ pageviewId: config.pageviewId || generateUUID(),
1255
+ sessionId: config.sessionId || getOrCreateSessionId(),
1256
+ userId: config.userId || "",
1257
+ deviceType: config.deviceType || "desktop",
1258
+ country: config.country || "",
1259
+ region: config.region || "",
1260
+ auctionEnabled: config.auctionEnabled ?? true,
1261
+ warningsEnabled: config.warningsEnabled ?? true,
1262
+ errorsEnabled: config.errorsEnabled ?? true,
1263
+ viewabilityEnabled: config.viewabilityEnabled ?? true,
1264
+ storageAllowed: config.storageAllowed ?? true,
1265
+ logLevel: config.logLevel || "INFO",
1266
+ pbjsGlobalName: config.pbjsGlobalName || "pbjs",
1267
+ attachPbjsListeners: config.attachPbjsListeners ?? true,
1268
+ revenueFlushDelayMs: Number.isFinite(config.revenueFlushDelayMs) && config.revenueFlushDelayMs >= 0 ? config.revenueFlushDelayMs : REVENUE_FLUSH_DELAY_MS
1269
+ };
1270
+ this.logger = createLogger({
1271
+ prefix: "[bidkernel][prebid-analytics]",
1272
+ logLevel: this.config.logLevel
1273
+ });
1274
+ if (requestedEndpoint && !endpoint) {
1275
+ this.log(
1276
+ "ERROR",
1277
+ `Rejected untrusted analytics endpoint ${requestedEndpoint}: must be an absolute https:// URL without embedded credentials. Events will not be transmitted.`
1278
+ );
1279
+ }
1280
+ this.boundFlushBeacon = () => {
1281
+ this.flushAllSlotsTimeInView();
1282
+ this.flushBeacon();
1283
+ };
1284
+ this.boundVisibilityChange = () => this.handleVisibilityChange();
1280
1285
  }
1281
- }
1282
- function hookAdsManagerPrototype(adsManagerCtorOrProto) {
1283
- if (!adsManagerCtorOrProto) return;
1284
- try {
1285
- const proto = adsManagerCtorOrProto.prototype || adsManagerCtorOrProto;
1286
- if (proto && typeof proto.init === "function" && !proto.init.__bidkernelHooked) {
1287
- const origInit = proto.init;
1288
- const hookedInit = function(...args) {
1289
- for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
1290
- try {
1291
- instance.attachImaAdsManager(this);
1292
- } catch {
1286
+ enable() {
1287
+ if (this.isEnabled) return;
1288
+ this.isEnabled = true;
1289
+ _BidkernelPrebidAnalytics.activeInstances.add(this);
1290
+ this.logger.setLevel(this.config.logLevel);
1291
+ if (!this.config.endpoint) {
1292
+ this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
1293
+ }
1294
+ if (this.config.viewabilityEnabled) {
1295
+ if (typeof IntersectionObserver !== "undefined") {
1296
+ this.intersectionObserver = new IntersectionObserver(this.handleIntersection.bind(this), {
1297
+ threshold: [0.5]
1298
+ });
1299
+ for (const record of this.slotViewabilityRecords.values()) {
1300
+ if (record.element) {
1301
+ this.intersectionObserver.observe(record.element);
1293
1302
  }
1294
1303
  }
1295
- return origInit.apply(this, args);
1296
- };
1297
- hookedInit.__bidkernelHooked = true;
1298
- proto.init = hookedInit;
1304
+ }
1299
1305
  }
1300
- if (proto && typeof proto.start === "function" && !proto.start.__bidkernelHooked) {
1301
- const origStart = proto.start;
1302
- const hookedStart = function(...args) {
1303
- for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
1304
- try {
1305
- instance.attachImaAdsManager(this);
1306
- } catch {
1307
- }
1306
+ if (this.config.attachPbjsListeners) {
1307
+ this.attachPbjs();
1308
+ }
1309
+ if (typeof window !== "undefined") {
1310
+ if (!this.pageUrl) {
1311
+ try {
1312
+ const u = new URL(window.location.href);
1313
+ this.pageUrl = u.origin + u.pathname;
1314
+ } catch {
1315
+ this.pageUrl = window.location.href;
1308
1316
  }
1309
- return origStart.apply(this, args);
1310
- };
1311
- hookedStart.__bidkernelHooked = true;
1312
- proto.start = hookedStart;
1317
+ }
1318
+ window.addEventListener("pagehide", this.boundFlushBeacon);
1319
+ if (typeof document !== "undefined") {
1320
+ document.addEventListener("visibilitychange", this.boundVisibilityChange);
1321
+ }
1322
+ this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
1323
+ this.resendPersistedBatches();
1313
1324
  }
1314
- } catch {
1315
1325
  }
1316
- }
1317
- function hookAdsLoader(adsLoaderCtor) {
1318
- if (!adsLoaderCtor) return;
1319
- try {
1320
- const proto = adsLoaderCtor.prototype;
1321
- if (!proto) return;
1322
- if (typeof proto.addEventListener === "function" && !proto.addEventListener.__bidkernelHooked) {
1323
- const origAddEventListener = proto.addEventListener;
1324
- const hookedAddEventListener = function(type, listener, ...rest) {
1325
- if ((type === "adsManagerLoaded" || type === (this?.AdsManagerLoadedEvent?.Type?.ADS_MANAGER_LOADED || "adsManagerLoaded")) && typeof listener === "function") {
1326
- let wrappedListener = adsLoaderListenerMap.get(listener);
1327
- if (!wrappedListener) {
1328
- wrappedListener = function(event) {
1329
- if (event && typeof event.getAdsManager === "function" && !event.getAdsManager.__bidkernelHooked) {
1330
- const origGetAdsManager = event.getAdsManager;
1331
- event.getAdsManager = function(...args) {
1332
- const adsManager = origGetAdsManager.apply(this, args);
1333
- if (adsManager) {
1334
- for (const sdk of BidkernelPrebidAnalytics.getActiveInstances()) {
1335
- try {
1336
- sdk.attachImaAdsManager(adsManager);
1337
- } catch {
1338
- }
1339
- }
1340
- }
1341
- return adsManager;
1342
- };
1343
- event.getAdsManager.__bidkernelHooked = true;
1326
+ /**
1327
+ * Binds the Prebid event handlers on the configured global and replays
1328
+ * pbjs.getEvents() history. enable() calls this when attachPbjsListeners is
1329
+ * true. An integration that loads Prebid lazily constructs the instance with
1330
+ * attachPbjsListeners=false and calls this once pbjs exists; disable() (and
1331
+ * therefore navigate()) drops the handlers, so call it again after either.
1332
+ * Idempotent. Returns true when handlers are bound after the call, false
1333
+ * when the instance is disabled or pbjs.onEvent is not available yet.
1334
+ */
1335
+ attachPbjs() {
1336
+ if (!this.isEnabled) return false;
1337
+ if (this.boundPbjsHandlers.length > 0) return true;
1338
+ const win = typeof window !== "undefined" ? window : {};
1339
+ const pbjs = win[this.config.pbjsGlobalName] || {};
1340
+ if (typeof pbjs.onEvent !== "function") {
1341
+ this.log("WARN", "pbjs.onEvent is not defined. Prebid analytics will not function.");
1342
+ return false;
1343
+ }
1344
+ this.log("DEBUG", "Attaching event listeners to pbjs");
1345
+ const handlerMap = {
1346
+ auctionInit: this.handleAuctionInit.bind(this),
1347
+ auctionEnd: this.handleAuctionEnd.bind(this),
1348
+ bidRequested: this.handleBidRequested.bind(this),
1349
+ bidResponse: this.handleBidResponse.bind(this),
1350
+ bidTimeout: this.handleBidTimeout.bind(this),
1351
+ bidWon: this.handleBidWon.bind(this),
1352
+ noBid: this.handleNoBid.bind(this),
1353
+ adRenderFailed: this.handleAdRenderFailed.bind(this),
1354
+ adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
1355
+ };
1356
+ for (const [name, fn] of Object.entries(handlerMap)) {
1357
+ pbjs.onEvent(name, fn);
1358
+ this.boundPbjsHandlers.push({ event: name, handler: fn });
1359
+ }
1360
+ if (typeof pbjs.getEvents === "function") {
1361
+ try {
1362
+ const pastEvents = pbjs.getEvents();
1363
+ if (Array.isArray(pastEvents)) {
1364
+ const newPastEvents = pastEvents.slice(this.replayedEventCount);
1365
+ this.replayedEventCount = pastEvents.length;
1366
+ if (newPastEvents.length > 0) {
1367
+ this.log(
1368
+ "DEBUG",
1369
+ `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
1370
+ );
1371
+ for (const ev of newPastEvents) {
1372
+ if (!ev) continue;
1373
+ const eventType = ev.eventType || ev.event || ev.name;
1374
+ const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
1375
+ const handler = handlerMap[eventType];
1376
+ if (handler) {
1377
+ handler(args);
1344
1378
  }
1345
- return listener.apply(this, arguments);
1346
- };
1347
- adsLoaderListenerMap.set(listener, wrappedListener);
1379
+ }
1348
1380
  }
1349
- return origAddEventListener.call(this, type, wrappedListener, ...rest);
1350
1381
  }
1351
- return origAddEventListener.call(this, type, listener, ...rest);
1352
- };
1353
- hookedAddEventListener.__bidkernelHooked = true;
1354
- proto.addEventListener = hookedAddEventListener;
1355
- }
1356
- if (typeof proto.removeEventListener === "function" && !proto.removeEventListener.__bidkernelHooked) {
1357
- const origRemoveEventListener = proto.removeEventListener;
1358
- const hookedRemoveEventListener = function(type, listener, ...rest) {
1359
- const wrapped = typeof listener === "function" ? adsLoaderListenerMap.get(listener) : void 0;
1360
- return origRemoveEventListener.call(this, type, wrapped || listener, ...rest);
1361
- };
1362
- hookedRemoveEventListener.__bidkernelHooked = true;
1363
- proto.removeEventListener = hookedRemoveEventListener;
1382
+ } catch (e) {
1383
+ this.log("WARN", "Failed to replay historical events from pbjs.getEvents()", e);
1384
+ }
1364
1385
  }
1365
- } catch {
1386
+ return true;
1366
1387
  }
1367
- }
1368
- function hookImaPrototype(ima) {
1369
- if (!ima || typeof ima !== "object") return;
1370
- try {
1371
- if (ima.AdsManagerLoadedEvent) {
1372
- hookAdsManagerLoadedEvent(ima.AdsManagerLoadedEvent);
1388
+ disable() {
1389
+ if (!this.isEnabled) return;
1390
+ this.flushAllSlotsTimeInView();
1391
+ this.isEnabled = false;
1392
+ _BidkernelPrebidAnalytics.activeInstances.delete(this);
1393
+ for (const record of this.slotViewabilityRecords.values()) {
1394
+ if (record.dwellTimer) {
1395
+ clearTimeout(record.dwellTimer);
1396
+ record.dwellTimer = null;
1397
+ record.dwellStartedAt = null;
1398
+ }
1399
+ for (const l of record.thresholdListeners) {
1400
+ if (l.timer) {
1401
+ clearTimeout(l.timer);
1402
+ l.timer = null;
1403
+ }
1404
+ }
1373
1405
  }
1374
- const loadedDesc = Object.getOwnPropertyDescriptor(ima, "AdsManagerLoadedEvent");
1375
- if (!loadedDesc || loadedDesc.configurable) {
1376
- let _loadedEvent = ima.AdsManagerLoadedEvent;
1406
+ if (this.intersectionObserver) {
1407
+ this.intersectionObserver.disconnect();
1408
+ this.intersectionObserver = null;
1409
+ }
1410
+ this.slotViewabilityRecords.clear();
1411
+ this.elementToSlotId.clear();
1412
+ for (const cleanup of this.videoDetachCleanups) {
1377
1413
  try {
1378
- Object.defineProperty(ima, "AdsManagerLoadedEvent", {
1379
- configurable: true,
1380
- enumerable: true,
1381
- get() {
1382
- return _loadedEvent;
1383
- },
1384
- set(val) {
1385
- _loadedEvent = val;
1386
- hookAdsManagerLoadedEvent(val);
1387
- }
1388
- });
1414
+ cleanup();
1389
1415
  } catch {
1390
1416
  }
1391
1417
  }
1392
- if (ima.AdsLoader) {
1393
- hookAdsLoader(ima.AdsLoader);
1394
- }
1395
- const loaderDesc = Object.getOwnPropertyDescriptor(ima, "AdsLoader");
1396
- if (!loaderDesc || loaderDesc.configurable) {
1397
- let _loader = ima.AdsLoader;
1418
+ this.videoDetachCleanups.clear();
1419
+ for (const cleanup of Array.from(this.clickDetachCleanups)) {
1398
1420
  try {
1399
- Object.defineProperty(ima, "AdsLoader", {
1400
- configurable: true,
1401
- enumerable: true,
1402
- get() {
1403
- return _loader;
1404
- },
1405
- set(val) {
1406
- _loader = val;
1407
- hookAdsLoader(val);
1408
- }
1409
- });
1421
+ cleanup();
1410
1422
  } catch {
1411
1423
  }
1412
1424
  }
1413
- if (ima.AdsManager) {
1414
- hookAdsManagerPrototype(ima.AdsManager);
1425
+ this.clickDetachCleanups.clear();
1426
+ this.slotElements.clear();
1427
+ this.cachedWinningBids.clear();
1428
+ this.slotEmittedImpressionKeys.clear();
1429
+ this.slotLastAuctionIds.clear();
1430
+ this.auctionBidderOutcomes.clear();
1431
+ if (this.flushTimer) {
1432
+ clearInterval(this.flushTimer);
1433
+ this.flushTimer = null;
1415
1434
  }
1416
- const mgrDesc = Object.getOwnPropertyDescriptor(ima, "AdsManager");
1417
- if (!mgrDesc || mgrDesc.configurable) {
1418
- let _mgr = ima.AdsManager;
1419
- try {
1420
- Object.defineProperty(ima, "AdsManager", {
1421
- configurable: true,
1422
- enumerable: true,
1423
- get() {
1424
- return _mgr;
1425
- },
1426
- set(val) {
1427
- _mgr = val;
1428
- hookAdsManagerPrototype(val);
1429
- }
1430
- });
1431
- } catch {
1435
+ if (this.revenueFlushTimer) {
1436
+ clearTimeout(this.revenueFlushTimer);
1437
+ this.revenueFlushTimer = null;
1438
+ }
1439
+ if (this.persistedCleanupTimer) {
1440
+ clearTimeout(this.persistedCleanupTimer);
1441
+ this.persistedCleanupTimer = null;
1442
+ }
1443
+ if (typeof window !== "undefined") {
1444
+ window.removeEventListener("pagehide", this.boundFlushBeacon);
1445
+ if (typeof document !== "undefined") {
1446
+ document.removeEventListener("visibilitychange", this.boundVisibilityChange);
1432
1447
  }
1433
1448
  }
1434
- } catch {
1435
- }
1436
- }
1437
- function initImaInterception() {
1438
- if (typeof window === "undefined") return;
1439
- const win = window;
1440
- if (win.google?.ima) {
1441
- hookImaPrototype(win.google.ima);
1442
- }
1443
- if (win.google && typeof win.google === "object") {
1444
- const imaDesc = Object.getOwnPropertyDescriptor(win.google, "ima");
1445
- if (!imaDesc || imaDesc.configurable) {
1446
- let _ima = win.google.ima;
1447
- try {
1448
- Object.defineProperty(win.google, "ima", {
1449
- configurable: true,
1450
- enumerable: true,
1451
- get() {
1452
- return _ima;
1453
- },
1454
- set(val) {
1455
- _ima = val;
1456
- hookImaPrototype(val);
1457
- }
1458
- });
1459
- } catch {
1449
+ if (this.boundPbjsHandlers.length > 0) {
1450
+ const win = typeof window !== "undefined" ? window : {};
1451
+ const pbjs = win[this.config.pbjsGlobalName] || {};
1452
+ if (typeof pbjs.offEvent === "function") {
1453
+ for (const { event, handler } of this.boundPbjsHandlers) {
1454
+ pbjs.offEvent(event, handler);
1455
+ }
1460
1456
  }
1457
+ this.boundPbjsHandlers = [];
1461
1458
  }
1459
+ this.flush();
1462
1460
  }
1463
- const googleDesc = Object.getOwnPropertyDescriptor(win, "google");
1464
- if (!googleDesc || googleDesc.configurable) {
1465
- let _google = win.google;
1466
- try {
1467
- Object.defineProperty(win, "google", {
1468
- configurable: true,
1469
- enumerable: true,
1470
- get() {
1471
- return _google;
1472
- },
1473
- set(val) {
1474
- _google = val;
1475
- if (_google && typeof _google === "object") {
1476
- if (_google.ima) {
1477
- hookImaPrototype(_google.ima);
1478
- }
1479
- const iDesc = Object.getOwnPropertyDescriptor(_google, "ima");
1480
- if (!iDesc || iDesc.configurable) {
1481
- let _i = _google.ima;
1482
- try {
1483
- Object.defineProperty(_google, "ima", {
1484
- configurable: true,
1485
- enumerable: true,
1486
- get() {
1487
- return _i;
1488
- },
1489
- set(iv) {
1490
- _i = iv;
1491
- hookImaPrototype(iv);
1492
- }
1493
- });
1494
- } catch {
1495
- }
1461
+ handleVisibilityChange() {
1462
+ if (typeof document === "undefined") return;
1463
+ if (document.visibilityState === "hidden") {
1464
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1465
+ for (const record of this.slotViewabilityRecords.values()) {
1466
+ if (record.inView && record.lastEnteredViewAt !== null) {
1467
+ record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
1468
+ record.lastEnteredViewAt = null;
1469
+ this.checkThresholdListeners(record);
1470
+ }
1471
+ if (record.dwellTimer) {
1472
+ clearTimeout(record.dwellTimer);
1473
+ record.dwellTimer = null;
1474
+ record.dwellStartedAt = null;
1475
+ }
1476
+ for (const l of record.thresholdListeners) {
1477
+ if (l.timer) {
1478
+ clearTimeout(l.timer);
1479
+ l.timer = null;
1480
+ }
1481
+ }
1482
+ const durationMs = Math.round(record.accumulatedTimeInViewMs);
1483
+ if (durationMs > 0) {
1484
+ this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
1485
+ auctionId: record.auctionId,
1486
+ transactionId: record.transactionId,
1487
+ adUnitCode: record.adUnitCode,
1488
+ viewableDurationMs: durationMs,
1489
+ bid: record.bidPayload,
1490
+ metadata: {
1491
+ ...record.metadata,
1492
+ refresh_index: String(record.refreshIndex)
1496
1493
  }
1494
+ });
1495
+ }
1496
+ }
1497
+ this.flushBeacon();
1498
+ } else if (document.visibilityState === "visible") {
1499
+ const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1500
+ for (const record of this.slotViewabilityRecords.values()) {
1501
+ if (record.inView) {
1502
+ record.lastEnteredViewAt = now;
1503
+ if (!record.viewableFired && record.dwellTimer === null) {
1504
+ const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
1505
+ record.dwellStartedAt = now;
1506
+ record.dwellTimer = setTimeout(() => {
1507
+ this.handleDwellComplete(record);
1508
+ }, dwellTarget);
1497
1509
  }
1510
+ this.scheduleThresholdTimers(record);
1498
1511
  }
1499
- });
1500
- } catch {
1501
- }
1502
- }
1503
- }
1504
- if (typeof window !== "undefined") {
1505
- initImaInterception();
1506
- }
1507
- var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1508
- static activeInstances = /* @__PURE__ */ new Set();
1509
- static getActiveInstances() {
1510
- return _BidkernelPrebidAnalytics.activeInstances;
1511
- }
1512
- static initImaInterception() {
1513
- initImaInterception();
1514
- }
1515
- config;
1516
- queue = [];
1517
- errorCount = 0;
1518
- flushTimer = null;
1519
- boundFlushBeacon;
1520
- boundVisibilityChange;
1521
- isEnabled = false;
1522
- boundPbjsHandlers = [];
1523
- // Captured at enable()/navigate() so events queued before an SPA route
1524
- // change flush with the page they occurred on, not the new URL.
1525
- pageUrl = "";
1526
- deduper = new PrebidEventDeduper();
1527
- consecutiveSendFailures = 0;
1528
- nextSendAllowedAt = 0;
1529
- replayedEventCount = 0;
1530
- revenueFlushTimer = null;
1531
- // localStorage keys of exit batches this instance persisted, so a page that
1532
- // survives its own pagehide/hidden (bfcache restore, tab re-focus) can
1533
- // remove them instead of leaving them for a duplicate resend.
1534
- persistedBatchKeys = [];
1535
- persistedCleanupTimer = null;
1536
- // Viewability & refresh tracking
1537
- intersectionObserver = null;
1538
- slotViewabilityRecords = /* @__PURE__ */ new Map();
1539
- elementToSlotId = /* @__PURE__ */ new Map();
1540
- slotRefreshIndices = /* @__PURE__ */ new Map();
1541
- slotLastAuctionIds = /* @__PURE__ */ new Map();
1542
- pendingThresholdListeners = /* @__PURE__ */ new Map();
1543
- // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
1544
- slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
1545
- // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
1546
- cachedWinningBids = /* @__PURE__ */ new Map();
1547
- // Active video player attachment cleanup routines
1548
- videoDetachCleanups = /* @__PURE__ */ new Set();
1549
- // Compacted bidder participation map per auction: auctionId -> Map<`${bidder}:${adUnitCode}`, BidderOutcomeEntry>
1550
- auctionBidderOutcomes = /* @__PURE__ */ new Map();
1551
- logger;
1552
- constructor(config) {
1553
- const requestedEndpoint = config.endpoint || "";
1554
- const endpoint = !requestedEndpoint || isTrustedEndpoint(requestedEndpoint) ? requestedEndpoint : "";
1555
- this.config = {
1556
- endpoint,
1557
- propertyId: config.propertyId || "",
1558
- pageviewId: config.pageviewId || generateUUID(),
1559
- sessionId: config.sessionId || getOrCreateSessionId(),
1560
- userId: config.userId || "",
1561
- deviceType: config.deviceType || "desktop",
1562
- country: config.country || "",
1563
- region: config.region || "",
1564
- auctionEnabled: config.auctionEnabled ?? true,
1565
- warningsEnabled: config.warningsEnabled ?? true,
1566
- errorsEnabled: config.errorsEnabled ?? true,
1567
- viewabilityEnabled: config.viewabilityEnabled ?? true,
1568
- logLevel: config.logLevel || "INFO",
1569
- pbjsGlobalName: config.pbjsGlobalName || "pbjs",
1570
- attachPbjsListeners: config.attachPbjsListeners ?? true,
1571
- revenueFlushDelayMs: Number.isFinite(config.revenueFlushDelayMs) && config.revenueFlushDelayMs >= 0 ? config.revenueFlushDelayMs : REVENUE_FLUSH_DELAY_MS
1572
- };
1573
- this.logger = createLogger({
1574
- prefix: "[bidkernel][prebid-analytics]",
1575
- logLevel: this.config.logLevel
1576
- });
1577
- if (requestedEndpoint && !endpoint) {
1578
- this.log(
1579
- "ERROR",
1580
- `Rejected untrusted analytics endpoint ${requestedEndpoint}: must be an absolute https:// URL without embedded credentials. Events will not be transmitted.`
1581
- );
1582
- }
1583
- this.boundFlushBeacon = () => {
1584
- this.flushAllSlotsTimeInView();
1585
- this.flushBeacon();
1586
- };
1587
- this.boundVisibilityChange = () => this.handleVisibilityChange();
1588
- }
1589
- enable() {
1590
- if (this.isEnabled) return;
1591
- this.isEnabled = true;
1592
- _BidkernelPrebidAnalytics.activeInstances.add(this);
1593
- this.logger.setLevel(this.config.logLevel);
1594
- if (!this.config.endpoint) {
1595
- this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
1596
- }
1597
- if (this.config.viewabilityEnabled) {
1598
- if (typeof IntersectionObserver !== "undefined") {
1599
- this.intersectionObserver = new IntersectionObserver(this.handleIntersection.bind(this), {
1600
- threshold: [0.5]
1601
- });
1602
- for (const record of this.slotViewabilityRecords.values()) {
1603
- if (record.element) {
1604
- this.intersectionObserver.observe(record.element);
1605
- }
1606
- }
1607
- }
1608
- }
1609
- if (this.config.attachPbjsListeners) {
1610
- const win = typeof window !== "undefined" ? window : {};
1611
- const pbjs = win[this.config.pbjsGlobalName] || {};
1612
- if (typeof pbjs.onEvent === "function") {
1613
- this.log("DEBUG", "Attaching event listeners to pbjs");
1614
- const events = [
1615
- ["auctionInit", this.handleAuctionInit.bind(this)],
1616
- ["auctionEnd", this.handleAuctionEnd.bind(this)],
1617
- ["bidRequested", this.handleBidRequested.bind(this)],
1618
- ["bidResponse", this.handleBidResponse.bind(this)],
1619
- ["bidTimeout", this.handleBidTimeout.bind(this)],
1620
- ["bidWon", this.handleBidWon.bind(this)],
1621
- ["noBid", this.handleNoBid.bind(this)],
1622
- ["adRenderFailed", this.handleAdRenderFailed.bind(this)],
1623
- ["adRenderSucceeded", this.handleAdRenderSucceeded.bind(this)]
1624
- ];
1625
- for (const [name, fn] of events) {
1626
- pbjs.onEvent(name, fn);
1627
- this.boundPbjsHandlers.push({ event: name, handler: fn });
1628
- }
1629
- } else {
1630
- this.log("WARN", "pbjs.onEvent is not defined. Prebid analytics will not function.");
1631
- }
1632
- if (typeof pbjs.getEvents === "function") {
1633
- try {
1634
- const pastEvents = pbjs.getEvents();
1635
- if (Array.isArray(pastEvents)) {
1636
- const newPastEvents = pastEvents.slice(this.replayedEventCount);
1637
- this.replayedEventCount = pastEvents.length;
1638
- if (newPastEvents.length > 0) {
1639
- this.log(
1640
- "DEBUG",
1641
- `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
1642
- );
1643
- const handlerMap = {
1644
- auctionInit: this.handleAuctionInit.bind(this),
1645
- auctionEnd: this.handleAuctionEnd.bind(this),
1646
- bidRequested: this.handleBidRequested.bind(this),
1647
- bidResponse: this.handleBidResponse.bind(this),
1648
- bidTimeout: this.handleBidTimeout.bind(this),
1649
- bidWon: this.handleBidWon.bind(this),
1650
- noBid: this.handleNoBid.bind(this),
1651
- adRenderFailed: this.handleAdRenderFailed.bind(this),
1652
- adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
1653
- };
1654
- for (const ev of newPastEvents) {
1655
- if (!ev) continue;
1656
- const eventType = ev.eventType || ev.event || ev.name;
1657
- const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
1658
- const handler = handlerMap[eventType];
1659
- if (handler) {
1660
- handler(args);
1661
- }
1662
- }
1663
- }
1664
- }
1665
- } catch (e) {
1666
- this.log("WARN", "Failed to replay historical events from pbjs.getEvents()", e);
1667
- }
1668
- }
1669
- }
1670
- if (typeof window !== "undefined") {
1671
- if (!this.pageUrl) {
1672
- try {
1673
- const u = new URL(window.location.href);
1674
- this.pageUrl = u.origin + u.pathname;
1675
- } catch {
1676
- this.pageUrl = window.location.href;
1677
- }
1678
- }
1679
- window.addEventListener("pagehide", this.boundFlushBeacon);
1680
- if (typeof document !== "undefined") {
1681
- document.addEventListener("visibilitychange", this.boundVisibilityChange);
1682
- }
1683
- this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
1684
- this.resendPersistedBatches();
1685
- initImaInterception();
1686
- if (window.google?.ima) {
1687
- hookImaPrototype(window.google.ima);
1688
- }
1689
- }
1690
- }
1691
- disable() {
1692
- if (!this.isEnabled) return;
1693
- this.flushAllSlotsTimeInView();
1694
- this.isEnabled = false;
1695
- _BidkernelPrebidAnalytics.activeInstances.delete(this);
1696
- for (const record of this.slotViewabilityRecords.values()) {
1697
- if (record.dwellTimer) {
1698
- clearTimeout(record.dwellTimer);
1699
- record.dwellTimer = null;
1700
- record.dwellStartedAt = null;
1701
- }
1702
- for (const l of record.thresholdListeners) {
1703
- if (l.timer) {
1704
- clearTimeout(l.timer);
1705
- l.timer = null;
1706
- }
1707
- }
1708
- }
1709
- if (this.intersectionObserver) {
1710
- this.intersectionObserver.disconnect();
1711
- this.intersectionObserver = null;
1712
- }
1713
- this.slotViewabilityRecords.clear();
1714
- this.elementToSlotId.clear();
1715
- for (const cleanup of this.videoDetachCleanups) {
1716
- try {
1717
- cleanup();
1718
- } catch {
1719
- }
1720
- }
1721
- this.videoDetachCleanups.clear();
1722
- this.cachedWinningBids.clear();
1723
- this.slotEmittedImpressionKeys.clear();
1724
- this.slotLastAuctionIds.clear();
1725
- this.auctionBidderOutcomes.clear();
1726
- if (this.flushTimer) {
1727
- clearInterval(this.flushTimer);
1728
- this.flushTimer = null;
1729
- }
1730
- if (this.revenueFlushTimer) {
1731
- clearTimeout(this.revenueFlushTimer);
1732
- this.revenueFlushTimer = null;
1733
- }
1734
- if (this.persistedCleanupTimer) {
1735
- clearTimeout(this.persistedCleanupTimer);
1736
- this.persistedCleanupTimer = null;
1737
- }
1738
- if (typeof window !== "undefined") {
1739
- window.removeEventListener("pagehide", this.boundFlushBeacon);
1740
- if (typeof document !== "undefined") {
1741
- document.removeEventListener("visibilitychange", this.boundVisibilityChange);
1742
- }
1743
- }
1744
- if (this.boundPbjsHandlers.length > 0) {
1745
- const win = typeof window !== "undefined" ? window : {};
1746
- const pbjs = win[this.config.pbjsGlobalName] || {};
1747
- if (typeof pbjs.offEvent === "function") {
1748
- for (const { event, handler } of this.boundPbjsHandlers) {
1749
- pbjs.offEvent(event, handler);
1750
- }
1751
- }
1752
- this.boundPbjsHandlers = [];
1753
- }
1754
- this.flush();
1755
- }
1756
- handleVisibilityChange() {
1757
- if (typeof document === "undefined") return;
1758
- if (document.visibilityState === "hidden") {
1759
- const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1760
- for (const record of this.slotViewabilityRecords.values()) {
1761
- if (record.inView && record.lastEnteredViewAt !== null) {
1762
- record.accumulatedTimeInViewMs += Math.max(0, now - record.lastEnteredViewAt);
1763
- record.lastEnteredViewAt = null;
1764
- this.checkThresholdListeners(record);
1765
- }
1766
- if (record.dwellTimer) {
1767
- clearTimeout(record.dwellTimer);
1768
- record.dwellTimer = null;
1769
- record.dwellStartedAt = null;
1770
- }
1771
- for (const l of record.thresholdListeners) {
1772
- if (l.timer) {
1773
- clearTimeout(l.timer);
1774
- l.timer = null;
1775
- }
1776
- }
1777
- const durationMs = Math.round(record.accumulatedTimeInViewMs);
1778
- if (durationMs > 0) {
1779
- this.enqueue(TraceEventType.TIME_IN_VIEW, "timeInView", {
1780
- auctionId: record.auctionId,
1781
- transactionId: record.transactionId,
1782
- adUnitCode: record.adUnitCode,
1783
- viewableDurationMs: durationMs,
1784
- bid: record.bidPayload,
1785
- metadata: {
1786
- ...record.metadata,
1787
- refresh_index: String(record.refreshIndex)
1788
- }
1789
- });
1790
- }
1791
- }
1792
- this.flushBeacon();
1793
- } else if (document.visibilityState === "visible") {
1794
- const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1795
- for (const record of this.slotViewabilityRecords.values()) {
1796
- if (record.inView) {
1797
- record.lastEnteredViewAt = now;
1798
- if (!record.viewableFired && record.dwellTimer === null) {
1799
- const dwellTarget = record.mediaType === "video" ? 2e3 : 1e3;
1800
- record.dwellStartedAt = now;
1801
- record.dwellTimer = setTimeout(() => {
1802
- this.handleDwellComplete(record);
1803
- }, dwellTarget);
1804
- }
1805
- this.scheduleThresholdTimers(record);
1806
- }
1807
- }
1512
+ }
1808
1513
  }
1809
1514
  }
1810
1515
  handleIntersection(entries) {
@@ -1880,6 +1585,105 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1880
1585
  }
1881
1586
  });
1882
1587
  }
1588
+ this.notifyViewable(record);
1589
+ }
1590
+ notifyViewable(record) {
1591
+ if (!record.viewableFired) return;
1592
+ for (const listener of record.viewableListeners) {
1593
+ if (listener.firedForIndex === record.refreshIndex) continue;
1594
+ listener.firedForIndex = record.refreshIndex;
1595
+ try {
1596
+ listener.callback(record.slotId);
1597
+ } catch (e) {
1598
+ this.log("ERROR", "Error in viewable listener callback", e);
1599
+ }
1600
+ }
1601
+ }
1602
+ /**
1603
+ * Calls back once per render cycle when the slot has met the IAB viewable
1604
+ * standard this adapter measures (immediately if it already has). Works
1605
+ * before the slot is observed: the listener waits for observeSlot. Returns
1606
+ * an unsubscribe function.
1607
+ */
1608
+ /**
1609
+ * Names the element to measure for a slot when the creative renders
1610
+ * somewhere other than the slot element (a sticky bar, an interstitial).
1611
+ * Applies to the current cycle at once and to every later render.
1612
+ */
1613
+ setSlotElement(slotId, element) {
1614
+ this.slotElements.set(slotId, element);
1615
+ const record = this.slotViewabilityRecords.get(slotId);
1616
+ if (record && record.element !== element) {
1617
+ this.observeSlot(element, slotId, {
1618
+ refreshIndex: record.refreshIndex,
1619
+ emitRefreshEvent: false
1620
+ });
1621
+ }
1622
+ }
1623
+ /**
1624
+ * Records a click on the slot's current creative, attributed to the cached
1625
+ * winning bid. Repeats within one second are one click. Returns whether a
1626
+ * CLICK was recorded.
1627
+ */
1628
+ recordClick(slotId, options) {
1629
+ if (!this.isEnabled || !slotId) return false;
1630
+ const now = Date.now();
1631
+ if (now - (this.lastClickAt.get(slotId) || 0) < CLICK_DEDUP_MS) return false;
1632
+ this.lastClickAt.set(slotId, now);
1633
+ const cached = this.getCachedBid(slotId);
1634
+ this.enqueue(TraceEventType.CLICK, "click", {
1635
+ auctionId: cached?.auctionId || "",
1636
+ transactionId: cached?.transactionId || "",
1637
+ adUnitCode: slotId,
1638
+ bid: cached?.bidTrace,
1639
+ metadata: {
1640
+ ...options?.metadata,
1641
+ refresh_index: String(this.getRefreshIndex(slotId))
1642
+ }
1643
+ });
1644
+ return true;
1645
+ }
1646
+ /**
1647
+ * Detects clicks into a cross-origin creative frame: a click inside an
1648
+ * iframe moves focus into it and blurs the window. Returns a detach
1649
+ * function; every tracker is detached on disable().
1650
+ */
1651
+ attachClickTracker(element, slotId) {
1652
+ if (typeof window === "undefined") return () => {
1653
+ };
1654
+ const onBlur = () => {
1655
+ setTimeout(() => {
1656
+ const active = document.activeElement;
1657
+ if (active && (active === element || element.contains(active))) {
1658
+ this.recordClick(slotId, { metadata: { source: "blur" } });
1659
+ window.focus();
1660
+ }
1661
+ }, 0);
1662
+ };
1663
+ window.addEventListener("blur", onBlur);
1664
+ const cleanup = () => {
1665
+ window.removeEventListener("blur", onBlur);
1666
+ this.clickDetachCleanups.delete(cleanup);
1667
+ };
1668
+ this.clickDetachCleanups.add(cleanup);
1669
+ return cleanup;
1670
+ }
1671
+ onViewable(slotId, callback) {
1672
+ const listener = { callback, firedForIndex: -1 };
1673
+ const record = this.slotViewabilityRecords.get(slotId);
1674
+ if (record) {
1675
+ record.viewableListeners.add(listener);
1676
+ this.notifyViewable(record);
1677
+ } else {
1678
+ if (!this.pendingViewableListeners.has(slotId)) {
1679
+ this.pendingViewableListeners.set(slotId, /* @__PURE__ */ new Set());
1680
+ }
1681
+ this.pendingViewableListeners.get(slotId).add(listener);
1682
+ }
1683
+ return () => {
1684
+ this.slotViewabilityRecords.get(slotId)?.viewableListeners.delete(listener);
1685
+ this.pendingViewableListeners.get(slotId)?.delete(listener);
1686
+ };
1883
1687
  }
1884
1688
  scheduleThresholdTimers(record) {
1885
1689
  if (!record.inView || typeof document !== "undefined" && document.visibilityState === "hidden") {
@@ -2069,6 +1873,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2069
1873
  }
2070
1874
  const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
2071
1875
  this.pendingThresholdListeners.delete(resolvedSlotId);
1876
+ const viewableListeners = this.pendingViewableListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
1877
+ this.pendingViewableListeners.delete(resolvedSlotId);
2072
1878
  const record = {
2073
1879
  slotId: resolvedSlotId,
2074
1880
  element: el,
@@ -2085,7 +1891,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2085
1891
  viewableFired: false,
2086
1892
  dwellTimer: null,
2087
1893
  dwellStartedAt: null,
2088
- thresholdListeners: listeners
1894
+ thresholdListeners: listeners,
1895
+ viewableListeners
2089
1896
  };
2090
1897
  this.slotViewabilityRecords.set(resolvedSlotId, record);
2091
1898
  if (el) {
@@ -2110,6 +1917,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2110
1917
  }
2111
1918
  }
2112
1919
  record.thresholdListeners = /* @__PURE__ */ new Set();
1920
+ record.viewableListeners = /* @__PURE__ */ new Set();
2113
1921
  if (record.element && this.intersectionObserver) {
2114
1922
  this.intersectionObserver.unobserve(record.element);
2115
1923
  this.elementToSlotId.delete(record.element);
@@ -2124,6 +1932,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2124
1932
  this.slotRefreshIndices.delete(slotId);
2125
1933
  this.slotLastAuctionIds.delete(slotId);
2126
1934
  this.pendingThresholdListeners.delete(slotId);
1935
+ this.pendingViewableListeners.delete(slotId);
2127
1936
  const prefix = `${escapeKeyPart(slotId)}:`;
2128
1937
  for (const key of Array.from(this.slotEmittedImpressionKeys)) {
2129
1938
  if (key === slotId || key.startsWith(prefix)) {
@@ -2170,72 +1979,6 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2170
1979
  this.cachedWinningBids.delete(oldest.value);
2171
1980
  }
2172
1981
  }
2173
- /**
2174
- * Searches cached winning bids for an entry matching the given filters (creativeId,
2175
- * adId, or mediaType), falling back to the most recent winning video bid.
2176
- */
2177
- findCachedWinningBid(filter) {
2178
- const now = Date.now();
2179
- const isConsumed = (entry) => {
2180
- return this.hasImpressionEmitted(
2181
- entry.adUnitCode,
2182
- void 0,
2183
- entry.auctionId,
2184
- entry.bidTrace.creativeId
2185
- );
2186
- };
2187
- if (filter?.creativeId || filter?.adId) {
2188
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2189
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2190
- if (filter.creativeId && (entry.bidTrace.creativeId === filter.creativeId || entry.rawBid?.creativeId === filter.creativeId) || filter.adId && (entry.rawBid?.adId === filter.adId || entry.adUnitCode === filter.adId)) {
2191
- return entry;
2192
- }
2193
- }
2194
- }
2195
- let bestUnconsumedMatch;
2196
- if (filter?.mediaType) {
2197
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2198
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2199
- const isEntryVideo = entry.bidTrace.mediaType === filter.mediaType || entry.rawBid?.mediaType === filter.mediaType || filter.mediaType === "video" && entry.rawBid?.mediaTypes?.video !== void 0;
2200
- if (isEntryVideo) {
2201
- if (!isConsumed(entry)) {
2202
- if (!bestUnconsumedMatch || entry.timestamp < bestUnconsumedMatch.timestamp) {
2203
- bestUnconsumedMatch = entry;
2204
- }
2205
- }
2206
- }
2207
- }
2208
- if (bestUnconsumedMatch) return bestUnconsumedMatch;
2209
- let bestFallbackMatch;
2210
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2211
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2212
- const isEntryVideo = entry.bidTrace.mediaType === filter.mediaType || entry.rawBid?.mediaType === filter.mediaType || filter.mediaType === "video" && entry.rawBid?.mediaTypes?.video !== void 0;
2213
- if (isEntryVideo) {
2214
- if (!bestFallbackMatch || entry.timestamp > bestFallbackMatch.timestamp) {
2215
- bestFallbackMatch = entry;
2216
- }
2217
- }
2218
- }
2219
- if (bestFallbackMatch) return bestFallbackMatch;
2220
- }
2221
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2222
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2223
- if (!isConsumed(entry)) {
2224
- if (!bestUnconsumedMatch || entry.timestamp > bestUnconsumedMatch.timestamp) {
2225
- bestUnconsumedMatch = entry;
2226
- }
2227
- }
2228
- }
2229
- if (bestUnconsumedMatch) return bestUnconsumedMatch;
2230
- let bestRecent;
2231
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2232
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2233
- if (!bestRecent || entry.timestamp > bestRecent.timestamp) {
2234
- bestRecent = entry;
2235
- }
2236
- }
2237
- return bestRecent;
2238
- }
2239
1982
  /** Records a dedup key, evicting oldest-first at the cap. */
2240
1983
  addImpressionKey(key) {
2241
1984
  if (this.slotEmittedImpressionKeys.has(key)) return;
@@ -2383,399 +2126,6 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2383
2126
  }
2384
2127
  return true;
2385
2128
  }
2386
- /**
2387
- * Bridges Google IMA SDK AdsManager events to Bidkernel analytics.
2388
- * Defers IMPRESSION emission until AdEvent.STARTED (or IMPRESSION),
2389
- * tracks milestones (FIRST_QUARTILE, MIDPOINT, THIRD_QUARTILE, COMPLETE),
2390
- * and handles AD_ERROR.
2391
- */
2392
- attachImaAdsManager(adsManager, options) {
2393
- if (!adsManager || typeof adsManager.addEventListener !== "function") {
2394
- this.log("WARN", "Invalid AdsManager passed to attachImaAdsManager");
2395
- return () => {
2396
- };
2397
- }
2398
- if (!adsManager.__bidkernelAttachedInstances) {
2399
- try {
2400
- Object.defineProperty(adsManager, "__bidkernelAttachedInstances", {
2401
- value: /* @__PURE__ */ new Set(),
2402
- configurable: true,
2403
- writable: true
2404
- });
2405
- } catch {
2406
- adsManager.__bidkernelAttachedInstances = /* @__PURE__ */ new Set();
2407
- }
2408
- }
2409
- if (adsManager.__bidkernelAttachedInstances.has(this)) {
2410
- return () => {
2411
- };
2412
- }
2413
- adsManager.__bidkernelAttachedInstances.add(this);
2414
- const slotId = options?.slotId || options?.adUnitCode || "video";
2415
- const adUnitCode = options?.adUnitCode || slotId;
2416
- const auctionId = options?.auctionId || "";
2417
- const transactionId = options?.transactionId || "";
2418
- const getWinningBid = (adData) => {
2419
- let cached = (auctionId ? this.getCachedBid(`${auctionId}:${adUnitCode}`) : void 0) || (auctionId && slotId !== adUnitCode ? this.getCachedBid(`${auctionId}:${slotId}`) : void 0) || (adUnitCode !== "video" ? this.getCachedBid(adUnitCode) : void 0) || (slotId !== adUnitCode && slotId !== "video" ? this.getCachedBid(slotId) : void 0);
2420
- if (!cached) {
2421
- cached = this.findCachedWinningBid({
2422
- creativeId: adData?.creativeId,
2423
- adId: adData?.adId,
2424
- mediaType: "video"
2425
- });
2426
- }
2427
- return cached;
2428
- };
2429
- let adStarted = false;
2430
- const onAdStartedOrImpression = (event) => {
2431
- adStarted = true;
2432
- this.log("DEBUG", "IMA AdEvent.STARTED / IMPRESSION received", event);
2433
- const ad = typeof event?.getAd === "function" ? event.getAd() : event?.ad;
2434
- const adData = {};
2435
- if (ad) {
2436
- adData.creativeId = (typeof ad.getCreativeId === "function" ? ad.getCreativeId() : ad.creativeId) || "";
2437
- adData.adId = (typeof ad.getAdId === "function" ? ad.getAdId() : ad.id) || "";
2438
- adData.title = (typeof ad.getTitle === "function" ? ad.getTitle() : ad.title) || "";
2439
- adData.duration = typeof ad.getDuration === "function" ? ad.getDuration() : ad.duration;
2440
- adData.advertiserName = typeof ad.getAdvertiserName === "function" ? ad.getAdvertiserName() : "";
2441
- }
2442
- const cached = getWinningBid(adData);
2443
- const resolvedSlotId = options?.slotId && options.slotId !== "video" ? options.slotId : options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || options?.slotId || options?.adUnitCode || "video";
2444
- const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || resolvedSlotId;
2445
- const resolvedAuctionId = auctionId || cached?.auctionId || "";
2446
- const resolvedTransactionId = transactionId || cached?.transactionId || "";
2447
- const mergedBid = {
2448
- ...cached?.rawBid || options?.bid,
2449
- mediaType: "video",
2450
- creativeId: adData.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || ""
2451
- };
2452
- const emitted = this.recordImpression(resolvedSlotId, {
2453
- adUnitCode: resolvedAdUnitCode,
2454
- auctionId: resolvedAuctionId,
2455
- transactionId: resolvedTransactionId,
2456
- mediaType: "video",
2457
- bid: mergedBid,
2458
- metadata: {
2459
- ...options?.metadata,
2460
- media_type: "video",
2461
- ...adData.title ? { ad_title: adData.title } : {},
2462
- ...Number.isFinite(adData.duration) ? { ad_duration_s: String(adData.duration) } : {}
2463
- }
2464
- });
2465
- if (emitted && options?.onImpression) {
2466
- try {
2467
- options.onImpression(resolvedSlotId, { ad, bid: mergedBid });
2468
- } catch (e) {
2469
- this.log("ERROR", "Error in onImpression callback", e);
2470
- }
2471
- }
2472
- };
2473
- const onMilestone = (milestone) => {
2474
- this.log("DEBUG", `IMA AdEvent milestone: ${milestone}`);
2475
- if (options?.onMilestone) {
2476
- try {
2477
- options.onMilestone(milestone, slotId);
2478
- } catch (e) {
2479
- this.log("ERROR", "Error in onMilestone callback", e);
2480
- }
2481
- }
2482
- };
2483
- const onAdClick = (event) => {
2484
- this.log("DEBUG", "IMA AdEvent.CLICK received", event);
2485
- const cached = getWinningBid();
2486
- const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || slotId;
2487
- this.enqueue(TraceEventType.CLICK, "click", {
2488
- auctionId: auctionId || cached?.auctionId,
2489
- transactionId: transactionId || cached?.transactionId,
2490
- adUnitCode: resolvedAdUnitCode,
2491
- bid: cached?.bidTrace || options?.bid,
2492
- metadata: {
2493
- ...options?.metadata,
2494
- media_type: "video"
2495
- }
2496
- });
2497
- };
2498
- const onAdError = (event) => {
2499
- this.log("WARN", "IMA AdErrorEvent received", event);
2500
- const err = typeof event?.getError === "function" ? event.getError() : event?.error || event;
2501
- const msg = (err && typeof err.getMessage === "function" ? err.getMessage() : err?.message) || String(err || "IMA ad error");
2502
- const code = (err && typeof err.getErrorCode === "function" ? err.getErrorCode() : err?.code) || "";
2503
- const cached = getWinningBid();
2504
- const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || slotId;
2505
- this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
2506
- auctionId: auctionId || cached?.auctionId,
2507
- transactionId: transactionId || cached?.transactionId,
2508
- adUnitCode: resolvedAdUnitCode,
2509
- bid: cached?.bidTrace || options?.bid,
2510
- metadata: {
2511
- ...options?.metadata,
2512
- reason: adStarted ? "ima_playback_error" : "ima_ad_error",
2513
- playback_phase: adStarted ? "post_start" : "pre_start",
2514
- message: msg,
2515
- ...code ? { ima_error_code: String(code) } : {}
2516
- },
2517
- error: typeof err === "object" ? err : new Error(msg)
2518
- });
2519
- if (options?.onError) {
2520
- try {
2521
- options.onError(err);
2522
- } catch (e) {
2523
- this.log("ERROR", "Error in onError callback", e);
2524
- }
2525
- }
2526
- };
2527
- const googleIma = typeof window !== "undefined" ? window.google?.ima : void 0;
2528
- const adEventType = googleIma?.AdEvent?.Type || {};
2529
- const adErrorEventType = googleIma?.AdErrorEvent?.Type || {};
2530
- const rawListeners = [
2531
- { type: adEventType.STARTED || "started", handler: onAdStartedOrImpression },
2532
- { type: adEventType.IMPRESSION || "impression", handler: onAdStartedOrImpression },
2533
- {
2534
- type: adEventType.FIRST_QUARTILE || "firstQuartile",
2535
- handler: () => onMilestone("firstQuartile")
2536
- },
2537
- { type: adEventType.MIDPOINT || "midpoint", handler: () => onMilestone("midpoint") },
2538
- {
2539
- type: adEventType.THIRD_QUARTILE || "thirdQuartile",
2540
- handler: () => onMilestone("thirdQuartile")
2541
- },
2542
- { type: adEventType.COMPLETE || "complete", handler: () => onMilestone("complete") },
2543
- { type: adEventType.CLICK || "click", handler: onAdClick },
2544
- { type: adErrorEventType.AD_ERROR || "adError", handler: onAdError }
2545
- ];
2546
- const seenListenerTypes = /* @__PURE__ */ new Set();
2547
- const listeners = [];
2548
- for (const l of rawListeners) {
2549
- if (!seenListenerTypes.has(l.type)) {
2550
- seenListenerTypes.add(l.type);
2551
- listeners.push(l);
2552
- }
2553
- }
2554
- for (const { type, handler } of listeners) {
2555
- try {
2556
- adsManager.addEventListener(type, handler);
2557
- } catch {
2558
- }
2559
- }
2560
- const cleanup = () => {
2561
- adsManager.__bidkernelAttachedInstances?.delete(this);
2562
- for (const { type, handler } of listeners) {
2563
- try {
2564
- adsManager.removeEventListener(type, handler);
2565
- } catch {
2566
- }
2567
- }
2568
- this.videoDetachCleanups.delete(cleanup);
2569
- };
2570
- this.videoDetachCleanups.add(cleanup);
2571
- return cleanup;
2572
- }
2573
- /**
2574
- * Attaches render hooks and viewability tracking to a video player.
2575
- * Supports HTML5 <video> elements, container elements, or Google IMA AdsManager.
2576
- */
2577
- attachVideoPlayer(target, options) {
2578
- if (!target) {
2579
- this.log("WARN", "attachVideoPlayer called with null/undefined target");
2580
- return () => {
2581
- };
2582
- }
2583
- let el = null;
2584
- if (typeof target === "string") {
2585
- if (typeof document !== "undefined") {
2586
- try {
2587
- el = document.querySelector(target) || document.getElementById(target);
2588
- } catch {
2589
- el = document.getElementById(target);
2590
- }
2591
- }
2592
- } else if (typeof HTMLElement !== "undefined" && target instanceof HTMLElement) {
2593
- el = target;
2594
- } else if (target && typeof target === "object" && target.nodeType === 1) {
2595
- el = target;
2596
- } else if (
2597
- // Check if target is an IMA AdsManager
2598
- typeof target.addEventListener === "function" && (typeof target.init === "function" || typeof target.start === "function" || typeof target.getCuePoints === "function" || typeof target.getVolume === "function" || target.__imaAdsManager)
2599
- ) {
2600
- return this.attachImaAdsManager(target, options);
2601
- } else if (target && typeof target.addEventListener === "function") {
2602
- el = target;
2603
- }
2604
- const slotId = options?.slotId || options?.adUnitCode || (el ? el.id : "") || "video";
2605
- const adUnitCode = options?.adUnitCode || slotId;
2606
- const auctionId = options?.auctionId || "";
2607
- const transactionId = options?.transactionId || "";
2608
- const getWinningBid = () => {
2609
- return (auctionId ? this.getCachedBid(`${auctionId}:${adUnitCode}`) : void 0) || (auctionId && slotId !== adUnitCode ? this.getCachedBid(`${auctionId}:${slotId}`) : void 0) || this.getCachedBid(adUnitCode) || (slotId !== adUnitCode ? this.getCachedBid(slotId) : void 0);
2610
- };
2611
- let videoEl = null;
2612
- if (el) {
2613
- if (el.tagName && el.tagName.toLowerCase() === "video") {
2614
- videoEl = el;
2615
- } else {
2616
- videoEl = el.querySelector("video");
2617
- }
2618
- }
2619
- const quartilesFired = {
2620
- q1: false,
2621
- q2: false,
2622
- q3: false,
2623
- q4: false
2624
- };
2625
- const triggerPlaybackImpression = (activeVideo) => {
2626
- const vEl = activeVideo || videoEl || (el?.querySelector ? el.querySelector("video") : null);
2627
- const cached = getWinningBid();
2628
- const mergedBid = {
2629
- ...cached?.rawBid || options?.bid,
2630
- mediaType: "video"
2631
- };
2632
- const emitted = this.recordImpression(slotId, {
2633
- adUnitCode,
2634
- auctionId: auctionId || cached?.auctionId,
2635
- transactionId: transactionId || cached?.transactionId,
2636
- mediaType: "video",
2637
- bid: mergedBid,
2638
- metadata: {
2639
- ...options?.metadata,
2640
- media_type: "video",
2641
- ...vEl && Number.isFinite(vEl.duration) ? { video_duration_s: String(Math.round(vEl.duration)) } : {}
2642
- }
2643
- });
2644
- if (emitted && options?.onImpression) {
2645
- try {
2646
- options.onImpression(slotId, { element: vEl || el, bid: mergedBid });
2647
- } catch (e) {
2648
- this.log("ERROR", "Error in onImpression callback", e);
2649
- }
2650
- }
2651
- };
2652
- const handlePlaying = (e) => {
2653
- this.log("DEBUG", "HTML5 video playing event received");
2654
- const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2655
- triggerPlaybackImpression(targetVideo);
2656
- };
2657
- const handleTimeUpdate = (e) => {
2658
- const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2659
- if (targetVideo && targetVideo.currentTime > 0) {
2660
- triggerPlaybackImpression(targetVideo);
2661
- if (Number.isFinite(targetVideo.duration) && targetVideo.duration > 0) {
2662
- const progress = targetVideo.currentTime / targetVideo.duration;
2663
- if (progress >= 0.25 && !quartilesFired.q1) {
2664
- quartilesFired.q1 = true;
2665
- options?.onMilestone?.("firstQuartile", slotId);
2666
- }
2667
- if (progress >= 0.5 && !quartilesFired.q2) {
2668
- quartilesFired.q2 = true;
2669
- options?.onMilestone?.("midpoint", slotId);
2670
- }
2671
- if (progress >= 0.75 && !quartilesFired.q3) {
2672
- quartilesFired.q3 = true;
2673
- options?.onMilestone?.("thirdQuartile", slotId);
2674
- }
2675
- }
2676
- }
2677
- };
2678
- const handleEnded = () => {
2679
- if (!quartilesFired.q4) {
2680
- quartilesFired.q4 = true;
2681
- options?.onMilestone?.("complete", slotId);
2682
- }
2683
- };
2684
- const handleError = (e) => {
2685
- const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2686
- const err = targetVideo?.error || videoEl?.error;
2687
- const cached = getWinningBid();
2688
- const msg = err ? `HTML5 video error code ${err.code}: ${err.message}` : "Video playback error";
2689
- this.log("WARN", msg);
2690
- this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
2691
- auctionId: auctionId || cached?.auctionId,
2692
- transactionId: transactionId || cached?.transactionId,
2693
- adUnitCode,
2694
- bid: cached?.bidTrace || options?.bid,
2695
- metadata: {
2696
- ...options?.metadata,
2697
- reason: "html5_video_error",
2698
- message: msg,
2699
- ...err?.code ? { video_error_code: String(err.code) } : {}
2700
- },
2701
- error: err ? new Error(msg) : void 0
2702
- });
2703
- if (options?.onError) {
2704
- try {
2705
- options.onError(err);
2706
- } catch (e2) {
2707
- this.log("ERROR", "Error in onError callback", e2);
2708
- }
2709
- }
2710
- };
2711
- const attachListenersToVideo = (targetVideo) => {
2712
- targetVideo.addEventListener("playing", handlePlaying);
2713
- targetVideo.addEventListener("play", handlePlaying);
2714
- targetVideo.addEventListener("timeupdate", handleTimeUpdate);
2715
- targetVideo.addEventListener("ended", handleEnded);
2716
- targetVideo.addEventListener("error", handleError);
2717
- };
2718
- const removeListenersFromVideo = (targetVideo) => {
2719
- targetVideo.removeEventListener("playing", handlePlaying);
2720
- targetVideo.removeEventListener("play", handlePlaying);
2721
- targetVideo.removeEventListener("timeupdate", handleTimeUpdate);
2722
- targetVideo.removeEventListener("ended", handleEnded);
2723
- targetVideo.removeEventListener("error", handleError);
2724
- };
2725
- let mutationObserver = null;
2726
- if (el && el !== videoEl) {
2727
- el.addEventListener("playing", handlePlaying, true);
2728
- el.addEventListener("play", handlePlaying, true);
2729
- el.addEventListener("timeupdate", handleTimeUpdate, true);
2730
- el.addEventListener("ended", handleEnded, true);
2731
- el.addEventListener("error", handleError, true);
2732
- }
2733
- if (videoEl) {
2734
- attachListenersToVideo(videoEl);
2735
- }
2736
- if (el && el.tagName?.toLowerCase() !== "video" && typeof MutationObserver !== "undefined") {
2737
- mutationObserver = new MutationObserver(() => {
2738
- const found = el.querySelector("video");
2739
- if (found && found !== videoEl) {
2740
- if (videoEl) removeListenersFromVideo(videoEl);
2741
- videoEl = found;
2742
- if (videoEl) attachListenersToVideo(videoEl);
2743
- }
2744
- });
2745
- mutationObserver.observe(el, { childList: true, subtree: true });
2746
- }
2747
- const cachedInitial = getWinningBid();
2748
- if (options?.trackViewability !== false && el) {
2749
- this.observeSlot(el, slotId, {
2750
- adUnitCode,
2751
- auctionId: auctionId || cachedInitial?.auctionId,
2752
- transactionId: transactionId || cachedInitial?.transactionId,
2753
- mediaType: "video",
2754
- bid: cachedInitial?.bidTrace || options?.bid,
2755
- metadata: options?.metadata,
2756
- emitRefreshEvent: false
2757
- });
2758
- }
2759
- const cleanup = () => {
2760
- if (mutationObserver) {
2761
- mutationObserver.disconnect();
2762
- mutationObserver = null;
2763
- }
2764
- if (el && el !== videoEl) {
2765
- el.removeEventListener("playing", handlePlaying, true);
2766
- el.removeEventListener("play", handlePlaying, true);
2767
- el.removeEventListener("timeupdate", handleTimeUpdate, true);
2768
- el.removeEventListener("ended", handleEnded, true);
2769
- el.removeEventListener("error", handleError, true);
2770
- }
2771
- if (videoEl) {
2772
- removeListenersFromVideo(videoEl);
2773
- }
2774
- this.videoDetachCleanups.delete(cleanup);
2775
- };
2776
- this.videoDetachCleanups.add(cleanup);
2777
- return cleanup;
2778
- }
2779
2129
  onTimeInViewThreshold(slotId, thresholdMs, callback) {
2780
2130
  const listener = {
2781
2131
  thresholdMs,
@@ -3194,9 +2544,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
3194
2544
  );
3195
2545
  }
3196
2546
  if (this.config.viewabilityEnabled && typeof document !== "undefined") {
3197
- let el = null;
2547
+ let el = adUnitCode && this.slotElements.get(adUnitCode) || null;
3198
2548
  const targetId = adUnitCode || data.adId || bid.adId;
3199
- if (targetId) {
2549
+ if (!el && targetId) {
3200
2550
  el = document.getElementById(targetId);
3201
2551
  }
3202
2552
  if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
@@ -3367,313 +2717,1034 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
3367
2717
  };
3368
2718
  }
3369
2719
  }
3370
- sendFetch(payload, useKeepalive = false, onDelivered, isReplay = false) {
3371
- const fetchOpts = {
3372
- method: "POST",
3373
- // Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
3374
- headers: { "Content-Type": SAFE_CONTENT_TYPE },
3375
- body: payload.encoded
3376
- };
3377
- if (useKeepalive) {
3378
- fetchOpts.keepalive = true;
2720
+ sendFetch(payload, useKeepalive = false, onDelivered, isReplay = false) {
2721
+ const fetchOpts = {
2722
+ method: "POST",
2723
+ // Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
2724
+ headers: { "Content-Type": SAFE_CONTENT_TYPE },
2725
+ body: payload.encoded
2726
+ };
2727
+ if (useKeepalive) {
2728
+ fetchOpts.keepalive = true;
2729
+ }
2730
+ fetch(payload.url, fetchOpts).then((res) => {
2731
+ if (!res.ok) {
2732
+ this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
2733
+ if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 408) {
2734
+ this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
2735
+ return;
2736
+ }
2737
+ if (!isReplay) this.handleSendFailure(payload.events);
2738
+ } else {
2739
+ if (!isReplay) {
2740
+ this.consecutiveSendFailures = 0;
2741
+ this.nextSendAllowedAt = 0;
2742
+ }
2743
+ if (onDelivered) onDelivered();
2744
+ }
2745
+ }).catch((err) => {
2746
+ this.log("ERROR", "Failed to send batch", err);
2747
+ if (!isReplay) this.handleSendFailure(payload.events);
2748
+ });
2749
+ const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
2750
+ if (proc && typeof proc._tickCallback === "function") {
2751
+ try {
2752
+ proc._tickCallback();
2753
+ } catch {
2754
+ }
2755
+ }
2756
+ }
2757
+ handleSendFailure(events) {
2758
+ this.consecutiveSendFailures++;
2759
+ const backoff = Math.min(
2760
+ FLUSH_INTERVAL_MS * 2 ** (this.consecutiveSendFailures - 1),
2761
+ MAX_SEND_BACKOFF_MS
2762
+ );
2763
+ this.nextSendAllowedAt = Date.now() + backoff;
2764
+ if (this.consecutiveSendFailures <= MAX_CONSECUTIVE_SEND_FAILURES) {
2765
+ this.requeue(events);
2766
+ } else {
2767
+ this.log(
2768
+ "WARN",
2769
+ `Dropping ${events.length} events after ${this.consecutiveSendFailures} consecutive send failures`
2770
+ );
2771
+ }
2772
+ }
2773
+ flush() {
2774
+ if (Date.now() < this.nextSendAllowedAt) return;
2775
+ let payload = this.drainBatch();
2776
+ while (payload) {
2777
+ this.sendFetch(payload, false);
2778
+ if (Date.now() < this.nextSendAllowedAt) break;
2779
+ payload = this.drainBatch();
2780
+ }
2781
+ }
2782
+ flushBeacon() {
2783
+ let payload = this.drainBatch();
2784
+ while (payload) {
2785
+ let sent = false;
2786
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
2787
+ try {
2788
+ const blob = new Blob([payload.encoded], { type: SAFE_CONTENT_TYPE });
2789
+ sent = navigator.sendBeacon(payload.url, blob);
2790
+ } catch {
2791
+ sent = false;
2792
+ }
2793
+ }
2794
+ if (!sent) {
2795
+ const persistKey = this.persistBatch(payload.encoded);
2796
+ this.sendFetch(payload, true, () => this.removePersistedBatch(persistKey));
2797
+ }
2798
+ payload = this.drainBatch();
2799
+ }
2800
+ this.schedulePersistedCleanup();
2801
+ }
2802
+ // --- Exit-batch persistence -----------------------------------------------
2803
+ // Key prefix for this instance's property. encodeURIComponent keeps the
2804
+ // property segment free of the "_" the timestamp suffix is parsed against,
2805
+ // so a property ID containing one cannot forge another property's prefix.
2806
+ pendingKeyPrefix() {
2807
+ return `${PENDING_BATCH_KEY_PREFIX}${encodeURIComponent(this.config.propertyId)}_`;
2808
+ }
2809
+ persistBatch(encoded) {
2810
+ if (!storageAllowed) return null;
2811
+ try {
2812
+ if (typeof localStorage === "undefined") return null;
2813
+ if (!this.config.propertyId) return null;
2814
+ let pendingCount = 0;
2815
+ for (let i = 0; i < localStorage.length; i++) {
2816
+ const k = localStorage.key(i);
2817
+ if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
2818
+ }
2819
+ if (pendingCount >= MAX_PENDING_BATCHES) return null;
2820
+ const key = `${this.pendingKeyPrefix()}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
2821
+ localStorage.setItem(key, bytesToBase64(encoded));
2822
+ this.persistedBatchKeys.push(key);
2823
+ return key;
2824
+ } catch {
2825
+ return null;
2826
+ }
2827
+ }
2828
+ removePersistedBatch(key) {
2829
+ if (!key) return;
2830
+ try {
2831
+ if (typeof localStorage !== "undefined") localStorage.removeItem(key);
2832
+ } catch {
2833
+ }
2834
+ const idx = this.persistedBatchKeys.indexOf(key);
2835
+ if (idx !== -1) this.persistedBatchKeys.splice(idx, 1);
2836
+ }
2837
+ // A page that is still running PENDING_BATCH_CLEANUP_DELAY_MS after an exit
2838
+ // flush was never torn down, so its beacons have long since gone out; drop
2839
+ // the persisted copies rather than letting a later pageview resend them.
2840
+ schedulePersistedCleanup() {
2841
+ if (this.persistedBatchKeys.length === 0) return;
2842
+ if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
2843
+ this.persistedCleanupTimer = setTimeout(() => {
2844
+ this.persistedCleanupTimer = null;
2845
+ for (const key of Array.from(this.persistedBatchKeys)) {
2846
+ this.removePersistedBatch(key);
2847
+ }
2848
+ }, PENDING_BATCH_CLEANUP_DELAY_MS);
2849
+ }
2850
+ // Resend batches a previous pageview persisted at exit but could not
2851
+ // confirm. Only this property's own keys are claimed: batches belonging to
2852
+ // another property sharing this origin are left for that property's next
2853
+ // pageview, since resending them here would file them under this property.
2854
+ // Keys are claimed (removed) before sending so concurrent tabs don't
2855
+ // double-send; the batch bytes carry their original pageview, session, and
2856
+ // event timestamps, so late rows attribute correctly.
2857
+ resendPersistedBatches() {
2858
+ try {
2859
+ if (typeof localStorage === "undefined" || !this.config.endpoint) return;
2860
+ if (!this.config.propertyId) return;
2861
+ const ownPrefix = this.pendingKeyPrefix();
2862
+ const keys = [];
2863
+ const legacyKeys = [];
2864
+ for (let i = 0; i < localStorage.length; i++) {
2865
+ const k = localStorage.key(i);
2866
+ if (!k || !k.startsWith(PENDING_BATCH_KEY_PREFIX)) continue;
2867
+ if (k.startsWith(ownPrefix)) {
2868
+ keys.push(k);
2869
+ } else if (isLegacyPendingKey(k)) {
2870
+ legacyKeys.push(k);
2871
+ }
2872
+ }
2873
+ for (const key of legacyKeys) {
2874
+ try {
2875
+ localStorage.removeItem(key);
2876
+ } catch {
2877
+ }
2878
+ }
2879
+ for (const key of keys) {
2880
+ let value = null;
2881
+ try {
2882
+ value = localStorage.getItem(key);
2883
+ localStorage.removeItem(key);
2884
+ } catch {
2885
+ continue;
2886
+ }
2887
+ if (!value) continue;
2888
+ const ts = parseInt(key.slice(ownPrefix.length), 10);
2889
+ if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
2890
+ let encoded;
2891
+ try {
2892
+ encoded = base64ToBytes(value);
2893
+ } catch {
2894
+ continue;
2895
+ }
2896
+ if (encoded.byteLength === 0) continue;
2897
+ this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
2898
+ this.sendFetch(
2899
+ {
2900
+ url: `${this.config.endpoint}/${this.config.propertyId}`,
2901
+ encoded,
2902
+ events: []
2903
+ },
2904
+ false,
2905
+ void 0,
2906
+ true
2907
+ );
2908
+ }
2909
+ } catch {
2910
+ }
2911
+ }
2912
+ log(level, msg, ...args) {
2913
+ this.logger.log(level, msg, ...args);
2914
+ }
2915
+ };
2916
+ var defaultLogger = createLogger({
2917
+ prefix: "[bidkernel]",
2918
+ logLevel: "INFO"
2919
+ });
2920
+ function extractAnalyticsOptions(config, allowProviderless = true) {
2921
+ if (!config) return null;
2922
+ if (Array.isArray(config)) {
2923
+ const matched = config.find((item) => item?.provider === "bidkernel") ?? (allowProviderless ? config.find(
2924
+ (item) => item && typeof item === "object" && item.provider === void 0
2925
+ ) : void 0);
2926
+ return matched ? extractAnalyticsOptions(matched, allowProviderless) : null;
2927
+ }
2928
+ if (typeof config === "object") {
2929
+ if (config.options && typeof config.options === "object") {
2930
+ return config.options;
2931
+ }
2932
+ if (config.config && typeof config.config === "object") {
2933
+ return config.config;
2934
+ }
2935
+ if (config.propertyId || config.endpoint) {
2936
+ return config;
2937
+ }
2938
+ }
2939
+ return null;
2940
+ }
2941
+ function getStashedConfig(win) {
2942
+ if (!win) return null;
2943
+ if (win._bidkernelAnalyticsConfig) {
2944
+ const extracted = extractAnalyticsOptions(win._bidkernelAnalyticsConfig);
2945
+ if (extracted) return extracted;
2946
+ }
2947
+ if (win.bidkernelPrebidAnalytics) {
2948
+ const bpa = win.bidkernelPrebidAnalytics;
2949
+ if (bpa.options) {
2950
+ const extracted = extractAnalyticsOptions(bpa.options);
2951
+ if (extracted) return extracted;
2952
+ }
2953
+ if (bpa.config) {
2954
+ const extracted = extractAnalyticsOptions(bpa.config);
2955
+ if (extracted) return extracted;
2956
+ }
2957
+ if (bpa.propertyId) {
2958
+ return extractAnalyticsOptions(bpa);
2959
+ }
2960
+ }
2961
+ return null;
2962
+ }
2963
+ function registerPrebidAnalytics(pbjsGlobalName = "pbjs", defaults) {
2964
+ const win = typeof window !== "undefined" ? window : {};
2965
+ win[pbjsGlobalName] = win[pbjsGlobalName] || {};
2966
+ const pbjs = win[pbjsGlobalName];
2967
+ const enableInstance = (options) => {
2968
+ const previous = win._bidkernelPrebidAnalytics;
2969
+ const pinnedEndpoint = previous?.getEndpoint?.() || "";
2970
+ const merged = {
2971
+ pbjsGlobalName,
2972
+ ...defaults,
2973
+ ...options
2974
+ };
2975
+ if (pinnedEndpoint && !sameEndpointOrigin(merged.endpoint || "", pinnedEndpoint)) {
2976
+ defaultLogger.warn(
2977
+ `Ignoring analytics endpoint change to ${merged.endpoint || "(empty)"}: this page is pinned to ${pinnedEndpoint}. Reload to change the ingest endpoint.`
2978
+ );
2979
+ merged.endpoint = pinnedEndpoint;
2980
+ }
2981
+ if (previous) {
2982
+ try {
2983
+ previous.disable();
2984
+ } catch (e) {
2985
+ defaultLogger.warn("Failed to disable previous analytics instance:", e);
2986
+ }
2987
+ }
2988
+ const analytics = new BidkernelPrebidAnalytics(merged);
2989
+ analytics.enable();
2990
+ win._bidkernelPrebidAnalytics = analytics;
2991
+ return analytics;
2992
+ };
2993
+ const doRegister = () => {
2994
+ try {
2995
+ if (pbjs.adapterManager && typeof pbjs.adapterManager.registerAnalyticsAdapter === "function") {
2996
+ pbjs.adapterManager.registerAnalyticsAdapter({
2997
+ adapter: {
2998
+ enableAnalytics: (config) => {
2999
+ const options = extractAnalyticsOptions(config, !Array.isArray(config));
3000
+ if (options) {
3001
+ enableInstance(options);
3002
+ }
3003
+ },
3004
+ disableAnalytics: () => {
3005
+ if (win._bidkernelPrebidAnalytics) {
3006
+ win._bidkernelPrebidAnalytics.disable();
3007
+ }
3008
+ }
3009
+ },
3010
+ code: "bidkernel"
3011
+ });
3012
+ }
3013
+ const stashed = getStashedConfig(win);
3014
+ if (stashed && (stashed.propertyId || defaults?.propertyId)) {
3015
+ enableInstance(stashed);
3016
+ }
3017
+ } catch (e) {
3018
+ defaultLogger.warn("Failed to register standard Prebid analytics adapter:", e);
3019
+ }
3020
+ };
3021
+ if (pbjs.adapterManager && typeof pbjs.adapterManager.registerAnalyticsAdapter === "function") {
3022
+ doRegister();
3023
+ } else {
3024
+ pbjs.que = pbjs.que || [];
3025
+ pbjs.que.push(doRegister);
3026
+ }
3027
+ }
3028
+
3029
+ // src/video.ts
3030
+ var internals = (analytics) => analytics;
3031
+ var adsLoaderListenerMap = /* @__PURE__ */ new WeakMap();
3032
+ function hookAdsManagerLoadedEvent(loadedEventCtor) {
3033
+ if (!loadedEventCtor) return;
3034
+ try {
3035
+ const loadedProto = loadedEventCtor.prototype;
3036
+ if (loadedProto && typeof loadedProto.getAdsManager === "function" && !loadedProto.getAdsManager.__bidkernelHooked) {
3037
+ const originalGetAdsManager = loadedProto.getAdsManager;
3038
+ const hookedGetAdsManager = function(...args) {
3039
+ const adsManager = originalGetAdsManager.apply(this, args);
3040
+ if (adsManager) {
3041
+ for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
3042
+ try {
3043
+ instance.attachImaAdsManager(adsManager);
3044
+ } catch {
3045
+ }
3046
+ }
3047
+ }
3048
+ return adsManager;
3049
+ };
3050
+ hookedGetAdsManager.__bidkernelHooked = true;
3051
+ loadedProto.getAdsManager = hookedGetAdsManager;
3052
+ }
3053
+ } catch {
3054
+ }
3055
+ }
3056
+ function hookAdsManagerPrototype(adsManagerCtorOrProto) {
3057
+ if (!adsManagerCtorOrProto) return;
3058
+ try {
3059
+ const proto = adsManagerCtorOrProto.prototype || adsManagerCtorOrProto;
3060
+ if (proto && typeof proto.init === "function" && !proto.init.__bidkernelHooked) {
3061
+ const origInit = proto.init;
3062
+ const hookedInit = function(...args) {
3063
+ for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
3064
+ try {
3065
+ instance.attachImaAdsManager(this);
3066
+ } catch {
3067
+ }
3068
+ }
3069
+ return origInit.apply(this, args);
3070
+ };
3071
+ hookedInit.__bidkernelHooked = true;
3072
+ proto.init = hookedInit;
3073
+ }
3074
+ if (proto && typeof proto.start === "function" && !proto.start.__bidkernelHooked) {
3075
+ const origStart = proto.start;
3076
+ const hookedStart = function(...args) {
3077
+ for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
3078
+ try {
3079
+ instance.attachImaAdsManager(this);
3080
+ } catch {
3081
+ }
3082
+ }
3083
+ return origStart.apply(this, args);
3084
+ };
3085
+ hookedStart.__bidkernelHooked = true;
3086
+ proto.start = hookedStart;
3087
+ }
3088
+ } catch {
3089
+ }
3090
+ }
3091
+ function hookAdsLoader(adsLoaderCtor) {
3092
+ if (!adsLoaderCtor) return;
3093
+ try {
3094
+ const proto = adsLoaderCtor.prototype;
3095
+ if (!proto) return;
3096
+ if (typeof proto.addEventListener === "function" && !proto.addEventListener.__bidkernelHooked) {
3097
+ const origAddEventListener = proto.addEventListener;
3098
+ const hookedAddEventListener = function(type, listener, ...rest) {
3099
+ if ((type === "adsManagerLoaded" || type === (this?.AdsManagerLoadedEvent?.Type?.ADS_MANAGER_LOADED || "adsManagerLoaded")) && typeof listener === "function") {
3100
+ let wrappedListener = adsLoaderListenerMap.get(listener);
3101
+ if (!wrappedListener) {
3102
+ wrappedListener = function(event) {
3103
+ if (event && typeof event.getAdsManager === "function" && !event.getAdsManager.__bidkernelHooked) {
3104
+ const origGetAdsManager = event.getAdsManager;
3105
+ event.getAdsManager = function(...args) {
3106
+ const adsManager = origGetAdsManager.apply(this, args);
3107
+ if (adsManager) {
3108
+ for (const sdk of BidkernelPrebidAnalytics.getActiveInstances()) {
3109
+ try {
3110
+ sdk.attachImaAdsManager(adsManager);
3111
+ } catch {
3112
+ }
3113
+ }
3114
+ }
3115
+ return adsManager;
3116
+ };
3117
+ event.getAdsManager.__bidkernelHooked = true;
3118
+ }
3119
+ return listener.apply(this, arguments);
3120
+ };
3121
+ adsLoaderListenerMap.set(listener, wrappedListener);
3122
+ }
3123
+ return origAddEventListener.call(this, type, wrappedListener, ...rest);
3124
+ }
3125
+ return origAddEventListener.call(this, type, listener, ...rest);
3126
+ };
3127
+ hookedAddEventListener.__bidkernelHooked = true;
3128
+ proto.addEventListener = hookedAddEventListener;
3129
+ }
3130
+ if (typeof proto.removeEventListener === "function" && !proto.removeEventListener.__bidkernelHooked) {
3131
+ const origRemoveEventListener = proto.removeEventListener;
3132
+ const hookedRemoveEventListener = function(type, listener, ...rest) {
3133
+ const wrapped = typeof listener === "function" ? adsLoaderListenerMap.get(listener) : void 0;
3134
+ return origRemoveEventListener.call(this, type, wrapped || listener, ...rest);
3135
+ };
3136
+ hookedRemoveEventListener.__bidkernelHooked = true;
3137
+ proto.removeEventListener = hookedRemoveEventListener;
3138
+ }
3139
+ } catch {
3140
+ }
3141
+ }
3142
+ function hookImaPrototype(ima) {
3143
+ if (!ima || typeof ima !== "object") return;
3144
+ try {
3145
+ if (ima.AdsManagerLoadedEvent) {
3146
+ hookAdsManagerLoadedEvent(ima.AdsManagerLoadedEvent);
3379
3147
  }
3380
- fetch(payload.url, fetchOpts).then((res) => {
3381
- if (!res.ok) {
3382
- this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
3383
- if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 408) {
3384
- this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
3385
- return;
3386
- }
3387
- if (!isReplay) this.handleSendFailure(payload.events);
3388
- } else {
3389
- if (!isReplay) {
3390
- this.consecutiveSendFailures = 0;
3391
- this.nextSendAllowedAt = 0;
3392
- }
3393
- if (onDelivered) onDelivered();
3394
- }
3395
- }).catch((err) => {
3396
- this.log("ERROR", "Failed to send batch", err);
3397
- if (!isReplay) this.handleSendFailure(payload.events);
3398
- });
3399
- const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
3400
- if (proc && typeof proc._tickCallback === "function") {
3148
+ const loadedDesc = Object.getOwnPropertyDescriptor(ima, "AdsManagerLoadedEvent");
3149
+ if (!loadedDesc || loadedDesc.configurable) {
3150
+ let _loadedEvent = ima.AdsManagerLoadedEvent;
3401
3151
  try {
3402
- proc._tickCallback();
3152
+ Object.defineProperty(ima, "AdsManagerLoadedEvent", {
3153
+ configurable: true,
3154
+ enumerable: true,
3155
+ get() {
3156
+ return _loadedEvent;
3157
+ },
3158
+ set(val) {
3159
+ _loadedEvent = val;
3160
+ hookAdsManagerLoadedEvent(val);
3161
+ }
3162
+ });
3403
3163
  } catch {
3404
3164
  }
3405
3165
  }
3406
- }
3407
- handleSendFailure(events) {
3408
- this.consecutiveSendFailures++;
3409
- const backoff = Math.min(
3410
- FLUSH_INTERVAL_MS * 2 ** (this.consecutiveSendFailures - 1),
3411
- MAX_SEND_BACKOFF_MS
3412
- );
3413
- this.nextSendAllowedAt = Date.now() + backoff;
3414
- if (this.consecutiveSendFailures <= MAX_CONSECUTIVE_SEND_FAILURES) {
3415
- this.requeue(events);
3416
- } else {
3417
- this.log(
3418
- "WARN",
3419
- `Dropping ${events.length} events after ${this.consecutiveSendFailures} consecutive send failures`
3420
- );
3421
- }
3422
- }
3423
- flush() {
3424
- if (Date.now() < this.nextSendAllowedAt) return;
3425
- let payload = this.drainBatch();
3426
- while (payload) {
3427
- this.sendFetch(payload, false);
3428
- if (Date.now() < this.nextSendAllowedAt) break;
3429
- payload = this.drainBatch();
3166
+ if (ima.AdsLoader) {
3167
+ hookAdsLoader(ima.AdsLoader);
3430
3168
  }
3431
- }
3432
- flushBeacon() {
3433
- let payload = this.drainBatch();
3434
- while (payload) {
3435
- let sent = false;
3436
- if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
3437
- try {
3438
- const blob = new Blob([payload.encoded], { type: SAFE_CONTENT_TYPE });
3439
- sent = navigator.sendBeacon(payload.url, blob);
3440
- } catch {
3441
- sent = false;
3442
- }
3169
+ const loaderDesc = Object.getOwnPropertyDescriptor(ima, "AdsLoader");
3170
+ if (!loaderDesc || loaderDesc.configurable) {
3171
+ let _loader = ima.AdsLoader;
3172
+ try {
3173
+ Object.defineProperty(ima, "AdsLoader", {
3174
+ configurable: true,
3175
+ enumerable: true,
3176
+ get() {
3177
+ return _loader;
3178
+ },
3179
+ set(val) {
3180
+ _loader = val;
3181
+ hookAdsLoader(val);
3182
+ }
3183
+ });
3184
+ } catch {
3443
3185
  }
3444
- if (!sent) {
3445
- const persistKey = this.persistBatch(payload.encoded);
3446
- this.sendFetch(payload, true, () => this.removePersistedBatch(persistKey));
3186
+ }
3187
+ if (ima.AdsManager) {
3188
+ hookAdsManagerPrototype(ima.AdsManager);
3189
+ }
3190
+ const mgrDesc = Object.getOwnPropertyDescriptor(ima, "AdsManager");
3191
+ if (!mgrDesc || mgrDesc.configurable) {
3192
+ let _mgr = ima.AdsManager;
3193
+ try {
3194
+ Object.defineProperty(ima, "AdsManager", {
3195
+ configurable: true,
3196
+ enumerable: true,
3197
+ get() {
3198
+ return _mgr;
3199
+ },
3200
+ set(val) {
3201
+ _mgr = val;
3202
+ hookAdsManagerPrototype(val);
3203
+ }
3204
+ });
3205
+ } catch {
3447
3206
  }
3448
- payload = this.drainBatch();
3449
3207
  }
3450
- this.schedulePersistedCleanup();
3208
+ } catch {
3451
3209
  }
3452
- // --- Exit-batch persistence -----------------------------------------------
3453
- // Key prefix for this instance's property. encodeURIComponent keeps the
3454
- // property segment free of the "_" the timestamp suffix is parsed against,
3455
- // so a property ID containing one cannot forge another property's prefix.
3456
- pendingKeyPrefix() {
3457
- return `${PENDING_BATCH_KEY_PREFIX}${encodeURIComponent(this.config.propertyId)}_`;
3210
+ }
3211
+ function initImaInterception() {
3212
+ if (typeof window === "undefined") return;
3213
+ const win = window;
3214
+ if (win.google?.ima) {
3215
+ hookImaPrototype(win.google.ima);
3458
3216
  }
3459
- persistBatch(encoded) {
3460
- try {
3461
- if (typeof localStorage === "undefined") return null;
3462
- if (!this.config.propertyId) return null;
3463
- let pendingCount = 0;
3464
- for (let i = 0; i < localStorage.length; i++) {
3465
- const k = localStorage.key(i);
3466
- if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
3217
+ if (win.google && typeof win.google === "object") {
3218
+ const imaDesc = Object.getOwnPropertyDescriptor(win.google, "ima");
3219
+ if (!imaDesc || imaDesc.configurable) {
3220
+ let _ima = win.google.ima;
3221
+ try {
3222
+ Object.defineProperty(win.google, "ima", {
3223
+ configurable: true,
3224
+ enumerable: true,
3225
+ get() {
3226
+ return _ima;
3227
+ },
3228
+ set(val) {
3229
+ _ima = val;
3230
+ hookImaPrototype(val);
3231
+ }
3232
+ });
3233
+ } catch {
3467
3234
  }
3468
- if (pendingCount >= MAX_PENDING_BATCHES) return null;
3469
- const key = `${this.pendingKeyPrefix()}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
3470
- localStorage.setItem(key, bytesToBase64(encoded));
3471
- this.persistedBatchKeys.push(key);
3472
- return key;
3473
- } catch {
3474
- return null;
3475
3235
  }
3476
3236
  }
3477
- removePersistedBatch(key) {
3478
- if (!key) return;
3237
+ const googleDesc = Object.getOwnPropertyDescriptor(win, "google");
3238
+ if (!googleDesc || googleDesc.configurable) {
3239
+ let _google = win.google;
3479
3240
  try {
3480
- if (typeof localStorage !== "undefined") localStorage.removeItem(key);
3241
+ Object.defineProperty(win, "google", {
3242
+ configurable: true,
3243
+ enumerable: true,
3244
+ get() {
3245
+ return _google;
3246
+ },
3247
+ set(val) {
3248
+ _google = val;
3249
+ if (_google && typeof _google === "object") {
3250
+ if (_google.ima) {
3251
+ hookImaPrototype(_google.ima);
3252
+ }
3253
+ const iDesc = Object.getOwnPropertyDescriptor(_google, "ima");
3254
+ if (!iDesc || iDesc.configurable) {
3255
+ let _i = _google.ima;
3256
+ try {
3257
+ Object.defineProperty(_google, "ima", {
3258
+ configurable: true,
3259
+ enumerable: true,
3260
+ get() {
3261
+ return _i;
3262
+ },
3263
+ set(iv) {
3264
+ _i = iv;
3265
+ hookImaPrototype(iv);
3266
+ }
3267
+ });
3268
+ } catch {
3269
+ }
3270
+ }
3271
+ }
3272
+ }
3273
+ });
3481
3274
  } catch {
3482
3275
  }
3483
- const idx = this.persistedBatchKeys.indexOf(key);
3484
- if (idx !== -1) this.persistedBatchKeys.splice(idx, 1);
3485
3276
  }
3486
- // A page that is still running PENDING_BATCH_CLEANUP_DELAY_MS after an exit
3487
- // flush was never torn down, so its beacons have long since gone out; drop
3488
- // the persisted copies rather than letting a later pageview resend them.
3489
- schedulePersistedCleanup() {
3490
- if (this.persistedBatchKeys.length === 0) return;
3491
- if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
3492
- this.persistedCleanupTimer = setTimeout(() => {
3493
- this.persistedCleanupTimer = null;
3494
- for (const key of Array.from(this.persistedBatchKeys)) {
3495
- this.removePersistedBatch(key);
3277
+ }
3278
+ if (typeof window !== "undefined") {
3279
+ initImaInterception();
3280
+ }
3281
+ function findCachedWinningBid(analytics, filter) {
3282
+ const self = internals(analytics);
3283
+ const now = Date.now();
3284
+ const isConsumed = (entry) => {
3285
+ return self.hasImpressionEmitted(
3286
+ entry.adUnitCode,
3287
+ void 0,
3288
+ entry.auctionId,
3289
+ entry.bidTrace.creativeId
3290
+ );
3291
+ };
3292
+ if (filter?.creativeId || filter?.adId) {
3293
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3294
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3295
+ if (filter.creativeId && (entry.bidTrace.creativeId === filter.creativeId || entry.rawBid?.creativeId === filter.creativeId) || filter.adId && (entry.rawBid?.adId === filter.adId || entry.adUnitCode === filter.adId)) {
3296
+ return entry;
3496
3297
  }
3497
- }, PENDING_BATCH_CLEANUP_DELAY_MS);
3298
+ }
3498
3299
  }
3499
- // Resend batches a previous pageview persisted at exit but could not
3500
- // confirm. Only this property's own keys are claimed: batches belonging to
3501
- // another property sharing this origin are left for that property's next
3502
- // pageview, since resending them here would file them under this property.
3503
- // Keys are claimed (removed) before sending so concurrent tabs don't
3504
- // double-send; the batch bytes carry their original pageview, session, and
3505
- // event timestamps, so late rows attribute correctly.
3506
- resendPersistedBatches() {
3507
- try {
3508
- if (typeof localStorage === "undefined" || !this.config.endpoint) return;
3509
- if (!this.config.propertyId) return;
3510
- const ownPrefix = this.pendingKeyPrefix();
3511
- const keys = [];
3512
- const legacyKeys = [];
3513
- for (let i = 0; i < localStorage.length; i++) {
3514
- const k = localStorage.key(i);
3515
- if (!k || !k.startsWith(PENDING_BATCH_KEY_PREFIX)) continue;
3516
- if (k.startsWith(ownPrefix)) {
3517
- keys.push(k);
3518
- } else if (isLegacyPendingKey(k)) {
3519
- legacyKeys.push(k);
3300
+ let bestUnconsumedMatch;
3301
+ if (filter?.mediaType) {
3302
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3303
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3304
+ const isEntryVideo = entry.bidTrace.mediaType === filter.mediaType || entry.rawBid?.mediaType === filter.mediaType || filter.mediaType === "video" && entry.rawBid?.mediaTypes?.video !== void 0;
3305
+ if (isEntryVideo) {
3306
+ if (!isConsumed(entry)) {
3307
+ if (!bestUnconsumedMatch || entry.timestamp < bestUnconsumedMatch.timestamp) {
3308
+ bestUnconsumedMatch = entry;
3309
+ }
3520
3310
  }
3521
3311
  }
3522
- for (const key of legacyKeys) {
3523
- try {
3524
- localStorage.removeItem(key);
3525
- } catch {
3312
+ }
3313
+ if (bestUnconsumedMatch) return bestUnconsumedMatch;
3314
+ let bestFallbackMatch;
3315
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3316
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3317
+ const isEntryVideo = entry.bidTrace.mediaType === filter.mediaType || entry.rawBid?.mediaType === filter.mediaType || filter.mediaType === "video" && entry.rawBid?.mediaTypes?.video !== void 0;
3318
+ if (isEntryVideo) {
3319
+ if (!bestFallbackMatch || entry.timestamp > bestFallbackMatch.timestamp) {
3320
+ bestFallbackMatch = entry;
3526
3321
  }
3527
3322
  }
3528
- for (const key of keys) {
3529
- let value = null;
3530
- try {
3531
- value = localStorage.getItem(key);
3532
- localStorage.removeItem(key);
3533
- } catch {
3534
- continue;
3535
- }
3536
- if (!value) continue;
3537
- const ts = parseInt(key.slice(ownPrefix.length), 10);
3538
- if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
3539
- let encoded;
3540
- try {
3541
- encoded = base64ToBytes(value);
3542
- } catch {
3543
- continue;
3544
- }
3545
- if (encoded.byteLength === 0) continue;
3546
- this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
3547
- this.sendFetch(
3548
- {
3549
- url: `${this.config.endpoint}/${this.config.propertyId}`,
3550
- encoded,
3551
- events: []
3552
- },
3553
- false,
3554
- void 0,
3555
- true
3556
- );
3323
+ }
3324
+ if (bestFallbackMatch) return bestFallbackMatch;
3325
+ }
3326
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3327
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3328
+ if (!isConsumed(entry)) {
3329
+ if (!bestUnconsumedMatch || entry.timestamp > bestUnconsumedMatch.timestamp) {
3330
+ bestUnconsumedMatch = entry;
3557
3331
  }
3558
- } catch {
3559
3332
  }
3560
3333
  }
3561
- log(level, msg, ...args) {
3562
- this.logger.log(level, msg, ...args);
3334
+ if (bestUnconsumedMatch) return bestUnconsumedMatch;
3335
+ let bestRecent;
3336
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3337
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3338
+ if (!bestRecent || entry.timestamp > bestRecent.timestamp) {
3339
+ bestRecent = entry;
3340
+ }
3563
3341
  }
3564
- };
3565
- var defaultLogger = createLogger({
3566
- prefix: "[bidkernel]",
3567
- logLevel: "INFO"
3568
- });
3569
- function extractAnalyticsOptions(config, allowProviderless = true) {
3570
- if (!config) return null;
3571
- if (Array.isArray(config)) {
3572
- const matched = config.find((item) => item?.provider === "bidkernel") ?? (allowProviderless ? config.find(
3573
- (item) => item && typeof item === "object" && item.provider === void 0
3574
- ) : void 0);
3575
- return matched ? extractAnalyticsOptions(matched, allowProviderless) : null;
3342
+ return bestRecent;
3343
+ }
3344
+ function attachImaAdsManager(analytics, adsManager, options) {
3345
+ const self = internals(analytics);
3346
+ if (!adsManager || typeof adsManager.addEventListener !== "function") {
3347
+ self.log("WARN", "Invalid AdsManager passed to attachImaAdsManager");
3348
+ return () => {
3349
+ };
3576
3350
  }
3577
- if (typeof config === "object") {
3578
- if (config.options && typeof config.options === "object") {
3579
- return config.options;
3351
+ if (!adsManager.__bidkernelAttachedInstances) {
3352
+ try {
3353
+ Object.defineProperty(adsManager, "__bidkernelAttachedInstances", {
3354
+ value: /* @__PURE__ */ new Map(),
3355
+ configurable: true,
3356
+ writable: true
3357
+ });
3358
+ } catch {
3359
+ adsManager.__bidkernelAttachedInstances = /* @__PURE__ */ new Map();
3580
3360
  }
3581
- if (config.config && typeof config.config === "object") {
3582
- return config.config;
3361
+ }
3362
+ const attached = adsManager.__bidkernelAttachedInstances;
3363
+ const previous = attached.get(analytics);
3364
+ if (previous) {
3365
+ if (!options) return () => {
3366
+ };
3367
+ previous();
3368
+ }
3369
+ const slotId = options?.slotId || options?.adUnitCode || "video";
3370
+ const adUnitCode = options?.adUnitCode || slotId;
3371
+ const auctionId = options?.auctionId || "";
3372
+ const transactionId = options?.transactionId || "";
3373
+ const getWinningBid = (adData) => {
3374
+ let cached = (auctionId ? self.getCachedBid(`${auctionId}:${adUnitCode}`) : void 0) || (auctionId && slotId !== adUnitCode ? self.getCachedBid(`${auctionId}:${slotId}`) : void 0) || (adUnitCode !== "video" ? self.getCachedBid(adUnitCode) : void 0) || (slotId !== adUnitCode && slotId !== "video" ? self.getCachedBid(slotId) : void 0);
3375
+ if (!cached) {
3376
+ cached = findCachedWinningBid(analytics, {
3377
+ creativeId: adData?.creativeId,
3378
+ adId: adData?.adId,
3379
+ mediaType: "video"
3380
+ });
3583
3381
  }
3584
- if (config.propertyId || config.endpoint) {
3585
- return config;
3382
+ return cached;
3383
+ };
3384
+ let adStarted = false;
3385
+ const onAdStartedOrImpression = (event) => {
3386
+ adStarted = true;
3387
+ self.log("DEBUG", "IMA AdEvent.STARTED / IMPRESSION received", event);
3388
+ const ad = typeof event?.getAd === "function" ? event.getAd() : event?.ad;
3389
+ const adData = {};
3390
+ if (ad) {
3391
+ adData.creativeId = (typeof ad.getCreativeId === "function" ? ad.getCreativeId() : ad.creativeId) || "";
3392
+ adData.adId = (typeof ad.getAdId === "function" ? ad.getAdId() : ad.id) || "";
3393
+ adData.title = (typeof ad.getTitle === "function" ? ad.getTitle() : ad.title) || "";
3394
+ adData.duration = typeof ad.getDuration === "function" ? ad.getDuration() : ad.duration;
3395
+ adData.advertiserName = typeof ad.getAdvertiserName === "function" ? ad.getAdvertiserName() : "";
3396
+ }
3397
+ const cached = getWinningBid(adData);
3398
+ const resolvedSlotId = options?.slotId && options.slotId !== "video" ? options.slotId : options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || options?.slotId || options?.adUnitCode || "video";
3399
+ const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || resolvedSlotId;
3400
+ const resolvedAuctionId = auctionId || cached?.auctionId || "";
3401
+ const resolvedTransactionId = transactionId || cached?.transactionId || "";
3402
+ const mergedBid = {
3403
+ ...cached?.rawBid || options?.bid,
3404
+ mediaType: "video",
3405
+ creativeId: adData.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || ""
3406
+ };
3407
+ const emitted = self.recordImpression(resolvedSlotId, {
3408
+ adUnitCode: resolvedAdUnitCode,
3409
+ auctionId: resolvedAuctionId,
3410
+ transactionId: resolvedTransactionId,
3411
+ mediaType: "video",
3412
+ bid: mergedBid,
3413
+ metadata: {
3414
+ ...options?.metadata,
3415
+ media_type: "video",
3416
+ ...adData.title ? { ad_title: adData.title } : {},
3417
+ ...Number.isFinite(adData.duration) ? { ad_duration_s: String(adData.duration) } : {}
3418
+ }
3419
+ });
3420
+ if (emitted && options?.onImpression) {
3421
+ try {
3422
+ options.onImpression(resolvedSlotId, { ad, bid: mergedBid });
3423
+ } catch (e) {
3424
+ self.log("ERROR", "Error in onImpression callback", e);
3425
+ }
3586
3426
  }
3587
- }
3588
- return null;
3589
- }
3590
- function getStashedConfig(win) {
3591
- if (!win) return null;
3592
- if (win._bidkernelAnalyticsConfig) {
3593
- const extracted = extractAnalyticsOptions(win._bidkernelAnalyticsConfig);
3594
- if (extracted) return extracted;
3595
- }
3596
- if (win.bidkernelPrebidAnalytics) {
3597
- const bpa = win.bidkernelPrebidAnalytics;
3598
- if (bpa.options) {
3599
- const extracted = extractAnalyticsOptions(bpa.options);
3600
- if (extracted) return extracted;
3427
+ };
3428
+ const onMilestone = (milestone) => {
3429
+ self.log("DEBUG", `IMA AdEvent milestone: ${milestone}`);
3430
+ if (options?.onMilestone) {
3431
+ try {
3432
+ options.onMilestone(milestone, slotId);
3433
+ } catch (e) {
3434
+ self.log("ERROR", "Error in onMilestone callback", e);
3435
+ }
3601
3436
  }
3602
- if (bpa.config) {
3603
- const extracted = extractAnalyticsOptions(bpa.config);
3604
- if (extracted) return extracted;
3437
+ };
3438
+ const onAdClick = (event) => {
3439
+ self.log("DEBUG", "IMA AdEvent.CLICK received", event);
3440
+ const cached = getWinningBid();
3441
+ const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || slotId;
3442
+ self.enqueue(TraceEventType.CLICK, "click", {
3443
+ auctionId: auctionId || cached?.auctionId,
3444
+ transactionId: transactionId || cached?.transactionId,
3445
+ adUnitCode: resolvedAdUnitCode,
3446
+ bid: cached?.bidTrace || options?.bid,
3447
+ metadata: {
3448
+ ...options?.metadata,
3449
+ media_type: "video"
3450
+ }
3451
+ });
3452
+ };
3453
+ const onAdError = (event) => {
3454
+ self.log("WARN", "IMA AdErrorEvent received", event);
3455
+ const err = typeof event?.getError === "function" ? event.getError() : event?.error || event;
3456
+ const msg = (err && typeof err.getMessage === "function" ? err.getMessage() : err?.message) || String(err || "IMA ad error");
3457
+ const code = (err && typeof err.getErrorCode === "function" ? err.getErrorCode() : err?.code) || "";
3458
+ const cached = getWinningBid();
3459
+ const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || slotId;
3460
+ self.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
3461
+ auctionId: auctionId || cached?.auctionId,
3462
+ transactionId: transactionId || cached?.transactionId,
3463
+ adUnitCode: resolvedAdUnitCode,
3464
+ bid: cached?.bidTrace || options?.bid,
3465
+ metadata: {
3466
+ ...options?.metadata,
3467
+ reason: adStarted ? "ima_playback_error" : "ima_ad_error",
3468
+ playback_phase: adStarted ? "post_start" : "pre_start",
3469
+ message: msg,
3470
+ ...code ? { ima_error_code: String(code) } : {}
3471
+ },
3472
+ error: typeof err === "object" ? err : new Error(msg)
3473
+ });
3474
+ if (options?.onError) {
3475
+ try {
3476
+ options.onError(err);
3477
+ } catch (e) {
3478
+ self.log("ERROR", "Error in onError callback", e);
3479
+ }
3605
3480
  }
3606
- if (bpa.propertyId) {
3607
- return extractAnalyticsOptions(bpa);
3481
+ };
3482
+ const googleIma = typeof window !== "undefined" ? window.google?.ima : void 0;
3483
+ const adEventType = googleIma?.AdEvent?.Type || {};
3484
+ const adErrorEventType = googleIma?.AdErrorEvent?.Type || {};
3485
+ const rawListeners = [
3486
+ { type: adEventType.STARTED || "started", handler: onAdStartedOrImpression },
3487
+ { type: adEventType.IMPRESSION || "impression", handler: onAdStartedOrImpression },
3488
+ {
3489
+ type: adEventType.FIRST_QUARTILE || "firstQuartile",
3490
+ handler: () => onMilestone("firstQuartile")
3491
+ },
3492
+ { type: adEventType.MIDPOINT || "midpoint", handler: () => onMilestone("midpoint") },
3493
+ {
3494
+ type: adEventType.THIRD_QUARTILE || "thirdQuartile",
3495
+ handler: () => onMilestone("thirdQuartile")
3496
+ },
3497
+ { type: adEventType.COMPLETE || "complete", handler: () => onMilestone("complete") },
3498
+ { type: adEventType.CLICK || "click", handler: onAdClick },
3499
+ { type: adErrorEventType.AD_ERROR || "adError", handler: onAdError }
3500
+ ];
3501
+ const seenListenerTypes = /* @__PURE__ */ new Set();
3502
+ const listeners = [];
3503
+ for (const l of rawListeners) {
3504
+ if (!seenListenerTypes.has(l.type)) {
3505
+ seenListenerTypes.add(l.type);
3506
+ listeners.push(l);
3507
+ }
3508
+ }
3509
+ for (const { type, handler } of listeners) {
3510
+ try {
3511
+ adsManager.addEventListener(type, handler);
3512
+ } catch {
3608
3513
  }
3609
3514
  }
3610
- return null;
3515
+ const cleanup = () => {
3516
+ attached.delete(analytics);
3517
+ for (const { type, handler } of listeners) {
3518
+ try {
3519
+ adsManager.removeEventListener(type, handler);
3520
+ } catch {
3521
+ }
3522
+ }
3523
+ self.videoDetachCleanups.delete(cleanup);
3524
+ };
3525
+ attached.set(analytics, cleanup);
3526
+ self.videoDetachCleanups.add(cleanup);
3527
+ return cleanup;
3611
3528
  }
3612
- function registerPrebidAnalytics(pbjsGlobalName = "pbjs", defaults) {
3613
- const win = typeof window !== "undefined" ? window : {};
3614
- win[pbjsGlobalName] = win[pbjsGlobalName] || {};
3615
- const pbjs = win[pbjsGlobalName];
3616
- const enableInstance = (options) => {
3617
- const previous = win._bidkernelPrebidAnalytics;
3618
- const pinnedEndpoint = previous?.getEndpoint?.() || "";
3619
- const merged = {
3620
- pbjsGlobalName,
3621
- ...defaults,
3622
- ...options
3529
+ function attachVideoPlayer(analytics, target, options) {
3530
+ const self = internals(analytics);
3531
+ if (!target) {
3532
+ self.log("WARN", "attachVideoPlayer called with null/undefined target");
3533
+ return () => {
3623
3534
  };
3624
- if (pinnedEndpoint && !sameEndpointOrigin(merged.endpoint || "", pinnedEndpoint)) {
3625
- defaultLogger.warn(
3626
- `Ignoring analytics endpoint change to ${merged.endpoint || "(empty)"}: this page is pinned to ${pinnedEndpoint}. Reload to change the ingest endpoint.`
3627
- );
3628
- merged.endpoint = pinnedEndpoint;
3535
+ }
3536
+ let el = null;
3537
+ if (typeof target === "string") {
3538
+ if (typeof document !== "undefined") {
3539
+ try {
3540
+ el = document.querySelector(target) || document.getElementById(target);
3541
+ } catch {
3542
+ el = document.getElementById(target);
3543
+ }
3544
+ }
3545
+ } else if (typeof HTMLElement !== "undefined" && target instanceof HTMLElement) {
3546
+ el = target;
3547
+ } else if (target && typeof target === "object" && target.nodeType === 1) {
3548
+ el = target;
3549
+ } else if (
3550
+ // Check if target is an IMA AdsManager
3551
+ typeof target.addEventListener === "function" && (typeof target.init === "function" || typeof target.start === "function" || typeof target.getCuePoints === "function" || typeof target.getVolume === "function" || target.__imaAdsManager)
3552
+ ) {
3553
+ return attachImaAdsManager(analytics, target, options);
3554
+ } else if (target && typeof target.addEventListener === "function") {
3555
+ el = target;
3556
+ }
3557
+ const slotId = options?.slotId || options?.adUnitCode || (el ? el.id : "") || "video";
3558
+ const adUnitCode = options?.adUnitCode || slotId;
3559
+ const auctionId = options?.auctionId || "";
3560
+ const transactionId = options?.transactionId || "";
3561
+ const getWinningBid = () => {
3562
+ return (auctionId ? self.getCachedBid(`${auctionId}:${adUnitCode}`) : void 0) || (auctionId && slotId !== adUnitCode ? self.getCachedBid(`${auctionId}:${slotId}`) : void 0) || self.getCachedBid(adUnitCode) || (slotId !== adUnitCode ? self.getCachedBid(slotId) : void 0);
3563
+ };
3564
+ let videoEl = null;
3565
+ if (el) {
3566
+ if (el.tagName && el.tagName.toLowerCase() === "video") {
3567
+ videoEl = el;
3568
+ } else {
3569
+ videoEl = el.querySelector("video");
3629
3570
  }
3630
- if (previous) {
3571
+ }
3572
+ const quartilesFired = {
3573
+ q1: false,
3574
+ q2: false,
3575
+ q3: false,
3576
+ q4: false
3577
+ };
3578
+ const triggerPlaybackImpression = (activeVideo) => {
3579
+ const vEl = activeVideo || videoEl || (el?.querySelector ? el.querySelector("video") : null);
3580
+ const cached = getWinningBid();
3581
+ const mergedBid = {
3582
+ ...cached?.rawBid || options?.bid,
3583
+ mediaType: "video"
3584
+ };
3585
+ const emitted = self.recordImpression(slotId, {
3586
+ adUnitCode,
3587
+ auctionId: auctionId || cached?.auctionId,
3588
+ transactionId: transactionId || cached?.transactionId,
3589
+ mediaType: "video",
3590
+ bid: mergedBid,
3591
+ metadata: {
3592
+ ...options?.metadata,
3593
+ media_type: "video",
3594
+ ...vEl && Number.isFinite(vEl.duration) ? { video_duration_s: String(Math.round(vEl.duration)) } : {}
3595
+ }
3596
+ });
3597
+ if (emitted && options?.onImpression) {
3631
3598
  try {
3632
- previous.disable();
3599
+ options.onImpression(slotId, { element: vEl || el, bid: mergedBid });
3633
3600
  } catch (e) {
3634
- defaultLogger.warn("Failed to disable previous analytics instance:", e);
3601
+ self.log("ERROR", "Error in onImpression callback", e);
3635
3602
  }
3636
3603
  }
3637
- const analytics = new BidkernelPrebidAnalytics(merged);
3638
- analytics.enable();
3639
- win._bidkernelPrebidAnalytics = analytics;
3640
- return analytics;
3641
3604
  };
3642
- const doRegister = () => {
3643
- try {
3644
- if (pbjs.adapterManager && typeof pbjs.adapterManager.registerAnalyticsAdapter === "function") {
3645
- pbjs.adapterManager.registerAnalyticsAdapter({
3646
- adapter: {
3647
- enableAnalytics: (config) => {
3648
- const options = extractAnalyticsOptions(config, !Array.isArray(config));
3649
- if (options) {
3650
- enableInstance(options);
3651
- }
3652
- },
3653
- disableAnalytics: () => {
3654
- if (win._bidkernelPrebidAnalytics) {
3655
- win._bidkernelPrebidAnalytics.disable();
3656
- }
3657
- }
3658
- },
3659
- code: "bidkernel"
3660
- });
3605
+ const handlePlaying = (e) => {
3606
+ self.log("DEBUG", "HTML5 video playing event received");
3607
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
3608
+ triggerPlaybackImpression(targetVideo);
3609
+ };
3610
+ const handleTimeUpdate = (e) => {
3611
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
3612
+ if (targetVideo && targetVideo.currentTime > 0) {
3613
+ triggerPlaybackImpression(targetVideo);
3614
+ if (Number.isFinite(targetVideo.duration) && targetVideo.duration > 0) {
3615
+ const progress = targetVideo.currentTime / targetVideo.duration;
3616
+ if (progress >= 0.25 && !quartilesFired.q1) {
3617
+ quartilesFired.q1 = true;
3618
+ options?.onMilestone?.("firstQuartile", slotId);
3619
+ }
3620
+ if (progress >= 0.5 && !quartilesFired.q2) {
3621
+ quartilesFired.q2 = true;
3622
+ options?.onMilestone?.("midpoint", slotId);
3623
+ }
3624
+ if (progress >= 0.75 && !quartilesFired.q3) {
3625
+ quartilesFired.q3 = true;
3626
+ options?.onMilestone?.("thirdQuartile", slotId);
3627
+ }
3661
3628
  }
3662
- const stashed = getStashedConfig(win);
3663
- if (stashed && (stashed.propertyId || defaults?.propertyId)) {
3664
- enableInstance(stashed);
3629
+ }
3630
+ };
3631
+ const handleEnded = () => {
3632
+ if (!quartilesFired.q4) {
3633
+ quartilesFired.q4 = true;
3634
+ options?.onMilestone?.("complete", slotId);
3635
+ }
3636
+ };
3637
+ const handleError = (e) => {
3638
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
3639
+ const err = targetVideo?.error || videoEl?.error;
3640
+ const cached = getWinningBid();
3641
+ const msg = err ? `HTML5 video error code ${err.code}: ${err.message}` : "Video playback error";
3642
+ self.log("WARN", msg);
3643
+ self.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
3644
+ auctionId: auctionId || cached?.auctionId,
3645
+ transactionId: transactionId || cached?.transactionId,
3646
+ adUnitCode,
3647
+ bid: cached?.bidTrace || options?.bid,
3648
+ metadata: {
3649
+ ...options?.metadata,
3650
+ reason: "html5_video_error",
3651
+ message: msg,
3652
+ ...err?.code ? { video_error_code: String(err.code) } : {}
3653
+ },
3654
+ error: err ? new Error(msg) : void 0
3655
+ });
3656
+ if (options?.onError) {
3657
+ try {
3658
+ options.onError(err);
3659
+ } catch (e2) {
3660
+ self.log("ERROR", "Error in onError callback", e2);
3665
3661
  }
3666
- } catch (e) {
3667
- defaultLogger.warn("Failed to register standard Prebid analytics adapter:", e);
3668
3662
  }
3669
3663
  };
3670
- if (pbjs.adapterManager && typeof pbjs.adapterManager.registerAnalyticsAdapter === "function") {
3671
- doRegister();
3672
- } else {
3673
- pbjs.que = pbjs.que || [];
3674
- pbjs.que.push(doRegister);
3664
+ const attachListenersToVideo = (targetVideo) => {
3665
+ targetVideo.addEventListener("playing", handlePlaying);
3666
+ targetVideo.addEventListener("play", handlePlaying);
3667
+ targetVideo.addEventListener("timeupdate", handleTimeUpdate);
3668
+ targetVideo.addEventListener("ended", handleEnded);
3669
+ targetVideo.addEventListener("error", handleError);
3670
+ };
3671
+ const removeListenersFromVideo = (targetVideo) => {
3672
+ targetVideo.removeEventListener("playing", handlePlaying);
3673
+ targetVideo.removeEventListener("play", handlePlaying);
3674
+ targetVideo.removeEventListener("timeupdate", handleTimeUpdate);
3675
+ targetVideo.removeEventListener("ended", handleEnded);
3676
+ targetVideo.removeEventListener("error", handleError);
3677
+ };
3678
+ let mutationObserver = null;
3679
+ if (el && el !== videoEl) {
3680
+ el.addEventListener("playing", handlePlaying, true);
3681
+ el.addEventListener("play", handlePlaying, true);
3682
+ el.addEventListener("timeupdate", handleTimeUpdate, true);
3683
+ el.addEventListener("ended", handleEnded, true);
3684
+ el.addEventListener("error", handleError, true);
3685
+ }
3686
+ if (videoEl) {
3687
+ attachListenersToVideo(videoEl);
3688
+ }
3689
+ if (el && el.tagName?.toLowerCase() !== "video" && typeof MutationObserver !== "undefined") {
3690
+ mutationObserver = new MutationObserver(() => {
3691
+ const found = el.querySelector("video");
3692
+ if (found && found !== videoEl) {
3693
+ if (videoEl) removeListenersFromVideo(videoEl);
3694
+ videoEl = found;
3695
+ if (videoEl) attachListenersToVideo(videoEl);
3696
+ }
3697
+ });
3698
+ mutationObserver.observe(el, { childList: true, subtree: true });
3675
3699
  }
3700
+ const cachedInitial = getWinningBid();
3701
+ if (options?.trackViewability !== false && el) {
3702
+ self.observeSlot(el, slotId, {
3703
+ adUnitCode,
3704
+ auctionId: auctionId || cachedInitial?.auctionId,
3705
+ transactionId: transactionId || cachedInitial?.transactionId,
3706
+ mediaType: "video",
3707
+ bid: cachedInitial?.bidTrace || options?.bid,
3708
+ metadata: options?.metadata,
3709
+ emitRefreshEvent: false
3710
+ });
3711
+ }
3712
+ const cleanup = () => {
3713
+ if (mutationObserver) {
3714
+ mutationObserver.disconnect();
3715
+ mutationObserver = null;
3716
+ }
3717
+ if (el && el !== videoEl) {
3718
+ el.removeEventListener("playing", handlePlaying, true);
3719
+ el.removeEventListener("play", handlePlaying, true);
3720
+ el.removeEventListener("timeupdate", handleTimeUpdate, true);
3721
+ el.removeEventListener("ended", handleEnded, true);
3722
+ el.removeEventListener("error", handleError, true);
3723
+ }
3724
+ if (videoEl) {
3725
+ removeListenersFromVideo(videoEl);
3726
+ }
3727
+ self.videoDetachCleanups.delete(cleanup);
3728
+ };
3729
+ self.videoDetachCleanups.add(cleanup);
3730
+ return cleanup;
3676
3731
  }
3732
+ BidkernelPrebidAnalytics.prototype.attachImaAdsManager = function(adsManager, options) {
3733
+ return attachImaAdsManager(this, adsManager, options);
3734
+ };
3735
+ BidkernelPrebidAnalytics.prototype.attachVideoPlayer = function(target, options) {
3736
+ return attachVideoPlayer(this, target, options);
3737
+ };
3738
+ var coreEnable = BidkernelPrebidAnalytics.prototype.enable;
3739
+ BidkernelPrebidAnalytics.prototype.enable = function() {
3740
+ coreEnable.call(this);
3741
+ if (typeof window !== "undefined") {
3742
+ initImaInterception();
3743
+ if (window.google?.ima) {
3744
+ hookImaPrototype(window.google.ima);
3745
+ }
3746
+ }
3747
+ };
3677
3748
 
3678
3749
  // src/index.ts
3679
3750
  function getbidkernel(alias = "bidkernel") {
@@ -3707,7 +3778,11 @@ function getbidkernel(alias = "bidkernel") {
3707
3778
  }
3708
3779
  export {
3709
3780
  BidkernelPrebidAnalytics,
3781
+ CACHED_BID_TTL_MS,
3710
3782
  PrebidEventDeduper,
3783
+ attachImaAdsManager,
3784
+ attachVideoPlayer,
3785
+ getOrCreateSessionId,
3711
3786
  getPrebidEventKey,
3712
3787
  getbidkernel,
3713
3788
  hookImaPrototype,