@formo/analytics 1.37.0 → 1.38.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 (40) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +64 -0
  2. package/dist/cjs/src/FormoAnalytics.js +155 -5
  3. package/dist/cjs/src/FormoAnalyticsProvider.js +1 -0
  4. package/dist/cjs/src/evm/EvmEventTracker.d.ts +38 -10
  5. package/dist/cjs/src/evm/EvmEventTracker.js +182 -0
  6. package/dist/cjs/src/evm/EvmProviderRegistry.js +26 -4
  7. package/dist/cjs/src/evm/EvmRequestTracker.d.ts +28 -10
  8. package/dist/cjs/src/evm/EvmRequestTracker.js +199 -55
  9. package/dist/cjs/src/provider/detection.d.ts +30 -0
  10. package/dist/cjs/src/provider/detection.js +62 -0
  11. package/dist/cjs/src/provider/index.d.ts +1 -1
  12. package/dist/cjs/src/provider/index.js +3 -1
  13. package/dist/cjs/src/types/base.d.ts +23 -0
  14. package/dist/cjs/src/types/provider.d.ts +16 -0
  15. package/dist/cjs/src/types/provider.js +17 -1
  16. package/dist/cjs/src/version.d.ts +1 -1
  17. package/dist/cjs/src/version.js +1 -1
  18. package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +44 -0
  19. package/dist/cjs/src/wagmi/WagmiEventHandler.js +306 -34
  20. package/dist/esm/src/FormoAnalytics.d.ts +64 -0
  21. package/dist/esm/src/FormoAnalytics.js +155 -5
  22. package/dist/esm/src/FormoAnalyticsProvider.js +1 -0
  23. package/dist/esm/src/evm/EvmEventTracker.d.ts +38 -10
  24. package/dist/esm/src/evm/EvmEventTracker.js +183 -1
  25. package/dist/esm/src/evm/EvmProviderRegistry.js +27 -5
  26. package/dist/esm/src/evm/EvmRequestTracker.d.ts +28 -10
  27. package/dist/esm/src/evm/EvmRequestTracker.js +198 -54
  28. package/dist/esm/src/provider/detection.d.ts +30 -0
  29. package/dist/esm/src/provider/detection.js +60 -0
  30. package/dist/esm/src/provider/index.d.ts +1 -1
  31. package/dist/esm/src/provider/index.js +1 -1
  32. package/dist/esm/src/types/base.d.ts +23 -0
  33. package/dist/esm/src/types/provider.d.ts +16 -0
  34. package/dist/esm/src/types/provider.js +16 -0
  35. package/dist/esm/src/version.d.ts +1 -1
  36. package/dist/esm/src/version.js +1 -1
  37. package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +44 -0
  38. package/dist/esm/src/wagmi/WagmiEventHandler.js +306 -34
  39. package/dist/index.umd.min.js +1 -1
  40. package/package.json +2 -2
@@ -5,6 +5,13 @@ import { AutocaptureEventType } from "../tracking/TrackingPolicy";
5
5
  /** What the request tracker needs from the SDK that owns it. */
