@formo/analytics 1.39.0 → 1.40.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.
Files changed (44) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +4 -1
  2. package/dist/cjs/src/FormoAnalytics.js +47 -32
  3. package/dist/cjs/src/event/EventFactory.d.ts +9 -1
  4. package/dist/cjs/src/event/EventFactory.js +31 -16
  5. package/dist/cjs/src/event/EventManager.d.ts +3 -1
  6. package/dist/cjs/src/event/EventManager.js +33 -4
  7. package/dist/cjs/src/event/cancellation.d.ts +4 -0
  8. package/dist/cjs/src/event/cancellation.js +7 -0
  9. package/dist/cjs/src/event/type.d.ts +1 -0
  10. package/dist/cjs/src/event/utils.d.ts +5 -1
  11. package/dist/cjs/src/event/utils.js +57 -2
  12. package/dist/cjs/src/evm/EvmEventTracker.d.ts +7 -0
  13. package/dist/cjs/src/evm/EvmEventTracker.js +31 -5
  14. package/dist/cjs/src/queue/EventQueue.d.ts +1 -0
  15. package/dist/cjs/src/queue/EventQueue.js +11 -6
  16. package/dist/cjs/src/storage/StorageManager.d.ts +2 -0
  17. package/dist/cjs/src/storage/StorageManager.js +6 -0
  18. package/dist/cjs/src/storage/index.d.ts +1 -0
  19. package/dist/cjs/src/storage/index.js +7 -1
  20. package/dist/cjs/src/version.d.ts +1 -1
  21. package/dist/cjs/src/version.js +1 -1
  22. package/dist/esm/src/FormoAnalytics.d.ts +4 -1
  23. package/dist/esm/src/FormoAnalytics.js +45 -30
  24. package/dist/esm/src/event/EventFactory.d.ts +9 -1
  25. package/dist/esm/src/event/EventFactory.js +31 -16
  26. package/dist/esm/src/event/EventManager.d.ts +3 -1
  27. package/dist/esm/src/event/EventManager.js +33 -4
  28. package/dist/esm/src/event/cancellation.d.ts +4 -0
  29. package/dist/esm/src/event/cancellation.js +4 -0
  30. package/dist/esm/src/event/type.d.ts +1 -0
  31. package/dist/esm/src/event/utils.d.ts +5 -1
  32. package/dist/esm/src/event/utils.js +56 -3
  33. package/dist/esm/src/evm/EvmEventTracker.d.ts +7 -0
  34. package/dist/esm/src/evm/EvmEventTracker.js +31 -5
  35. package/dist/esm/src/queue/EventQueue.d.ts +1 -0
  36. package/dist/esm/src/queue/EventQueue.js +11 -6
  37. package/dist/esm/src/storage/StorageManager.d.ts +2 -0
  38. package/dist/esm/src/storage/StorageManager.js +6 -0
  39. package/dist/esm/src/storage/index.d.ts +1 -0
  40. package/dist/esm/src/storage/index.js +5 -0
  41. package/dist/esm/src/version.d.ts +1 -1
  42. package/dist/esm/src/version.js +1 -1
  43. package/dist/index.umd.min.js +1 -1
  44. package/package.json +2 -2
