@bidkernel/analytics 0.11.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,11 +54,24 @@ 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;
60
67
  /** Whether to attach pbjs.onEvent listeners. Defaults to true. Set false for full SDK mode. */
61
68
  attachPbjsListeners?: boolean;
69
+ /**
70
+ * Milliseconds a batch holding a revenue event (impression, win, click)
71
+ * waits before it is sent, so the events of one render cycle share a POST.
72
+ * 0 sends on the next tick. Defaults to 3000.
73
+ */
74
+ revenueFlushDelayMs?: number;
62
75
  }
63
76
  interface SlotObserveOptions {
64
77
  adUnitCode?: string;
@@ -117,6 +130,15 @@ interface SlotViewabilityState {
117
130
  * registerPrebidAnalytics.
118
131
  */
119
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;
120
142
  /**
121
143
  * Canonical duplicate-detection key for a Prebid event. Shared by the adapter
122
144
  * and the diagnostic extension so the two can never drift apart. Covers the
@@ -143,6 +165,13 @@ declare class BidkernelPrebidAnalytics {
143
165
  private static activeInstances;
144
166
  static getActiveInstances(): Set<BidkernelPrebidAnalytics>;
145
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;
146
175
  private config;
147
176
  private queue;
148
177
  private errorCount;
@@ -156,7 +185,7 @@ declare class BidkernelPrebidAnalytics {
156
185
  private consecutiveSendFailures;
157
186
  private nextSendAllowedAt;
158
187
  private replayedEventCount;
159
- private immediateFlushScheduled;
188
+ private revenueFlushTimer;
160
189
  private persistedBatchKeys;
161
190
  private persistedCleanupTimer;
162
191
  private intersectionObserver;
@@ -165,6 +194,10 @@ declare class BidkernelPrebidAnalytics {
165
194
  private slotRefreshIndices;
166
195
  private slotLastAuctionIds;
167
196
  private pendingThresholdListeners;
197
+ private pendingViewableListeners;
198
+ private slotElements;
199
+ private clickDetachCleanups;
200
+ private lastClickAt;
168
201
  private slotEmittedImpressionKeys;
169
202
  private cachedWinningBids;
170
203
  private videoDetachCleanups;
@@ -172,10 +205,48 @@ declare class BidkernelPrebidAnalytics {
172
205
  private logger;
173
206
  constructor(config: AnalyticsConfig);
174
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;
175
218
  disable(): void;
176
219
  private handleVisibilityChange;
177
220
  private handleIntersection;
178
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;
179
250
  private scheduleThresholdTimers;
180
251
  private checkThresholdListeners;
181
252
  private getCurrentTimeInView;
@@ -331,4 +402,4 @@ interface AdsGlobal {
331
402
  */
332
403
  declare function getbidkernel(alias?: string): AdsGlobal;
333
404
 
334
- 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,
@@ -1075,7 +1076,8 @@ var EVENT_NAME_TO_TYPE = {
1075
1076
  timeInView: TraceEventType.TIME_IN_VIEW,
1076
1077
  viewable: TraceEventType.VIEWABLE
1077
1078
  };
1078
- var IMMEDIATE_FLUSH_TYPES = /* @__PURE__ */ new Set([
1079
+ var REVENUE_FLUSH_DELAY_MS = 3e3;
1080
+ var REVENUE_FLUSH_TYPES = /* @__PURE__ */ new Set([
1079
1081
  TraceEventType.IMPRESSION,
1080
1082
  TraceEventType.BID_WIN,
1081
1083
  TraceEventType.CLICK
@@ -1084,11 +1086,13 @@ var CACHED_BID_TTL_MS = 30 * 60 * 1e3;
1084
1086
  var MAX_CACHED_BID_KEYS = 200;
1085
1087
  var MAX_IMPRESSION_KEYS = 1e3;
1086
1088
  var MAX_TRACKED_AUCTIONS = 50;
1089
+ var CLICK_DEDUP_MS = 1e3;
1087
1090
  var SESSION_KEY = "_bidkernel_session";
1088
1091
  var SESSION_TS_KEY = "_bidkernel_session_ts";
1089
1092
  var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
1090
1093
  var inMemorySessionId = null;
1091
1094
  var inMemorySessionTs = 0;
1095
+ var storageAllowed = true;
1092
1096
  function generateUUID() {
1093
1097
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1094
1098
  return crypto.randomUUID();
@@ -1158,6 +1162,7 @@ function sameEndpointOrigin(a, b) {
1158
1162
  function extendSession() {
1159
1163
  const now = Date.now();
1160
1164
  inMemorySessionTs = now;
1165
+ if (!storageAllowed) return;
1161
1166
  try {
1162
1167
  if (typeof localStorage !== "undefined") {
1163
1168
  localStorage.setItem(SESSION_TS_KEY, now.toString());
@@ -1167,18 +1172,23 @@ function extendSession() {
1167
1172
  }
1168
1173
  function getOrCreateSessionId() {
1169
1174
  const now = Date.now();
1175
+ const memoryFresh = !!inMemorySessionId && !!inMemorySessionTs && now - inMemorySessionTs <= THIRTY_MINUTES_MS;
1170
1176
  try {
1171
- if (typeof localStorage !== "undefined") {
1177
+ if (storageAllowed && typeof localStorage !== "undefined") {
1172
1178
  const storedId = localStorage.getItem(SESSION_KEY);
1173
1179
  const tsStr = localStorage.getItem(SESSION_TS_KEY);
1174
1180
  const storedTs = tsStr ? parseInt(tsStr, 10) || 0 : 0;
1175
1181
  if (storedId && storedTs && now - storedTs <= THIRTY_MINUTES_MS) {
1176
1182
  localStorage.setItem(SESSION_TS_KEY, now.toString());
1183
+ inMemorySessionId = storedId;
1184
+ inMemorySessionTs = now;
1177
1185
  return storedId;
1178
1186
  }
1179
- const newId = generateUUID();
1187
+ const newId = memoryFresh ? inMemorySessionId : generateUUID();
1180
1188
  localStorage.setItem(SESSION_KEY, newId);
1181
1189
  localStorage.setItem(SESSION_TS_KEY, now.toString());
1190
+ inMemorySessionId = newId;
1191
+ inMemorySessionTs = now;
1182
1192
  return newId;
1183
1193
  }
1184
1194
  } catch {
@@ -1544,6 +1554,17 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1544
1554
  static initImaInterception() {
1545
1555
  initImaInterception();
1546
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
+ }
1547
1568
  config;
1548
1569
  queue = [];
1549
1570
  errorCount = 0;
@@ -1559,7 +1580,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1559
1580
  consecutiveSendFailures = 0;
1560
1581
  nextSendAllowedAt = 0;
1561
1582
  replayedEventCount = 0;
1562
- immediateFlushScheduled = false;
1583
+ revenueFlushTimer = null;
1563
1584
  // localStorage keys of exit batches this instance persisted, so a page that
1564
1585
  // survives its own pagehide/hidden (bfcache restore, tab re-focus) can
1565
1586
  // remove them instead of leaving them for a duplicate resend.
@@ -1572,6 +1593,12 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1572
1593
  slotRefreshIndices = /* @__PURE__ */ new Map();
1573
1594
  slotLastAuctionIds = /* @__PURE__ */ new Map();
1574
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();
1575
1602
  // Impression deduplication per slot and refresh cycle (strictly 1 impression per cycle)
1576
1603
  slotEmittedImpressionKeys = /* @__PURE__ */ new Set();
1577
1604
  // Winning bid cache from bidWon to marry with subsequent render triggers (adRenderSucceeded, video, etc.)
@@ -1582,6 +1609,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1582
1609
  auctionBidderOutcomes = /* @__PURE__ */ new Map();
1583
1610
  logger;
1584
1611
  constructor(config) {
1612
+ if (config.storageAllowed !== void 0) {
1613
+ _BidkernelPrebidAnalytics.setStorageAllowed(config.storageAllowed);
1614
+ }
1585
1615
  const requestedEndpoint = config.endpoint || "";
1586
1616
  const endpoint = !requestedEndpoint || isTrustedEndpoint(requestedEndpoint) ? requestedEndpoint : "";
1587
1617
  this.config = {
@@ -1597,9 +1627,11 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1597
1627
  warningsEnabled: config.warningsEnabled ?? true,
1598
1628
  errorsEnabled: config.errorsEnabled ?? true,
1599
1629
  viewabilityEnabled: config.viewabilityEnabled ?? true,
1630
+ storageAllowed: config.storageAllowed ?? true,
1600
1631
  logLevel: config.logLevel || "INFO",
1601
1632
  pbjsGlobalName: config.pbjsGlobalName || "pbjs",
1602
- attachPbjsListeners: config.attachPbjsListeners ?? true
1633
+ attachPbjsListeners: config.attachPbjsListeners ?? true,
1634
+ revenueFlushDelayMs: Number.isFinite(config.revenueFlushDelayMs) && config.revenueFlushDelayMs >= 0 ? config.revenueFlushDelayMs : REVENUE_FLUSH_DELAY_MS
1603
1635
  };
1604
1636
  this.logger = createLogger({
1605
1637
  prefix: "[bidkernel][prebid-analytics]",
@@ -1638,65 +1670,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1638
1670
  }
1639
1671
  }
1640
1672
  if (this.config.attachPbjsListeners) {
1641
- const win = typeof window !== "undefined" ? window : {};
1642
- const pbjs = win[this.config.pbjsGlobalName] || {};
1643
- if (typeof pbjs.onEvent === "function") {
1644
- this.log("DEBUG", "Attaching event listeners to pbjs");
1645
- const events = [
1646
- ["auctionInit", this.handleAuctionInit.bind(this)],
1647
- ["auctionEnd", this.handleAuctionEnd.bind(this)],
1648
- ["bidRequested", this.handleBidRequested.bind(this)],
1649
- ["bidResponse", this.handleBidResponse.bind(this)],
1650
- ["bidTimeout", this.handleBidTimeout.bind(this)],
1651
- ["bidWon", this.handleBidWon.bind(this)],
1652
- ["noBid", this.handleNoBid.bind(this)],
1653
- ["adRenderFailed", this.handleAdRenderFailed.bind(this)],
1654
- ["adRenderSucceeded", this.handleAdRenderSucceeded.bind(this)]
1655
- ];
1656
- for (const [name, fn] of events) {
1657
- pbjs.onEvent(name, fn);
1658
- this.boundPbjsHandlers.push({ event: name, handler: fn });
1659
- }
1660
- } else {
1661
- this.log("WARN", "pbjs.onEvent is not defined. Prebid analytics will not function.");
1662
- }
1663
- if (typeof pbjs.getEvents === "function") {
1664
- try {
1665
- const pastEvents = pbjs.getEvents();
1666
- if (Array.isArray(pastEvents)) {
1667
- const newPastEvents = pastEvents.slice(this.replayedEventCount);
1668
- this.replayedEventCount = pastEvents.length;
1669
- if (newPastEvents.length > 0) {
1670
- this.log(
1671
- "DEBUG",
1672
- `Replaying ${newPastEvents.length} historical events from pbjs.getEvents()`
1673
- );
1674
- const handlerMap = {
1675
- auctionInit: this.handleAuctionInit.bind(this),
1676
- auctionEnd: this.handleAuctionEnd.bind(this),
1677
- bidRequested: this.handleBidRequested.bind(this),
1678
- bidResponse: this.handleBidResponse.bind(this),
1679
- bidTimeout: this.handleBidTimeout.bind(this),
1680
- bidWon: this.handleBidWon.bind(this),
1681
- noBid: this.handleNoBid.bind(this),
1682
- adRenderFailed: this.handleAdRenderFailed.bind(this),
1683
- adRenderSucceeded: this.handleAdRenderSucceeded.bind(this)
1684
- };
1685
- for (const ev of newPastEvents) {
1686
- if (!ev) continue;
1687
- const eventType = ev.eventType || ev.event || ev.name;
1688
- const args = ev.args !== void 0 ? ev.args : ev.data !== void 0 ? ev.data : ev;
1689
- const handler = handlerMap[eventType];
1690
- if (handler) {
1691
- handler(args);
1692
- }
1693
- }
1694
- }
1695
- }
1696
- } catch (e) {
1697
- this.log("WARN", "Failed to replay historical events from pbjs.getEvents()", e);
1698
- }
1699
- }
1673
+ this.attachPbjs();
1700
1674
  }
1701
1675
  if (typeof window !== "undefined") {
1702
1676
  if (!this.pageUrl) {
@@ -1719,6 +1693,68 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1719
1693
  }
1720
1694
  }
1721
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
+ }
1722
1758
  disable() {
1723
1759
  if (!this.isEnabled) return;
1724
1760
  this.flushAllSlotsTimeInView();
@@ -1750,6 +1786,14 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1750
1786
  }
1751
1787
  }
1752
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();
1753
1797
  this.cachedWinningBids.clear();
1754
1798
  this.slotEmittedImpressionKeys.clear();
1755
1799
  this.slotLastAuctionIds.clear();
@@ -1758,6 +1802,10 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1758
1802
  clearInterval(this.flushTimer);
1759
1803
  this.flushTimer = null;
1760
1804
  }
1805
+ if (this.revenueFlushTimer) {
1806
+ clearTimeout(this.revenueFlushTimer);
1807
+ this.revenueFlushTimer = null;
1808
+ }
1761
1809
  if (this.persistedCleanupTimer) {
1762
1810
  clearTimeout(this.persistedCleanupTimer);
1763
1811
  this.persistedCleanupTimer = null;
@@ -1907,6 +1955,105 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
1907
1955
  }
1908
1956
  });
1909
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
+ };
1910
2057
  }
1911
2058
  scheduleThresholdTimers(record) {
1912
2059
  if (!record.inView || typeof document !== "undefined" && document.visibilityState === "hidden") {
@@ -2096,6 +2243,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2096
2243
  }
2097
2244
  const listeners = this.pendingThresholdListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
2098
2245
  this.pendingThresholdListeners.delete(resolvedSlotId);
2246
+ const viewableListeners = this.pendingViewableListeners.get(resolvedSlotId) || /* @__PURE__ */ new Set();
2247
+ this.pendingViewableListeners.delete(resolvedSlotId);
2099
2248
  const record = {
2100
2249
  slotId: resolvedSlotId,
2101
2250
  element: el,
@@ -2112,7 +2261,8 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2112
2261
  viewableFired: false,
2113
2262
  dwellTimer: null,
2114
2263
  dwellStartedAt: null,
2115
- thresholdListeners: listeners
2264
+ thresholdListeners: listeners,
2265
+ viewableListeners
2116
2266
  };
2117
2267
  this.slotViewabilityRecords.set(resolvedSlotId, record);
2118
2268
  if (el) {
@@ -2137,6 +2287,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2137
2287
  }
2138
2288
  }
2139
2289
  record.thresholdListeners = /* @__PURE__ */ new Set();
2290
+ record.viewableListeners = /* @__PURE__ */ new Set();
2140
2291
  if (record.element && this.intersectionObserver) {
2141
2292
  this.intersectionObserver.unobserve(record.element);
2142
2293
  this.elementToSlotId.delete(record.element);
@@ -2151,6 +2302,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2151
2302
  this.slotRefreshIndices.delete(slotId);
2152
2303
  this.slotLastAuctionIds.delete(slotId);
2153
2304
  this.pendingThresholdListeners.delete(slotId);
2305
+ this.pendingViewableListeners.delete(slotId);
2154
2306
  const prefix = `${escapeKeyPart(slotId)}:`;
2155
2307
  for (const key of Array.from(this.slotEmittedImpressionKeys)) {
2156
2308
  if (key === slotId || key.startsWith(prefix)) {
@@ -2425,19 +2577,21 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2425
2577
  if (!adsManager.__bidkernelAttachedInstances) {
2426
2578
  try {
2427
2579
  Object.defineProperty(adsManager, "__bidkernelAttachedInstances", {
2428
- value: /* @__PURE__ */ new Set(),
2580
+ value: /* @__PURE__ */ new Map(),
2429
2581
  configurable: true,
2430
2582
  writable: true
2431
2583
  });
2432
2584
  } catch {
2433
- adsManager.__bidkernelAttachedInstances = /* @__PURE__ */ new Set();
2585
+ adsManager.__bidkernelAttachedInstances = /* @__PURE__ */ new Map();
2434
2586
  }
2435
2587
  }
2436
- if (adsManager.__bidkernelAttachedInstances.has(this)) {
2437
- return () => {
2588
+ const attached = adsManager.__bidkernelAttachedInstances;
2589
+ const previous = attached.get(this);
2590
+ if (previous) {
2591
+ if (!options) return () => {
2438
2592
  };
2593
+ previous();
2439
2594
  }
2440
- adsManager.__bidkernelAttachedInstances.add(this);
2441
2595
  const slotId = options?.slotId || options?.adUnitCode || "video";
2442
2596
  const adUnitCode = options?.adUnitCode || slotId;
2443
2597
  const auctionId = options?.auctionId || "";
@@ -2585,7 +2739,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2585
2739
  }
2586
2740
  }
2587
2741
  const cleanup = () => {
2588
- adsManager.__bidkernelAttachedInstances?.delete(this);
2742
+ attached.delete(this);
2589
2743
  for (const { type, handler } of listeners) {
2590
2744
  try {
2591
2745
  adsManager.removeEventListener(type, handler);
@@ -2594,6 +2748,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
2594
2748
  }
2595
2749
  this.videoDetachCleanups.delete(cleanup);
2596
2750
  };
2751
+ attached.set(this, cleanup);
2597
2752
  this.videoDetachCleanups.add(cleanup);
2598
2753
  return cleanup;
2599
2754
  }
@@ -3221,9 +3376,9 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
3221
3376
  );
3222
3377
  }
3223
3378
  if (this.config.viewabilityEnabled && typeof document !== "undefined") {
3224
- let el = null;
3379
+ let el = adUnitCode && this.slotElements.get(adUnitCode) || null;
3225
3380
  const targetId = adUnitCode || data.adId || bid.adId;
3226
- if (targetId) {
3381
+ if (!el && targetId) {
3227
3382
  el = document.getElementById(targetId);
3228
3383
  }
3229
3384
  if (!el && typeof window !== "undefined" && window.googletag?.pubads) {
@@ -3322,12 +3477,11 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
3322
3477
  }
3323
3478
  if (this.queue.length >= BATCH_SIZE) {
3324
3479
  this.flush();
3325
- } else if (IMMEDIATE_FLUSH_TYPES.has(type) && !this.immediateFlushScheduled) {
3326
- this.immediateFlushScheduled = true;
3327
- setTimeout(() => {
3328
- this.immediateFlushScheduled = false;
3480
+ } else if (REVENUE_FLUSH_TYPES.has(type) && this.revenueFlushTimer === null) {
3481
+ this.revenueFlushTimer = setTimeout(() => {
3482
+ this.revenueFlushTimer = null;
3329
3483
  this.flush();
3330
- }, 0);
3484
+ }, this.config.revenueFlushDelayMs);
3331
3485
  }
3332
3486
  }
3333
3487
  shouldSample(type, level) {
@@ -3485,6 +3639,7 @@ var BidkernelPrebidAnalytics = class _BidkernelPrebidAnalytics {
3485
3639
  return `${PENDING_BATCH_KEY_PREFIX}${encodeURIComponent(this.config.propertyId)}_`;
3486
3640
  }
3487
3641
  persistBatch(encoded) {
3642
+ if (!storageAllowed) return null;
3488
3643
  try {
3489
3644
  if (typeof localStorage === "undefined") return null;
3490
3645
  if (!this.config.propertyId) return null;
@@ -3737,6 +3892,7 @@ function getbidkernel(alias = "bidkernel") {
3737
3892
  0 && (module.exports = {
3738
3893
  BidkernelPrebidAnalytics,
3739
3894
  PrebidEventDeduper,
3895
+ getOrCreateSessionId,
3740
3896
  getPrebidEventKey,
3741
3897
  getbidkernel,
3742
3898
  hookImaPrototype,