@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
  /**
@@ -56,10 +56,20 @@ var __rest = (this && this.__rest) || function (s, e) {
56
56
  }
57
57
  return t;
58
58
  };
59
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
60
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
61
+ if (ar || !(i in from)) {
62
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
63
+ ar[i] = from[i];
64
+ }
65
+ }
66
+ return to.concat(ar || Array.prototype.slice.call(from));
67
+ };
59
68
  import { logger } from "../logger";
69
+ import { isUserRejectionError } from "../provider";
60
70
  import { parseChainId } from "../utils/chain";
61
71
  import { validateAndChecksumAddress } from "../utils/address";
62
- import { SignatureStatus, TransactionStatus, WRAPPED_REQUEST_SYMBOL, WRAPPED_REQUEST_REF_SYMBOL, } from "../types";
72
+ import { SignatureStatus, TransactionStatus, WRAPPED_REQUEST_SYMBOL, WRAPPED_REQUEST_REF_SYMBOL, WRAPPED_REQUEST_OWNER_SYMBOL, } from "../types";
63
73
  import { readBatchId, readBatchStatusCode, batchCallOutcome, batchReceiptForCall, } from "./batch";
64
74
  /**
65
75
  * Decode a hex-encoded `personal_sign` message.
@@ -97,6 +107,19 @@ function hexToUtf8(hex) {
97
107
  * probed - an SDK-issued lookup on a serialising transport can wedge the
98
108
  * user's wallet, which is never an acceptable price for a label.
99
109
  */
