@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.mjs CHANGED
@@ -859,7 +859,14 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
859
859
  slotViewabilityRecords = /* @__PURE__ */ new Map();
860
860
  elementToSlotId = /* @__PURE__ */ new Map();
861
861
  slotRefreshIndices = /* @__PURE__ */ new Map();
862
+ slotLastAuctionIds = /* @__PURE__ */ new Map();
862
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();
863
870
  constructor(config) {
864
871
  this.config = {
865
872
  endpoint: config.endpoint || "",
@@ -1005,6 +1012,16 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1005
1012
  }
1006
1013
  this.slotViewabilityRecords.clear();
1007
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();
1008
1025
  if (this.flushTimer) {
1009
1026
  clearInterval(this.flushTimer);
1010
1027
  this.flushTimer = null;
@@ -1327,6 +1344,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1327
1344
  if (options?.bid !== void 0) existing.bidPayload = options.bid;
1328
1345
  if (options?.metadata !== void 0) existing.metadata = options.metadata;
1329
1346
  }
1347
+ if (options?.auctionId) {
1348
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1349
+ }
1330
1350
  if (el) {
1331
1351
  if (existing.element && existing.element !== el && this.intersectionObserver) {
1332
1352
  this.intersectionObserver.unobserve(existing.element);
@@ -1339,6 +1359,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1339
1359
  } else {
1340
1360
  const initialRefreshIndex = options?.refreshIndex ?? this.slotRefreshIndices.get(resolvedSlotId) ?? 0;
1341
1361
  this.slotRefreshIndices.set(resolvedSlotId, initialRefreshIndex);
1362
+ if (options?.auctionId) {
1363
+ this.slotLastAuctionIds.set(resolvedSlotId, options.auctionId);
1364
+ }
1342
1365
  const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
1343
1366
  this.pendingThresholdListeners.delete(resolvedSlotId);
1344
1367
  const record = {
@@ -1394,7 +1417,485 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1394
1417
  this.unobserveSlot(slotId);
1395
1418
  this.slotViewabilityRecords.delete(slotId);
1396
1419
  this.slotRefreshIndices.delete(slotId);
1420
+ this.slotLastAuctionIds.delete(slotId);
1397
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;
1398
1899
  }
1399
1900
  onTimeInViewThreshold(slotId, thresholdMs, callback) {
1400
1901
  const listener = {
@@ -1607,21 +2108,45 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1607
2108
  handleBidWon(data) {
1608
2109
  if (this.isDuplicate("bidWon", data)) return;
1609
2110
  this.log("DEBUG", "bidWon", data);
1610
- this.enqueue(TraceEventType.BID_WIN, "bidWon", {
1611
- auctionId: data.auctionId || "",
1612
- transactionId: data.transactionId || "",
1613
- adUnitCode: data.adUnitCode || "",
1614
- bid: {
1615
- bidder: data.bidderCode || data.bidder || "",
1616
- cpm: Number.isFinite(data.originalCpm) ? data.originalCpm : Number.isFinite(data.cpm) ? data.cpm : 0,
1617
- currency: data.originalCurrency ?? data.currency ?? "USD",
1618
- ...parseBidDimensions(data),
1619
- dealId: data.dealId || "",
1620
- mediaType: data.mediaType || "banner",
1621
- latencyMs: Number.isFinite(data.timeToRespond) ? data.timeToRespond : 0,
1622
- advertiserDomain: data.meta?.advertiserDomains?.[0] || "",
1623
- 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);
1624
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
+ }
2143
+ }
2144
+ }
2145
+ this.enqueue(TraceEventType.BID_WIN, "bidWon", {
2146
+ auctionId,
2147
+ transactionId,
2148
+ adUnitCode,
2149
+ bid: bidTrace
1625
2150
  });
1626
2151
  }
1627
2152
  handleNoBid(data) {
@@ -1660,44 +2185,81 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1660
2185
  if (this.isDuplicate("adRenderSucceeded", data)) return;
1661
2186
  this.log("DEBUG", "adRenderSucceeded", data);
1662
2187
  const bid = data.bid || data || {};
1663
- const adUnitCode = data.adUnitCode || bid.adUnitCode || "";
2188
+ const adUnitCode = data.adUnitCode || bid.adUnitCode || data.adId || bid.adId || "";
1664
2189
  const auctionId = bid.auctionId || data.auctionId || "";
1665
2190
  const transactionId = bid.transactionId || data.transactionId || "";
1666
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);
1667
2195
  const bidTrace = {
1668
- bidder: bid.bidderCode || bid.bidder || "",
1669
- cpm: Number.isFinite(bid.originalCpm) ? bid.originalCpm : Number.isFinite(bid.cpm) ? bid.cpm : 0,
1670
- currency: bid.originalCurrency ?? bid.currency ?? "USD",
1671
- ...parseBidDimensions(bid),
1672
- dealId: bid.dealId || "",
1673
- mediaType,
1674
- latencyMs: Number.isFinite(bid.timeToRespond) ? bid.timeToRespond : 0,
1675
- advertiserDomain: bid.meta?.advertiserDomains?.[0] || "",
1676
- 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 || ""
1677
2205
  };
1678
- 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);
1679
2211
  if (isRefresh) {
1680
2212
  this.flushSlotTimeInView(adUnitCode);
1681
- const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? 0) + 1;
2213
+ const nextRefreshIndex = (this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0)) + 1;
1682
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
+ }
1683
2224
  this.enqueue(TraceEventType.REFRESH, "refresh", {
1684
- auctionId,
1685
- transactionId,
2225
+ auctionId: resolvedAuctionId,
2226
+ transactionId: resolvedTransactionId,
1686
2227
  adUnitCode,
1687
2228
  bid: bidTrace,
1688
2229
  metadata: { refresh_index: String(nextRefreshIndex) }
1689
2230
  });
1690
- } else if (adUnitCode && !this.slotRefreshIndices.has(adUnitCode)) {
1691
- 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
+ }
1692
2238
  }
