@formo/analytics 1.38.2 → 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 (74) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +4 -1
  2. package/dist/cjs/src/FormoAnalytics.js +56 -36
  3. package/dist/cjs/src/FormoAnalyticsProvider.js +20 -0
  4. package/dist/cjs/src/event/EventFactory.d.ts +9 -1
  5. package/dist/cjs/src/event/EventFactory.js +31 -16
  6. package/dist/cjs/src/event/EventManager.d.ts +3 -1
  7. package/dist/cjs/src/event/EventManager.js +33 -4
  8. package/dist/cjs/src/event/cancellation.d.ts +4 -0
  9. package/dist/cjs/src/event/cancellation.js +7 -0
  10. package/dist/cjs/src/event/type.d.ts +1 -0
  11. package/dist/cjs/src/event/utils.d.ts +5 -1
  12. package/dist/cjs/src/event/utils.js +57 -2
  13. package/dist/cjs/src/evm/EvmEventTracker.d.ts +7 -0
  14. package/dist/cjs/src/evm/EvmEventTracker.js +31 -5
  15. package/dist/cjs/src/queue/EventQueue.d.ts +1 -0
  16. package/dist/cjs/src/queue/EventQueue.js +11 -6
  17. package/dist/cjs/src/solana/SolanaManager.d.ts +37 -9
  18. package/dist/cjs/src/solana/SolanaManager.js +135 -20
  19. package/dist/cjs/src/solana/SolanaStoreHandler.d.ts +6 -0
  20. package/dist/cjs/src/solana/SolanaStoreHandler.js +17 -8
  21. package/dist/cjs/src/solana/SolanaWalletStandardRegistry.d.ts +163 -0
  22. package/dist/cjs/src/solana/SolanaWalletStandardRegistry.js +447 -0
  23. package/dist/cjs/src/solana/index.d.ts +10 -5
  24. package/dist/cjs/src/solana/index.js +12 -6
  25. package/dist/cjs/src/solana/storeTypes.d.ts +20 -1
  26. package/dist/cjs/src/solana/types.d.ts +32 -9
  27. package/dist/cjs/src/solana/types.js +15 -0
  28. package/dist/cjs/src/solana/walletStandardTypes.d.ts +54 -0
  29. package/dist/cjs/src/solana/walletStandardTypes.js +21 -0
  30. package/dist/cjs/src/storage/StorageManager.d.ts +2 -0
  31. package/dist/cjs/src/storage/StorageManager.js +6 -0
  32. package/dist/cjs/src/storage/index.d.ts +1 -0
  33. package/dist/cjs/src/storage/index.js +7 -1
  34. package/dist/cjs/src/types/base.d.ts +15 -5
  35. package/dist/cjs/src/version.d.ts +1 -1
  36. package/dist/cjs/src/version.js +1 -1
  37. package/dist/esm/src/FormoAnalytics.d.ts +4 -1
  38. package/dist/esm/src/FormoAnalytics.js +54 -34
  39. package/dist/esm/src/FormoAnalyticsProvider.js +20 -0
  40. package/dist/esm/src/event/EventFactory.d.ts +9 -1
  41. package/dist/esm/src/event/EventFactory.js +31 -16
  42. package/dist/esm/src/event/EventManager.d.ts +3 -1
  43. package/dist/esm/src/event/EventManager.js +33 -4
  44. package/dist/esm/src/event/cancellation.d.ts +4 -0
  45. package/dist/esm/src/event/cancellation.js +4 -0
  46. package/dist/esm/src/event/type.d.ts +1 -0
  47. package/dist/esm/src/event/utils.d.ts +5 -1
  48. package/dist/esm/src/event/utils.js +56 -3
  49. package/dist/esm/src/evm/EvmEventTracker.d.ts +7 -0
  50. package/dist/esm/src/evm/EvmEventTracker.js +31 -5
  51. package/dist/esm/src/queue/EventQueue.d.ts +1 -0
  52. package/dist/esm/src/queue/EventQueue.js +11 -6
  53. package/dist/esm/src/solana/SolanaManager.d.ts +37 -9
  54. package/dist/esm/src/solana/SolanaManager.js +135 -20
  55. package/dist/esm/src/solana/SolanaStoreHandler.d.ts +6 -0
  56. package/dist/esm/src/solana/SolanaStoreHandler.js +18 -9
  57. package/dist/esm/src/solana/SolanaWalletStandardRegistry.d.ts +163 -0
  58. package/dist/esm/src/solana/SolanaWalletStandardRegistry.js +444 -0
  59. package/dist/esm/src/solana/index.d.ts +10 -5
  60. package/dist/esm/src/solana/index.js +10 -5
  61. package/dist/esm/src/solana/storeTypes.d.ts +20 -1
  62. package/dist/esm/src/solana/types.d.ts +32 -9
  63. package/dist/esm/src/solana/types.js +14 -0
  64. package/dist/esm/src/solana/walletStandardTypes.d.ts +54 -0
  65. package/dist/esm/src/solana/walletStandardTypes.js +18 -0
  66. package/dist/esm/src/storage/StorageManager.d.ts +2 -0
  67. package/dist/esm/src/storage/StorageManager.js +6 -0
  68. package/dist/esm/src/storage/index.d.ts +1 -0
  69. package/dist/esm/src/storage/index.js +5 -0
  70. package/dist/esm/src/types/base.d.ts +15 -5
  71. package/dist/esm/src/version.d.ts +1 -1
  72. package/dist/esm/src/version.js +1 -1
  73. package/dist/index.umd.min.js +1 -1
  74. package/package.json +2 -2
