@meshconnect/uwc-core 1.2.3 → 1.3.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 (37) hide show
  1. package/dist/events.d.ts +39 -0
  2. package/dist/events.d.ts.map +1 -1
  3. package/dist/events.js +4 -0
  4. package/dist/events.js.map +1 -1
  5. package/dist/observability/focus-tracker.d.ts +30 -11
  6. package/dist/observability/focus-tracker.d.ts.map +1 -1
  7. package/dist/observability/focus-tracker.js +54 -18
  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 +25 -6
  11. package/dist/observability/instrument-handoff.js.map +1 -1
  12. package/dist/observability/relay-drop-tracker.d.ts +37 -0
  13. package/dist/observability/relay-drop-tracker.d.ts.map +1 -0
  14. package/dist/observability/relay-drop-tracker.js +30 -0
  15. package/dist/observability/relay-drop-tracker.js.map +1 -0
  16. package/dist/observability/telemetry.d.ts +8 -0
  17. package/dist/observability/telemetry.d.ts.map +1 -1
  18. package/dist/observability/telemetry.js +32 -5
  19. package/dist/observability/telemetry.js.map +1 -1
  20. package/dist/universal-wallet-connector.d.ts.map +1 -1
  21. package/dist/universal-wallet-connector.js +18 -11
  22. package/dist/universal-wallet-connector.js.map +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/package.json +3 -3
  26. package/src/events.ts +48 -1
  27. package/src/observability/focus-tracker.test.ts +95 -8
  28. package/src/observability/focus-tracker.ts +72 -22
  29. package/src/observability/instrument-handoff.test.ts +62 -0
  30. package/src/observability/instrument-handoff.ts +37 -6
  31. package/src/observability/relay-drop-tracker.test.ts +84 -0
  32. package/src/observability/relay-drop-tracker.ts +53 -0
  33. package/src/observability/telemetry.test.ts +18 -0
  34. package/src/observability/telemetry.ts +41 -6
  35. package/src/universal-wallet-connector.observability.test.ts +54 -8
  36. package/src/universal-wallet-connector.ts +22 -12
  37. package/src/version.ts +1 -1
@@ -47,6 +47,68 @@ describe('instrumentHandoff', () => {
47
47
  expect(end.timedOut).toBe(false)
48
48
  })
49
49
 