1693
- const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? 0 : 0;
1694
- this.enqueue(TraceEventType.IMPRESSION, "impression", {
1695
- auctionId,
1696
- transactionId,
2239
+ const currentRefreshIndex = adUnitCode ? this.slotRefreshIndices.get(adUnitCode) ?? (record ? record.refreshIndex : 0) : 0;
2240
+ const alreadyEmitted = adUnitCode ? this.hasImpressionEmitted(
1697
2241
  adUnitCode,
1698
- bid: bidTrace,
1699
- metadata: { refresh_index: String(currentRefreshIndex) }
1700
- });
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
+ }
1701
2263
  if (this.config.viewabilityEnabled && typeof document !== "undefined") {
1702
2264
  let el = null;
1703
2265
  const targetId = adUnitCode || data.adId || bid.adId;
@@ -1725,8 +2287,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1725
2287
  if (el) {
1726
2288
  this.observeSlot(el, adUnitCode || targetId, {
1727
2289
  adUnitCode,
1728
- auctionId,
1729
- transactionId,
2290
+ auctionId: resolvedAuctionId,
2291
+ transactionId: resolvedTransactionId,
1730
2292
  mediaType,
1731
2293
  bid: bidTrace,
1732
2294
  refreshIndex: currentRefreshIndex,
@@ -1968,7 +2530,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1968
2530
  if (this.persistedCleanupTimer) clearTimeout(this.persistedCleanupTimer);
1969
2531
  this.persistedCleanupTimer = setTimeout(() => {
1970
2532
  this.persistedCleanupTimer = null;
1971
- for (const key of [...this.persistedBatchKeys]) {
2533
+ for (const key of Array.from(this.persistedBatchKeys)) {
1972
2534
  this.removePersistedBatch(key);
1973
2535
  }
1974
2536
  }, PENDING_BATCH_CLEANUP_DELAY_MS);
@@ -2138,7 +2700,28 @@ function getbidkernel(alias = "bidkernel") {
2138
2700
  }
2139
2701
  const win = window;
2140
2702
  win[alias] = win[alias] || { q: [] };
2141
- 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;
2142
2725
  }
2143
2726
  export {
2144
2727
  BidkernelPrebidAnalytics,