6
6
  export interface EvmRequestTrackerDeps {
7
7
  isAutocaptureEnabled(eventType: AutocaptureEventType): boolean;
8
+ /**
9
+ * Hybrid-capture dedup: true when a PENDING wagmi mutation already covers
10
+ * this request, so the mutation handler owns the capture. TanStack sets a
11
+ * mutation pending BEFORE its mutationFn issues the wallet call, so a
12
+ * hook-driven request always matches; an imperative one never does.
13
+ */
14
+ shouldSkipRequestCapture?(method: string, params: unknown[]): boolean;
8
15
  signature(params: {
9
16
  status: SignatureStatus;
10
17
  chainId?: ChainID;
@@ -23,16 +30,6 @@ export interface EvmRequestTrackerDeps {
23
30
  function_args?: Record<string, unknown>;
24
31
  }, properties?: IFormoEventProperties): Promise<void>;
25
32
  }
26
- /**
27
- * Autocapture for signatures and transactions, by wrapping a provider's
28
- * `request`.
29
- *
30
- * The wrapper is deliberately thin: it observes the call the dapp was already
31
- * making and never issues one of its own. That rule is why the chain a
32
- * request ran on is read from `eth_chainId` calls the app makes, rather than
33
- * probed - an SDK-issued lookup on a serialising transport can wedge the
34
- * user's wallet, which is never an acceptable price for a label.
35
- */
36
33
  export declare class EvmRequestTracker {
37
34
  private readonly wallet;
38
35
  private readonly registry;
@@ -50,6 +47,8 @@ export declare class EvmRequestTracker {
50
47
  constructor(wallet: WalletStateStore, registry: EvmProviderRegistry, deps: EvmRequestTrackerDeps);
51
48
  /** Stop every poll in flight. Terminal, like the event queue's close(). */
52
49
  cleanup(): void;
50
+ /** Providers whose owner list includes this instance; pruned on cleanup. */
51
+ private wrappedProviders;
53
52
  /** Re-arm a poll, unless this tracker has been torn down. */
54
53
  private schedulePoll;
55
54
  /**
@@ -59,6 +58,25 @@ export declare class EvmRequestTracker {
59
58
  * would be missed for the rest of the session.
60
59
  */
61
60
  registerRequestListeners(provider: EIP1193Provider): boolean;
61
+ /** Install the wrapper function onto the provider; separated so the
62
+ * routing shim above stays small. */
63
+ private installWrappedRequest;
64
+ /**
65
+ * The wrapper body proper: everything a request observation does, run
66
+ * against THIS instance's registry, wallet state, and event queue. Kept
67
+ * as a method so a surviving wrapper installed by a previous SDK
68
+ * instance can hand calls to the current one.
69
+ */
70
+ private dispatchWrappedRequest;
71
+ /**
72
+ * Wallet attribution for request-derived events.
73
+ *
74
+ * Live per read through the registry, so a WalletConnect session names
75
+ * its actual signer ("MetaMask Wallet", "Ledger Live") - the live-test
76
+ * rows had provider_name EMPTY on every signature and transaction, which
77
+ * made per-wallet activity unanswerable in the warehouse.
78
+ */
79
+ private attributionFor;
62
80
  private buildSignatureEventPayload;
63
81
  private buildTransactionEventPayload;
64
82
  /**
@@ -57,9 +57,19 @@ var __rest = (this && this.__rest) || function (s, e) {
57
57
  }
58
58
  return t;
59
59
  };
60
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
61
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
62
+ if (ar || !(i in from)) {
63
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
64
+ ar[i] = from[i];
65
+ }
66
+ }
67
+ return to.concat(ar || Array.prototype.slice.call(from));
68
+ };
60
69
  Object.defineProperty(exports, "__esModule", { value: true });
61
70
  exports.EvmRequestTracker = void 0;
62
71
  var logger_1 = require("../logger");
72
+ var provider_1 = require("../provider");
63
73
  var chain_1 = require("../utils/chain");
64
74
  var address_1 = require("../utils/address");
65
75
  var types_1 = require("../types");
@@ -100,6 +110,19 @@ function hexToUtf8(hex) {
100
110
  * probed - an SDK-issued lookup on a serialising transport can wedge the
101
111
  * user's wallet, which is never an acceptable price for a label.
102
112
  */
113
+ /**
114
+ * Providers with a dispatch already on the synchronous call stack.
115
+ *
116
+ * A page can end up with LAYERED Formo wrappers: another library wraps our
117
+ * wrapper, the SDK rebuilds, and the new instance wraps the outer function
118
+ * because the marker is not on it. Both layers route to the same newest
119
+ * live tracker, which would instrument one user request twice. Every
120
+ * dispatch issues its underlying request synchronously (deliberately -
121
+ * the wallet call always goes out first), so an inner shim reached during
122
+ * that window belongs to the SAME user request and passes straight
123
+ * through.
124
+ */
125
+ var dispatchInFlight = new WeakSet();
103
126
  var EvmRequestTracker = /** @class */ (function () {
104
127
  function EvmRequestTracker(wallet, registry, deps) {
105
128
  this.wallet = wallet;
@@ -115,12 +138,28 @@ var EvmRequestTracker = /** @class */ (function () {
115
138
  */
116
139
  this.polls = new Set();
117
140
  this.disposed = false;
141
+ /** Providers whose owner list includes this instance; pruned on cleanup. */
142
+ this.wrappedProviders = new Set();
118
143
  }
119
144
  /** Stop every poll in flight. Terminal, like the event queue's close(). */
120
145
  EvmRequestTracker.prototype.cleanup = function () {
146
+ var _this = this;
121
147
  this.disposed = true;
122
148
  this.polls.forEach(function (timer) { return clearTimeout(timer); });
123
149
  this.polls.clear();
150
+ // Remove this instance from every provider's owner list. The lists
151
+ // live on LONG-LIVED provider objects; leaving disposed trackers in
152
+ // them retains each old instance's whole object graph across rebuilds,
153
+ // growing without bound under HMR.
154
+ this.wrappedProviders.forEach(function (provider) {
155
+ var owners = provider[types_1.WRAPPED_REQUEST_OWNER_SYMBOL];
156
+ if (Array.isArray(owners)) {
157
+ var idx = owners.indexOf(_this);
158
+ if (idx !== -1)
159
+ owners.splice(idx, 1);
160
+ }
161
+ });
162
+ this.wrappedProviders.clear();
124
163
  };
125
164
  /** Re-arm a poll, unless this tracker has been torn down. */
126
165
  EvmRequestTracker.prototype.schedulePoll = function (fn, delayMs) {
@@ -146,19 +185,118 @@ var EvmRequestTracker = /** @class */ (function () {
146
185
  logger_1.logger.error("Provider not found for request (signature, transaction) tracking");
147
186
  return false;
148
187
  }
149
- // Check if the provider is already wrapped with our SDK's wrapper
188
+ // Already wrapped: take OWNERSHIP rather than skipping. The wrapper
189
+ // survives an SDK rebuild and closes over the instance that installed
190
+ // it, whose queue is closed after cleanup - "skip" made the rebuilt
191
+ // instance report success while every request-derived event died in
192
+ // the dead instance's queue. The wrapper reads the owner slot per call.
150
193
  var currentRequest = provider.request;
151
194
  if (this.registry.isWrapped(provider, currentRequest)) {
152
- logger_1.logger.info("Provider already wrapped with our SDK; skipping request wrapping.");
195
+ var owners = provider[types_1.WRAPPED_REQUEST_OWNER_SYMBOL];
196
+ if (!Array.isArray(owners)) {
197
+ // A wrapper without its owner list cannot be taken over, and
198
+ // claiming success would silence every request event.
199
+ logger_1.logger.warn("wrapped without owner list; cannot rebind");
200
+ return false;
201
+ }
202
+ var idx = owners.indexOf(this);
203
+ if (idx !== -1)
204
+ owners.splice(idx, 1);
205
+ owners.push(this);
206
+ this.wrappedProviders.add(provider);
207
+ logger_1.logger.info("Provider already wrapped; rebinding the wrapper to this instance.");
153
208
  return true;
154
209
  }
155
210
  var request = provider.request.bind(provider);
156
211
  var wrappedRequest = function (_a) { return __awaiter(_this, [_a], void 0, function (_b) {
212
+ var owners, liveOwner;
213
+ var method = _b.method, params = _b.params;
214
+ return __generator(this, function (_c) {
215
+ owners = provider[types_1.WRAPPED_REQUEST_OWNER_SYMBOL];
216
+ if (dispatchInFlight.has(provider)) {
217
+ // An outer Formo wrapper is already instrumenting this very
218
+ // request; this layer only forwards.
219
+ return [2 /*return*/, request({ method: method, params: params })];
220
+ }
221
+ liveOwner = Array.isArray(owners)
222
+ ? __spreadArray([], owners, true).reverse().find(function (o) { return !o.disposed; })
223
+ : undefined;
224
+ dispatchInFlight.add(provider);
225
+ try {
226
+ return [2 /*return*/, (liveOwner !== null && liveOwner !== void 0 ? liveOwner : this).dispatchWrappedRequest({ method: method, params: params }, provider, request)];
227
+ }
228
+ finally {
229
+ // Cleared as soon as the dispatch call RETURNS its promise: the
230
+ // underlying request has been issued synchronously by then, so
231
+ // the window covers exactly the nested layers of this one call
232
+ // and never a concurrent request.
233
+ dispatchInFlight.delete(provider);
234
+ }
235
+ return [2 /*return*/];
236
+ });
237
+ }); };
238
+ try {
239
+ // MERGE with any existing list rather than overwriting: a wallet that
240
+ // replaced `request` forces a re-wrap, and discarding the prior list
241
+ // would drop other live instances from ownership - the newest-live
242
+ // fallback then has nobody to fall back to.
243
+ var slot = provider;
244
+ var prior = slot[types_1.WRAPPED_REQUEST_OWNER_SYMBOL];
245
+ var owners = Array.isArray(prior) ? prior : [];
246
+ var idx = owners.indexOf(this);
247
+ if (idx !== -1)
248
+ owners.splice(idx, 1);
249
+ owners.push(this);
250
+ if (!Array.isArray(prior)) {
251
+ slot[types_1.WRAPPED_REQUEST_OWNER_SYMBOL] = owners;
252
+ }
253
+ this.wrappedProviders.add(provider);
254
+ }
255
+ catch (_a) {
256
+ /* frozen provider: the request write below fails too and aborts */
257
+ }
258
+ return this.installWrappedRequest(provider, wrappedRequest);
259
+ };
260
+ /** Install the wrapper function onto the provider; separated so the
261
+ * routing shim above stays small. */
262
+ EvmRequestTracker.prototype.installWrappedRequest = function (provider, wrappedRequest) {
263
+ // Mark the wrapper so we can detect if request is replaced externally and keep a reference on provider
264
+ wrappedRequest[types_1.WRAPPED_REQUEST_SYMBOL] = true;
265
+ // Both writes go inside the try. A frozen or non-extensible provider
266
+ // throws on the symbol assignment just as readily as on `request`, and
267
+ // that one used to sit outside the guard, so an unwrappable provider
268
+ // aborted registration instead of being skipped.
269
+ try {
270
+ provider[types_1.WRAPPED_REQUEST_REF_SYMBOL] =
271
+ wrappedRequest;
272
+ provider.request = wrappedRequest;
273
+ // Read back: an accessor or Proxy can ACCEPT the assignment without
274
+ // installing it, and success here is a promise that capture works.
275
+ if (provider.request !== wrappedRequest) {
276
+ logger_1.logger.warn("request assignment swallowed; not wrapped");
277
+ return false;
278
+ }
279
+ return true;
280
+ }
281
+ catch (e) {
282
+ logger_1.logger.warn("Failed to wrap provider.request; skipping", e);
283
+ return false;
284
+ }
285
+ };
286
+ /**
287
+ * The wrapper body proper: everything a request observation does, run
288
+ * against THIS instance's registry, wallet state, and event queue. Kept
289
+ * as a method so a surviving wrapper installed by a previous SDK
290
+ * instance can hand calls to the current one.
291
+ */
292
+ EvmRequestTracker.prototype.dispatchWrappedRequest = function (_a, provider_2, request_1) {
293
+ return __awaiter(this, arguments, void 0, function (_b, provider, request) {
157
294
  var generation_1, responsePromise, capturedChainId_1, response_1, error_1, rpcError, txPromise, txChainId_1, transactionHash_1, error_2, rpcError;
158
295
  var _this = this;
296
+ var _c, _d, _e, _f;
159
297
  var method = _b.method, params = _b.params;
160
- return __generator(this, function (_c) {
161
- switch (_c.label) {
298
+ return __generator(this, function (_g) {
299
+ switch (_g.label) {
162
300
  case 0:
163
301
  // Learn the chain from a call the APP was making anyway.
164
302
  //
@@ -187,6 +325,10 @@ var EvmRequestTracker = /** @class */ (function () {
187
325
  logger_1.logger.debug("Signature event skipped (autocapture.signature: false)", { method: method });
188
326
  return [2 /*return*/, request({ method: method, params: params })];
189
327
  }
328
+ if ((_d = (_c = this.deps).shouldSkipRequestCapture) === null || _d === void 0 ? void 0 : _d.call(_c, method, params)) {
329
+ // A pending wagmi mutation owns this capture.
330
+ return [2 /*return*/, request({ method: method, params: params })];
331
+ }
190
332
  responsePromise = request({ method: method, params: params });
191
333
  // Attach a no-op handler now so a rejection arriving before the await
192
334
  // below is never reported as unhandled. The real handling is there.
@@ -199,7 +341,7 @@ var EvmRequestTracker = /** @class */ (function () {
199
341
  switch (_a.label) {
200
342
  case 0:
201
343
  _a.trys.push([0, 2, , 3]);
202
- return [4 /*yield*/, this.deps.signature(__assign({ status: types_1.SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
344
+ return [4 /*yield*/, this.deps.signature(__assign({ status: types_1.SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)), this.attributionFor(provider))];
203
345
  case 1:
204
346
  _a.sent();
205
347
  return [3 /*break*/, 3];
@@ -211,12 +353,12 @@ var EvmRequestTracker = /** @class */ (function () {
211
353
  }
212
354
  });
213
355
  }); })();
214
- _c.label = 1;
356
+ _g.label = 1;
215
357
  case 1:
216
- _c.trys.push([1, 3, , 4]);
358
+ _g.trys.push([1, 3, , 4]);
217
359
  return [4 /*yield*/, responsePromise];
218
360
  case 2:
219
- response_1 = _c.sent();
361
+ response_1 = _g.sent();
220
362
  // Track signature confirmation only for truthy responses
221
363
  if (response_1) {
222
364
  (function () { return __awaiter(_this, void 0, void 0, function () {
@@ -225,7 +367,7 @@ var EvmRequestTracker = /** @class */ (function () {
225
367
  switch (_a.label) {
226
368
  case 0:
227
369
  _a.trys.push([0, 2, , 3]);
228
- return [4 /*yield*/, this.deps.signature(__assign({ status: types_1.SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)))];
370
+ return [4 /*yield*/, this.deps.signature(__assign({ status: types_1.SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)), this.attributionFor(provider))];
229
371
  case 1:
230
372
  _a.sent();
231
373
  return [3 /*break*/, 3];
@@ -240,9 +382,9 @@ var EvmRequestTracker = /** @class */ (function () {
240
382
  }
241
383
  return [2 /*return*/, response_1];
242
384
  case 3:
243
- error_1 = _c.sent();
385
+ error_1 = _g.sent();
244
386
  rpcError = error_1;
245
- if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
387
+ if ((0, provider_1.isUserRejectionError)(rpcError)) {
246
388
  // Use the already cast rpcError to avoid duplication
247
389
  (function () { return __awaiter(_this, void 0, void 0, function () {
248
390
  var e_3;
@@ -250,7 +392,7 @@ var EvmRequestTracker = /** @class */ (function () {
250
392
  switch (_a.label) {
251
393
  case 0:
252
394
  _a.trys.push([0, 2, , 3]);
253
- return [4 /*yield*/, this.deps.signature(__assign({ status: types_1.SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
395
+ return [4 /*yield*/, this.deps.signature(__assign({ status: types_1.SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)), this.attributionFor(provider))];
254
396
  case 1:
255
397
  _a.sent();
256
398
  return [3 /*break*/, 3];
@@ -282,6 +424,10 @@ var EvmRequestTracker = /** @class */ (function () {
282
424
  logger_1.logger.debug("Transaction event skipped (autocapture.transaction: false)", { method: method });
283
425
  return [2 /*return*/, request({ method: method, params: params })];
284
426
  }
427
+ if ((_f = (_e = this.deps).shouldSkipRequestCapture) === null || _f === void 0 ? void 0 : _f.call(_e, method, params)) {
428
+ // A pending wagmi mutation owns this capture.
429
+ return [2 /*return*/, request({ method: method, params: params })];
430
+ }
285
431
  txPromise = request({ method: method, params: params });
286
432
  txPromise.catch(function () { return undefined; });
287
433
  txChainId_1 = this.registry.resolveChainId(provider);
@@ -294,7 +440,7 @@ var EvmRequestTracker = /** @class */ (function () {
294
440
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
295
441
  case 1:
296
442
  payload = _a.sent();
297
- return [4 /*yield*/, this.deps.transaction(__assign({ status: types_1.TransactionStatus.STARTED }, payload))];
443
+ return [4 /*yield*/, this.deps.transaction(__assign({ status: types_1.TransactionStatus.STARTED }, payload), this.attributionFor(provider))];
298
444
  case 2:
299
445
  _a.sent();
300
446
  return [3 /*break*/, 4];
@@ -306,12 +452,12 @@ var EvmRequestTracker = /** @class */ (function () {
306
452
  }
307
453
  });
308
454
  }); })();
309
- _c.label = 5;
455
+ _g.label = 5;
310
456
  case 5:
311
- _c.trys.push([5, 7, , 8]);
457
+ _g.trys.push([5, 7, , 8]);
312
458
  return [4 /*yield*/, txPromise];
313
459
  case 6:
314
- transactionHash_1 = _c.sent();
460
+ transactionHash_1 = _g.sent();
315
461
  (function () { return __awaiter(_this, void 0, void 0, function () {
316
462
  var payload, e_5;
317
463
  return __generator(this, function (_a) {
@@ -321,7 +467,7 @@ var EvmRequestTracker = /** @class */ (function () {
321
467
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
322
468
  case 1:
323
469
  payload = _a.sent();
324
- return [4 /*yield*/, this.deps.transaction(__assign(__assign({ status: types_1.TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }))];
470
+ return [4 /*yield*/, this.deps.transaction(__assign(__assign({ status: types_1.TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }), this.attributionFor(provider))];
325
471
  case 2:
326
472
  _a.sent();
327
473
  // Start async polling for transaction receipt
@@ -337,9 +483,9 @@ var EvmRequestTracker = /** @class */ (function () {
337
483
  }); })();
338
484
  return [2 /*return*/, transactionHash_1];
339
485
  case 7:
340
- error_2 = _c.sent();
486
+ error_2 = _g.sent();
341
487
  rpcError = error_2;
342
- if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
488
+ if ((0, provider_1.isUserRejectionError)(rpcError)) {
343
489
  // Use the already cast rpcError to avoid duplication
344
490
  (function () { return __awaiter(_this, void 0, void 0, function () {
345
491
  var payload, e_6;
@@ -350,7 +496,7 @@ var EvmRequestTracker = /** @class */ (function () {
350
496
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
351
497
  case 1:
352
498
  payload = _a.sent();
353
- return [4 /*yield*/, this.deps.transaction(__assign({ status: types_1.TransactionStatus.REJECTED }, payload))];
499
+ return [4 /*yield*/, this.deps.transaction(__assign({ status: types_1.TransactionStatus.REJECTED }, payload), this.attributionFor(provider))];
354
500
  case 2:
355
501
  _a.sent();
356
502
  return [3 /*break*/, 4];
@@ -367,23 +513,19 @@ var EvmRequestTracker = /** @class */ (function () {
367
513
  case 8: return [2 /*return*/, request({ method: method, params: params })];
368
514
  }
369
515
  });
370
- }); };
371
- // Mark the wrapper so we can detect if request is replaced externally and keep a reference on provider
372
- wrappedRequest[types_1.WRAPPED_REQUEST_SYMBOL] = true;
373
- // Both writes go inside the try. A frozen or non-extensible provider
374
- // throws on the symbol assignment just as readily as on `request`, and
375
- // that one used to sit outside the guard, so an unwrappable provider
376
- // aborted registration instead of being skipped.
377
- try {
378
- provider[types_1.WRAPPED_REQUEST_REF_SYMBOL] =
379
- wrappedRequest;
380
- provider.request = wrappedRequest;
381
- return true;
382
- }
383
- catch (e) {
384
- logger_1.logger.warn("Failed to wrap provider.request; skipping", e);
385
- return false;
386
- }
516
+ });
517
+ };
518
+ /**
519
+ * Wallet attribution for request-derived events.
520
+ *
521
+ * Live per read through the registry, so a WalletConnect session names
522
+ * its actual signer ("MetaMask Wallet", "Ledger Live") - the live-test
523
+ * rows had provider_name EMPTY on every signature and transaction, which
524
+ * made per-wallet activity unanswerable in the warehouse.
525
+ */
526
+ EvmRequestTracker.prototype.attributionFor = function (provider) {
527
+ var info = this.registry.infoFor(provider);
528
+ return { providerName: info.name, rdns: info.rdns };
387
529
  };
388
530
  EvmRequestTracker.prototype.buildSignatureEventPayload = function (method, params,
389
531
  // Intentionally not read. Kept for positional call-site arity.
@@ -449,7 +591,7 @@ var EvmRequestTracker = /** @class */ (function () {
449
591
  /**
450
592
  * Polls for transaction receipt and emits tx.status = CONFIRMED or REVERTED.
451
593
  */
452
- EvmRequestTracker.prototype.pollTransactionReceipt = function (provider_1, transactionHash_2, payload_1) {
594
+ EvmRequestTracker.prototype.pollTransactionReceipt = function (provider_2, transactionHash_2, payload_1) {
453
595
  return __awaiter(this, arguments, void 0, function (provider, transactionHash, payload, maxAttempts, intervalMs) {
454
596
  var attempts, poll;
455
597
  var _this = this;
@@ -479,7 +621,7 @@ var EvmRequestTracker = /** @class */ (function () {
479
621
  // status: 1 = success, 0 = reverted
480
622
  if (receipt.status === "0x1" || receipt.status === 1) {
481
623
  this.deps
482
- .transaction(__assign(__assign({ status: types_1.TransactionStatus.CONFIRMED }, payload), { transactionHash: transactionHash }))
624
+ .transaction(__assign(__assign({ status: types_1.TransactionStatus.CONFIRMED }, payload), { transactionHash: transactionHash }), this.attributionFor(provider))
483
625
  .catch(function (e) {
484
626
  return logger_1.logger.error("Formo: Failed to track transaction confirmation", e);
485
627
  });
@@ -487,7 +629,7 @@ var EvmRequestTracker = /** @class */ (function () {
487
629
  }
488
630
  else if (receipt.status === "0x0" || receipt.status === 0) {
489
631
  this.deps
490
- .transaction(__assign(__assign({ status: types_1.TransactionStatus.REVERTED }, payload), { transactionHash: transactionHash }))
632
+ .transaction(__assign(__assign({ status: types_1.TransactionStatus.REVERTED }, payload), { transactionHash: transactionHash }), this.attributionFor(provider))
491
633
  .catch(function (e) {
492
634
  return logger_1.logger.error("Formo: Failed to track transaction revert", e);
493
635
  });
@@ -530,10 +672,10 @@ var EvmRequestTracker = /** @class */ (function () {
530
672
  */
531
673
  EvmRequestTracker.prototype.trackBatchedCalls = function (provider, request, params) {
532
674
  return __awaiter(this, void 0, void 0, function () {
533
- var sendPromise, batch, calls, declared, chainId, address, payloads, _i, payloads_1, p, properties, rest, result, batchId, _a, payloads_2, p, properties, rest, error_3, rpcError, _b, payloads_3, p, properties, rest;
534
- var _c;
535
- return __generator(this, function (_d) {
536
- switch (_d.label) {
675
+ var sendPromise, batch, calls, declared, chainId, address, attribution, payloads, _i, payloads_1, p, properties, rest, result, batchId, _a, payloads_2, p, properties, rest, error_3, rpcError, _b, payloads_3, p, properties, rest;
676
+ var _c, _d, _e;
677
+ return __generator(this, function (_f) {
678
+ switch (_f.label) {
537
679
  case 0:
538
680
  if (!this.deps.isAutocaptureEnabled("transaction")) {
539
681
  logger_1.logger.debug("Transaction event skipped (autocapture.transaction: false)", {
@@ -541,13 +683,17 @@ var EvmRequestTracker = /** @class */ (function () {
541
683
  });
542
684
  return [2 /*return*/, request({ method: "wallet_sendCalls", params: params })];
543
685
  }
686
+ if ((_d = (_c = this.deps).shouldSkipRequestCapture) === null || _d === void 0 ? void 0 : _d.call(_c, "wallet_sendCalls", params)) {
687
+ // A pending wagmi sendCalls mutation owns this capture.
688
+ return [2 /*return*/, request({ method: "wallet_sendCalls", params: params })];
689
+ }
544
690
  sendPromise = request({ method: "wallet_sendCalls", params: params });
545
691
  sendPromise.catch(function () { return undefined; });
546
692
  batch = params[0];
547
693
  calls = Array.isArray(batch === null || batch === void 0 ? void 0 : batch.calls) ? batch.calls : [];
548
694
  declared = typeof (batch === null || batch === void 0 ? void 0 : batch.chainId) === "string" ? (0, chain_1.parseChainId)(batch.chainId) : undefined;
549
695
  chainId = declared || this.registry.resolveChainId(provider);
550
- address = (0, address_1.validateAndChecksumAddress)((_c = batch === null || batch === void 0 ? void 0 : batch.from) !== null && _c !== void 0 ? _c : "");
696
+ address = (0, address_1.validateAndChecksumAddress)((_e = batch === null || batch === void 0 ? void 0 : batch.from) !== null && _e !== void 0 ? _e : "");
551
697
  if (!address) {
552
698
  // Nothing can be attributed without a sender, and inventing one would
553
699
  // be worse than reporting nothing. The user's call still goes through.
@@ -566,16 +712,14 @@ var EvmRequestTracker = /** @class */ (function () {
566
712
  if (!provider || provider === this.wallet.provider || !this.wallet.provider) {
567
713
  this.wallet.backfill(address, chainId, provider);
568
714
  }
715
+ attribution = this.attributionFor(provider);
569
716
  payloads = calls.map(function (call, index) { return ({
570
717
  chainId: chainId,
571
718
  address: address,
572
719
  to: call === null || call === void 0 ? void 0 : call.to,
573
720
  value: call === null || call === void 0 ? void 0 : call.value,
574
721
  data: call === null || call === void 0 ? void 0 : call.data,
575
- properties: {
576
- batch_size: calls.length,
577
- batch_index: index,
578
- },
722
+ properties: __assign({ batch_size: calls.length, batch_index: index }, attribution),
579
723
  }); });
580
724
  // STARTED carries no batch id: the wallet has not issued one yet, exactly
581
725
  // as `eth_sendTransaction` has no hash at this point. Position within the
@@ -587,12 +731,12 @@ var EvmRequestTracker = /** @class */ (function () {
587
731
  .transaction(__assign({ status: types_1.TransactionStatus.STARTED }, rest), properties)
588
732
  .catch(function (e) { return logger_1.logger.error("Formo: Failed to track batch call start", e); });
589
733
  }
590
- _d.label = 1;
734
+ _f.label = 1;
591
735
  case 1:
592
- _d.trys.push([1, 3, , 4]);
736
+ _f.trys.push([1, 3, , 4]);
593
737
  return [4 /*yield*/, sendPromise];
594
738
  case 2:
595
- result = _d.sent();
739
+ result = _f.sent();
596
740
  batchId = (0, batch_1.readBatchId)(result);
597
741
  for (_a = 0, payloads_2 = payloads; _a < payloads_2.length; _a++) {
598
742
  p = payloads_2[_a];
@@ -607,9 +751,9 @@ var EvmRequestTracker = /** @class */ (function () {
607
751
  this.pollBatchStatus(provider, batchId, payloads);
608
752
  return [2 /*return*/, result];
609
753
  case 3:
610
- error_3 = _d.sent();
754
+ error_3 = _f.sent();
611
755
  rpcError = error_3;
612
- if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
756
+ if ((0, provider_1.isUserRejectionError)(rpcError)) {
613
757
  // One rejection dismisses the whole prompt, so every call in it is
614
758
  // rejected. Reporting only the first would undercount.
615
759
  for (_b = 0, payloads_3 = payloads; _b < payloads_3.length; _b++) {
@@ -639,7 +783,7 @@ var EvmRequestTracker = /** @class */ (function () {
639
783
  * what makes a partially reverted non-atomic batch report honestly instead
640
784
  * of marking every call with the batch's worst outcome.
641
785
  */
642
- EvmRequestTracker.prototype.pollBatchStatus = function (provider_1, batchId_1, payloads_4) {
786
+ EvmRequestTracker.prototype.pollBatchStatus = function (provider_2, batchId_1, payloads_4) {
643
787
  return __awaiter(this, arguments, void 0, function (provider, batchId, payloads, maxAttempts, intervalMs) {
644
788
  var attempts, poll;
645
789
  var _this = this;
@@ -41,6 +41,36 @@ export interface ProviderInfo {
41
41
  * ```
42
42
  */
43
43
  export declare function detectInjectedProviderInfo(provider: EIP1193Provider): ProviderInfo;
44
+ /**
45
+ * Was this error the USER declining, whatever transport delivered it?
46
+ *
47
+ * Three dialects say "the user said no":
48
+ * - EIP-1193: code 4001 (UserRejectedRequest).
49
+ * - WalletConnect sdkErrors: codes 5000-5005 (USER_REJECTED and its
50
+ * variants). A LIVE MetaMask Mobile session rejecting a transaction
51
+ * produced one of these and the SDK's 4001-only match reported nothing -
52
+ * every WalletConnect rejection was silently uncounted.
53
+ * - viem: a typed UserRejectedRequestError, sometimes without the numeric
54
+ * code surviving the wrapping.
55
+ *
56
+ * The real code often hides under `cause` (viem nests, WC wraps), so the
57
+ * chain is walked a few levels.
58
+ */
59
+ export declare function isUserRejectionError(error: unknown): boolean;
60
+ /**
61
+ * The wallet on the far side of a WalletConnect session.
62
+ *
63
+ * WalletConnect is a transport, not a wallet: the signing wallet (Ledger
64
+ * Live, MetaMask Mobile, Safe, ...) identifies itself in the session's peer
65
+ * metadata. Reporting only "WalletConnect" hides every wallet behind it -
66
+ * production showed Ledger at effectively zero while its sessions were being
67
+ * tracked under the transport's name. Reads synchronous state only; never
68
+ * issues an RPC.
69
+ */
70
+ export declare function readWalletConnectPeer(provider: EIP1193Provider): {
71
+ name: string;
72
+ url?: string;
73
+ } | undefined;
44
74
  /**
45
75
  * Validates that a provider implements the required EIP-1193 interface
46
76
  *
@@ -2,9 +2,22 @@
2
2
  /**
3
3
  * Provider detection utilities for identifying wallet providers
4
4
  */
5
+ var __assign = (this && this.__assign) || function () {
6
+ __assign = Object.assign || function(t) {
7
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
8
+ s = arguments[i];
9
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
10
+ t[p] = s[p];
11
+ }
12
+ return t;
13
+ };
14
+ return __assign.apply(this, arguments);
15
+ };
5
16
  Object.defineProperty(exports, "__esModule", { value: true });
6
17
  exports.DEFAULT_PROVIDER_ICON = void 0;
7
18
  exports.detectInjectedProviderInfo = detectInjectedProviderInfo;
19
+ exports.isUserRejectionError = isUserRejectionError;
20
+ exports.readWalletConnectPeer = readWalletConnectPeer;
8
21
  exports.isValidProvider = isValidProvider;
9
22
  /**
10
23
  * Default icon for providers without custom icons
@@ -81,6 +94,55 @@ function detectInjectedProviderInfo(provider) {
81
94
  icon: exports.DEFAULT_PROVIDER_ICON,
82
95
  };
83
96
  }
97
+ /**
98
+ * Was this error the USER declining, whatever transport delivered it?
99
+ *
100
+ * Three dialects say "the user said no":
101
+ * - EIP-1193: code 4001 (UserRejectedRequest).
102
+ * - WalletConnect sdkErrors: codes 5000-5005 (USER_REJECTED and its
103
+ * variants). A LIVE MetaMask Mobile session rejecting a transaction
104
+ * produced one of these and the SDK's 4001-only match reported nothing -
105
+ * every WalletConnect rejection was silently uncounted.
106
+ * - viem: a typed UserRejectedRequestError, sometimes without the numeric
107
+ * code surviving the wrapping.
108
+ *
109
+ * The real code often hides under `cause` (viem nests, WC wraps), so the
110
+ * chain is walked a few levels.
111
+ */
112
+ function isUserRejectionError(error) {
113
+ var cursor = error;
114
+ for (var depth = 0; cursor && depth < 5; depth++) {
115
+ var code = cursor.code;
116
+ if (code === 4001 ||
117
+ (typeof code === "number" && code >= 5000 && code <= 5005) ||
118
+ cursor.name === "UserRejectedRequestError") {
119
+ return true;
120
+ }
121
+ cursor = cursor.cause;
122
+ }
123
+ return false;
124
+ }
125
+ /**
126
+ * The wallet on the far side of a WalletConnect session.
127
+ *
128
+ * WalletConnect is a transport, not a wallet: the signing wallet (Ledger
129
+ * Live, MetaMask Mobile, Safe, ...) identifies itself in the session's peer
130
+ * metadata. Reporting only "WalletConnect" hides every wallet behind it -
131
+ * production showed Ledger at effectively zero while its sessions were being
132
+ * tracked under the transport's name. Reads synchronous state only; never
133
+ * issues an RPC.
134
+ */
135
+ function readWalletConnectPeer(provider) {
136
+ var _a;
137
+ var session = provider.session;
138
+ var metadata = (_a = session === null || session === void 0 ? void 0 : session.peer) === null || _a === void 0 ? void 0 : _a.metadata;
139
+ if (!metadata || typeof metadata.name !== "string" || metadata.name.length === 0) {
140
+ return undefined;
141
+ }
142
+ return __assign({ name: metadata.name }, (typeof metadata.url === "string" && metadata.url.length > 0
143
+ ? { url: metadata.url }
144
+ : {}));
145
+ }
84
146
  /**
85
147
  * Validates that a provider implements the required EIP-1193 interface
86
148
  *