@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
@@ -75,6 +75,16 @@ var RESERVED_FIELDS = new Set([
75
75
  "transactionHash",
76
76
  "function_name",
77
77
  "function_args",
78
+ // Wallet attribution. Function args are spread AFTER the attribution
79
+ // properties, so an ABI input named `providerName` would silently
80
+ // relabel the wallet on every broadcast and receipt event. `rdns` is
81
+ // reserved with it even though THIS path (wagmi mutation attribution)
82
+ // emits only the name: elsewhere in the SDK - the EIP-1193 request path
83
+ // and connect events - `rdns` is the wallet-identity key, so an ABI
84
+ // input by that name would land in the same warehouse column as real
85
+ // wallet identities.
86
+ "providerName",
87
+ "rdns",
78
88
  ]);
79
89
  /**
80
90
  * Connections already announced to a destination during this page load.
@@ -242,8 +252,6 @@ var walletConnectPeerLookups = new WeakSet();
242
252
  * newest kicked one, so a slow resolution from a PREVIOUS session cannot
243
253
  * land after the current session's and overwrite it. */
244
254
  var walletConnectPeerLatest = new WeakMap();
245
- /** Connections whose provider already has the hybrid-capture wrapper. */
246
- var wagmiWrappedConnections = new WeakSet();
247
255
  /**
248
256
  * Pending transactions, shared per destination rather than per handler.
249
257
  *
@@ -428,6 +436,57 @@ var WagmiEventHandler = /** @class */ (function () {
428
436
  * Key format: `${queryHash}:${status}`
429
437
  */
430
438
  this.processedQueries = new Set();
439
+ /**
440
+ * Start resolving the wallet behind a WalletConnect connection.
441
+ *
442
+ * Fire-and-forget on purpose: emission paths are synchronous by design
443
+ * and must never wait (see the connect marker comment). Called at the
444
+ * START of the status/address flows rather than only at read time - the
445
+ * lookup is one microtask for an initialised connector, and the emission
446
+ * sits behind several awaits, so kicking early usually means even the
447
+ * FIRST connect names the real wallet. When the race is lost the event
448
+ * honestly says "WalletConnect" and every later event names the peer.
449
+ */
450
+ /**
451
+ * Install the request wrapper on the active connector's provider.
452
+ *
453
+ * This is what lets wagmi mode capture IMPERATIVE viem calls
454
+ * (walletClient.sendTransaction / .signMessage / .writeContract / raw
455
+ * request), which create no mutation and were silently lost. Hook-driven
456
+ * calls stay owned by the mutation handlers via the pending-mutation
457
+ * dedup. Fire-and-forget per connection; a provider that cannot be
458
+ * produced simply keeps mutation-only capture.
459
+ */
460
+ /**
461
+ * Connections THIS instance has wrapped. Deliberately per-instance, not
462
+ * module-level: a module-level guard let a rebuilt SDK instance skip
463
+ * re-wrapping, so ownership stayed with the torn-down instance and every
464
+ * capture died in its closed queue - and it also stopped a second
465
+ * write-key instance from ever registering. Re-wrapping is safe and
466
+ * cheap: the request tracker rebinds ownership of an intact wrapper.
467
+ */
468
+ /**
469
+ * Latest wrap attempt per connector. Every kick re-resolves
470
+ * getProvider() - a connector can hand out a REPLACEMENT provider after
471
+ * a reconnect, so a once-only guard would leave the new session's
472
+ * provider unwrapped forever. Re-wrapping is idempotent: the request
473
+ * tracker rebinds ownership of an intact wrapper. The epoch lets a slow
474
+ * resolution from a superseded kick (an earlier session, an earlier
475
+ * chain) be discarded instead of overwriting fresher state.
476
+ */
477
+ this.wrapEpochs = new WeakMap();
478
+ /** Consecutive automatic wrap retries per connector; see below. */
479
+ this.wrapRetryCounts = new WeakMap();
480
+ /** Pending retry timers, cancelled by cleanup(). */
481
+ this.wrapRetryTimers = new Set();
482
+ /**
483
+ * Bumped on every observed disconnect. Every wrap attempt captures it
484
+ * at kick time and its resolution stands down on a mismatch, so a
485
+ * disconnect invalidates ALL pending wraps at once - including ones
486
+ * whose getProvider() had not resolved yet, which no per-connector
487
+ * bookkeeping can reach because nothing has been recorded for them.
488
+ */
489
+ this.wrapSessionGeneration = 0;
431
490
  this.formo = formoAnalytics;
432
491
  this.wagmiConfig = wagmiConfig;
433
492
  this.queryClient = queryClient;
@@ -602,6 +661,10 @@ var WagmiEventHandler = /** @class */ (function () {
602
661
  var connectionUnsubscribe = this.wagmiConfig.subscribe(function (state) { var _a, _b; return "".concat((_a = state.current) !== null && _a !== void 0 ? _a : "", "|").concat((_b = _this.getConnectedAddress(state)) !== null && _b !== void 0 ? _b : ""); }, function (key, prevKey) {
603
662
  var _a = key.split("|"), address = _a[1];
604
663
  var _b = (prevKey !== null && prevKey !== void 0 ? prevKey : "|").split("|"), prevAddress = _b[1];
664
+ // A connector switch keeps status "connected", so the status
665
+ // subscription never re-wraps; the NEW active connection's provider
666
+ // must be instrumented from here or its imperative calls are lost.
667
+ _this.wrapActiveConnectorProvider(_this.getState());
605
668
  _this.handleActiveAddressChange(address || undefined, prevAddress || undefined);
606
669
  });
607
670
  this.unsubscribers.push(connectionUnsubscribe);
@@ -907,6 +970,19 @@ var WagmiEventHandler = /** @class */ (function () {
907
970
  return __generator(this, function (_e) {
908
971
  switch (_e.label) {
909
972
  case 0:
973
+ if (status === "disconnected" || status === "reconnecting") {
974
+ // The wrapped session is over - or, for "reconnecting", about to be
975
+ // replaced without ever passing through "disconnected". This must
976
+ // run for EVERY such transition, before the processing lock and
977
+ // regardless of whether a wallet was being tracked, or a rapid
978
+ // same-connector reconnect retains the old provider and the chain
979
+ // callback writes the new session's chain onto it. The generation
980
+ // bump invalidates every wrap still in flight from the ended
981
+ // session, resolved or not. The "connected" that follows re-wraps.
982
+ this.wrapSessionGeneration += 1;
983
+ this.fallbackConnector = undefined;
984
+ this.fallbackProvider = undefined;
985
+ }
910
986
  // Prevent concurrent processing
911
987
  if (this.trackingState.isProcessing) {
912
988
  // Dropped, not queued - but remember that a disconnect went past.
@@ -1443,10 +1519,31 @@ var WagmiEventHandler = /** @class */ (function () {
1443
1519
  */
1444
1520
  WagmiEventHandler.prototype.handleChainChange = function (chainId, prevChainId) {
1445
1521
  return __awaiter(this, void 0, void 0, function () {
1446
- var state, address, hadAddress, wasAnnounced, announcedNow;
1447
- return __generator(this, function (_a) {
1448
- switch (_a.label) {
1522
+ var live, activeConnector, state, address, hadAddress, wasAnnounced, announcedNow;
1523
+ var _a, _b, _c;
1524
+ return __generator(this, function (_d) {
1525
+ switch (_d.label) {
1449
1526
  case 0:
1527
+ // Keep the fallback-wrapped provider's registry chain in step with the
1528
+ // store, so request-derived events are labelled correctly even when the
1529
+ // provider exposes no synchronous chainId. Only when the report is
1530
+ // about the connection that provider belongs to: after a connector
1531
+ // switch the store's chain describes the NEW connection, and writing it
1532
+ // to the old provider would mislabel that wallet's events.
1533
+ if (this.fallbackProvider !== undefined) {
1534
+ live = this.getState();
1535
+ activeConnector = live.current
1536
+ ? (_a = live.connections.get(live.current)) === null || _a === void 0 ? void 0 : _a.connector
1537
+ : undefined;
1538
+ if (activeConnector === this.fallbackConnector) {
1539
+ try {
1540
+ (_c = (_b = this.formo)._rememberWagmiProviderChain) === null || _c === void 0 ? void 0 : _c.call(_b, this.fallbackProvider, chainId);
1541
+ }
1542
+ catch (_e) {
1543
+ /* never let bookkeeping break the chain flow */
1544
+ }
1545
+ }
1546
+ }
1450
1547
  if (chainId === prevChainId || chainId === undefined) {
1451
1548
  return [2 /*return*/];
1452
1549
  }
@@ -1492,7 +1589,7 @@ var WagmiEventHandler = /** @class */ (function () {
1492
1589
  });
1493
1590
  return [4 /*yield*/, this.applyChainForTrackedWallet(address, chainId)];
1494
1591
  case 1:
1495
- _a.sent();
1592
+ _d.sent();
1496
1593
  return [2 /*return*/];
1497
1594
  }
1498
1595
  });
@@ -2250,6 +2347,75 @@ var WagmiEventHandler = /** @class */ (function () {
2250
2347
  });
2251
2348
  return found;
2252
2349
  };
2350
+ /**
2351
+ * A specific connector's name and rdns, for the fallback-wrapped
2352
+ * provider's request-derived events.
2353
+ *
2354
+ * The NAME is exactly what the hook path emits for that connector
2355
+ * (`getConnectorName`: the live WalletConnect peer for a WalletConnect
2356
+ * connector, the connector's own name otherwise), so a branded connector
2357
+ * keeps its name on both paths and a session that changes wallets
2358
+ * renames on both. The rdns is the connector's when wagmi knows it as
2359
+ * ONE value (EIP-6963 discovered connectors carry theirs, some as a
2360
+ * one-element list); a connector that matches several rdns values does
2361
+ * not say which one this provider is, so nothing is passed and the
2362
+ * registry keeps the sniffed one.
2363
+ */
2364
+ WagmiEventHandler.prototype.attributionForConnector = function (connector, state) {
2365
+ var name = this.nameForConnector(connector, state);
2366
+ if (!name) {
2367
+ return undefined;
2368
+ }
2369
+ var raw = connector.rdns;
2370
+ var rdns = typeof raw === "string" && raw.length > 0
2371
+ ? raw
2372
+ : Array.isArray(raw) && raw.length === 1 && typeof raw[0] === "string" && raw[0].length > 0
2373
+ ? raw[0]
2374
+ : undefined;
2375
+ return __assign({ name: name }, (rdns && { rdns: rdns }));
2376
+ };
2377
+ /**
2378
+ * Resolver handed to the registry at wrap time, read at each request
2379
+ * start. BOUND to the connector whose `getProvider` resolved to the
2380
+ * wrapped provider, not to whichever connector is current: an app can
2381
+ * keep a wallet client over a previous connector's provider and issue a
2382
+ * request through it after switching, and that request belongs to the
2383
+ * wallet that handles it. The peer name is still read live. A provider
2384
+ * SHARED by two connectors is re-bound when the new connector's
2385
+ * resolution lands (one microtask for an injected connector); a request
2386
+ * issued inside that gap keeps the previous label.
2387
+ */
2388
+ WagmiEventHandler.prototype.boundConnectorAttribution = function (connector) {
2389
+ var _this = this;
2390
+ return function () {
2391
+ if (_this.disposed)
2392
+ return undefined;
2393
+ try {
2394
+ return _this.attributionForConnector(connector, _this.getState());
2395
+ }
2396
+ catch (_a) {
2397
+ return undefined;
2398
+ }
2399
+ };
2400
+ };
2401
+ /** `getConnectorName` for a SPECIFIC connector, current or not. */
2402
+ WagmiEventHandler.prototype.nameForConnector = function (connector, state) {
2403
+ var _a;
2404
+ var cached = walletConnectPeerNames.get(connector);
2405
+ if (cached) {
2406
+ return cached;
2407
+ }
2408
+ var current = state.current
2409
+ ? (_a = state.connections.get(state.current)) === null || _a === void 0 ? void 0 : _a.connector
2410
+ : undefined;
2411
+ if (current === connector) {
2412
+ // Backstop kick, as in getConnectorName; only the current connection
2413
+ // has a session to look up.
2414
+ this.kickWalletConnectPeerLookup(state);
2415
+ }
2416
+ var name = connector.name;
2417
+ return typeof name === "string" && name.length > 0 ? name : undefined;
2418
+ };
2253
2419
  /**
2254
2420
  * Get the connector name from Wagmi state
2255
2421
  */
@@ -2271,30 +2437,10 @@ var WagmiEventHandler = /** @class */ (function () {
2271
2437
  this.kickWalletConnectPeerLookup(state);
2272
2438
  return connector.name;
2273
2439
  };
2274
- /**
2275
- * Start resolving the wallet behind a WalletConnect connection.
2276
- *
2277
- * Fire-and-forget on purpose: emission paths are synchronous by design
2278
- * and must never wait (see the connect marker comment). Called at the
2279
- * START of the status/address flows rather than only at read time - the
2280
- * lookup is one microtask for an initialised connector, and the emission
2281
- * sits behind several awaits, so kicking early usually means even the
2282
- * FIRST connect names the real wallet. When the race is lost the event
2283
- * honestly says "WalletConnect" and every later event names the peer.
2284
- */
2285
- /**
2286
- * Install the request wrapper on the active connector's provider.
2287
- *
2288
- * This is what lets wagmi mode capture IMPERATIVE viem calls
2289
- * (walletClient.sendTransaction / .signMessage / .writeContract / raw
2290
- * request), which create no mutation and were silently lost. Hook-driven
2291
- * calls stay owned by the mutation handlers via the pending-mutation
2292
- * dedup. Fire-and-forget per connection; a provider that cannot be
2293
- * produced simply keeps mutation-only capture.
2294
- */
2295
- WagmiEventHandler.prototype.wrapActiveConnectorProvider = function (state) {
2440
+ WagmiEventHandler.prototype.wrapActiveConnectorProvider = function (state, isRetry) {
2296
2441
  var _this = this;
2297
- var _a, _b;
2442
+ var _a, _b, _c;
2443
+ if (isRetry === void 0) { isRetry = false; }
2298
2444
  // OPT-IN only. Wagmi mode's baseline never touches the signing
2299
2445
  // transport; instrumenting the provider is an explicit integrator
2300
2446
  // decision (options.wagmi.eip1193Fallback), made auditable in
@@ -2303,25 +2449,124 @@ var WagmiEventHandler = /** @class */ (function () {
2303
2449
  if (!optedIn) {
2304
2450
  return;
2305
2451
  }
2452
+ if (state.status !== "connected") {
2453
+ // Status kicks run for every transition; only a connected snapshot
2454
+ // describes a session worth wrapping.
2455
+ return;
2456
+ }
2306
2457
  var connection = state.current
2307
2458
  ? state.connections.get(state.current)
2308
2459
  : undefined;
2309
2460
  var connector = connection === null || connection === void 0 ? void 0 : connection.connector;
2310
- if (!connection ||
2311
- typeof (connector === null || connector === void 0 ? void 0 : connector.getProvider) !== "function" ||
2312
- wagmiWrappedConnections.has(connection)) {
2461
+ if (!connection || typeof (connector === null || connector === void 0 ? void 0 : connector.getProvider) !== "function") {
2313
2462
  return;
2314
2463
  }
2315
- wagmiWrappedConnections.add(connection);
2316
- connector
2317
- .getProvider()
2464
+ var epoch = ((_c = this.wrapEpochs.get(connector)) !== null && _c !== void 0 ? _c : 0) + 1;
2465
+ this.wrapEpochs.set(connector, epoch);
2466
+ var session = this.wrapSessionGeneration;
2467
+ // A store-driven kick resets the retry budget; automatic retries
2468
+ // (below) spend it.
2469
+ if (!isRetry) {
2470
+ this.wrapRetryCounts.delete(connector);
2471
+ }
2472
+ // When the NEWEST attempt fails, older in-flight resolutions have
2473
+ // been epoch-discarded (they may describe an earlier session) and
2474
+ // nothing else would wrap the provider until the next store update.
2475
+ // Recover by re-kicking fresh with live state, bounded so a
2476
+ // persistently failing connector cannot loop: after the budget is
2477
+ // spent, recovery waits for a real store update, which resets it.
2478
+ var retryAfterFailure = function () {
2479
+ var _a;
2480
+ if (_this.disposed)
2481
+ return;
2482
+ if (_this.wrapEpochs.get(connector) !== epoch) {
2483
+ // Superseded: the newer attempt owns recovery.
2484
+ return;
2485
+ }
2486
+ var failures = ((_a = _this.wrapRetryCounts.get(connector)) !== null && _a !== void 0 ? _a : 0) + 1;
2487
+ _this.wrapRetryCounts.set(connector, failures);
2488
+ if (failures > 3)
2489
+ return;
2490
+ var timer = setTimeout(function () {
2491
+ var _a;
2492
+ _this.wrapRetryTimers.delete(timer);
2493
+ if (_this.disposed)
2494
+ return;
2495
+ // Re-kick only while THIS connector is still the active one and
2496
+ // no newer attempt superseded this one. A retry for a connector
2497
+ // the user has left must not fire at the current connector: with
2498
+ // the retry flag it would skip the budget reset and, worse, its
2499
+ // fresh epoch would discard the current connector's own in-flight
2500
+ // resolution.
2501
+ var live = _this.getState();
2502
+ var stillCurrent = live.current !== undefined &&
2503
+ ((_a = live.connections.get(live.current)) === null || _a === void 0 ? void 0 : _a.connector) === connector;
2504
+ if (stillCurrent && _this.wrapEpochs.get(connector) === epoch) {
2505
+ _this.wrapActiveConnectorProvider(live, true);
2506
+ }
2507
+ }, 25 * failures);
2508
+ _this.wrapRetryTimers.add(timer);
2509
+ };
2510
+ var resolution;
2511
+ try {
2512
+ resolution = connector.getProvider();
2513
+ }
2514
+ catch (_d) {
2515
+ // A synchronous throw must not escape into the subscription
2516
+ // callback: address-change handling runs after this kick and a
2517
+ // propagated throw would silently skip it.
2518
+ retryAfterFailure();
2519
+ return;
2520
+ }
2521
+ resolution
2318
2522
  .then(function (provider) {
2319
- var _a, _b;
2320
- (_b = (_a = _this.formo)._wrapWagmiProvider) === null || _b === void 0 ? void 0 : _b.call(_a, provider);
2523
+ var _a, _b, _c, _d;
2524
+ if (_this.wrapEpochs.get(connector) !== epoch) {
2525
+ // A newer kick is in flight or already resolved; this answer may
2526
+ // describe an earlier session. Let the newest one win.
2527
+ return;
2528
+ }
2529
+ // The fallback installs only the request wrapper, so a provider
2530
+ // without a synchronous chainId property would never teach the
2531
+ // registry its chain and its events would carry chain 0. The wagmi
2532
+ // store knows; feed it here and on every later chain change. Read
2533
+ // the chain from LIVE state, not from a snapshot taken before this
2534
+ // asynchronous resolution: a switch that lands while getProvider is
2535
+ // in flight fires handleChainChange before fallbackProvider exists,
2536
+ // so a pre-resolution snapshot would stick as the recorded chain.
2537
+ // Activity is judged by CONNECTOR identity: wagmi replaces the
2538
+ // connection record itself on every account or chain update.
2539
+ var live = _this.getState();
2540
+ // A wrap only describes a CONNECTED session. Status changes kick
2541
+ // this path for every transition, and a resolution landing while
2542
+ // disconnected would re-point the fallback pair at a provider
2543
+ // whose session is over.
2544
+ var stillActive = !_this.disposed &&
2545
+ _this.wrapSessionGeneration === session &&
2546
+ live.status === "connected" &&
2547
+ live.current !== undefined &&
2548
+ ((_a = live.connections.get(live.current)) === null || _a === void 0 ? void 0 : _a.connector) === connector;
2549
+ if (!stillActive) {
2550
+ // The user moved on while getProvider was in flight. Wrapping
2551
+ // now would instrument a provider whose chain nobody feeds -
2552
+ // its requests would report chain 0. Switching back re-kicks.
2553
+ return;
2554
+ }
2555
+ var chainId = (_b = _this.getActiveConnectionChainId(live)) !== null && _b !== void 0 ? _b : live.chainId;
2556
+ var wrapped = (_d = (_c = _this.formo)._wrapWagmiProvider) === null || _d === void 0 ? void 0 : _d.call(_c, provider, typeof chainId === "number" ? chainId : undefined, _this.boundConnectorAttribution(connector));
2557
+ if (wrapped !== true) {
2558
+ // The provider was refused (invalid shape, frozen provider,
2559
+ // torn-down SDK).
2560
+ retryAfterFailure();
2561
+ return;
2562
+ }
2563
+ _this.wrapRetryCounts.delete(connector);
2564
+ _this.fallbackConnector = connector;
2565
+ _this.fallbackProvider = provider;
2321
2566
  })
2322
2567
  .catch(function () {
2323
- // Mutation-only capture remains; retry on the next connection.
2324
- wagmiWrappedConnections.delete(connection);
2568
+ // Mutation-only capture remains.
2569
+ retryAfterFailure();
2325
2570
  });
2326
2571
  };
2327
2572
  WagmiEventHandler.prototype.kickWalletConnectPeerLookup = function (state) {
@@ -2411,6 +2656,8 @@ var WagmiEventHandler = /** @class */ (function () {
2411
2656
  * Clean up all subscriptions
2412
2657
  */
2413
2658
  WagmiEventHandler.prototype.cleanup = function () {
2659
+ this.wrapRetryTimers.forEach(function (t) { return clearTimeout(t); });
2660
+ this.wrapRetryTimers.clear();
2414
2661
  logger_1.logger.debug("WagmiEventHandler: Cleaning up subscriptions");
2415
2662
  if (!this.disposed) {
2416
2663
  this.disposed = true;
@@ -31,6 +31,12 @@ export interface WagmiConnector {
31
31
  name: string;
32
32
  type: string;
33
33
  uid: string;
34
+ /**
35
+ * Any connector implementation may expose an rdns; EIP-6963 discovered
36
+ * connectors are one source. A connector matching several rdns values
37
+ * lists them all.
38
+ */
39
+ rdns?: string | readonly string[];
34
40
  }
35
41
  /**
36
42
  * Wagmi config interface
@@ -192,7 +192,15 @@ function buildSafeFunctionArgs(functionArgs, reservedFields) {
192
192
  var result = {};
193
193
  for (var _i = 0, _a = Object.entries(functionArgs); _i < _a.length; _i++) {
194
194
  var _b = _a[_i], key = _b[0], val = _b[1];
195
- var safeKey = reservedFields.has(key) ? "arg_".concat(key) : key;
195
+ // A prefixed key must not land on another argument's own name: an ABI
196
+ // with both `to` and `arg_to` would otherwise keep only one of them,
197
+ // whichever came later. Keep prefixing until the name is free.
198
+ var safeKey = key;
199
+ if (reservedFields.has(key)) {
200
+ do {
201
+ safeKey = "arg_".concat(safeKey);
202
+ } while (safeKey in functionArgs);
203
+ }
196
204
  result[safeKey] = val;
197
205
  // If the value is a nested object (struct), flatten it
198
206
  // Skip flattened keys that would overwrite existing top-level args
@@ -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;
@@ -1015,6 +1015,9 @@ var FormoAnalytics = /** @class */ (function () {
1015
1015
  // Drop anything already buffered so a pending timer/pagehide flush
1016
1016
  // cannot ship events after consent withdrawal.
1017
1017
  this.eventManager.clear();
1018
+ // Identity is purged below; registered sessions must be re-learned
1019
+ // on opt-in, and nothing else would retry an already-adopted one.
1020
+ this.evmEvents.markRegisteredAdoptionsPending();
1018
1021
  this.reset();
1019
1022
  logger.info("Successfully opted out of tracking");
1020
1023
  };
@@ -1334,9 +1337,9 @@ var FormoAnalytics = /** @class */ (function () {
1334
1337
  *
1335
1338
  * With several live SDK instances (multi write-key pages) registering
1336
1339
  * the SAME provider, request-derived events (signatures, transactions)
1337
- * go to the most recently registered live instance - the same
1338
- * single-observer semantics discovery has always had for the request
1339
- * wrapper. Lifecycle events (connect, chain, disconnect) reach every
1340
+ * go to the most recently CREATED live instance, regardless of the
1341
+ * order registrations happen to land in - the same single-observer
1342
+ * semantics discovery has always had for the request wrapper. Lifecycle events (connect, chain, disconnect) reach every
1340
1343
  * instance. Fanning request observations out to all instances is a
1341
1344
  * separate feature.
1342
1345
  *
@@ -1360,15 +1363,43 @@ var FormoAnalytics = /** @class */ (function () {
1360
1363
  * gap. Lifecycle (connect/chain/disconnect) stays store-driven: only the
1361
1364
  * request wrapper installs here. Double counting is prevented in the
1362
1365
  * wrapper via `shouldSkipRequestCapture`.
1366
+ *
1367
+ * `attribution` resolves, live, the name and rdns of the connector this
1368
+ * provider was wrapped for. Request-derived events are named from the
1369
+ * registry, which for an unannounced provider falls back to flag
1370
+ * sniffing; recording the connector's resolver here keeps them in
1371
+ * agreement with the hook-driven events for that connector.
1363
1372
  */
1364
- FormoAnalytics.prototype._wrapWagmiProvider = function (provider) {
1373
+ FormoAnalytics.prototype._wrapWagmiProvider = function (provider, chainId, attribution) {
1365
1374
  if (this.isCleanedUp || !isValidProvider(provider))
1366
- return;
1375
+ return false;
1367
1376
  try {
1368
- this.evmRequests.registerRequestListeners(provider);
1377
+ // The tracker reports refusals (frozen provider, unrebindable
1378
+ // wrapper) by returning false rather than throwing; treat those as
1379
+ // failures too so the caller can retry later.
1380
+ if (!this.evmRequests.registerRequestListeners(provider)) {
1381
+ return false;
1382
+ }
1383
+ this.evm.rememberAttribution(provider, attribution);
1384
+ if (chainId !== undefined) {
1385
+ this.evm.rememberChain(provider, chainId);
1386
+ }
1387
+ return true;
1369
1388
  }
1370
1389
  catch (e) {
1371
1390
  logger.warn("Failed to wrap wagmi provider for hybrid capture", e);
1391
+ return false;
1392
+ }
1393
+ };
1394
+ /** INTERNAL. Chain updates for the fallback-wrapped provider. */
1395
+ FormoAnalytics.prototype._rememberWagmiProviderChain = function (provider, chainId) {
1396
+ if (this.isCleanedUp)
1397
+ return;
1398
+ try {
1399
+ this.evm.rememberChain(provider, chainId);
1400
+ }
1401
+ catch (_a) {
1402
+ /* bookkeeping only */
1372
1403
  }
1373
1404
  };
1374
1405
  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
@@ -69,6 +69,44 @@ var defaultContext = {
69
69
  hasOptedOutTracking: function () { return false; },
70
70
  };
71
71
  export var FormoAnalyticsContext = createContext(defaultContext);
72
+ /**
73
+ * A stable key over the serializable parts of Options. The provider effect
74
+ * re-initialises the SDK when this key changes; anything that alters SDK
75
+ * behaviour must be represented here, or a runtime change to it is silently
76
+ * ignored. Complex objects are tracked by presence, plus the flags that
77
+ * change what the SDK does with them.
78
+ */
79
+ export var computeOptionsKey = function (options) {
80
+ var _a;
81
+ if (!options)
82
+ return 'undefined';
83
+ var serializableOptions = {
84
+ tracking: options.tracking,
85
+ autocapture: options.autocapture,
86
+ crossSubdomainCookies: options.crossSubdomainCookies,
87
+ apiHost: options.apiHost,
88
+ flushAt: options.flushAt,
89
+ flushInterval: options.flushInterval,
90
+ retryCount: options.retryCount,
91
+ maxQueueSize: options.maxQueueSize,
92
+ logger: options.logger,
93
+ referral: options.referral,
94
+ evm: options.evm,
95
+ // For complex objects, just track their presence, not their content
96
+ hasProvider: !!options.provider,
97
+ hasWagmi: !!options.wagmi,
98
+ wagmiEip1193Fallback: !!((_a = options.wagmi) === null || _a === void 0 ? void 0 : _a.eip1193Fallback),
99
+ hasReady: !!options.ready,
100
+ };
101
+ try {
102
+ return JSON.stringify(serializableOptions);
103
+ }
104
+ catch (error) {
105
+ // Fallback to timestamp if serialization fails
106
+ logger.warn('Failed to serialize options, using timestamp', error);
107
+ return Date.now().toString();
108
+ }
109
+ };
72
110
  export var FormoAnalyticsProvider = function (props) {
73
111
  var writeKey = props.writeKey, _a = props.disabled, disabled = _a === void 0 ? false : _a, children = props.children;
74
112
  // Keep the app running without analytics if no Write Key is provided or disabled
@@ -87,37 +125,7 @@ var InitializedAnalytics = function (_a) {
87
125
  var _b = useState(defaultContext), sdk = _b[0], setSdk = _b[1];
88
126
  var sdkRef = useRef(defaultContext);
89
127
  initStorageManager(writeKey);
90
- // Create a stable key from options that ignores complex objects and functions
91
- // We only care about serializable config values that would affect SDK behavior
92
- var optionsKey = useMemo(function () {
93
- if (!options)
94
- return 'undefined';
95
- // Extract only the serializable parts of options
96
- var serializableOptions = {
97
- tracking: options.tracking,
98
- autocapture: options.autocapture,
99
- crossSubdomainCookies: options.crossSubdomainCookies,
100
- apiHost: options.apiHost,
101
- flushAt: options.flushAt,
102
- flushInterval: options.flushInterval,
103
- retryCount: options.retryCount,
104
- maxQueueSize: options.maxQueueSize,
105
- logger: options.logger,
106
- referral: options.referral,
107
- // For complex objects, just track their presence, not their content
108
- hasProvider: !!options.provider,
109
- hasWagmi: !!options.wagmi,
110
- hasReady: !!options.ready,
111
- };
112
- try {
113
- return JSON.stringify(serializableOptions);
114
- }
115
- catch (error) {
116
- // Fallback to timestamp if serialization fails
117
- logger.warn('Failed to serialize options, using timestamp', error);
118
- return Date.now().toString();
119
- }
120
- }, [options]);
128
+ var optionsKey = useMemo(function () { return computeOptionsKey(options); }, [options]);
121
129
  useEffect(function () {
122
130
  var isCleanedUp = false;
123
131
  var initialize = function () { return __awaiter(void 0, void 0, void 0, function () {