@bidkernel/analytics 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21,7 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  BidkernelPrebidAnalytics: () => BidkernelPrebidAnalytics,
24
+ CACHED_BID_TTL_MS: () => CACHED_BID_TTL_MS,
24
25
  PrebidEventDeduper: () => PrebidEventDeduper,
26
+ attachImaAdsManager: () => attachImaAdsManager,
27
+ attachVideoPlayer: () => attachVideoPlayer,
25
28
  getOrCreateSessionId: () => getOrCreateSessionId,
26
29
  getPrebidEventKey: () => getPrebidEventKey,
27
30
  getbidkernel: () => getbidkernel,
@@ -545,28 +548,6 @@ var TraceEventBatch = {
545
548
  reader.skip(tag & 7);
546
549
  }
547
550
  return message;
548
- },
549
- create(base) {
550
- return TraceEventBatch.fromPartial(base ?? {});
551
- },
552
- fromPartial(object) {
553
- const message = createBaseTraceEventBatch();
554
- message.propertyId = object.propertyId ?? "";
555
- message.pageviewId = object.pageviewId ?? "";
556
- message.sessionId = object.sessionId ?? "";
557
- message.pageUrl = object.pageUrl ?? "";
558
- message.country = object.country ?? "";
559
- message.region = object.region ?? "";
560
- message.deviceType = object.deviceType ?? "";
561
- message.userId = object.userId ?? "";
562
- message.domain = object.domain ?? "";
563
- message.events = object.events?.map((e) => TraceEvent.fromPartial(e)) || [];
564
- message.browser = object.browser ?? "";
565
- message.deviceManufacturer = object.deviceManufacturer ?? "";
566
- message.deviceModel = object.deviceModel ?? "";
567
- message.city = object.city ?? "";
568
- message.postalCode = object.postalCode ?? "";
569
- return message;
570
551
  }
571
552
  };
572
553
  function createBaseTraceEvent() {
@@ -731,33 +712,6 @@ var TraceEvent = {
731
712
  reader.skip(tag & 7);
732
713
  }
733
714
  return message;
734
- },
735
- create(base) {
736
- return TraceEvent.fromPartial(base ?? {});
737
- },
738
- fromPartial(object) {
739
- const message = createBaseTraceEvent();
740
- message.timestampMs = object.timestampMs ?? 0;
741
- message.type = object.type ?? 0;
742
- message.eventName = object.eventName ?? "";
743
- message.auctionId = object.auctionId ?? "";
744
- message.transactionId = object.transactionId ?? "";
745
- message.adUnitCode = object.adUnitCode ?? "";
746
- message.bid = object.bid !== void 0 && object.bid !== null ? BidTrace.fromPartial(object.bid) : void 0;
747
- message.namespace = object.namespace ?? "";
748
- message.eventId = object.eventId ?? "";
749
- message.metadata = globalThis.Object.entries(object.metadata ?? {}).reduce(
750
- (acc, [key, value]) => {
751
- if (value !== void 0) {
752
- acc[key] = globalThis.String(value);
753
- }
754
- return acc;
755
- },
756
- {}
757
- );
758
- message.viewableDurationMs = object.viewableDurationMs ?? void 0;
759
- message.bidderOutcomes = object.bidderOutcomes?.map((e) => BidderOutcomeEntry.fromPartial(e)) || [];
760
- return message;
761
715
  }
762
716
  };
763
717
  function createBaseTraceEvent_MetadataEntry() {
@@ -801,15 +755,6 @@ var TraceEvent_MetadataEntry = {
801
755
  reader.skip(tag & 7);
802
756
  }
803
757
  return message;
804
- },
805
- create(base) {
806
- return TraceEvent_MetadataEntry.fromPartial(base ?? {});
807
- },
808
- fromPartial(object) {
809
- const message = createBaseTraceEvent_MetadataEntry();
810
- message.key = object.key ?? "";
811
- message.value = object.value ?? "";
812
- return message;
813
758
  }
814
759
  };
815
760
  function createBaseBidderOutcomeEntry() {
@@ -883,18 +828,6 @@ var BidderOutcomeEntry = {
883
828
  reader.skip(tag & 7);
884
829
  }
885
830
  return message;
886
- },
887
- create(base) {
888
- return BidderOutcomeEntry.fromPartial(base ?? {});
889
- },
890
- fromPartial(object) {
891
- const message = createBaseBidderOutcomeEntry();
892
- message.bidder = object.bidder ?? "";
893
- message.adUnitCode = object.adUnitCode ?? "";
894
- message.mediaType = object.mediaType ?? "";
895
- message.outcome = object.outcome ?? 0;
896
- message.latencyMs = object.latencyMs ?? 0;
897
- return message;
898
831
  }
899
832
  };
900
833
  function createBaseBidTrace() {
@@ -1029,23 +962,6 @@ var BidTrace = {
1029
962
  reader.skip(tag & 7);
1030
963
  }
1031
964
  return message;
1032
- },
1033
- create(base) {
1034
- return BidTrace.fromPartial(base ?? {});
1035
- },
1036
- fromPartial(object) {
1037
- const message = createBaseBidTrace();
1038
- message.bidder = object.bidder ?? "";
1039
- message.cpm = object.cpm ?? 0;
1040
- message.currency = object.currency ?? "";
1041
- message.width = object.width ?? 0;
1042
- message.height = object.height ?? 0;
1043
- message.dealId = object.dealId ?? "";
1044
- message.mediaType = object.mediaType ?? "";
1045
- message.latencyMs = object.latencyMs ?? 0;
1046
- message.advertiserDomain = object.advertiserDomain ?? "";
1047
- message.creativeId = object.creativeId ?? "";
1048
- return message;
1049
965
  }
1050
966
  };
1051
967
 
@@ -1159,17 +1075,24 @@ function sameEndpointOrigin(a, b) {
1159
1075
  return false;
1160
1076
  }
1161
1077
  }
