@meshconnect/uwc-core 1.2.3-snapshot.bd9aae7 → 1.2.3-snapshot.dcfdbbf

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 (44) hide show
  1. package/dist/events.d.ts +5 -42
  2. package/dist/events.d.ts.map +1 -1
  3. package/dist/events.js +0 -4
  4. package/dist/events.js.map +1 -1
  5. package/dist/observability/focus-tracker.d.ts +12 -25
  6. package/dist/observability/focus-tracker.d.ts.map +1 -1
  7. package/dist/observability/focus-tracker.js +19 -42
  8. package/dist/observability/focus-tracker.js.map +1 -1
  9. package/dist/observability/instrument-handoff.d.ts.map +1 -1
  10. package/dist/observability/instrument-handoff.js +6 -24
  11. package/dist/observability/instrument-handoff.js.map +1 -1
  12. package/dist/observability/lifecycle-bridge.d.ts +31 -13
  13. package/dist/observability/lifecycle-bridge.d.ts.map +1 -1
  14. package/dist/observability/lifecycle-bridge.js +167 -92
  15. package/dist/observability/lifecycle-bridge.js.map +1 -1
  16. package/dist/observability/telemetry.d.ts +0 -8
  17. package/dist/observability/telemetry.d.ts.map +1 -1
  18. package/dist/observability/telemetry.js +5 -49
  19. package/dist/observability/telemetry.js.map +1 -1
  20. package/dist/services/connection-service.d.ts +43 -0
  21. package/dist/services/connection-service.d.ts.map +1 -1
  22. package/dist/services/connection-service.js +163 -9
  23. package/dist/services/connection-service.js.map +1 -1
  24. package/dist/services/network-switch-service.d.ts.map +1 -1
  25. package/dist/services/network-switch-service.js +4 -1
  26. package/dist/services/network-switch-service.js.map +1 -1
  27. package/dist/universal-wallet-connector.d.ts.map +1 -1
  28. package/dist/universal-wallet-connector.js +1 -7
  29. package/dist/universal-wallet-connector.js.map +1 -1
  30. package/package.json +5 -5
  31. package/src/events.ts +6 -50
  32. package/src/observability/focus-tracker.test.ts +8 -60
  33. package/src/observability/focus-tracker.ts +23 -59
  34. package/src/observability/instrument-handoff.test.ts +0 -42
  35. package/src/observability/instrument-handoff.ts +6 -26
  36. package/src/observability/lifecycle-bridge.test.ts +197 -0
  37. package/src/observability/lifecycle-bridge.ts +211 -99
  38. package/src/observability/telemetry.test.ts +0 -35
  39. package/src/observability/telemetry.ts +6 -58
  40. package/src/services/connection-service.test.ts +272 -0
  41. package/src/services/connection-service.ts +190 -10
  42. package/src/services/network-switch-service.test.ts +33 -0
  43. package/src/services/network-switch-service.ts +4 -1
  44. package/src/universal-wallet-connector.ts +1 -11