50
+ it('counts relay-socket drops seen during the handoff onto the terminal', async () => {
51
+ const em = new EventManager()
52
+ const seen = record(em)
53
+ const wcCtx: HandoffContext = { ...CTX, connectionMode: 'walletConnect' }
54
+ await instrumentHandoff(
55
+ { eventManager: em, timeoutMs: 0 },
56
+ wcCtx,
57
+ async () => {
58
+ // The relay socket drops (and recovers) while the sign request is pending.
59
+ em.emit('relayDisconnected', undefined)
60
+ em.emit('relayReconnected', undefined)
61
+ em.emit('relayDisconnected', undefined)
62
+ return 'sig'
63
+ }
64
+ )
65
+ const end = seen.find(s => s.name === 'walletSucceeded')?.data as {
66
+ relayDropCount: number
67
+ backgroundCount: number
68
+ backgroundedMs: number
69
+ }
70
+ expect(end.relayDropCount).toBe(2)
71
+ // No backgrounding occurred in this test runtime.
72
+ expect(end.backgroundCount).toBe(0)
73
+ expect(end.backgroundedMs).toBe(0)
74
+ })
75
+
76
+ it('stops counting relay drops once the handoff settles', async () => {
77
+ const em = new EventManager()
78
+ const seen = record(em)
79
+ const wcCtx: HandoffContext = { ...CTX, connectionMode: 'walletConnect' }
80
+ await instrumentHandoff(
81
+ { eventManager: em, timeoutMs: 0 },
82
+ wcCtx,
83
+ async () => 'sig'
84
+ )
85
+ // A drop after settle must be ignored (listener removed) and never throw.
86
+ expect(() => em.emit('relayDisconnected', undefined)).not.toThrow()
87
+ const end = seen.find(s => s.name === 'walletSucceeded')?.data as {
88
+ relayDropCount: number
89
+ }
90
+ expect(end.relayDropCount).toBe(0)
91
+ })
92
+
93
+ it('omits relayDropCount for non-WalletConnect handoffs (no misattribution)', async () => {
94
+ const em = new EventManager()
95
+ const seen = record(em)
96
+ // A WC relay flap firing during an injected handoff must NOT be attributed to it.
97
+ await instrumentHandoff(
98
+ { eventManager: em, timeoutMs: 0 },
99
+ CTX,
100
+ async () => {
101
+ em.emit('relayDisconnected', undefined)
102
+ return 'sig'
103
+ }
104
+ )
105
+ const end = seen.find(s => s.name === 'walletSucceeded')?.data as {
106
+ relayDropCount?: number
107
+ }
108
+ // Undefined on the bus → dropped by compactUndefined on the telemetry egress.
109
+ expect(end.relayDropCount).toBeUndefined()
110
+ })
111
+
50
112
  it('carries targetAlreadyActive through awaitingWallet and the terminal', async () => {
51
113
  const em = new EventManager()
52
114
  const seen = record(em)
@@ -15,6 +15,7 @@ import {
15
15
  } from '../utils/to-wallet-error'
16
16
  import { derivePlatform, deriveWalletFlowType } from './telemetry'
17
17
  import { trackFocusDuringHandoff } from './focus-tracker'
18
+ import { trackRelayDropsDuringHandoff } from './relay-drop-tracker'
18
19
 
19
20
  /** Routing facets known at the moment of a wallet handoff. No address, no payload. */
20
21
  export interface HandoffContext {
@@ -141,7 +142,32 @@ export async function instrumentHandoff<T>(
141
142
  // Observe page-visibility transitions ONLY while this handoff is pending — the
142
143
  // app-switch signal for deep-link flows (left for the wallet / came back). The
143
144
  // teardown in `finally` removes the listeners the instant the op settles.
144
- const stopFocus = trackFocusDuringHandoff(eventManager, handoffId, platform)
145
+ // Pass `now` through so backgroundedMs shares the same clock as durationMs
146
+ // (consistent, and deterministic under an injected clock in tests).
147
+ const focus = trackFocusDuringHandoff(
148
+ eventManager,
149
+ handoffId,
150
+ platform,
151
+ undefined,
152
+ undefined,
153
+ now
154
+ )
155
+
156
+ // Count relay-socket drops seen while this handoff is pending (WalletConnect
157
+ // only — the tracker is a no-op for other modes so a WC relay flap isn't
158
+ // misattributed to an injected/TON handoff in flight at the same time).
159
+ const relayDrops = trackRelayDropsDuringHandoff(
160
+ eventManager,
161
+ context.connectionMode
162
+ )
163
+
164
+ // Backgrounding + relay-drop summary, snapshotted onto each terminal so ONE
165
+ // event answers "backgrounded? how long? socket dropped? recovered?".
166
+ // `relayDropCount` is undefined off WalletConnect so `compactUndefined` drops it.
167
+ const handoffMetrics = () => ({
168
+ ...focus.summary(),
169
+ ...relayDrops.summary()
170
+ })
145
171
 
146
172
  // Same routing facets as `awaitingWallet` so every terminal is self-describing
147
173
  // (no handoffId self-join needed to know which wallet / chain / mode it closed).
@@ -164,7 +190,8 @@ export async function instrumentHandoff<T>(
164
190
  operation: context.operation,
165
191
  durationMs: now() - start,
166
192
  timeoutMs,
167
- ...routing
193
+ ...routing,
194
+ ...handoffMetrics()
168
195
  })
169
196
  }, timeoutMs)
170
197
  : undefined
@@ -177,7 +204,8 @@ export async function instrumentHandoff<T>(
177
204
  durationMs: now() - start,
178
205
  timedOut,
179
206
  timeoutMs: timedOut ? timeoutMs : undefined,
180
- ...routing
207
+ ...routing,
208
+ ...handoffMetrics()
181
209
  })