1162
- function extendSession() {
1163
- const now = Date.now();
1164
- inMemorySessionTs = now;
1165
- if (!storageAllowed) return;
1078
+ var SESSION_PERSIST_INTERVAL_MS = 60 * 1e3;
1079
+ var persistedSessionTs = 0;
1080
+ function persistSessionTs(now) {
1166
1081
  try {
1167
1082
  if (typeof localStorage !== "undefined") {
1168
1083
  localStorage.setItem(SESSION_TS_KEY, now.toString());
1084
+ persistedSessionTs = now;
1169
1085
  }
1170
1086
  } catch {
1171
1087
  }
1172
1088
  }
1089
+ function extendSession() {
1090
+ const now = Date.now();
1091
+ inMemorySessionTs = now;
1092
+ if (!storageAllowed) return;
1093
+ if (now - persistedSessionTs < SESSION_PERSIST_INTERVAL_MS) return;
1094
+ persistSessionTs(now);
1095
+ }
1173
1096
  function getOrCreateSessionId() {
1174
1097
  const now = Date.now();
1175
1098
  const memoryFresh = !!inMemorySessionId && !!inMemorySessionTs && now - inMemorySessionTs <= THIRTY_MINUTES_MS;
@@ -1179,14 +1102,14 @@ function getOrCreateSessionId() {
1179
1102
  const tsStr = localStorage.getItem(SESSION_TS_KEY);
1180
1103
  const storedTs = tsStr ? parseInt(tsStr, 10) || 0 : 0;
1181
1104
  if (storedId && storedTs && now - storedTs <= THIRTY_MINUTES_MS) {
1182
- localStorage.setItem(SESSION_TS_KEY, now.toString());
1105
+ persistSessionTs(now);
1183
1106
  inMemorySessionId = storedId;
1184
1107
  inMemorySessionTs = now;
1185
1108
  return storedId;
1186
1109
  }
1187
1110
  const newId = memoryFresh ? inMemorySessionId : generateUUID();
1188
1111
  localStorage.setItem(SESSION_KEY, newId);
1189
- localStorage.setItem(SESSION_TS_KEY, now.toString());
1112
+ persistSessionTs(now);
1190
1113
  inMemorySessionId = newId;
1191
1114
  inMemorySessionTs = now;
1192
1115
  return newId;
@@ -1296,537 +1219,281 @@ function parseBidDimensions(bid) {
1296
1219
  height: Number.isFinite(bid?.height) ? bid.height : 0
1297
1220
  };
1298
1221
  }
1299
- var adsLoaderListenerMap = /* @__PURE__ */ new WeakMap();
1300
- function hookAdsManagerLoadedEvent(loadedEventCtor) {
1301
- if (!loadedEventCtor) return;
1302
- try {
1303
- const loadedProto = loadedEventCtor.prototype;
1304
- if (loadedProto && typeof loadedProto.getAdsManager === "function" && !loadedProto.getAdsManager.__bidkernelHooked) {
1305
- const originalGetAdsManager = loadedProto.getAdsManager;
1306
- const hookedGetAdsManager = function(...args) {
1307
- const adsManager = originalGetAdsManager.apply(this, args);
1308
- if (adsManager) {
1309
- for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
1310
- try {
1311
- instance.attachImaAdsManager(adsManager);
1312
- } catch {
1313
- }
1314
- }
1315
- }
1316
- return adsManager;
1317
- };
1318
- hookedGetAdsManager.__bidkernelHooked = true;
1319
- loadedProto.getAdsManager = hookedGetAdsManager;
1320
- }
1321
- } catch {
1222
+ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1223
+ static activeInstances = /* @__PURE__ */ new Set();
1224
+ static getActiveInstances() {
1225
+ return _BidkernelPrebidAnalytics.activeInstances;
1322
1226
  }
1323
- }
1324
- function hookAdsManagerPrototype(adsManagerCtorOrProto) {
1325
- if (!adsManagerCtorOrProto) return;
1326
- try {
1327
- const proto = adsManagerCtorOrProto.prototype || adsManagerCtorOrProto;
1328
- if (proto && typeof proto.init === "function" && !proto.init.__bidkernelHooked) {
1329
- const origInit = proto.init;
1330
- const hookedInit = function(...args) {
1331
- for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
1332
- try {
1333
- instance.attachImaAdsManager(this);
1334
- } catch {
1335
- }
1336
- }
1337
- return origInit.apply(this, args);
1338
- };
1339
- hookedInit.__bidkernelHooked = true;
1340
- proto.init = hookedInit;
1227
+ /**
1228
+ * Allows or forbids localStorage writes for every instance on the page.
1229
+ * Collection and delivery are unaffected: only the persisted session id,
1230
+ * its timestamp and exit batches are gated.
1231
+ */
1232
+ static setStorageAllowed(allowed) {
1233
+ storageAllowed = allowed;
1234
+ persistedSessionTs = 0;
1235
+ }
1236
+ static isStorageAllowed() {
1237
+ return storageAllowed;
1238
+ }
1239
+ config;
1240
+ queue = [];
1241
+ errorCount = 0;
1242
+ flushTimer = null;
1243
+ boundFlushBeacon;
1244
+ boundVisibilityChange;
1245
+ isEnabled = false;
1246
+ boundPbjsHandlers = [];
1247
+ // Captured at enable()/navigate() so events queued before an SPA route
1248
+ // change flush with the page they occurred on, not the new URL.
1249
+ pageUrl = "";
1250
+ deduper = new PrebidEventDeduper();
1251
+ consecutiveSendFailures = 0;
1252
+ nextSendAllowedAt = 0;
1253
+ replayedEventCount = 0;
1254
+ revenueFlushTimer = null;
1255
+ // localStorage keys of exit batches this instance persisted, so a page that
1256
+ // survives its own pagehide/hidden (bfcache restore, tab re-focus) can
1257
+ // remove them instead of leaving them for a duplicate resend.
1258
+ persistedBatchKeys = [];
1259
+ persistedCleanupTimer = null;
1260
+ // Viewability & refresh tracking
1261
+ intersectionObserver = null;
1262
+ slotViewabilityRecords = /* @__PURE__ */ new Map();
1263
+ elementToSlotId = /* @__PURE__ */ new Map();
1264
+ slotRefreshIndices = /* @__PURE__ */ new Map();
1265
+ slotLastAuctionIds = /* @__PURE__ */ new Map();
1266
+ pendingThresholdListeners = /* @__PURE__ */ new Map();
1267
+ pendingViewableListeners = /* @__PURE__ */ new Map();
1268
+ // Element a subscriber named for a slot (a sticky bar or interstitial that
1269
+ // renders outside the slot element); wins over document.getElementById.
1270
+ slotElements = /* @__PURE__ */ new Map();
1271
+ clickDetachCleanups = /* @__PURE__ */ new Set();
1272
+ lastClickAt = /* @__PURE__ */ new Map();
1273
+ // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
1274
+ slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
1275
+ // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
1276
+ cachedWinningBids = /* @__PURE__ */ new Map();
1277
+ // Active video player attachment cleanup routines (registered by ./video)
1278
+ videoDetachCleanups = /* @__PURE__ */ new Set();
1279
+ // Compacted bidder participation map per auction: auctionId -> Map<`${bidder}:${adUnitCode}`, BidderOutcomeEntry>
1280
+ auctionBidderOutcomes = /* @__PURE__ */ new Map();
1281
+ logger;
1282
+ constructor(config) {
1283
+ if (config.storageAllowed !== void 0) {
1284
+ _BidkernelPrebidAnalytics.setStorageAllowed(config.storageAllowed);
1341
1285
  }
1342
- if (proto && typeof proto.start === "function" && !proto.start.__bidkernelHooked) {
1343
- const origStart = proto.start;
1344
- const hookedStart = function(...args) {
1345
- for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
1346
- try {
1347
- instance.attachImaAdsManager(this);
1348
- } catch {
1349
- }
1350
- }
1351
- return origStart.apply(this, args);
1352
- };
1353
- hookedStart.__bidkernelHooked = true;
1354
- proto.start = hookedStart;
1286
+ const requestedEndpoint = config.endpoint || "";
1287
+ const endpoint = !requestedEndpoint || isTrustedEndpoint(requestedEndpoint) ? requestedEndpoint : "";
1288
+ this.config = {
1289
+ endpoint,
1290
+ propertyId: config.propertyId || "",
1291
+ pageviewId: config.pageviewId || generateUUID(),
1292
+ sessionId: config.sessionId || getOrCreateSessionId(),
1293
+ userId: config.userId || "",
1294
+ deviceType: config.deviceType || "desktop",
1295
+ country: config.country || "",
1296
+ region: config.region || "",
1297
+ auctionEnabled: config.auctionEnabled ?? true,
1298
+ warningsEnabled: config.warningsEnabled ?? true,
1299
+ errorsEnabled: config.errorsEnabled ?? true,
1300
+ viewabilityEnabled: config.viewabilityEnabled ?? true,
1301
+ storageAllowed: config.storageAllowed ?? true,
1302
+ logLevel: config.logLevel || "INFO",
1303
+ pbjsGlobalName: config.pbjsGlobalName || "pbjs",
1304
+ attachPbjsListeners: config.attachPbjsListeners ?? true,
1305
+ revenueFlushDelayMs: Number.isFinite(config.revenueFlushDelayMs) && config.revenueFlushDelayMs >= 0 ? config.revenueFlushDelayMs : REVENUE_FLUSH_DELAY_MS
1306
+ };
1307
+ this.logger = createLogger({
1308
+ prefix: "[bidkernel][prebid-analytics]",
1309
+ logLevel: this.config.logLevel
1310
+ });
1311
+ if (requestedEndpoint && !endpoint) {
1312
+ this.log(
1313
+ "ERROR",
1314
+ `Rejected untrusted analytics endpoint ${requestedEndpoint}: must be an absolute https:// URL without embedded credentials. Events will not be transmitted.`
1315
+ );
1355
1316
  }
1356
- } catch {
1317
+ this.boundFlushBeacon = () => {
1318
+ this.flushAllSlotsTimeInView();
1319
+ this.flushBeacon();
1320
+ };
1321
+ this.boundVisibilityChange = () => this.handleVisibilityChange();
1357
1322
  }
1358
- }
1359
- function hookAdsLoader(adsLoaderCtor) {
1360
- if (!adsLoaderCtor) return;
1361
- try {
1362
- const proto = adsLoaderCtor.prototype;
1363
- if (!proto) return;
1364
- if (typeof proto.addEventListener === "function" && !proto.addEventListener.__bidkernelHooked) {
1365
- const origAddEventListener = proto.addEventListener;
1366
- const hookedAddEventListener = function(type, listener, ...rest) {
1367
- if ((type === "adsManagerLoaded" || type === (this?.AdsManagerLoadedEvent?.Type?.ADS_MANAGER_LOADED || "adsManagerLoaded")) && typeof listener === "function") {
1368
- let wrappedListener = adsLoaderListenerMap.get(listener);
1369
- if (!wrappedListener) {
1370
- wrappedListener = function(event) {
1371
- if (event && typeof event.getAdsManager === "function" && !event.getAdsManager.__bidkernelHooked) {
1372
- const origGetAdsManager = event.getAdsManager;
1373
- event.getAdsManager = function(...args) {
1374
- const adsManager = origGetAdsManager.apply(this, args);
1375
- if (adsManager) {
1376
- for (const sdk of BidkernelPrebidAnalytics.getActiveInstances()) {
1377
- try {
1378
- sdk.attachImaAdsManager(adsManager);
1379
- } catch {
1380
- }
1381
- }
1382
- }
1383
- return adsManager;
1384
- };
1385
- event.getAdsManager.__bidkernelHooked = true;
1386
- }
1387
- return listener.apply(this, arguments);
1388
- };
1389
- adsLoaderListenerMap.set(listener, wrappedListener);
1323
+ enable() {
1324
+ if (this.isEnabled) return;
1325
+ this.isEnabled = true;
1326
+ _BidkernelPrebidAnalytics.activeInstances.add(this);
1327
+ this.logger.setLevel(this.config.logLevel);
1328
+ if (!this.config.endpoint) {
1329
+ this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
1330
+ }
1331
+ if (this.config.viewabilityEnabled) {
1332
+ if (typeof IntersectionObserver !== "undefined") {
1333
+ this.intersectionObserver = new IntersectionObserver(this.handleIntersection.bind(this), {
1334
+ threshold: [0.5]
1335
+ });
1336
+ for (const record of this.slotViewabilityRecords.values()) {
1337
+ if (record.element) {
1338
+ this.intersectionObserver.observe(record.element);
1390
1339
  }
1391
- return origAddEventListener.call(this, type, wrappedListener, ...rest);
1392
1340
  }
1393
- return origAddEventListener.call(this, type, listener, ...rest);
1394
- };
1395
- hookedAddEventListener.__bidkernelHooked = true;
1396
- proto.addEventListener = hookedAddEventListener;
1397
- }
1398
- if (typeof proto.removeEventListener === "function" && !proto.removeEventListener.__bidkernelHooked) {
1399
- const origRemoveEventListener = proto.removeEventListener;
1400
- const hookedRemoveEventListener = function(type, listener, ...rest) {
1401
- const wrapped = typeof listener === "function" ? adsLoaderListenerMap.get(listener) : void 0;
1402
- return origRemoveEventListener.call(this, type, wrapped || listener, ...rest);
1403
- };
1404
- hookedRemoveEventListener.__bidkernelHooked = true;
1405
- proto.removeEventListener = hookedRemoveEventListener;
1341
+ }
1406
1342
  }
1407
- } catch {
1408
- }
1409
- }
1410
- function hookImaPrototype(ima) {
1411
- if (!ima || typeof ima !== "object") return;
1412
- try {
1413
- if (ima.AdsManagerLoadedEvent) {
1414
- hookAdsManagerLoadedEvent(ima.AdsManagerLoadedEvent);
1343
+ if (this.config.attachPbjsListeners) {
1344
+ this.attachPbjs();
1415
1345
  }
1416
- const loadedDesc = Object.getOwnPropertyDescriptor(ima, "AdsManagerLoadedEvent");
1417
- if (!loadedDesc || loadedDesc.configurable) {
1418
- let _loadedEvent = ima.AdsManagerLoadedEvent;
1419
- try {
1420
- Object.defineProperty(ima, "AdsManagerLoadedEvent", {
1421
- configurable: true,
1422
- enumerable: true,
1423
- get() {
1424
- return _loadedEvent;
1425
- },
1426
- set(val) {
1427
- _loadedEvent = val;
1428
- hookAdsManagerLoadedEvent(val);
1429
- }
1430
- });
1431
- } catch {
1346
+ if (typeof window !== "undefined") {
1347
+ if (!this.pageUrl) {
1348
+ try {
1349
+ const u = new URL(window.location.href);
1350
+ this.pageUrl = u.origin + u.pathname;
1351
+ } catch {
1352
+ this.pageUrl = window.location.href;
1353
+ }
1354
+ }
1355
+ window.addEventListener("pagehide", this.boundFlushBeacon);
1356
+ if (typeof document !== "undefined") {
1357
+ document.addEventListener("visibilitychange", this.boundVisibilityChange);
1432
1358
  }
1359
+ this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
1360
+ this.resendPersistedBatches();
1433
1361
  }
1434
- if (ima.AdsLoader) {
1435
- hookAdsLoader(ima.AdsLoader);
1362
+ }
1363
+ /**
1364
+ * Binds the Prebid event handlers on the configured global and replays
1365
+ * pbjs.getEvents() history. enable() calls this when attachPbjsListeners is
1366
+ * true. An integration that loads Prebid lazily constructs the instance with
1367
+ * attachPbjsListeners=false and calls this once pbjs exists; disable() (and
1368
+ * therefore navigate()) drops the handlers, so call it again after either.
1369
+ * Idempotent. Returns true when handlers are bound after the call, false
1370
+ * when the instance is disabled or pbjs.onEvent is not available yet.
1371
+ */
1372
+ attachPbjs() {
1373
+ if (!this.isEnabled) return false;
1374
+ if (this.boundPbjsHandlers.length > 0) return true;
1375
+ const win = typeof window !== "undefined" ? window : {};
1376
+ const pbjs = win[this.config.pbjsGlobalName] || {};
1377
+ if (typeof pbjs.onEvent !== "function") {
1378
+ this.log("WARN", "pbjs.onEvent is not defined. Prebid analytics will not function.");
1379
+ return false;
1436
1380
  }
1437
- const loaderDesc = Object.getOwnPropertyDescriptor(ima, "AdsLoader");
1438
- if (!loaderDesc || loaderDesc.configurable) {
1439
- let _loader = ima.AdsLoader;
1381
+ this.log("DEBUG", "Attaching event listeners to pbjs");
1382
+ const handlerMap = {
1383
+ auctionInit: this.handleAuctionInit.bind(this),
1384
+ auctionEnd: this.handleAuctionEnd.bind(this),
1385
+ bidRequested: this.handleBidRequested.bind(this),
1386
+ bidResponse: this.handleBidResponse.bind(this),
1387
+ bidTimeout: this.handleBidTimeout.bind(this),
1388
+ bidWon: this.handleBidWon.bind(this),
1389
+ noBid: this.handleNoBid.bind(this),
1390
+ adRenderFailed: this.handleAdRenderFailed.bind(this),
1391
+ adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
1392
+ };
1393
+ for (const [name, fn] of Object.entries(handlerMap)) {
1394
+ pbjs.onEvent(name, fn);
1395
+ this.boundPbjsHandlers.push({ event: name, handler: fn });
1396
+ }
1397
+ if (typeof pbjs.getEvents === "function") {
1440
1398
  try {
1441
- Object.defineProperty(ima, "AdsLoader", {
1442
- configurable: true,
1443
- enumerable: true,
1444
- get() {
1445
- return _loader;
1446
- },
1447
- set(val) {
1448
- _loader = val;
1449
- hookAdsLoader(val);
1399
+ const pastEvents = pbjs.getEvents();
1400
+ if (Array.isArray(pastEvents)) {
1401
+ const newPastEvents = pastEvents.slice(this.replayedEventCount);
1402
+ this.replayedEventCount = pastEvents.length;
1403
+ if (newPastEvents.length > 0) {
1404
+ this.log(
1405
+ "DEBUG",
1406
+ `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
1407
+ );
1408
+ for (const ev of newPastEvents) {
1409
+ if (!ev) continue;
1410
+ const eventType = ev.eventType || ev.event || ev.name;
1411
+ const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
1412
+ const handler = handlerMap[eventType];
1413
+ if (handler) {
1414
+ handler(args);
1415
+ }
1416
+ }
1450
1417
  }
1451
- });
1452
- } catch {
1418
+ }
1419
+ } catch (e) {
1420
+ this.log("WARN", "Failed to replay historical events from pbjs.getEvents()", e);
1453
1421
  }
1454
1422
  }
1455
- if (ima.AdsManager) {
1456
- hookAdsManagerPrototype(ima.AdsManager);
1423
+ return true;
1424
+ }
1425
+ disable() {
1426
+ if (!this.isEnabled) return;
1427
+ this.flushAllSlotsTimeInView();
1428
+ this.isEnabled = false;
1429
+ _BidkernelPrebidAnalytics.activeInstances.delete(this);
1430
+ for (const record of this.slotViewabilityRecords.values()) {
1431
+ if (record.dwellTimer) {
1432
+ clearTimeout(record.dwellTimer);
1433
+ record.dwellTimer = null;
1434
+ record.dwellStartedAt = null;
1435
+ }
1436
+ for (const l of record.thresholdListeners) {
1437
+ if (l.timer) {
1438
+ clearTimeout(l.timer);
1439
+ l.timer = null;
1440
+ }
1441
+ }
1457
1442
  }
1458
- const mgrDesc = Object.getOwnPropertyDescriptor(ima, "AdsManager");
1459
- if (!mgrDesc || mgrDesc.configurable) {
1460
- let _mgr = ima.AdsManager;
1443
+ if (this.intersectionObserver) {
1444
+ this.intersectionObserver.disconnect();
1445
+ this.intersectionObserver = null;
1446
+ }
1447
+ this.slotViewabilityRecords.clear();
1448
+ this.elementToSlotId.clear();
1449
+ for (const cleanup of this.videoDetachCleanups) {
1461
1450
  try {
1462
- Object.defineProperty(ima, "AdsManager", {
1463
- configurable: true,
1464
- enumerable: true,
1465
- get() {
1466
- return _mgr;
1467
- },
1468
- set(val) {
1469
- _mgr = val;
1470
- hookAdsManagerPrototype(val);
1471
- }
1472
- });
1451
+ cleanup();
1473
1452
  } catch {
1474
1453
  }
1475
1454
  }
1476
- } catch {
1477
- }
1478
- }
1479
- function initImaInterception() {
1480
- if (typeof window === "undefined") return;
1481
- const win = window;
1482
- if (win.google?.ima) {
1483
- hookImaPrototype(win.google.ima);
1484
- }
1485
- if (win.google && typeof win.google === "object") {
1486
- const imaDesc = Object.getOwnPropertyDescriptor(win.google, "ima");
1487
- if (!imaDesc || imaDesc.configurable) {
1488
- let _ima = win.google.ima;
1455
+ this.videoDetachCleanups.clear();
1456
+ for (const cleanup of Array.from(this.clickDetachCleanups)) {
1489
1457
  try {
1490
- Object.defineProperty(win.google, "ima", {
1491
- configurable: true,
1492
- enumerable: true,
1493
- get() {
1494
- return _ima;
1495
- },
1496
- set(val) {
1497
- _ima = val;
1498
- hookImaPrototype(val);
1499
- }
1500
- });
1458
+ cleanup();
1501
1459
  } catch {
1502
1460
  }
1503
1461
  }
1504
- }
1505
- const googleDesc = Object.getOwnPropertyDescriptor(win, "google");
1506
- if (!googleDesc || googleDesc.configurable) {
1507
- let _google = win.google;
1508
- try {
1509
- Object.defineProperty(win, "google", {
1510
- configurable: true,
1511
- enumerable: true,
1512
- get() {
1513
- return _google;
1514
- },
1515
- set(val) {
1516
- _google = val;
1517
- if (_google && typeof _google === "object") {
1518
- if (_google.ima) {
1519
- hookImaPrototype(_google.ima);
1520
- }
1521
- const iDesc = Object.getOwnPropertyDescriptor(_google, "ima");
1522
- if (!iDesc || iDesc.configurable) {
1523
- let _i = _google.ima;
1524
- try {
1525
- Object.defineProperty(_google, "ima", {
1526
- configurable: true,
1527
- enumerable: true,
1528
- get() {
1529
- return _i;
1530
- },
1531
- set(iv) {
1532
- _i = iv;
1533
- hookImaPrototype(iv);
1534
- }
1535
- });
1536
- } catch {
1537
- }
1538
- }
1539
- }
1462
+ this.clickDetachCleanups.clear();
1463
+ this.slotElements.clear();
1464
+ this.cachedWinningBids.clear();
1465
+ this.slotEmittedImpressionKeys.clear();
1466
+ this.slotLastAuctionIds.clear();
1467
+ this.auctionBidderOutcomes.clear();
1468
+ if (this.flushTimer) {
1469
+ clearInterval(this.flushTimer);
1470
+ this.flushTimer = null;
1471
+ }
1472
+ if (this.revenueFlushTimer) {
1473
+ clearTimeout(this.revenueFlushTimer);
1474
+ this.revenueFlushTimer = null;
1475
+ }
1476
+ if (this.persistedCleanupTimer) {
1477
+ clearTimeout(this.persistedCleanupTimer);
1478
+ this.persistedCleanupTimer = null;
1479
+ }
1480
+ if (typeof window !== "undefined") {
1481
+ window.removeEventListener("pagehide", this.boundFlushBeacon);
1482
+ if (typeof document !== "undefined") {
1483
+ document.removeEventListener("visibilitychange", this.boundVisibilityChange);
1484
+ }
1485
+ }
1486
+ if (this.boundPbjsHandlers.length > 0) {
1487
+ const win = typeof window !== "undefined" ? window : {};
1488
+ const pbjs = win[this.config.pbjsGlobalName] || {};
1489
+ if (typeof pbjs.offEvent === "function") {
1490
+ for (const { event, handler } of this.boundPbjsHandlers) {
1491
+ pbjs.offEvent(event, handler);
1540
1492
  }
1541
- });
1542
- } catch {
1493
+ }
1494
+ this.boundPbjsHandlers = [];
1543
1495
  }
1544
- }
1545
- }
1546
- if (typeof window !== "undefined") {
1547
- initImaInterception();
1548
- }
1549
- var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1550
- static activeInstances = /* @__PURE__ */ new Set();
1551
- static getActiveInstances() {
1552
- return _BidkernelPrebidAnalytics.activeInstances;
1553
- }
1554
- static initImaInterception() {
1555
- initImaInterception();
1556
- }
1557
- /**
1558
- * Allows or forbids localStorage writes for every instance on the page.
1559
- * Collection and delivery are unaffected: only the persisted session id,
1560
- * its timestamp and exit batches are gated.
1561
- */
1562
- static setStorageAllowed(allowed) {
1563
- storageAllowed = allowed;
1564
- }
1565
- static isStorageAllowed() {
1566
- return storageAllowed;
1567
- }
1568
- config;
1569
- queue = [];
1570
- errorCount = 0;
1571
- flushTimer = null;
1572
- boundFlushBeacon;
1573
- boundVisibilityChange;
1574
- isEnabled = false;
1575
- boundPbjsHandlers = [];
1576
- // Captured at enable()/navigate() so events queued before an SPA route
1577
- // change flush with the page they occurred on, not the new URL.
1578
- pageUrl = "";
1579
- deduper = new PrebidEventDeduper();
1580
- consecutiveSendFailures = 0;
1581
- nextSendAllowedAt = 0;
1582
- replayedEventCount = 0;
1583
- revenueFlushTimer = null;
1584
- // localStorage keys of exit batches this instance persisted, so a page that
1585
- // survives its own pagehide/hidden (bfcache restore, tab re-focus) can
1586
- // remove them instead of leaving them for a duplicate resend.
1587
- persistedBatchKeys = [];
1588
- persistedCleanupTimer = null;
1589
- // Viewability & refresh tracking
1590
- intersectionObserver = null;
1591
- slotViewabilityRecords = /* @__PURE__ */ new Map();
1592
- elementToSlotId = /* @__PURE__ */ new Map();
1593
- slotRefreshIndices = /* @__PURE__ */ new Map();
1594
- slotLastAuctionIds = /* @__PURE__ */ new Map();
1595
- pendingThresholdListeners = /* @__PURE__ */ new Map();
1596
- pendingViewableListeners = /* @__PURE__ */ new Map();
1597
- // Element a subscriber named for a slot (a sticky bar or interstitial that
1598
- // renders outside the slot element); wins over document.getElementById.
1599
- slotElements = /* @__PURE__ */ new Map();
1600
- clickDetachCleanups = /* @__PURE__ */ new Set();
1601
- lastClickAt = /* @__PURE__ */ new Map();
1602
- // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
1603
- slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
1604
- // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
1605
- cachedWinningBids = /* @__PURE__ */ new Map();
1606
- // Active video player attachment cleanup routines
1607
- videoDetachCleanups = /* @__PURE__ */ new Set();
1608
- // Compacted bidder participation map per auction: auctionId -> Map<`${bidder}:${adUnitCode}`, BidderOutcomeEntry>
1609
- auctionBidderOutcomes = /* @__PURE__ */ new Map();
1610
- logger;
1611
- constructor(config) {
1612
- if (config.storageAllowed !== void 0) {
1613
- _BidkernelPrebidAnalytics.setStorageAllowed(config.storageAllowed);
1614
- }
1615
- const requestedEndpoint = config.endpoint || "";
1616
- const endpoint = !requestedEndpoint || isTrustedEndpoint(requestedEndpoint) ? requestedEndpoint : "";
1617
- this.config = {
1618
- endpoint,
1619
- propertyId: config.propertyId || "",
1620
- pageviewId: config.pageviewId || generateUUID(),
1621
- sessionId: config.sessionId || getOrCreateSessionId(),
1622
- userId: config.userId || "",
1623
- deviceType: config.deviceType || "desktop",
1624
- country: config.country || "",
1625
- region: config.region || "",
1626
- auctionEnabled: config.auctionEnabled ?? true,
1627
- warningsEnabled: config.warningsEnabled ?? true,
1628
- errorsEnabled: config.errorsEnabled ?? true,
1629
- viewabilityEnabled: config.viewabilityEnabled ?? true,
1630
- storageAllowed: config.storageAllowed ?? true,
1631
- logLevel: config.logLevel || "INFO",
1632
- pbjsGlobalName: config.pbjsGlobalName || "pbjs",
1633
- attachPbjsListeners: config.attachPbjsListeners ?? true,
1634
- revenueFlushDelayMs: Number.isFinite(config.revenueFlushDelayMs) && config.revenueFlushDelayMs >= 0 ? config.revenueFlushDelayMs : REVENUE_FLUSH_DELAY_MS
1635
- };
1636
- this.logger = createLogger({
1637
- prefix: "[bidkernel][prebid-analytics]",
1638
- logLevel: this.config.logLevel
1639
- });
1640
- if (requestedEndpoint && !endpoint) {
1641
- this.log(
1642
- "ERROR",
1643
- `Rejected untrusted analytics endpoint ${requestedEndpoint}: must be an absolute https:// URL without embedded credentials. Events will not be transmitted.`
1644
- );
1645
- }
1646
- this.boundFlushBeacon = () => {
1647
- this.flushAllSlotsTimeInView();
1648
- this.flushBeacon();
1649
- };
1650
- this.boundVisibilityChange = () => this.handleVisibilityChange();
1651
- }
1652
- enable() {
1653
- if (this.isEnabled) return;
1654
- this.isEnabled = true;
1655
- _BidkernelPrebidAnalytics.activeInstances.add(this);
1656
- this.logger.setLevel(this.config.logLevel);
1657
- if (!this.config.endpoint) {
1658
- this.log("WARN", "Endpoint is empty. Analytics events will not be transmitted.");
1659
- }
1660
- if (this.config.viewabilityEnabled) {
1661
- if (typeof IntersectionObserver !== "undefined") {
1662
- this.intersectionObserver = new IntersectionObserver(this.handleIntersection.bind(this), {
1663
- threshold: [0.5]
1664
- });
1665
- for (const record of this.slotViewabilityRecords.values()) {
1666
- if (record.element) {
1667
- this.intersectionObserver.observe(record.element);
1668
- }
1669
- }
1670
- }
1671
- }
1672
- if (this.config.attachPbjsListeners) {
1673
- this.attachPbjs();
1674
- }
1675
- if (typeof window !== "undefined") {
1676
- if (!this.pageUrl) {
1677
- try {
1678
- const u = new URL(window.location.href);
1679
- this.pageUrl = u.origin + u.pathname;
1680
- } catch {
1681
- this.pageUrl = window.location.href;
1682
- }
1683
- }
1684
- window.addEventListener("pagehide", this.boundFlushBeacon);
1685
- if (typeof document !== "undefined") {
1686
- document.addEventListener("visibilitychange", this.boundVisibilityChange);
1687
- }
1688
- this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
1689
- this.resendPersistedBatches();
1690
- initImaInterception();
1691
- if (window.google?.ima) {
1692
- hookImaPrototype(window.google.ima);
1693
- }
1694
- }
1695
- }
1696
- /**
1697
- * Binds the Prebid event handlers on the configured global and replays
1698
- * pbjs.getEvents() history. enable() calls this when attachPbjsListeners is
1699
- * true. An integration that loads Prebid lazily constructs the instance with
1700
- * attachPbjsListeners=false and calls this once pbjs exists; disable() (and
1701
- * therefore navigate()) drops the handlers, so call it again after either.
1702
- * Idempotent. Returns true when handlers are bound after the call, false
1703
- * when the instance is disabled or pbjs.onEvent is not available yet.
1704
- */
1705
- attachPbjs() {
1706
- if (!this.isEnabled) return false;
1707
- if (this.boundPbjsHandlers.length > 0) return true;
1708
- const win = typeof window !== "undefined" ? window : {};
1709
- const pbjs = win[this.config.pbjsGlobalName] || {};
1710
- if (typeof pbjs.onEvent !== "function") {
1711
- this.log("WARN", "pbjs.onEvent is not defined. Prebid analytics will not function.");
1712
- return false;
1713
- }
1714
- this.log("DEBUG", "Attaching event listeners to pbjs");
1715
- const handlerMap = {
1716
- auctionInit: this.handleAuctionInit.bind(this),
1717
- auctionEnd: this.handleAuctionEnd.bind(this),
1718
- bidRequested: this.handleBidRequested.bind(this),
1719
- bidResponse: this.handleBidResponse.bind(this),
1720
- bidTimeout: this.handleBidTimeout.bind(this),
1721
- bidWon: this.handleBidWon.bind(this),
1722
- noBid: this.handleNoBid.bind(this),
1723
- adRenderFailed: this.handleAdRenderFailed.bind(this),
1724
- adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
1725
- };
1726
- for (const [name, fn] of Object.entries(handlerMap)) {
1727
- pbjs.onEvent(name, fn);
1728
- this.boundPbjsHandlers.push({ event: name, handler: fn });
1729
- }
1730
- if (typeof pbjs.getEvents === "function") {
1731
- try {
1732
- const pastEvents = pbjs.getEvents();
1733
- if (Array.isArray(pastEvents)) {
1734
- const newPastEvents = pastEvents.slice(this.replayedEventCount);
1735
- this.replayedEventCount = pastEvents.length;
1736
- if (newPastEvents.length > 0) {
1737
- this.log(
1738
- "DEBUG",
1739
- `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
1740
- );
1741
- for (const ev of newPastEvents) {
1742
- if (!ev) continue;
1743
- const eventType = ev.eventType || ev.event || ev.name;
1744
- const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
1745
- const handler = handlerMap[eventType];
1746
- if (handler) {
1747
- handler(args);
1748
- }
1749
- }
1750
- }
1751
- }
1752
- } catch (e) {
1753
- this.log("WARN", "Failed to replay historical events from pbjs.getEvents()", e);
1754
- }
1755
- }
1756
- return true;
1757
- }
1758
- disable() {
1759
- if (!this.isEnabled) return;
1760
- this.flushAllSlotsTimeInView();
1761
- this.isEnabled = false;
1762
- _BidkernelPrebidAnalytics.activeInstances.delete(this);
1763
- for (const record of this.slotViewabilityRecords.values()) {
1764
- if (record.dwellTimer) {
1765
- clearTimeout(record.dwellTimer);
1766
- record.dwellTimer = null;
1767
- record.dwellStartedAt = null;
1768
- }
1769
- for (const l of record.thresholdListeners) {
1770
- if (l.timer) {
1771
- clearTimeout(l.timer);
1772
- l.timer = null;
1773
- }
1774
- }
1775
- }
1776
- if (this.intersectionObserver) {
1777
- this.intersectionObserver.disconnect();
1778
- this.intersectionObserver = null;
1779
- }
1780
- this.slotViewabilityRecords.clear();
1781
- this.elementToSlotId.clear();
1782
- for (const cleanup of this.videoDetachCleanups) {
1783
- try {
1784
- cleanup();
1785
- } catch {
1786
- }
1787
- }
1788
- this.videoDetachCleanups.clear();
1789
- for (const cleanup of Array.from(this.clickDetachCleanups)) {
1790
- try {
1791
- cleanup();
1792
- } catch {
1793
- }
1794
- }
1795
- this.clickDetachCleanups.clear();
1796
- this.slotElements.clear();
1797
- this.cachedWinningBids.clear();
1798
- this.slotEmittedImpressionKeys.clear();
1799
- this.slotLastAuctionIds.clear();
1800
- this.auctionBidderOutcomes.clear();
1801
- if (this.flushTimer) {
1802
- clearInterval(this.flushTimer);
1803
- this.flushTimer = null;
1804
- }
1805
- if (this.revenueFlushTimer) {
1806
- clearTimeout(this.revenueFlushTimer);
1807
- this.revenueFlushTimer = null;
1808
- }
1809
- if (this.persistedCleanupTimer) {
1810
- clearTimeout(this.persistedCleanupTimer);
1811
- this.persistedCleanupTimer = null;
1812
- }
1813
- if (typeof window !== "undefined") {
1814
- window.removeEventListener("pagehide", this.boundFlushBeacon);
1815
- if (typeof document !== "undefined") {
1816
- document.removeEventListener("visibilitychange", this.boundVisibilityChange);
1817
- }
1818
- }
1819
- if (this.boundPbjsHandlers.length > 0) {
1820
- const win = typeof window !== "undefined" ? window : {};
1821
- const pbjs = win[this.config.pbjsGlobalName] || {};
1822
- if (typeof pbjs.offEvent === "function") {
1823
- for (const { event, handler } of this.boundPbjsHandlers) {
1824
- pbjs.offEvent(event, handler);
1825
- }
1826
- }
1827
- this.boundPbjsHandlers = [];
1828
- }
1829
- this.flush();
1496
+ this.flush();
1830
1497
  }
1831
1498
  handleVisibilityChange() {
1832
1499
  if (typeof document === "undefined") return;
@@ -2346,74 +2013,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2346
2013
  while (this.cachedWinningBids.size > MAX_CACHED_BID_KEYS) {
2347
2014
  const oldest = this.cachedWinningBids.keys().next();
2348
2015
  if (oldest.done) break;
2349
- this.cachedWinningBids.delete(oldest.value);
2350
- }
2351
- }
2352
- /**
2353
- * Searches cached winning bids for an entry matching the given filters (creativeId,
2354
- * adId, or mediaType), falling back to the most recent winning video bid.
2355
- */
2356
- findCachedWinningBid(filter) {
2357
- const now = Date.now();
2358
- const isConsumed = (entry) => {
2359
- return this.hasImpressionEmitted(
2360
- entry.adUnitCode,
2361
- void 0,
2362
- entry.auctionId,
2363
- entry.bidTrace.creativeId
2364
- );
2365
- };
2366
- if (filter?.creativeId || filter?.adId) {
2367
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2368
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2369
- if (filter.creativeId && (entry.bidTrace.creativeId === filter.creativeId || entry.rawBid?.creativeId === filter.creativeId) || filter.adId && (entry.rawBid?.adId === filter.adId || entry.adUnitCode === filter.adId)) {
2370
- return entry;
2371
- }
2372
- }
2373
- }
2374
- let bestUnconsumedMatch;
2375
- if (filter?.mediaType) {
2376
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2377
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2378
- const isEntryVideo = entry.bidTrace.mediaType === filter.mediaType || entry.rawBid?.mediaType === filter.mediaType || filter.mediaType === "video" && entry.rawBid?.mediaTypes?.video !== void 0;
2379
- if (isEntryVideo) {
2380
- if (!isConsumed(entry)) {
2381
- if (!bestUnconsumedMatch || entry.timestamp < bestUnconsumedMatch.timestamp) {
2382
- bestUnconsumedMatch = entry;
2383
- }
2384
- }
2385
- }
2386
- }
2387
- if (bestUnconsumedMatch) return bestUnconsumedMatch;
2388
- let bestFallbackMatch;
2389
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2390
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2391
- const isEntryVideo = entry.bidTrace.mediaType === filter.mediaType || entry.rawBid?.mediaType === filter.mediaType || filter.mediaType === "video" && entry.rawBid?.mediaTypes?.video !== void 0;
2392
- if (isEntryVideo) {
2393
- if (!bestFallbackMatch || entry.timestamp > bestFallbackMatch.timestamp) {
2394
- bestFallbackMatch = entry;
2395
- }
2396
- }
2397
- }
2398
- if (bestFallbackMatch) return bestFallbackMatch;
2399
- }
2400
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2401
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2402
- if (!isConsumed(entry)) {
2403
- if (!bestUnconsumedMatch || entry.timestamp > bestUnconsumedMatch.timestamp) {
2404
- bestUnconsumedMatch = entry;
2405
- }
2406
- }
2407
- }
2408
- if (bestUnconsumedMatch) return bestUnconsumedMatch;
2409
- let bestRecent;
2410
- for (const entry of Array.from(this.cachedWinningBids.values())) {
2411
- if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
2412
- if (!bestRecent || entry.timestamp > bestRecent.timestamp) {
2413
- bestRecent = entry;
2414
- }
2016
+ this.cachedWinningBids.delete(oldest.value);
2415
2017
  }
2416
- return bestRecent;
2417
2018
  }
2418
2019
  /** Records a dedup key, evicting oldest-first at the cap. */
2419
2020
  addImpressionKey(key) {
@@ -2562,1301 +2163,1672 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2562
2163
  }
2563
2164
  return true;
2564
2165
  }
2565
- /**
2566
- * Bridges Google IMA SDK AdsManager events to Bidkernel analytics.
2567
- * Defers IMPRESSION emission until AdEvent.STARTED (or IMPRESSION),
2568
- * tracks milestones (FIRST_QUARTILE, MIDPOINT, THIRD_QUARTILE, COMPLETE),
2569
- * and handles AD_ERROR.
2570
- */
2571
- attachImaAdsManager(adsManager, options) {
2572
- if (!adsManager || typeof adsManager.addEventListener !== "function") {
2573
- this.log("WARN", "Invalid AdsManager passed to attachImaAdsManager");
2574
- return () => {
2575
- };
2576
- }
2577
- if (!adsManager.__bidkernelAttachedInstances) {
2578
- try {
2579
- Object.defineProperty(adsManager, "__bidkernelAttachedInstances", {
2580
- value: /* @__PURE__ */ new Map(),
2581
- configurable: true,
2582
- writable: true
2583
- });
2584
- } catch {
2585
- adsManager.__bidkernelAttachedInstances = /* @__PURE__ */ new Map();
2586
- }
2587
- }
2588
- const attached = adsManager.__bidkernelAttachedInstances;
2589
- const previous = attached.get(this);
2590
- if (previous) {
2591
- if (!options) return () => {
2592
- };
2593
- previous();
2594
- }
2595
- const slotId = options?.slotId || options?.adUnitCode || "video";
2596
- const adUnitCode = options?.adUnitCode || slotId;
2597
- const auctionId = options?.auctionId || "";
2598
- const transactionId = options?.transactionId || "";
2599
- const getWinningBid = (adData) => {
2600
- 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);
2601
- if (!cached) {
2602
- cached = this.findCachedWinningBid({
2603
- creativeId: adData?.creativeId,
2604
- adId: adData?.adId,
2605
- mediaType: "video"
2606
- });
2607
- }
2608
- return cached;
2166
+ onTimeInViewThreshold(slotId, thresholdMs, callback) {
2167
+ const listener = {
2168
+ thresholdMs,
2169
+ callback,
2170
+ timer: null,
2171
+ fired: false
2609
2172
  };
2610
- let adStarted = false;
2611
- const onAdStartedOrImpression = (event) => {
2612
- adStarted = true;
2613
- this.log("DEBUG", "IMA AdEvent.STARTED / IMPRESSION received", event);
2614
- const ad = typeof event?.getAd === "function" ? event.getAd() : event?.ad;
2615
- const adData = {};
2616
- if (ad) {
2617
- adData.creativeId = (typeof ad.getCreativeId === "function" ? ad.getCreativeId() : ad.creativeId) || "";
2618
- adData.adId = (typeof ad.getAdId === "function" ? ad.getAdId() : ad.id) || "";
2619
- adData.title = (typeof ad.getTitle === "function" ? ad.getTitle() : ad.title) || "";
2620
- adData.duration = typeof ad.getDuration === "function" ? ad.getDuration() : ad.duration;
2621
- adData.advertiserName = typeof ad.getAdvertiserName === "function" ? ad.getAdvertiserName() : "";
2622
- }
2623
- const cached = getWinningBid(adData);
2624
- const resolvedSlotId = options?.slotId && options.slotId !== "video" ? options.slotId : options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || options?.slotId || options?.adUnitCode || "video";
2625
- const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || resolvedSlotId;
2626
- const resolvedAuctionId = auctionId || cached?.auctionId || "";
2627
- const resolvedTransactionId = transactionId || cached?.transactionId || "";
2628
- const mergedBid = {
2629
- ...cached?.rawBid || options?.bid,
2630
- mediaType: "video",
2631
- creativeId: adData.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || ""
2632
- };
2633
- const emitted = this.recordImpression(resolvedSlotId, {
2634
- adUnitCode: resolvedAdUnitCode,
2635
- auctionId: resolvedAuctionId,
2636
- transactionId: resolvedTransactionId,
2637
- mediaType: "video",
2638
- bid: mergedBid,
2639
- metadata: {
2640
- ...options?.metadata,
2641
- media_type: "video",
2642
- ...adData.title ? { ad_title: adData.title } : {},
2643
- ...Number.isFinite(adData.duration) ? { ad_duration_s: String(adData.duration) } : {}
2644
- }
2645
- });
2646
- if (emitted && options?.onImpression) {
2173
+ const record = this.slotViewabilityRecords.get(slotId);
2174
+ if (record) {
2175
+ record.thresholdListeners.add(listener);
2176
+ const currentTotal = this.getCurrentTimeInView(record);
2177
+ if (currentTotal >= thresholdMs) {
2178
+ listener.fired = true;
2647
2179
  try {
2648
- options.onImpression(resolvedSlotId, { ad, bid: mergedBid });
2180
+ callback(slotId, currentTotal);
2649
2181
  } catch (e) {
2650
- this.log("ERROR", "Error in onImpression callback", e);
2182
+ this.log("ERROR", "Error in threshold callback", e);
2651
2183
  }
2184
+ } else if (record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
2185
+ const remaining = thresholdMs - currentTotal;
2186
+ listener.timer = setTimeout(() => {
2187
+ listener.timer = null;
2188
+ if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
2189
+ const nowTotal = this.getCurrentTimeInView(record);
2190
+ if (nowTotal >= thresholdMs) {
2191
+ listener.fired = true;
2192
+ try {
2193
+ callback(slotId, nowTotal);
2194
+ } catch (e) {
2195
+ this.log("ERROR", "Error in threshold callback", e);
2196
+ }
2197
+ }
2198
+ }
2199
+ }, remaining);
2652
2200
  }
2653
- };
2654
- const onMilestone = (milestone) => {
2655
- this.log("DEBUG", `IMA AdEvent milestone: ${milestone}`);
2656
- if (options?.onMilestone) {
2657
- try {
2658
- options.onMilestone(milestone, slotId);
2659
- } catch (e) {
2660
- this.log("ERROR", "Error in onMilestone callback", e);
2661
- }
2201
+ } else {
2202
+ if (!this.pendingThresholdListeners.has(slotId)) {
2203
+ this.pendingThresholdListeners.set(slotId, /* @__PURE__ */ new Set());
2204
+ }
2205
+ this.pendingThresholdListeners.get(slotId).add(listener);
2206
+ }
2207
+ return () => {
2208
+ if (listener.timer) {
2209
+ clearTimeout(listener.timer);
2210
+ listener.timer = null;
2211
+ }
2212
+ if (record) {
2213
+ record.thresholdListeners.delete(listener);
2214
+ }
2215
+ const pending = this.pendingThresholdListeners.get(slotId);
2216
+ if (pending) {
2217
+ pending.delete(listener);
2662
2218
  }
2663
2219
  };
2664
- const onAdClick = (event) => {
2665
- this.log("DEBUG", "IMA AdEvent.CLICK received", event);
2666
- const cached = getWinningBid();
2667
- const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || slotId;
2668
- this.enqueue(TraceEventType.CLICK, "click", {
2669
- auctionId: auctionId || cached?.auctionId,
2670
- transactionId: transactionId || cached?.transactionId,
2671
- adUnitCode: resolvedAdUnitCode,
2672
- bid: cached?.bidTrace || options?.bid,
2673
- metadata: {
2674
- ...options?.metadata,
2675
- media_type: "video"
2220
+ }
2221
+ getViewabilityState(slotId) {
2222
+ const record = this.slotViewabilityRecords.get(slotId);
2223
+ if (!record) return void 0;
2224
+ const timeInViewMs = Math.round(this.getCurrentTimeInView(record));
2225
+ return {
2226
+ inView: record.inView,
2227
+ viewable: record.viewableFired,
2228
+ timeInViewMs,
2229
+ refreshIndex: record.refreshIndex
2230
+ };
2231
+ }
2232
+ /** The validated ingest endpoint in use, or "" when none passed validation. */
2233
+ getEndpoint() {
2234
+ return this.config.endpoint;
2235
+ }
2236
+ getRefreshIndex(slotId) {
2237
+ return this.slotRefreshIndices.get(slotId) ?? 0;
2238
+ }
2239
+ triggerSlotRefresh(slotId, options) {
2240
+ const record = this.slotViewabilityRecords.get(slotId);
2241
+ if (record && record.element) {
2242
+ this.observeSlot(record.element, slotId, options);
2243
+ }
2244
+ }
2245
+ trackRawEvent(level, eventName, data) {
2246
+ const eventType = EVENT_NAME_TO_TYPE[eventName] ?? (level === "ERROR" ? TraceEventType.ERROR : TraceEventType.TRACE_EVENT_TYPE_UNSPECIFIED);
2247
+ this.enqueue(eventType, eventName, data, level);
2248
+ }
2249
+ setUserId(userId) {
2250
+ this.config.userId = userId;
2251
+ }
2252
+ setSessionId(sessionId) {
2253
+ this.config.sessionId = sessionId;
2254
+ }
2255
+ navigate(pageviewId, pageUrl) {
2256
+ this.disable();
2257
+ this.slotRefreshIndices.clear();
2258
+ this.pendingThresholdListeners.clear();
2259
+ this.config.pageviewId = pageviewId || generateUUID();
2260
+ if (pageUrl) {
2261
+ try {
2262
+ const u = new URL(pageUrl);
2263
+ this.pageUrl = u.origin + u.pathname;
2264
+ } catch {
2265
+ this.pageUrl = pageUrl;
2266
+ }
2267
+ } else {
2268
+ this.pageUrl = "";
2269
+ }
2270
+ this.logger.resetTiming();
2271
+ extendSession();
2272
+ this.enable();
2273
+ }
2274
+ handleAuctionInit(data) {
2275
+ if (this.isDuplicate("auctionInit", data)) return;
2276
+ this.log("DEBUG", "auctionInit", data);
2277
+ const auctionId = data.auctionId || "";
2278
+ if (auctionId) {
2279
+ this.auctionBidderOutcomes.delete(auctionId);
2280
+ this.getOrCreateAuctionOutcomes(auctionId);
2281
+ }
2282
+ this.enqueue(TraceEventType.AUCTION_START, "auctionStart", {
2283
+ auctionId,
2284
+ transactionId: data.transactionId || ""
2285
+ });
2286
+ }
2287
+ handleAuctionEnd(data) {
2288
+ if (this.isDuplicate("auctionEnd", data)) return;
2289
+ this.log("DEBUG", "auctionEnd", data);
2290
+ const auctionId = data.auctionId || "";
2291
+ const outcomesMap = auctionId ? this.auctionBidderOutcomes.get(auctionId) : void 0;
2292
+ const bidderOutcomes = outcomesMap ? Array.from(outcomesMap.values()) : [];
2293
+ if (auctionId) {
2294
+ this.auctionBidderOutcomes.delete(auctionId);
2295
+ }
2296
+ this.enqueue(TraceEventType.AUCTION_END, "auctionEnd", {
2297
+ auctionId,
2298
+ transactionId: data.transactionId || "",
2299
+ bidderOutcomes
2300
+ });
2301
+ }
2302
+ handleBidRequested(data) {
2303
+ if (this.isDuplicate("bidRequested", data)) return;
2304
+ this.log("DEBUG", "bidRequested", data);
2305
+ const auctionId = data.auctionId || "";
2306
+ const bidder = data.bidderCode || data.bidder || "";
2307
+ if (!auctionId || !bidder) return;
2308
+ const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
2309
+ if (Array.isArray(data.bids)) {
2310
+ data.bids.forEach((bid) => {
2311
+ const adUnitCode = bid.adUnitCode || "";
2312
+ const mediaTypes = Object.keys(bid.mediaTypes || {});
2313
+ const mediaType = mediaTypes.length > 0 ? mediaTypes[0] : bid.mediaType || "banner";
2314
+ const key = `${bidder}:${adUnitCode}`;
2315
+ if (!auctionMap.has(key)) {
2316
+ auctionMap.set(key, {
2317
+ bidder,
2318
+ adUnitCode,
2319
+ mediaType,
2320
+ outcome: BidderOutcome.BIDDER_OUTCOME_NO_RESPONSE,
2321
+ latencyMs: 0
2322
+ });
2676
2323
  }
2677
2324
  });
2678
- };
2679
- const onAdError = (event) => {
2680
- this.log("WARN", "IMA AdErrorEvent received", event);
2681
- const err = typeof event?.getError === "function" ? event.getError() : event?.error || event;
2682
- const msg = (err && typeof err.getMessage === "function" ? err.getMessage() : err?.message) || String(err || "IMA ad error");
2683
- const code = (err && typeof err.getErrorCode === "function" ? err.getErrorCode() : err?.code) || "";
2684
- const cached = getWinningBid();
2685
- const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || slotId;
2686
- this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
2687
- auctionId: auctionId || cached?.auctionId,
2688
- transactionId: transactionId || cached?.transactionId,
2689
- adUnitCode: resolvedAdUnitCode,
2690
- bid: cached?.bidTrace || options?.bid,
2691
- metadata: {
2692
- ...options?.metadata,
2693
- reason: adStarted ? "ima_playback_error" : "ima_ad_error",
2694
- playback_phase: adStarted ? "post_start" : "pre_start",
2695
- message: msg,
2696
- ...code ? { ima_error_code: String(code) } : {}
2697
- },
2698
- error: typeof err === "object" ? err : new Error(msg)
2325
+ }
2326
+ }
2327
+ handleBidResponse(data) {
2328
+ if (this.isDuplicate("bidResponse", data)) return;
2329
+ this.log("DEBUG", "bidResponse", data);
2330
+ const auctionId = data.auctionId || "";
2331
+ const bidder = data.bidderCode || data.bidder || "";
2332
+ const adUnitCode = data.adUnitCode || "";
2333
+ const cpm = Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0;
2334
+ const latencyMs = Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0;
2335
+ const resMediaTypes = Object.keys(data.mediaTypes || {});
2336
+ const mediaType = data.mediaType || (data.mediaTypes?.video ? "video" : resMediaTypes.length > 0 ? resMediaTypes[0] : "banner");
2337
+ if (auctionId && bidder) {
2338
+ const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
2339
+ const key = `${bidder}:${adUnitCode}`;
2340
+ const entry = auctionMap.get(key);
2341
+ const outcome = cpm > 0 ? BidderOutcome.BIDDER_OUTCOME_BID : BidderOutcome.BIDDER_OUTCOME_NO_BID;
2342
+ if (entry) {
2343
+ entry.outcome = outcome;
2344
+ entry.latencyMs = latencyMs;
2345
+ if (mediaType) entry.mediaType = mediaType;
2346
+ } else {
2347
+ auctionMap.set(key, {
2348
+ bidder,
2349
+ adUnitCode,
2350
+ mediaType,
2351
+ outcome,
2352
+ latencyMs
2353
+ });
2354
+ }
2355
+ }
2356
+ if (cpm > 0) {
2357
+ this.enqueue(TraceEventType.BID_RESPONSE, "bidResponse", {
2358
+ auctionId,
2359
+ transactionId: data.transactionId || "",
2360
+ adUnitCode,
2361
+ bid: {
2362
+ bidder,
2363
+ cpm,
2364
+ currency: data.originalCurrency ?? data.currency ?? "USD",
2365
+ ...parseBidDimensions(data),
2366
+ dealId: data.dealId || "",
2367
+ mediaType,
2368
+ latencyMs,
2369
+ advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
2370
+ creativeId: data.creativeId || ""
2371
+ }
2699
2372
  });
2700
- if (options?.onError) {
2701
- try {
2702
- options.onError(err);
2703
- } catch (e) {
2704
- this.log("ERROR", "Error in onError callback", e);
2373
+ }
2374
+ }
2375
+ handleBidTimeout(data) {
2376
+ if (this.isDuplicate("bidTimeout", data)) return;
2377
+ this.log("DEBUG", "bidTimeout", data);
2378
+ const items = Array.isArray(data) ? data : data ? [data] : [];
2379
+ for (const t of items) {
2380
+ const auctionId = t.auctionId || "";
2381
+ const bidder = t.bidderCode || t.bidder || "";
2382
+ const adUnitCode = t.adUnitCode || "";
2383
+ const latencyMs = Number.isFinite(t.timeout) ? t.timeout : 0;
2384
+ if (auctionId && bidder) {
2385
+ const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
2386
+ const key = `${bidder}:${adUnitCode}`;
2387
+ const entry = auctionMap.get(key);
2388
+ if (entry) {
2389
+ entry.outcome = BidderOutcome.BIDDER_OUTCOME_TIMEOUT;
2390
+ if (latencyMs > 0) entry.latencyMs = latencyMs;
2391
+ } else {
2392
+ auctionMap.set(key, {
2393
+ bidder,
2394
+ adUnitCode,
2395
+ mediaType: "banner",
2396
+ outcome: BidderOutcome.BIDDER_OUTCOME_TIMEOUT,
2397
+ latencyMs
2398
+ });
2705
2399
  }
2706
2400
  }
2707
- };
2708
- const googleIma = typeof window !== "undefined" ? window.google?.ima : void 0;
2709
- const adEventType = googleIma?.AdEvent?.Type || {};
2710
- const adErrorEventType = googleIma?.AdErrorEvent?.Type || {};
2711
- const rawListeners = [
2712
- { type: adEventType.STARTED || "started", handler: onAdStartedOrImpression },
2713
- { type: adEventType.IMPRESSION || "impression", handler: onAdStartedOrImpression },
2714
- {
2715
- type: adEventType.FIRST_QUARTILE || "firstQuartile",
2716
- handler: () => onMilestone("firstQuartile")
2717
- },
2718
- { type: adEventType.MIDPOINT || "midpoint", handler: () => onMilestone("midpoint") },
2719
- {
2720
- type: adEventType.THIRD_QUARTILE || "thirdQuartile",
2721
- handler: () => onMilestone("thirdQuartile")
2722
- },
2723
- { type: adEventType.COMPLETE || "complete", handler: () => onMilestone("complete") },
2724
- { type: adEventType.CLICK || "click", handler: onAdClick },
2725
- { type: adErrorEventType.AD_ERROR || "adError", handler: onAdError }
2726
- ];
2727
- const seenListenerTypes = /* @__PURE__ */ new Set();
2728
- const listeners = [];
2729
- for (const l of rawListeners) {
2730
- if (!seenListenerTypes.has(l.type)) {
2731
- seenListenerTypes.add(l.type);
2732
- listeners.push(l);
2733
- }
2734
2401
  }
2735
- for (const { type, handler } of listeners) {
2736
- try {
2737
- adsManager.addEventListener(type, handler);
2738
- } catch {
2402
+ }
2403
+ handleBidWon(data) {
2404
+ if (this.isDuplicate("bidWon", data)) return;
2405
+ this.log("DEBUG", "bidWon", data);
2406
+ const auctionId = data.auctionId || "";
2407
+ const transactionId = data.transactionId || "";
2408
+ const adUnitCode = data.adUnitCode || data.adId || "";
2409
+ const wonMediaTypes = Object.keys(data.mediaTypes || {});
2410
+ const resolvedMediaType = data.mediaType || (data.mediaTypes?.video ? "video" : wonMediaTypes.length > 0 ? wonMediaTypes[0] : "banner");
2411
+ const bidTrace = {
2412
+ bidder: data.bidderCode || data.bidder || "",
2413
+ cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
2414
+ currency: data.originalCurrency ?? data.currency ?? "USD",
2415
+ ...parseBidDimensions(data),
2416
+ dealId: data.dealId || "",
2417
+ mediaType: resolvedMediaType,
2418
+ latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
2419
+ advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
2420
+ creativeId: data.creativeId || ""
2421
+ };
2422
+ if (adUnitCode) {
2423
+ const cachedEntry = {
2424
+ bidTrace,
2425
+ auctionId,
2426
+ transactionId,
2427
+ adUnitCode,
2428
+ rawBid: data,
2429
+ timestamp: Date.now()
2430
+ };
2431
+ this.cachedWinningBids.set(adUnitCode, cachedEntry);
2432
+ if (data.adId && data.adId !== adUnitCode) {
2433
+ this.cachedWinningBids.set(data.adId, cachedEntry);
2739
2434
  }
2740
- }
2741
- const cleanup = () => {
2742
- attached.delete(this);
2743
- for (const { type, handler } of listeners) {
2744
- try {
2745
- adsManager.removeEventListener(type, handler);
2746
- } catch {
2435
+ if (auctionId) {
2436
+ this.cachedWinningBids.set(`${auctionId}:${adUnitCode}`, cachedEntry);
2437
+ if (data.adId && data.adId !== adUnitCode) {
2438
+ this.cachedWinningBids.set(`${auctionId}:${data.adId}`, cachedEntry);
2747
2439
  }
2748
2440
  }
2749
- this.videoDetachCleanups.delete(cleanup);
2750
- };
2751
- attached.set(this, cleanup);
2752
- this.videoDetachCleanups.add(cleanup);
2753
- return cleanup;
2754
- }
2755
- /**
2756
- * Attaches render hooks and viewability tracking to a video player.
2757
- * Supports HTML5 <video> elements, container elements, or Google IMA AdsManager.
2758
- */
2759
- attachVideoPlayer(target, options) {
2760
- if (!target) {
2761
- this.log("WARN", "attachVideoPlayer called with null/undefined target");
2762
- return () => {
2763
- };
2441
+ this.pruneCachedWinningBids();
2764
2442
  }
2765
- let el = null;
2766
- if (typeof target === "string") {
2767
- if (typeof document !== "undefined") {
2768
- try {
2769
- el = document.querySelector(target) || document.getElementById(target);
2770
- } catch {
2771
- el = document.getElementById(target);
2772
- }
2773
- }
2774
- } else if (typeof HTMLElement !== "undefined" && target instanceof HTMLElement) {
2775
- el = target;
2776
- } else if (target && typeof target === "object" && target.nodeType === 1) {
2777
- el = target;
2778
- } else if (
2779
- // Check if target is an IMA AdsManager
2780
- typeof target.addEventListener === "function" && (typeof target.init === "function" || typeof target.start === "function" || typeof target.getCuePoints === "function" || typeof target.getVolume === "function" || target.__imaAdsManager)
2781
- ) {
2782
- return this.attachImaAdsManager(target, options);
2783
- } else if (target && typeof target.addEventListener === "function") {
2784
- el = target;
2785
- }
2786
- const slotId = options?.slotId || options?.adUnitCode || (el ? el.id : "") || "video";
2787
- const adUnitCode = options?.adUnitCode || slotId;
2788
- const auctionId = options?.auctionId || "";
2789
- const transactionId = options?.transactionId || "";
2790
- const getWinningBid = () => {
2791
- 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);
2792
- };
2793
- let videoEl = null;
2794
- if (el) {
2795
- if (el.tagName && el.tagName.toLowerCase() === "video") {
2796
- videoEl = el;
2443
+ this.enqueue(TraceEventType.BID_WIN, "bidWon", {
2444
+ auctionId,
2445
+ transactionId,
2446
+ adUnitCode,
2447
+ bid: bidTrace
2448
+ });
2449
+ }
2450
+ handleNoBid(data) {
2451
+ if (this.isDuplicate("noBid", data)) return;
2452
+ this.log("DEBUG", "noBid", data);
2453
+ const auctionId = data.auctionId || "";
2454
+ const bidder = data.bidderCode || data.bidder || "";
2455
+ const adUnitCode = data.adUnitCode || "";
2456
+ if (auctionId && bidder) {
2457
+ const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
2458
+ const key = `${bidder}:${adUnitCode}`;
2459
+ const entry = auctionMap.get(key);
2460
+ if (entry) {
2461
+ if (entry.outcome === BidderOutcome.BIDDER_OUTCOME_NO_RESPONSE) {
2462
+ entry.outcome = BidderOutcome.BIDDER_OUTCOME_NO_BID;
2463
+ }
2797
2464
  } else {
2798
- videoEl = el.querySelector("video");
2465
+ auctionMap.set(key, {
2466
+ bidder,
2467
+ adUnitCode,
2468
+ mediaType: "banner",
2469
+ outcome: BidderOutcome.BIDDER_OUTCOME_NO_BID,
2470
+ latencyMs: 0
2471
+ });
2799
2472
  }
2800
2473
  }
2801
- const quartilesFired = {
2802
- q1: false,
2803
- q2: false,
2804
- q3: false,
2805
- q4: false
2474
+ }
2475
+ handleAdRenderFailed(data) {
2476
+ if (this.isDuplicate("adRenderFailed", data)) return;
2477
+ this.log("DEBUG", "adRenderFailed", data);
2478
+ const bid = data.bid || {};
2479
+ this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
2480
+ auctionId: bid.auctionId || data.auctionId || "",
2481
+ transactionId: bid.transactionId || data.transactionId || "",
2482
+ adUnitCode: bid.adUnitCode || data.adUnitCode || "",
2483
+ bid: {
2484
+ bidder: bid.bidderCode || bid.bidder || data.bidderCode || data.bidder || ""
2485
+ },
2486
+ metadata: {
2487
+ reason: String(data.reason || ""),
2488
+ message: String(data.message || data.error?.message || "")
2489
+ },
2490
+ error: data.error || (data.message ? new Error(String(data.message)) : void 0)
2491
+ });
2492
+ }
2493
+ // Prebid's adRenderSucceeded payload is { doc, bid, adId }: the winning bid
2494
+ // lives under data.bid, unlike the flat bidWon payload.
2495
+ handleAdRenderSucceeded(data) {
2496
+ if (this.isDuplicate("adRenderSucceeded", data)) return;
2497
+ this.log("DEBUG", "adRenderSucceeded", data);
2498
+ const bid = data.bid || data || {};
2499
+ const adUnitCode = data.adUnitCode || bid.adUnitCode || data.adId || bid.adId || "";
2500
+ const auctionId = bid.auctionId || data.auctionId || "";
2501
+ const transactionId = bid.transactionId || data.transactionId || "";
2502
+ const mediaType = bid.mediaType || "banner";
2503
+ const slotKey = data.adUnitCode || bid.adUnitCode || "";
2504
+ const altKey = data.adId || bid.adId || "";
2505
+ const cached = (auctionId && slotKey ? this.getCachedBid(`${auctionId}:${slotKey}`) : void 0) || (auctionId && altKey ? this.getCachedBid(`${auctionId}:${altKey}`) : void 0) || (slotKey ? this.getCachedBid(slotKey) : void 0) || (altKey ? this.getCachedBid(altKey) : void 0);
2506
+ const isVideo = mediaType === "video" || bid.mediaType === "video" || bid.mediaTypes?.video !== void 0 || cached?.bidTrace?.mediaType === "video" || cached?.rawBid?.mediaType === "video" || adUnitCode && this.slotViewabilityRecords.get(adUnitCode)?.mediaType === "video";
2507
+ const resolvedMediaType = isVideo ? "video" : mediaType || cached?.bidTrace?.mediaType || "banner";
2508
+ const bidTrace = {
2509
+ bidder: bid.bidderCode || bid.bidder || cached?.bidTrace?.bidder || "",
2510
+ cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : cached?.bidTrace?.cpm ?? 0,
2511
+ currency: bid.originalCurrency ?? bid.currency ?? cached?.bidTrace?.currency ?? "USD",
2512
+ ...parseBidDimensions(bid).width ? parseBidDimensions(bid) : cached?.bidTrace ? { width: cached.bidTrace.width, height: cached.bidTrace.height } : parseBidDimensions(bid),
2513
+ dealId: bid.dealId || cached?.bidTrace?.dealId || "",
2514
+ mediaType: resolvedMediaType,
2515
+ latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : cached?.bidTrace?.latencyMs ?? 0,
2516
+ advertiserDomain: bid.meta?.advertiserDomains?.[0] || cached?.bidTrace?.advertiserDomain || "",
2517
+ creativeId: bid.creativeId || cached?.bidTrace?.creativeId || ""
2806
2518
  };
2807
- const triggerPlaybackImpression = (activeVideo) => {
2808
- const vEl = activeVideo || videoEl || (el?.querySelector ? el.querySelector("video") : null);
2809
- const cached = getWinningBid();
2810
- const mergedBid = {
2811
- ...cached?.rawBid || options?.bid,
2812
- mediaType: "video"
2813
- };
2814
- const emitted = this.recordImpression(slotId, {
2519
+ const resolvedAuctionId = auctionId || cached?.auctionId || "";
2520
+ const resolvedTransactionId = transactionId || cached?.transactionId || "";
2521
+ const record = adUnitCode ? this.slotViewabilityRecords.get(adUnitCode) : void 0;
2522
+ const isDifferentAuction = record && resolvedAuctionId && record.auctionId && record.auctionId !== resolvedAuctionId;
2523
+ const isRefresh = Boolean(adUnitCode && record && isDifferentAuction);
2524
+ if (isRefresh) {
2525
+ this.flushSlotTimeInView(adUnitCode);
2526
+ const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0)) + 1;
2527
+ this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
2528
+ this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
2529
+ if (record) {
2530
+ record.refreshIndex = nextRefreshIndex;
2531
+ record.auctionId = resolvedAuctionId;
2532
+ record.transactionId = resolvedTransactionId;
2533
+ record.bidPayload = bidTrace;
2534
+ record.viewableFired = false;
2535
+ record.accumulatedTimeInViewMs = 0;
2536
+ }
2537
+ this.enqueue(TraceEventType.REFRESH, "refresh", {
2538
+ auctionId: resolvedAuctionId,
2539
+ transactionId: resolvedTransactionId,
2815
2540
  adUnitCode,
2816
- auctionId: auctionId || cached?.auctionId,
2817
- transactionId: transactionId || cached?.transactionId,
2818
- mediaType: "video",
2819
- bid: mergedBid,
2820
- metadata: {
2821
- ...options?.metadata,
2822
- media_type: "video",
2823
- ...vEl && Number.isFinite(vEl.duration) ? { video_duration_s: String(Math.round(vEl.duration)) } : {}
2824
- }
2541
+ bid: bidTrace,
2542
+ metadata: { refresh_index: String(nextRefreshIndex) }
2825
2543
  });
2826
- if (emitted && options?.onImpression) {
2827
- try {
2828
- options.onImpression(slotId, { element: vEl || el, bid: mergedBid });
2829
- } catch (e) {
2830
- this.log("ERROR", "Error in onImpression callback", e);
2831
- }
2832
- }
2833
- };
2834
- const handlePlaying = (e) => {
2835
- this.log("DEBUG", "HTML5 video playing event received");
2836
- const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2837
- triggerPlaybackImpression(targetVideo);
2838
- };
2839
- const handleTimeUpdate = (e) => {
2840
- const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2841
- if (targetVideo && targetVideo.currentTime > 0) {
2842
- triggerPlaybackImpression(targetVideo);
2843
- if (Number.isFinite(targetVideo.duration) && targetVideo.duration > 0) {
2844
- const progress = targetVideo.currentTime / targetVideo.duration;
2845
- if (progress >= 0.25 && !quartilesFired.q1) {
2846
- quartilesFired.q1 = true;
2847
- options?.onMilestone?.("firstQuartile", slotId);
2848
- }
2849
- if (progress >= 0.5 && !quartilesFired.q2) {
2850
- quartilesFired.q2 = true;
2851
- options?.onMilestone?.("midpoint", slotId);
2852
- }
2853
- if (progress >= 0.75 && !quartilesFired.q3) {
2854
- quartilesFired.q3 = true;
2855
- options?.onMilestone?.("thirdQuartile", slotId);
2856
- }
2857
- }
2544
+ } else if (adUnitCode) {
2545
+ if (resolvedAuctionId) {
2546
+ this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
2858
2547
  }
2859
- };
2860
- const handleEnded = () => {
2861
- if (!quartilesFired.q4) {
2862
- quartilesFired.q4 = true;
2863
- options?.onMilestone?.("complete", slotId);
2548
+ if (!this.slotRefreshIndices.has(adUnitCode)) {
2549
+ this.slotRefreshIndices.set(adUnitCode, 0);
2864
2550
  }
2865
- };
2866
- const handleError = (e) => {
2867
- const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
2868
- const err = targetVideo?.error || videoEl?.error;
2869
- const cached = getWinningBid();
2870
- const msg = err ? `HTML5 video error code ${err.code}: ${err.message}` : "Video playback error";
2871
- this.log("WARN", msg);
2872
- this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
2873
- auctionId: auctionId || cached?.auctionId,
2874
- transactionId: transactionId || cached?.transactionId,
2551
+ }
2552
+ const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0) : 0;
2553
+ if (!isVideo) {
2554
+ const alreadyEmitted = adUnitCode ? this.hasImpressionEmitted(
2875
2555
  adUnitCode,
2876
- bid: cached?.bidTrace || options?.bid,
2877
- metadata: {
2878
- ...options?.metadata,
2879
- reason: "html5_video_error",
2880
- message: msg,
2881
- ...err?.code ? { video_error_code: String(err.code) } : {}
2882
- },
2883
- error: err ? new Error(msg) : void 0
2884
- });
2885
- if (options?.onError) {
2886
- try {
2887
- options.onError(err);
2888
- } catch (e2) {
2889
- this.log("ERROR", "Error in onError callback", e2);
2556
+ currentRefreshIndex,
2557
+ resolvedAuctionId,
2558
+ bidTrace.creativeId
2559
+ ) : false;
2560
+ if (!alreadyEmitted) {
2561
+ if (adUnitCode) {
2562
+ this.markImpressionEmitted(
2563
+ adUnitCode,
2564
+ currentRefreshIndex,
2565
+ resolvedAuctionId,
2566
+ bidTrace.creativeId
2567
+ );
2890
2568
  }
2569
+ this.enqueue(TraceEventType.IMPRESSION, "impression", {
2570
+ auctionId: resolvedAuctionId,
2571
+ transactionId: resolvedTransactionId,
2572
+ adUnitCode,
2573
+ bid: bidTrace,
2574
+ metadata: { refresh_index: String(currentRefreshIndex) }
2575
+ });
2891
2576
  }
2892
- };
2893
- const attachListenersToVideo = (targetVideo) => {
2894
- targetVideo.addEventListener("playing", handlePlaying);
2895
- targetVideo.addEventListener("play", handlePlaying);
2896
- targetVideo.addEventListener("timeupdate", handleTimeUpdate);
2897
- targetVideo.addEventListener("ended", handleEnded);
2898
- targetVideo.addEventListener("error", handleError);
2899
- };
2900
- const removeListenersFromVideo = (targetVideo) => {
2901
- targetVideo.removeEventListener("playing", handlePlaying);
2902
- targetVideo.removeEventListener("play", handlePlaying);
2903
- targetVideo.removeEventListener("timeupdate", handleTimeUpdate);
2904
- targetVideo.removeEventListener("ended", handleEnded);
2905
- targetVideo.removeEventListener("error", handleError);
2906
- };
2907
- let mutationObserver = null;
2908
- if (el && el !== videoEl) {
2909
- el.addEventListener("playing", handlePlaying, true);
2910
- el.addEventListener("play", handlePlaying, true);
2911
- el.addEventListener("timeupdate", handleTimeUpdate, true);
2912
- el.addEventListener("ended", handleEnded, true);
2913
- el.addEventListener("error", handleError, true);
2914
- }
2915
- if (videoEl) {
2916
- attachListenersToVideo(videoEl);
2917
- }
2918
- if (el && el.tagName?.toLowerCase() !== "video" && typeof MutationObserver !== "undefined") {
2919
- mutationObserver = new MutationObserver(() => {
2920
- const found = el.querySelector("video");
2921
- if (found && found !== videoEl) {
2922
- if (videoEl) removeListenersFromVideo(videoEl);
2923
- videoEl = found;
2924
- if (videoEl) attachListenersToVideo(videoEl);
2925
- }
2926
- });
2927
- mutationObserver.observe(el, { childList: true, subtree: true });
2928
- }
2929
- const cachedInitial = getWinningBid();
2930
- if (options?.trackViewability !== false && el) {
2931
- this.observeSlot(el, slotId, {
2932
- adUnitCode,
2933
- auctionId: auctionId || cachedInitial?.auctionId,
2934
- transactionId: transactionId || cachedInitial?.transactionId,
2935
- mediaType: "video",
2936
- bid: cachedInitial?.bidTrace || options?.bid,
2937
- metadata: options?.metadata,
2938
- emitRefreshEvent: false
2939
- });
2577
+ } else {
2578
+ this.log(
2579
+ "DEBUG",
2580
+ `adRenderSucceeded received for video ad unit ${adUnitCode || "unknown"}: suppressing IMPRESSION until verified video lifecycle event`
2581
+ );
2940
2582
  }
2941
- const cleanup = () => {
2942
- if (mutationObserver) {
2943
- mutationObserver.disconnect();
2944
- mutationObserver = null;
2945
- }
2946
- if (el && el !== videoEl) {
2947
- el.removeEventListener("playing", handlePlaying, true);
2948
- el.removeEventListener("play", handlePlaying, true);
2949
- el.removeEventListener("timeupdate", handleTimeUpdate, true);
2950
- el.removeEventListener("ended", handleEnded, true);
2951
- el.removeEventListener("error", handleError, true);
2952
- }
2953
- if (videoEl) {
2954
- removeListenersFromVideo(videoEl);
2583
+ if (this.config.viewabilityEnabled && typeof document !== "undefined") {
2584
+ let el = adUnitCode && this.slotElements.get(adUnitCode) || null;
2585
+ const targetId = adUnitCode || data.adId || bid.adId;
2586
+ if (!el && targetId) {
2587
+ el = document.getElementById(targetId);
2955
2588
  }
2956
- this.videoDetachCleanups.delete(cleanup);
2957
- };
2958
- this.videoDetachCleanups.add(cleanup);
2959
- return cleanup;
2960
- }
2961
- onTimeInViewThreshold(slotId, thresholdMs, callback) {
2962
- const listener = {
2963
- thresholdMs,
2964
- callback,
2965
- timer: null,
2966
- fired: false
2967
- };
2968
- const record = this.slotViewabilityRecords.get(slotId);
2969
- if (record) {
2970
- record.thresholdListeners.add(listener);
2971
- const currentTotal = this.getCurrentTimeInView(record);
2972
- if (currentTotal >= thresholdMs) {
2973
- listener.fired = true;
2589
+ if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
2974
2590
  try {
2975
- callback(slotId, currentTotal);
2976
- } catch (e) {
2977
- this.log("ERROR", "Error in threshold callback", e);
2591
+ const slots = window.googletag.pubads().getSlots();
2592
+ for (const slot of slots) {
2593
+ if (slot.getAdUnitPath?.() === adUnitCode || slot.getSlotElementId?.() === adUnitCode) {
2594
+ el = document.getElementById(slot.getSlotElementId());
2595
+ if (el) break;
2596
+ }
2597
+ }
2598
+ } catch {
2978
2599
  }
2979
- } else if (record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
2980
- const remaining = thresholdMs - currentTotal;
2981
- listener.timer = setTimeout(() => {
2982
- listener.timer = null;
2983
- if (!listener.fired && record.inView && (typeof document === "undefined" || document.visibilityState === "visible")) {
2984
- const nowTotal = this.getCurrentTimeInView(record);
2985
- if (nowTotal >= thresholdMs) {
2986
- listener.fired = true;
2987
- try {
2988
- callback(slotId, nowTotal);
2989
- } catch (e) {
2990
- this.log("ERROR", "Error in threshold callback", e);
2991
- }
2600
+ }
2601
+ if (!el && targetId) {
2602
+ try {
2603
+ for (const candidate of Array.from(document.querySelectorAll("[data-ad-slot]"))) {
2604
+ if (candidate.getAttribute("data-ad-slot") === targetId) {
2605
+ el = candidate;
2606
+ break;
2992
2607
  }
2993
2608
  }
2994
- }, remaining);
2609
+ } catch {
2610
+ }
2995
2611
  }
2996
- } else {
2997
- if (!this.pendingThresholdListeners.has(slotId)) {
2998
- this.pendingThresholdListeners.set(slotId, /* @__PURE__ */ new Set());
2612
+ if (el) {
2613
+ this.observeSlot(el, adUnitCode || targetId, {
2614
+ adUnitCode,
2615
+ auctionId: resolvedAuctionId,
2616
+ transactionId: resolvedTransactionId,
2617
+ mediaType: resolvedMediaType,
2618
+ bid: bidTrace,
2619
+ refreshIndex: currentRefreshIndex,
2620
+ emitRefreshEvent: false
2621
+ });
2999
2622
  }
3000
- this.pendingThresholdListeners.get(slotId).add(listener);
3001
2623
  }
3002
- return () => {
3003
- if (listener.timer) {
3004
- clearTimeout(listener.timer);
3005
- listener.timer = null;
3006
- }
3007
- if (record) {
3008
- record.thresholdListeners.delete(listener);
3009
- }
3010
- const pending = this.pendingThresholdListeners.get(slotId);
3011
- if (pending) {
3012
- pending.delete(listener);
3013
- }
3014
- };
3015
- }
3016
- getViewabilityState(slotId) {
3017
- const record = this.slotViewabilityRecords.get(slotId);
3018
- if (!record) return void 0;
3019
- const timeInViewMs = Math.round(this.getCurrentTimeInView(record));
3020
- return {
3021
- inView: record.inView,
3022
- viewable: record.viewableFired,
3023
- timeInViewMs,
3024
- refreshIndex: record.refreshIndex
3025
- };
3026
- }
3027
- /** The validated ingest endpoint in use, or "" when none passed validation. */
3028
- getEndpoint() {
3029
- return this.config.endpoint;
3030
2624
  }
3031
- getRefreshIndex(slotId) {
3032
- return this.slotRefreshIndices.get(slotId) ?? 0;
2625
+ isDuplicate(eventName, data) {
2626
+ return this.deduper.isDuplicate(eventName, data);
3033
2627
  }
3034
- triggerSlotRefresh(slotId, options) {
3035
- const record = this.slotViewabilityRecords.get(slotId);
3036
- if (record && record.element) {
3037
- this.observeSlot(record.element, slotId, options);
2628
+ enqueue(type, eventName, data, level) {
2629
+ if (!this.isEnabled) return;
2630
+ if (!this.shouldSample(type, level)) return;
2631
+ extendSession();
2632
+ const protoEvent = {
2633
+ eventId: generateUUID(),
2634
+ timestampMs: Date.now(),
2635
+ type,
2636
+ eventName,
2637
+ auctionId: data.auctionId || "",
2638
+ transactionId: data.transactionId || "",
2639
+ adUnitCode: data.adUnitCode || data.elementId || "",
2640
+ namespace: data.ns || data.namespace || ""
2641
+ };
2642
+ if (data.bid) {
2643
+ protoEvent.bid = {
2644
+ bidder: data.bid.bidder || "",
2645
+ cpm: Number.isFinite(data.bid.cpm) ? data.bid.cpm : 0,
2646
+ // No defaults here: handlers with a priced bid (bidResponse, bidWon,
2647
+ // impression) set currency/mediaType themselves. Defaulting for the
2648
+ // rest would stamp fake "USD"/"banner" on noBid and bidRequest rows.
2649
+ currency: data.bid.currency || "",
2650
+ width: Number.isFinite(data.bid.width) ? data.bid.width : 0,
2651
+ height: Number.isFinite(data.bid.height) ? data.bid.height : 0,
2652
+ dealId: data.bid.dealId || "",
2653
+ mediaType: data.bid.mediaType || "",
2654
+ latencyMs: Number.isFinite(data.bid.latencyMs) ? data.bid.latencyMs : 0,
2655
+ advertiserDomain: data.bid.advertiserDomain || "",
2656
+ creativeId: data.bid.creativeId || ""
2657
+ };
2658
+ }
2659
+ if (data.viewableDurationMs !== void 0) {
2660
+ protoEvent.viewableDurationMs = Number.isFinite(data.viewableDurationMs) ? data.viewableDurationMs : 0;
2661
+ }
2662
+ if (data.bidderOutcomes && Array.isArray(data.bidderOutcomes)) {
2663
+ protoEvent.bidderOutcomes = data.bidderOutcomes;
2664
+ }
2665
+ const metadata = { ...data.metadata };
2666
+ if (data.error !== void 0 && data.error !== null) {
2667
+ metadata.error = typeof data.error === "object" && data.error.message ? String(data.error.message) : String(data.error);
2668
+ }
2669
+ if (data.gpid) {
2670
+ metadata.gpid = String(data.gpid);
2671
+ }
2672
+ if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
2673
+ metadata.refresh_index = String(data.refreshIndex);
2674
+ }
2675
+ const boundedMetadata = boundMetadata(metadata);
2676
+ if (Object.keys(boundedMetadata).length > 0) {
2677
+ protoEvent.metadata = boundedMetadata;
2678
+ }
2679
+ this.queue.push(protoEvent);
2680
+ if (this.queue.length > MAX_QUEUE_SIZE) {
2681
+ this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
2682
+ }
2683
+ if (this.queue.length >= BATCH_SIZE) {
2684
+ this.flush();
2685
+ } else if (REVENUE_FLUSH_TYPES.has(type) && this.revenueFlushTimer === null) {
2686
+ this.revenueFlushTimer = setTimeout(() => {
2687
+ this.revenueFlushTimer = null;
2688
+ this.flush();
2689
+ }, this.config.revenueFlushDelayMs);
3038
2690
  }
3039
2691
  }
3040
- trackRawEvent(level, eventName, data) {
3041
- const eventType = EVENT_NAME_TO_TYPE[eventName] ?? (level === "ERROR" ? TraceEventType.ERROR : TraceEventType.TRACE_EVENT_TYPE_UNSPECIFIED);
3042
- this.enqueue(eventType, eventName, data, level);
2692
+ shouldSample(type, level) {
2693
+ if (type === TraceEventType.ERROR) {
2694
+ this.errorCount++;
2695
+ if (this.errorCount > MAX_ERRORS_PER_SESSION) return false;
2696
+ return this.config.errorsEnabled;
2697
+ }
2698
+ if (level === "WARN") {
2699
+ return this.config.warningsEnabled;
2700
+ }
2701
+ return this.config.auctionEnabled;
3043
2702
  }
3044
- setUserId(userId) {
3045
- this.config.userId = userId;
2703
+ requeue(events) {
2704
+ if (!events.length) return;
2705
+ this.queue = events.concat(this.queue);
2706
+ if (this.queue.length > MAX_QUEUE_SIZE) {
2707
+ this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
2708
+ }
3046
2709
  }
3047
- setSessionId(sessionId) {
3048
- this.config.sessionId = sessionId;
2710
+ // Drains a batch (up to MAX_PAYLOAD_BYTES) from the queue.
2711
+ // Shared by flush() and flushBeacon().
2712
+ drainBatch() {
2713
+ if (this.queue.length === 0 || !this.config.endpoint) return null;
2714
+ const domain = typeof window !== "undefined" ? window.location.hostname : "";
2715
+ const buildBatch = (evts) => ({
2716
+ propertyId: this.config.propertyId,
2717
+ pageviewId: this.config.pageviewId,
2718
+ sessionId: this.config.sessionId,
2719
+ pageUrl: this.pageUrl,
2720
+ country: this.config.country,
2721
+ region: this.config.region,
2722
+ deviceType: this.config.deviceType,
2723
+ userId: this.config.userId,
2724
+ domain,
2725
+ events: evts
2726
+ });
2727
+ for (; ; ) {
2728
+ if (this.queue.length === 0) return null;
2729
+ let events = this.queue;
2730
+ let encoded = TraceEventBatch.encode(
2731
+ buildBatch(events)
2732
+ ).finish();
2733
+ while (events.length > 1 && encoded.byteLength > MAX_PAYLOAD_BYTES) {
2734
+ events = events.slice(0, Math.floor(events.length / 2));
2735
+ encoded = TraceEventBatch.encode(
2736
+ buildBatch(events)
2737
+ ).finish();
2738
+ }
2739
+ if (encoded.byteLength > MAX_PAYLOAD_BYTES) {
2740
+ const [dropped] = this.queue.splice(0, 1);
2741
+ this.log(
2742
+ "WARN",
2743
+ `Dropping oversized event ${dropped?.eventName || dropped?.eventId} (${encoded.byteLength} bytes exceeds the ${MAX_PAYLOAD_BYTES}-byte cap on its own)`
2744
+ );
2745
+ continue;
2746
+ }
2747
+ this.queue = this.queue.slice(events.length);
2748
+ return {
2749
+ url: `${this.config.endpoint}/${this.config.propertyId}`,
2750
+ // protobufjs types finish() as Uint8Array<ArrayBufferLike>; the buffer is
2751
+ // always a plain ArrayBuffer, so narrow for fetch/Blob compatibility.
2752
+ encoded,
2753
+ events
2754
+ };
2755
+ }
3049
2756
  }
3050
- navigate(pageviewId, pageUrl) {
3051
- this.disable();
3052
- this.slotRefreshIndices.clear();
3053
- this.pendingThresholdListeners.clear();
3054
- this.config.pageviewId = pageviewId || generateUUID();
3055
- if (pageUrl) {
2757
+ sendFetch(payload, useKeepalive = false, onDelivered, isReplay = false) {
2758
+ const fetchOpts = {
2759
+ method: "POST",
2760
+ // Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
2761
+ headers: { "Content-Type": SAFE_CONTENT_TYPE },
2762
+ body: payload.encoded
2763
+ };
2764
+ if (useKeepalive) {
2765
+ fetchOpts.keepalive = true;
2766
+ }
2767
+ fetch(payload.url, fetchOpts).then((res) => {
2768
+ if (!res.ok) {
2769
+ this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
2770
+ if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 408) {
2771
+ this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
2772
+ return;
2773
+ }
2774
+ if (!isReplay) this.handleSendFailure(payload.events);
2775
+ } else {
2776
+ if (!isReplay) {
2777
+ this.consecutiveSendFailures = 0;
2778
+ this.nextSendAllowedAt = 0;
2779
+ }
2780
+ if (onDelivered) onDelivered();
2781
+ }
2782
+ }).catch((err) => {
2783
+ this.log("ERROR", "Failed to send batch", err);
2784
+ if (!isReplay) this.handleSendFailure(payload.events);
2785
+ });
2786
+ const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
2787
+ if (proc && typeof proc._tickCallback === "function") {
3056
2788
  try {
3057
- const u = new URL(pageUrl);
3058
- this.pageUrl = u.origin + u.pathname;
2789
+ proc._tickCallback();
3059
2790
  } catch {
3060
- this.pageUrl = pageUrl;
3061
2791
  }
3062
- } else {
3063
- this.pageUrl = "";
3064
- }
3065
- this.logger.resetTiming();
3066
- extendSession();
3067
- this.enable();
2792
+ }
3068
2793
  }
3069
- handleAuctionInit(data) {
3070
- if (this.isDuplicate("auctionInit", data)) return;
3071
- this.log("DEBUG", "auctionInit", data);
3072
- const auctionId = data.auctionId || "";
3073
- if (auctionId) {
3074
- this.auctionBidderOutcomes.delete(auctionId);
3075
- this.getOrCreateAuctionOutcomes(auctionId);
2794
+ handleSendFailure(events) {
2795
+ this.consecutiveSendFailures++;
2796
+ const backoff = Math.min(
2797
+ FLUSH_INTERVAL_MS * 2 ** (this.consecutiveSendFailures - 1),
2798
+ MAX_SEND_BACKOFF_MS
2799
+ );
2800
+ this.nextSendAllowedAt = Date.now() + backoff;
2801
+ if (this.consecutiveSendFailures <= MAX_CONSECUTIVE_SEND_FAILURES) {
2802
+ this.requeue(events);
2803
+ } else {
2804
+ this.log(
2805
+ "WARN",
2806
+ `Dropping ${events.length} events after ${this.consecutiveSendFailures} consecutive send failures`
2807
+ );
3076
2808
  }
3077
- this.enqueue(TraceEventType.AUCTION_START, "auctionStart", {
3078
- auctionId,
3079
- transactionId: data.transactionId || ""
3080
- });
3081
2809
  }
3082
- handleAuctionEnd(data) {
3083
- if (this.isDuplicate("auctionEnd", data)) return;
3084
- this.log("DEBUG", "auctionEnd", data);
3085
- const auctionId = data.auctionId || "";
3086
- const outcomesMap = auctionId ? this.auctionBidderOutcomes.get(auctionId) : void 0;
3087
- const bidderOutcomes = outcomesMap ? Array.from(outcomesMap.values()) : [];
3088
- if (auctionId) {
3089
- this.auctionBidderOutcomes.delete(auctionId);
2810
+ flush() {
2811
+ if (Date.now() < this.nextSendAllowedAt) return;
2812
+ let payload = this.drainBatch();
2813
+ while (payload) {
2814
+ this.sendFetch(payload, false);
2815
+ if (Date.now() < this.nextSendAllowedAt) break;
2816
+ payload = this.drainBatch();
3090
2817
  }
3091
- this.enqueue(TraceEventType.AUCTION_END, "auctionEnd", {
3092
- auctionId,
3093
- transactionId: data.transactionId || "",
3094
- bidderOutcomes
3095
- });
3096
2818
  }
3097
- handleBidRequested(data) {
3098
- if (this.isDuplicate("bidRequested", data)) return;
3099
- this.log("DEBUG", "bidRequested", data);
3100
- const auctionId = data.auctionId || "";
3101
- const bidder = data.bidderCode || data.bidder || "";
3102
- if (!auctionId || !bidder) return;
3103
- const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
3104
- if (Array.isArray(data.bids)) {
3105
- data.bids.forEach((bid) => {
3106
- const adUnitCode = bid.adUnitCode || "";
3107
- const mediaTypes = Object.keys(bid.mediaTypes || {});
3108
- const mediaType = mediaTypes.length > 0 ? mediaTypes[0] : bid.mediaType || "banner";
3109
- const key = `${bidder}:${adUnitCode}`;
3110
- if (!auctionMap.has(key)) {
3111
- auctionMap.set(key, {
3112
- bidder,
3113
- adUnitCode,
3114
- mediaType,
3115
- outcome: BidderOutcome.BIDDER_OUTCOME_NO_RESPONSE,
3116
- latencyMs: 0
3117
- });
2819
+ flushBeacon() {
2820
+ let payload = this.drainBatch();
2821
+ while (payload) {
2822
+ let sent = false;
2823
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
2824
+ try {
2825
+ const blob = new Blob([payload.encoded], { type: SAFE_CONTENT_TYPE });
2826
+ sent = navigator.sendBeacon(payload.url, blob);
2827
+ } catch {
2828
+ sent = false;
3118
2829
  }
3119
- });
2830
+ }
2831
+ if (!sent) {
2832
+ const persistKey = this.persistBatch(payload.encoded);
2833
+ this.sendFetch(payload, true, () => this.removePersistedBatch(persistKey));
2834
+ }
2835
+ payload = this.drainBatch();
3120
2836
  }
2837
+ this.schedulePersistedCleanup();
3121
2838
  }
3122
- handleBidResponse(data) {
3123
- if (this.isDuplicate("bidResponse", data)) return;
3124
- this.log("DEBUG", "bidResponse", data);
3125
- const auctionId = data.auctionId || "";
3126
- const bidder = data.bidderCode || data.bidder || "";
3127
- const adUnitCode = data.adUnitCode || "";
3128
- const cpm = Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0;
3129
- const latencyMs = Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0;
3130
- const resMediaTypes = Object.keys(data.mediaTypes || {});
3131
- const mediaType = data.mediaType || (data.mediaTypes?.video ? "video" : resMediaTypes.length > 0 ? resMediaTypes[0] : "banner");
3132
- if (auctionId && bidder) {
3133
- const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
3134
- const key = `${bidder}:${adUnitCode}`;
3135
- const entry = auctionMap.get(key);
3136
- const outcome = cpm > 0 ? BidderOutcome.BIDDER_OUTCOME_BID : BidderOutcome.BIDDER_OUTCOME_NO_BID;
3137
- if (entry) {
3138
- entry.outcome = outcome;
3139
- entry.latencyMs = latencyMs;
3140
- if (mediaType) entry.mediaType = mediaType;
3141
- } else {
3142
- auctionMap.set(key, {
3143
- bidder,
3144
- adUnitCode,
3145
- mediaType,
3146
- outcome,
3147
- latencyMs
3148
- });
2839
+ // --- Exit-batch persistence -----------------------------------------------
2840
+ // Key prefix for this instance's property. encodeURIComponent keeps the
2841
+ // property segment free of the "_" the timestamp suffix is parsed against,
2842
+ // so a property ID containing one cannot forge another property's prefix.
2843
+ pendingKeyPrefix() {
2844
+ return `${PENDING_BATCH_KEY_PREFIX}${encodeURIComponent(this.config.propertyId)}_`;
2845
+ }
2846
+ persistBatch(encoded) {
2847
+ if (!storageAllowed) return null;
2848
+ try {
2849
+ if (typeof localStorage === "undefined") return null;
2850
+ if (!this.config.propertyId) return null;
2851
+ let pendingCount = 0;
2852
+ for (let i = 0; i < localStorage.length; i++) {
2853
+ const k = localStorage.key(i);
2854
+ if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
3149
2855
  }
2856
+ if (pendingCount >= MAX_PENDING_BATCHES) return null;
2857
+ const key = `${this.pendingKeyPrefix()}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
2858
+ localStorage.setItem(key, bytesToBase64(encoded));
2859
+ this.persistedBatchKeys.push(key);
2860
+ return key;
2861
+ } catch {
2862
+ return null;
3150
2863
  }
3151
- if (cpm > 0) {
3152
- this.enqueue(TraceEventType.BID_RESPONSE, "bidResponse", {
3153
- auctionId,
3154
- transactionId: data.transactionId || "",
3155
- adUnitCode,
3156
- bid: {
3157
- bidder,
3158
- cpm,
3159
- currency: data.originalCurrency ?? data.currency ?? "USD",
3160
- ...parseBidDimensions(data),
3161
- dealId: data.dealId || "",
3162
- mediaType,
3163
- latencyMs,
3164
- advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
3165
- creativeId: data.creativeId || ""
3166
- }
3167
- });
2864
+ }
2865
+ removePersistedBatch(key) {
2866
+ if (!key) return;
2867
+ try {
2868
+ if (typeof localStorage !== "undefined") localStorage.removeItem(key);
2869
+ } catch {
3168
2870
  }
2871
+ const idx = this.persistedBatchKeys.indexOf(key);
2872
+ if (idx !== -1) this.persistedBatchKeys.splice(idx, 1);
3169
2873
  }
3170
- handleBidTimeout(data) {
3171
- if (this.isDuplicate("bidTimeout", data)) return;
3172
- this.log("DEBUG", "bidTimeout", data);
3173
- const items = Array.isArray(data) ? data : data ? [data] : [];
3174
- for (const t of items) {
3175
- const auctionId = t.auctionId || "";
3176
- const bidder = t.bidderCode || t.bidder || "";
3177
- const adUnitCode = t.adUnitCode || "";
3178
- const latencyMs = Number.isFinite(t.timeout) ? t.timeout : 0;
3179
- if (auctionId && bidder) {
3180
- const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
3181
- const key = `${bidder}:${adUnitCode}`;
3182
- const entry = auctionMap.get(key);
3183
- if (entry) {
3184
- entry.outcome = BidderOutcome.BIDDER_OUTCOME_TIMEOUT;
3185
- if (latencyMs > 0) entry.latencyMs = latencyMs;
3186
- } else {
3187
- auctionMap.set(key, {
3188
- bidder,
3189
- adUnitCode,
3190
- mediaType: "banner",
3191
- outcome: BidderOutcome.BIDDER_OUTCOME_TIMEOUT,
3192
- latencyMs
3193
- });
3194
- }
2874
+ // A page that is still running PENDING_BATCH_CLEANUP_DELAY_MS after an exit
2875
+ // flush was never torn down, so its beacons have long since gone out; drop
2876
+ // the persisted copies rather than letting a later pageview resend them.
2877
+ schedulePersistedCleanup() {
2878
+ if (this.persistedBatchKeys.length === 0) return;
2879
+ if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
2880
+ this.persistedCleanupTimer = setTimeout(() => {
2881
+ this.persistedCleanupTimer = null;
2882
+ for (const key of Array.from(this.persistedBatchKeys)) {
2883
+ this.removePersistedBatch(key);
3195
2884
  }
3196
- }
2885
+ }, PENDING_BATCH_CLEANUP_DELAY_MS);
3197
2886
  }
3198
- handleBidWon(data) {
3199
- if (this.isDuplicate("bidWon", data)) return;
3200
- this.log("DEBUG", "bidWon", data);
3201
- const auctionId = data.auctionId || "";
3202
- const transactionId = data.transactionId || "";
3203
- const adUnitCode = data.adUnitCode || data.adId || "";
3204
- const wonMediaTypes = Object.keys(data.mediaTypes || {});
3205
- const resolvedMediaType = data.mediaType || (data.mediaTypes?.video ? "video" : wonMediaTypes.length > 0 ? wonMediaTypes[0] : "banner");
3206
- const bidTrace = {
3207
- bidder: data.bidderCode || data.bidder || "",
3208
- cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
3209
- currency: data.originalCurrency ?? data.currency ?? "USD",
3210
- ...parseBidDimensions(data),
3211
- dealId: data.dealId || "",
3212
- mediaType: resolvedMediaType,
3213
- latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
3214
- advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
3215
- creativeId: data.creativeId || ""
3216
- };
3217
- if (adUnitCode) {
3218
- const cachedEntry = {
3219
- bidTrace,
3220
- auctionId,
3221
- transactionId,
3222
- adUnitCode,
3223
- rawBid: data,
3224
- timestamp: Date.now()
3225
- };
3226
- this.cachedWinningBids.set(adUnitCode, cachedEntry);
3227
- if (data.adId && data.adId !== adUnitCode) {
3228
- this.cachedWinningBids.set(data.adId, cachedEntry);
2887
+ // Resend batches a previous pageview persisted at exit but could not
2888
+ // confirm. Only this property's own keys are claimed: batches belonging to
2889
+ // another property sharing this origin are left for that property's next
2890
+ // pageview, since resending them here would file them under this property.
2891
+ // Keys are claimed (removed) before sending so concurrent tabs don't
2892
+ // double-send; the batch bytes carry their original pageview, session, and
2893
+ // event timestamps, so late rows attribute correctly.
2894
+ resendPersistedBatches() {
2895
+ try {
2896
+ if (typeof localStorage === "undefined" || !this.config.endpoint) return;
2897
+ if (!this.config.propertyId) return;
2898
+ const ownPrefix = this.pendingKeyPrefix();
2899
+ const keys = [];
2900
+ const legacyKeys = [];
2901
+ for (let i = 0; i < localStorage.length; i++) {
2902
+ const k = localStorage.key(i);
2903
+ if (!k || !k.startsWith(PENDING_BATCH_KEY_PREFIX)) continue;
2904
+ if (k.startsWith(ownPrefix)) {
2905
+ keys.push(k);
2906
+ } else if (isLegacyPendingKey(k)) {
2907
+ legacyKeys.push(k);
2908
+ }
2909
+ }
2910
+ for (const key of legacyKeys) {
2911
+ try {
2912
+ localStorage.removeItem(key);
2913
+ } catch {
2914
+ }
3229
2915
  }
3230
- if (auctionId) {
3231
- this.cachedWinningBids.set(`${auctionId}:${adUnitCode}`, cachedEntry);
3232
- if (data.adId && data.adId !== adUnitCode) {
3233
- this.cachedWinningBids.set(`${auctionId}:${data.adId}`, cachedEntry);
2916
+ for (const key of keys) {
2917
+ let value = null;
2918
+ try {
2919
+ value = localStorage.getItem(key);
2920
+ localStorage.removeItem(key);
2921
+ } catch {
2922
+ continue;
2923
+ }
2924
+ if (!value) continue;
2925
+ const ts = parseInt(key.slice(ownPrefix.length), 10);
2926
+ if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
2927
+ let encoded;
2928
+ try {
2929
+ encoded = base64ToBytes(value);
2930
+ } catch {
2931
+ continue;
3234
2932
  }
2933
+ if (encoded.byteLength === 0) continue;
2934
+ this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
2935
+ this.sendFetch(
2936
+ {
2937
+ url: `${this.config.endpoint}/${this.config.propertyId}`,
2938
+ encoded,
2939
+ events: []
2940
+ },
2941
+ false,
2942
+ void 0,
2943
+ true
2944
+ );
3235
2945
  }
3236
- this.pruneCachedWinningBids();
2946
+ } catch {
3237
2947
  }
3238
- this.enqueue(TraceEventType.BID_WIN, "bidWon", {
3239
- auctionId,
3240
- transactionId,
3241
- adUnitCode,
3242
- bid: bidTrace
3243
- });
3244
2948
  }
3245
- handleNoBid(data) {
3246
- if (this.isDuplicate("noBid", data)) return;
3247
- this.log("DEBUG", "noBid", data);
3248
- const auctionId = data.auctionId || "";
3249
- const bidder = data.bidderCode || data.bidder || "";
3250
- const adUnitCode = data.adUnitCode || "";
3251
- if (auctionId && bidder) {
3252
- const auctionMap = this.getOrCreateAuctionOutcomes(auctionId);
3253
- const key = `${bidder}:${adUnitCode}`;
3254
- const entry = auctionMap.get(key);
3255
- if (entry) {
3256
- if (entry.outcome === BidderOutcome.BIDDER_OUTCOME_NO_RESPONSE) {
3257
- entry.outcome = BidderOutcome.BIDDER_OUTCOME_NO_BID;
3258
- }
3259
- } else {
3260
- auctionMap.set(key, {
3261
- bidder,
3262
- adUnitCode,
3263
- mediaType: "banner",
3264
- outcome: BidderOutcome.BIDDER_OUTCOME_NO_BID,
3265
- latencyMs: 0
3266
- });
3267
- }
2949
+ log(level, msg, ...args) {
2950
+ this.logger.log(level, msg, ...args);
2951
+ }
2952
+ };
2953
+ var defaultLogger = createLogger({
2954
+ prefix: "[bidkernel]",
2955
+ logLevel: "INFO"
2956
+ });
2957
+ function extractAnalyticsOptions(config, allowProviderless = true) {
2958
+ if (!config) return null;
2959
+ if (Array.isArray(config)) {
2960
+ const matched = config.find((item) => item?.provider === "bidkernel") ?? (allowProviderless ? config.find(
2961
+ (item) => item && typeof item === "object" && item.provider === void 0
2962
+ ) : void 0);
2963
+ return matched ? extractAnalyticsOptions(matched, allowProviderless) : null;
2964
+ }
2965
+ if (typeof config === "object") {
2966
+ if (config.options && typeof config.options === "object") {
2967
+ return config.options;
2968
+ }
2969
+ if (config.config && typeof config.config === "object") {
2970
+ return config.config;
2971
+ }
2972
+ if (config.propertyId || config.endpoint) {
2973
+ return config;
3268
2974
  }
3269
2975
  }
3270
- handleAdRenderFailed(data) {
3271
- if (this.isDuplicate("adRenderFailed", data)) return;
3272
- this.log("DEBUG", "adRenderFailed", data);
3273
- const bid = data.bid || {};
3274
- this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
3275
- auctionId: bid.auctionId || data.auctionId || "",
3276
- transactionId: bid.transactionId || data.transactionId || "",
3277
- adUnitCode: bid.adUnitCode || data.adUnitCode || "",
3278
- bid: {
3279
- bidder: bid.bidderCode || bid.bidder || data.bidderCode || data.bidder || ""
3280
- },
3281
- metadata: {
3282
- reason: String(data.reason || ""),
3283
- message: String(data.message || data.error?.message || "")
3284
- },
3285
- error: data.error || (data.message ? new Error(String(data.message)) : void 0)
3286
- });
2976
+ return null;
2977
+ }
2978
+ function getStashedConfig(win) {
2979
+ if (!win) return null;
2980
+ if (win._bidkernelAnalyticsConfig) {
2981
+ const extracted = extractAnalyticsOptions(win._bidkernelAnalyticsConfig);
2982
+ if (extracted) return extracted;
3287
2983
  }
3288
- // Prebid's adRenderSucceeded payload is { doc, bid, adId }: the winning bid
3289
- // lives under data.bid, unlike the flat bidWon payload.
3290
- handleAdRenderSucceeded(data) {
3291
- if (this.isDuplicate("adRenderSucceeded", data)) return;
3292
- this.log("DEBUG", "adRenderSucceeded", data);
3293
- const bid = data.bid || data || {};
3294
- const adUnitCode = data.adUnitCode || bid.adUnitCode || data.adId || bid.adId || "";
3295
- const auctionId = bid.auctionId || data.auctionId || "";
3296
- const transactionId = bid.transactionId || data.transactionId || "";
3297
- const mediaType = bid.mediaType || "banner";
3298
- const slotKey = data.adUnitCode || bid.adUnitCode || "";
3299
- const altKey = data.adId || bid.adId || "";
3300
- const cached = (auctionId && slotKey ? this.getCachedBid(`${auctionId}:${slotKey}`) : void 0) || (auctionId && altKey ? this.getCachedBid(`${auctionId}:${altKey}`) : void 0) || (slotKey ? this.getCachedBid(slotKey) : void 0) || (altKey ? this.getCachedBid(altKey) : void 0);
3301
- const isVideo = mediaType === "video" || bid.mediaType === "video" || bid.mediaTypes?.video !== void 0 || cached?.bidTrace?.mediaType === "video" || cached?.rawBid?.mediaType === "video" || adUnitCode && this.slotViewabilityRecords.get(adUnitCode)?.mediaType === "video";
3302
- const resolvedMediaType = isVideo ? "video" : mediaType || cached?.bidTrace?.mediaType || "banner";
3303
- const bidTrace = {
3304
- bidder: bid.bidderCode || bid.bidder || cached?.bidTrace?.bidder || "",
3305
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : cached?.bidTrace?.cpm ?? 0,
3306
- currency: bid.originalCurrency ?? bid.currency ?? cached?.bidTrace?.currency ?? "USD",
3307
- ...parseBidDimensions(bid).width ? parseBidDimensions(bid) : cached?.bidTrace ? { width: cached.bidTrace.width, height: cached.bidTrace.height } : parseBidDimensions(bid),
3308
- dealId: bid.dealId || cached?.bidTrace?.dealId || "",
3309
- mediaType: resolvedMediaType,
3310
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : cached?.bidTrace?.latencyMs ?? 0,
3311
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || cached?.bidTrace?.advertiserDomain || "",
3312
- creativeId: bid.creativeId || cached?.bidTrace?.creativeId || ""
2984
+ if (win.bidkernelPrebidAnalytics) {
2985
+ const bpa = win.bidkernelPrebidAnalytics;
2986
+ if (bpa.options) {
2987
+ const extracted = extractAnalyticsOptions(bpa.options);
2988
+ if (extracted) return extracted;
2989
+ }
2990
+ if (bpa.config) {
2991
+ const extracted = extractAnalyticsOptions(bpa.config);
2992
+ if (extracted) return extracted;
2993
+ }
2994
+ if (bpa.propertyId) {
2995
+ return extractAnalyticsOptions(bpa);
2996
+ }
2997
+ }
2998
+ return null;
2999
+ }
3000
+ function registerPrebidAnalytics(pbjsGlobalName = "pbjs", defaults) {
3001
+ const win = typeof window !== "undefined" ? window : {};
3002
+ win[pbjsGlobalName] = win[pbjsGlobalName] || {};
3003
+ const pbjs = win[pbjsGlobalName];
3004
+ const enableInstance = (options) => {
3005
+ const previous = win._bidkernelPrebidAnalytics;
3006
+ const pinnedEndpoint = previous?.getEndpoint?.() || "";
3007
+ const merged = {
3008
+ pbjsGlobalName,
3009
+ ...defaults,
3010
+ ...options
3313
3011
  };
3314
- const resolvedAuctionId = auctionId || cached?.auctionId || "";
3315
- const resolvedTransactionId = transactionId || cached?.transactionId || "";
3316
- const record = adUnitCode ? this.slotViewabilityRecords.get(adUnitCode) : void 0;
3317
- const isDifferentAuction = record && resolvedAuctionId && record.auctionId && record.auctionId !== resolvedAuctionId;
3318
- const isRefresh = Boolean(adUnitCode && record && isDifferentAuction);
3319
- if (isRefresh) {
3320
- this.flushSlotTimeInView(adUnitCode);
3321
- const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0)) + 1;
3322
- this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
3323
- this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
3324
- if (record) {
3325
- record.refreshIndex = nextRefreshIndex;
3326
- record.auctionId = resolvedAuctionId;
3327
- record.transactionId = resolvedTransactionId;
3328
- record.bidPayload = bidTrace;
3329
- record.viewableFired = false;
3330
- record.accumulatedTimeInViewMs = 0;
3331
- }
3332
- this.enqueue(TraceEventType.REFRESH, "refresh", {
3333
- auctionId: resolvedAuctionId,
3334
- transactionId: resolvedTransactionId,
3335
- adUnitCode,
3336
- bid: bidTrace,
3337
- metadata: { refresh_index: String(nextRefreshIndex) }
3338
- });
3339
- } else if (adUnitCode) {
3340
- if (resolvedAuctionId) {
3341
- this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
3342
- }
3343
- if (!this.slotRefreshIndices.has(adUnitCode)) {
3344
- this.slotRefreshIndices.set(adUnitCode, 0);
3012
+ if (pinnedEndpoint && !sameEndpointOrigin(merged.endpoint || "", pinnedEndpoint)) {
3013
+ defaultLogger.warn(
3014
+ `Ignoring analytics endpoint change to ${merged.endpoint || "(empty)"}: this page is pinned to ${pinnedEndpoint}. Reload to change the ingest endpoint.`
3015
+ );
3016
+ merged.endpoint = pinnedEndpoint;
3017
+ }
3018
+ if (previous) {
3019
+ try {
3020
+ previous.disable();
3021
+ } catch (e) {
3022
+ defaultLogger.warn("Failed to disable previous analytics instance:", e);
3345
3023
  }
3346
3024
  }
3347
- const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0) : 0;
3348
- if (!isVideo) {
3349
- const alreadyEmitted = adUnitCode ? this.hasImpressionEmitted(
3350
- adUnitCode,
3351
- currentRefreshIndex,
3352
- resolvedAuctionId,
3353
- bidTrace.creativeId
3354
- ) : false;
3355
- if (!alreadyEmitted) {
3356
- if (adUnitCode) {
3357
- this.markImpressionEmitted(
3358
- adUnitCode,
3359
- currentRefreshIndex,
3360
- resolvedAuctionId,
3361
- bidTrace.creativeId
3362
- );
3363
- }
3364
- this.enqueue(TraceEventType.IMPRESSION, "impression", {
3365
- auctionId: resolvedAuctionId,
3366
- transactionId: resolvedTransactionId,
3367
- adUnitCode,
3368
- bid: bidTrace,
3369
- metadata: { refresh_index: String(currentRefreshIndex) }
3025
+ const analytics = new BidkernelPrebidAnalytics(merged);
3026
+ analytics.enable();
3027
+ win._bidkernelPrebidAnalytics = analytics;
3028
+ return analytics;
3029
+ };
3030
+ const doRegister = () => {
3031
+ try {
3032
+ if (pbjs.adapterManager && typeof pbjs.adapterManager.registerAnalyticsAdapter === "function") {
3033
+ pbjs.adapterManager.registerAnalyticsAdapter({
3034
+ adapter: {
3035
+ enableAnalytics: (config) => {
3036
+ const options = extractAnalyticsOptions(config, !Array.isArray(config));
3037
+ if (options) {
3038
+ enableInstance(options);
3039
+ }
3040
+ },
3041
+ disableAnalytics: () => {
3042
+ if (win._bidkernelPrebidAnalytics) {
3043
+ win._bidkernelPrebidAnalytics.disable();
3044
+ }
3045
+ }
3046
+ },
3047
+ code: "bidkernel"
3370
3048
  });
3371
3049
  }
3372
- } else {
3373
- this.log(
3374
- "DEBUG",
3375
- `adRenderSucceeded received for video ad unit ${adUnitCode || "unknown"}: suppressing IMPRESSION until verified video lifecycle event`
3376
- );
3050
+ const stashed = getStashedConfig(win);
3051
+ if (stashed && (stashed.propertyId || defaults?.propertyId)) {
3052
+ enableInstance(stashed);
3053
+ }
3054
+ } catch (e) {
3055
+ defaultLogger.warn("Failed to register standard Prebid analytics adapter:", e);
3377
3056
  }
3378
- if (this.config.viewabilityEnabled && typeof document !== "undefined") {
3379
- let el = adUnitCode && this.slotElements.get(adUnitCode) || null;
3380
- const targetId = adUnitCode || data.adId || bid.adId;
3381
- if (!el && targetId) {
3382
- el = document.getElementById(targetId);
3383
- }
3384
- if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
3057
+ };
3058
+ if (pbjs.adapterManager && typeof pbjs.adapterManager.registerAnalyticsAdapter === "function") {
3059
+ doRegister();
3060
+ } else {
3061
+ pbjs.que = pbjs.que || [];
3062
+ pbjs.que.push(doRegister);
3063
+ }
3064
+ }
3065
+
3066
+ // src/video.ts
3067
+ var internals = (analytics) => analytics;
3068
+ var adsLoaderListenerMap = /* @__PURE__ */ new WeakMap();
3069
+ var adsLoadersWithErrorListener = /* @__PURE__ */ new WeakSet();
3070
+ function describeImaError(event) {
3071
+ const err = typeof event?.getError === "function" ? event.getError() : event?.error || event;
3072
+ const call = (name) => err && typeof err[name] === "function" ? err[name]() : void 0;
3073
+ const message = call("getMessage") || err?.message || String(err || "IMA ad error");
3074
+ const code = call("getErrorCode") ?? err?.code;
3075
+ const vastCode = call("getVastErrorCode");
3076
+ const type = call("getType") ?? err?.type;
3077
+ const metadata = { message };
3078
+ if (code) metadata.error_code = String(code);
3079
+ if (vastCode) metadata.vast_error_code = String(vastCode);
3080
+ if (type) metadata.error_type = String(type);
3081
+ return { err, message, metadata };
3082
+ }
3083
+ function recordImaAdError(analytics, event, ctx) {
3084
+ const self = internals(analytics);
3085
+ const { err, message, metadata } = describeImaError(event);
3086
+ self.log("WARN", "IMA AdErrorEvent received", event);
3087
+ self.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
3088
+ auctionId: ctx.auctionId,
3089
+ transactionId: ctx.transactionId,
3090
+ adUnitCode: ctx.adUnitCode,
3091
+ bid: ctx.bid,
3092
+ metadata: {
3093
+ ...ctx.metadata,
3094
+ reason: ctx.adStarted ? "ima_playback_error" : "ima_ad_error",
3095
+ playback_phase: ctx.adStarted ? "post_start" : "pre_start",
3096
+ ...metadata
3097
+ },
3098
+ error: typeof err === "object" ? err : new Error(message)
3099
+ });
3100
+ return err;
3101
+ }
3102
+ function ensureAdsLoaderErrorListener(loader, addEventListener) {
3103
+ if (!loader || typeof loader !== "object" || adsLoadersWithErrorListener.has(loader)) return;
3104
+ adsLoadersWithErrorListener.add(loader);
3105
+ const type = typeof window !== "undefined" && window.google?.ima?.AdErrorEvent?.Type?.AD_ERROR || "adError";
3106
+ try {
3107
+ addEventListener.call(loader, type, (event) => {
3108
+ for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
3385
3109
  try {
3386
- const slots = window.googletag.pubads().getSlots();
3387
- for (const slot of slots) {
3388
- if (slot.getAdUnitPath?.() === adUnitCode || slot.getSlotElementId?.() === adUnitCode) {
3389
- el = document.getElementById(slot.getSlotElementId());
3390
- if (el) break;
3391
- }
3392
- }
3110
+ const cached = findCachedWinningBid(instance, { mediaType: "video" });
3111
+ recordImaAdError(instance, event, {
3112
+ adStarted: false,
3113
+ adUnitCode: cached?.adUnitCode || "video",
3114
+ auctionId: cached?.auctionId,
3115
+ transactionId: cached?.transactionId,
3116
+ bid: cached?.bidTrace
3117
+ });
3393
3118
  } catch {
3394
3119
  }
3395
3120
  }
3396
- if (!el && targetId) {
3397
- try {
3398
- for (const candidate of Array.from(document.querySelectorAll("[data-ad-slot]"))) {
3399
- if (candidate.getAttribute("data-ad-slot") === targetId) {
3400
- el = candidate;
3401
- break;
3121
+ });
3122
+ } catch {
3123
+ }
3124
+ }
3125
+ function hookAdsManagerLoadedEvent(loadedEventCtor) {
3126
+ if (!loadedEventCtor) return;
3127
+ try {
3128
+ const loadedProto = loadedEventCtor.prototype;
3129
+ if (loadedProto && typeof loadedProto.getAdsManager === "function" && !loadedProto.getAdsManager.__bidkernelHooked) {
3130
+ const originalGetAdsManager = loadedProto.getAdsManager;
3131
+ const hookedGetAdsManager = function(...args) {
3132
+ const adsManager = originalGetAdsManager.apply(this, args);
3133
+ if (adsManager) {
3134
+ for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
3135
+ try {
3136
+ instance.attachImaAdsManager(adsManager);
3137
+ } catch {
3402
3138
  }
3403
3139
  }
3404
- } catch {
3405
3140
  }
3406
- }
3407
- if (el) {
3408
- this.observeSlot(el, adUnitCode || targetId, {
3409
- adUnitCode,
3410
- auctionId: resolvedAuctionId,
3411
- transactionId: resolvedTransactionId,
3412
- mediaType: resolvedMediaType,
3413
- bid: bidTrace,
3414
- refreshIndex: currentRefreshIndex,
3415
- emitRefreshEvent: false
3416
- });
3417
- }
3418
- }
3419
- }
3420
- isDuplicate(eventName, data) {
3421
- return this.deduper.isDuplicate(eventName, data);
3422
- }
3423
- enqueue(type, eventName, data, level) {
3424
- if (!this.isEnabled) return;
3425
- if (!this.shouldSample(type, level)) return;
3426
- extendSession();
3427
- const protoEvent = {
3428
- eventId: generateUUID(),
3429
- timestampMs: Date.now(),
3430
- type,
3431
- eventName,
3432
- auctionId: data.auctionId || "",
3433
- transactionId: data.transactionId || "",
3434
- adUnitCode: data.adUnitCode || data.elementId || "",
3435
- namespace: data.ns || data.namespace || ""
3436
- };
3437
- if (data.bid) {
3438
- protoEvent.bid = {
3439
- bidder: data.bid.bidder || "",
3440
- cpm: Number.isFinite(data.bid.cpm) ? data.bid.cpm : 0,
3441
- // No defaults here: handlers with a priced bid (bidResponse, bidWon,
3442
- // impression) set currency/mediaType themselves. Defaulting for the
3443
- // rest would stamp fake "USD"/"banner" on noBid and bidRequest rows.
3444
- currency: data.bid.currency || "",
3445
- width: Number.isFinite(data.bid.width) ? data.bid.width : 0,
3446
- height: Number.isFinite(data.bid.height) ? data.bid.height : 0,
3447
- dealId: data.bid.dealId || "",
3448
- mediaType: data.bid.mediaType || "",
3449
- latencyMs: Number.isFinite(data.bid.latencyMs) ? data.bid.latencyMs : 0,
3450
- advertiserDomain: data.bid.advertiserDomain || "",
3451
- creativeId: data.bid.creativeId || ""
3141
+ return adsManager;
3452
3142
  };
3143
+ hookedGetAdsManager.__bidkernelHooked = true;
3144
+ loadedProto.getAdsManager = hookedGetAdsManager;
3453
3145
  }
3454
- if (data.viewableDurationMs !== void 0) {
3455
- protoEvent.viewableDurationMs = Number.isFinite(data.viewableDurationMs) ? data.viewableDurationMs : 0;
3456
- }
3457
- if (data.bidderOutcomes && Array.isArray(data.bidderOutcomes)) {
3458
- protoEvent.bidderOutcomes = data.bidderOutcomes;
3459
- }
3460
- const metadata = { ...data.metadata };
3461
- if (data.error !== void 0 && data.error !== null) {
3462
- metadata.error = typeof data.error === "object" && data.error.message ? String(data.error.message) : String(data.error);
3463
- }
3464
- if (data.gpid) {
3465
- metadata.gpid = String(data.gpid);
3466
- }
3467
- if (data.refreshIndex !== void 0 && metadata.refresh_index === void 0) {
3468
- metadata.refresh_index = String(data.refreshIndex);
3469
- }
3470
- const boundedMetadata = boundMetadata(metadata);
3471
- if (Object.keys(boundedMetadata).length > 0) {
3472
- protoEvent.metadata = boundedMetadata;
3473
- }
3474
- this.queue.push(protoEvent);
3475
- if (this.queue.length > MAX_QUEUE_SIZE) {
3476
- this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
3477
- }
3478
- if (this.queue.length >= BATCH_SIZE) {
3479
- this.flush();
3480
- } else if (REVENUE_FLUSH_TYPES.has(type) && this.revenueFlushTimer === null) {
3481
- this.revenueFlushTimer = setTimeout(() => {
3482
- this.revenueFlushTimer = null;
3483
- this.flush();
3484
- }, this.config.revenueFlushDelayMs);
3485
- }
3146
+ } catch {
3486
3147
  }
3487
- shouldSample(type, level) {
3488
- if (type === TraceEventType.ERROR) {
3489
- this.errorCount++;
3490
- if (this.errorCount > MAX_ERRORS_PER_SESSION) return false;
3491
- return this.config.errorsEnabled;
3492
- }
3493
- if (level === "WARN") {
3494
- return this.config.warningsEnabled;
3148
+ }
3149
+ function hookAdsManagerPrototype(adsManagerCtorOrProto) {
3150
+ if (!adsManagerCtorOrProto) return;
3151
+ try {
3152
+ const proto = adsManagerCtorOrProto.prototype || adsManagerCtorOrProto;
3153
+ if (proto && typeof proto.init === "function" && !proto.init.__bidkernelHooked) {
3154
+ const origInit = proto.init;
3155
+ const hookedInit = function(...args) {
3156
+ for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
3157
+ try {
3158
+ instance.attachImaAdsManager(this);
3159
+ } catch {
3160
+ }
3161
+ }
3162
+ return origInit.apply(this, args);
3163
+ };
3164
+ hookedInit.__bidkernelHooked = true;
3165
+ proto.init = hookedInit;
3495
3166
  }
3496
- return this.config.auctionEnabled;
3497
- }
3498
- requeue(events) {
3499
- if (!events.length) return;
3500
- this.queue = events.concat(this.queue);
3501
- if (this.queue.length > MAX_QUEUE_SIZE) {
3502
- this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
3167
+ if (proto && typeof proto.start === "function" && !proto.start.__bidkernelHooked) {
3168
+ const origStart = proto.start;
3169
+ const hookedStart = function(...args) {
3170
+ for (const instance of BidkernelPrebidAnalytics.getActiveInstances()) {
3171
+ try {
3172
+ instance.attachImaAdsManager(this);
3173
+ } catch {
3174
+ }
3175
+ }
3176
+ return origStart.apply(this, args);
3177
+ };
3178
+ hookedStart.__bidkernelHooked = true;
3179
+ proto.start = hookedStart;
3503
3180
  }
3181
+ } catch {
3504
3182
  }
3505
- // Drains a batch (up to MAX_PAYLOAD_BYTES) from the queue.
3506
- // Shared by flush() and flushBeacon().
3507
- drainBatch() {
3508
- if (this.queue.length === 0 || !this.config.endpoint) return null;
3509
- const domain = typeof window !== "undefined" ? window.location.hostname : "";
3510
- const buildBatch = (evts) => ({
3511
- propertyId: this.config.propertyId,
3512
- pageviewId: this.config.pageviewId,
3513
- sessionId: this.config.sessionId,
3514
- pageUrl: this.pageUrl,
3515
- country: this.config.country,
3516
- region: this.config.region,
3517
- deviceType: this.config.deviceType,
3518
- userId: this.config.userId,
3519
- domain,
3520
- events: evts
3521
- });
3522
- for (; ; ) {
3523
- if (this.queue.length === 0) return null;
3524
- let events = this.queue;
3525
- let encoded = TraceEventBatch.encode(
3526
- buildBatch(events)
3527
- ).finish();
3528
- while (events.length > 1 && encoded.byteLength > MAX_PAYLOAD_BYTES) {
3529
- events = events.slice(0, Math.floor(events.length / 2));
3530
- encoded = TraceEventBatch.encode(
3531
- buildBatch(events)
3532
- ).finish();
3533
- }
3534
- if (encoded.byteLength > MAX_PAYLOAD_BYTES) {
3535
- const [dropped] = this.queue.splice(0, 1);
3536
- this.log(
3537
- "WARN",
3538
- `Dropping oversized event ${dropped?.eventName || dropped?.eventId} (${encoded.byteLength} bytes exceeds the ${MAX_PAYLOAD_BYTES}-byte cap on its own)`
3539
- );
3540
- continue;
3541
- }
3542
- this.queue = this.queue.slice(events.length);
3543
- return {
3544
- url: `${this.config.endpoint}/${this.config.propertyId}`,
3545
- // protobufjs types finish() as Uint8Array<ArrayBufferLike>; the buffer is
3546
- // always a plain ArrayBuffer, so narrow for fetch/Blob compatibility.
3547
- encoded,
3548
- events
3183
+ }
3184
+ function hookAdsLoader(adsLoaderCtor) {
3185
+ if (!adsLoaderCtor) return;
3186
+ try {
3187
+ const proto = adsLoaderCtor.prototype;
3188
+ if (!proto) return;
3189
+ if (typeof proto.addEventListener === "function" && !proto.addEventListener.__bidkernelHooked) {
3190
+ const origAddEventListener = proto.addEventListener;
3191
+ const hookedAddEventListener = function(type, listener, ...rest) {
3192
+ ensureAdsLoaderErrorListener(this, origAddEventListener);
3193
+ if ((type === "adsManagerLoaded" || type === (this?.AdsManagerLoadedEvent?.Type?.ADS_MANAGER_LOADED || "adsManagerLoaded")) && typeof listener === "function") {
3194
+ let wrappedListener = adsLoaderListenerMap.get(listener);
3195
+ if (!wrappedListener) {
3196
+ wrappedListener = function(event) {
3197
+ if (event && typeof event.getAdsManager === "function" && !event.getAdsManager.__bidkernelHooked) {
3198
+ const origGetAdsManager = event.getAdsManager;
3199
+ event.getAdsManager = function(...args) {
3200
+ const adsManager = origGetAdsManager.apply(this, args);
3201
+ if (adsManager) {
3202
+ for (const sdk of BidkernelPrebidAnalytics.getActiveInstances()) {
3203
+ try {
3204
+ sdk.attachImaAdsManager(adsManager);
3205
+ } catch {
3206
+ }
3207
+ }
3208
+ }
3209
+ return adsManager;
3210
+ };
3211
+ event.getAdsManager.__bidkernelHooked = true;
3212
+ }
3213
+ return listener.apply(this, arguments);
3214
+ };
3215
+ adsLoaderListenerMap.set(listener, wrappedListener);
3216
+ }
3217
+ return origAddEventListener.call(this, type, wrappedListener, ...rest);
3218
+ }
3219
+ return origAddEventListener.call(this, type, listener, ...rest);
3220
+ };
3221
+ hookedAddEventListener.__bidkernelHooked = true;
3222
+ proto.addEventListener = hookedAddEventListener;
3223
+ }
3224
+ if (typeof proto.removeEventListener === "function" && !proto.removeEventListener.__bidkernelHooked) {
3225
+ const origRemoveEventListener = proto.removeEventListener;
3226
+ const hookedRemoveEventListener = function(type, listener, ...rest) {
3227
+ const wrapped = typeof listener === "function" ? adsLoaderListenerMap.get(listener) : void 0;
3228
+ return origRemoveEventListener.call(this, type, wrapped || listener, ...rest);
3549
3229
  };
3230
+ hookedRemoveEventListener.__bidkernelHooked = true;
3231
+ proto.removeEventListener = hookedRemoveEventListener;
3550
3232
  }
3233
+ } catch {
3551
3234
  }
3552
- sendFetch(payload, useKeepalive = false, onDelivered, isReplay = false) {
3553
- const fetchOpts = {
3554
- method: "POST",
3555
- // Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
3556
- headers: { "Content-Type": SAFE_CONTENT_TYPE },
3557
- body: payload.encoded
3558
- };
3559
- if (useKeepalive) {
3560
- fetchOpts.keepalive = true;
3235
+ }
3236
+ function hookImaPrototype(ima) {
3237
+ if (!ima || typeof ima !== "object") return;
3238
+ try {
3239
+ if (ima.AdsManagerLoadedEvent) {
3240
+ hookAdsManagerLoadedEvent(ima.AdsManagerLoadedEvent);
3561
3241
  }
3562
- fetch(payload.url, fetchOpts).then((res) => {
3563
- if (!res.ok) {
3564
- this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
3565
- if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 408) {
3566
- this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
3567
- return;
3568
- }
3569
- if (!isReplay) this.handleSendFailure(payload.events);
3570
- } else {
3571
- if (!isReplay) {
3572
- this.consecutiveSendFailures = 0;
3573
- this.nextSendAllowedAt = 0;
3574
- }
3575
- if (onDelivered) onDelivered();
3576
- }
3577
- }).catch((err) => {
3578
- this.log("ERROR", "Failed to send batch", err);
3579
- if (!isReplay) this.handleSendFailure(payload.events);
3580
- });
3581
- const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
3582
- if (proc && typeof proc._tickCallback === "function") {
3242
+ const loadedDesc = Object.getOwnPropertyDescriptor(ima, "AdsManagerLoadedEvent");
3243
+ if (!loadedDesc || loadedDesc.configurable) {
3244
+ let _loadedEvent = ima.AdsManagerLoadedEvent;
3583
3245
  try {
3584
- proc._tickCallback();
3246
+ Object.defineProperty(ima, "AdsManagerLoadedEvent", {
3247
+ configurable: true,
3248
+ enumerable: true,
3249
+ get() {
3250
+ return _loadedEvent;
3251
+ },
3252
+ set(val) {
3253
+ _loadedEvent = val;
3254
+ hookAdsManagerLoadedEvent(val);
3255
+ }
3256
+ });
3585
3257
  } catch {
3586
3258
  }
3587
3259
  }
3588
- }
3589
- handleSendFailure(events) {
3590
- this.consecutiveSendFailures++;
3591
- const backoff = Math.min(
3592
- FLUSH_INTERVAL_MS * 2 ** (this.consecutiveSendFailures - 1),
3593
- MAX_SEND_BACKOFF_MS
3594
- );
3595
- this.nextSendAllowedAt = Date.now() + backoff;
3596
- if (this.consecutiveSendFailures <= MAX_CONSECUTIVE_SEND_FAILURES) {
3597
- this.requeue(events);
3598
- } else {
3599
- this.log(
3600
- "WARN",
3601
- `Dropping ${events.length} events after ${this.consecutiveSendFailures} consecutive send failures`
3602
- );
3603
- }
3604
- }
3605
- flush() {
3606
- if (Date.now() < this.nextSendAllowedAt) return;
3607
- let payload = this.drainBatch();
3608
- while (payload) {
3609
- this.sendFetch(payload, false);
3610
- if (Date.now() < this.nextSendAllowedAt) break;
3611
- payload = this.drainBatch();
3260
+ if (ima.AdsLoader) {
3261
+ hookAdsLoader(ima.AdsLoader);
3612
3262
  }
3613
- }
3614
- flushBeacon() {
3615
- let payload = this.drainBatch();
3616
- while (payload) {
3617
- let sent = false;
3618
- if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
3619
- try {
3620
- const blob = new Blob([payload.encoded], { type: SAFE_CONTENT_TYPE });
3621
- sent = navigator.sendBeacon(payload.url, blob);
3622
- } catch {
3623
- sent = false;
3624
- }
3263
+ const loaderDesc = Object.getOwnPropertyDescriptor(ima, "AdsLoader");
3264
+ if (!loaderDesc || loaderDesc.configurable) {
3265
+ let _loader = ima.AdsLoader;
3266
+ try {
3267
+ Object.defineProperty(ima, "AdsLoader", {
3268
+ configurable: true,
3269
+ enumerable: true,
3270
+ get() {
3271
+ return _loader;
3272
+ },
3273
+ set(val) {
3274
+ _loader = val;
3275
+ hookAdsLoader(val);
3276
+ }
3277
+ });
3278
+ } catch {
3625
3279
  }
3626
- if (!sent) {
3627
- const persistKey = this.persistBatch(payload.encoded);
3628
- this.sendFetch(payload, true, () => this.removePersistedBatch(persistKey));
3280
+ }
3281
+ if (ima.AdsManager) {
3282
+ hookAdsManagerPrototype(ima.AdsManager);
3283
+ }
3284
+ const mgrDesc = Object.getOwnPropertyDescriptor(ima, "AdsManager");
3285
+ if (!mgrDesc || mgrDesc.configurable) {
3286
+ let _mgr = ima.AdsManager;
3287
+ try {
3288
+ Object.defineProperty(ima, "AdsManager", {
3289
+ configurable: true,
3290
+ enumerable: true,
3291
+ get() {
3292
+ return _mgr;
3293
+ },
3294
+ set(val) {
3295
+ _mgr = val;
3296
+ hookAdsManagerPrototype(val);
3297
+ }
3298
+ });
3299
+ } catch {
3629
3300
  }
3630
- payload = this.drainBatch();
3631
3301
  }
3632
- this.schedulePersistedCleanup();
3302
+ } catch {
3633
3303
  }
3634
- // --- Exit-batch persistence -----------------------------------------------
3635
- // Key prefix for this instance's property. encodeURIComponent keeps the
3636
- // property segment free of the "_" the timestamp suffix is parsed against,
3637
- // so a property ID containing one cannot forge another property's prefix.
3638
- pendingKeyPrefix() {
3639
- return `${PENDING_BATCH_KEY_PREFIX}${encodeURIComponent(this.config.propertyId)}_`;
3304
+ }
3305
+ function initImaInterception() {
3306
+ if (typeof window === "undefined") return;
3307
+ const win = window;
3308
+ if (win.google?.ima) {
3309
+ hookImaPrototype(win.google.ima);
3640
3310
  }
3641
- persistBatch(encoded) {
3642
- if (!storageAllowed) return null;
3643
- try {
3644
- if (typeof localStorage === "undefined") return null;
3645
- if (!this.config.propertyId) return null;
3646
- let pendingCount = 0;
3647
- for (let i = 0; i < localStorage.length; i++) {
3648
- const k = localStorage.key(i);
3649
- if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
3311
+ if (win.google && typeof win.google === "object") {
3312
+ const imaDesc = Object.getOwnPropertyDescriptor(win.google, "ima");
3313
+ if (!imaDesc || imaDesc.configurable) {
3314
+ let _ima = win.google.ima;
3315
+ try {
3316
+ Object.defineProperty(win.google, "ima", {
3317
+ configurable: true,
3318
+ enumerable: true,
3319
+ get() {
3320
+ return _ima;
3321
+ },
3322
+ set(val) {
3323
+ _ima = val;
3324
+ hookImaPrototype(val);
3325
+ }
3326
+ });
3327
+ } catch {
3650
3328
  }
3651
- if (pendingCount >= MAX_PENDING_BATCHES) return null;
3652
- const key = `${this.pendingKeyPrefix()}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
3653
- localStorage.setItem(key, bytesToBase64(encoded));
3654
- this.persistedBatchKeys.push(key);
3655
- return key;
3656
- } catch {
3657
- return null;
3658
3329
  }
3659
3330
  }
3660
- removePersistedBatch(key) {
3661
- if (!key) return;
3331
+ const googleDesc = Object.getOwnPropertyDescriptor(win, "google");
3332
+ if (!googleDesc || googleDesc.configurable) {
3333
+ let _google = win.google;
3662
3334
  try {
3663
- if (typeof localStorage !== "undefined") localStorage.removeItem(key);
3335
+ Object.defineProperty(win, "google", {
3336
+ configurable: true,
3337
+ enumerable: true,
3338
+ get() {
3339
+ return _google;
3340
+ },
3341
+ set(val) {
3342
+ _google = val;
3343
+ if (_google && typeof _google === "object") {
3344
+ if (_google.ima) {
3345
+ hookImaPrototype(_google.ima);
3346
+ }
3347
+ const iDesc = Object.getOwnPropertyDescriptor(_google, "ima");
3348
+ if (!iDesc || iDesc.configurable) {
3349
+ let _i = _google.ima;
3350
+ try {
3351
+ Object.defineProperty(_google, "ima", {
3352
+ configurable: true,
3353
+ enumerable: true,
3354
+ get() {
3355
+ return _i;
3356
+ },
3357
+ set(iv) {
3358
+ _i = iv;
3359
+ hookImaPrototype(iv);
3360
+ }
3361
+ });
3362
+ } catch {
3363
+ }
3364
+ }
3365
+ }
3366
+ }
3367
+ });
3664
3368
  } catch {
3665
3369
  }
3666
- const idx = this.persistedBatchKeys.indexOf(key);
3667
- if (idx !== -1) this.persistedBatchKeys.splice(idx, 1);
3668
3370
  }
3669
- // A page that is still running PENDING_BATCH_CLEANUP_DELAY_MS after an exit
3670
- // flush was never torn down, so its beacons have long since gone out; drop
3671
- // the persisted copies rather than letting a later pageview resend them.
3672
- schedulePersistedCleanup() {
3673
- if (this.persistedBatchKeys.length === 0) return;
3674
- if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
3675
- this.persistedCleanupTimer = setTimeout(() => {
3676
- this.persistedCleanupTimer = null;
3677
- for (const key of Array.from(this.persistedBatchKeys)) {
3678
- this.removePersistedBatch(key);
3371
+ }
3372
+ if (typeof window !== "undefined") {
3373
+ initImaInterception();
3374
+ }
3375
+ function findCachedWinningBid(analytics, filter) {
3376
+ const self = internals(analytics);
3377
+ const now = Date.now();
3378
+ const isConsumed = (entry) => {
3379
+ return self.hasImpressionEmitted(
3380
+ entry.adUnitCode,
3381
+ void 0,
3382
+ entry.auctionId,
3383
+ entry.bidTrace.creativeId
3384
+ );
3385
+ };
3386
+ if (filter?.creativeId || filter?.adId) {
3387
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3388
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3389
+ if (filter.creativeId && (entry.bidTrace.creativeId === filter.creativeId || entry.rawBid?.creativeId === filter.creativeId) || filter.adId && (entry.rawBid?.adId === filter.adId || entry.adUnitCode === filter.adId)) {
3390
+ return entry;
3679
3391
  }
3680
- }, PENDING_BATCH_CLEANUP_DELAY_MS);
3392
+ }
3681
3393
  }
3682
- // Resend batches a previous pageview persisted at exit but could not
3683
- // confirm. Only this property's own keys are claimed: batches belonging to
3684
- // another property sharing this origin are left for that property's next
3685
- // pageview, since resending them here would file them under this property.
3686
- // Keys are claimed (removed) before sending so concurrent tabs don't
3687
- // double-send; the batch bytes carry their original pageview, session, and
3688
- // event timestamps, so late rows attribute correctly.
3689
- resendPersistedBatches() {
3690
- try {
3691
- if (typeof localStorage === "undefined" || !this.config.endpoint) return;
3692
- if (!this.config.propertyId) return;
3693
- const ownPrefix = this.pendingKeyPrefix();
3694
- const keys = [];
3695
- const legacyKeys = [];
3696
- for (let i = 0; i < localStorage.length; i++) {
3697
- const k = localStorage.key(i);
3698
- if (!k || !k.startsWith(PENDING_BATCH_KEY_PREFIX)) continue;
3699
- if (k.startsWith(ownPrefix)) {
3700
- keys.push(k);
3701
- } else if (isLegacyPendingKey(k)) {
3702
- legacyKeys.push(k);
3394
+ let bestUnconsumedMatch;
3395
+ if (filter?.mediaType) {
3396
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3397
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3398
+ const isEntryVideo = entry.bidTrace.mediaType === filter.mediaType || entry.rawBid?.mediaType === filter.mediaType || filter.mediaType === "video" && entry.rawBid?.mediaTypes?.video !== void 0;
3399
+ if (isEntryVideo) {
3400
+ if (!isConsumed(entry)) {
3401
+ if (!bestUnconsumedMatch || entry.timestamp < bestUnconsumedMatch.timestamp) {
3402
+ bestUnconsumedMatch = entry;
3403
+ }
3703
3404
  }
3704
3405
  }
3705
- for (const key of legacyKeys) {
3706
- try {
3707
- localStorage.removeItem(key);
3708
- } catch {
3406
+ }
3407
+ if (bestUnconsumedMatch) return bestUnconsumedMatch;
3408
+ let bestFallbackMatch;
3409
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3410
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3411
+ const isEntryVideo = entry.bidTrace.mediaType === filter.mediaType || entry.rawBid?.mediaType === filter.mediaType || filter.mediaType === "video" && entry.rawBid?.mediaTypes?.video !== void 0;
3412
+ if (isEntryVideo) {
3413
+ if (!bestFallbackMatch || entry.timestamp > bestFallbackMatch.timestamp) {
3414
+ bestFallbackMatch = entry;
3709
3415
  }
3710
3416
  }
3711
- for (const key of keys) {
3712
- let value = null;
3713
- try {
3714
- value = localStorage.getItem(key);
3715
- localStorage.removeItem(key);
3716
- } catch {
3717
- continue;
3718
- }
3719
- if (!value) continue;
3720
- const ts = parseInt(key.slice(ownPrefix.length), 10);
3721
- if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
3722
- let encoded;
3723
- try {
3724
- encoded = base64ToBytes(value);
3725
- } catch {
3726
- continue;
3727
- }
3728
- if (encoded.byteLength === 0) continue;
3729
- this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
3730
- this.sendFetch(
3731
- {
3732
- url: `${this.config.endpoint}/${this.config.propertyId}`,
3733
- encoded,
3734
- events: []
3735
- },
3736
- false,
3737
- void 0,
3738
- true
3739
- );
3417
+ }
3418
+ if (bestFallbackMatch) return bestFallbackMatch;
3419
+ }
3420
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3421
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3422
+ if (!isConsumed(entry)) {
3423
+ if (!bestUnconsumedMatch || entry.timestamp > bestUnconsumedMatch.timestamp) {
3424
+ bestUnconsumedMatch = entry;
3740
3425
  }
3741
- } catch {
3742
3426
  }
3743
3427
  }
3744
- log(level, msg, ...args) {
3745
- this.logger.log(level, msg, ...args);
3428
+ if (bestUnconsumedMatch) return bestUnconsumedMatch;
3429
+ let bestRecent;
3430
+ for (const entry of Array.from(self.cachedWinningBids.values())) {
3431
+ if (now - entry.timestamp > CACHED_BID_TTL_MS) continue;
3432
+ if (!bestRecent || entry.timestamp > bestRecent.timestamp) {
3433
+ bestRecent = entry;
3434
+ }
3746
3435
  }
3747
- };
3748
- var defaultLogger = createLogger({
3749
- prefix: "[bidkernel]",
3750
- logLevel: "INFO"
3751
- });
3752
- function extractAnalyticsOptions(config, allowProviderless = true) {
3753
- if (!config) return null;
3754
- if (Array.isArray(config)) {
3755
- const matched = config.find((item) => item?.provider === "bidkernel") ?? (allowProviderless ? config.find(
3756
- (item) => item && typeof item === "object" && item.provider === void 0
3757
- ) : void 0);
3758
- return matched ? extractAnalyticsOptions(matched, allowProviderless) : null;
3436
+ return bestRecent;
3437
+ }
3438
+ function attachImaAdsManager(analytics, adsManager, options) {
3439
+ const self = internals(analytics);
3440
+ if (!adsManager || typeof adsManager.addEventListener !== "function") {
3441
+ self.log("WARN", "Invalid AdsManager passed to attachImaAdsManager");
3442
+ return () => {
3443
+ };
3759
3444
  }
3760
- if (typeof config === "object") {
3761
- if (config.options && typeof config.options === "object") {
3762
- return config.options;
3445
+ if (!adsManager.__bidkernelAttachedInstances) {
3446
+ try {
3447
+ Object.defineProperty(adsManager, "__bidkernelAttachedInstances", {
3448
+ value: /* @__PURE__ */ new Map(),
3449
+ configurable: true,
3450
+ writable: true
3451
+ });
3452
+ } catch {
3453
+ adsManager.__bidkernelAttachedInstances = /* @__PURE__ */ new Map();
3763
3454
  }
3764
- if (config.config && typeof config.config === "object") {
3765
- return config.config;
3455
+ }
3456
+ const attached = adsManager.__bidkernelAttachedInstances;
3457
+ const previous = attached.get(analytics);
3458
+ if (previous) {
3459
+ if (!options) return () => {
3460
+ };
3461
+ previous();
3462
+ }
3463
+ const slotId = options?.slotId || options?.adUnitCode || "video";
3464
+ const adUnitCode = options?.adUnitCode || slotId;
3465
+ const auctionId = options?.auctionId || "";
3466
+ const transactionId = options?.transactionId || "";
3467
+ const getWinningBid = (adData) => {
3468
+ 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);
3469
+ if (!cached) {
3470
+ cached = findCachedWinningBid(analytics, {
3471
+ creativeId: adData?.creativeId,
3472
+ adId: adData?.adId,
3473
+ mediaType: "video"
3474
+ });
3766
3475
  }
3767
- if (config.propertyId || config.endpoint) {
3768
- return config;
3476
+ return cached;
3477
+ };
3478
+ let adStarted = false;
3479
+ const onAdStartedOrImpression = (event) => {
3480
+ adStarted = true;
3481
+ self.log("DEBUG", "IMA AdEvent.STARTED / IMPRESSION received", event);
3482
+ const ad = typeof event?.getAd === "function" ? event.getAd() : event?.ad;
3483
+ const adData = {};
3484
+ if (ad) {
3485
+ adData.creativeId = (typeof ad.getCreativeId === "function" ? ad.getCreativeId() : ad.creativeId) || "";
3486
+ adData.adId = (typeof ad.getAdId === "function" ? ad.getAdId() : ad.id) || "";
3487
+ adData.title = (typeof ad.getTitle === "function" ? ad.getTitle() : ad.title) || "";
3488
+ adData.duration = typeof ad.getDuration === "function" ? ad.getDuration() : ad.duration;
3489
+ adData.advertiserName = typeof ad.getAdvertiserName === "function" ? ad.getAdvertiserName() : "";
3490
+ }
3491
+ const cached = getWinningBid(adData);
3492
+ const resolvedSlotId = options?.slotId && options.slotId !== "video" ? options.slotId : options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || options?.slotId || options?.adUnitCode || "video";
3493
+ const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || resolvedSlotId;
3494
+ const resolvedAuctionId = auctionId || cached?.auctionId || "";
3495
+ const resolvedTransactionId = transactionId || cached?.transactionId || "";
3496
+ const mergedBid = {
3497
+ ...cached?.rawBid || options?.bid,
3498
+ mediaType: "video",
3499
+ creativeId: adData.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || ""
3500
+ };
3501
+ const emitted = self.recordImpression(resolvedSlotId, {
3502
+ adUnitCode: resolvedAdUnitCode,
3503
+ auctionId: resolvedAuctionId,
3504
+ transactionId: resolvedTransactionId,
3505
+ mediaType: "video",
3506
+ bid: mergedBid,
3507
+ metadata: {
3508
+ ...options?.metadata,
3509
+ media_type: "video",
3510
+ ...adData.title ? { ad_title: adData.title } : {},
3511
+ ...Number.isFinite(adData.duration) ? { ad_duration_s: String(adData.duration) } : {}
3512
+ }
3513
+ });
3514
+ if (emitted && options?.onImpression) {
3515
+ try {
3516
+ options.onImpression(resolvedSlotId, { ad, bid: mergedBid });
3517
+ } catch (e) {
3518
+ self.log("ERROR", "Error in onImpression callback", e);
3519
+ }
3769
3520
  }
3770
- }
3771
- return null;
3772
- }
3773
- function getStashedConfig(win) {
3774
- if (!win) return null;
3775
- if (win._bidkernelAnalyticsConfig) {
3776
- const extracted = extractAnalyticsOptions(win._bidkernelAnalyticsConfig);
3777
- if (extracted) return extracted;
3778
- }
3779
- if (win.bidkernelPrebidAnalytics) {
3780
- const bpa = win.bidkernelPrebidAnalytics;
3781
- if (bpa.options) {
3782
- const extracted = extractAnalyticsOptions(bpa.options);
3783
- if (extracted) return extracted;
3521
+ };
3522
+ const onMilestone = (milestone) => {
3523
+ self.log("DEBUG", `IMA AdEvent milestone: ${milestone}`);
3524
+ if (options?.onMilestone) {
3525
+ try {
3526
+ options.onMilestone(milestone, slotId);
3527
+ } catch (e) {
3528
+ self.log("ERROR", "Error in onMilestone callback", e);
3529
+ }
3784
3530
  }
3785
- if (bpa.config) {
3786
- const extracted = extractAnalyticsOptions(bpa.config);
3787
- if (extracted) return extracted;
3531
+ };
3532
+ const onAdClick = (event) => {
3533
+ self.log("DEBUG", "IMA AdEvent.CLICK received", event);
3534
+ const cached = getWinningBid();
3535
+ const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || slotId;
3536
+ self.enqueue(TraceEventType.CLICK, "click", {
3537
+ auctionId: auctionId || cached?.auctionId,
3538
+ transactionId: transactionId || cached?.transactionId,
3539
+ adUnitCode: resolvedAdUnitCode,
3540
+ bid: cached?.bidTrace || options?.bid,
3541
+ metadata: {
3542
+ ...options?.metadata,
3543
+ media_type: "video"
3544
+ }
3545
+ });
3546
+ };
3547
+ const onAdError = (event) => {
3548
+ const cached = getWinningBid();
3549
+ const resolvedAdUnitCode = options?.adUnitCode && options.adUnitCode !== "video" ? options.adUnitCode : cached?.adUnitCode || slotId;
3550
+ const err = recordImaAdError(analytics, event, {
3551
+ adStarted,
3552
+ adUnitCode: resolvedAdUnitCode,
3553
+ auctionId: auctionId || cached?.auctionId,
3554
+ transactionId: transactionId || cached?.transactionId,
3555
+ bid: cached?.bidTrace || options?.bid,
3556
+ metadata: options?.metadata
3557
+ });
3558
+ if (options?.onError) {
3559
+ try {
3560
+ options.onError(err);
3561
+ } catch (e) {
3562
+ self.log("ERROR", "Error in onError callback", e);
3563
+ }
3788
3564
  }
3789
- if (bpa.propertyId) {
3790
- return extractAnalyticsOptions(bpa);
3565
+ };
3566
+ const googleIma = typeof window !== "undefined" ? window.google?.ima : void 0;
3567
+ const adEventType = googleIma?.AdEvent?.Type || {};
3568
+ const adErrorEventType = googleIma?.AdErrorEvent?.Type || {};
3569
+ const rawListeners = [
3570
+ { type: adEventType.STARTED || "started", handler: onAdStartedOrImpression },
3571
+ { type: adEventType.IMPRESSION || "impression", handler: onAdStartedOrImpression },
3572
+ {
3573
+ type: adEventType.FIRST_QUARTILE || "firstQuartile",
3574
+ handler: () => onMilestone("firstQuartile")
3575
+ },
3576
+ { type: adEventType.MIDPOINT || "midpoint", handler: () => onMilestone("midpoint") },
3577
+ {
3578
+ type: adEventType.THIRD_QUARTILE || "thirdQuartile",
3579
+ handler: () => onMilestone("thirdQuartile")
3580
+ },
3581
+ { type: adEventType.COMPLETE || "complete", handler: () => onMilestone("complete") },
3582
+ { type: adEventType.CLICK || "click", handler: onAdClick },
3583
+ { type: adErrorEventType.AD_ERROR || "adError", handler: onAdError }
3584
+ ];
3585
+ const seenListenerTypes = /* @__PURE__ */ new Set();
3586
+ const listeners = [];
3587
+ for (const l of rawListeners) {
3588
+ if (!seenListenerTypes.has(l.type)) {
3589
+ seenListenerTypes.add(l.type);
3590
+ listeners.push(l);
3591
+ }
3592
+ }
3593
+ for (const { type, handler } of listeners) {
3594
+ try {
3595
+ adsManager.addEventListener(type, handler);
3596
+ } catch {
3791
3597
  }
3792
3598
  }
3793
- return null;
3599
+ const cleanup = () => {
3600
+ attached.delete(analytics);
3601
+ for (const { type, handler } of listeners) {
3602
+ try {
3603
+ adsManager.removeEventListener(type, handler);
3604
+ } catch {
3605
+ }
3606
+ }
3607
+ self.videoDetachCleanups.delete(cleanup);
3608
+ };
3609
+ attached.set(analytics, cleanup);
3610
+ self.videoDetachCleanups.add(cleanup);
3611
+ return cleanup;
3794
3612
  }
3795
- function registerPrebidAnalytics(pbjsGlobalName = "pbjs", defaults) {
3796
- const win = typeof window !== "undefined" ? window : {};
3797
- win[pbjsGlobalName] = win[pbjsGlobalName] || {};
3798
- const pbjs = win[pbjsGlobalName];
3799
- const enableInstance = (options) => {
3800
- const previous = win._bidkernelPrebidAnalytics;
3801
- const pinnedEndpoint = previous?.getEndpoint?.() || "";
3802
- const merged = {
3803
- pbjsGlobalName,
3804
- ...defaults,
3805
- ...options
3613
+ function attachVideoPlayer(analytics, target, options) {
3614
+ const self = internals(analytics);
3615
+ if (!target) {
3616
+ self.log("WARN", "attachVideoPlayer called with null/undefined target");
3617
+ return () => {
3806
3618
  };
3807
- if (pinnedEndpoint && !sameEndpointOrigin(merged.endpoint || "", pinnedEndpoint)) {
3808
- defaultLogger.warn(
3809
- `Ignoring analytics endpoint change to ${merged.endpoint || "(empty)"}: this page is pinned to ${pinnedEndpoint}. Reload to change the ingest endpoint.`
3810
- );
3811
- merged.endpoint = pinnedEndpoint;
3619
+ }
3620
+ let el = null;
3621
+ if (typeof target === "string") {
3622
+ if (typeof document !== "undefined") {
3623
+ try {
3624
+ el = document.querySelector(target) || document.getElementById(target);
3625
+ } catch {
3626
+ el = document.getElementById(target);
3627
+ }
3628
+ }
3629
+ } else if (typeof HTMLElement !== "undefined" && target instanceof HTMLElement) {
3630
+ el = target;
3631
+ } else if (target && typeof target === "object" && target.nodeType === 1) {
3632
+ el = target;
3633
+ } else if (
3634
+ // Check if target is an IMA AdsManager
3635
+ typeof target.addEventListener === "function" && (typeof target.init === "function" || typeof target.start === "function" || typeof target.getCuePoints === "function" || typeof target.getVolume === "function" || target.__imaAdsManager)
3636
+ ) {
3637
+ return attachImaAdsManager(analytics, target, options);
3638
+ } else if (target && typeof target.addEventListener === "function") {
3639
+ el = target;
3640
+ }
3641
+ const slotId = options?.slotId || options?.adUnitCode || (el ? el.id : "") || "video";
3642
+ const adUnitCode = options?.adUnitCode || slotId;
3643
+ const auctionId = options?.auctionId || "";
3644
+ const transactionId = options?.transactionId || "";
3645
+ const getWinningBid = () => {
3646
+ 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);
3647
+ };
3648
+ let videoEl = null;
3649
+ if (el) {
3650
+ if (el.tagName && el.tagName.toLowerCase() === "video") {
3651
+ videoEl = el;
3652
+ } else {
3653
+ videoEl = el.querySelector("video");
3812
3654
  }
3813
- if (previous) {
3655
+ }
3656
+ const quartilesFired = {
3657
+ q1: false,
3658
+ q2: false,
3659
+ q3: false,
3660
+ q4: false
3661
+ };
3662
+ const triggerPlaybackImpression = (activeVideo) => {
3663
+ const vEl = activeVideo || videoEl || (el?.querySelector ? el.querySelector("video") : null);
3664
+ const cached = getWinningBid();
3665
+ const mergedBid = {
3666
+ ...cached?.rawBid || options?.bid,
3667
+ mediaType: "video"
3668
+ };
3669
+ const emitted = self.recordImpression(slotId, {
3670
+ adUnitCode,
3671
+ auctionId: auctionId || cached?.auctionId,
3672
+ transactionId: transactionId || cached?.transactionId,
3673
+ mediaType: "video",
3674
+ bid: mergedBid,
3675
+ metadata: {
3676
+ ...options?.metadata,
3677
+ media_type: "video",
3678
+ ...vEl && Number.isFinite(vEl.duration) ? { video_duration_s: String(Math.round(vEl.duration)) } : {}
3679
+ }
3680
+ });
3681
+ if (emitted && options?.onImpression) {
3814
3682
  try {
3815
- previous.disable();
3683
+ options.onImpression(slotId, { element: vEl || el, bid: mergedBid });
3816
3684
  } catch (e) {
3817
- defaultLogger.warn("Failed to disable previous analytics instance:", e);
3685
+ self.log("ERROR", "Error in onImpression callback", e);
3818
3686
  }
3819
3687
  }
3820
- const analytics = new BidkernelPrebidAnalytics(merged);
3821
- analytics.enable();
3822
- win._bidkernelPrebidAnalytics = analytics;
3823
- return analytics;
3824
3688
  };
3825
- const doRegister = () => {
3826
- try {
3827
- if (pbjs.adapterManager && typeof pbjs.adapterManager.registerAnalyticsAdapter === "function") {
3828
- pbjs.adapterManager.registerAnalyticsAdapter({
3829
- adapter: {
3830
- enableAnalytics: (config) => {
3831
- const options = extractAnalyticsOptions(config, !Array.isArray(config));
3832
- if (options) {
3833
- enableInstance(options);
3834
- }
3835
- },
3836
- disableAnalytics: () => {
3837
- if (win._bidkernelPrebidAnalytics) {
3838
- win._bidkernelPrebidAnalytics.disable();
3839
- }
3840
- }
3841
- },
3842
- code: "bidkernel"
3843
- });
3689
+ const handlePlaying = (e) => {
3690
+ self.log("DEBUG", "HTML5 video playing event received");
3691
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
3692
+ triggerPlaybackImpression(targetVideo);
3693
+ };
3694
+ const handleTimeUpdate = (e) => {
3695
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
3696
+ if (targetVideo && targetVideo.currentTime > 0) {
3697
+ triggerPlaybackImpression(targetVideo);
3698
+ if (Number.isFinite(targetVideo.duration) && targetVideo.duration > 0) {
3699
+ const progress = targetVideo.currentTime / targetVideo.duration;
3700
+ if (progress >= 0.25 && !quartilesFired.q1) {
3701
+ quartilesFired.q1 = true;
3702
+ options?.onMilestone?.("firstQuartile", slotId);
3703
+ }
3704
+ if (progress >= 0.5 && !quartilesFired.q2) {
3705
+ quartilesFired.q2 = true;
3706
+ options?.onMilestone?.("midpoint", slotId);
3707
+ }
3708
+ if (progress >= 0.75 && !quartilesFired.q3) {
3709
+ quartilesFired.q3 = true;
3710
+ options?.onMilestone?.("thirdQuartile", slotId);
3711
+ }
3844
3712
  }
3845
- const stashed = getStashedConfig(win);
3846
- if (stashed && (stashed.propertyId || defaults?.propertyId)) {
3847
- enableInstance(stashed);
3713
+ }
3714
+ };
3715
+ const handleEnded = () => {
3716
+ if (!quartilesFired.q4) {
3717
+ quartilesFired.q4 = true;
3718
+ options?.onMilestone?.("complete", slotId);
3719
+ }
3720
+ };
3721
+ const handleError = (e) => {
3722
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
3723
+ const err = targetVideo?.error || videoEl?.error;
3724
+ const cached = getWinningBid();
3725
+ const msg = err ? `HTML5 video error code ${err.code}: ${err.message}` : "Video playback error";
3726
+ self.log("WARN", msg);
3727
+ self.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
3728
+ auctionId: auctionId || cached?.auctionId,
3729
+ transactionId: transactionId || cached?.transactionId,
3730
+ adUnitCode,
3731
+ bid: cached?.bidTrace || options?.bid,
3732
+ metadata: {
3733
+ ...options?.metadata,
3734
+ reason: "html5_video_error",
3735
+ message: msg,
3736
+ ...err?.code ? { error_code: String(err.code) } : {}
3737
+ },
3738
+ error: err ? new Error(msg) : void 0
3739
+ });
3740
+ if (options?.onError) {
3741
+ try {
3742
+ options.onError(err);
3743
+ } catch (e2) {
3744
+ self.log("ERROR", "Error in onError callback", e2);
3848
3745
  }
3849
- } catch (e) {
3850
- defaultLogger.warn("Failed to register standard Prebid analytics adapter:", e);
3851
3746
  }
3852
3747
  };
3853
- if (pbjs.adapterManager && typeof pbjs.adapterManager.registerAnalyticsAdapter === "function") {
3854
- doRegister();
3855
- } else {
3856
- pbjs.que = pbjs.que || [];
3857
- pbjs.que.push(doRegister);
3748
+ const attachListenersToVideo = (targetVideo) => {
3749
+ targetVideo.addEventListener("playing", handlePlaying);
3750
+ targetVideo.addEventListener("play", handlePlaying);
3751
+ targetVideo.addEventListener("timeupdate", handleTimeUpdate);
3752
+ targetVideo.addEventListener("ended", handleEnded);
3753
+ targetVideo.addEventListener("error", handleError);
3754
+ };
3755
+ const removeListenersFromVideo = (targetVideo) => {
3756
+ targetVideo.removeEventListener("playing", handlePlaying);
3757
+ targetVideo.removeEventListener("play", handlePlaying);
3758
+ targetVideo.removeEventListener("timeupdate", handleTimeUpdate);
3759
+ targetVideo.removeEventListener("ended", handleEnded);
3760
+ targetVideo.removeEventListener("error", handleError);
3761
+ };
3762
+ let mutationObserver = null;
3763
+ if (el && el !== videoEl) {
3764
+ el.addEventListener("playing", handlePlaying, true);
3765
+ el.addEventListener("play", handlePlaying, true);
3766
+ el.addEventListener("timeupdate", handleTimeUpdate, true);
3767
+ el.addEventListener("ended", handleEnded, true);
3768
+ el.addEventListener("error", handleError, true);
3769
+ }
3770
+ if (videoEl) {
3771
+ attachListenersToVideo(videoEl);
3772
+ }
3773
+ if (el && el.tagName?.toLowerCase() !== "video" && typeof MutationObserver !== "undefined") {
3774
+ mutationObserver = new MutationObserver(() => {
3775
+ const found = el.querySelector("video");
3776
+ if (found && found !== videoEl) {
3777
+ if (videoEl) removeListenersFromVideo(videoEl);
3778
+ videoEl = found;
3779
+ if (videoEl) attachListenersToVideo(videoEl);
3780
+ }
3781
+ });
3782
+ mutationObserver.observe(el, { childList: true, subtree: true });
3858
3783
  }
3784
+ const cachedInitial = getWinningBid();
3785
+ if (options?.trackViewability !== false && el) {
3786
+ self.observeSlot(el, slotId, {
3787
+ adUnitCode,
3788
+ auctionId: auctionId || cachedInitial?.auctionId,
3789
+ transactionId: transactionId || cachedInitial?.transactionId,
3790
+ mediaType: "video",
3791
+ bid: cachedInitial?.bidTrace || options?.bid,
3792
+ metadata: options?.metadata,
3793
+ emitRefreshEvent: false
3794
+ });
3795
+ }
3796
+ const cleanup = () => {
3797
+ if (mutationObserver) {
3798
+ mutationObserver.disconnect();
3799
+ mutationObserver = null;
3800
+ }
3801
+ if (el && el !== videoEl) {
3802
+ el.removeEventListener("playing", handlePlaying, true);
3803
+ el.removeEventListener("play", handlePlaying, true);
3804
+ el.removeEventListener("timeupdate", handleTimeUpdate, true);
3805
+ el.removeEventListener("ended", handleEnded, true);
3806
+ el.removeEventListener("error", handleError, true);
3807
+ }
3808
+ if (videoEl) {
3809
+ removeListenersFromVideo(videoEl);
3810
+ }
3811
+ self.videoDetachCleanups.delete(cleanup);
3812
+ };
3813
+ self.videoDetachCleanups.add(cleanup);
3814
+ return cleanup;
3859
3815
  }
3816
+ BidkernelPrebidAnalytics.prototype.attachImaAdsManager = function(adsManager, options) {
3817
+ return attachImaAdsManager(this, adsManager, options);
3818
+ };
3819
+ BidkernelPrebidAnalytics.prototype.attachVideoPlayer = function(target, options) {
3820
+ return attachVideoPlayer(this, target, options);
3821
+ };
3822
+ var coreEnable = BidkernelPrebidAnalytics.prototype.enable;
3823
+ BidkernelPrebidAnalytics.prototype.enable = function() {
3824
+ coreEnable.call(this);
3825
+ if (typeof window !== "undefined") {
3826
+ initImaInterception();
3827
+ if (window.google?.ima) {
3828
+ hookImaPrototype(window.google.ima);
3829
+ }
3830
+ }
3831
+ };
3860
3832
 
3861
3833
  // src/index.ts
3862
3834
  function getbidkernel(alias = "bidkernel") {
@@ -3891,7 +3863,10 @@ function getbidkernel(alias = "bidkernel") {
3891
3863
  // Annotate the CommonJS export names for ESM import in node:
3892
3864
  0 && (module.exports = {
3893
3865
  BidkernelPrebidAnalytics,
3866
+ CACHED_BID_TTL_MS,
3894
3867
  PrebidEventDeduper,
3868
+ attachImaAdsManager,
3869
+ attachVideoPlayer,
3895
3870
  getOrCreateSessionId,
3896
3871
  getPrebidEventKey,
3897
3872
  getbidkernel,