110
+ /**
111
+ * Providers with a dispatch already on the synchronous call stack.
112
+ *
113
+ * A page can end up with LAYERED Formo wrappers: another library wraps our
114
+ * wrapper, the SDK rebuilds, and the new instance wraps the outer function
115
+ * because the marker is not on it. Both layers route to the same newest
116
+ * live tracker, which would instrument one user request twice. Every
117
+ * dispatch issues its underlying request synchronously (deliberately -
118
+ * the wallet call always goes out first), so an inner shim reached during
119
+ * that window belongs to the SAME user request and passes straight
120
+ * through.
121
+ */
122
+ var dispatchInFlight = new WeakSet();
100
123
  var EvmRequestTracker = /** @class */ (function () {
101
124
  function EvmRequestTracker(wallet, registry, deps) {
102
125
  this.wallet = wallet;
@@ -112,12 +135,28 @@ var EvmRequestTracker = /** @class */ (function () {
112
135
  */
113
136
  this.polls = new Set();
114
137
  this.disposed = false;
138
+ /** Providers whose owner list includes this instance; pruned on cleanup. */
139
+ this.wrappedProviders = new Set();
115
140
  }
116
141
  /** Stop every poll in flight. Terminal, like the event queue's close(). */
117
142
  EvmRequestTracker.prototype.cleanup = function () {
143
+ var _this = this;
118
144
  this.disposed = true;
119
145
  this.polls.forEach(function (timer) { return clearTimeout(timer); });
120
146
  this.polls.clear();
147
+ // Remove this instance from every provider's owner list. The lists
148
+ // live on LONG-LIVED provider objects; leaving disposed trackers in
149
+ // them retains each old instance's whole object graph across rebuilds,
150
+ // growing without bound under HMR.
151
+ this.wrappedProviders.forEach(function (provider) {
152
+ var owners = provider[WRAPPED_REQUEST_OWNER_SYMBOL];
153
+ if (Array.isArray(owners)) {
154
+ var idx = owners.indexOf(_this);
155
+ if (idx !== -1)
156
+ owners.splice(idx, 1);
157
+ }
158
+ });
159
+ this.wrappedProviders.clear();
121
160
  };
122
161
  /** Re-arm a poll, unless this tracker has been torn down. */
123
162
  EvmRequestTracker.prototype.schedulePoll = function (fn, delayMs) {
@@ -143,19 +182,118 @@ var EvmRequestTracker = /** @class */ (function () {
143
182
  logger.error("Provider not found for request (signature, transaction) tracking");
144
183
  return false;
145
184
  }
146
- // Check if the provider is already wrapped with our SDK's wrapper
185
+ // Already wrapped: take OWNERSHIP rather than skipping. The wrapper
186
+ // survives an SDK rebuild and closes over the instance that installed
187
+ // it, whose queue is closed after cleanup - "skip" made the rebuilt
188
+ // instance report success while every request-derived event died in
189
+ // the dead instance's queue. The wrapper reads the owner slot per call.
147
190
  var currentRequest = provider.request;
148
191
  if (this.registry.isWrapped(provider, currentRequest)) {
149
- logger.info("Provider already wrapped with our SDK; skipping request wrapping.");
192
+ var owners = provider[WRAPPED_REQUEST_OWNER_SYMBOL];
193
+ if (!Array.isArray(owners)) {
194
+ // A wrapper without its owner list cannot be taken over, and
195
+ // claiming success would silence every request event.
196
+ logger.warn("wrapped without owner list; cannot rebind");
197
+ return false;
198
+ }
199
+ var idx = owners.indexOf(this);
200
+ if (idx !== -1)
201
+ owners.splice(idx, 1);
202
+ owners.push(this);
203
+ this.wrappedProviders.add(provider);
204
+ logger.info("Provider already wrapped; rebinding the wrapper to this instance.");
150
205
  return true;
151
206
  }
152
207
  var request = provider.request.bind(provider);
153
208
  var wrappedRequest = function (_a) { return __awaiter(_this, [_a], void 0, function (_b) {
209
+ var owners, liveOwner;
210
+ var method = _b.method, params = _b.params;
211
+ return __generator(this, function (_c) {
212
+ owners = provider[WRAPPED_REQUEST_OWNER_SYMBOL];
213
+ if (dispatchInFlight.has(provider)) {
214
+ // An outer Formo wrapper is already instrumenting this very
215
+ // request; this layer only forwards.
216
+ return [2 /*return*/, request({ method: method, params: params })];
217
+ }
218
+ liveOwner = Array.isArray(owners)
219
+ ? __spreadArray([], owners, true).reverse().find(function (o) { return !o.disposed; })
220
+ : undefined;
221
+ dispatchInFlight.add(provider);
222
+ try {
223
+ return [2 /*return*/, (liveOwner !== null && liveOwner !== void 0 ? liveOwner : this).dispatchWrappedRequest({ method: method, params: params }, provider, request)];
224
+ }
225
+ finally {
226
+ // Cleared as soon as the dispatch call RETURNS its promise: the
227
+ // underlying request has been issued synchronously by then, so
228
+ // the window covers exactly the nested layers of this one call
229
+ // and never a concurrent request.
230
+ dispatchInFlight.delete(provider);
231
+ }
232
+ return [2 /*return*/];
233
+ });
234
+ }); };
235
+ try {
236
+ // MERGE with any existing list rather than overwriting: a wallet that
237
+ // replaced `request` forces a re-wrap, and discarding the prior list
238
+ // would drop other live instances from ownership - the newest-live
239
+ // fallback then has nobody to fall back to.
240
+ var slot = provider;
241
+ var prior = slot[WRAPPED_REQUEST_OWNER_SYMBOL];
242
+ var owners = Array.isArray(prior) ? prior : [];
243
+ var idx = owners.indexOf(this);
244
+ if (idx !== -1)
245
+ owners.splice(idx, 1);
246
+ owners.push(this);
247
+ if (!Array.isArray(prior)) {
248
+ slot[WRAPPED_REQUEST_OWNER_SYMBOL] = owners;
249
+ }
250
+ this.wrappedProviders.add(provider);
251
+ }
252
+ catch (_a) {
253
+ /* frozen provider: the request write below fails too and aborts */
254
+ }
255
+ return this.installWrappedRequest(provider, wrappedRequest);
256
+ };
257
+ /** Install the wrapper function onto the provider; separated so the
258
+ * routing shim above stays small. */
259
+ EvmRequestTracker.prototype.installWrappedRequest = function (provider, wrappedRequest) {
260
+ // Mark the wrapper so we can detect if request is replaced externally and keep a reference on provider
261
+ wrappedRequest[WRAPPED_REQUEST_SYMBOL] = true;
262
+ // Both writes go inside the try. A frozen or non-extensible provider
263
+ // throws on the symbol assignment just as readily as on `request`, and
264
+ // that one used to sit outside the guard, so an unwrappable provider
265
+ // aborted registration instead of being skipped.
266
+ try {
267
+ provider[WRAPPED_REQUEST_REF_SYMBOL] =
268
+ wrappedRequest;
269
+ provider.request = wrappedRequest;
270
+ // Read back: an accessor or Proxy can ACCEPT the assignment without
271
+ // installing it, and success here is a promise that capture works.
272
+ if (provider.request !== wrappedRequest) {
273
+ logger.warn("request assignment swallowed; not wrapped");
274
+ return false;
275
+ }
276
+ return true;
277
+ }
278
+ catch (e) {
279
+ logger.warn("Failed to wrap provider.request; skipping", e);
280
+ return false;
281
+ }
282
+ };
283
+ /**
284
+ * The wrapper body proper: everything a request observation does, run
285
+ * against THIS instance's registry, wallet state, and event queue. Kept
286
+ * as a method so a surviving wrapper installed by a previous SDK
287
+ * instance can hand calls to the current one.
288
+ */
289
+ EvmRequestTracker.prototype.dispatchWrappedRequest = function (_a, provider_1, request_1) {
290
+ return __awaiter(this, arguments, void 0, function (_b, provider, request) {
154
291
  var generation_1, responsePromise, capturedChainId_1, response_1, error_1, rpcError, txPromise, txChainId_1, transactionHash_1, error_2, rpcError;
155
292
  var _this = this;
293
+ var _c, _d, _e, _f;
156
294
  var method = _b.method, params = _b.params;
157
- return __generator(this, function (_c) {
158
- switch (_c.label) {
295
+ return __generator(this, function (_g) {
296
+ switch (_g.label) {
159
297
  case 0:
160
298
  // Learn the chain from a call the APP was making anyway.
161
299
  //
@@ -184,6 +322,10 @@ var EvmRequestTracker = /** @class */ (function () {
184
322
  logger.debug("Signature event skipped (autocapture.signature: false)", { method: method });
185
323
  return [2 /*return*/, request({ method: method, params: params })];
186
324
  }
325
+ if ((_d = (_c = this.deps).shouldSkipRequestCapture) === null || _d === void 0 ? void 0 : _d.call(_c, method, params)) {
326
+ // A pending wagmi mutation owns this capture.
327
+ return [2 /*return*/, request({ method: method, params: params })];
328
+ }
187
329
  responsePromise = request({ method: method, params: params });
188
330
  // Attach a no-op handler now so a rejection arriving before the await
189
331
  // below is never reported as unhandled. The real handling is there.
@@ -196,7 +338,7 @@ var EvmRequestTracker = /** @class */ (function () {
196
338
  switch (_a.label) {
197
339
  case 0:
198
340
  _a.trys.push([0, 2, , 3]);
199
- return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
341
+ return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)), this.attributionFor(provider))];
200
342
  case 1:
201
343
  _a.sent();
202
344
  return [3 /*break*/, 3];
@@ -208,12 +350,12 @@ var EvmRequestTracker = /** @class */ (function () {
208
350
  }
209
351
  });
210
352
  }); })();
211
- _c.label = 1;
353
+ _g.label = 1;
212
354
  case 1:
213
- _c.trys.push([1, 3, , 4]);
355
+ _g.trys.push([1, 3, , 4]);
214
356
  return [4 /*yield*/, responsePromise];
215
357
  case 2:
216
- response_1 = _c.sent();
358
+ response_1 = _g.sent();
217
359
  // Track signature confirmation only for truthy responses
218
360
  if (response_1) {
219
361
  (function () { return __awaiter(_this, void 0, void 0, function () {
@@ -222,7 +364,7 @@ var EvmRequestTracker = /** @class */ (function () {
222
364
  switch (_a.label) {
223
365
  case 0:
224
366
  _a.trys.push([0, 2, , 3]);
225
- return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)))];
367
+ return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)), this.attributionFor(provider))];
226
368
  case 1:
227
369
  _a.sent();
228
370
  return [3 /*break*/, 3];
@@ -237,9 +379,9 @@ var EvmRequestTracker = /** @class */ (function () {
237
379
  }
238
380
  return [2 /*return*/, response_1];
239
381
  case 3:
240
- error_1 = _c.sent();
382
+ error_1 = _g.sent();
241
383
  rpcError = error_1;
242
- if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
384
+ if (isUserRejectionError(rpcError)) {
243
385
  // Use the already cast rpcError to avoid duplication
244
386
  (function () { return __awaiter(_this, void 0, void 0, function () {
245
387
  var e_3;
@@ -247,7 +389,7 @@ var EvmRequestTracker = /** @class */ (function () {
247
389
  switch (_a.label) {
248
390
  case 0:
249
391
  _a.trys.push([0, 2, , 3]);
250
- return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
392
+ return [4 /*yield*/, this.deps.signature(__assign({ status: SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)), this.attributionFor(provider))];
251
393
  case 1:
252
394
  _a.sent();
253
395
  return [3 /*break*/, 3];
@@ -279,6 +421,10 @@ var EvmRequestTracker = /** @class */ (function () {
279
421
  logger.debug("Transaction event skipped (autocapture.transaction: false)", { method: method });
280
422
  return [2 /*return*/, request({ method: method, params: params })];
281
423
  }
424
+ if ((_f = (_e = this.deps).shouldSkipRequestCapture) === null || _f === void 0 ? void 0 : _f.call(_e, method, params)) {
425
+ // A pending wagmi mutation owns this capture.
426
+ return [2 /*return*/, request({ method: method, params: params })];
427
+ }
282
428
  txPromise = request({ method: method, params: params });
283
429
  txPromise.catch(function () { return undefined; });
284
430
  txChainId_1 = this.registry.resolveChainId(provider);
@@ -291,7 +437,7 @@ var EvmRequestTracker = /** @class */ (function () {
291
437
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
292
438
  case 1:
293
439
  payload = _a.sent();
294
- return [4 /*yield*/, this.deps.transaction(__assign({ status: TransactionStatus.STARTED }, payload))];
440
+ return [4 /*yield*/, this.deps.transaction(__assign({ status: TransactionStatus.STARTED }, payload), this.attributionFor(provider))];
295
441
  case 2:
296
442
  _a.sent();
297
443
  return [3 /*break*/, 4];
@@ -303,12 +449,12 @@ var EvmRequestTracker = /** @class */ (function () {
303
449
  }
304
450
  });
305
451
  }); })();
306
- _c.label = 5;
452
+ _g.label = 5;
307
453
  case 5:
308
- _c.trys.push([5, 7, , 8]);
454
+ _g.trys.push([5, 7, , 8]);
309
455
  return [4 /*yield*/, txPromise];
310
456
  case 6:
311
- transactionHash_1 = _c.sent();
457
+ transactionHash_1 = _g.sent();
312
458
  (function () { return __awaiter(_this, void 0, void 0, function () {
313
459
  var payload, e_5;
314
460
  return __generator(this, function (_a) {
@@ -318,7 +464,7 @@ var EvmRequestTracker = /** @class */ (function () {
318
464
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
319
465
  case 1:
320
466
  payload = _a.sent();
321
- return [4 /*yield*/, this.deps.transaction(__assign(__assign({ status: TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }))];
467
+ return [4 /*yield*/, this.deps.transaction(__assign(__assign({ status: TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }), this.attributionFor(provider))];
322
468
  case 2:
323
469
  _a.sent();
324
470
  // Start async polling for transaction receipt
@@ -334,9 +480,9 @@ var EvmRequestTracker = /** @class */ (function () {
334
480
  }); })();
335
481
  return [2 /*return*/, transactionHash_1];
336
482
  case 7:
337
- error_2 = _c.sent();
483
+ error_2 = _g.sent();
338
484
  rpcError = error_2;
339
- if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
485
+ if (isUserRejectionError(rpcError)) {
340
486
  // Use the already cast rpcError to avoid duplication
341
487
  (function () { return __awaiter(_this, void 0, void 0, function () {
342
488
  var payload, e_6;
@@ -347,7 +493,7 @@ var EvmRequestTracker = /** @class */ (function () {
347
493
  return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
348
494
  case 1:
349
495
  payload = _a.sent();
350
- return [4 /*yield*/, this.deps.transaction(__assign({ status: TransactionStatus.REJECTED }, payload))];
496
+ return [4 /*yield*/, this.deps.transaction(__assign({ status: TransactionStatus.REJECTED }, payload), this.attributionFor(provider))];
351
497
  case 2:
352
498
  _a.sent();
353
499
  return [3 /*break*/, 4];
@@ -364,23 +510,19 @@ var EvmRequestTracker = /** @class */ (function () {
364
510
  case 8: return [2 /*return*/, request({ method: method, params: params })];
365
511
  }
366
512
  });
367
- }); };
368
- // Mark the wrapper so we can detect if request is replaced externally and keep a reference on provider
369
- wrappedRequest[WRAPPED_REQUEST_SYMBOL] = true;
370
- // Both writes go inside the try. A frozen or non-extensible provider
371
- // throws on the symbol assignment just as readily as on `request`, and
372
- // that one used to sit outside the guard, so an unwrappable provider
373
- // aborted registration instead of being skipped.
374
- try {
375
- provider[WRAPPED_REQUEST_REF_SYMBOL] =
376
- wrappedRequest;
377
- provider.request = wrappedRequest;
378
- return true;
379
- }
380
- catch (e) {
381
- logger.warn("Failed to wrap provider.request; skipping", e);
382
- return false;
383
- }
513
+ });
514
+ };
515
+ /**
516
+ * Wallet attribution for request-derived events.
517
+ *
518
+ * Live per read through the registry, so a WalletConnect session names
519
+ * its actual signer ("MetaMask Wallet", "Ledger Live") - the live-test
520
+ * rows had provider_name EMPTY on every signature and transaction, which
521
+ * made per-wallet activity unanswerable in the warehouse.
522
+ */
523
+ EvmRequestTracker.prototype.attributionFor = function (provider) {
524
+ var info = this.registry.infoFor(provider);
525
+ return { providerName: info.name, rdns: info.rdns };
384
526
  };
385
527
  EvmRequestTracker.prototype.buildSignatureEventPayload = function (method, params,
386
528
  // Intentionally not read. Kept for positional call-site arity.
@@ -476,7 +618,7 @@ var EvmRequestTracker = /** @class */ (function () {
476
618
  // status: 1 = success, 0 = reverted
477
619
  if (receipt.status === "0x1" || receipt.status === 1) {
478
620
  this.deps
479
- .transaction(__assign(__assign({ status: TransactionStatus.CONFIRMED }, payload), { transactionHash: transactionHash }))
621
+ .transaction(__assign(__assign({ status: TransactionStatus.CONFIRMED }, payload), { transactionHash: transactionHash }), this.attributionFor(provider))
480
622
  .catch(function (e) {
481
623
  return logger.error("Formo: Failed to track transaction confirmation", e);
482
624
  });
@@ -484,7 +626,7 @@ var EvmRequestTracker = /** @class */ (function () {
484
626
  }
485
627
  else if (receipt.status === "0x0" || receipt.status === 0) {
486
628
  this.deps
487
- .transaction(__assign(__assign({ status: TransactionStatus.REVERTED }, payload), { transactionHash: transactionHash }))
629
+ .transaction(__assign(__assign({ status: TransactionStatus.REVERTED }, payload), { transactionHash: transactionHash }), this.attributionFor(provider))
488
630
  .catch(function (e) {
489
631
  return logger.error("Formo: Failed to track transaction revert", e);
490
632
  });
@@ -527,10 +669,10 @@ var EvmRequestTracker = /** @class */ (function () {
527
669
  */
528
670
  EvmRequestTracker.prototype.trackBatchedCalls = function (provider, request, params) {
529
671
  return __awaiter(this, void 0, void 0, function () {
530
- 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;
531
- var _c;
532
- return __generator(this, function (_d) {
533
- switch (_d.label) {
672
+ 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;
673
+ var _c, _d, _e;
674
+ return __generator(this, function (_f) {
675
+ switch (_f.label) {
534
676
  case 0:
535
677
  if (!this.deps.isAutocaptureEnabled("transaction")) {
536
678
  logger.debug("Transaction event skipped (autocapture.transaction: false)", {
@@ -538,13 +680,17 @@ var EvmRequestTracker = /** @class */ (function () {
538
680
  });
539
681
  return [2 /*return*/, request({ method: "wallet_sendCalls", params: params })];
540
682
  }
683
+ if ((_d = (_c = this.deps).shouldSkipRequestCapture) === null || _d === void 0 ? void 0 : _d.call(_c, "wallet_sendCalls", params)) {
684
+ // A pending wagmi sendCalls mutation owns this capture.
685
+ return [2 /*return*/, request({ method: "wallet_sendCalls", params: params })];
686
+ }
541
687
  sendPromise = request({ method: "wallet_sendCalls", params: params });
542
688
  sendPromise.catch(function () { return undefined; });
543
689
  batch = params[0];
544
690
  calls = Array.isArray(batch === null || batch === void 0 ? void 0 : batch.calls) ? batch.calls : [];
545
691
  declared = typeof (batch === null || batch === void 0 ? void 0 : batch.chainId) === "string" ? parseChainId(batch.chainId) : undefined;
546
692
  chainId = declared || this.registry.resolveChainId(provider);
547
- address = validateAndChecksumAddress((_c = batch === null || batch === void 0 ? void 0 : batch.from) !== null && _c !== void 0 ? _c : "");
693
+ address = validateAndChecksumAddress((_e = batch === null || batch === void 0 ? void 0 : batch.from) !== null && _e !== void 0 ? _e : "");
548
694
  if (!address) {
549
695
  // Nothing can be attributed without a sender, and inventing one would
550
696
  // be worse than reporting nothing. The user's call still goes through.
@@ -563,16 +709,14 @@ var EvmRequestTracker = /** @class */ (function () {
563
709
  if (!provider || provider === this.wallet.provider || !this.wallet.provider) {
564
710
  this.wallet.backfill(address, chainId, provider);
565
711
  }
712
+ attribution = this.attributionFor(provider);
566
713
  payloads = calls.map(function (call, index) { return ({
567
714
  chainId: chainId,
568
715
  address: address,
569
716
  to: call === null || call === void 0 ? void 0 : call.to,
570
717
  value: call === null || call === void 0 ? void 0 : call.value,
571
718
  data: call === null || call === void 0 ? void 0 : call.data,
572
- properties: {
573
- batch_size: calls.length,
574
- batch_index: index,
575
- },
719
+ properties: __assign({ batch_size: calls.length, batch_index: index }, attribution),
576
720
  }); });
577
721
  // STARTED carries no batch id: the wallet has not issued one yet, exactly
578
722
  // as `eth_sendTransaction` has no hash at this point. Position within the
@@ -584,12 +728,12 @@ var EvmRequestTracker = /** @class */ (function () {
584
728
  .transaction(__assign({ status: TransactionStatus.STARTED }, rest), properties)
585
729
  .catch(function (e) { return logger.error("Formo: Failed to track batch call start", e); });
586
730
  }
587
- _d.label = 1;
731
+ _f.label = 1;
588
732
  case 1:
589
- _d.trys.push([1, 3, , 4]);
733
+ _f.trys.push([1, 3, , 4]);
590
734
  return [4 /*yield*/, sendPromise];
591
735
  case 2:
592
- result = _d.sent();
736
+ result = _f.sent();
593
737
  batchId = readBatchId(result);
594
738
  for (_a = 0, payloads_2 = payloads; _a < payloads_2.length; _a++) {
595
739
  p = payloads_2[_a];
@@ -604,9 +748,9 @@ var EvmRequestTracker = /** @class */ (function () {
604
748
  this.pollBatchStatus(provider, batchId, payloads);
605
749
  return [2 /*return*/, result];
606
750
  case 3:
607
- error_3 = _d.sent();
751
+ error_3 = _f.sent();
608
752
  rpcError = error_3;
609
- if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
753
+ if (isUserRejectionError(rpcError)) {
610
754
  // One rejection dismisses the whole prompt, so every call in it is
611
755
  // rejected. Reporting only the first would undercount.
612
756
  for (_b = 0, payloads_3 = payloads; _b < payloads_3.length; _b++) {
@@ -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
  *
@@ -1,6 +1,17 @@
1
1
  /**
2
2
  * Provider detection utilities for identifying wallet providers
3
3
  */
4
+ var __assign = (this && this.__assign) || function () {
5
+ __assign = Object.assign || function(t) {
6
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
7
+ s = arguments[i];
8
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
9
+ t[p] = s[p];
10
+ }
11
+ return t;
12
+ };
13
+ return __assign.apply(this, arguments);
14
+ };
4
15
  /**
5
16
  * Default icon for providers without custom icons
6
17
  */
@@ -76,6 +87,55 @@ export function detectInjectedProviderInfo(provider) {
76
87
  icon: DEFAULT_PROVIDER_ICON,
77
88
  };
78
89
  }
90
+ /**
91
+ * Was this error the USER declining, whatever transport delivered it?
92
+ *
93
+ * Three dialects say "the user said no":
94
+ * - EIP-1193: code 4001 (UserRejectedRequest).
95
+ * - WalletConnect sdkErrors: codes 5000-5005 (USER_REJECTED and its
96
+ * variants). A LIVE MetaMask Mobile session rejecting a transaction
97
+ * produced one of these and the SDK's 4001-only match reported nothing -
98
+ * every WalletConnect rejection was silently uncounted.
99
+ * - viem: a typed UserRejectedRequestError, sometimes without the numeric
100
+ * code surviving the wrapping.
101
+ *
102
+ * The real code often hides under `cause` (viem nests, WC wraps), so the
103
+ * chain is walked a few levels.
104
+ */
105
+ export function isUserRejectionError(error) {
106
+ var cursor = error;
107
+ for (var depth = 0; cursor && depth < 5; depth++) {
108
+ var code = cursor.code;
109
+ if (code === 4001 ||
110
+ (typeof code === "number" && code >= 5000 && code <= 5005) ||
111
+ cursor.name === "UserRejectedRequestError") {
112
+ return true;
113
+ }
114
+ cursor = cursor.cause;
115
+ }
116
+ return false;
117
+ }
118
+ /**
119
+ * The wallet on the far side of a WalletConnect session.
120
+ *
121
+ * WalletConnect is a transport, not a wallet: the signing wallet (Ledger
122
+ * Live, MetaMask Mobile, Safe, ...) identifies itself in the session's peer
123
+ * metadata. Reporting only "WalletConnect" hides every wallet behind it -
124
+ * production showed Ledger at effectively zero while its sessions were being
125
+ * tracked under the transport's name. Reads synchronous state only; never
126
+ * issues an RPC.
127
+ */
128
+ export function readWalletConnectPeer(provider) {
129
+ var _a;
130
+ var session = provider.session;
131
+ var metadata = (_a = session === null || session === void 0 ? void 0 : session.peer) === null || _a === void 0 ? void 0 : _a.metadata;
132
+ if (!metadata || typeof metadata.name !== "string" || metadata.name.length === 0) {
133
+ return undefined;
134
+ }
135
+ return __assign({ name: metadata.name }, (typeof metadata.url === "string" && metadata.url.length > 0
136
+ ? { url: metadata.url }
137
+ : {}));
138
+ }
79
139
  /**
80
140
  * Validates that a provider implements the required EIP-1193 interface
81
141
  *
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Provider-related exports
3
3
  */
4
- export { detectInjectedProviderInfo, isValidProvider, DEFAULT_PROVIDER_ICON, } from './detection';
4
+ export { detectInjectedProviderInfo, isValidProvider, readWalletConnectPeer, isUserRejectionError, DEFAULT_PROVIDER_ICON, } from './detection';
5
5
  export type { WalletProviderFlags, ProviderInfo, } from './detection';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * Provider-related exports
3
3
  */
4
- export { detectInjectedProviderInfo, isValidProvider, DEFAULT_PROVIDER_ICON, } from './detection';
4
+ export { detectInjectedProviderInfo, isValidProvider, readWalletConnectPeer, isUserRejectionError, DEFAULT_PROVIDER_ICON, } from './detection';
5
5
  //# sourceMappingURL=index.js.map