@@ -270,7 +270,7 @@ var EvmEventTracker = /** @class */ (function () {
270
270
  };
271
271
  /** Stop listening for wallet announcements. Called from SDK teardown. */
272
272
  EvmEventTracker.prototype.cleanup = function () {
273
- var _a;
273
+ var _a, _b;
274
274
  // A replay in flight (awaiting the active wallet's accounts) must not
275
275
  // resume into a torn-down instance and commit a session there.
276
276
  this.disposed = true;
@@ -280,11 +280,13 @@ var EvmEventTracker = /** @class */ (function () {
280
280
  this.retryRequested = false;
281
281
  try {
282
282
  (_a = this.unsubscribeDiscovery) === null || _a === void 0 ? void 0 : _a.call(this);
283
+ (_b = this.discoveryStore) === null || _b === void 0 ? void 0 : _b.destroy();
283
284
  }
284
285
  catch (e) {
285
286
  logger_1.logger.warn("Failed to unsubscribe from provider discovery", e);
286
287
  }
287
288
  this.unsubscribeDiscovery = undefined;
289
+ this.discoveryStore = undefined;
288
290
  };
289
291
  EvmEventTracker.prototype.sessionGeneration = function (provider) {
290
292
  var _a;
@@ -1018,6 +1020,7 @@ var EvmEventTracker = /** @class */ (function () {
1018
1020
  this.wallet.provider = provider;
1019
1021
  }
1020
1022
  this.wallet.set('evm', { chainId: nextChainId });
1023
+ this.deps.retryDetection();
1021
1024
  _a.label = 1;
1022
1025
  case 1:
1023
1026
  _a.trys.push([1, 5, , 6]);
@@ -1335,12 +1338,17 @@ var EvmEventTracker = /** @class */ (function () {
1335
1338
  });
1336
1339
  });
1337
1340
  };
1341
+ /** Whether Formo owns provider lifecycle tracking. */
1342
+ EvmEventTracker.prototype.tracksDiscovered = function () {
1343
+ return !this.deps.isWagmiMode();
1344
+ };
1338
1345
  EvmEventTracker.prototype.getProviders = function () {
1339
1346
  return __awaiter(this, void 0, void 0, function () {
1340
1347
  var store, providers, injected, injectedProviderInfo, injectedDetail, uniqueProviders, _i, uniqueProviders_1, detail;
1341
1348
  var _this = this;
1342
1349
  return __generator(this, function (_a) {
1343
1350
  store = (0, mipd_1.createStore)();
1351
+ this.discoveryStore = store;
1344
1352
  providers = store.getProviders();
1345
1353
  this.unsubscribeDiscovery = store.subscribe(function (providerDetails) {
1346
1354
  providers = providerDetails;
@@ -1359,8 +1367,12 @@ var EvmEventTracker = /** @class */ (function () {
1359
1367
  var p = detail === null || detail === void 0 ? void 0 : detail.provider;
1360
1368
  return !!p && !_this.registry.isTracked(p);
1361
1369
  });
1362
- if (newDetails.length > 0) {
1370
+ // Wagmi detects only providers added by this announcement.
1371
+ var toDetect = _this.tracksDiscovered() ? newDetails : newlyAddedDetails;
1372
+ if (_this.tracksDiscovered() && newDetails.length > 0) {
1363
1373
  _this.trackProviders(newDetails);
1374
+ }
1375
+ if (toDetect.length > 0) {
1364
1376
  // Detect newly discovered wallets (session de-dupes) with error handling
1365
1377
  (function () { return __awaiter(_this, void 0, void 0, function () {
1366
1378
  var e_3;
@@ -1368,7 +1380,7 @@ var EvmEventTracker = /** @class */ (function () {
1368
1380
  switch (_a.label) {
1369
1381
  case 0:
1370
1382
  _a.trys.push([0, 2, , 3]);
1371
- return [4 /*yield*/, this.detectWallets(newDetails)];
1383
+ return [4 /*yield*/, this.detectWallets(toDetect)];
1372
1384
  case 1:
1373
1385
  _a.sent();
1374
1386
  return [3 /*break*/, 3];
@@ -1394,7 +1406,7 @@ var EvmEventTracker = /** @class */ (function () {
1394
1406
  if (this.registry.injected &&
1395
1407
  this.registry.injected.provider === injected) {
1396
1408
  // Ensure it's tracked
1397
- if (!this.registry.isTracked(injected)) {
1409
+ if (this.tracksDiscovered() && !this.registry.isTracked(injected)) {
1398
1410
  this.trackEIP1193Provider(injected);
1399
1411
  }
1400
1412
  // Merge with existing providers instead of overwriting
@@ -1402,7 +1414,7 @@ var EvmEventTracker = /** @class */ (function () {
1402
1414
  return [2 /*return*/, this.registry.all];
1403
1415
  }
1404
1416
  // Re-check if the injected provider is already tracked just before tracking
1405
- if (!this.registry.isTracked(injected)) {
1417
+ if (this.tracksDiscovered() && !this.registry.isTracked(injected)) {
1406
1418
  this.trackEIP1193Provider(injected);
1407
1419
  }
1408
1420
  injectedProviderInfo = (0, provider_1.detectInjectedProviderInfo)(injected);
@@ -1462,6 +1474,20 @@ var EvmEventTracker = /** @class */ (function () {
1462
1474
  });
1463
1475
  });
1464
1476
  };
1477
+ EvmEventTracker.prototype.detectableProviders = function () {
1478
+ var _a;
1479
+ var available = new Set();
1480
+ (_a = this.discoveryStore) === null || _a === void 0 ? void 0 : _a.getProviders().forEach(function (detail) {
1481
+ return available.add(detail.provider);
1482
+ });
1483
+ var injected = typeof window !== "undefined" ? window.ethereum : undefined;
1484
+ if (injected)
1485
+ available.add(injected);
1486
+ this.externallyRegistered.forEach(function (provider) { return available.add(provider); });
1487
+ return this.registry.all.filter(function (detail) {
1488
+ return available.has(detail.provider);
1489
+ });
1490
+ };
1465
1491
  /**
1466
1492
  * Seed a provider's chain from whatever it already exposes synchronously.
1467
1493
  *
@@ -28,6 +28,7 @@ export declare class EventQueue implements IEventQueue {
28
28
  private pendingFlush;
29
29
  private payloadHashes;
30
30
  private canSend?;
31
+ private generation;
31
32
  private closed;
32
33
  private disposePageLeave;
33
34
  constructor(writeKey: string, options: Options);
@@ -89,6 +89,7 @@ var EventQueue = /** @class */ (function () {
89
89
  this.queue = [];
90
90
  this.queueByteSize = 0; // running total of queued items' byteSize
91
91
  this.payloadHashes = new Set();
92
+ this.generation = 0;
92
93
  // Terminal shutdown flag. Once set, enqueue() and flush() are no-ops for
93
94
  // the rest of this instance's life. See close().
94
95
  this.closed = false;
@@ -222,6 +223,7 @@ var EventQueue = /** @class */ (function () {
222
223
  * withdrawal / SDK teardown so nothing buffered can be sent later.
223
224
  */
224
225
  EventQueue.prototype.clear = function () {
226
+ this.generation++;
225
227
  if (this.timer) {
226
228
  clearTimeout(this.timer);
227
229
  this.timer = null;
@@ -229,6 +231,8 @@ var EventQueue = /** @class */ (function () {
229
231
  this.queue = [];
230
232
  this.queueByteSize = 0;
231
233
  this.payloadHashes.clear();
234
+ this.flushed = false;
235
+ this.pendingFlush = null;
232
236
  };
233
237
  /**
234
238
  * Terminal shutdown. Unlike clear(), which only empties the buffer and can
@@ -265,11 +269,12 @@ var EventQueue = /** @class */ (function () {
265
269
  });
266
270
  EventQueue.prototype.enqueue = function (event, callback) {
267
271
  return __awaiter(this, void 0, void 0, function () {
268
- var message_id, queueItem, hasReachedFlushAt, hasReachedQueueSize;
272
+ var generation, message_id, queueItem, hasReachedFlushAt, hasReachedQueueSize;
269
273
  return __generator(this, function (_a) {
270
274
  switch (_a.label) {
271
275
  case 0:
272
276
  callback = callback || noop;
277
+ generation = this.generation;
273
278
  // A torn-down instance must never buffer, however late the caller
274
279
  // arrives. See close().
275
280
  if (this.closed)
@@ -282,12 +287,12 @@ var EventQueue = /** @class */ (function () {
282
287
  return [4 /*yield*/, this.generateMessageId(event)];
283
288
  case 1:
284
289
  message_id = _a.sent();
285
- // Re-check after the await. A caller that entered before close() is
286
- // suspended here, and on a queue that has not flushed yet its event
287
- // would push and flush immediately - the exact shape of the bug close()
288
- // exists to stop.
289
- if (this.closed)
290
+ if (this.closed || generation !== this.generation)
290
291
  return [2 /*return*/];
292
+ if (this.canSend && !this.canSend()) {
293
+ this.clear();
294
+ return [2 /*return*/];
295
+ }
291
296
  // check if the message already exists
292
297
  if (this.isDuplicate(message_id)) {
293
298
  logger_1.logger.warn("Event already enqueued, try again after ".concat((0, utils_1.millisecondsToSecond)(this.flushIntervalMs), " seconds."));
@@ -2,8 +2,10 @@ import { IStorage, StorageType } from "./type";
2
2
  export declare class StorageManager {
3
3
  private readonly writeKey;
4
4
  private storages;
5
+ private resolvedTypes;
5
6
  constructor(writeKey: string);
6
7
  getStorage(type: StorageType): IStorage;
8
+ getResolvedType(type: StorageType): StorageType;
7
9
  private createStorage;
8
10
  private getWebStorage;
9
11
  }
@@ -19,6 +19,7 @@ var StorageManager = /** @class */ (function () {
19
19
  function StorageManager(writeKey) {
20
20
  this.writeKey = writeKey;
21
21
  this.storages = new Map();
22
+ this.resolvedTypes = new Map();
22
23
  }
23
24
  StorageManager.prototype.getStorage = function (type) {
24
25
  if (!this.storages.has(type)) {
@@ -39,9 +40,14 @@ var StorageManager = /** @class */ (function () {
39
40
  }
40
41
  // Add to cache
41
42
  this.storages.set(type, storage);
43
+ this.resolvedTypes.set(type, currentType);
42
44
  }
43
45
  return this.storages.get(type);
44
46
  };
47
+ StorageManager.prototype.getResolvedType = function (type) {
48
+ this.getStorage(type);
49
+ return this.resolvedTypes.get(type);
50
+ };
45
51
  StorageManager.prototype.createStorage = function (type) {
46
52
  switch (type) {
47
53
  case "cookieStorage":
@@ -5,4 +5,5 @@ export declare const cookie: () => IStorage;
5
5
  export declare const local: () => IStorage;
6
6
  export declare const session: () => IStorage;
7
7
  export declare const memory: () => IStorage;
8
+ export declare const usesCookieStorage: () => boolean;
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.memory = exports.session = exports.local = exports.cookie = void 0;
17
+ exports.usesCookieStorage = exports.memory = exports.session = exports.local = exports.cookie = void 0;
18
18
  exports.initStorageManager = initStorageManager;
19
19
  var StorageManager_1 = require("./StorageManager");
20
20
  __exportStar(require("./type"), exports);
@@ -38,4 +38,10 @@ var session = function () { return getStorageInstance("sessionStorage"); };
38
38
  exports.session = session;
39
39
  var memory = function () { return getStorageInstance("memoryStorage"); };
40
40
  exports.memory = memory;
41
+ var usesCookieStorage = function () {
42
+ if (!globalManager)
43
+ return false;
44
+ return globalManager.getResolvedType("cookieStorage") === "cookieStorage";
45
+ };
46
+ exports.usesCookieStorage = usesCookieStorage;
41
47
  //# sourceMappingURL=index.js.map
@@ -1,2 +1,2 @@
1
- export declare const version = "1.39.0";
1
+ export declare const version = "1.40.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.version = void 0;
4
4
  // This file is auto-generated by scripts/update-version.js during npm version
5
5
  // Do not edit manually - it will be overwritten
6
- exports.version = '1.39.0';
6
+ exports.version = '1.40.0';
7
7
  //# sourceMappingURL=version.js.map
@@ -54,6 +54,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
54
54
  private _onPopStateListener?;
55
55
  private _onLocationChangeListener?;
56
56
  private _pageHooksDisposed;
57
+ private _pageGeneration;
57
58
  config: Config;
58
59
  /**
59
60
  * The wallet later events are attributed to, derived from whichever
@@ -87,7 +88,8 @@ export declare class FormoAnalytics implements IFormoAnalytics {
87
88
  */
88
89
  page(category?: string, name?: string, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
89
90
  /**
90
- * Reset the current user session.
91
+ * Reset user and wallet state while preserving the browser's anonymous id.
92
+ * Use `optOutTracking()` to clear the anonymous id and the attribution.
91
93
  * @returns {void}
92
94
  */
93
95
  reset(): void;
@@ -142,6 +144,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
142
144
  chainId?: ChainID;
143
145
  address?: Address;
144
146
  }): void;
147
+ private retryWalletDetection;
145
148
  /** @see WalletStateStore.clearProvider */
146
149
  private clearActiveProvider;
147
150
  /** @see WalletStateStore.backfill */
@@ -49,6 +49,7 @@ import { EVENTS_API_HOST, EventType, LOCAL_ANONYMOUS_ID_KEY, SESSION_USER_ID_KEY
49
49
  import { cookie, session, initStorageManager } from "./storage";
50
50
  import { getIdentityCookieDomain, getIdentityCookieSecurity, } from "./storage/cookiePolicy";
51
51
  import { EventManager } from "./event";
52
+ import { clearAnonymousId } from "./event/utils";
52
53
  import { EventQueue } from "./queue";
53
54
  import { logger, Logger } from "./logger";
54
55
  import { setConsentFlag, getConsentFlag, removeConsentFlag, } from "./consent";
@@ -90,6 +91,7 @@ var FormoAnalytics = /** @class */ (function () {
90
91
  /** In-memory URL used to deduplicate SPA pageview events. */
91
92
  this._currentUrl = "";
92
93
  this._pageHooksDisposed = false;
94
+ this._pageGeneration = 0;
93
95
  this.currentUserId = "";
94
96
  /**
95
97
  * Clean up resources and event listeners
@@ -187,6 +189,7 @@ var FormoAnalytics = /** @class */ (function () {
187
189
  isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
188
190
  isTrackingSuppressed: function () { return _this.isTrackingSuppressed(); },
189
191
  willTrackEvent: function (chainId) { return _this.willTrackEvent(chainId); },
192
+ retryDetection: function () { return _this.retryWalletDetection(); },
190
193
  isWagmiMode: function () { return _this.isWagmiMode; },
191
194
  connect: function (params, properties) { return _this.connect(params, properties); },
192
195
  disconnect: function (params) { return _this.disconnect(params); },
@@ -215,7 +218,7 @@ var FormoAnalytics = /** @class */ (function () {
215
218
  // ever sent once the user has opted out, even via a timer or
216
219
  // pagehide flush scheduled before opt-out.
217
220
  canSend: function () { return !_this.hasOptedOutTracking(); },
218
- }), options);
221
+ }), options, function () { return !_this.hasOptedOutTracking(); });
219
222
  // Check consent status on initialization
220
223
  if (this.hasOptedOutTracking()) {
221
224
  logger.info("User has previously opted out of tracking");
@@ -317,7 +320,7 @@ var FormoAnalytics = /** @class */ (function () {
317
320
  });
318
321
  FormoAnalytics.init = function (writeKey, options) {
319
322
  return __awaiter(this, void 0, void 0, function () {
320
- var analytics, discovered;
323
+ var analytics, discovered, error_1;
321
324
  return __generator(this, function (_a) {
322
325
  switch (_a.label) {
323
326
  case 0:
@@ -327,18 +330,21 @@ var FormoAnalytics = /** @class */ (function () {
327
330
  logger.info("FormoAnalytics: Skipping provider detection (EVM disabled)");
328
331
  return [3 /*break*/, 5];
329
332
  case 1:
330
- if (!!analytics.isWagmiMode) return [3 /*break*/, 4];
333
+ _a.trys.push([1, 4, , 5]);
331
334
  return [4 /*yield*/, analytics.evmEvents.getProviders()];
332
335
  case 2:
333
336
  discovered = _a.sent();
334
337
  return [4 /*yield*/, analytics.evmEvents.detectWallets(discovered)];
335
338
  case 3:
336
339
  _a.sent();
337
- analytics.evmEvents.trackProviders(discovered);
340
+ if (!analytics.isWagmiMode) {
341
+ analytics.evmEvents.trackProviders(discovered);
342
+ }
338
343
  return [3 /*break*/, 5];
339
344
  case 4:
340
- logger.info("FormoAnalytics: Skipping provider detection (Wagmi mode)");
341
- _a.label = 5;
345
+ error_1 = _a.sent();
346
+ logger.warn("FormoAnalytics: Provider discovery failed", error_1);
347
+ return [3 /*break*/, 5];
342
348
  case 5: return [2 /*return*/, analytics];
343
349
  }
344
350
  });
@@ -369,7 +375,8 @@ var FormoAnalytics = /** @class */ (function () {
369
375
  });
370
376
  };
371
377
  /**
372
- * Reset the current user session.
378
+ * Reset user and wallet state while preserving the browser's anonymous id.
379
+ * Use `optOutTracking()` to clear the anonymous id and the attribution.
373
380
  * @returns {void}
374
381
  */
375
382
  FormoAnalytics.prototype.reset = function () {
@@ -380,15 +387,11 @@ var FormoAnalytics = /** @class */ (function () {
380
387
  // page lifetime, because they fall back to currentAddress. Keep the
381
388
  // EVM provider reference so tracking can resume on the next connect.
382
389
  this.wallet.reset();
383
- cookie().remove(LOCAL_ANONYMOUS_ID_KEY);
384
390
  cookie().remove(SESSION_USER_ID_KEY);
385
391
  cookie().remove(SESSION_WALLET_DETECTED_KEY);
386
392
  cookie().remove(SESSION_WALLET_IDENTIFIED_KEY);
387
393
  cookie().remove(ACTIVE_WALLET_KEY);
388
- // Stored traffic-source attribution (referrer/UTM) is tracking data;
389
- // clear it too so reset()/optOutTracking() don't leave it to be
390
- // re-attached to the next session's events.
391
- session().remove(SESSION_TRAFFIC_SOURCE_KEY);
394
+ // Attribution belongs to the visit, so reset preserves it.
392
395
  };
393
396
  FormoAnalytics.prototype.cleanup = function () {
394
397
  this.isCleanedUp = true;
@@ -572,6 +575,12 @@ var FormoAnalytics = /** @class */ (function () {
572
575
  */
573
576
  FormoAnalytics.prototype.syncWalletState = function (params) {
574
577
  this.wallet.syncWalletState(params);
578
+ this.retryWalletDetection();
579
+ };
580
+ FormoAnalytics.prototype.retryWalletDetection = function () {
581
+ if (this.isCleanedUp)
582
+ return;
583
+ void this.evmEvents.detectWallets(this.evmEvents.detectableProviders());
575
584
  };
576
585
  /** @see WalletStateStore.clearProvider */
577
586
  FormoAnalytics.prototype.clearActiveProvider = function () {
@@ -771,6 +780,11 @@ var FormoAnalytics = /** @class */ (function () {
771
780
  return [2 /*return*/];
772
781
  }
773
782
  if (!!params) return [3 /*break*/, 12];
783
+ // Wagmi owns wallet identification.
784
+ if (this.isWagmiMode) {
785
+ logger.info("identify() without params is a no-op in Wagmi mode");
786
+ return [2 /*return*/];
787
+ }
774
788
  // If no params provided, auto-identify
775
789
  logger.info("Auto-identifying with providers:", this.evm.all.map(function (p) { return p.info.name; }));
776
790
  _i = 0, _a = this.evm.all;
@@ -950,10 +964,8 @@ var FormoAnalytics = /** @class */ (function () {
950
964
  return __generator(this, function (_c) {
951
965
  switch (_c.label) {
952
966
  case 0:
953
- // detect() marks wallet detection (a cookie write) before
954
- // trackEvent's consent check - gate it for a suppressed visitor or
955
- // excluded environment (opt-out / timezone / host / path).
956
- if (this.isTrackingSuppressed()) {
967
+ // Apply all policy checks before persisting the detection marker.
968
+ if (!this.shouldTrack()) {
957
969
  logger.info("detect() skipped: tracking is suppressed for this visitor or environment");
958
970
  return [2 /*return*/];
959
971
  }
@@ -1015,6 +1027,7 @@ var FormoAnalytics = /** @class */ (function () {
1015
1027
  // Set opt-out flag in persistent storage using direct cookie access
1016
1028
  // This must be done before switching storage to ensure persistence
1017
1029
  setConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY, "true");
1030
+ this._pageGeneration++;
1018
1031
  // Drop anything already buffered so a pending timer/pagehide flush
1019
1032
  // cannot ship events after consent withdrawal.
1020
1033
  this.eventManager.clear();
@@ -1022,6 +1035,9 @@ var FormoAnalytics = /** @class */ (function () {
1022
1035
  // on opt-in, and nothing else would retry an already-adopted one.
1023
1036
  this.evmEvents.markRegisteredAdoptionsPending();
1024
1037
  this.reset();
1038
+ // Consent withdrawal also clears the browser id and the attribution.
1039
+ clearAnonymousId(LOCAL_ANONYMOUS_ID_KEY);
1040
+ session().remove(SESSION_TRAFFIC_SOURCE_KEY);
1025
1041
  logger.info("Successfully opted out of tracking");
1026
1042
  };
1027
1043
  /**
@@ -1048,11 +1064,9 @@ var FormoAnalytics = /** @class */ (function () {
1048
1064
  logger.info("Opting back into tracking");
1049
1065
  // Remove opt-out flag
1050
1066
  removeConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY);
1051
- // A wallet connected while opted out was declined by syncWalletState, and
1052
- // an unchanged wagmi connection produces no status or chain update to
1053
- // retry on. Without this, opting back in leaves that wallet invisible for
1054
- // the rest of the page load.
1067
+ // Retry wallet adoption skipped while opted out.
1055
1068
  (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
1069
+ this.retryWalletDetection();
1056
1070
  logger.info("Successfully opted back into tracking");
1057
1071
  };
1058
1072
  /**
@@ -1133,29 +1147,30 @@ var FormoAnalytics = /** @class */ (function () {
1133
1147
  };
1134
1148
  FormoAnalytics.prototype.trackPageHit = function (category, name, properties, context, callback) {
1135
1149
  return __awaiter(this, void 0, void 0, function () {
1150
+ var canTrack, generation;
1136
1151
  var _this = this;
1137
1152
  return __generator(this, function (_a) {
1138
- // A route change can end path-based suppression; a provider registered
1139
- // while suppressed gets its refused session adoption retried here.
1140
- // Idempotent and cheap when nothing is pending.
1141
- if (!this.isCleanedUp) {
1153
+ canTrack = this.shouldTrack();
1154
+ if (!this.isCleanedUp && canTrack) {
1142
1155
  try {
1143
1156
  this.evmEvents.retryExternalAdoptions();
1157
+ this.retryWalletDetection();
1144
1158
  }
1145
1159
  catch (_b) {
1146
- /* never let the retry break a page hit */
1160
+ // Detection retries must not break page tracking.
1147
1161
  }
1148
1162
  }
1149
- if (!this.shouldTrack()) {
1163
+ if (!canTrack) {
1150
1164
  logger.info("Track page hit: Skipping event due to tracking configuration");
1151
1165
  return [2 /*return*/];
1152
1166
  }
1167
+ generation = this._pageGeneration;
1153
1168
  setTimeout(function () {
1154
1169
  // Drop in-flight page hits from an SDK instance that was torn down
1155
1170
  // between scheduling and firing (e.g. provider remount in React Strict
1156
1171
  // Mode / HMR). Otherwise the orphan instance would queue a page event
1157
1172
  // here with its stale, never-populated `currentAddress`.
1158
- if (_this._pageHooksDisposed)
1173
+ if (_this._pageHooksDisposed || generation !== _this._pageGeneration)
1159
1174
  return;
1160
1175
  (function () { return __awaiter(_this, void 0, void 0, function () {
1161
1176
  var e_2;
@@ -1185,7 +1200,7 @@ var FormoAnalytics = /** @class */ (function () {
1185
1200
  };
1186
1201
  FormoAnalytics.prototype.trackEvent = function (type, payload, properties, context, callback) {
1187
1202
  return __awaiter(this, void 0, void 0, function () {
1188
- var error_1;
1203
+ var error_2;
1189
1204
  return __generator(this, function (_a) {
1190
1205
  switch (_a.label) {
1191
1206
  case 0:
@@ -1204,8 +1219,8 @@ var FormoAnalytics = /** @class */ (function () {
1204
1219
  _a.sent();
1205
1220
  return [3 /*break*/, 3];
1206
1221
  case 2:
1207
- error_1 = _a.sent();
1208
- logger.error("Error tracking event:", error_1);
1222
+ error_2 = _a.sent();
1223
+ logger.error("Error tracking event:", error_2);
1209
1224
  return [3 /*break*/, 3];
1210
1225
  case 3: return [2 /*return*/];
1211
1226
  }
@@ -1,10 +1,16 @@
1
1
  import { Address, APIEvent, ChainID, IFormoEvent, IFormoEventContext, IFormoEventProperties, Nullable, Options, SignatureStatus, TransactionStatus } from "../types";
2
2
  import { IEventFactory } from "./type";
3
3
  declare class EventFactory implements IEventFactory {
4
+ /** Consulted once per event, after the only await in creation. */
5
+ private readonly canCreate;
4
6
  private options?;
5
7
  private compiledPathPattern?;
6
8
  private excludedQueryParams;
7
- constructor(options?: Options);
9
+ /** Bumped by invalidate(); an event created across a bump is dropped. */
10
+ private generation;
11
+ constructor(options?: Options,
12
+ /** Consulted once per event, after the only await in creation. */
13
+ canCreate?: () => boolean);
8
14
  /**
9
15
  * Validate an address for both EVM and Solana chains.
10
16
  * Uses chainId for strict validation when available.
@@ -56,6 +62,8 @@ declare class EventFactory implements IEventFactory {
56
62
  private redactStoredTrafficSources;
57
63
  private getTrafficSources;
58
64
  private getScreen;
65
+ /** Drop every event still being created. Called on clear() and close(). */
66
+ invalidate(): void;
59
67
  private generateContext;
60
68
  /**
61
69
  * Add any missing default page properties using values from options and defaults
@@ -67,11 +67,18 @@ import { CHANNEL, CLICK_ID_PARAMS, DEFAULT_EXCLUDED_QUERY_PARAMS, DEFAULT_REFERR
67
67
  import { sanitizeTrafficSources } from "./sanitize";
68
68
  import { generateAnonymousId } from "./utils";
69
69
  import { detectBrowser } from "../browser/browsers";
70
+ import { EVENT_CREATION_CANCELLED } from "./cancellation";
70
71
  var ISO_3166_ALPHA_2_REGEX = /^[A-Z]{2}$/;
71
72
  var EventFactory = /** @class */ (function () {
72
- function EventFactory(options) {
73
+ function EventFactory(options,
74
+ /** Consulted once per event, after the only await in creation. */
75
+ canCreate) {
76
+ if (canCreate === void 0) { canCreate = function () { return true; }; }
73
77
  var _this = this;
74
78
  var _a, _b;
79
+ this.canCreate = canCreate;
80
+ /** Bumped by invalidate(); an event created across a bump is dropped. */
81
+ this.generation = 0;
75
82
  this.extractUTMParameters = function (url) {
76
83
  var result = {
77
84
  utm_campaign: "",
@@ -404,6 +411,10 @@ var EventFactory = /** @class */ (function () {
404
411
  return safeDefaults;
405
412
  }
406
413
  };
414
+ /** Drop every event still being created. Called on clear() and close(). */
415
+ EventFactory.prototype.invalidate = function () {
416
+ this.generation++;
417
+ };
407
418
  // Contextual fields that are automatically collected and populated by the Formo SDK
408
419
  EventFactory.prototype.generateContext = function (context) {
409
420
  return __awaiter(this, void 0, void 0, function () {
@@ -427,24 +438,28 @@ var EventFactory = /** @class */ (function () {
427
438
  };
428
439
  EventFactory.prototype.getEnrichedEvent = function (formoEvent, context) {
429
440
  return __awaiter(this, void 0, void 0, function () {
430
- var commonEventData, eventChainId, validAddress, processedEvent;
431
- var _a;
432
- var _b, _c;
433
- return __generator(this, function (_d) {
434
- switch (_d.label) {
441
+ var generation, enrichedContext, commonEventData, eventChainId, validAddress, processedEvent;
442
+ var _a, _b;
443
+ return __generator(this, function (_c) {
444
+ switch (_c.label) {
435
445
  case 0:
436
- _a = {};
446
+ generation = this.generation;
437
447
  return [4 /*yield*/, this.generateContext(context)];
438
448
  case 1:
439
- commonEventData = (_a.context = _d.sent(),
440
- _a.original_timestamp = getCurrentTimeFormatted(),
441
- _a.user_id = formoEvent.user_id,
442
- _a.type = formoEvent.type,
443
- _a.channel = CHANNEL,
444
- _a.version = VERSION,
445
- _a);
446
- commonEventData.anonymous_id = generateAnonymousId(LOCAL_ANONYMOUS_ID_KEY, (_b = this.options) === null || _b === void 0 ? void 0 : _b.crossSubdomainCookies);
447
- eventChainId = (_c = formoEvent.properties) === null || _c === void 0 ? void 0 : _c.chainId;
449
+ enrichedContext = _c.sent();
450
+ if (generation !== this.generation || !this.canCreate()) {
451
+ throw EVENT_CREATION_CANCELLED;
452
+ }
453
+ commonEventData = {
454
+ context: enrichedContext,
455
+ original_timestamp: getCurrentTimeFormatted(),
456
+ user_id: formoEvent.user_id,
457
+ type: formoEvent.type,
458
+ channel: CHANNEL,
459
+ version: VERSION,
460
+ };
461
+ commonEventData.anonymous_id = generateAnonymousId(LOCAL_ANONYMOUS_ID_KEY, (_a = this.options) === null || _a === void 0 ? void 0 : _a.crossSubdomainCookies);
462
+ eventChainId = (_b = formoEvent.properties) === null || _b === void 0 ? void 0 : _b.chainId;
448
463
  validAddress = this.validateEventAddress(formoEvent.address, eventChainId);
449
464
  commonEventData.address = validAddress;
450
465
  processedEvent = mergeDeepRight(formoEvent, commonEventData);
@@ -5,14 +5,16 @@ import { IEventFactory, IEventManager } from "./type";
5
5
  * A service to generate valid event payloads and queue them for processing
6
6
  */
7
7
  declare class EventManager implements IEventManager {
8
+ private readonly canAcceptEvent;
8
9
  eventQueue: IEventQueue;
9
10
  eventFactory: IEventFactory;
11
+ private generation;
10
12
  /**
11
13
  *
12
14
  * @param eventQueue Event queue instance
13
15
  * @param options Optional configuration (referral parsing, etc.)
14
16
  */
15
- constructor(eventQueue: IEventQueue, options?: Options);
17
+ constructor(eventQueue: IEventQueue, options?: Options, canAcceptEvent?: () => boolean);
16
18
  /**
17
19
  * Consumes a new incoming event
18
20
  * @param event Incoming event data