@bidkernel/analytics 0.12.0 → 0.16.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.d.ts CHANGED
@@ -54,6 +54,13 @@ interface AnalyticsConfig {
54
54
  errorsEnabled?: boolean;
55
55
  /** Whether to enable client-side IAB viewability and time-in-view measurement. Defaults to true. */
56
56
  viewabilityEnabled?: boolean;
57
+ /**
58
+ * Whether identifiers may be written to localStorage (session id and its
59
+ * timestamp, exit batches). Defaults to true. Set false until storage
60
+ * consent is known, then flip with setStorageAllowed(true); events are
61
+ * always collected and sent regardless.
62
+ */
63
+ storageAllowed?: boolean;
57
64
  logLevel?: "DEBUG" | "INFO" | "WARN" | "ERROR";
58
65
  /** Window global holding the Prebid instance to attach to. Defaults to "pbjs". */
59
66
  pbjsGlobalName?: string;
@@ -123,6 +130,15 @@ interface SlotViewabilityState {
123
130
  * registerPrebidAnalytics.
124
131
  */
125
132
  declare function isTrustedEndpoint(raw: string): boolean;
133
+ /**
134
+ * The visitor's session id: a 30-minute rolling session persisted in
135
+ * localStorage when storage is allowed, kept in memory otherwise. A session
136
+ * begun in memory (before storage consent) is adopted and persisted the
137
+ * first time this runs with storage allowed, so consent never splits a
138
+ * session. Exported so an integration can report the same id the adapter
139
+ * stamps on its batches.
140
+ */
141
+ declare function getOrCreateSessionId(): string;
126
142
  /**
127
143
  * Canonical duplicate-detection key for a Prebid event. Shared by the adapter
128
144
  * and the diagnostic extension so the two can never drift apart. Covers the
@@ -149,6 +165,13 @@ declare class BidkernelPrebidAnalytics {
149
165
  private static activeInstances;
150
166
  static getActiveInstances(): Set<BidkernelPrebidAnalytics>;
151
167
  static initImaInterception(): void;
168
+ /**
169
+ * Allows or forbids localStorage writes for every instance on the page.
170
+ * Collection and delivery are unaffected: only the persisted session id,
171
+ * its timestamp and exit batches are gated.
172
+ */
173
+ static setStorageAllowed(allowed: boolean): void;
174
+ static isStorageAllowed(): boolean;
152
175
  private config;
153
176
  private queue;
154
177
  private errorCount;
@@ -171,6 +194,10 @@ declare class BidkernelPrebidAnalytics {
171
194
  private slotRefreshIndices;
172
195
  private slotLastAuctionIds;
173
196
  private pendingThresholdListeners;
197
+ private pendingViewableListeners;
198
+ private slotElements;
199
+ private clickDetachCleanups;
200
+ private lastClickAt;
174
201
  private slotEmittedImpressionKeys;
175
202
  private cachedWinningBids;
176
203
  private videoDetachCleanups;
@@ -178,10 +205,48 @@ declare class BidkernelPrebidAnalytics {
178
205
  private logger;
179
206
  constructor(config: AnalyticsConfig);
180
207
  enable(): void;
208
+ /**
209
+ * Binds the Prebid event handlers on the configured global and replays
210
+ * pbjs.getEvents() history. enable() calls this when attachPbjsListeners is
211
+ * true. An integration that loads Prebid lazily constructs the instance with
212
+ * attachPbjsListeners=false and calls this once pbjs exists; disable() (and
213
+ * therefore navigate()) drops the handlers, so call it again after either.
214
+ * Idempotent. Returns true when handlers are bound after the call, false
215
+ * when the instance is disabled or pbjs.onEvent is not available yet.
216
+ */
217
+ attachPbjs(): boolean;
181
218
  disable(): void;
182
219
  private handleVisibilityChange;
183
220
  private handleIntersection;
184
221
  private handleDwellComplete;
222
+ private notifyViewable;
223
+ /**
224
+ * Calls back once per render cycle when the slot has met the IAB viewable
225
+ * standard this adapter measures (immediately if it already has). Works
226
+ * before the slot is observed: the listener waits for observeSlot. Returns
227
+ * an unsubscribe function.
228
+ */
229
+ /**
230
+ * Names the element to measure for a slot when the creative renders
231
+ * somewhere other than the slot element (a sticky bar, an interstitial).
232
+ * Applies to the current cycle at once and to every later render.
233
+ */
234
+ setSlotElement(slotId: string, element: HTMLElement): void;
235
+ /**
236
+ * Records a click on the slot's current creative, attributed to the cached
237
+ * winning bid. Repeats within one second are one click. Returns whether a
238
+ * CLICK was recorded.
239
+ */
240
+ recordClick(slotId: string, options?: {
241
+ metadata?: Record<string, string>;
242
+ }): boolean;
243
+ /**
244
+ * Detects clicks into a cross-origin creative frame: a click inside an
245
+ * iframe moves focus into it and blurs the window. Returns a detach
246
+ * function; every tracker is detached on disable().
247
+ */
248
+ attachClickTracker(element: HTMLElement, slotId: string): () => void;
249
+ onViewable(slotId: string, callback: (slotId: string) => void): () => void;
185
250
  private scheduleThresholdTimers;
186
251
  private checkThresholdListeners;
187
252
  private getCurrentTimeInView;
@@ -337,4 +402,4 @@ interface AdsGlobal {
337
402
  */
338
403
  declare function getbidkernel(alias?: string): AdsGlobal;
339
404
 
340
- export { type AdsGlobal, type AnalyticsConfig, BidkernelPrebidAnalytics, type CachedWinningBid, PrebidEventDeduper, type SizeMapping, type SlotConfig, type SlotObserveOptions, type SlotSize, type SlotSizes, type SlotViewabilityState, type VideoPlayerOptions, getPrebidEventKey, getbidkernel, hookImaPrototype, initImaInterception, isTrustedEndpoint, registerPrebidAnalytics };
405
+ export { type AdsGlobal, type AnalyticsConfig, BidkernelPrebidAnalytics, type CachedWinningBid, PrebidEventDeduper, type SizeMapping, type SlotConfig, type SlotObserveOptions, type SlotSize, type SlotSizes, type SlotViewabilityState, type VideoPlayerOptions, getOrCreateSessionId, getPrebidEventKey, getbidkernel, hookImaPrototype, initImaInterception, isTrustedEndpoint, registerPrebidAnalytics };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  BidkernelPrebidAnalytics: () => BidkernelPrebidAnalytics,
24
24
  PrebidEventDeduper: () => PrebidEventDeduper,
25
+ getOrCreateSessionId: () => getOrCreateSessionId,
25
26
  getPrebidEventKey: () => getPrebidEventKey,
26
27
  getbidkernel: () => getbidkernel,
27
28
  hookImaPrototype: () => hookImaPrototype,
@@ -1085,11 +1086,13 @@ var CACHED_BID_TTL_MS = 30 * 60 * 1e3;
1085
1086
  var MAX_CACHED_BID_KEYS = 200;
1086
1087
  var MAX_IMPRESSION_KEYS = 1e3;
1087
1088
  var MAX_TRACKED_AUCTIONS = 50;
1089
+ var CLICK_DEDUP_MS = 1e3;
1088
1090
  var SESSION_KEY = "_bidkernel_session";
1089
1091
  var SESSION_TS_KEY = "_bidkernel_session_ts";
1090
1092
  var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
1091
1093
  var inMemorySessionId = null;
1092
1094
  var inMemorySessionTs = 0;
1095
+ var storageAllowed = true;
1093
1096
  function generateUUID() {
1094
1097
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1095
1098
  return crypto.randomUUID();
@@ -1159,6 +1162,7 @@ function sameEndpointOrigin(a, b) {
1159
1162
  function extendSession() {
1160
1163
  const now = Date.now();
1161
1164
  inMemorySessionTs = now;
1165
+ if (!storageAllowed) return;
1162
1166
  try {
1163
1167
  if (typeof localStorage !== "undefined") {
1164
1168
  localStorage.setItem(SESSION_TS_KEY, now.toString());
@@ -1168,18 +1172,23 @@ function extendSession() {
1168
1172
  }
1169
1173
  function getOrCreateSessionId() {
1170
1174
  const now = Date.now();
1175
+ const memoryFresh = !!inMemorySessionId && !!inMemorySessionTs && now - inMemorySessionTs <= THIRTY_MINUTES_MS;
1171
1176
  try {
1172
- if (typeof localStorage !== "undefined") {
1177
+ if (storageAllowed && typeof localStorage !== "undefined") {
1173
1178
  const storedId = localStorage.getItem(SESSION_KEY);
1174
1179
  const tsStr = localStorage.getItem(SESSION_TS_KEY);
1175
1180
  const storedTs = tsStr ? parseInt(tsStr, 10) || 0 : 0;
1176
1181
  if (storedId && storedTs && now - storedTs <= THIRTY_MINUTES_MS) {
1177
1182
  localStorage.setItem(SESSION_TS_KEY, now.toString());
1183
+ inMemorySessionId = storedId;
1184
+ inMemorySessionTs = now;
1178
1185
  return storedId;
1179
1186
  }
1180
- const newId = generateUUID();
1187
+ const newId = memoryFresh ? inMemorySessionId : generateUUID();
1181
1188
  localStorage.setItem(SESSION_KEY, newId);
1182
1189
  localStorage.setItem(SESSION_TS_KEY, now.toString());
1190
+ inMemorySessionId = newId;
1191
+ inMemorySessionTs = now;
1183
1192
  return newId;
1184
1193
  }
1185
1194
  } catch {
@@ -1545,6 +1554,17 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1545
1554
  static initImaInterception() {
1546
1555
  initImaInterception();
1547
1556
  }
1557
+ /**
1558
+ * Allows or forbids localStorage writes for every instance on the page.
1559
+ * Collection and delivery are unaffected: only the persisted session id,
1560
+ * its timestamp and exit batches are gated.
1561
+ */
1562
+ static setStorageAllowed(allowed) {
1563
+ storageAllowed = allowed;
1564
+ }
1565
+ static isStorageAllowed() {
1566
+ return storageAllowed;
1567
+ }
1548
1568
  config;
1549
1569
  queue = [];
1550
1570
  errorCount = 0;
@@ -1573,6 +1593,12 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1573
1593
  slotRefreshIndices = /* @__PURE__ */ new Map();
1574
1594
  slotLastAuctionIds = /* @__PURE__ */ new Map();
1575
1595
  pendingThresholdListeners = /* @__PURE__ */ new Map();
1596
+ pendingViewableListeners = /* @__PURE__ */ new Map();
1597
+ // Element a subscriber named for a slot (a sticky bar or interstitial that
1598
+ // renders outside the slot element); wins over document.getElementById.
1599
+ slotElements = /* @__PURE__ */ new Map();
1600
+ clickDetachCleanups = /* @__PURE__ */ new Set();
1601
+ lastClickAt = /* @__PURE__ */ new Map();
1576
1602
  // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
1577
1603
  slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
1578
1604
  // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
@@ -1583,6 +1609,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1583
1609
  auctionBidderOutcomes = /* @__PURE__ */ new Map();
1584
1610
  logger;
1585
1611
  constructor(config) {
1612
+ if (config.storageAllowed !== void 0) {
1613
+ _BidkernelPrebidAnalytics.setStorageAllowed(config.storageAllowed);
1614
+ }
1586
1615
  const requestedEndpoint = config.endpoint || "";
1587
1616
  const endpoint = !requestedEndpoint || isTrustedEndpoint(requestedEndpoint) ? requestedEndpoint : "";
1588
1617
  this.config = {
@@ -1598,6 +1627,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1598
1627
  warningsEnabled: config.warningsEnabled ?? true,
1599
1628
  errorsEnabled: config.errorsEnabled ?? true,
1600
1629
  viewabilityEnabled: config.viewabilityEnabled ?? true,
1630
+ storageAllowed: config.storageAllowed ?? true,
1601
1631
  logLevel: config.logLevel || "INFO",
1602
1632
  pbjsGlobalName: config.pbjsGlobalName || "pbjs",
1603
1633
  attachPbjsListeners: config.attachPbjsListeners ?? true,
@@ -1640,65 +1670,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1640
1670
  }
1641
1671
  }
1642
1672
  if (this.config.attachPbjsListeners) {
1643
- const win = typeof window !== "undefined" ? window : {};
1644
- const pbjs = win[this.config.pbjsGlobalName] || {};
1645
- if (typeof pbjs.onEvent === "function") {
1646
- this.log("DEBUG", "Attaching event listeners to pbjs");
1647
- const events = [
1648
- ["auctionInit", this.handleAuctionInit.bind(this)],
1649
- ["auctionEnd", this.handleAuctionEnd.bind(this)],
1650
- ["bidRequested", this.handleBidRequested.bind(this)],
1651
- ["bidResponse", this.handleBidResponse.bind(this)],
1652
- ["bidTimeout", this.handleBidTimeout.bind(this)],
1653
- ["bidWon", this.handleBidWon.bind(this)],
1654
- ["noBid", this.handleNoBid.bind(this)],
1655
- ["adRenderFailed", this.handleAdRenderFailed.bind(this)],
1656
- ["adRenderSucceeded", this.handleAdRenderSucceeded.bind(this)]
1657
- ];
1658
- for (const [name, fn] of events) {
1659
- pbjs.onEvent(name, fn);
1660
- this.boundPbjsHandlers.push({ event: name, handler: fn });
1661
- }
1662
- } else {
1663
- this.log("WARN", "pbjs.onEvent is not defined. Prebid analytics will not function.");
1664
- }
1665
- if (typeof pbjs.getEvents === "function") {
1666
- try {
1667
- const pastEvents = pbjs.getEvents();
1668
- if (Array.isArray(pastEvents)) {
1669
- const newPastEvents = pastEvents.slice(this.replayedEventCount);
1670
- this.replayedEventCount = pastEvents.length;
1671
- if (newPastEvents.length > 0) {
1672
- this.log(
1673
- "DEBUG",
1674
- `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
1675
- );
1676
- const handlerMap = {
1677
- auctionInit: this.handleAuctionInit.bind(this),
1678
- auctionEnd: this.handleAuctionEnd.bind(this),
1679
- bidRequested: this.handleBidRequested.bind(this),
1680
- bidResponse: this.handleBidResponse.bind(this),
1681
- bidTimeout: this.handleBidTimeout.bind(this),
1682
- bidWon: this.handleBidWon.bind(this),
1683
- noBid: this.handleNoBid.bind(this),
1684
- adRenderFailed: this.handleAdRenderFailed.bind(this),
1685
- adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
1686
- };
1687
- for (const ev of newPastEvents) {
1688
- if (!ev) continue;
1689
- const eventType = ev.eventType || ev.event || ev.name;
1690
- const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
1691
- const handler = handlerMap[eventType];
1692
- if (handler) {
1693
- handler(args);
1694
- }
1695
- }
1696
- }
1697
- }
1698
- } catch (e) {
1699
- this.log("WARN", "Failed to replay historical events from pbjs.getEvents()", e);
1700
- }
1701
- }
1673
+ this.attachPbjs();
1702
1674
  }
1703
1675
  if (typeof window !== "undefined") {
1704
1676
  if (!this.pageUrl) {
@@ -1721,6 +1693,68 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1721
1693
  }
1722
1694
  }
1723
1695
  }
1696
+ /**
1697
+ * Binds the Prebid event handlers on the configured global and replays
1698
+ * pbjs.getEvents() history. enable() calls this when attachPbjsListeners is
1699
+ * true. An integration that loads Prebid lazily constructs the instance with
1700
+ * attachPbjsListeners=false and calls this once pbjs exists; disable() (and
1701
+ * therefore navigate()) drops the handlers, so call it again after either.
1702
+ * Idempotent. Returns true when handlers are bound after the call, false
1703
+ * when the instance is disabled or pbjs.onEvent is not available yet.
1704
+ */
1705
+ attachPbjs() {
1706
+ if (!this.isEnabled) return false;
1707
+ if (this.boundPbjsHandlers.length > 0) return true;
1708
+ const win = typeof window !== "undefined" ? window : {};
1709
+ const pbjs = win[this.config.pbjsGlobalName] || {};
1710
+ if (typeof pbjs.onEvent !== "function") {
1711
+ this.log("WARN", "pbjs.onEvent is not defined. Prebid analytics will not function.");
1712
+ return false;
1713
+ }
1714
+ this.log("DEBUG", "Attaching event listeners to pbjs");
1715
+ const handlerMap = {
1716
+ auctionInit: this.handleAuctionInit.bind(this),
1717
+ auctionEnd: this.handleAuctionEnd.bind(this),
1718
+ bidRequested: this.handleBidRequested.bind(this),
1719
+ bidResponse: this.handleBidResponse.bind(this),
1720
+ bidTimeout: this.handleBidTimeout.bind(this),
1721
+ bidWon: this.handleBidWon.bind(this),
1722
+ noBid: this.handleNoBid.bind(this),
1723
+ adRenderFailed: this.handleAdRenderFailed.bind(this),
1724
+ adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
1725
+ };
1726
+ for (const [name, fn] of Object.entries(handlerMap)) {
1727
+ pbjs.onEvent(name, fn);
1728
+ this.boundPbjsHandlers.push({ event: name, handler: fn });
1729
+ }
1730
+ if (typeof pbjs.getEvents === "function") {
1731
+ try {
1732
+ const pastEvents = pbjs.getEvents();
1733
+ if (Array.isArray(pastEvents)) {
1734
+ const newPastEvents = pastEvents.slice(this.replayedEventCount);
1735
+ this.replayedEventCount = pastEvents.length;
1736
+ if (newPastEvents.length > 0) {
1737
+ this.log(
1738
+ "DEBUG",
1739
+ `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
1740
+ );
1741
+ for (const ev of newPastEvents) {
1742
+ if (!ev) continue;
1743
+ const eventType = ev.eventType || ev.event || ev.name;
1744
+ const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
1745
+ const handler = handlerMap[eventType];
1746
+ if (handler) {
1747
+ handler(args);
1748
+ }
1749
+ }
1750
+ }
1751
+ }
1752
+ } catch (e) {
1753
+ this.log("WARN", "Failed to replay historical events from pbjs.getEvents()", e);
1754
+ }
1755
+ }
1756
+ return true;
1757
+ }
1724
1758
  disable() {
1725
1759
  if (!this.isEnabled) return;
1726
1760
  this.flushAllSlotsTimeInView();
@@ -1752,6 +1786,14 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1752
1786
  }
1753
1787
  }
1754
1788
  this.videoDetachCleanups.clear();
1789
+ for (const cleanup of Array.from(this.clickDetachCleanups)) {
1790
+ try {
1791
+ cleanup();
1792
+ } catch {
1793
+ }
1794
+ }
1795
+ this.clickDetachCleanups.clear();
1796
+ this.slotElements.clear();
1755
1797
  this.cachedWinningBids.clear();
1756
1798
  this.slotEmittedImpressionKeys.clear();
1757
1799
  this.slotLastAuctionIds.clear();
@@ -1913,6 +1955,105 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1913
1955
  }
1914
1956
  });
1915
1957
  }
1958
+ this.notifyViewable(record);
1959
+ }
1960
+ notifyViewable(record) {
1961
+ if (!record.viewableFired) return;
1962
+ for (const listener of record.viewableListeners) {
1963
+ if (listener.firedForIndex === record.refreshIndex) continue;
1964
+ listener.firedForIndex = record.refreshIndex;
1965
+ try {
1966
+ listener.callback(record.slotId);
1967
+ } catch (e) {
1968
+ this.log("ERROR", "Error in viewable listener callback", e);
1969
+ }
1970
+ }
1971
+ }
1972
+ /**
1973
+ * Calls back once per render cycle when the slot has met the IAB viewable
1974
+ * standard this adapter measures (immediately if it already has). Works
1975
+ * before the slot is observed: the listener waits for observeSlot. Returns
1976
+ * an unsubscribe function.
1977
+ */
1978
+ /**
1979
+ * Names the element to measure for a slot when the creative renders
1980
+ * somewhere other than the slot element (a sticky bar, an interstitial).
1981
+ * Applies to the current cycle at once and to every later render.
1982
+ */
1983
+ setSlotElement(slotId, element) {
1984
+ this.slotElements.set(slotId, element);
1985
+ const record = this.slotViewabilityRecords.get(slotId);
1986
+ if (record && record.element !== element) {
1987
+ this.observeSlot(element, slotId, {
1988
+ refreshIndex: record.refreshIndex,
1989
+ emitRefreshEvent: false
1990
+ });
1991
+ }
1992
+ }
1993
+ /**
1994
+ * Records a click on the slot's current creative, attributed to the cached
1995
+ * winning bid. Repeats within one second are one click. Returns whether a
1996
+ * CLICK was recorded.
1997
+ */
1998
+ recordClick(slotId, options) {
1999
+ if (!this.isEnabled || !slotId) return false;
2000
+ const now = Date.now();
2001
+ if (now - (this.lastClickAt.get(slotId) || 0) < CLICK_DEDUP_MS) return false;
2002
+ this.lastClickAt.set(slotId, now);
2003
+ const cached = this.getCachedBid(slotId);
2004
+ this.enqueue(TraceEventType.CLICK, "click", {
2005
+ auctionId: cached?.auctionId || "",
2006
+ transactionId: cached?.transactionId || "",
2007
+ adUnitCode: slotId,
2008
+ bid: cached?.bidTrace,
2009
+ metadata: {
2010
+ ...options?.metadata,
2011
+ refresh_index: String(this.getRefreshIndex(slotId))
2012
+ }
2013
+ });
2014
+ return true;
2015
+ }
2016
+ /**
2017
+ * Detects clicks into a cross-origin creative frame: a click inside an
2018
+ * iframe moves focus into it and blurs the window. Returns a detach
2019
+ * function; every tracker is detached on disable().
2020
+ */
2021
+ attachClickTracker(element, slotId) {
2022
+ if (typeof window === "undefined") return () => {
2023
+ };
2024
+ const onBlur = () => {
2025
+ setTimeout(() => {
2026
+ const active = document.activeElement;
2027
+ if (active && (active === element || element.contains(active))) {
2028
+ this.recordClick(slotId, { metadata: { source: "blur" } });
2029
+ window.focus();
2030
+ }
2031
+ }, 0);
2032
+ };
2033
+ window.addEventListener("blur", onBlur);
2034
+ const cleanup = () => {
2035
+ window.removeEventListener("blur", onBlur);
2036
+ this.clickDetachCleanups.delete(cleanup);
2037
+ };
2038
+ this.clickDetachCleanups.add(cleanup);
2039
+ return cleanup;
2040
+ }
2041
+ onViewable(slotId, callback) {
2042
+ const listener = { callback, firedForIndex: -1 };
2043
+ const record = this.slotViewabilityRecords.get(slotId);
2044
+ if (record) {
2045
+ record.viewableListeners.add(listener);
2046
+ this.notifyViewable(record);
2047
+ } else {
2048
+ if (!this.pendingViewableListeners.has(slotId)) {
2049
+ this.pendingViewableListeners.set(slotId, /* @__PURE__ */ new Set());
2050
+ }
2051
+ this.pendingViewableListeners.get(slotId).add(listener);
2052
+ }
2053
+ return () => {
2054
+ this.slotViewabilityRecords.get(slotId)?.viewableListeners.delete(listener);
2055
+ this.pendingViewableListeners.get(slotId)?.delete(listener);
2056
+ };
1916
2057
  }
1917
2058
  scheduleThresholdTimers(record) {
1918
2059
  if (!record.inView || typeof document !== "undefined" && document.visibilityState === "hidden") {
@@ -2102,6 +2243,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2102
2243
  }
2103
2244
  const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
2104
2245
  this.pendingThresholdListeners.delete(resolvedSlotId);
2246
+ const viewableListeners = this.pendingViewableListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
2247
+ this.pendingViewableListeners.delete(resolvedSlotId);
2105
2248
  const record = {
2106
2249
  slotId: resolvedSlotId,
2107
2250
  element: el,
@@ -2118,7 +2261,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2118
2261
  viewableFired: false,
2119
2262
  dwellTimer: null,
2120
2263
  dwellStartedAt: null,
2121
- thresholdListeners: listeners
2264
+ thresholdListeners: listeners,
2265
+ viewableListeners
2122
2266
  };
2123
2267
  this.slotViewabilityRecords.set(resolvedSlotId, record);
2124
2268
  if (el) {
@@ -2143,6 +2287,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2143
2287
  }
2144
2288
  }
2145
2289
  record.thresholdListeners = /* @__PURE__ */ new Set();
2290
+ record.viewableListeners = /* @__PURE__ */ new Set();
2146
2291
  if (record.element && this.intersectionObserver) {
2147
2292
  this.intersectionObserver.unobserve(record.element);
2148
2293
  this.elementToSlotId.delete(record.element);
@@ -2157,6 +2302,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2157
2302
  this.slotRefreshIndices.delete(slotId);
2158
2303
  this.slotLastAuctionIds.delete(slotId);
2159
2304
  this.pendingThresholdListeners.delete(slotId);
2305
+ this.pendingViewableListeners.delete(slotId);
2160
2306
  const prefix = `${escapeKeyPart(slotId)}:`;
2161
2307
  for (const key of Array.from(this.slotEmittedImpressionKeys)) {
2162
2308
  if (key === slotId || key.startsWith(prefix)) {
@@ -2431,19 +2577,21 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2431
2577
  if (!adsManager.__bidkernelAttachedInstances) {
2432
2578
  try {
2433
2579
  Object.defineProperty(adsManager, "__bidkernelAttachedInstances", {
2434
- value: /* @__PURE__ */ new Set(),
2580
+ value: /* @__PURE__ */ new Map(),
2435
2581
  configurable: true,
2436
2582
  writable: true
2437
2583
  });
2438
2584
  } catch {
2439
- adsManager.__bidkernelAttachedInstances = /* @__PURE__ */ new Set();
2585
+ adsManager.__bidkernelAttachedInstances = /* @__PURE__ */ new Map();
2440
2586
  }
2441
2587
  }
2442
- if (adsManager.__bidkernelAttachedInstances.has(this)) {
2443
- return () => {
2588
+ const attached = adsManager.__bidkernelAttachedInstances;
2589
+ const previous = attached.get(this);
2590
+ if (previous) {
2591
+ if (!options) return () => {
2444
2592
  };
2593
+ previous();
2445
2594
  }
2446
- adsManager.__bidkernelAttachedInstances.add(this);
2447
2595
  const slotId = options?.slotId || options?.adUnitCode || "video";
2448
2596
  const adUnitCode = options?.adUnitCode || slotId;
2449
2597
  const auctionId = options?.auctionId || "";
@@ -2591,7 +2739,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2591
2739
  }
2592
2740
  }
2593
2741
  const cleanup = () => {
2594
- adsManager.__bidkernelAttachedInstances?.delete(this);
2742
+ attached.delete(this);
2595
2743
  for (const { type, handler } of listeners) {
2596
2744
  try {
2597
2745
  adsManager.removeEventListener(type, handler);
@@ -2600,6 +2748,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2600
2748
  }
2601
2749
  this.videoDetachCleanups.delete(cleanup);
2602
2750
  };
2751
+ attached.set(this, cleanup);
2603
2752
  this.videoDetachCleanups.add(cleanup);
2604
2753
  return cleanup;
2605
2754
  }
@@ -3227,9 +3376,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
3227
3376
  );
3228
3377
  }
3229
3378
  if (this.config.viewabilityEnabled && typeof document !== "undefined") {
3230
- let el = null;
3379
+ let el = adUnitCode && this.slotElements.get(adUnitCode) || null;
3231
3380
  const targetId = adUnitCode || data.adId || bid.adId;
3232
- if (targetId) {
3381
+ if (!el && targetId) {
3233
3382
  el = document.getElementById(targetId);
3234
3383
  }
3235
3384
  if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
@@ -3490,6 +3639,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
3490
3639
  return `${PENDING_BATCH_KEY_PREFIX}${encodeURIComponent(this.config.propertyId)}_`;
3491
3640
  }
3492
3641
  persistBatch(encoded) {
3642
+ if (!storageAllowed) return null;
3493
3643
  try {
3494
3644
  if (typeof localStorage === "undefined") return null;
3495
3645
  if (!this.config.propertyId) return null;
@@ -3742,6 +3892,7 @@ function getbidkernel(alias = "bidkernel") {
3742
3892
  0 && (module.exports = {
3743
3893
  BidkernelPrebidAnalytics,
3744
3894
  PrebidEventDeduper,
3895
+ getOrCreateSessionId,
3745
3896
  getPrebidEventKey,
3746
3897
  getbidkernel,
3747
3898
  hookImaPrototype,