@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.js CHANGED
@@ -658,6 +658,11 @@ var MAX_QUEUE_SIZE = 200;
658
658
  var MAX_SEND_BACKOFF_MS = 5 * 60 * 1e3;
659
659
  var MAX_CONSECUTIVE_SEND_FAILURES = 10;
660
660
  var MAX_PAYLOAD_BYTES = 32 * 1024;
661
+ var SAFE_CONTENT_TYPE = "text/plain";
662
+ var PENDING_BATCH_KEY_PREFIX = "_bidkernel_pending_";
663
+ var MAX_PENDING_BATCHES = 20;
664
+ var PENDING_BATCH_MAX_AGE_MS = 2 * 60 * 60 * 1e3;
665
+ var PENDING_BATCH_CLEANUP_DELAY_MS = 1e4;
661
666
  var EVENT_NAME_TO_TYPE = {
662
667
  auctionStart: TraceEventType.AUCTION_START,
663
668
  auctionEnd: TraceEventType.AUCTION_END,
@@ -673,6 +678,11 @@ var EVENT_NAME_TO_TYPE = {
673
678
  timeInView: TraceEventType.TIME_IN_VIEW,
674
679
  viewable: TraceEventType.VIEWABLE
675
680
  };
681
+ var IMMEDIATE_FLUSH_TYPES = /* @__PURE__ */ new Set([
682
+ TraceEventType.IMPRESSION,
683
+ TraceEventType.BID_WIN,
684
+ TraceEventType.CLICK
685
+ ]);
676
686
  var SESSION_KEY = "_bidkernel_session";
677
687
  var SESSION_TS_KEY = "_bidkernel_session_ts";
678
688
  var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
@@ -688,6 +698,22 @@ function generateUUID() {
688
698
  return v.toString(16);
689
699
  });
690
700
  }
701
+ function bytesToBase64(bytes) {
702
+ let binary = "";
703
+ const chunk = 8192;
704
+ for (let i = 0; i < bytes.length; i += chunk) {
705
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
706
+ }
707
+ return btoa(binary);
708
+ }
709
+ function base64ToBytes(b64) {
710
+ const binary = atob(b64);
711
+ const bytes = new Uint8Array(binary.length);
712
+ for (let i = 0; i < binary.length; i++) {
713
+ bytes[i] = binary.charCodeAt(i);
714
+ }
715
+ return bytes;
716
+ }
691
717
  function extendSession() {
692
718
  const now = Date.now();
693
719
  inMemorySessionTs = now;
@@ -852,12 +878,25 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
852
878
  consecutiveSendFailures = 0;
853
879
  nextSendAllowedAt = 0;
854
880
  replayedEventCount = 0;
881
+ immediateFlushScheduled = false;
882
+ // localStorage keys of exit batches this instance persisted, so a page that
883
+ // survives its own pagehide/hidden (bfcache restore, tab re-focus) can
884
+ // remove them instead of leaving them for a duplicate resend.
885
+ persistedBatchKeys = [];
886
+ persistedCleanupTimer = null;
855
887
  // Viewability & refresh tracking
856
888
  intersectionObserver = null;
857
889
  slotViewabilityRecords = /* @__PURE__ */ new Map();
858
890
  elementToSlotId = /* @__PURE__ */ new Map();
859
891
  slotRefreshIndices = /* @__PURE__ */ new Map();
892
+ slotLastAuctionIds = /* @__PURE__ */ new Map();
860
893
  pendingThresholdListeners = /* @__PURE__ */ new Map();
894
+ // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
895
+ slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
896
+ // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
897
+ cachedWinningBids = /* @__PURE__ */ new Map();
898
+ // Active video player attachment cleanup routines
899
+ videoDetachCleanups = /* @__PURE__ */ new Set();
861
900
  constructor(config) {
862
901
  this.config = {
863
902
  endpoint: config.endpoint || "",
@@ -976,6 +1015,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
976
1015
  document.addEventListener("visibilitychange", this.boundVisibilityChange);
977
1016
  }
978
1017
  this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
1018
+ this.resendPersistedBatches();
979
1019
  }
980
1020
  }
981
1021
  disable() {
@@ -1002,10 +1042,24 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1002
1042
  }
1003
1043
  this.slotViewabilityRecords.clear();
1004
1044
  this.elementToSlotId.clear();
1045
+ for (const cleanup of this.videoDetachCleanups) {
1046
+ try {
1047
+ cleanup();
1048
+ } catch {
1049
+ }
1050
+ }
1051
+ this.videoDetachCleanups.clear();
1052
+ this.cachedWinningBids.clear();
1053
+ this.slotEmittedImpressionKeys.clear();
1054
+ this.slotLastAuctionIds.clear();
1005
1055
  if (this.flushTimer) {
1006
1056
  clearInterval(this.flushTimer);
1007
1057
  this.flushTimer = null;
1008
1058
  }