@@ -29,11 +29,23 @@ export interface AttachLifecycleOptions {
29
29
  * The active connector's `onLifecycleEvent` (already bound). `undefined` when the
30
30
  * connector can't observe lifecycle at all; the value it returns is `undefined`
31
31
  * when THIS session can't be observed (e.g. no live `.on`). Either way the
32
- * bridge emits `lifecycleTelemetryUnavailable` and becomes a no-op.
32
+ * bridge emits `lifecycleTelemetryUnavailable` and becomes a no-op FOR THIS
33
+ * STREAM — `subscribeAccounts` below is attached independently.
33
34
  */
34
35
  subscribe:
35
36
  | ((l: (e: WalletLifecycleEvent) => void) => (() => void) | undefined)
36
37
  | undefined
38
+ /**
39
+ * The active connector's `onAccountsChanged` (already bound), carrying the
40
+ * REAL account list — a separate concern from `subscribe` above (some
41
+ * connectors, e.g. Tron today, implement one without the other). Absent
42
+ * entirely when the connector doesn't support real account-sync.
43
+ */
44
+ subscribeAccounts?:
45
+ | ((l: (accounts: string[]) => void) => (() => void) | undefined)
46
+ | undefined
47
+ /** Invoked with the resolved (post-echo-filter) real account list whenever `subscribeAccounts` delivers one. Required whenever `subscribeAccounts` is provided. */
48
+ onAccountsChanged?: (accounts: string[]) => void
37
49
  /** Resolved per-event so a mid-session account/chain change is reflected. */
38
50
  getRouting: () => LifecycleRouting
39
51
  connectionMode: ConnectionMode
@@ -42,8 +54,9 @@ export interface AttachLifecycleOptions {
42
54
  /** Direct page vs production iframe — stamped on `lifecycleTelemetryUnavailable`. */
43
55
  runtime?: 'direct' | 'iframe'
44
56
  /**
45
- * Window (ms) within which a wallet `chainChanged` is treated as the echo of a
46
- * UWC-initiated `switchNetwork` and suppressed. Default 3000.
57
+ * Window (ms) within which a wallet `chainChanged`/`accountsChanged` is
58
+ * treated as the echo of a UWC-initiated `switchNetwork` and suppressed.
59
+ * Default 3000.
47
60
  */
48
61
  echoWindowMs?: number
49
62
  /**
@@ -53,29 +66,37 @@ export interface AttachLifecycleOptions {
53
66
  * provider's echo would otherwise surface as wallet-initiated. Same one-shot
54
67
  * consume semantics as a bus-armed switch.
55
68
  */
56
- initialSwitch?: { chainId: NetworkId; at: number } | undefined
69
+ initialSwitch?:
70
+ | { chainId: NetworkId; address?: string; at: number }
71
+ | undefined
57
72
  now?: () => number
58
73
  }
59
74
 
60
75
  /**
61
76
  * Bridge a connector's PII-safe `WalletLifecycleEvent` stream onto the event bus
62
77
  * as `walletAccountChanged` / `walletChainChanged` / `walletDisconnected` /
63
- * `walletSessionExpired`.
78
+ * `walletSessionExpired`, AND (separately) forward the connector's real
79
+ * account list to `onAccountsChanged` for session-sync.
64
80
  *
65
- * Two correctness concerns are handled here:
81
+ * Three correctness concerns are handled here:
66
82
  * - **Echo suppression.** When UWC drives a `switchNetwork`, the wallet emits its
67
- * own `chainChanged` for the same chain — that's already captured by UWC's
68
- * `networkSwitched` / the switch handoff, so re-emitting it as a wallet-
69
- * initiated `walletChainChanged` would double-count. We watch the bus for
70
- * `networkSwitched` and drop a matching `chainChanged` inside `echoWindowMs`.
71
- * - **Availability is signal.** If the connector can't observe lifecycle, we emit
72
- * `lifecycleTelemetryUnavailable` ONCE so a dashboard can tell "silent" from
73
- * "unwired".
83
+ * own `chainChanged`/`accountsChanged` for the same change — that's already
84
+ * captured by UWC's `networkSwitched` / the switch handoff, so re-emitting it
85
+ * would double-count. We watch the bus for `networkSwitched` and drop a
86
+ * matching echo of either kind inside `echoWindowMs`.
87
+ * - **Availability is signal, per stream.** If the connector can't observe
88
+ * telemetry lifecycle, we emit `lifecycleTelemetryUnavailable` ONCE but
89
+ * that signal is specific to the TELEMETRY stream. A connector missing
90
+ * `onLifecycleEvent` but implementing `onAccountsChanged` (e.g. Tron today)
91
+ * still gets real account-sync; the two streams are independently attached.
92
+ * - **Shared emission budget.** Both streams count against the same
93
+ * `MAX_LIFECYCLE_EVENTS_PER_ATTACHMENT` ceiling — a storming provider
94
+ * doesn't get double budget just because two streams exist.
74
95
  *
75
- * @returns a teardown that removes the connector subscription AND the bus watcher.
96
+ * @returns a teardown that removes both connector subscriptions AND the bus watcher.
76
97
  */
77
98
  export function attachLifecycle(opts: AttachLifecycleOptions): () => void {
78
- const { eventManager, subscribe, getRouting } = opts
99
+ const { eventManager, subscribe, subscribeAccounts, getRouting } = opts
79
100
  const runtime = opts.runtime ?? 'direct'
80
101
  const now = opts.now ?? Date.now
81
102
  const echoWindowMs = opts.echoWindowMs ?? 3000
@@ -89,11 +110,11 @@ export function attachLifecycle(opts: AttachLifecycleOptions): () => void {
89
110
  runtime
90
111
  })
91
112
 
92
- if (!subscribe) {
113
+ if (!subscribe && !subscribeAccounts) {
93
114
  // In the production iframe the injected subscription is deliberately not
94
115
  // attempted (the provider is a Comlink proxy) — the BRIDGE, not the connector,
95
- // is why lifecycle is unobservable. On the direct page, a connector with no
96
- // `onLifecycleEvent` simply has no lifecycle support for this namespace yet.
116
+ // is why lifecycle is unobservable. On the direct page, a connector with
117
+ // neither stream simply has no lifecycle support for this namespace yet.
97
118
  emitUnavailable(
98
119
  runtime === 'iframe'
99
120
  ? 'bridge_subscription_unavailable'
@@ -102,115 +123,206 @@ export function attachLifecycle(opts: AttachLifecycleOptions): () => void {
102
123
  return () => {}
103
124
  }
104
125
 
105
- // Record UWC-initiated switches so the wallet's chainChanged echo can be
106
- // dropped. Armed from BOTH signals: `awaitingWallet` (operation switchNetwork)
107
- // arms at the handoff START, because some providers emit their chainChanged
108
- // BEFORE the switch request resolves i.e. before `networkSwitched` exists
109
- // and `networkSwitched` re-arms at completion for the late-echo case. The
110
- // re-arm means one extra suppression window can follow an early echo; the
111
- // only event it could swallow is a sub-window user re-switch to the chain
112
- // that just became active — accepted over double-counting every early echo.
113
- let lastSwitch: { chainId: NetworkId; at: number } | undefined =
114
- opts.initialSwitch
126
+ // Record UWC-initiated switches so a wallet echo can be dropped. Armed from
127
+ // BOTH signals: `awaitingWallet` (operation switchNetwork) arms at the
128
+ // handoff START, because some providers emit their chainChanged BEFORE the
129
+ // switch request resolves, and `networkSwitched` re-arms at completion
130
+ // (also carrying the resolved address, for the accounts echo check — the
131
+ // target address isn't known before the switch resolves, so there is no
132
+ // early-arm equivalent for the address half).
133
+ //
134
+ // A LIST, not a single slot: two rapid, back-to-back switches (A then B)
135
+ // each get their own pending entry. A single slot would have the SECOND
136
+ // switch's arm silently erase the FIRST switch's still-unconsumed window,
137
+ // so a delayed wallet echo for A (arriving after B has already completed)
138
+ // would be misclassified as a genuine wallet-initiated change instead of
139
+ // the suppressed echo it actually is.
140
+ type PendingSwitch = {
141
+ chainId?: NetworkId | undefined
142
+ address?: string | undefined
143
+ at: number
144
+ }
145
+ let pendingSwitches: PendingSwitch[] = opts.initialSwitch
146
+ ? [opts.initialSwitch]
147
+ : []
148
+ const pruneExpired = () => {
149
+ pendingSwitches = pendingSwitches.filter(p => now() - p.at <= echoWindowMs)
150
+ }
151
+ // `awaitingWallet` then `networkSwitched` are usually the START and
152
+ // COMPLETION of the SAME switch — re-arming updates that existing entry
153
+ // (refreshing `at`, adding the resolved address) rather than pushing a
154
+ // duplicate, which would otherwise leave a phantom entry behind after only
155
+ // one of the two is consumed by a single wallet echo.
156
+ const armOrRearm = (partial: {
157
+ chainId?: NetworkId | undefined
158
+ address?: string | undefined
159
+ }) => {
160
+ pruneExpired()
161
+ if (partial.chainId !== undefined) {
162
+ const idx = pendingSwitches.findIndex(p => p.chainId === partial.chainId)
163
+ if (idx !== -1) {
164
+ pendingSwitches[idx] = {
165
+ ...pendingSwitches[idx]!,
166
+ ...partial,
167
+ at: now()
168
+ }
169
+ return
170
+ }
171
+ }
172
+ pendingSwitches.push({ ...partial, at: now() })
173
+ }
115
174
  const offAwaiting = eventManager.on('awaitingWallet', data => {
116
175
  if (data.operation === 'switchNetwork' && data.chainId !== undefined) {
117
- lastSwitch = { chainId: data.chainId, at: now() }
176
+ armOrRearm({ chainId: data.chainId })
118
177
  }
119
178
  })
120
179
  const offSwitched = eventManager.on('networkSwitched', data => {
121
- lastSwitch = { chainId: data.network.id, at: now() }
180
+ armOrRearm({ chainId: data.network.id, address: data.address })
122
181
  })
123
182
  const detachBusWatchers = () => {
124
183
  offAwaiting()
125
184
  offSwitched()
126
185
  }
127
186
 
187
+ // Emission budget for THIS attachment, shared across both streams —
188
+ // suppressed echoes don't spend it.
189
+ let emitted = 0
190
+ let capNotified = false
191
+ const overCap = (): boolean => {
192
+ if (emitted >= MAX_LIFECYCLE_EVENTS_PER_ATTACHMENT) {
193
+ // Mark the trip ONCE — a silent cap would be indistinguishable from a
194
+ // provider that simply went quiet.
195
+ if (!capNotified) {
196
+ capNotified = true
197
+ emitUnavailable('emission_cap')
198
+ }
199
+ return true
200
+ }
201
+ return false
202
+ }
203
+
128
204
  // Defense-in-depth: a connector whose subscribe THROWS (e.g. a provider whose
129
205
  // `.on` rejects an un-proxied handler across the iframe bridge) must degrade to
130
206
  // "unavailable", never break the connect path that called us.
131
207
  let unsub: (() => void) | undefined
132
- // Emission budget for THIS attachment — suppressed echoes don't spend it.
133
- let emitted = 0
134
- let capNotified = false
135
- try {
136
- unsub = subscribe(e => {
137
- // The whole body is guarded: wallet SDKs dispatch their listeners
138
- // unguarded (e.g. TonConnect's bare `forEach`), so a throw here — from
139
- // `getRouting()` or any future edit — would abort delivery to the
140
- // wallet's OWN later subscribers, including one resolving a pending
141
- // connect. Telemetry must never do that.
142
- try {
143
- if (emitted >= MAX_LIFECYCLE_EVENTS_PER_ATTACHMENT) {
144
- // Mark the trip ONCE — a silent cap would be indistinguishable from a
145
- // provider that simply went quiet.
146
- if (!capNotified) {
147
- capNotified = true
148
- emitUnavailable('emission_cap')
208
+ if (subscribe) {
209
+ try {
210
+ unsub = subscribe(e => {
211
+ // The whole body is guarded: wallet SDKs dispatch their listeners
212
+ // unguarded (e.g. TonConnect's bare `forEach`), so a throw here would
213
+ // abort delivery to the wallet's OWN later subscribers, including one
214
+ // resolving a pending connect. Telemetry must never do that.
215
+ try {
216
+ if (overCap()) return
217
+ const routing = getRouting()
218
+ switch (e.type) {
219
+ case 'accountChanged':
220
+ emitted++
221
+ eventManager.emit('walletAccountChanged', routing)
222
+ return
223
+ case 'chainChanged': {
224
+ pruneExpired()
225
+ const idx =
226
+ e.chainId !== undefined
227
+ ? pendingSwitches.findIndex(
228
+ p => p.chainId !== undefined && p.chainId === e.chainId
229
+ )
230
+ : -1
231
+ if (idx !== -1) {
232
+ // Consume the echo: a UWC switch produces exactly one wallet echo, so
233
+ // a LATER chainChanged to the same chain inside the window is a genuine
234
+ // user-initiated switch-back and must not be suppressed. Only the
235
+ // chain half is consumed — a still-pending address echo (if the
236
+ // switch also changed address format) must still be checked; drop the
237
+ // entry entirely once nothing is left pending on it.
238
+ const entry = pendingSwitches[idx]!
239
+ if (entry.address !== undefined) {
240
+ pendingSwitches[idx] = { ...entry, chainId: undefined }
241
+ } else {
242
+ pendingSwitches.splice(idx, 1)
243
+ }
244
+ return
245
+ }
246
+ emitted++
247
+ eventManager.emit('walletChainChanged', {
248
+ ...routing,
249
+ ...(e.chainId !== undefined ? { chainId: e.chainId } : {})
250
+ })
251
+ return
252
+ }
253
+ case 'disconnect':
254
+ emitted++
255
+ eventManager.emit('walletDisconnected', routing)
256
+ return
257
+ case 'sessionExpired':
258
+ emitted++
259
+ eventManager.emit('walletSessionExpired', routing)
260
+ return
149
261
  }
150
- return
262
+ } catch {
263
+ // Swallowed: better to drop one lifecycle record than to break the
264
+ // wallet's event dispatch.
151
265
  }
152
- const routing = getRouting()
153
- switch (e.type) {
154
- case 'accountChanged':
155
- emitted++
156
- eventManager.emit('walletAccountChanged', routing)
157
- return
158
- case 'chainChanged': {
159
- const isEcho =
160
- !!lastSwitch &&
161
- e.chainId !== undefined &&
162
- lastSwitch.chainId === e.chainId &&
163
- now() - lastSwitch.at <= echoWindowMs
164
- if (isEcho) {
165
- // Consume the echo: a UWC switch produces exactly one wallet echo, so
166
- // a LATER chainChanged to the same chain inside the window is a genuine
167
- // user-initiated switch-back and must not be suppressed.
168
- lastSwitch = undefined
169
- return
266
+ })
267
+ } catch {
268
+ unsub = undefined
269
+ }
270
+ if (!unsub) {
271
+ // The connector accepted the subscribe call but this session has no
272
+ // observable telemetry surface (returned undefined or threw). Over the
273
+ // bridge, that absence is the bridge's doing, not the provider's.
274
+ emitUnavailable(
275
+ runtime === 'iframe'
276
+ ? 'bridge_subscription_unavailable'
277
+ : 'provider_subscription_unavailable'
278
+ )
279
+ }
280
+ }
281
+
282
+ let unsubAccounts: (() => void) | undefined
283
+ if (subscribeAccounts) {
284
+ try {
285
+ unsubAccounts = subscribeAccounts(accounts => {
286
+ try {
287
+ if (overCap()) return
288
+ const primary = accounts[0]
289
+ pruneExpired()
290
+ const idx =
291
+ accounts.length > 0
292
+ ? pendingSwitches.findIndex(
293
+ p => p.address !== undefined && p.address === primary
294
+ )
295
+ : -1
296
+ if (idx !== -1) {
297
+ // Same one-shot consume as the chainChanged echo above, mirrored
298
+ // for the address half.
299
+ const entry = pendingSwitches[idx]!
300
+ if (entry.chainId !== undefined) {
301
+ pendingSwitches[idx] = { ...entry, address: undefined }
302
+ } else {
303
+ pendingSwitches.splice(idx, 1)
170
304
  }
171
- emitted++
172
- eventManager.emit('walletChainChanged', {
173
- ...routing,
174
- ...(e.chainId !== undefined ? { chainId: e.chainId } : {})
175
- })
176
305
  return
177
306
  }
178
- case 'disconnect':
179
- emitted++
180
- eventManager.emit('walletDisconnected', routing)
181
- return
182
- case 'sessionExpired':
183
- emitted++
184
- eventManager.emit('walletSessionExpired', routing)
185
- return
307
+ emitted++
308
+ opts.onAccountsChanged?.(accounts)
309
+ } catch {
310
+ // Swallowed: never break the wallet SDK's own listener dispatch.
186
311
  }
187
- } catch {
188
- // Swallowed: better to drop one lifecycle record than to break the
189
- // wallet's event dispatch.
190
- }
191
- })
192
- } catch {
193
- unsub = undefined
312
+ })
313
+ } catch {
314
+ unsubAccounts = undefined
315
+ }
194
316
  }
195
317
 
196
- // The connector accepted the subscribe call but this session has no observable
197
- // surface (returned undefined) or threw — treat as "unavailable" and don't
198
- // leave the bus watchers attached.
199
- if (!unsub) {
318
+ if (!unsub && !unsubAccounts) {
200
319
  detachBusWatchers()
201
- // `onLifecycleEvent` ran but exposed no live subscription surface for this
202
- // session (returned undefined or threw). Over the bridge, that absence is the
203
- // bridge's doing, not the provider's.
204
- emitUnavailable(
205
- runtime === 'iframe'
206
- ? 'bridge_subscription_unavailable'
207
- : 'provider_subscription_unavailable'
208
- )
209
320
  return () => {}
210
321
  }
211
322
 
212
323
  return () => {
213
324
  detachBusWatchers()
214
- unsub()
325
+ unsub?.()
326
+ unsubAccounts?.()
215
327
  }
216
328
  }
@@ -520,41 +520,6 @@ describe('normalizeEvent', () => {
520
520
  expect(JSON.stringify(out)).not.toContain('0xAbC123')
521
521
  })
522
522
 
523
- it('normalizes relayDisconnected as an unclassified walletConnect signal, message scrubbed', () => {
524
- const out = normalizeEvent(
525
- 'relayDisconnected',
526
- {
527
- error: {
528
- type: 'unknown',
529
- message: 'transport closed 0xAbC1230000000000000000000000000000000000'
530
- }
531
- },
532
- SDK_SESSION,
533
- NOW
534
- )
535
- expect(out.connectionMode).toBe('walletConnect')
536
- expect(out.source).toBe('provider')
537
- // A drop is observational, NOT a failure — must never carry a failureKind
538
- // (else it inflates failure metrics).
539
- expect(out.failureKind).toBeUndefined()
540
- expect(JSON.stringify(out)).not.toContain('0xAbC123')
541
- })
542
-
543
- it('normalizes relayDisconnected with no reason (clean drop) — no error field', () => {
544
- const out = normalizeEvent('relayDisconnected', {}, SDK_SESSION, NOW)
545
- expect(out.connectionMode).toBe('walletConnect')
546
- expect(out.error).toBeUndefined()
547
- expect(out.failureKind).toBeUndefined()
548
- })
549
-
550
- it('normalizes relayReconnected as an unclassified walletConnect signal', () => {
551
- const out = normalizeEvent('relayReconnected', undefined, SDK_SESSION, NOW)
552
- expect(out.name).toBe('relayReconnected')
553
- expect(out.connectionMode).toBe('walletConnect')
554
- expect(out.source).toBe('provider')
555
- expect(out.failureKind).toBeUndefined()
556
- })
557
-
558
523
  it('normalizes walletConnectInitError as a walletConnect client_init_failed, message scrubbed', () => {
559
524
  const out = normalizeEvent(
560
525
  'walletConnectInitError',
@@ -154,14 +154,6 @@ export interface UWCTelemetryEvent {
154
154
  * handoffCovered to avoid double-counting.
155
155
  */
156
156
  handoffCovered?: boolean | undefined
157
- /** `pageVisibleDuringHandoff` only: how long the page was hidden for that segment (ms). */
158
- hiddenMs?: number | undefined
159
- /** Handoff terminals: cumulative time the page was backgrounded during the handoff (ms). */
160
- backgroundedMs?: number | undefined
161
- /** Handoff terminals: how many times the page was backgrounded during the handoff. */
162
- backgroundCount?: number | undefined
163
- /** Handoff terminals, WalletConnect only: relay-socket drops seen while the handoff was pending. */
164
- relayDropCount?: number | undefined
165
157
  }
166
158
 
167
159
  /**
@@ -370,10 +362,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
370
362
  durationMs: d.durationMs,
371
363
  timedOut: d.timedOut,
372
364
  timeoutMs: d.timeoutMs,
373
- targetAlreadyActive: d.targetAlreadyActive,
374
- backgroundedMs: d.backgroundedMs,
375
- backgroundCount: d.backgroundCount,
376
- relayDropCount: d.relayDropCount
365
+ targetAlreadyActive: d.targetAlreadyActive
377
366
  }
378
367
  }
379
368
  case 'walletRejected': {
@@ -392,10 +381,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
392
381
  targetAlreadyActive: d.targetAlreadyActive,
393
382
  errorCode: d.errorCode,
394
383
  failureKind: d.failureKind,
395
- source: d.source,
396
- backgroundedMs: d.backgroundedMs,
397
- backgroundCount: d.backgroundCount,
398
- relayDropCount: d.relayDropCount
384
+ source: d.source
399
385
  }
400
386
  }
401
387
  case 'walletTimedOut': {
@@ -410,10 +396,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
410
396
  handoffId: d.handoffId,
411
397
  durationMs: d.durationMs,
412
398
  timeoutMs: d.timeoutMs,
413
- targetAlreadyActive: d.targetAlreadyActive,
414
- backgroundedMs: d.backgroundedMs,
415
- backgroundCount: d.backgroundCount,
416
- relayDropCount: d.relayDropCount
399
+ targetAlreadyActive: d.targetAlreadyActive
417
400
  }
418
401
  }
419
402
  case 'walletFailed': {
@@ -433,10 +416,7 @@ function buildTelemetryEvent<K extends UWCEventName>(
433
416
  errorCode: d.errorCode,
434
417
  failureKind: d.failureKind,
435
418
  source: d.source,
436
- error: { type: d.error.type, message: scrubMessage(d.error.message) },
437
- backgroundedMs: d.backgroundedMs,
438
- backgroundCount: d.backgroundCount,
439
- relayDropCount: d.relayDropCount
419
+ error: { type: d.error.type, message: scrubMessage(d.error.message) }
440
420
  }
441
421
  }
442
422
  case 'capabilitiesUpdated': {
@@ -468,15 +448,8 @@ function buildTelemetryEvent<K extends UWCEventName>(
468
448
  }
469
449
  case 'pageHiddenDuringHandoff':
470
450
  case 'pageVisibleDuringHandoff': {
471
- // Cast to the visible shape (superset). `hiddenMs` is absent on the hidden
472
- // event undefined dropped by compactUndefined.
473
- const d = data as UWCEventMap['pageVisibleDuringHandoff']
474
- return {
475
- ...base,
476
- handoffId: d.handoffId,
477
- platform: d.platform,
478
- hiddenMs: d.hiddenMs
479
- }
451
+ const d = data as UWCEventMap['pageHiddenDuringHandoff']
452
+ return { ...base, handoffId: d.handoffId, platform: d.platform }
480
453
  }
481
454
  case 'bridgeError': {
482
455
  const d = data as UWCEventMap['bridgeError']
@@ -500,31 +473,6 @@ function buildTelemetryEvent<K extends UWCEventName>(
500
473
  error: { type: d.error.type, message: scrubMessage(d.error.message) }
501
474
  }
502
475
  }
503
- case 'relayDisconnected': {
504
- const d = data as UWCEventMap['relayDisconnected']
505
- // The relay socket dropped. Observational, NOT a failure: a drop only
506
- // matters if a round-trip was in flight — deliberately left without a
507
- // `failureKind` so it never inflates failure metrics. Pair with
508
- // `relayReconnected` (same sdkSessionId) for the dead-window duration.
509
- return {
510
- ...base,
511
- connectionMode: 'walletConnect',
512
- source: 'provider',
513
- ...(d.error
514
- ? {
515
- error: {
516
- type: d.error.type,
517
- message: scrubMessage(d.error.message)
518
- }
519
- }
520
- : {})
521
- }
522
- }
523
- case 'relayReconnected': {
524
- // Socket recovered. Name-only + mode; the diagnostic value is the timestamp
525
- // relative to the paired `relayDisconnected`.
526
- return { ...base, connectionMode: 'walletConnect', source: 'provider' }
527
- }
528
476
  case 'walletConnectInitError': {
529
477
  const d = data as UWCEventMap['walletConnectInitError']
530
478
  // SignClient.init failed — the WC client never came up. Distinct from