182
210
  return result
183
211
  } catch (error) {
@@ -207,7 +235,8 @@ export async function instrumentHandoff<T>(
207
235
  errorCode,
208
236
  failureKind,
209
237
  source,
210
- ...routing
238
+ ...routing,
239
+ ...handoffMetrics()
211
240
  })
212
241
  } else {
213
242
  eventManager.emit('walletFailed', {
@@ -220,12 +249,14 @@ export async function instrumentHandoff<T>(
220
249
  errorCode,
221
250
  failureKind,
222
251
  source,
223
- ...routing
252
+ ...routing,
253
+ ...handoffMetrics()
224
254
  })
225
255
  }
226
256
  throw error
227
257
  } finally {
228
258
  if (timer) clearTimeout(timer)
229
- stopFocus()
259
+ relayDrops.stop()
260
+ focus.stop()
230
261
  }
231
262
  }
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { EventManager } from '../managers/event-manager'
3
+ import { trackRelayDropsDuringHandoff } from './relay-drop-tracker'
4
+
5
+ describe('trackRelayDropsDuringHandoff', () => {
6
+ it('counts each relay drop seen during a WalletConnect handoff', () => {
7
+ const em = new EventManager()
8
+ const tracker = trackRelayDropsDuringHandoff(em, 'walletConnect')
9
+
10
+ expect(tracker.summary()).toEqual({ relayDropCount: 0 })
11
+
12
+ em.emit('relayDisconnected', undefined)
13
+ em.emit('relayDisconnected', undefined)
14
+ expect(tracker.summary()).toEqual({ relayDropCount: 2 })
15
+ })
16
+
17
+ it('reports undefined (not 0) off WalletConnect so the terminal omits the field', () => {
18
+ // A `0` on an injected handoff would read as "a relay we watched, and it
19
+ // held" — but no relay is in play, so the field must be absent entirely.
20
+ const em = new EventManager()
21
+ for (const mode of ['injected', 'tonConnect'] as const) {
22
+ const tracker = trackRelayDropsDuringHandoff(em, mode)
23
+ expect(tracker.summary()).toEqual({ relayDropCount: undefined })
24
+ }
25
+ })
26
+
27
+ it('does NOT subscribe off WalletConnect — a relay flap on the shared bus is ignored', () => {
28
+ const em = new EventManager()
29
+ const tracker = trackRelayDropsDuringHandoff(em, 'injected')
30
+
31
+ // The bus is client-global; a WC relay drop firing while an injected handoff
32
+ // is in flight must not be attributed to it.
33
+ em.emit('relayDisconnected', undefined)
34
+ em.emit('relayDisconnected', undefined)
35
+ expect(tracker.summary()).toEqual({ relayDropCount: undefined })
36
+ })
37
+
38
+ it('stops counting after stop() — drops outside the blind window do not accrue', () => {
39
+ const em = new EventManager()
40
+ const tracker = trackRelayDropsDuringHandoff(em, 'walletConnect')
41
+
42
+ em.emit('relayDisconnected', undefined)
43
+ tracker.stop()
44
+ em.emit('relayDisconnected', undefined)
45
+ em.emit('relayDisconnected', undefined)
46
+
47
+ expect(tracker.summary()).toEqual({ relayDropCount: 1 })
48
+ })
49
+
50
+ it('only counts relayDisconnected — reconnects and other bus events are ignored', () => {
51
+ const em = new EventManager()
52
+ const tracker = trackRelayDropsDuringHandoff(em, 'walletConnect')
53
+
54
+ em.emit('relayDisconnected', undefined)
55
+ em.emit('relayReconnected', undefined)
56
+ em.emit('relayReconnected', undefined)
57
+
58
+ expect(tracker.summary()).toEqual({ relayDropCount: 1 })
59
+ })
60
+
61
+ it('stop() is a safe no-op off WalletConnect (nothing was subscribed)', () => {
62
+ const em = new EventManager()
63
+ const tracker = trackRelayDropsDuringHandoff(em, 'tonConnect')
64
+ expect(() => tracker.stop()).not.toThrow()
65
+ expect(tracker.summary()).toEqual({ relayDropCount: undefined })
66
+ })
67
+
68
+ it('two concurrent WalletConnect handoffs count independently and unsubscribe independently', () => {
69
+ // The bus is shared; each tracker owns its own listener, so stopping one
70
+ // must not silence the other (and both saw the same drop).
71
+ const em = new EventManager()
72
+ const a = trackRelayDropsDuringHandoff(em, 'walletConnect')
73
+ const b = trackRelayDropsDuringHandoff(em, 'walletConnect')
74
+
75
+ em.emit('relayDisconnected', undefined)
76
+ expect(a.summary()).toEqual({ relayDropCount: 1 })
77
+ expect(b.summary()).toEqual({ relayDropCount: 1 })
78
+
79
+ a.stop()
80
+ em.emit('relayDisconnected', undefined)
81
+ expect(a.summary()).toEqual({ relayDropCount: 1 })
82
+ expect(b.summary()).toEqual({ relayDropCount: 2 })
83
+ })
84
+ })
@@ -0,0 +1,53 @@
1
+ import type { ConnectionMode } from '@meshconnect/uwc-types'
2
+ import type { EventManager } from '../managers/event-manager'
3
+
4
+ /** Handle returned by `trackRelayDropsDuringHandoff`. */
5
+ export interface RelayDropHandoffTracker {
6
+ /**
7
+ * Remove the relay-drop subscription. Always call on settle (no-op off
8
+ * WalletConnect, where nothing was subscribed).
9
+ */
10
+ stop: () => void
11
+ /**
12
+ * Relay-drop count at call time, or `undefined` off WalletConnect. Undefined
13
+ * (not `0`) so the terminal's `compactUndefined` omits the field entirely — a
14
+ * `0` on an injected/TON handoff would falsely imply "a WC relay we watched,
15
+ * and it held", when in fact no relay was in play.
16
+ */
17
+ summary: () => { relayDropCount: number | undefined }
18
+ }
19
+
20
+ /**
21
+ * While a wallet handoff is pending, count relay-socket drops seen on the UWC
22
+ * event bus. The WalletConnect client emits `relayDisconnected` once per outage
23
+ * (see `attachRelayLifecycleListeners`); this is the CARE-263 signal — an
24
+ * iOS-suspended WebView drops the relay socket mid-handoff and strands the
25
+ * in-flight sign/connect response until WC core's slow heartbeat reconnects, so
26
+ * the drop count on a terminal explains a long/failed handoff that focus timing
27
+ * alone can't.
28
+ *
29
+ * WalletConnect ONLY. The relay bus is client-global, so an injected or TON
30
+ * handoff that happens to be in flight during a WC relay flap would otherwise
31
+ * misattribute the drop; off WalletConnect this is a no-op tracker whose
32
+ * `summary()` reports `undefined`. Mirrors `trackFocusDuringHandoff`: attach
33
+ * right after `awaitingWallet`, and `stop()` (called the instant the op settles)
34
+ * removes the subscription so drops are only counted during the blind window.
35
+ */
36
+ export function trackRelayDropsDuringHandoff(
37
+ eventManager: EventManager,
38
+ connectionMode: ConnectionMode
39
+ ): RelayDropHandoffTracker {
40
+ if (connectionMode !== 'walletConnect') {
41
+ return { stop: () => {}, summary: () => ({ relayDropCount: undefined }) }
42
+ }
43
+
44
+ let relayDropCount = 0
45
+ const off = eventManager.on('relayDisconnected', () => {
46
+ relayDropCount++
47
+ })
48
+
49
+ return {
50
+ stop: off,
51
+ summary: () => ({ relayDropCount })
52
+ }
53
+ }
@@ -520,6 +520,24 @@ describe('normalizeEvent', () => {
520
520
  expect(JSON.stringify(out)).not.toContain('0xAbC123')
521
521
  })