1059
+ if (this.persistedCleanupTimer) {
1060
+ clearTimeout(this.persistedCleanupTimer);
1061
+ this.persistedCleanupTimer = null;
1062
+ }
1009
1063
  if (typeof window !== "undefined") {
1010
1064
  window.removeEventListener("pagehide", this.boundFlushBeacon);
1011
1065
  if (typeof document !== "undefined") {
@@ -1320,6 +1374,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1320
1374
  if (options?.bid !== void 0) existing.bidPayload = options.bid;
1321
1375
  if (options?.metadata !== void 0) existing.metadata = options.metadata;
1322
1376
  }
1377
+ if (options?.auctionId) {
1378
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1379
+ }
1323
1380
  if (el) {
1324
1381
  if (existing.element && existing.element !== el && this.intersectionObserver) {
1325
1382
  this.intersectionObserver.unobserve(existing.element);
@@ -1332,6 +1389,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1332
1389
  } else {
1333
1390
  const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
1334
1391
  this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
1392
+ if (options?.auctionId) {
1393
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1394
+ }
1335
1395
  const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
1336
1396
  this.pendingThresholdListeners.delete(resolvedSlotId);
1337
1397
  const record = {
@@ -1387,7 +1447,485 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1387
1447
  this.unobserveSlot(slotId);
1388
1448
  this.slotViewabilityRecords.delete(slotId);
1389
1449
  this.slotRefreshIndices.delete(slotId);
1450
+ this.slotLastAuctionIds.delete(slotId);
1390
1451
  this.pendingThresholdListeners.delete(slotId);
1452
+ const prefix = `${escapeKeyPart(slotId)}:`;
1453
+ for (const key of Array.from(this.slotEmittedImpressionKeys)) {
1454
+ if (key === slotId || key.startsWith(prefix)) {
1455
+ this.slotEmittedImpressionKeys.delete(key);
1456
+ }
1457
+ }
1458
+ this.cachedWinningBids.delete(slotId);
1459
+ }
1460
+ hasImpressionEmitted(slotId, refreshIndex, auctionId, creativeId) {
1461
+ const rIndex = refreshIndex ?? this.slotRefreshIndices.get(slotId) ?? 0;
1462
+ const baseSlotKey = `${escapeKeyPart(slotId)}:${rIndex}`;
1463
+ if (creativeId) {
1464
+ if (auctionId) {
1465
+ return this.slotEmittedImpressionKeys.has(
1466
+ `${baseSlotKey}:auc:${escapeKeyPart(auctionId)}:${escapeKeyPart(creativeId)}`
1467
+ ) || this.slotEmittedImpressionKeys.has(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1468
+ }
1469
+ return this.slotEmittedImpressionKeys.has(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1470
+ }
1471
+ if (auctionId) {
1472
+ return this.slotEmittedImpressionKeys.has(`${baseSlotKey}:auc:${escapeKeyPart(auctionId)}`);
1473
+ }
1474
+ return this.slotEmittedImpressionKeys.has(baseSlotKey);
1475
+ }
1476
+ markImpressionEmitted(slotId, refreshIndex, auctionId, creativeId) {
1477
+ const baseSlotKey = `${escapeKeyPart(slotId)}:${refreshIndex}`;
1478
+ this.slotEmittedImpressionKeys.add(baseSlotKey);
1479
+ if (creativeId) {
1480
+ this.slotEmittedImpressionKeys.add(`${baseSlotKey}:${escapeKeyPart(creativeId)}`);
1481
+ }
1482
+ if (auctionId) {
1483
+ this.slotEmittedImpressionKeys.add(`${baseSlotKey}:auc:${escapeKeyPart(auctionId)}`);
1484
+ if (creativeId) {
1485
+ this.slotEmittedImpressionKeys.add(
1486
+ `${baseSlotKey}:auc:${escapeKeyPart(auctionId)}:${escapeKeyPart(creativeId)}`
1487
+ );
1488
+ }
1489
+ }
1490
+ }
1491
+ recordImpression(slotId, options) {
1492
+ if (!this.isEnabled) return false;
1493
+ const resolvedSlotId = slotId || options?.adUnitCode || "";
1494
+ const adUnitCode = options?.adUnitCode || resolvedSlotId;
1495
+ 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);
1496
+ const rawBid = options?.bid || cached?.rawBid || {};
1497
+ const auctionId = options?.auctionId || rawBid.auctionId || cached?.auctionId || "";
1498
+ const transactionId = options?.transactionId || rawBid.transactionId || cached?.transactionId || "";
1499
+ const creativeId = rawBid.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || "";
1500
+ const mediaType = options?.mediaType || rawBid.mediaType || cached?.bidTrace?.mediaType || "banner";
1501
+ const targetSlot = resolvedSlotId || adUnitCode;
1502
+ const rec = (resolvedSlotId ? this.slotViewabilityRecords.get(resolvedSlotId) : void 0) || (adUnitCode ? this.slotViewabilityRecords.get(adUnitCode) : void 0);
1503
+ const lastAuctionId = targetSlot ? this.slotLastAuctionIds.get(targetSlot) || (rec ? rec.auctionId : void 0) : void 0;
1504
+ const isDifferentAuction = Boolean(
1505
+ targetSlot && auctionId && lastAuctionId && lastAuctionId !== auctionId && options?.refreshIndex === void 0
1506
+ );
1507
+ const bidTrace = {
1508
+ bidder: rawBid.bidderCode || rawBid.bidder || cached?.bidTrace?.bidder || "",
1509
+ cpm: Number.isFinite(rawBid.originalCpm) ? rawBid.originalCpm : Number.isFinite(rawBid.cpm) ? rawBid.cpm : cached?.bidTrace?.cpm ?? 0,
1510
+ currency: rawBid.originalCurrency ?? rawBid.currency ?? cached?.bidTrace?.currency ?? "USD",
1511
+ ...parseBidDimensions(rawBid).width ? parseBidDimensions(rawBid) : cached?.bidTrace ? { width: cached.bidTrace.width, height: cached.bidTrace.height } : parseBidDimensions(rawBid),
1512
+ dealId: rawBid.dealId || cached?.bidTrace?.dealId || "",
1513
+ mediaType,
1514
+ latencyMs: Number.isFinite(rawBid.timeToRespond) ? rawBid.timeToRespond : cached?.bidTrace?.latencyMs ?? 0,
1515
+ advertiserDomain: rawBid.meta?.advertiserDomains?.[0] || cached?.bidTrace?.advertiserDomain || "",
1516
+ creativeId: rawBid.creativeId || cached?.bidTrace?.creativeId || ""
1517
+ };
1518
+ if (isDifferentAuction && targetSlot) {
1519
+ this.flushSlotTimeInView(targetSlot);
1520
+ const nextRefreshIndex = (this.slotRefreshIndices.get(targetSlot) ?? (rec ? rec.refreshIndex : 0)) + 1;
1521
+ this.slotRefreshIndices.set(targetSlot, nextRefreshIndex);
1522
+ this.slotLastAuctionIds.set(targetSlot, auctionId);
1523
+ if (rec) {
1524
+ rec.refreshIndex = nextRefreshIndex;
1525
+ rec.auctionId = auctionId;
1526
+ rec.transactionId = transactionId;
1527
+ rec.bidPayload = bidTrace;
1528
+ rec.viewableFired = false;
1529
+ rec.accumulatedTimeInViewMs = 0;
1530
+ }
1531
+ this.enqueue(TraceEventType.REFRESH, "refresh", {
1532
+ auctionId,
1533
+ transactionId,
1534
+ adUnitCode: targetSlot,
1535
+ bid: bidTrace,
1536
+ metadata: {
1537
+ ...options?.metadata,
1538
+ refresh_index: String(nextRefreshIndex)
1539
+ }
1540
+ });
1541
+ } else if (targetSlot) {
1542
+ if (auctionId) {
1543
+ this.slotLastAuctionIds.set(targetSlot, auctionId);
1544
+ }
1545
+ if (!this.slotRefreshIndices.has(targetSlot)) {
1546
+ this.slotRefreshIndices.set(targetSlot, 0);
1547
+ }
1548
+ }
1549
+ const currentRefreshIndex = options?.refreshIndex ?? (resolvedSlotId ? this.slotRefreshIndices.get(resolvedSlotId) ?? (adUnitCode ? this.slotRefreshIndices.get(adUnitCode) : void 0) ?? (rec ? rec.refreshIndex : 0) : 0);
1550
+ const alreadyEmitted = resolvedSlotId && this.hasImpressionEmitted(resolvedSlotId, currentRefreshIndex, auctionId, creativeId) || adUnitCode && adUnitCode !== resolvedSlotId && this.hasImpressionEmitted(adUnitCode, currentRefreshIndex, auctionId, creativeId);
1551
+ if (alreadyEmitted) {
1552
+ this.log(
1553
+ "DEBUG",
1554
+ `Impression already emitted for slot ${resolvedSlotId || adUnitCode} in cycle ${currentRefreshIndex}`
1555
+ );
1556
+ return false;
1557
+ }
1558
+ if (resolvedSlotId)
1559
+ this.markImpressionEmitted(resolvedSlotId, currentRefreshIndex, auctionId, creativeId);
1560
+ if (adUnitCode && adUnitCode !== resolvedSlotId)
1561
+ this.markImpressionEmitted(adUnitCode, currentRefreshIndex, auctionId, creativeId);
1562
+ this.enqueue(TraceEventType.IMPRESSION, "impression", {
1563
+ auctionId,
1564
+ transactionId,
1565
+ adUnitCode,
1566
+ bid: bidTrace,
1567
+ metadata: {
1568
+ ...options?.metadata,
1569
+ refresh_index: String(currentRefreshIndex)
1570
+ }
1571
+ });
1572
+ if (rec) {
1573
+ if (!rec.bidPayload && bidTrace) rec.bidPayload = bidTrace;
1574
+ if (!rec.auctionId && auctionId) rec.auctionId = auctionId;
1575
+ if (!rec.transactionId && transactionId) rec.transactionId = transactionId;
1576
+ }
1577
+ return true;
1578
+ }
1579
+ /**
1580
+ * Bridges Google IMA SDK AdsManager events to Bidkernel analytics.
1581
+ * Defers IMPRESSION emission until AdEvent.STARTED (or IMPRESSION),
1582
+ * tracks milestones (FIRST_QUARTILE, MIDPOINT, THIRD_QUARTILE, COMPLETE),
1583
+ * and handles AD_ERROR.
1584
+ */
1585
+ attachImaAdsManager(adsManager, options) {
1586
+ if (!adsManager || typeof adsManager.addEventListener !== "function") {
1587
+ this.log("WARN", "Invalid AdsManager passed to attachImaAdsManager");
1588
+ return () => {
1589
+ };
1590
+ }
1591
+ const slotId = options?.slotId || options?.adUnitCode || "video";
1592
+ const adUnitCode = options?.adUnitCode || slotId;
1593
+ const auctionId = options?.auctionId || "";
1594
+ const transactionId = options?.transactionId || "";
1595
+ const getWinningBid = () => {
1596
+ 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);
1597
+ };
1598
+ const onAdStartedOrImpression = (event) => {
1599
+ this.log("DEBUG", "IMA AdEvent.STARTED / IMPRESSION received", event);
1600
+ const ad = typeof event?.getAd === "function" ? event.getAd() : event?.ad;
1601
+ const adData = {};
1602
+ if (ad) {
1603
+ adData.creativeId = (typeof ad.getCreativeId === "function" ? ad.getCreativeId() : ad.creativeId) || "";
1604
+ adData.adId = (typeof ad.getAdId === "function" ? ad.getAdId() : ad.id) || "";
1605
+ adData.title = (typeof ad.getTitle === "function" ? ad.getTitle() : ad.title) || "";
1606
+ adData.duration = typeof ad.getDuration === "function" ? ad.getDuration() : ad.duration;
1607
+ adData.advertiserName = typeof ad.getAdvertiserName === "function" ? ad.getAdvertiserName() : "";
1608
+ }
1609
+ const cached = getWinningBid();
1610
+ const mergedBid = {
1611
+ ...cached?.rawBid || options?.bid,
1612
+ mediaType: "video",
1613
+ creativeId: adData.creativeId || cached?.bidTrace?.creativeId || options?.bid?.creativeId || ""
1614
+ };
1615
+ const emitted = this.recordImpression(slotId, {
1616
+ adUnitCode,
1617
+ auctionId: auctionId || cached?.auctionId,
1618
+ transactionId: transactionId || cached?.transactionId,
1619
+ mediaType: "video",
1620
+ bid: mergedBid,
1621
+ metadata: {
1622
+ ...options?.metadata,
1623
+ media_type: "video",
1624
+ ...adData.title ? { ad_title: adData.title } : {},
1625
+ ...Number.isFinite(adData.duration) ? { ad_duration_s: String(adData.duration) } : {}
1626
+ }
1627
+ });
1628
+ if (emitted && options?.onImpression) {
1629
+ try {
1630
+ options.onImpression(slotId, { ad, bid: mergedBid });
1631
+ } catch (e) {
1632
+ this.log("ERROR", "Error in onImpression callback", e);
1633
+ }
1634
+ }
1635
+ };
1636
+ const onMilestone = (milestone) => {
1637
+ this.log("DEBUG", `IMA AdEvent milestone: ${milestone}`);
1638
+ if (options?.onMilestone) {
1639
+ try {
1640
+ options.onMilestone(milestone, slotId);
1641
+ } catch (e) {
1642
+ this.log("ERROR", "Error in onMilestone callback", e);
1643
+ }
1644
+ }
1645
+ };
1646
+ const onAdClick = (event) => {
1647
+ this.log("DEBUG", "IMA AdEvent.CLICK received", event);
1648
+ const cached = getWinningBid();
1649
+ this.enqueue(TraceEventType.CLICK, "click", {
1650
+ auctionId: auctionId || cached?.auctionId,
1651
+ transactionId: transactionId || cached?.transactionId,
1652
+ adUnitCode,
1653
+ bid: cached?.bidTrace || options?.bid,
1654
+ metadata: {
1655
+ ...options?.metadata,
1656
+ media_type: "video"
1657
+ }
1658
+ });
1659
+ };
1660
+ const onAdError = (event) => {
1661
+ this.log("WARN", "IMA AdErrorEvent received", event);
1662
+ const err = typeof event?.getError === "function" ? event.getError() : event?.error || event;
1663
+ const msg = (err && typeof err.getMessage === "function" ? err.getMessage() : err?.message) || String(err || "IMA ad error");
1664
+ const code = (err && typeof err.getErrorCode === "function" ? err.getErrorCode() : err?.code) || "";
1665
+ const cached = getWinningBid();
1666
+ this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
1667
+ auctionId: auctionId || cached?.auctionId,
1668
+ transactionId: transactionId || cached?.transactionId,
1669
+ adUnitCode,
1670
+ bid: cached?.bidTrace || options?.bid,
1671
+ metadata: {
1672
+ ...options?.metadata,
1673
+ reason: "ima_ad_error",
1674
+ message: msg,
1675
+ ...code ? { ima_error_code: String(code) } : {}
1676
+ },
1677
+ error: typeof err === "object" ? err : new Error(msg)
1678
+ });
1679
+ if (options?.onError) {
1680
+ try {
1681
+ options.onError(err);
1682
+ } catch (e) {
1683
+ this.log("ERROR", "Error in onError callback", e);
1684
+ }
1685
+ }
1686
+ };
1687
+ const googleIma = typeof window !== "undefined" ? window.google?.ima : void 0;
1688
+ const adEventType = googleIma?.AdEvent?.Type || {};
1689
+ const adErrorEventType = googleIma?.AdErrorEvent?.Type || {};
1690
+ const listeners = [
1691
+ { type: adEventType.STARTED || "started", handler: onAdStartedOrImpression },
1692
+ { type: adEventType.IMPRESSION || "impression", handler: onAdStartedOrImpression },
1693
+ {
1694
+ type: adEventType.FIRST_QUARTILE || "firstQuartile",
1695
+ handler: () => onMilestone("firstQuartile")
1696
+ },
1697
+ { type: adEventType.MIDPOINT || "midpoint", handler: () => onMilestone("midpoint") },
1698
+ {
1699
+ type: adEventType.THIRD_QUARTILE || "thirdQuartile",
1700
+ handler: () => onMilestone("thirdQuartile")
1701
+ },
1702
+ { type: adEventType.COMPLETE || "complete", handler: () => onMilestone("complete") },
1703
+ { type: adEventType.CLICK || "click", handler: onAdClick },
1704
+ { type: adErrorEventType.AD_ERROR || "adError", handler: onAdError }
1705
+ ];
1706
+ for (const { type, handler } of listeners) {
1707
+ try {
1708
+ adsManager.addEventListener(type, handler);
1709
+ } catch {
1710
+ }
1711
+ }
1712
+ const cleanup = () => {
1713
+ for (const { type, handler } of listeners) {
1714
+ try {
1715
+ adsManager.removeEventListener(type, handler);
1716
+ } catch {
1717
+ }
1718
+ }
1719
+ this.videoDetachCleanups.delete(cleanup);
1720
+ };
1721
+ this.videoDetachCleanups.add(cleanup);
1722
+ return cleanup;
1723
+ }
1724
+ /**
1725
+ * Attaches render hooks and viewability tracking to a video player.
1726
+ * Supports HTML5 <video> elements, container elements, or Google IMA AdsManager.
1727
+ */
1728
+ attachVideoPlayer(target, options) {
1729
+ if (!target) {
1730
+ this.log("WARN", "attachVideoPlayer called with null/undefined target");
1731
+ return () => {
1732
+ };
1733
+ }
1734
+ let el = null;
1735
+ if (typeof target === "string") {
1736
+ if (typeof document !== "undefined") {
1737
+ try {
1738
+ el = document.querySelector(target) || document.getElementById(target);
1739
+ } catch {
1740
+ el = document.getElementById(target);
1741
+ }
1742
+ }
1743
+ } else if (typeof HTMLElement !== "undefined" && target instanceof HTMLElement) {
1744
+ el = target;
1745
+ } else if (target && typeof target === "object" && target.nodeType === 1) {
1746
+ el = target;
1747
+ } else if (
1748
+ // Check if target is an IMA AdsManager
1749
+ typeof target.addEventListener === "function" && (typeof target.init === "function" || typeof target.start === "function" || typeof target.getCuePoints === "function" || typeof target.getVolume === "function" || target.__imaAdsManager)
1750
+ ) {
1751
+ return this.attachImaAdsManager(target, options);
1752
+ } else if (target && typeof target.addEventListener === "function") {
1753
+ el = target;
1754
+ }
1755
+ const slotId = options?.slotId || options?.adUnitCode || (el ? el.id : "") || "video";
1756
+ const adUnitCode = options?.adUnitCode || slotId;
1757
+ const auctionId = options?.auctionId || "";
1758
+ const transactionId = options?.transactionId || "";
1759
+ const getWinningBid = () => {
1760
+ 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);
1761
+ };
1762
+ let videoEl = null;
1763
+ if (el) {
1764
+ if (el.tagName && el.tagName.toLowerCase() === "video") {
1765
+ videoEl = el;
1766
+ } else {
1767
+ videoEl = el.querySelector("video");
1768
+ }
1769
+ }
1770
+ const quartilesFired = {
1771
+ q1: false,
1772
+ q2: false,
1773
+ q3: false,
1774
+ q4: false
1775
+ };
1776
+ const triggerPlaybackImpression = (activeVideo) => {
1777
+ const vEl = activeVideo || videoEl || (el?.querySelector ? el.querySelector("video") : null);
1778
+ const cached = getWinningBid();
1779
+ const mergedBid = {
1780
+ ...cached?.rawBid || options?.bid,
1781
+ mediaType: "video"
1782
+ };
1783
+ const emitted = this.recordImpression(slotId, {
1784
+ adUnitCode,
1785
+ auctionId: auctionId || cached?.auctionId,
1786
+ transactionId: transactionId || cached?.transactionId,
1787
+ mediaType: "video",
1788
+ bid: mergedBid,
1789
+ metadata: {
1790
+ ...options?.metadata,
1791
+ media_type: "video",
1792
+ ...vEl && Number.isFinite(vEl.duration) ? { video_duration_s: String(Math.round(vEl.duration)) } : {}
1793
+ }
1794
+ });
1795
+ if (emitted && options?.onImpression) {
1796
+ try {
1797
+ options.onImpression(slotId, { element: vEl || el, bid: mergedBid });
1798
+ } catch (e) {
1799
+ this.log("ERROR", "Error in onImpression callback", e);
1800
+ }
1801
+ }
1802
+ };
1803
+ const handlePlaying = (e) => {
1804
+ this.log("DEBUG", "HTML5 video playing event received");
1805
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
1806
+ triggerPlaybackImpression(targetVideo);
1807
+ };
1808
+ const handleTimeUpdate = (e) => {
1809
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
1810
+ if (targetVideo && targetVideo.currentTime > 0) {
1811
+ triggerPlaybackImpression(targetVideo);
1812
+ if (Number.isFinite(targetVideo.duration) && targetVideo.duration > 0) {
1813
+ const progress = targetVideo.currentTime / targetVideo.duration;
1814
+ if (progress >= 0.25 && !quartilesFired.q1) {
1815
+ quartilesFired.q1 = true;
1816
+ options?.onMilestone?.("firstQuartile", slotId);
1817
+ }
1818
+ if (progress >= 0.5 && !quartilesFired.q2) {
1819
+ quartilesFired.q2 = true;
1820
+ options?.onMilestone?.("midpoint", slotId);
1821
+ }
1822
+ if (progress >= 0.75 && !quartilesFired.q3) {
1823
+ quartilesFired.q3 = true;
1824
+ options?.onMilestone?.("thirdQuartile", slotId);
1825
+ }
1826
+ }
1827
+ }
1828
+ };
1829
+ const handleEnded = () => {
1830
+ if (!quartilesFired.q4) {
1831
+ quartilesFired.q4 = true;
1832
+ options?.onMilestone?.("complete", slotId);
1833
+ }
1834
+ };
1835
+ const handleError = (e) => {
1836
+ const targetVideo = e?.target instanceof HTMLVideoElement ? e.target : videoEl;
1837
+ const err = targetVideo?.error || videoEl?.error;
1838
+ const cached = getWinningBid();
1839
+ const msg = err ? `HTML5 video error code ${err.code}: ${err.message}` : "Video playback error";
1840
+ this.log("WARN", msg);
1841
+ this.enqueue(TraceEventType.AD_RENDER_FAILED, "adRenderFailed", {
1842
+ auctionId: auctionId || cached?.auctionId,
1843
+ transactionId: transactionId || cached?.transactionId,
1844
+ adUnitCode,
1845
+ bid: cached?.bidTrace || options?.bid,
1846
+ metadata: {
1847
+ ...options?.metadata,
1848
+ reason: "html5_video_error",
1849
+ message: msg,
1850
+ ...err?.code ? { video_error_code: String(err.code) } : {}
1851
+ },
1852
+ error: err ? new Error(msg) : void 0
1853
+ });
1854
+ if (options?.onError) {
1855
+ try {
1856
+ options.onError(err);
1857
+ } catch (e2) {
1858
+ this.log("ERROR", "Error in onError callback", e2);
1859
+ }
1860
+ }
1861
+ };
1862
+ const attachListenersToVideo = (targetVideo) => {
1863
+ targetVideo.addEventListener("playing", handlePlaying);
1864
+ targetVideo.addEventListener("play", handlePlaying);
1865
+ targetVideo.addEventListener("timeupdate", handleTimeUpdate);
1866
+ targetVideo.addEventListener("ended", handleEnded);
1867
+ targetVideo.addEventListener("error", handleError);
1868
+ };
1869
+ const removeListenersFromVideo = (targetVideo) => {
1870
+ targetVideo.removeEventListener("playing", handlePlaying);
1871
+ targetVideo.removeEventListener("play", handlePlaying);
1872
+ targetVideo.removeEventListener("timeupdate", handleTimeUpdate);
1873
+ targetVideo.removeEventListener("ended", handleEnded);
1874
+ targetVideo.removeEventListener("error", handleError);
1875
+ };
1876
+ let mutationObserver = null;
1877
+ if (el && el !== videoEl) {
1878
+ el.addEventListener("playing", handlePlaying, true);
1879
+ el.addEventListener("play", handlePlaying, true);
1880
+ el.addEventListener("timeupdate", handleTimeUpdate, true);
1881
+ el.addEventListener("ended", handleEnded, true);
1882
+ el.addEventListener("error", handleError, true);
1883
+ }
1884
+ if (videoEl) {
1885
+ attachListenersToVideo(videoEl);
1886
+ }
1887
+ if (el && el.tagName?.toLowerCase() !== "video" && typeof MutationObserver !== "undefined") {
1888
+ mutationObserver = new MutationObserver(() => {
1889
+ const found = el.querySelector("video");
1890
+ if (found && found !== videoEl) {
1891
+ if (videoEl) removeListenersFromVideo(videoEl);
1892
+ videoEl = found;
1893
+ if (videoEl) attachListenersToVideo(videoEl);
1894
+ }
1895
+ });
1896
+ mutationObserver.observe(el, { childList: true, subtree: true });
1897
+ }
1898
+ const cachedInitial = getWinningBid();
1899
+ if (options?.trackViewability !== false && el) {
1900
+ this.observeSlot(el, slotId, {
1901
+ adUnitCode,
1902
+ auctionId: auctionId || cachedInitial?.auctionId,
1903
+ transactionId: transactionId || cachedInitial?.transactionId,
1904
+ mediaType: "video",
1905
+ bid: cachedInitial?.bidTrace || options?.bid,
1906
+ metadata: options?.metadata,
1907
+ emitRefreshEvent: false
1908
+ });
1909
+ }
1910
+ const cleanup = () => {
1911
+ if (mutationObserver) {
1912
+ mutationObserver.disconnect();
1913
+ mutationObserver = null;
1914
+ }
1915
+ if (el && el !== videoEl) {
1916
+ el.removeEventListener("playing", handlePlaying, true);
1917
+ el.removeEventListener("play", handlePlaying, true);
1918
+ el.removeEventListener("timeupdate", handleTimeUpdate, true);
1919
+ el.removeEventListener("ended", handleEnded, true);
1920
+ el.removeEventListener("error", handleError, true);
1921
+ }
1922
+ if (videoEl) {
1923
+ removeListenersFromVideo(videoEl);
1924
+ }
1925
+ this.videoDetachCleanups.delete(cleanup);
1926
+ };
1927
+ this.videoDetachCleanups.add(cleanup);
1928
+ return cleanup;
1391
1929
  }
