@bidkernel/analytics 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -628,6 +628,11 @@ var MAX_QUEUE_SIZE = 200;
628
628
  var MAX_SEND_BACKOFF_MS = 5 * 60 * 1e3;
629
629
  var MAX_CONSECUTIVE_SEND_FAILURES = 10;
630
630
  var MAX_PAYLOAD_BYTES = 32 * 1024;
631
+ var SAFE_CONTENT_TYPE = "text/plain";
632
+ var PENDING_BATCH_KEY_PREFIX = "_bidkernel_pending_";
633
+ var MAX_PENDING_BATCHES = 20;
634
+ var PENDING_BATCH_MAX_AGE_MS = 2 * 60 * 60 * 1e3;
635
+ var PENDING_BATCH_CLEANUP_DELAY_MS = 1e4;
631
636
  var EVENT_NAME_TO_TYPE = {
632
637
  auctionStart: TraceEventType.AUCTION_START,
633
638
  auctionEnd: TraceEventType.AUCTION_END,
@@ -643,6 +648,11 @@ var EVENT_NAME_TO_TYPE = {
643
648
  timeInView: TraceEventType.TIME_IN_VIEW,
644
649
  viewable: TraceEventType.VIEWABLE
645
650
  };
651
+ var IMMEDIATE_FLUSH_TYPES = /* @__PURE__ */ new Set([
652
+ TraceEventType.IMPRESSION,
653
+ TraceEventType.BID_WIN,
654
+ TraceEventType.CLICK
655
+ ]);
646
656
  var SESSION_KEY = "_bidkernel_session";
647
657
  var SESSION_TS_KEY = "_bidkernel_session_ts";
648
658
  var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
@@ -658,6 +668,22 @@ function generateUUID() {
658
668
  return v.toString(16);
659
669
  });
660
670
  }
671
+ function bytesToBase64(bytes) {
672
+ let binary = "";
673
+ const chunk = 8192;
674
+ for (let i = 0; i < bytes.length; i += chunk) {
675
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
676
+ }
677
+ return btoa(binary);
678
+ }
679
+ function base64ToBytes(b64) {
680
+ const binary = atob(b64);
681
+ const bytes = new Uint8Array(binary.length);
682
+ for (let i = 0; i < binary.length; i++) {
683
+ bytes[i] = binary.charCodeAt(i);
684
+ }
685
+ return bytes;
686
+ }
661
687
  function extendSession() {
662
688
  const now = Date.now();
663
689
  inMemorySessionTs = now;
@@ -822,12 +848,25 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
822
848
  consecutiveSendFailures = 0;
823
849
  nextSendAllowedAt = 0;
824
850
  replayedEventCount = 0;
851
+ immediateFlushScheduled = false;
852
+ // localStorage keys of exit batches this instance persisted, so a page that
853
+ // survives its own pagehide/hidden (bfcache restore, tab re-focus) can
854
+ // remove them instead of leaving them for a duplicate resend.
855
+ persistedBatchKeys = [];
856
+ persistedCleanupTimer = null;
825
857
  // Viewability & refresh tracking
826
858
  intersectionObserver = null;
827
859
  slotViewabilityRecords = /* @__PURE__ */ new Map();
828
860
  elementToSlotId = /* @__PURE__ */ new Map();
829
861
  slotRefreshIndices = /* @__PURE__ */ new Map();
862
+ slotLastAuctionIds = /* @__PURE__ */ new Map();
830
863
  pendingThresholdListeners = /* @__PURE__ */ new Map();
864
+ // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
865
+ slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
866
+ // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
867
+ cachedWinningBids = /* @__PURE__ */ new Map();
868
+ // Active video player attachment cleanup routines
869
+ videoDetachCleanups = /* @__PURE__ */ new Set();
831
870
  constructor(config) {
832
871
  this.config = {
833
872
  endpoint: config.endpoint || "",
@@ -946,6 +985,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
946
985
  document.addEventListener("visibilitychange", this.boundVisibilityChange);
947
986
  }
948
987
  this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
988
+ this.resendPersistedBatches();
949
989
  }
950
990
  }
951
991
  disable() {
@@ -972,10 +1012,24 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
972
1012
  }
973
1013
  this.slotViewabilityRecords.clear();
974
1014
  this.elementToSlotId.clear();
1015
+ for (const cleanup of this.videoDetachCleanups) {
1016
+ try {
1017
+ cleanup();
1018
+ } catch {
1019
+ }
1020
+ }
1021
+ this.videoDetachCleanups.clear();
1022
+ this.cachedWinningBids.clear();
1023
+ this.slotEmittedImpressionKeys.clear();
1024
+ this.slotLastAuctionIds.clear();
975
1025
  if (this.flushTimer) {
976
1026
  clearInterval(this.flushTimer);
977
1027
  this.flushTimer = null;
978
1028
  }