@@ -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 */
@@ -52,6 +52,7 @@ var constants_1 = require("./constants");
52
52
  var storage_1 = require("./storage");
53
53
  var cookiePolicy_1 = require("./storage/cookiePolicy");
54
54
  var event_1 = require("./event");
55
+ var utils_1 = require("./event/utils");
55
56
  var queue_1 = require("./queue");
56
57
  var logger_1 = require("./logger");
57
58
  var consent_1 = require("./consent");
@@ -68,7 +69,7 @@ var wagmi_1 = require("./wagmi");
68
69
  var solana_1 = require("./solana");
69
70
  var SolanaManager_1 = require("./solana/SolanaManager");
70
71
  // Internal: the Privy identify is reached through identify(user), not exported.
71
- var utils_1 = require("./privy/utils");
72
+ var utils_2 = require("./privy/utils");
72
73
  /**
73
74
  * Constants for provider switching reasons
74
75
  */
@@ -93,6 +94,7 @@ var FormoAnalytics = /** @class */ (function () {
93
94
  /** In-memory URL used to deduplicate SPA pageview events. */
94
95
  this._currentUrl = "";
95
96
  this._pageHooksDisposed = false;
97
+ this._pageGeneration = 0;
96
98
  this.currentUserId = "";
97
99
  /**
98
100
  * Clean up resources and event listeners
@@ -190,6 +192,7 @@ var FormoAnalytics = /** @class */ (function () {
190
192
  isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
191
193
  isTrackingSuppressed: function () { return _this.isTrackingSuppressed(); },
192
194
  willTrackEvent: function (chainId) { return _this.willTrackEvent(chainId); },
195
+ retryDetection: function () { return _this.retryWalletDetection(); },
193
196
  isWagmiMode: function () { return _this.isWagmiMode; },
194
197
  connect: function (params, properties) { return _this.connect(params, properties); },
195
198
  disconnect: function (params) { return _this.disconnect(params); },
@@ -218,7 +221,7 @@ var FormoAnalytics = /** @class */ (function () {
218
221
  // ever sent once the user has opted out, even via a timer or
219
222
  // pagehide flush scheduled before opt-out.
220
223
  canSend: function () { return !_this.hasOptedOutTracking(); },
221
- }), options);
224
+ }), options, function () { return !_this.hasOptedOutTracking(); });
222
225
  // Check consent status on initialization