1392
1930
  onTimeInViewThreshold(slotId, thresholdMs, callback) {
1393
1931
  const listener = {
@@ -1600,21 +2138,45 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1600
2138
  handleBidWon(data) {
1601
2139
  if (this.isDuplicate("bidWon", data)) return;
1602
2140
  this.log("DEBUG", "bidWon", data);
1603
- this.enqueue(TraceEventType.BID_WIN, "bidWon", {
1604
- auctionId: data.auctionId || "",
1605
- transactionId: data.transactionId || "",
1606
- adUnitCode: data.adUnitCode || "",
1607
- bid: {
1608
- bidder: data.bidderCode || data.bidder || "",
1609
- cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1610
- currency: data.originalCurrency ?? data.currency ?? "USD",
1611
- ...parseBidDimensions(data),
1612
- dealId: data.dealId || "",
1613
- mediaType: data.mediaType || "banner",
1614
- latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
1615
- advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
1616
- creativeId: data.creativeId || ""
2141
+ const auctionId = data.auctionId || "";
2142
+ const transactionId = data.transactionId || "";
2143
+ const adUnitCode = data.adUnitCode || data.adId || "";
2144
+ const bidTrace = {
2145
+ bidder: data.bidderCode || data.bidder || "",
2146
+ cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
2147
+ currency: data.originalCurrency ?? data.currency ?? "USD",
2148
+ ...parseBidDimensions(data),
2149
+ dealId: data.dealId || "",
2150
+ mediaType: data.mediaType || "banner",
2151
+ latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
2152
+ advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
2153
+ creativeId: data.creativeId || ""
2154
+ };
2155
+ if (adUnitCode) {
2156
+ const cachedEntry = {
2157
+ bidTrace,
2158
+ auctionId,
2159
+ transactionId,
2160
+ adUnitCode,
2161
+ rawBid: data,
2162
+ timestamp: Date.now()
2163
+ };
2164
+ this.cachedWinningBids.set(adUnitCode, cachedEntry);
2165
+ if (data.adId && data.adId !== adUnitCode) {
2166
+ this.cachedWinningBids.set(data.adId, cachedEntry);
2167
+ }
2168
+ if (auctionId) {
2169
+ this.cachedWinningBids.set(`${auctionId}:${adUnitCode}`, cachedEntry);
2170
+ if (data.adId && data.adId !== adUnitCode) {
2171
+ this.cachedWinningBids.set(`${auctionId}:${data.adId}`, cachedEntry);
2172
+ }
1617
2173
  }
2174
+ }
2175
+ this.enqueue(TraceEventType.BID_WIN, "bidWon", {
2176
+ auctionId,
2177
+ transactionId,
2178
+ adUnitCode,
2179
+ bid: bidTrace
1618
2180
  });
1619
2181
  }
1620
2182
  handleNoBid(data) {
@@ -1653,44 +2215,81 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1653
2215
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1654
2216
  this.log("DEBUG", "adRenderSucceeded", data);
1655
2217
  const bid = data.bid || data || {};
1656
- const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
2218
+ const adUnitCode = data.adUnitCode || bid.adUnitCode || data.adId || bid.adId || "";
1657
2219
  const auctionId = bid.auctionId || data.auctionId || "";
1658
2220
  const transactionId = bid.transactionId || data.transactionId || "";
1659
2221
  const mediaType = bid.mediaType || "banner";
2222
+ const slotKey = data.adUnitCode || bid.adUnitCode || "";
2223
+ const altKey = data.adId || bid.adId || "";
2224
+ 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);
1660
2225
  const bidTrace = {
1661
- bidder: bid.bidderCode || bid.bidder || "",
1662
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1663
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1664
- ...parseBidDimensions(bid),
1665
- dealId: bid.dealId || "",
1666
- mediaType,
1667
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1668
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1669
- creativeId: bid.creativeId || ""
2226
+ bidder: bid.bidderCode || bid.bidder || cached?.bidTrace?.bidder || "",
2227
+ cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : cached?.bidTrace?.cpm ?? 0,
2228
+ currency: bid.originalCurrency ?? bid.currency ?? cached?.bidTrace?.currency ?? "USD",
2229
+ ...parseBidDimensions(bid).width ? parseBidDimensions(bid) : cached?.bidTrace ? { width: cached.bidTrace.width, height: cached.bidTrace.height } : parseBidDimensions(bid),
2230
+ dealId: bid.dealId || cached?.bidTrace?.dealId || "",
2231
+ mediaType: mediaType || cached?.bidTrace?.mediaType || "banner",
2232
+ latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : cached?.bidTrace?.latencyMs ?? 0,
2233
+ advertiserDomain: bid.meta?.advertiserDomains?.[0] || cached?.bidTrace?.advertiserDomain || "",
2234
+ creativeId: bid.creativeId || cached?.bidTrace?.creativeId || ""
1670
2235
  };
1671
- const isRefresh = adUnitCode && (this.slotViewabilityRecords.has(adUnitCode) || this.slotRefreshIndices.has(adUnitCode) && this.slotRefreshIndices.get(adUnitCode) > 0);
2236
+ const resolvedAuctionId = auctionId || cached?.auctionId || "";
2237
+ const resolvedTransactionId = transactionId || cached?.transactionId || "";
2238
+ const record = adUnitCode ? this.slotViewabilityRecords.get(adUnitCode) : void 0;
2239
+ const isDifferentAuction = record && resolvedAuctionId && record.auctionId && record.auctionId !== resolvedAuctionId;
2240
+ const isRefresh = Boolean(adUnitCode && record && isDifferentAuction);
1672
2241
  if (isRefresh) {
1673
2242
  this.flushSlotTimeInView(adUnitCode);
1674
- const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
2243
+ const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0)) + 1;
1675
2244
  this.slotRefreshIndices.set(adUnitCode, nextRefreshIndex);
2245
+ this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
2246
+ if (record) {
2247
+ record.refreshIndex = nextRefreshIndex;
2248
+ record.auctionId = resolvedAuctionId;
2249
+ record.transactionId = resolvedTransactionId;
2250
+ record.bidPayload = bidTrace;
2251
+ record.viewableFired = false;
2252
+ record.accumulatedTimeInViewMs = 0;
2253
+ }
1676
2254
  this.enqueue(TraceEventType.REFRESH, "refresh", {
1677
- auctionId,
1678
- transactionId,
2255
+ auctionId: resolvedAuctionId,
2256
+ transactionId: resolvedTransactionId,
1679
2257
  adUnitCode,
1680
2258
  bid: bidTrace,
1681
2259
  metadata: { refresh_index: String(nextRefreshIndex) }
1682
2260
  });
1683
- } else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
1684
- this.slotRefreshIndices.set(adUnitCode, 0);
2261
+ } else if (adUnitCode) {
2262
+ if (resolvedAuctionId) {
2263
+ this.slotLastAuctionIds.set(adUnitCode, resolvedAuctionId);
2264
+ }
2265
+ if (!this.slotRefreshIndices.has(adUnitCode)) {
2266
+ this.slotRefreshIndices.set(adUnitCode, 0);
2267
+ }
1685
2268
  }
1686
- const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
1687
- this.enqueue(TraceEventType.IMPRESSION, "impression", {
1688
- auctionId,
1689
- transactionId,
2269
+ const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0) : 0;
2270
+ const alreadyEmitted = adUnitCode ? this.hasImpressionEmitted(
1690
2271
  adUnitCode,
1691
- bid: bidTrace,
1692
- metadata: { refresh_index: String(currentRefreshIndex) }
1693
- });
2272
+ currentRefreshIndex,
2273
+ resolvedAuctionId,
2274
+ bidTrace.creativeId
2275
+ ) : false;
2276
+ if (!alreadyEmitted) {
2277
+ if (adUnitCode) {
2278
+ this.markImpressionEmitted(
2279
+ adUnitCode,
2280
+ currentRefreshIndex,
2281
+ resolvedAuctionId,
2282
+ bidTrace.creativeId
2283
+ );
2284
+ }
2285
+ this.enqueue(TraceEventType.IMPRESSION, "impression", {
2286
+ auctionId: resolvedAuctionId,
2287
+ transactionId: resolvedTransactionId,
2288
+ adUnitCode,
2289
+ bid: bidTrace,
2290
+ metadata: { refresh_index: String(currentRefreshIndex) }
2291
+ });
2292
+ }
1694
2293
  if (this.config.viewabilityEnabled && typeof document !== "undefined") {
1695
2294
  let el = null;
1696
2295
  const targetId = adUnitCode || data.adId || bid.adId;
@@ -1718,8 +2317,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1718
2317
  if (el) {
1719
2318
  this.observeSlot(el, adUnitCode || targetId, {
1720
2319
  adUnitCode,
1721
- auctionId,
1722
- transactionId,
2320
+ auctionId: resolvedAuctionId,
2321
+ transactionId: resolvedTransactionId,
1723
2322
  mediaType,
1724
2323
  bid: bidTrace,
1725
2324
  refreshIndex: currentRefreshIndex,
@@ -1784,6 +2383,12 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1784
2383
  }
1785
2384
  if (this.queue.length >= BATCH_SIZE) {
1786
2385
  this.flush();
2386
+ } else if (IMMEDIATE_FLUSH_TYPES.has(type) && !this.immediateFlushScheduled) {
2387
+ this.immediateFlushScheduled = true;
2388
+ setTimeout(() => {
2389
+ this.immediateFlushScheduled = false;
2390
+ this.flush();
2391
+ }, 0);
1787
2392
  }
1788
2393
  }
1789
2394
  shouldSample(type, level) {
@@ -1840,10 +2445,11 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1840
2445
  events
1841
2446
  };
1842
2447
  }
1843
- sendFetch(payload, useKeepalive = false) {
2448
+ sendFetch(payload, useKeepalive = false, onDelivered) {
1844
2449
  const fetchOpts = {
1845
2450
  method: "POST",
1846
- headers: { "Content-Type": "application/x-protobuf" },
2451
+ // Safelisted content type: no CORS preflight (see SAFE_CONTENT_TYPE).
2452
+ headers: { "Content-Type": SAFE_CONTENT_TYPE },
1847
2453
  body: payload.encoded
1848
2454
  };
1849
2455
  if (useKeepalive) {
@@ -1852,7 +2458,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1852
2458
  fetch(payload.url, fetchOpts).then((res) => {
1853
2459
  if (!res.ok) {
1854
2460
  this.log("WARN", `Failed to send batch: HTTP ${res.status}`);
1855
- if (res.status >= 400 && res.status < 500) {
2461
+ if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 408) {
1856
2462
  this.log("WARN", `Dropping batch due to non-retryable client error HTTP ${res.status}`);
1857
2463
  return;
1858
2464
  }
@@ -1860,14 +2466,16 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1860
2466
  } else {
1861
2467
  this.consecutiveSendFailures = 0;
1862
2468
  this.nextSendAllowedAt = 0;
2469
+ if (onDelivered) onDelivered();
1863
2470
  }
1864
2471
  }).catch((err) => {
1865
2472
  this.log("ERROR", "Failed to send batch", err);
1866
2473
  this.handleSendFailure(payload.events);
1867
2474
  });
1868
- if (typeof process !== "undefined" && typeof process._tickCallback === "function") {
2475
+ const proc = typeof globalThis !== "undefined" ? globalThis.process : void 0;
2476
+ if (proc && typeof proc._tickCallback === "function") {
1869
2477
  try {
1870
- process._tickCallback();
2478
+ proc._tickCallback();
1871
2479
  } catch {
1872
2480
  }
1873
2481
  }
@@ -1903,19 +2511,99 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1903
2511
  let sent = false;
1904
2512
  if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
1905
2513
  try {
1906
- const blob = new Blob([payload.encoded], {
1907
- type: "application/x-protobuf"
1908
- });
2514
+ const blob = new Blob([payload.encoded], { type: SAFE_CONTENT_TYPE });
1909
2515
  sent = navigator.sendBeacon(payload.url, blob);
1910
2516
  } catch {
1911
2517
  sent = false;
1912
2518
  }
1913
2519
  }
1914
2520
  if (!sent) {
1915
- this.sendFetch(payload, true);
2521
+ const persistKey = this.persistBatch(payload.encoded);
2522
+ this.sendFetch(payload, true, () => this.removePersistedBatch(persistKey));
1916
2523
  }
1917
2524
  payload = this.drainBatch();
1918
2525
  }
2526
+ this.schedulePersistedCleanup();
2527
+ }
2528
+ // --- Exit-batch persistence -----------------------------------------------
2529
+ persistBatch(encoded) {
2530
+ try {
2531
+ if (typeof localStorage === "undefined") return null;
2532
+ let pendingCount = 0;
2533
+ for (let i = 0; i < localStorage.length; i++) {
2534
+ const k = localStorage.key(i);
2535
+ if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) pendingCount++;
2536
+ }
2537
+ if (pendingCount >= MAX_PENDING_BATCHES) return null;
2538
+ const key = `${PENDING_BATCH_KEY_PREFIX}${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
2539
+ localStorage.setItem(key, bytesToBase64(encoded));
2540
+ this.persistedBatchKeys.push(key);
2541
+ return key;
2542
+ } catch {
2543
+ return null;
2544
+ }
2545
+ }
2546
+ removePersistedBatch(key) {
2547
+ if (!key) return;
2548
+ try {
2549
+ if (typeof localStorage !== "undefined") localStorage.removeItem(key);
2550
+ } catch {
2551
+ }
2552
+ const idx = this.persistedBatchKeys.indexOf(key);
2553
+ if (idx !== -1) this.persistedBatchKeys.splice(idx, 1);
2554
+ }
2555
+ // A page that is still running PENDING_BATCH_CLEANUP_DELAY_MS after an exit
2556
+ // flush was never torn down, so its beacons have long since gone out; drop
2557
+ // the persisted copies rather than letting a later pageview resend them.
2558
+ schedulePersistedCleanup() {
2559
+ if (this.persistedBatchKeys.length === 0) return;
2560
+ if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
2561
+ this.persistedCleanupTimer = setTimeout(() => {
2562
+ this.persistedCleanupTimer = null;
2563
+ for (const key of Array.from(this.persistedBatchKeys)) {
2564
+ this.removePersistedBatch(key);
2565
+ }
2566
+ }, PENDING_BATCH_CLEANUP_DELAY_MS);
2567
+ }
2568
+ // Resend batches a previous pageview persisted at exit but could not
2569
+ // confirm. Keys are claimed (removed) before sending so concurrent tabs
2570
+ // don't double-send; the batch bytes carry their original pageview, session,
2571
+ // and event timestamps, so late rows attribute correctly.
2572
+ resendPersistedBatches() {
2573
+ try {
2574
+ if (typeof localStorage === "undefined" || !this.config.endpoint) return;
2575
+ const keys = [];
2576
+ for (let i = 0; i < localStorage.length; i++) {
2577
+ const k = localStorage.key(i);
2578
+ if (k && k.startsWith(PENDING_BATCH_KEY_PREFIX)) keys.push(k);
2579
+ }
2580
+ for (const key of keys) {
2581
+ let value = null;
2582
+ try {
2583
+ value = localStorage.getItem(key);
2584
+ localStorage.removeItem(key);
2585
+ } catch {
2586
+ continue;
2587
+ }
2588
+ if (!value) continue;
2589
+ const ts = parseInt(key.slice(PENDING_BATCH_KEY_PREFIX.length), 10);
2590
+ if (!Number.isFinite(ts) || Date.now() - ts > PENDING_BATCH_MAX_AGE_MS) continue;
2591
+ let encoded;
2592
+ try {
2593
+ encoded = base64ToBytes(value);
2594
+ } catch {
2595
+ continue;
2596
+ }
2597
+ if (encoded.byteLength === 0) continue;
2598
+ this.log("DEBUG", `Resending persisted exit batch (${encoded.byteLength} bytes)`);
2599
+ this.sendFetch({
2600
+ url: `${this.config.endpoint}/${this.config.propertyId}`,
2601
+ encoded,
2602
+ events: []
2603
+ });
2604
+ }
2605
+ } catch {
2606
+ }
1919
2607
  }
1920
2608
  log(level, msg, ...args) {
1921
2609
  const levels = { DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 };
@@ -2042,7 +2730,28 @@ function getbidkernel(alias = "bidkernel") {
2042
2730
  }
2043
2731
  const win = window;
2044
2732
  win[alias] = win[alias] || { q: [] };
2045
- return win[alias];
2733
+ const sdk = win[alias];
2734
+ if (!sdk.attachVideoPlayer) {
2735
+ sdk.attachVideoPlayer = (target, options) => {
2736
+ const inst = win._bidkernelPrebidAnalytics;
2737
+ return inst?.attachVideoPlayer ? inst.attachVideoPlayer(target, options) : () => {
2738
+ };
2739
+ };
2740
+ }
2741
+ if (!sdk.attachImaAdsManager) {
2742
+ sdk.attachImaAdsManager = (adsManager, options) => {
2743
+ const inst = win._bidkernelPrebidAnalytics;
2744
+ return inst?.attachImaAdsManager ? inst.attachImaAdsManager(adsManager, options) : () => {
2745
+ };
2746
+ };
2747
+ }
2748
+ if (!sdk.recordImpression) {
2749
+ sdk.recordImpression = (slotId, options) => {
2750
+ const inst = win._bidkernelPrebidAnalytics;
2751
+ return inst?.recordImpression ? inst.recordImpression(slotId, options) : false;
2752
+ };
2753
+ }
2754
+ return sdk;
2046
2755
  }
2047
2756
  // Annotate the CommonJS export names for ESM import in node:
2048
2757
  0 && (module.exports = {