@formo/analytics 1.38.0 → 1.38.2

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 (34) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +15 -4
  2. package/dist/cjs/src/FormoAnalytics.js +37 -6
  3. package/dist/cjs/src/FormoAnalyticsProvider.d.ts +8 -0
  4. package/dist/cjs/src/FormoAnalyticsProvider.js +41 -32
  5. package/dist/cjs/src/evm/EvmEventTracker.d.ts +100 -9
  6. package/dist/cjs/src/evm/EvmEventTracker.js +357 -19
  7. package/dist/cjs/src/evm/EvmProviderRegistry.d.ts +34 -2
  8. package/dist/cjs/src/evm/EvmProviderRegistry.js +61 -4
  9. package/dist/cjs/src/evm/EvmRequestTracker.d.ts +21 -4
  10. package/dist/cjs/src/evm/EvmRequestTracker.js +54 -28
  11. package/dist/cjs/src/version.d.ts +1 -1
  12. package/dist/cjs/src/version.js +1 -1
  13. package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +67 -1
  14. package/dist/cjs/src/wagmi/WagmiEventHandler.js +286 -39
  15. package/dist/cjs/src/wagmi/types.d.ts +6 -0
  16. package/dist/cjs/src/wagmi/utils.js +9 -1
  17. package/dist/esm/src/FormoAnalytics.d.ts +15 -4
  18. package/dist/esm/src/FormoAnalytics.js +37 -6
  19. package/dist/esm/src/FormoAnalyticsProvider.d.ts +8 -0
  20. package/dist/esm/src/FormoAnalyticsProvider.js +39 -31
  21. package/dist/esm/src/evm/EvmEventTracker.d.ts +100 -9
  22. package/dist/esm/src/evm/EvmEventTracker.js +357 -19
  23. package/dist/esm/src/evm/EvmProviderRegistry.d.ts +34 -2
  24. package/dist/esm/src/evm/EvmProviderRegistry.js +61 -4
  25. package/dist/esm/src/evm/EvmRequestTracker.d.ts +21 -4
  26. package/dist/esm/src/evm/EvmRequestTracker.js +54 -28
  27. package/dist/esm/src/version.d.ts +1 -1
  28. package/dist/esm/src/version.js +1 -1
  29. package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +67 -1
  30. package/dist/esm/src/wagmi/WagmiEventHandler.js +286 -39
  31. package/dist/esm/src/wagmi/types.d.ts +6 -0
  32. package/dist/esm/src/wagmi/utils.js +9 -1
  33. package/dist/index.umd.min.js +1 -1
  34. package/package.json +2 -2