522
522
 
523
+ it('normalizes relayDisconnected as an unclassified walletConnect signal (void, no reason)', () => {
524
+ // The relayer carries no payload, so the event is void — never an error, and
525
+ // observational (no failureKind, so it can't inflate failure metrics).
526
+ const out = normalizeEvent('relayDisconnected', undefined, SDK_SESSION, NOW)
527
+ expect(out.connectionMode).toBe('walletConnect')
528
+ expect(out.source).toBe('provider')
529
+ expect(out.error).toBeUndefined()
530
+ expect(out.failureKind).toBeUndefined()
531
+ })
532
+
533
+ it('normalizes relayReconnected as an unclassified walletConnect signal', () => {
534
+ const out = normalizeEvent('relayReconnected', undefined, SDK_SESSION, NOW)
535
+ expect(out.name).toBe('relayReconnected')
536
+ expect(out.connectionMode).toBe('walletConnect')
537
+ expect(out.source).toBe('provider')
538
+ expect(out.failureKind).toBeUndefined()
539
+ })
540
+
523
541
  it('normalizes walletConnectInitError as a walletConnect client_init_failed, message scrubbed', () => {
524
542
  const out = normalizeEvent(
525
543
  'walletConnectInitError',
@@ -154,6 +154,14 @@ 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
157
165
  }
158
166
 
159
167
  /**
@@ -362,7 +370,10 @@ function buildTelemetryEvent<K extends UWCEventName>(
362
370
  durationMs: d.durationMs,
363
371
  timedOut: d.timedOut,
364
372
  timeoutMs: d.timeoutMs,
365
- targetAlreadyActive: d.targetAlreadyActive
373
+ targetAlreadyActive: d.targetAlreadyActive,
374
+ backgroundedMs: d.backgroundedMs,
375
+ backgroundCount: d.backgroundCount,
376
+ relayDropCount: d.relayDropCount
366
377
  }
367
378
  }
368
379
  case 'walletRejected': {
@@ -381,7 +392,10 @@ function buildTelemetryEvent<K extends UWCEventName>(
381
392
  targetAlreadyActive: d.targetAlreadyActive,
382
393
  errorCode: d.errorCode,
383
394
  failureKind: d.failureKind,
384
- source: d.source
395
+ source: d.source,
396
+ backgroundedMs: d.backgroundedMs,
397
+ backgroundCount: d.backgroundCount,
398
+ relayDropCount: d.relayDropCount
385
399
  }
386
400
  }
387
401
  case 'walletTimedOut': {
@@ -396,7 +410,10 @@ function buildTelemetryEvent<K extends UWCEventName>(
396
410
  handoffId: d.handoffId,
397
411
  durationMs: d.durationMs,
398
412
  timeoutMs: d.timeoutMs,
399
- targetAlreadyActive: d.targetAlreadyActive
413
+ targetAlreadyActive: d.targetAlreadyActive,
414
+ backgroundedMs: d.backgroundedMs,
415
+ backgroundCount: d.backgroundCount,
416
+ relayDropCount: d.relayDropCount
400
417
  }
401
418
  }
402
419
  case 'walletFailed': {
@@ -416,7 +433,10 @@ function buildTelemetryEvent<K extends UWCEventName>(
416
433
  errorCode: d.errorCode,
417
434
  failureKind: d.failureKind,
418
435
  source: d.source,
419
- error: { type: d.error.type, message: scrubMessage(d.error.message) }
436
+ error: { type: d.error.type, message: scrubMessage(d.error.message) },
437
+ backgroundedMs: d.backgroundedMs,
438
+ backgroundCount: d.backgroundCount,
439
+ relayDropCount: d.relayDropCount
420
440
  }
421
441
  }
422
442
  case 'capabilitiesUpdated': {
@@ -448,8 +468,15 @@ function buildTelemetryEvent<K extends UWCEventName>(
448
468
  }
449
469
  case 'pageHiddenDuringHandoff':
450
470
  case 'pageVisibleDuringHandoff': {
451
- const d = data as UWCEventMap['pageHiddenDuringHandoff']
452
- return { ...base, handoffId: d.handoffId, platform: d.platform }
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
+ }
453
480
  }
454
481
  case 'bridgeError': {
455
482
  const d = data as UWCEventMap['bridgeError']
@@ -473,6 +500,14 @@ function buildTelemetryEvent<K extends UWCEventName>(
473
500
  error: { type: d.error.type, message: scrubMessage(d.error.message) }
474
501
  }
475
502
  }
503
+ case 'relayDisconnected':
504
+ case 'relayReconnected': {
505
+ // Relay transport lifecycle (WalletConnect). Observational — NOT a failure,
506
+ // so deliberately no `failureKind` (never inflates failure metrics). The
507
+ // relayer carries no payload; pair the two by sdkSessionId + timestamp for
508
+ // the dead-window duration.
509
+ return { ...base, connectionMode: 'walletConnect', source: 'provider' }
510
+ }
476
511
  case 'walletConnectInitError': {
477
512
  const d = data as UWCEventMap['walletConnectInitError']
478
513
  // SignClient.init failed — the WC client never came up. Distinct from
@@ -26,10 +26,10 @@ const reattachLifecycleMock = vi.fn()
26
26
  const switchNetworkMock = vi.fn()
27
27
  const sendTransactionMock = vi.fn()
28
28
  const getWalletCapabilitiesMock = vi.fn()
29
- // Captures the positional args the façade passes to the WalletConnect connector
30
- // (config, networkRpcMap, onRelayError, onInitError) so a test can invoke the
31
- // detached-error sinks and assert the emitted telemetry — this is the only place
32
- // the sink→event wiring (and its arg order) is exercised.
29
+ // Captures the args the façade passes to the WalletConnect connector
30
+ // (config, networkRpcMap, telemetryObserver) so a test can invoke the relay
31
+ // telemetry sinks and assert the emitted event — this is the only place the
32
+ // sink→bus wiring is exercised.
33
33
  const wcConnectorCapture: { args: unknown[] } = { args: [] }
34
34
 
35
35
  const connectedSession: Session = {
@@ -187,10 +187,14 @@ describe('UniversalWalletConnector — observability wiring', () => {
187
187
  walletConnectConfig: { projectId: 'test' }
188
188
  })
189
189
 
190
- // The façade passes the connector (config, networkRpcMap, onRelayError,
191
- // onInitError). Grabbing arg[3] and firing it guards both the wiring and the
192
- // positional arg order a swap with onRelayError would surface the wrong event.
193
- const onInitError = wcConnectorCapture.args[3] as (e: unknown) => void
190
+ // The façade passes the connector (config, networkRpcMap, telemetryObserver).
191
+ // Grabbing the observer's onInitError sink and firing it guards the wiring;
192
+ // the named field can't be confused with onRelayError the way a positional
193
+ // arg could (the reason #3350 moved to a typed options object).
194
+ const telemetryObserver = wcConnectorCapture.args[2] as {
195
+ onInitError: (e: unknown) => void
196
+ }
197
+ const onInitError = telemetryObserver.onInitError
194
198
  expect(typeof onInitError).toBe('function')
195
199
  onInitError(
196
200
  new Error(
@@ -207,6 +211,48 @@ describe('UniversalWalletConnector — observability wiring', () => {
207
211
  expect(JSON.stringify(initError)).not.toContain('0xAbC123')
208
212
  })
209
213
 
214
+ it('wires the relay observer sinks → each emits its own telemetry event to the observer', () => {
215
+ // The whole point of #3350: a single typed observer whose named sinks map
216
+ // 1:1 to bus events. Fire each and assert the right event lands — this is
217
+ // the connector→bus lambda wiring, previously only covered end-to-end.
218
+ const events: UWCTelemetryEvent[] = []
219
+ const observer: UWCObserver = { onEvent: e => events.push(e) }
220
+ new UniversalWalletConnector({
221
+ networks,
222
+ wallets,
223
+ observer,
224
+ walletConnectConfig: { projectId: 'test' }
225
+ })
226
+
227
+ const telemetryObserver = wcConnectorCapture.args[2] as {
228
+ onRelayError: (e: unknown) => void
229
+ onRelayDisconnect: () => void
230
+ onRelayReconnect: () => void
231
+ }
232
+
233
+ telemetryObserver.onRelayError(
234
+ new Error(
235
+ 'WebSocket closed abnormally 0xAbC1230000000000000000000000000000000000'
236
+ )
237
+ )
238
+ telemetryObserver.onRelayDisconnect()
239
+ telemetryObserver.onRelayReconnect()
240
+
241
+ const relayError = events.find(e => e.name === 'relayError')
242
+ expect(relayError).toBeDefined()
243
+ expect(relayError!.connectionMode).toBe('walletConnect')
244
+ // Same egress scrubbing as every other error record.
245
+ expect(JSON.stringify(relayError)).not.toContain('0xAbC123')
246
+
247
+ const dropped = events.find(e => e.name === 'relayDisconnected')
248
+ expect(dropped).toBeDefined()
249
+ expect(dropped!.connectionMode).toBe('walletConnect')
250
+
251
+ const recovered = events.find(e => e.name === 'relayReconnected')
252
+ expect(recovered).toBeDefined()
253
+ expect(recovered!.connectionMode).toBe('walletConnect')
254
+ })
255
+
210
256
  it('stamps a consumer-supplied correlationId (string) as its own field', async () => {
211
257
  const events: UWCTelemetryEvent[] = []
212
258
  const uwc = new UniversalWalletConnector({
@@ -314,18 +314,28 @@ export class UniversalWalletConnector {
314
314
  new WalletConnectConnector(
315
315
  configuredWalletConnectConfig,
316
316
  this.networkRpcMap,
317
- // Surface relay-socket failures (e.g. code 3000 JWT clock skew) that are
318
- // detached from the connect handoff, as a telemetry event.
319
- (error: unknown) =>
320
- this.eventManager.emit('relayError', {
321
- error: toWalletError(error)
322
- }),
323
- // Surface a failed SignClient.init as its own signal — otherwise it has no
324
- // dedicated telemetry and only appears as a generic connect failure.
325
- (error: unknown) =>
326
- this.eventManager.emit('walletConnectInitError', {
327
- error: toWalletError(error)
328
- })
317
+ {
318
+ // Surface relay-socket failures (e.g. code 3000 JWT clock skew) that are
319
+ // detached from the connect handoff, as a telemetry event.
320
+ onRelayError: (error: unknown) =>
321
+ this.eventManager.emit('relayError', {
322
+ error: toWalletError(error)
323
+ }),
324
+ // Surface a failed SignClient.init as its own signal — otherwise it has no
325
+ // dedicated telemetry and only appears as a generic connect failure.
326
+ onInitError: (error: unknown) =>
327
+ this.eventManager.emit('walletConnectInitError', {
328
+ error: toWalletError(error)
329
+ }),
330
+ // Relay transport dropped (once per outage) — the FACT is the signal; the
331
+ // relayer carries no payload, so emit the bare event (CARE-263).
332
+ onRelayDisconnect: () =>
333
+ this.eventManager.emit('relayDisconnected', undefined),
334
+ // Relay transport recovered — pair with `relayDisconnected` (same
335
+ // sdkSessionId) to measure the dead window.
336
+ onRelayReconnect: () =>
337
+ this.eventManager.emit('relayReconnected', undefined)
338
+ }
329
339
  )
330
340
  )
331
341
  }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs — do not edit.
2
- export const UWC_CORE_VERSION = '1.2.3'
2
+ export const UWC_CORE_VERSION = '1.3.0'