223
226
  if (this.hasOptedOutTracking()) {
224
227
  logger_1.logger.info("User has previously opted out of tracking");
@@ -245,9 +248,12 @@ var FormoAnalytics = /** @class */ (function () {
245
248
  this.evmEvents.trackEIP1193Provider(provider);
246
249
  }
247
250
  }
248
- // Initialize Solana manager if Solana options are provided
249
- if (options.solana) {
250
- this.solanaManager = new SolanaManager_1.SolanaManager(this, options.solana);
251
+ // Solana wallets are discovered through the Wallet Standard
252
+ // unconditionally, the way EVM wallets are through EIP-6963: an app
253
+ // that never configures Solana still gets its connects. `solana: false`
254
+ // is the opt-out; an object adds framework-kit's store or a cluster.
255
+ if (options.solana !== false) {
256
+ this.solanaManager = new SolanaManager_1.SolanaManager(this, typeof options.solana === "object" ? options.solana : undefined);
251
257
  }
252
258
  this._currentUrl = window.location.href;
253
259
  // Seed currentAddress/currentChainId from the persisted snapshot before
@@ -317,7 +323,7 @@ var FormoAnalytics = /** @class */ (function () {
317
323
  });
318
324
  FormoAnalytics.init = function (writeKey, options) {
319
325
  return __awaiter(this, void 0, void 0, function () {
320
- var analytics, discovered;
326
+ var analytics, discovered, error_1;
321
327
  return __generator(this, function (_a) {
322
328
  switch (_a.label) {
323
329
  case 0:
@@ -327,18 +333,21 @@ var FormoAnalytics = /** @class */ (function () {
327
333
  logger_1.logger.info("FormoAnalytics: Skipping provider detection (EVM disabled)");
328
334
  return [3 /*break*/, 5];
329
335
  case 1:
330
- if (!!analytics.isWagmiMode) return [3 /*break*/, 4];
336
+ _a.trys.push([1, 4, , 5]);
331
337
  return [4 /*yield*/, analytics.evmEvents.getProviders()];
332
338
  case 2:
333
339
  discovered = _a.sent();
334
340
  return [4 /*yield*/, analytics.evmEvents.detectWallets(discovered)];
335
341
  case 3:
336
342
  _a.sent();
337
- analytics.evmEvents.trackProviders(discovered);
343
+ if (!analytics.isWagmiMode) {
344
+ analytics.evmEvents.trackProviders(discovered);
345
+ }
338
346
  return [3 /*break*/, 5];
339
347
  case 4:
340
- logger_1.logger.info("FormoAnalytics: Skipping provider detection (Wagmi mode)");
341
- _a.label = 5;
348
+ error_1 = _a.sent();
349
+ logger_1.logger.warn("FormoAnalytics: Provider discovery failed", error_1);
350
+ return [3 /*break*/, 5];
342
351
  case 5: return [2 /*return*/, analytics];
343
352
  }
344
353
  });
@@ -369,7 +378,8 @@ var FormoAnalytics = /** @class */ (function () {
369
378
  });
370
379
  };
371
380
  /**
372
- * Reset the current user session.
381
+ * Reset user and wallet state while preserving the browser's anonymous id.
382
+ * Use `optOutTracking()` to clear the anonymous id and the attribution.
373
383
  * @returns {void}
374
384
  */
375
385
  FormoAnalytics.prototype.reset = function () {
@@ -380,15 +390,11 @@ var FormoAnalytics = /** @class */ (function () {
380
390
  // page lifetime, because they fall back to currentAddress. Keep the
381
391
  // EVM provider reference so tracking can resume on the next connect.
382
392
  this.wallet.reset();
383
- (0, storage_1.cookie)().remove(constants_1.LOCAL_ANONYMOUS_ID_KEY);
384
393
  (0, storage_1.cookie)().remove(constants_1.SESSION_USER_ID_KEY);
385
394
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_DETECTED_KEY);
386
395
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_IDENTIFIED_KEY);
387
396
  (0, storage_1.cookie)().remove(constants_1.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
- (0, storage_1.session)().remove(constants_1.SESSION_TRAFFIC_SOURCE_KEY);
397
+ // Attribution belongs to the visit, so reset preserves it.
392
398
  };
393
399
  FormoAnalytics.prototype.cleanup = function () {
394
400
  this.isCleanedUp = true;
@@ -572,6 +578,12 @@ var FormoAnalytics = /** @class */ (function () {
572
578
  */
573
579
  FormoAnalytics.prototype.syncWalletState = function (params) {
574
580
  this.wallet.syncWalletState(params);
581
+ this.retryWalletDetection();
582
+ };
583
+ FormoAnalytics.prototype.retryWalletDetection = function () {
584
+ if (this.isCleanedUp)
585
+ return;
586
+ void this.evmEvents.detectWallets(this.evmEvents.detectableProviders());
575
587
  };
576
588
  /** @see WalletStateStore.clearProvider */
577
589
  FormoAnalytics.prototype.clearActiveProvider = function () {
@@ -739,7 +751,7 @@ var FormoAnalytics = /** @class */ (function () {
739
751
  // namespace before emitting. It reads this.currentAddress itself to
740
752
  // preserve an already-connected wallet, so this dispatch is a thin
741
753
  // pass-through and both entry points behave identically.
742
- return [4 /*yield*/, (0, utils_1.identifyPrivyUser)(this, maybeUser, {
754
+ return [4 /*yield*/, (0, utils_2.identifyPrivyUser)(this, maybeUser, {
743
755
  activeAddress: opts.activeAddress,
744
756
  properties: opts.properties,
745
757
  })];
@@ -771,6 +783,11 @@ var FormoAnalytics = /** @class */ (function () {
771
783
  return [2 /*return*/];
772
784
  }
773
785
  if (!!params) return [3 /*break*/, 12];
786
+ // Wagmi owns wallet identification.
787
+ if (this.isWagmiMode) {
788
+ logger_1.logger.info("identify() without params is a no-op in Wagmi mode");
789
+ return [2 /*return*/];
790
+ }
774
791
  // If no params provided, auto-identify
775
792
  logger_1.logger.info("Auto-identifying with providers:", this.evm.all.map(function (p) { return p.info.name; }));
776
793
  _i = 0, _a = this.evm.all;
@@ -950,10 +967,8 @@ var FormoAnalytics = /** @class */ (function () {
950
967
  return __generator(this, function (_c) {
951
968
  switch (_c.label) {
952
969
  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()) {
970
+ // Apply all policy checks before persisting the detection marker.
971
+ if (!this.shouldTrack()) {
957
972
  logger_1.logger.info("detect() skipped: tracking is suppressed for this visitor or environment");
958
973
  return [2 /*return*/];
959
974
  }
@@ -1015,6 +1030,7 @@ var FormoAnalytics = /** @class */ (function () {
1015
1030
  // Set opt-out flag in persistent storage using direct cookie access
1016
1031
  // This must be done before switching storage to ensure persistence
1017
1032
  (0, consent_1.setConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY, "true");
1033
+ this._pageGeneration++;
1018
1034
  // Drop anything already buffered so a pending timer/pagehide flush
1019
1035
  // cannot ship events after consent withdrawal.
1020
1036
  this.eventManager.clear();
@@ -1022,6 +1038,9 @@ var FormoAnalytics = /** @class */ (function () {
1022
1038
  // on opt-in, and nothing else would retry an already-adopted one.
1023
1039
  this.evmEvents.markRegisteredAdoptionsPending();
1024
1040
  this.reset();
1041
+ // Consent withdrawal also clears the browser id and the attribution.
1042
+ (0, utils_1.clearAnonymousId)(constants_1.LOCAL_ANONYMOUS_ID_KEY);
1043
+ (0, storage_1.session)().remove(constants_1.SESSION_TRAFFIC_SOURCE_KEY);
1025
1044
  logger_1.logger.info("Successfully opted out of tracking");
1026
1045
  };
1027
1046
  /**
@@ -1048,11 +1067,9 @@ var FormoAnalytics = /** @class */ (function () {
1048
1067
  logger_1.logger.info("Opting back into tracking");
1049
1068
  // Remove opt-out flag
1050
1069
  (0, consent_1.removeConsentFlag)(this.writeKey, constants_1.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.
1070
+ // Retry wallet adoption skipped while opted out.
1055
1071
  (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
1072
+ this.retryWalletDetection();
1056
1073
  logger_1.logger.info("Successfully opted back into tracking");
1057
1074
  };
1058
1075
  /**
@@ -1133,29 +1150,30 @@ var FormoAnalytics = /** @class */ (function () {
1133
1150
  };
1134
1151
  FormoAnalytics.prototype.trackPageHit = function (category, name, properties, context, callback) {
1135
1152
  return __awaiter(this, void 0, void 0, function () {
1153
+ var canTrack, generation;
1136
1154
  var _this = this;
1137
1155
  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) {
1156
+ canTrack = this.shouldTrack();
1157
+ if (!this.isCleanedUp && canTrack) {
1142
1158
  try {
1143
1159
  this.evmEvents.retryExternalAdoptions();
1160
+ this.retryWalletDetection();
1144
1161
  }
1145
1162
  catch (_b) {
1146
- /* never let the retry break a page hit */
1163
+ // Detection retries must not break page tracking.
1147
1164
  }
1148
1165
  }
1149
- if (!this.shouldTrack()) {
1166
+ if (!canTrack) {
1150
1167
  logger_1.logger.info("Track page hit: Skipping event due to tracking configuration");
1151
1168
  return [2 /*return*/];
1152
1169
  }
1170
+ generation = this._pageGeneration;
1153
1171
  setTimeout(function () {
1154
1172
  // Drop in-flight page hits from an SDK instance that was torn down
1155
1173
  // between scheduling and firing (e.g. provider remount in React Strict
1156
1174
  // Mode / HMR). Otherwise the orphan instance would queue a page event
1157
1175
  // here with its stale, never-populated `currentAddress`.
1158
- if (_this._pageHooksDisposed)
1176
+ if (_this._pageHooksDisposed || generation !== _this._pageGeneration)
1159
1177
  return;
1160
1178
  (function () { return __awaiter(_this, void 0, void 0, function () {
1161
1179
  var e_2;
@@ -1185,7 +1203,7 @@ var FormoAnalytics = /** @class */ (function () {
1185
1203
  };
1186
1204
  FormoAnalytics.prototype.trackEvent = function (type, payload, properties, context, callback) {
1187
1205
  return __awaiter(this, void 0, void 0, function () {
1188
- var error_1;
1206
+ var error_2;
1189
1207
  return __generator(this, function (_a) {
1190
1208
  switch (_a.label) {
1191
1209
  case 0:
@@ -1204,8 +1222,8 @@ var FormoAnalytics = /** @class */ (function () {
1204
1222
  _a.sent();
1205
1223
  return [3 /*break*/, 3];
1206
1224
  case 2:
1207
- error_1 = _a.sent();
1208
- logger_1.logger.error("Error tracking event:", error_1);
1225
+ error_2 = _a.sent();
1226
+ logger_1.logger.error("Error tracking event:", error_2);
1209
1227
  return [3 /*break*/, 3];
1210
1228
  case 3: return [2 /*return*/];
1211
1229
  }
@@ -1271,7 +1289,9 @@ var FormoAnalytics = /** @class */ (function () {
1271
1289
  */
1272
1290
  get: function () {
1273
1291
  if (!this.solanaManager) {
1274
- this.solanaManager = new SolanaManager_1.SolanaManager(this);
1292
+ // Only reachable after `solana: false` (or after cleanup). The host
1293
+ // opted out of discovery, so this manager serves the store path only.
1294
+ this.solanaManager = new SolanaManager_1.SolanaManager(this, undefined, false);
1275
1295
  }
1276
1296
  return this.solanaManager;
1277
1297
  },
@@ -72,6 +72,18 @@ var defaultContext = {
72
72
  hasOptedOutTracking: function () { return false; },
73
73
  };
74
74
  exports.FormoAnalyticsContext = (0, react_1.createContext)(defaultContext);
75
+ var optionObjectIds = new WeakMap();
76
+ var nextOptionObjectId = 1;
77
+ var optionObjectId = function (value) {
78
+ if (!value)
79
+ return undefined;
80
+ var id = optionObjectIds.get(value);
81
+ if (id === undefined) {
82
+ id = nextOptionObjectId++;
83
+ optionObjectIds.set(value, id);
84
+ }
85
+ return id;
86
+ };
75
87
  /**
76
88
  * A stable key over the serializable parts of Options. The provider effect
77
89
  * re-initialises the SDK when this key changes; anything that alters SDK
@@ -95,6 +107,14 @@ var computeOptionsKey = function (options) {
95
107
  logger: options.logger,
96
108
  referral: options.referral,
97
109
  evm: options.evm,
110
+ // `solana` is a boolean or an options object. A store is a live event
111
+ // source, so replacing it must reinitialize the SDK even when both the
112
+ // old and new options contain a store.
113
+ solana: typeof options.solana === "object" ? undefined : options.solana,
114
+ solanaStoreId: typeof options.solana === "object"
115
+ ? optionObjectId(options.solana.store)
116
+ : undefined,
117
+ solanaCluster: typeof options.solana === "object" ? options.solana.cluster : undefined,
98
118
  // For complex objects, just track their presence, not their content
99
119
  hasProvider: !!options.provider,
100
120
  hasWagmi: !!options.wagmi,
@@ -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
@@ -73,11 +73,18 @@ var constants_2 = require("./constants");
73
73
  var sanitize_1 = require("./sanitize");
74
74
  var utils_2 = require("./utils");
75
75
  var browsers_1 = require("../browser/browsers");
76
+ var cancellation_1 = require("./cancellation");
76
77
  var ISO_3166_ALPHA_2_REGEX = /^[A-Z]{2}$/;
77
78
  var EventFactory = /** @class */ (function () {
78
- function EventFactory(options) {
79
+ function EventFactory(options,
80
+ /** Consulted once per event, after the only await in creation. */
81
+ canCreate) {
82
+ if (canCreate === void 0) { canCreate = function () { return true; }; }
79
83
  var _this = this;
80
84
  var _a, _b;
85
+ this.canCreate = canCreate;
86
+ /** Bumped by invalidate(); an event created across a bump is dropped. */
87
+ this.generation = 0;
81
88
  this.extractUTMParameters = function (url) {
82
89
  var result = {
83
90
  utm_campaign: "",
@@ -410,6 +417,10 @@ var EventFactory = /** @class */ (function () {
410
417
  return safeDefaults;
411
418
  }
412
419
  };
420
+ /** Drop every event still being created. Called on clear() and close(). */
421
+ EventFactory.prototype.invalidate = function () {
422
+ this.generation++;
423
+ };
413
424
  // Contextual fields that are automatically collected and populated by the Formo SDK
414
425
  EventFactory.prototype.generateContext = function (context) {
415
426
  return __awaiter(this, void 0, void 0, function () {
@@ -433,24 +444,28 @@ var EventFactory = /** @class */ (function () {
433
444
  };
434
445
  EventFactory.prototype.getEnrichedEvent = function (formoEvent, context) {
435
446
  return __awaiter(this, void 0, void 0, function () {
436
- var commonEventData, eventChainId, validAddress, processedEvent;
437
- var _a;
438
- var _b, _c;
439
- return __generator(this, function (_d) {
440
- switch (_d.label) {
447
+ var generation, enrichedContext, commonEventData, eventChainId, validAddress, processedEvent;
448
+ var _a, _b;
449
+ return __generator(this, function (_c) {
450
+ switch (_c.label) {
441
451
  case 0:
442
- _a = {};
452
+ generation = this.generation;
443
453
  return [4 /*yield*/, this.generateContext(context)];
444
454
  case 1:
445
- commonEventData = (_a.context = _d.sent(),
446
- _a.original_timestamp = (0, timestamp_1.getCurrentTimeFormatted)(),
447
- _a.user_id = formoEvent.user_id,
448
- _a.type = formoEvent.type,
449
- _a.channel = constants_2.CHANNEL,
450
- _a.version = constants_2.VERSION,
451
- _a);
452
- commonEventData.anonymous_id = (0, utils_2.generateAnonymousId)(constants_1.LOCAL_ANONYMOUS_ID_KEY, (_b = this.options) === null || _b === void 0 ? void 0 : _b.crossSubdomainCookies);
453
- eventChainId = (_c = formoEvent.properties) === null || _c === void 0 ? void 0 : _c.chainId;
455
+ enrichedContext = _c.sent();
456
+ if (generation !== this.generation || !this.canCreate()) {
457
+ throw cancellation_1.EVENT_CREATION_CANCELLED;
458
+ }
459
+ commonEventData = {
460
+ context: enrichedContext,
461
+ original_timestamp: (0, timestamp_1.getCurrentTimeFormatted)(),
462
+ user_id: formoEvent.user_id,
463
+ type: formoEvent.type,
464
+ channel: constants_2.CHANNEL,
465
+ version: constants_2.VERSION,
466
+ };
467
+ commonEventData.anonymous_id = (0, utils_2.generateAnonymousId)(constants_1.LOCAL_ANONYMOUS_ID_KEY, (_a = this.options) === null || _a === void 0 ? void 0 : _a.crossSubdomainCookies);
468
+ eventChainId = (_b = formoEvent.properties) === null || _b === void 0 ? void 0 : _b.chainId;
454
469
  validAddress = this.validateEventAddress(formoEvent.address, eventChainId);
455
470
  commonEventData.address = validAddress;
456
471
  processedEvent = (0, mergeDeepRight_1.default)(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
@@ -50,6 +50,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
50
50
  exports.EventManager = void 0;
51
51
  var logger_1 = require("../logger");
52
52
  var EventFactory_1 = require("./EventFactory");
53
+ var cancellation_1 = require("./cancellation");
53
54
  var address_1 = require("../utils/address");
54
55
  /**
55
56
  * A service to generate valid event payloads and queue them for processing
@@ -60,9 +61,13 @@ var EventManager = /** @class */ (function () {
60
61
  * @param eventQueue Event queue instance
61
62
  * @param options Optional configuration (referral parsing, etc.)
62
63
  */
63
- function EventManager(eventQueue, options) {
64
+ function EventManager(eventQueue, options, canAcceptEvent) {
65
+ if (canAcceptEvent === void 0) { canAcceptEvent = function () { return true; }; }
66
+ var _this = this;
67
+ this.canAcceptEvent = canAcceptEvent;
68
+ this.generation = 0;
64
69
  this.eventQueue = eventQueue;
65
- this.eventFactory = new EventFactory_1.EventFactory(options);
70
+ this.eventFactory = new EventFactory_1.EventFactory(options, function () { return _this.canAcceptEvent(); });
66
71
  }
67
72
  /**
68
73
  * Consumes a new incoming event
@@ -70,14 +75,34 @@ var EventManager = /** @class */ (function () {
70
75
  */
71
76
  EventManager.prototype.addEvent = function (event, address, userId) {
72
77
  return __awaiter(this, void 0, void 0, function () {
73
- var callback, _event, formoEvent;
78
+ var callback, _event, generation, shouldContinue, formoEvent, error_1;
79
+ var _this = this;
74
80
  return __generator(this, function (_a) {
75
81
  switch (_a.label) {
76
82
  case 0:
77
83
  callback = event.callback, _event = __rest(event, ["callback"]);
78
- return [4 /*yield*/, this.eventFactory.create(_event, address, userId)];
84
+ generation = this.generation;
85
+ shouldContinue = function () {
86
+ return generation === _this.generation && _this.canAcceptEvent();
87
+ };
88
+ if (!shouldContinue())
89
+ return [2 /*return*/];
90
+ _a.label = 1;
79
91
  case 1:
92
+ _a.trys.push([1, 3, , 4]);
93
+ return [4 /*yield*/, this.eventFactory.create(_event, address, userId)];
94
+ case 2:
80
95
  formoEvent = _a.sent();
96
+ return [3 /*break*/, 4];
97
+ case 3:
98
+ error_1 = _a.sent();
99
+ if (error_1 === cancellation_1.EVENT_CREATION_CANCELLED)
100
+ return [2 /*return*/];
101
+ throw error_1;
102
+ case 4:
103
+ // Reject work invalidated while enrichment was pending.
104
+ if (!shouldContinue())
105
+ return [2 /*return*/];
81
106
  // Check if the final event has a blocked address - don't queue it
82
107
  if (formoEvent.address && (0, address_1.isBlockedAddress)(formoEvent.address)) {
83
108
  logger_1.logger.warn("Event blocked: Address ".concat(formoEvent.address, " is in the blocked list and cannot emit events"));
@@ -98,10 +123,14 @@ var EventManager = /** @class */ (function () {
98
123
  };
99
124
  /** Drop any buffered events (consent withdrawal). Recoverable. */
100
125
  EventManager.prototype.clear = function () {
126
+ this.generation++;
127
+ this.eventFactory.invalidate();
101
128
  this.eventQueue.clear();
102
129
  };
103
130
  /** Terminal shutdown on teardown: nothing can be sent after this. */
104
131
  EventManager.prototype.close = function () {
132
+ this.generation++;
133
+ this.eventFactory.invalidate();
105
134
  this.eventQueue.close();
106
135
  };
107
136
  return EventManager;
@@ -0,0 +1,4 @@
1
+ /** Internal control flow: an event invalidated by consent withdrawal. */
2
+ declare const EVENT_CREATION_CANCELLED: unique symbol;
3
+ export { EVENT_CREATION_CANCELLED };
4
+ //# sourceMappingURL=cancellation.d.ts.map
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EVENT_CREATION_CANCELLED = void 0;
4
+ /** Internal control flow: an event invalidated by consent withdrawal. */
5
+ var EVENT_CREATION_CANCELLED = Symbol();
6
+ exports.EVENT_CREATION_CANCELLED = EVENT_CREATION_CANCELLED;
7
+ //# sourceMappingURL=cancellation.js.map
@@ -5,6 +5,7 @@ export interface IEventManager {
5
5
  close(): void;
6
6
  }
7
7
  export interface IEventFactory {
8
+ invalidate(): void;
8
9
  create(event: APIEvent, address?: Address, userId?: string): Promise<IFormoEvent>;
9
10
  }
10
11
  //# sourceMappingURL=type.d.ts.map
@@ -1,4 +1,8 @@
1
1
  import { AnonymousID } from "../types";
2
2
  declare const generateAnonymousId: (key: string, crossSubdomainCookies?: boolean) => AnonymousID;
3
- export { generateAnonymousId };
3
+ /** Clear the anonymous id from every storage layer. */
4
+ declare const clearAnonymousId: (key: string) => void;
5
+ /** Test hook: forget the page-lifetime memory, as a new page load would. */
6
+ declare const __resetAnonymousIdMemory: () => void;
7
+ export { generateAnonymousId, clearAnonymousId, __resetAnonymousIdMemory };
4
8
  //# sourceMappingURL=utils.d.ts.map
@@ -11,15 +11,28 @@ var __assign = (this && this.__assign) || function () {
11
11
  return __assign.apply(this, arguments);
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.generateAnonymousId = void 0;
14
+ exports.__resetAnonymousIdMemory = exports.clearAnonymousId = exports.generateAnonymousId = void 0;
15
15
  var utils_1 = require("../utils");
16
16
  var storage_1 = require("../storage");
17
17
  var cookiePolicy_1 = require("../storage/cookiePolicy");
18
+ /** Page-lifetime fallback when persistent storage is unavailable. */
19
+ var volatileAnonymousId;
20
+ /** Persistent fallback when cookies are unavailable. */
21
+ var readLocal = function (key) {
22
+ try {
23
+ var value = (0, storage_1.local)().get(key);
24
+ return typeof value === "string" && value ? value : undefined;
25
+ }
26
+ catch (_a) {
27
+ return undefined;
28
+ }
29
+ };
18
30
  var generateAnonymousId = function (key, crossSubdomainCookies) {
31
+ var _a, _b;
19
32
  var storedAnonymousId = (0, storage_1.cookie)().get(key);
20
33
  var anonymousId = (storedAnonymousId && typeof storedAnonymousId === "string"
21
34
  ? storedAnonymousId
22
- : (0, utils_1.generateNativeUUID)());
35
+ : (_b = (_a = readLocal(key)) !== null && _a !== void 0 ? _a : volatileAnonymousId) !== null && _b !== void 0 ? _b : (0, utils_1.generateNativeUUID)());
23
36
  var domain = (0, cookiePolicy_1.getIdentityCookieDomain)(crossSubdomainCookies);
24
37
  // Re-set the cookie with the configured scope. When crossSubdomainCookies
25
38
  // is true, this migrates legacy host-only cookies on the current host to the apex
@@ -27,7 +40,49 @@ var generateAnonymousId = function (key, crossSubdomainCookies) {
27
40
  // example.com is not visible from app.example.com) cannot be migrated
28
41
  // until the user revisits that host.
29
42
  (0, storage_1.cookie)().set(key, anonymousId, __assign(__assign({ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
43
+ // Retry host-only when a domain-scoped write is rejected.
44
+ if (domain && (0, storage_1.cookie)().get(key) !== anonymousId) {
45
+ (0, storage_1.cookie)().set(key, anonymousId, __assign({ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()));
46
+ }
47
+ if ((0, storage_1.cookie)().get(key) === anonymousId) {
48
+ // Prefer the cookie and discard the fallback copy.
49
+ volatileAnonymousId = undefined;
50
+ if ((0, storage_1.usesCookieStorage)() && readLocal(key)) {
51
+ try {
52
+ (0, storage_1.local)().remove(key);
53
+ }
54
+ catch (_c) {
55
+ // Storage is unavailable.
56
+ }
57
+ }
58
+ return anonymousId;
59
+ }
60
+ // Persist across embedded page loads when cookies are blocked.
61
+ try {
62
+ (0, storage_1.local)().set(key, anonymousId);
63
+ }
64
+ catch (_d) {
65
+ // Memory remains the last fallback.
66
+ }
67
+ volatileAnonymousId = anonymousId;
30
68
  return anonymousId;
31
69
  };
32
70
  exports.generateAnonymousId = generateAnonymousId;
71
+ /** Clear the anonymous id from every storage layer. */
72
+ var clearAnonymousId = function (key) {
73
+ (0, storage_1.cookie)().remove(key);
74
+ try {
75
+ (0, storage_1.local)().remove(key);
76
+ }
77
+ catch (_a) {
78
+ // Storage is unavailable.
79
+ }
80
+ volatileAnonymousId = undefined;
81
+ };
82
+ exports.clearAnonymousId = clearAnonymousId;
83
+ /** Test hook: forget the page-lifetime memory, as a new page load would. */
84
+ var __resetAnonymousIdMemory = function () {
85
+ volatileAnonymousId = undefined;
86
+ };
87
+ exports.__resetAnonymousIdMemory = __resetAnonymousIdMemory;
33
88
  //# sourceMappingURL=utils.js.map