@@ -402,9 +402,9 @@ export declare class FormoAnalytics implements IFormoAnalytics {
402
402
  *
403
403
  * With several live SDK instances (multi write-key pages) registering
404
404
  * the SAME provider, request-derived events (signatures, transactions)
405
- * go to the most recently registered live instance - the same
406
- * single-observer semantics discovery has always had for the request
407
- * wrapper. Lifecycle events (connect, chain, disconnect) reach every
405
+ * go to the most recently CREATED live instance, regardless of the
406
+ * order registrations happen to land in - the same single-observer
407
+ * semantics discovery has always had for the request wrapper. Lifecycle events (connect, chain, disconnect) reach every
408
408
  * instance. Fanning request observations out to all instances is a
409
409
  * separate feature.
410
410
  *
@@ -428,8 +428,19 @@ export declare class FormoAnalytics implements IFormoAnalytics {
428
428
  * gap. Lifecycle (connect/chain/disconnect) stays store-driven: only the
429
429
  * request wrapper installs here. Double counting is prevented in the
430
430
  * wrapper via `shouldSkipRequestCapture`.
431
+ *
432
+ * `attribution` resolves, live, the name and rdns of the connector this
433
+ * provider was wrapped for. Request-derived events are named from the
434
+ * registry, which for an unannounced provider falls back to flag
435
+ * sniffing; recording the connector's resolver here keeps them in
436
+ * agreement with the hook-driven events for that connector.
431
437
  */
432
- _wrapWagmiProvider(provider: EIP1193Provider): void;
438
+ _wrapWagmiProvider(provider: EIP1193Provider, chainId?: number, attribution?: () => {
439
+ name: string;
440
+ rdns?: string;
441
+ } | undefined): boolean;
442
+ /** INTERNAL. Chain updates for the fallback-wrapped provider. */
443
+ _rememberWagmiProviderChain(provider: EIP1193Provider, chainId: number | undefined): void;
433
444
  registerProvider(provider: EIP1193Provider, info?: {
434
445
  name?: string;
435
446
  rdns?: string;
@@ -1018,6 +1018,9 @@ var FormoAnalytics = /** @class */ (function () {
1018
1018
  // Drop anything already buffered so a pending timer/pagehide flush
1019
1019
  // cannot ship events after consent withdrawal.
1020
1020
  this.eventManager.clear();
1021
+ // Identity is purged below; registered sessions must be re-learned
1022
+ // on opt-in, and nothing else would retry an already-adopted one.
1023
+ this.evmEvents.markRegisteredAdoptionsPending();
1021
1024
  this.reset();
1022
1025
  logger_1.logger.info("Successfully opted out of tracking");
1023
1026
  };
@@ -1337,9 +1340,9 @@ var FormoAnalytics = /** @class */ (function () {
1337
1340
  *
1338
1341
  * With several live SDK instances (multi write-key pages) registering
1339
1342
  * the SAME provider, request-derived events (signatures, transactions)
1340
- * go to the most recently registered live instance - the same
1341
- * single-observer semantics discovery has always had for the request
1342
- * wrapper. Lifecycle events (connect, chain, disconnect) reach every
1343
+ * go to the most recently CREATED live instance, regardless of the
1344
+ * order registrations happen to land in - the same single-observer
1345
+ * semantics discovery has always had for the request wrapper. Lifecycle events (connect, chain, disconnect) reach every
1343
1346
  * instance. Fanning request observations out to all instances is a
1344
1347
  * separate feature.
1345
1348
  *
@@ -1363,15 +1366,43 @@ var FormoAnalytics = /** @class */ (function () {
1363
1366
  * gap. Lifecycle (connect/chain/disconnect) stays store-driven: only the
1364
1367
  * request wrapper installs here. Double counting is prevented in the
1365
1368
  * wrapper via `shouldSkipRequestCapture`.
1369
+ *
1370
+ * `attribution` resolves, live, the name and rdns of the connector this
1371
+ * provider was wrapped for. Request-derived events are named from the
1372
+ * registry, which for an unannounced provider falls back to flag
1373
+ * sniffing; recording the connector's resolver here keeps them in
1374
+ * agreement with the hook-driven events for that connector.
1366
1375
  */
1367
- FormoAnalytics.prototype._wrapWagmiProvider = function (provider) {
1376
+ FormoAnalytics.prototype._wrapWagmiProvider = function (provider, chainId, attribution) {
1368
1377
  if (this.isCleanedUp || !(0, provider_1.isValidProvider)(provider))
1369
- return;
1378
+ return false;
1370
1379
  try {
1371
- this.evmRequests.registerRequestListeners(provider);
1380
+ // The tracker reports refusals (frozen provider, unrebindable
1381
+ // wrapper) by returning false rather than throwing; treat those as
1382
+ // failures too so the caller can retry later.
1383
+ if (!this.evmRequests.registerRequestListeners(provider)) {
1384
+ return false;
1385
+ }
1386
+ this.evm.rememberAttribution(provider, attribution);
1387
+ if (chainId !== undefined) {
1388
+ this.evm.rememberChain(provider, chainId);
1389
+ }
1390
+ return true;
1372
1391
  }
1373
1392
  catch (e) {
1374
1393
  logger_1.logger.warn("Failed to wrap wagmi provider for hybrid capture", e);
1394
+ return false;
1395
+ }
1396
+ };
1397
+ /** INTERNAL. Chain updates for the fallback-wrapped provider. */
1398
+ FormoAnalytics.prototype._rememberWagmiProviderChain = function (provider, chainId) {
1399
+ if (this.isCleanedUp)
1400
+ return;
1401
+ try {
1402
+ this.evm.rememberChain(provider, chainId);
1403
+ }
1404
+ catch (_a) {
1405
+ /* bookkeeping only */
1375
1406
  }
1376
1407
  };
1377
1408
  FormoAnalytics.prototype.registerProvider = function (provider, info) {
@@ -7,6 +7,14 @@ export interface FormoAnalyticsProviderProps {
7
7
  children: ReactNode;
8
8
  }
9
9
  export declare const FormoAnalyticsContext: import("react").Context<IFormoAnalytics>;
10
+ /**
11
+ * A stable key over the serializable parts of Options. The provider effect
12
+ * re-initialises the SDK when this key changes; anything that alters SDK
13
+ * behaviour must be represented here, or a runtime change to it is silently
14
+ * ignored. Complex objects are tracked by presence, plus the flags that
15
+ * change what the SDK does with them.
16
+ */
17
+ export declare const computeOptionsKey: (options?: Options) => string;
10
18
  export declare const FormoAnalyticsProvider: FC<FormoAnalyticsProviderProps>;
11
19
  export declare const useFormo: () => IFormoAnalytics;
12
20
  //# sourceMappingURL=FormoAnalyticsProvider.d.ts.map
@@ -47,7 +47,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
47
47
  }
48
48
  };
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.useFormo = exports.FormoAnalyticsProvider = exports.FormoAnalyticsContext = void 0;
50
+ exports.useFormo = exports.FormoAnalyticsProvider = exports.computeOptionsKey = exports.FormoAnalyticsContext = void 0;
51
51
  var jsx_runtime_1 = require("react/jsx-runtime");
52
52
  var react_1 = require("react");
53
53
  var FormoAnalytics_1 = require("./FormoAnalytics");
@@ -72,6 +72,45 @@ var defaultContext = {
72
72
  hasOptedOutTracking: function () { return false; },
73
73
  };
74
74
  exports.FormoAnalyticsContext = (0, react_1.createContext)(defaultContext);
75
+ /**
76
+ * A stable key over the serializable parts of Options. The provider effect
77
+ * re-initialises the SDK when this key changes; anything that alters SDK
78
+ * behaviour must be represented here, or a runtime change to it is silently
79
+ * ignored. Complex objects are tracked by presence, plus the flags that
80
+ * change what the SDK does with them.
81
+ */
82
+ var computeOptionsKey = function (options) {
83
+ var _a;
84
+ if (!options)
85
+ return 'undefined';
86
+ var serializableOptions = {
87
+ tracking: options.tracking,
88
+ autocapture: options.autocapture,
89
+ crossSubdomainCookies: options.crossSubdomainCookies,
90
+ apiHost: options.apiHost,
91
+ flushAt: options.flushAt,
92
+ flushInterval: options.flushInterval,
93
+ retryCount: options.retryCount,
94
+ maxQueueSize: options.maxQueueSize,
95
+ logger: options.logger,
96
+ referral: options.referral,
97
+ evm: options.evm,
98
+ // For complex objects, just track their presence, not their content
99
+ hasProvider: !!options.provider,
100
+ hasWagmi: !!options.wagmi,
101
+ wagmiEip1193Fallback: !!((_a = options.wagmi) === null || _a === void 0 ? void 0 : _a.eip1193Fallback),
102
+ hasReady: !!options.ready,
103
+ };
104
+ try {
105
+ return JSON.stringify(serializableOptions);
106
+ }
107
+ catch (error) {
108
+ // Fallback to timestamp if serialization fails
109
+ logger_1.logger.warn('Failed to serialize options, using timestamp', error);
110
+ return Date.now().toString();
111
+ }
112
+ };
113
+ exports.computeOptionsKey = computeOptionsKey;
75
114
  var FormoAnalyticsProvider = function (props) {
76
115
  var writeKey = props.writeKey, _a = props.disabled, disabled = _a === void 0 ? false : _a, children = props.children;
77
116
  // Keep the app running without analytics if no Write Key is provided or disabled
@@ -91,37 +130,7 @@ var InitializedAnalytics = function (_a) {
91
130
  var _b = (0, react_1.useState)(defaultContext), sdk = _b[0], setSdk = _b[1];
92
131
  var sdkRef = (0, react_1.useRef)(defaultContext);
93
132
  (0, storage_1.initStorageManager)(writeKey);
94
- // Create a stable key from options that ignores complex objects and functions
95
- // We only care about serializable config values that would affect SDK behavior
96
- var optionsKey = (0, react_1.useMemo)(function () {
97
- if (!options)
98
- return 'undefined';
99
- // Extract only the serializable parts of options
100
- var serializableOptions = {
101
- tracking: options.tracking,
102
- autocapture: options.autocapture,
103
- crossSubdomainCookies: options.crossSubdomainCookies,
104
- apiHost: options.apiHost,
105
- flushAt: options.flushAt,
106
- flushInterval: options.flushInterval,
107
- retryCount: options.retryCount,
108
- maxQueueSize: options.maxQueueSize,
109
- logger: options.logger,
110
- referral: options.referral,
111
- // For complex objects, just track their presence, not their content
112
- hasProvider: !!options.provider,
113
- hasWagmi: !!options.wagmi,
114
- hasReady: !!options.ready,
115
- };
116
- try {
117
- return JSON.stringify(serializableOptions);
118
- }
119
- catch (error) {
120
- // Fallback to timestamp if serialization fails
121
- logger_1.logger.warn('Failed to serialize options, using timestamp', error);
122
- return Date.now().toString();
123
- }
124
- }, [options]);
133
+ var optionsKey = (0, react_1.useMemo)(function () { return (0, exports.computeOptionsKey)(options); }, [options]);
125
134
  (0, react_1.useEffect)(function () {
126
135
  var isCleanedUp = false;
127
136
  var initialize = function () { return __awaiter(void 0, void 0, void 0, function () {
@@ -52,10 +52,91 @@ export declare class EvmEventTracker {
52
52
  * Announcement-driven cleanup must not touch them: they are never in an
53
53
  * announcement list, so "missing from the announcement" is their normal
54
54
  * state, not evidence of removal. A Set rather than a WeakSet because
55
- * suppressed adoptions retry from it; the registry holds these providers
56
- * strongly anyway, and untrack removes them.
55
+ * the corrected-detect pass iterates it; the registry's detail list
56
+ * holds these providers strongly for the instance's life anyway.
57
57
  */
58
58
  private externallyRegistered;
59
+ /**
60
+ * Registered providers whose session this SDK has NOT yet learned.
61
+ *
62
+ * Only these are retried on page hits and opt-in. Re-running adoption for
63
+ * every registered provider looked idempotent but was not: adoption is
64
+ * the same path a live `accountsChanged` takes, and that path treats a
65
+ * provider with a different address from the active one as a wallet
66
+ * switch. With two registered providers, or one registered next to a
67
+ * discovered wallet, every page hit emitted a disconnect and a connect
68
+ * that no user action caused.
69
+ *
70
+ * A provider enters at registration (it may have no session yet, or be
71
+ * refused because tracking is suppressed), again whenever a handler
72
+ * refuses its signal while suppressed (noted on entry AND at the
73
+ * suppressed commit, since the handlers gate on the active provider and
74
+ * re-check suppression after an await), and again for all of them when
75
+ * an opt-out purges identity. It leaves the first time a handler commits
76
+ * its session unsuppressed, or when it is untracked. Retrying reads the
77
+ * provider's SYNCHRONOUS accounts only - no RPC - so a registered
78
+ * provider whose session never fires `accountsChanged` (a wallet that
79
+ * signals `connect` alone while `autocapture.connect` is off, which
80
+ * installs a chain-only observer) is still adopted on the next hit. "No
81
+ * RPC" holds for the pending provider itself; the handler's switch
82
+ * arbitration may still ask the ACTIVE provider for its accounts, as it
83
+ * does for any live signal.
84
+ *
85
+ * Replay goes through the accounts handler, which has live wallet-switch
86
+ * semantics: a different provider with a different address is a switch.
87
+ * So a pending provider is replayed only while no OTHER wallet is active
88
+ * and known - it waits, at no cost, until that wallet is gone - unless
89
+ * its own signal was refused (`latestRefused`): then the replay does
90
+ * exactly what the live signal would have done, switch included.
91
+ */
92
+ private pendingAdoptions;
93
+ /**
94
+ * The pending provider whose own live signal was refused while
95
+ * suppressed, LATEST only. The SDK follows one active wallet and the
96
+ * newest signal wins, so replaying only the last refused signal reaches
97
+ * the state the live signals would have reached, without the switches
98
+ * in between; an earlier refused provider stays pending, unprivileged.
99
+ */
100
+ private latestRefused?;
101
+ private dropRefusal;
102
+ /**
103
+ * An opt-out purges wallet identity. Every registered session is then
104
+ * unknown again (pending; no refusal is ADDED, so the opt-in replay of a
105
+ * merely connected wallet does not switch, while a refusal already
106
+ * standing from an excluded route keeps its meaning), and the
107
+ * opt-in retry must re-learn all of them: with the active wallet's
108
+ * address gone, the accounts handler cannot even tell a second wallet's
109
+ * accounts apart from the active one's, and would ignore them.
110
+ */
111
+ markRegisteredAdoptionsPending(): void;
112
+ /**
113
+ * Remember a registered provider's signal that suppression refuses.
114
+ *
115
+ * Replay privilege (`latestRefused`) goes only to a provider whose
116
+ * session the replay can actually read - synchronous accounts - so an
117
+ * accountless `connect` (a chain-only observation, or a session still
118
+ * pairing) cannot displace an earlier refusal that is adoptable.
119
+ */
120
+ private noteRefusalIfSuppressed;
121
+ /**
122
+ * Registered providers whose session ended and have not signalled a
123
+ * new one. Some providers keep stale synchronous `accounts` after a
124
+ * disconnect; replaying those would re-install a session that is over.
125
+ * Any later session signal (connect, accountsChanged) lifts this.
126
+ */
127
+ private awaitingNewSession;
128
+ /** A handler has learned this provider's session; nothing is pending. */
129
+ private settleAdoption;
130
+ /**
131
+ * A registered provider's session has ended. Its NEXT session is not
132
+ * yet learned, and may announce itself in a way no handler adopts (a
133
+ * `connect` alone while `autocapture.connect` is off installs a
134
+ * chain-only observer), so it is pending again for the page-hit retry.
135
+ */
136
+ private reopenAdoption;
137
+ /** One retry scan at a time; a call during a scan queues one more. */
138
+ private retryInFlight;
139
+ private retryRequested;
59
140
  /**
60
141
  * The connect this SDK has already reported for a provider.
61
142
  *
@@ -74,6 +155,11 @@ export declare class EvmEventTracker {
74
155
  constructor(wallet: WalletStateStore, registry: EvmProviderRegistry, deps: EvmEventTrackerDeps);
75
156
  /** Stop listening for wallet announcements. Called from SDK teardown. */
76
157
  cleanup(): void;
158
+ /** Set by `cleanup()`; every awaited continuation checks it. */
159
+ private disposed;
160
+ /** Bumped when a registered provider's session ends. See reopenAdoption. */
161
+ private sessionGenerations;
162
+ private sessionGeneration;
77
163
  /** Drop a provider's reported connect. Called when it stops being active. */
78
164
  forgetAnnouncedConnect(provider: EIP1193Provider): void;
79
165
  /**
@@ -109,14 +195,19 @@ export declare class EvmEventTracker {
109
195
  */
110
196
  adoptExternalProvider(detail: EIP6963ProviderDetail): boolean;
111
197
  /**
112
- * Re-run session adoption for every registered external provider.
198
+ * Finish what registration could not.
199
+ *
200
+ * A registered provider's session may not have existed at registration,
201
+ * or its adoption was refused because tracking was suppressed (opt-out,
202
+ * excluded route) - and an existing session may never emit another
203
+ * accountsChanged, so nothing else would ever retry. Called when
204
+ * suppression can have ended (opt-in, page navigation).
113
205
  *
114
- * Registration while tracking was suppressed (opt-out, excluded route)
115
- * reached the adoption path and was refused - and a provider whose
116
- * session already exists may never emit another accountsChanged, so
117
- * nothing would ever retry. Called when suppression can have ended
118
- * (opt-in, page navigation). Idempotent: an already-adopted wallet is
119
- * deduplicated by the same state and markers as any repeated signal.
206
+ * Adoption is retried ONLY for providers not yet adopted
207
+ * (`pendingAdoptions`), only from their synchronous accounts, and only
208
+ * once suppression has actually ended. The corrected-detect pass below
209
+ * runs for every registered provider: it is deduplicated per session by
210
+ * rdns, so repeating it is free.
120
211
  */
121
212
  retryExternalAdoptions(): void;
122
213
  private registerAccountsChangedListener;