1029
+ if (this.persistedCleanupTimer) {
1030
+ clearTimeout(this.persistedCleanupTimer);
1031
+ this.persistedCleanupTimer = null;
1032
+ }
979
1033
  if (typeof window !== "undefined") {
980
1034
  window.removeEventListener("pagehide", this.boundFlushBeacon);
981
1035
  if (typeof document !== "undefined") {
@@ -1290,6 +1344,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1290
1344
  if (options?.bid !== void 0) existing.bidPayload = options.bid;
1291
1345
  if (options?.metadata !== void 0) existing.metadata = options.metadata;
1292
1346
  }
1347
+ if (options?.auctionId) {
1348
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1349
+ }
1293
1350
  if (el) {
1294
1351
  if (existing.element && existing.element !== el && this.intersectionObserver) {
1295
1352
  this.intersectionObserver.unobserve(existing.element);
@@ -1302,6 +1359,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1302
1359
  } else {
1303
1360
  const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
1304
1361
  this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
1362
+ if (options?.auctionId) {
1363
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1364
+ }
1305
1365
  const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
1306
1366
  this.pendingThresholdListeners.delete(resolvedSlotId);
1307
1367
  const record = {
@@ -1357,7 +1417,485 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1357
1417
  this.unobserveSlot(slotId);
1358
1418
  this.slotViewabilityRecords.delete(slotId);
1359
1419
  this.slotRefreshIndices.delete(slotId);
1420
+ this.slotLastAuctionIds.delete(slotId);
1360
1421
  this.pendingThresholdListeners.delete(slotId);
1422
+ const prefix = `${escapeKeyPart(slotId)}:`;
1423
+ for (const key of Array.from(this.slotEmittedImpressionKeys)) {
1424
+ if (key === slotId || key.startsWith(prefix)) {
1425
+ this.slotEmittedImpressionKeys.delete(key);
1426
+ }
1427
+ }
1428
+ this.cachedWinningBids.delete(slotId);
1429
+ }
1430
+ hasImpressionEmitted(slotId, refreshIndex, auctionId, creativeId) {
1431
+ const rIndex = refreshIndex ?? this.slotRefreshIndices.get(slotId) ?? 0;
1432
+ const baseSlotKey = `${escapeKeyPart(slotId)}:${rIndex}`;
1433
+ if (creativeId) {
1434
+ if (auctionId) {
1435
+ return this.slotEmittedImpressionKeys.has(
1436
+ `${baseSlotKey}:auc:${escapeKeyPart(auctionId)}:${escapeKeyPart(creativeId)}`
1437
+ ) || this.slotEmittedImpressionKeys.has(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1438
+ }
1439
+ return this.slotEmittedImpressionKeys.has(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1440
+ }
1441
+ if (auctionId) {
1442
+ return this.slotEmittedImpressionKeys.has(`${baseSlotKey}:auc:${escapeKeyPart(auctionId)}`);
1443
+ }
1444
+ return this.slotEmittedImpressionKeys.has(baseSlotKey);
1445
+ }
1446
+ markImpressionEmitted(slotId, refreshIndex, auctionId, creativeId) {
1447
+ const baseSlotKey = `${escapeKeyPart(slotId)}:${refreshIndex}`;
1448
+ this.slotEmittedImpressionKeys.add(baseSlotKey);
1449
+ if (creativeId) {
1450
+ this.slotEmittedImpressionKeys.add(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1451
+ }
1452
+ if (auctionId) {
1453
+ this.slotEmittedImpressionKeys.add(`${baseSlotKey}:auc:${escapeKeyPart(auctionId)}`);
1454
+ if (creativeId) {
1455
+ this.slotEmittedImpressionKeys.add(
1456
+ `${baseSlotKey}:auc:${escapeKeyPart(auctionId)}:${escapeKeyPart(creativeId)}`
1457
+ );
1458
+ }
1459
+ }
1460
+ }
1461
+ recordImpression(slotId, options) {
1462
+ if (!this.isEnabled) return false;
1463
+ const resolvedSlotId = slotId || options?.adUnitCode || "";
1464
+ const adUnitCode = options?.adUnitCode || resolvedSlotId;
1465
+ const cached = (options?.auctionId && resolvedSlotId ? this.cachedWinningBids.get(`${options.auctionId}:${resolvedSlotId}`) : void 0) || (options?.auctionId && adUnitCode ? this.cachedWinningBids.get(`${options.auctionId}:${adUnitCode}`) : void 0) || (resolvedSlotId ? this.cachedWinningBids.get(resolvedSlotId) : void 0) || (adUnitCode ? this.cachedWinningBids.get(adUnitCode) : void 0);
1466
+ const rawBid = options?.bid || cached?.rawBid || {};
1467
+ const auctionId = options?.auctionId || rawBid.auctionId || cached?.auctionId || "";
1468
+ const transactionId = options?.transactionId || rawBid.transactionId || cached?.transactionId || "";
1469
+ const creativeId = rawBid.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || "";
1470
+ const mediaType = options?.mediaType || rawBid.mediaType || cached?.bidTrace?.mediaType || "banner";
1471
+ const targetSlot = resolvedSlotId || adUnitCode;
1472
+ const rec = (resolvedSlotId ? this.slotViewabilityRecords.get(resolvedSlotId) : void 0) || (adUnitCode ? this.slotViewabilityRecords.get(adUnitCode) : void 0);
1473
+ const lastAuctionId = targetSlot ? this.slotLastAuctionIds.get(targetSlot) || (rec ? rec.auctionId : void 0) : void 0;
1474
+ const isDifferentAuction = Boolean(
1475
+ targetSlot && auctionId && lastAuctionId && lastAuctionId !== auctionId && options?.refreshIndex === void 0
1476
+ );
1477
+ const bidTrace = {
1478
+ bidder: rawBid.bidderCode || rawBid.bidder || cached?.bidTrace?.bidder || "",
1479
+ cpm: Number.isFinite(rawBid.originalCpm) ? rawBid.originalCpm : Number.isFinite(rawBid.cpm) ? rawBid.cpm : cached?.bidTrace?.cpm ?? 0,
1480
+ currency: rawBid.originalCurrency ?? rawBid.currency ?? cached?.bidTrace?.currency ?? "USD",
1481
+ ...parseBidDimensions(rawBid).width ? parseBidDimensions(rawBid) : cached?.bidTrace ? { width: cached.bidTrace.width, height: cached.bidTrace.height } : parseBidDimensions(rawBid),
1482
+ dealId: rawBid.dealId || cached?.bidTrace?.dealId || "",
1483
+ mediaType,
1484
+ latencyMs: Number.isFinite(rawBid.timeToRespond) ? rawBid.timeToRespond : cached?.bidTrace?.latencyMs ?? 0,
1485
+ advertiserDomain: rawBid.meta?.advertiserDomains?.[0] || cached?.bidTrace?.advertiserDomain || "",
1486
+ creativeId: rawBid.creativeId || cached?.bidTrace?.creativeId || ""
1487
+ };
1488
+ if (isDifferentAuction && targetSlot) {
1489
+ this.flushSlotTimeInView(targetSlot);
1490
+ const nextRefreshIndex = (this.slotRefreshIndices.get(targetSlot) ?? (rec ? rec.refreshIndex : 0)) + 1;
1491
+ this.slotRefreshIndices.set(targetSlot, nextRefreshIndex);
1492
+ this.slotLastAuctionIds.set(targetSlot, auctionId);
1493
+ if (rec) {
1494
+ rec.refreshIndex = nextRefreshIndex;
1495
+ rec.auctionId = auctionId;
1496
+ rec.transactionId = transactionId;
1497
+ rec.bidPayload = bidTrace;
1498
+ rec.viewableFired = false;
1499
+ rec.accumulatedTimeInViewMs = 0;
1500
+ }
1501
+ this.enqueue(TraceEventType.REFRESH, "refresh", {
1502
+ auctionId,
1503
+ transactionId,
1504
+ adUnitCode: targetSlot,
1505
+ bid: bidTrace,
1506
+ metadata: {
1507
+ ...options?.metadata,
1508
+ refresh_index: String(nextRefreshIndex)
1509
+ }
1510
+ });
1511
+ } else if (targetSlot) {
1512
+ if (auctionId) {
1513
+ this.slotLastAuctionIds.set(targetSlot, auctionId);
1514
+ }
1515
+ if (!this.slotRefreshIndices.has(targetSlot)) {
1516
+ this.slotRefreshIndices.set(targetSlot, 0);
1517
+ }
1518
+ }
1519
+ const currentRefreshIndex = options?.refreshIndex ?? (resolvedSlotId ? this.slotRefreshIndices.get(resolvedSlotId) ?? (adUnitCode ? this.slotRefreshIndices.get(adUnitCode) : void 0) ?? (rec ? rec.refreshIndex : 0) : 0);
1520
+ const alreadyEmitted = resolvedSlotId && this.hasImpressionEmitted(resolvedSlotId, currentRefreshIndex, auctionId, creativeId) || adUnitCode && adUnitCode !== resolvedSlotId && this.hasImpressionEmitted(adUnitCode, currentRefreshIndex, auctionId, creativeId);
1521
+ if (alreadyEmitted) {
1522
+ this.log(
1523
+ "DEBUG",
1524
+ `Impression already emitted for slot ${resolvedSlotId || adUnitCode} in cycle ${currentRefreshIndex}`
1525
+ );
1526
+ return false;
1527
+ }
1528
+ if (resolvedSlotId)
1529
+ this.markImpressionEmitted(resolvedSlotId, currentRefreshIndex, auctionId, creativeId);
1530
+ if (adUnitCode && adUnitCode !== resolvedSlotId)
1531
+ this.markImpressionEmitted(adUnitCode, currentRefreshIndex, auctionId, creativeId);
1532
+ this.enqueue(TraceEventType.IMPRESSION, "impression", {
1533
+ auctionId,
1534
+ transactionId,
1535
+ adUnitCode,
1536
+ bid: bidTrace,
1537
+ metadata: {
1538
+ ...options?.metadata,
1539
+ refresh_index: String(currentRefreshIndex)
1540
+ }
1541
+ });
1542
+ if (rec) {
1543
+ if (!rec.bidPayload && bidTrace) rec.bidPayload = bidTrace;
1544
+ if (!rec.auctionId && auctionId) rec.auctionId = auctionId;
1545
+ if (!rec.transactionId && transactionId) rec.transactionId = transactionId;
1546
+ }
1547
+ return true;
1548
+ }
1549
+ /**
1550
+ * Bridges Google IMA SDK AdsManager events to Bidkernel analytics.
1551
+ * Defers IMPRESSION emission until AdEvent.STARTED (or IMPRESSION),
1552
+ * tracks milestones (FIRST_QUARTILE, MIDPOINT, THIRD_QUARTILE, COMPLETE),
1553
+ * and handles AD_ERROR.
1554
+ */
1555
+ attachImaAdsManager(adsManager, options) {
1556
+ if (!adsManager || typeof adsManager.addEventListener !== "function") {
1557
+ this.log("WARN", "Invalid AdsManager passed to attachImaAdsManager");
1558
+ return () => {
1559
+ };
1560
+ }
1561
+ const slotId = options?.slotId || options?.adUnitCode || "video";
1562
+ const adUnitCode = options?.adUnitCode || slotId;
1563
+ const auctionId = options?.auctionId || "";
1564
+ const transactionId = options?.transactionId || "";
1565
+ const getWinningBid = () => {
1566
+ return (auctionId ? this.cachedWinningBids.get(`${auctionId}:${adUnitCode}`) : void 0) || (auctionId && slotId !== adUnitCode ? this.cachedWinningBids.get(`${auctionId}:${slotId}`) : void 0) || this.cachedWinningBids.get(adUnitCode) || (slotId !== adUnitCode ? this.cachedWinningBids.get(slotId) : void 0);
1567
+ };
1568
+ const onAdStartedOrImpression = (event) => {
1569
+ this.log("DEBUG", "IMA AdEvent.STARTED / IMPRESSION received", event);
1570
+ const ad = typeof event?.getAd === "function" ? event.getAd() : event?.ad;
1571
+ const adData = {};
1572
+ if (ad) {
1573
+ adData.creativeId = (typeof ad.getCreativeId === "function" ? ad.getCreativeId() : ad.creativeId) || "";
1574
+ adData.adId = (typeof ad.getAdId === "function" ? ad.getAdId() : ad.id) || "";
1575
+ adData.title = (typeof ad.getTitle === "function" ? ad.getTitle() : ad.title) || "";
1576
+ adData.duration = typeof ad.getDuration === "function" ? ad.getDuration() : ad.duration;
1577
+ adData.advertiserName = typeof ad.getAdvertiserName === "function" ? ad.getAdvertiserName() : "";
1578
+ }
1579
+ const cached = getWinningBid();
1580
+ const mergedBid = {
1581
+ ...cached?.rawBid || options?.bid,
1582
+ mediaType: "video",
1583
+ creativeId: adData.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || ""
1584
+ };
1585
+ const emitted = this.recordImpression(slotId, {
1586
+ adUnitCode,
1587
+ auctionId: auctionId || cached?.auctionId,
1588
+ transactionId: transactionId || cached?.transactionId,
1589
+ mediaType: "video",
1590
+ bid: mergedBid,
1591
+ metadata: {
1592
+ ...options?.metadata,
1593
+ media_type: "video",
1594
+ ...adData.title ? { ad_title: adData.title } : {},
1595
+ ...Number.isFinite(adData.duration) ? { ad_duration_s: String(adData.duration) } : {}
1596
+ }
1597
+ });
1598
+ if (emitted && options?.onImpression) {
1599
+ try {
1600
+ options.onImpression(slotId, { ad, bid: mergedBid });
1601
+ } catch (e) {
1602
+ this.log("ERROR", "Error in onImpression callback", e);
1603
+ }
1604
+ }
1605
+ };
1606
+ const onMilestone = (milestone) => {
1607
+ this.log("DEBUG", `IMA AdEvent milestone: ${milestone}`);
1608
+ if (options?.onMilestone) {
1609
+ try {
1610
+ options.onMilestone(milestone, slotId);
1611
+ } catch (e) {
1612
+ this.log("ERROR", "Error in onMilestone callback", e);
1613
+ }
1614
+ }
1615
+ };
1616
+ const onAdClick = (event) => {
1617
+ this.log("DEBUG", "IMA AdEvent.CLICK received", event);
1618
+ const cached = getWinningBid();
1619
+ this.enqueue(TraceEventType.CLICK, "click", {
1620
+ auctionId: auctionId || cached?.auctionId,
1621
+ transactionId: transactionId || cached?.transactionId,
1622
+ adUnitCode,
1623
+ bid: cached?.bidTrace || options?.bid,
1624
+ metadata: {
1625
+ ...options?.metadata,
1626
+ media_type: "video"
1627
+ }
1628
+ });
1629
+ };
1630
+ const onAdError = (event) => {
1631
+ this.log("WARN", "IMA AdErrorEvent received", event);
1632
+ const err = typeof event?.getError === "function" ? event.getError() : event?.error || event;
1633
+ const msg = (err && typeof err.getMessage === "function" ? err.getMessage() : err?.message) || String(err || "IMA ad error");
1634
+ const code = (err && typeof err.getErrorCode === "function" ? err.getErrorCode() : err?.code) || "";
1635
+ const cached = getWinningBid();
1636
+ this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
1637
+ auctionId: auctionId || cached?.auctionId,
1638
+ transactionId: transactionId || cached?.transactionId,
1639
+ adUnitCode,
1640
+ bid: cached?.bidTrace || options?.bid,
1641
+ metadata: {
1642
+ ...options?.metadata,
1643
+ reason: "ima_ad_error",
1644
+ message: msg,
1645
+ ...code ? { ima_error_code: String(code) } : {}
1646
+ },
1647
+ error: typeof err === "object" ? err : new Error(msg)
1648
+ });
1649
+ if (options?.onError) {
1650
+ try {
1651
+ options.onError(err);
1652
+ } catch (e) {
1653
+ this.log("ERROR", "Error in onError callback", e);
1654
+ }
1655
+ }
1656
+ };
1657
+ const googleIma = typeof window !== "undefined" ? window.google?.ima : void 0;
1658
+ const adEventType = googleIma?.AdEvent?.Type || {};
1659
+ const adErrorEventType = googleIma?.AdErrorEvent?.Type || {};
1660
+ const listeners = [
1661
+ { type: adEventType.STARTED || "started", handler: onAdStartedOrImpression },
1662
+ { type: adEventType.IMPRESSION || "impression", handler: onAdStartedOrImpression },
1663
+ {
1664
+ type: adEventType.FIRST_QUARTILE || "firstQuartile",
1665
+ handler: () => onMilestone("firstQuartile")
1666
+ },
1667
+ { type: adEventType.MIDPOINT || "midpoint", handler: () => onMilestone("midpoint") },
1668
+ {
1669
+ type: adEventType.THIRD_QUARTILE || "thirdQuartile",
1670
+ handler: () => onMilestone("thirdQuartile")
1671
+ },
1672
+ { type: adEventType.COMPLETE || "complete", handler: () => onMilestone("complete") },
1673
+ { type: adEventType.CLICK || "click", handler: onAdClick },
1674
+ { type: adErrorEventType.AD_ERROR || "adError", handler: onAdError }
1675
+ ];
1676
+ for (const { type, handler } of listeners) {
1677
+ try {
1678
+ adsManager.addEventListener(type, handler);
1679
+ } catch {
1680
+ }
1681
+ }
1682
+ const cleanup = () => {
1683
+ for (const { type, handler } of listeners) {
1684
+ try {
1685
+ adsManager.removeEventListener(type, handler);
1686
+ } catch {
1687
+ }
1688
+ }
1689
+ this.videoDetachCleanups.delete(cleanup);
1690
+ };
1691
+ this.videoDetachCleanups.add(cleanup);
1692
+ return cleanup;
1693
+ }
1694
+ /**
1695
+ * Attaches render hooks and viewability tracking to a video player.
1696
+ * Supports HTML5 <video> elements, container elements, or Google IMA AdsManager.
1697
+ */
1698
+ attachVideoPlayer(target, options) {
1699
+ if (!target) {
1700
+ this.log("WARN", "attachVideoPlayer called with null/undefined target");
1701
+ return () => {
1702
+ };
1703
+ }
1704
+ let el = null;
1705
+ if (typeof target === "string") {
1706
+ if (typeof document !== "undefined") {
1707
+ try {
1708
+ el = document.querySelector(target) || document.getElementById(target);
1709
+ } catch {
1710
+ el = document.getElementById(target);
1711
+ }
1712
+ }
1713
+ } else if (typeof HTMLElement !== "undefined" && target instanceof HTMLElement) {
1714
+ el = target;
1715
+ } else if (target && typeof target === "object" && target.nodeType === 1) {
1716
+ el = target;
1717
+ } else if (
1718
+ // Check if target is an IMA AdsManager
1719
+ typeof target.addEventListener === "function" && (typeof target.init === "function" || typeof target.start === "function" || typeof target.getCuePoints === "function" || typeof target.getVolume === "function" || target.__imaAdsManager)
1720
+ ) {
1721
+ return this.attachImaAdsManager(target, options);
1722
+ } else if (target && typeof target.addEventListener === "function") {
1723
+ el = target;
1724
+ }
1725
+ const slotId = options?.slotId || options?.adUnitCode || (el ? el.id : "") || "video";
1726
+ const adUnitCode = options?.adUnitCode || slotId;
1727
+ const auctionId = options?.auctionId || "";
1728
+ const transactionId = options?.transactionId || "";
1729
+ const getWinningBid = () => {
1730
+ return (auctionId ? this.cachedWinningBids.get(`${auctionId}:${adUnitCode}`) : void 0) || (auctionId && slotId !== adUnitCode ? this.cachedWinningBids.get(`${auctionId}:${slotId}`) : void 0) || this.cachedWinningBids.get(adUnitCode) || (slotId !== adUnitCode ? this.cachedWinningBids.get(slotId) : void 0);
1731
+ };
1732
+ let videoEl = null;
1733
+ if (el) {
1734
+ if (el.tagName && el.tagName.toLowerCase() === "video") {
1735
+ videoEl = el;
1736
+ } else {
1737
+ videoEl = el.querySelector("video");
1738
+ }
1739
+ }
1740
+ const quartilesFired = {
1741
+ q1: false,
1742
+ q2: false,
1743
+ q3: false,
1744
+ q4: false
1745
+ };
1746
+ const triggerPlaybackImpression = (activeVideo) => {
1747
+ const vEl = activeVideo || videoEl || (el?.querySelector ? el.querySelector("video") : null);
1748
+ const cached = getWinningBid();
1749
+ const mergedBid = {
1750
+ ...cached?.rawBid || options?.bid,
1751
+ mediaType: "video"
1752
+ };
1753
+ const emitted = this.recordImpression(slotId, {
1754
+ adUnitCode,
1755
+ auctionId: auctionId || cached?.auctionId,
1756
+ transactionId: transactionId || cached?.transactionId,
1757
+ mediaType: "video",
1758
+ bid: mergedBid,
1759
+ metadata: {
1760
+ ...options?.metadata,
1761
+ media_type: "video",
1762
+ ...vEl && Number.isFinite(vEl.duration) ? { video_duration_s: String(Math.round(vEl.duration)) } : {}
1763
+ }
1764
+ });
1765
+ if (emitted && options?.onImpression) {
1766
+ try {
1767
+ options.onImpression(slotId, { element: vEl || el, bid: mergedBid });
1768
+ } catch (e) {
1769
+ this.log("ERROR", "Error in onImpression callback", e);
1770
+ }
1771
+ }
1772
+ };
1773
+ const handlePlaying = (e) => {
1774
+ this.log("DEBUG", "HTML5 video playing event received");
1775
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
1776
+ triggerPlaybackImpression(targetVideo);
1777
+ };
1778
+ const handleTimeUpdate = (e) => {
1779
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
1780
+ if (targetVideo && targetVideo.currentTime > 0) {
1781
+ triggerPlaybackImpression(targetVideo);
1782
+ if (Number.isFinite(targetVideo.duration) && targetVideo.duration > 0) {
1783
+ const progress = targetVideo.currentTime / targetVideo.duration;
1784
+ if (progress >= 0.25 && !quartilesFired.q1) {
1785
+ quartilesFired.q1 = true;
1786
+ options?.onMilestone?.("firstQuartile", slotId);
1787
+ }
1788
+ if (progress >= 0.5 && !quartilesFired.q2) {
1789
+ quartilesFired.q2 = true;
1790
+ options?.onMilestone?.("midpoint", slotId);
1791
+ }
1792
+ if (progress >= 0.75 && !quartilesFired.q3) {
1793
+ quartilesFired.q3 = true;
1794
+ options?.onMilestone?.("thirdQuartile", slotId);
1795
+ }
1796
+ }
1797
+ }
1798
+ };
1799
+ const handleEnded = () => {
1800
+ if (!quartilesFired.q4) {
1801
+ quartilesFired.q4 = true;
1802
+ options?.onMilestone?.("complete", slotId);
1803
+ }
1804
+ };
1805
+ const handleError = (e) => {
1806
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
1807
+ const err = targetVideo?.error || videoEl?.error;
1808
+ const cached = getWinningBid();
1809
+ const msg = err ? `HTML5 video error code ${err.code}: ${err.message}` : "Video playback error";
1810
+ this.log("WARN", msg);
1811
+ this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
1812
+ auctionId: auctionId || cached?.auctionId,
1813
+ transactionId: transactionId || cached?.transactionId,
1814
+ adUnitCode,
1815
+ bid: cached?.bidTrace || options?.bid,
1816
+ metadata: {
1817
+ ...options?.metadata,
1818
+ reason: "html5_video_error",
1819
+ message: msg,
1820
+ ...err?.code ? { video_error_code: String(err.code) } : {}
1821
+ },
1822
+ error: err ? new Error(msg) : void 0
1823
+ });
1824
+ if (options?.onError) {
1825
+ try {
1826
+ options.onError(err);
1827
+ } catch (e2) {
1828
+ this.log("ERROR", "Error in onError callback", e2);
1829
+ }
1830
+ }
1831
+ };
1832
+ const attachListenersToVideo = (targetVideo) => {
1833
+ targetVideo.addEventListener("playing", handlePlaying);
1834
+ targetVideo.addEventListener("play", handlePlaying);
1835
+ targetVideo.addEventListener("timeupdate", handleTimeUpdate);
1836
+ targetVideo.addEventListener("ended", handleEnded);
1837
+ targetVideo.addEventListener("error", handleError);
1838
+ };
1839
+ const removeListenersFromVideo = (targetVideo) => {
1840
+ targetVideo.removeEventListener("playing", handlePlaying);
1841
+ targetVideo.removeEventListener("play", handlePlaying);
1842
+ targetVideo.removeEventListener("timeupdate", handleTimeUpdate);
1843
+ targetVideo.removeEventListener("ended", handleEnded);
1844
+ targetVideo.removeEventListener("error", handleError);
1845
+ };
1846
+ let mutationObserver = null;
1847
+ if (el && el !== videoEl) {
1848
+ el.addEventListener("playing", handlePlaying, true);
1849
+ el.addEventListener("play", handlePlaying, true);
1850
+ el.addEventListener("timeupdate", handleTimeUpdate, true);
1851
+ el.addEventListener("ended", handleEnded, true);
1852
+ el.addEventListener("error", handleError, true);
1853
+ }
1854
+ if (videoEl) {
1855
+ attachListenersToVideo(videoEl);
1856
+ }
1857
+ if (el && el.tagName?.toLowerCase() !== "video" && typeof MutationObserver !== "undefined") {
1858
+ mutationObserver = new MutationObserver(() => {
1859
+ const found = el.querySelector("video");
1860
+ if (found && found !== videoEl) {
1861
+ if (videoEl) removeListenersFromVideo(videoEl);
1862
+ videoEl = found;
1863
+ if (videoEl) attachListenersToVideo(videoEl);
1864
+ }
1865
+ });
1866
+ mutationObserver.observe(el, { childList: true, subtree: true });
1867
+ }
1868
+ const cachedInitial = getWinningBid();
1869
+ if (options?.trackViewability !== false && el) {
1870
+ this.observeSlot(el, slotId, {
1871
+ adUnitCode,
1872
+ auctionId: auctionId || cachedInitial?.auctionId,
1873
+ transactionId: transactionId || cachedInitial?.transactionId,
1874
+ mediaType: "video",
1875
+ bid: cachedInitial?.bidTrace || options?.bid,
1876
+ metadata: options?.metadata,
1877
+ emitRefreshEvent: false
1878
+ });
1879
+ }
1880
+ const cleanup = () => {
1881
+ if (mutationObserver) {
1882
+ mutationObserver.disconnect();
1883
+ mutationObserver = null;
1884
+ }
1885
+ if (el && el !== videoEl) {
1886
+ el.removeEventListener("playing", handlePlaying, true);
1887
+ el.removeEventListener("play", handlePlaying, true);
1888
+ el.removeEventListener("timeupdate", handleTimeUpdate, true);
1889
+ el.removeEventListener("ended", handleEnded, true);
1890
+ el.removeEventListener("error", handleError, true);
1891
+ }
1892
+ if (videoEl) {
1893
+ removeListenersFromVideo(videoEl);
1894
+ }
1895
+ this.videoDetachCleanups.delete(cleanup);
1896
+ };
1897
+ this.videoDetachCleanups.add(cleanup);
1898
+ return cleanup;
1361
1899
  }
1362
1900
  onTimeInViewThreshold(slotId, thresholdMs, callback) {
1363
1901
  const listener = {
@@ -1570,21 +2108,45 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1570
2108
  handleBidWon(data) {
1571
2109
  if (this.isDuplicate("bidWon", data)) return;
1572
2110
  this.log("DEBUG", "bidWon", data);
1573
- this.enqueue(TraceEventType.BID_WIN, "bidWon", {
1574
- auctionId: data.auctionId || "",
1575
- transactionId: data.transactionId || "",
1576
- adUnitCode: data.adUnitCode || "",
1577
- bid: {
1578
- bidder: data.bidderCode || data.bidder || "",
1579
- cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1580
- currency: data.originalCurrency ?? data.currency ?? "USD",
1581
- ...parseBidDimensions(data),
1582
- dealId: data.dealId || "",
1583
- mediaType: data.mediaType || "banner",
1584
- latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
1585
- advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
1586
- creativeId: data.creativeId || ""
2111
+ const auctionId = data.auctionId || "";
2112
+ const transactionId = data.transactionId || "";
2113
+ const adUnitCode = data.adUnitCode || data.adId || "";
2114
+ const bidTrace = {
2115
+ bidder: data.bidderCode || data.bidder || "",
2116
+ cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
2117
+ currency: data.originalCurrency ?? data.currency ?? "USD",
2118
+ ...parseBidDimensions(data),
2119
+ dealId: data.dealId || "",
2120
+ mediaType: data.mediaType || "banner",
2121
+ latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
2122
+ advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
2123
+ creativeId: data.creativeId || ""
2124
+ };
2125
+ if (adUnitCode) {
2126
+ const cachedEntry = {
2127
+ bidTrace,
2128
+ auctionId,
2129
+ transactionId,
2130
+ adUnitCode,
2131
+ rawBid: data,
2132
+ timestamp: Date.now()
2133
+ };
2134
+ this.cachedWinningBids.set(adUnitCode, cachedEntry);
2135
+ if (data.adId && data.adId !== adUnitCode) {
2136
+ this.cachedWinningBids.set(data.adId, cachedEntry);
2137
+ }
2138
+ if (auctionId) {
2139
+ this.cachedWinningBids.set(`${auctionId}:${adUnitCode}`, cachedEntry);
2140
+ if (data.adId && data.adId !== adUnitCode) {
2141
+ this.cachedWinningBids.set(`${auctionId}:${data.adId}`, cachedEntry);
2142
+ }
1587
2143
  }
2144
+ }
2145
+ this.enqueue(TraceEventType.BID_WIN, "bidWon", {
2146
+ auctionId,
2147
+ transactionId,
2148
+ adUnitCode,
2149
+ bid: bidTrace
1588
2150
  });
1589
2151
  }
1590
2152
  handleNoBid(data) {
@@ -1623,44 +2185,81 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1623
2185
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1624
2186
  this.log("DEBUG", "adRenderSucceeded", data);
1625
2187
  const bid = data.bid || data || {};
1626
- const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
2188
+ const adUnitCode = data.adUnitCode || bid.adUnitCode || data.adId || bid.adId || "";
1627
2189
  const auctionId = bid.auctionId || data.auctionId || "";
1628
2190
  const transactionId = bid.transactionId || data.transactionId || "";
1629
2191
  const mediaType = bid.mediaType || "banner";
2192
+ const slotKey = data.adUnitCode || bid.adUnitCode || "";
2193
+ const altKey = data.adId || bid.adId || "";
2194
+ const cached = (auctionId && slotKey ? this.cachedWinningBids.get(`${auctionId}:${slotKey}`) : void 0) || (auctionId && altKey ? this.cachedWinningBids.get(`${auctionId}:${altKey}`) : void 0) || (slotKey ? this.cachedWinningBids.get(slotKey) : void 0) || (altKey ? this.cachedWinningBids.get(altKey) : void 0);
1630
2195
  const bidTrace = {
1631
- bidder: bid.bidderCode || bid.bidder || "",
1632
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1633
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1634
- ...parseBidDimensions(bid),
1635
- dealId: bid.dealId || "",
1636
- mediaType,
1637
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1638
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1639
- creativeId: bid.creativeId || ""
2196
+ bidder: bid.bidderCode || bid.bidder || cached?.bidTrace?.bidder || "",
2197
+ cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : cached?.bidTrace?.cpm ?? 0,
2198
+ currency: bid.originalCurrency ?? bid.currency ?? cached?.bidTrace?.currency ?? "USD",
2199
+ ...parseBidDimensions(bid).width ? parseBidDimensions(bid) : cached?.bidTrace ? { width: cached.bidTrace.width, height: cached.bidTrace.height } : parseBidDimensions(bid),
2200
+ dealId: bid.dealId || cached?.bidTrace?.dealId || "",
2201
+ mediaType: mediaType || cached?.bidTrace?.mediaType || "banner",
2202
+ latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : cached?.bidTrace?.latencyMs ?? 0,
2203
+ advertiserDomain: bid.meta?.advertiserDomains?.[0] || cached?.bidTrace?.advertiserDomain || "",
2204
+ creativeId: bid.creativeId || cached?.bidTrace?.creativeId || ""
1640
2205
  };
1641
- const isRefresh = adUnitCode && (this.slotViewabilityRecords.has(adUnitCode) || this.slotRefreshIndices.has(adUnitCode) && this.slotRefreshIndices.get(adUnitCode) > 0);
2206
+ const resolvedAuctionId = auctionId || cached?.auctionId || "";
2207
+ const resolvedTransactionId = transactionId || cached?.transactionId || "";
2208
+ const record = adUnitCode ? this.slotViewabilityRecords.get(adUnitCode) : void 0;
2209
+ const isDifferentAuction = record && resolvedAuctionId && record.auctionId && record.auctionId !== resolvedAuctionId;
2210
+ const isRefresh = Boolean(adUnitCode && record && isDifferentAuction);
1642
2211
  if (isRefresh) {
1643
2212
  this.flushSlotTimeInView(adUnitCode);
1644
- const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
2213
+ const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0)) + 1;
1645
2214
  this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
2215
+ this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
2216
+ if (record) {
2217
+ record.refreshIndex = nextRefreshIndex;
2218
+ record.auctionId = resolvedAuctionId;
2219
+ record.transactionId = resolvedTransactionId;
2220
+ record.bidPayload = bidTrace;
2221
+ record.viewableFired = false;
2222
+ record.accumulatedTimeInViewMs = 0;
2223
+ }
1646
2224
  this.enqueue(TraceEventType.REFRESH, "refresh", {
1647
- auctionId,
1648
- transactionId,
2225
+ auctionId: resolvedAuctionId,
2226
+ transactionId: resolvedTransactionId,
1649
2227
  adUnitCode,
1650
2228
  bid: bidTrace,
1651
2229
  metadata: { refresh_index: String(nextRefreshIndex) }
1652
2230
  });
1653
- } else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
1654
- this.slotRefreshIndices.set(adUnitCode, 0);
2231
+ } else if (adUnitCode) {
2232
+ if (resolvedAuctionId) {
2233
+ this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
2234
+ }
2235
+ if (!this.slotRefreshIndices.has(adUnitCode)) {
2236
+ this.slotRefreshIndices.set(adUnitCode, 0);
2237
+ }
1655
2238
  }
1656
- const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
1657
- this.enqueue(TraceEventType.IMPRESSION, "impression", {
1658
- auctionId,
1659
- transactionId,
2239
+ const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0) : 0;
2240
+ const alreadyEmitted = adUnitCode ? this.hasImpressionEmitted(
1660
2241
  adUnitCode,
1661
- bid: bidTrace,
1662
- metadata: { refresh_index: String(currentRefreshIndex) }
1663
- });
2242
+ currentRefreshIndex,
2243
+ resolvedAuctionId,
2244
+ bidTrace.creativeId
2245
+ ) : false;
2246
+ if (!alreadyEmitted) {
2247
+ if (adUnitCode) {
2248
+ this.markImpressionEmitted(
2249
+ adUnitCode,
2250
+ currentRefreshIndex,
2251
+ resolvedAuctionId,
2252
+ bidTrace.creativeId
2253
+ );
2254
+ }
2255
+ this.enqueue(TraceEventType.IMPRESSION, "impression", {
2256
+ auctionId: resolvedAuctionId,
2257
+ transactionId: resolvedTransactionId,
2258
+ adUnitCode,
2259
+ bid: bidTrace,
2260
+ metadata: { refresh_index: String(currentRefreshIndex) }
2261
+ });
2262
+ }
1664
2263
  if (this.config.viewabilityEnabled && typeof document !== "undefined") {
1665
2264
  let el = null;
1666
2265
  const targetId = adUnitCode || data.adId || bid.adId;
@@ -1688,8 +2287,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1688
2287
  if (el) {
1689
2288
  this.observeSlot(el, adUnitCode || targetId, {
1690
2289
  adUnitCode,
1691
- auctionId,
1692
- transactionId,
2290
+ auctionId: resolvedAuctionId,
2291
+ transactionId: resolvedTransactionId,
1693
2292
  mediaType,
1694
2293
  bid: bidTrace,
1695
2294
  refreshIndex: currentRefreshIndex,
@@ -1754,6 +2353,12 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1754
2353
  }
1755
2354
  if (this.queue.length >= BATCH_SIZE) {
1756
2355
  this.flush();
2356
+ } else if (IMMEDIATE_FLUSH_TYPES.has(type) && !this.immediateFlushScheduled) {
2357
+ this.immediateFlushScheduled = true;
2358
+ setTimeout(() => {
2359
+ this.immediateFlushScheduled = false;
2360
+ this.flush();
2361
+ }, 0);
1757
2362
  }
1758
2363
  }
1759
2364
  shouldSample(type, level) {
@@ -1810,10 +2415,11 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1810
2415
  events
1811
2416
  };
1812
2417
  }
1813
- sendFetch(payload, useKeepalive = false) {
2418
+ sendFetch(payload, useKeepalive = false, onDelivered) {
1814
2419
  const fetchOpts = {
1815
2420
  method: "POST",
1816
- headers: { "Content-Type": "application/x-protobuf" },
2421
+ // Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
2422
+ headers: { "Content-Type": SAFE_CONTENT_TYPE },
1817
2423
  body: payload.encoded
1818
2424
  };
1819
2425
  if (useKeepalive) {
@@ -1822,7 +2428,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1822
2428
  fetch(payload.url, fetchOpts).then((res) => {
1823
2429
  if (!res.ok) {
1824
2430
  this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
1825
- if (res.status >= 400 && res.status < 500) {
2431
+ if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 408) {
1826
2432
  this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
1827
2433
  return;
1828
2434
  }
@@ -1830,14 +2436,16 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1830
2436
  } else {
1831
2437
  this.consecutiveSendFailures = 0;
1832
2438
  this.nextSendAllowedAt = 0;
2439
+ if (onDelivered) onDelivered();
1833
2440
  }
1834
2441
  }).catch((err) => {
1835
2442
  this.log("ERROR", "Failed to send batch", err);
1836
2443
  this.handleSendFailure(payload.events);
1837
2444
  });
1838
- if (typeof process !== "undefined" && typeof process._tickCallback === "function") {
2445
+ const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
2446
+ if (proc && typeof proc._tickCallback === "function") {
1839
2447
  try {
1840
- process._tickCallback();
2448
+ proc._tickCallback();
1841
2449
  } catch {
1842
2450
  }
1843
2451
  }
@@ -1873,19 +2481,99 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1873
2481
  let sent = false;
1874
2482
  if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
1875
2483
  try {
1876
- const blob = new Blob([payload.encoded], {
1877
- type: "application/x-protobuf"
1878
- });
2484
+ const blob = new Blob([payload.encoded], { type: SAFE_CONTENT_TYPE });
1879
2485
  sent = navigator.sendBeacon(payload.url, blob);
1880
2486
  } catch {
1881
2487
  sent = false;
1882
2488
  }
1883
2489
  }
1884
2490
  if (!sent) {
1885
- this.sendFetch(payload, true);
2491
+ const persistKey = this.persistBatch(payload.encoded);
2492
+ this.sendFetch(payload, true, () => this.removePersistedBatch(persistKey));
1886
2493
  }
1887
2494
  payload = this.drainBatch();
1888
2495
  }
2496
+ this.schedulePersistedCleanup();
2497
+ }
2498
+ // --- Exit-batch persistence -----------------------------------------------
2499
+ persistBatch(encoded) {
2500
+ try {
2501
+ if (typeof localStorage === "undefined") return null;
2502
+ let pendingCount = 0;
2503
+ for (let i = 0; i < localStorage.length; i++) {
2504
+ const k = localStorage.key(i);
2505
+ if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
2506
+ }
2507
+ if (pendingCount >= MAX_PENDING_BATCHES) return null;
2508
+ const key = `${PENDING_BATCH_KEY_PREFIX}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
2509
+ localStorage.setItem(key, bytesToBase64(encoded));
2510
+ this.persistedBatchKeys.push(key);
2511
+ return key;
2512
+ } catch {
2513
+ return null;
2514
+ }
2515
+ }
2516
+ removePersistedBatch(key) {
2517
+ if (!key) return;
2518
+ try {
2519
+ if (typeof localStorage !== "undefined") localStorage.removeItem(key);
2520
+ } catch {
2521
+ }
2522
+ const idx = this.persistedBatchKeys.indexOf(key);
2523
+ if (idx !== -1) this.persistedBatchKeys.splice(idx, 1);
2524
+ }
2525
+ // A page that is still running PENDING_BATCH_CLEANUP_DELAY_MS after an exit
2526
+ // flush was never torn down, so its beacons have long since gone out; drop
2527
+ // the persisted copies rather than letting a later pageview resend them.
2528
+ schedulePersistedCleanup() {
2529
+ if (this.persistedBatchKeys.length === 0) return;
2530
+ if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
2531
+ this.persistedCleanupTimer = setTimeout(() => {
2532
+ this.persistedCleanupTimer = null;
2533
+ for (const key of Array.from(this.persistedBatchKeys)) {
2534
+ this.removePersistedBatch(key);
2535
+ }
2536
+ }, PENDING_BATCH_CLEANUP_DELAY_MS);
2537
+ }
2538
+ // Resend batches a previous pageview persisted at exit but could not
2539
+ // confirm. Keys are claimed (removed) before sending so concurrent tabs
2540
+ // don't double-send; the batch bytes carry their original pageview, session,
2541
+ // and event timestamps, so late rows attribute correctly.
2542
+ resendPersistedBatches() {
2543
+ try {
2544
+ if (typeof localStorage === "undefined" || !this.config.endpoint) return;
2545
+ const keys = [];
2546
+ for (let i = 0; i < localStorage.length; i++) {
2547
+ const k = localStorage.key(i);
2548
+ if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) keys.push(k);
2549
+ }
2550
+ for (const key of keys) {
2551
+ let value = null;
2552
+ try {
2553
+ value = localStorage.getItem(key);
2554
+ localStorage.removeItem(key);
2555
+ } catch {
2556
+ continue;
2557
+ }
2558
+ if (!value) continue;
2559
+ const ts = parseInt(key.slice(PENDING_BATCH_KEY_PREFIX.length), 10);
2560
+ if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
2561
+ let encoded;
2562
+ try {
2563
+ encoded = base64ToBytes(value);
2564
+ } catch {
2565
+ continue;
2566
+ }
2567
+ if (encoded.byteLength === 0) continue;
2568
+ this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
2569
+ this.sendFetch({
2570
+ url: `${this.config.endpoint}/${this.config.propertyId}`,
2571
+ encoded,
2572
+ events: []
2573
+ });
2574
+ }
2575
+ } catch {
2576
+ }
1889
2577
  }
1890
2578
  log(level, msg, ...args) {
1891
2579
  const levels = { DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 };
@@ -2012,7 +2700,28 @@ function getbidkernel(alias = "bidkernel") {
2012
2700
  }
2013
2701
  const win = window;
2014
2702
  win[alias] = win[alias] || { q: [] };
2015
- return win[alias];
2703
+ const sdk = win[alias];
2704
+ if (!sdk.attachVideoPlayer) {
2705
+ sdk.attachVideoPlayer = (target, options) => {
2706
+ const inst = win._bidkernelPrebidAnalytics;
2707
+ return inst?.attachVideoPlayer ? inst.attachVideoPlayer(target, options) : () => {
2708
+ };
2709
+ };
2710
+ }
2711
+ if (!sdk.attachImaAdsManager) {
2712
+ sdk.attachImaAdsManager = (adsManager, options) => {
2713
+ const inst = win._bidkernelPrebidAnalytics;
2714
+ return inst?.attachImaAdsManager ? inst.attachImaAdsManager(adsManager, options) : () => {
2715
+ };
2716
+ };
2717
+ }
2718
+ if (!sdk.recordImpression) {
2719
+ sdk.recordImpression = (slotId, options) => {
2720
+ const inst = win._bidkernelPrebidAnalytics;
2721
+ return inst?.recordImpression ? inst.recordImpression(slotId, options) : false;
2722
+ };
2723
+ }
2724
+ return sdk;
2016
2725
  }
2017
2726
  export {
2018
2727
  BidkernelPrebidAnalytics,