@bidkernel/analytics 0.6.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
@@ -889,7 +889,14 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
889
889
  slotViewabilityRecords = /* @__PURE__ */ new Map();
890
890
  elementToSlotId = /* @__PURE__ */ new Map();
891
891
  slotRefreshIndices = /* @__PURE__ */ new Map();
892
+ slotLastAuctionIds = /* @__PURE__ */ new Map();
892
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();
893
900
  constructor(config) {
894
901
  this.config = {
895
902
  endpoint: config.endpoint || "",
@@ -1035,6 +1042,16 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1035
1042
  }
1036
1043
  this.slotViewabilityRecords.clear();
1037
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();
1038
1055
  if (this.flushTimer) {
1039
1056
  clearInterval(this.flushTimer);
1040
1057
  this.flushTimer = null;
@@ -1357,6 +1374,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1357
1374
  if (options?.bid !== void 0) existing.bidPayload = options.bid;
1358
1375
  if (options?.metadata !== void 0) existing.metadata = options.metadata;
1359
1376
  }
1377
+ if (options?.auctionId) {
1378
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1379
+ }
1360
1380
  if (el) {
1361
1381
  if (existing.element && existing.element !== el && this.intersectionObserver) {
1362
1382
  this.intersectionObserver.unobserve(existing.element);
@@ -1369,6 +1389,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1369
1389
  } else {
1370
1390
  const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
1371
1391
  this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
1392
+ if (options?.auctionId) {
1393
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1394
+ }
1372
1395
  const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
1373
1396
  this.pendingThresholdListeners.delete(resolvedSlotId);
1374
1397
  const record = {
@@ -1424,7 +1447,485 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1424
1447
  this.unobserveSlot(slotId);
1425
1448
  this.slotViewabilityRecords.delete(slotId);
1426
1449
  this.slotRefreshIndices.delete(slotId);
1450
+ this.slotLastAuctionIds.delete(slotId);
1427
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;
1428
1929
  }
1429
1930
  onTimeInViewThreshold(slotId, thresholdMs, callback) {
1430
1931
  const listener = {
@@ -1637,21 +2138,45 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1637
2138
  handleBidWon(data) {
1638
2139
  if (this.isDuplicate("bidWon", data)) return;
1639
2140
  this.log("DEBUG", "bidWon", data);
1640
- this.enqueue(TraceEventType.BID_WIN, "bidWon", {
1641
- auctionId: data.auctionId || "",
1642
- transactionId: data.transactionId || "",
1643
- adUnitCode: data.adUnitCode || "",
1644
- bid: {
1645
- bidder: data.bidderCode || data.bidder || "",
1646
- cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1647
- currency: data.originalCurrency ?? data.currency ?? "USD",
1648
- ...parseBidDimensions(data),
1649
- dealId: data.dealId || "",
1650
- mediaType: data.mediaType || "banner",
1651
- latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
1652
- advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
1653
- 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);
1654
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
+ }
2173
+ }
2174
+ }
2175
+ this.enqueue(TraceEventType.BID_WIN, "bidWon", {
2176
+ auctionId,
2177
+ transactionId,
2178
+ adUnitCode,
2179
+ bid: bidTrace
1655
2180
  });
1656
2181
  }
1657
2182
  handleNoBid(data) {
@@ -1690,44 +2215,81 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1690
2215
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1691
2216
  this.log("DEBUG", "adRenderSucceeded", data);
1692
2217
  const bid = data.bid || data || {};
1693
- const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
2218
+ const adUnitCode = data.adUnitCode || bid.adUnitCode || data.adId || bid.adId || "";
1694
2219
  const auctionId = bid.auctionId || data.auctionId || "";
1695
2220
  const transactionId = bid.transactionId || data.transactionId || "";
1696
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);
1697
2225
  const bidTrace = {
1698
- bidder: bid.bidderCode || bid.bidder || "",
1699
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1700
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1701
- ...parseBidDimensions(bid),
1702
- dealId: bid.dealId || "",
1703
- mediaType,
1704
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1705
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1706
- 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 || ""
1707
2235
  };
1708
- 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);
1709
2241
  if (isRefresh) {
1710
2242
  this.flushSlotTimeInView(adUnitCode);
1711
- const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
2243
+ const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0)) + 1;
1712
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
+ }
1713
2254
  this.enqueue(TraceEventType.REFRESH, "refresh", {
1714
- auctionId,
1715
- transactionId,
2255
+ auctionId: resolvedAuctionId,
2256
+ transactionId: resolvedTransactionId,
1716
2257
  adUnitCode,
1717
2258
  bid: bidTrace,
1718
2259
  metadata: { refresh_index: String(nextRefreshIndex) }
1719
2260
  });
1720
- } else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
1721
- 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
+ }
1722
2268
  }
1723
- const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
1724
- this.enqueue(TraceEventType.IMPRESSION, "impression", {
1725
- auctionId,
1726
- transactionId,
2269
+ const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0) : 0;
2270
+ const alreadyEmitted = adUnitCode ? this.hasImpressionEmitted(
1727
2271
  adUnitCode,
1728
- bid: bidTrace,
1729
- metadata: { refresh_index: String(currentRefreshIndex) }
1730
- });
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
+ }
1731
2293
  if (this.config.viewabilityEnabled && typeof document !== "undefined") {
1732
2294
  let el = null;
1733
2295
  const targetId = adUnitCode || data.adId || bid.adId;
@@ -1755,8 +2317,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1755
2317
  if (el) {
1756
2318
  this.observeSlot(el, adUnitCode || targetId, {
1757
2319
  adUnitCode,
1758
- auctionId,
1759
- transactionId,
2320
+ auctionId: resolvedAuctionId,
2321
+ transactionId: resolvedTransactionId,
1760
2322
  mediaType,
1761
2323
  bid: bidTrace,
1762
2324
  refreshIndex: currentRefreshIndex,
@@ -1998,7 +2560,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1998
2560
  if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
1999
2561
  this.persistedCleanupTimer = setTimeout(() => {
2000
2562
  this.persistedCleanupTimer = null;
2001
- for (const key of [...this.persistedBatchKeys]) {
2563
+ for (const key of Array.from(this.persistedBatchKeys)) {
2002
2564
  this.removePersistedBatch(key);
2003
2565
  }
2004
2566
  }, PENDING_BATCH_CLEANUP_DELAY_MS);
@@ -2168,7 +2730,28 @@ function getbidkernel(alias = "bidkernel") {
2168
2730
  }
2169
2731
  const win = window;
2170
2732
  win[alias] = win[alias] || { q: [] };
2171
- 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;
2172
2755
  }
2173
2756
  // Annotate the CommonJS export names for ESM import in node:
2174
2757
  0 && (module.exports = {