@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.
- package/dist/events.d.ts +39 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -0
- package/dist/events.js.map +1 -1
- package/dist/observability/focus-tracker.d.ts +30 -11
- package/dist/observability/focus-tracker.d.ts.map +1 -1
- package/dist/observability/focus-tracker.js +54 -18
- package/dist/observability/focus-tracker.js.map +1 -1
- package/dist/observability/instrument-handoff.d.ts.map +1 -1
- package/dist/observability/instrument-handoff.js +25 -6
- package/dist/observability/instrument-handoff.js.map +1 -1
- package/dist/observability/relay-drop-tracker.d.ts +37 -0
- package/dist/observability/relay-drop-tracker.d.ts.map +1 -0
- package/dist/observability/relay-drop-tracker.js +30 -0
- package/dist/observability/relay-drop-tracker.js.map +1 -0
- package/dist/observability/telemetry.d.ts +8 -0
- package/dist/observability/telemetry.d.ts.map +1 -1
- package/dist/observability/telemetry.js +32 -5
- package/dist/observability/telemetry.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +18 -11
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/events.ts +48 -1
- package/src/observability/focus-tracker.test.ts +95 -8
- package/src/observability/focus-tracker.ts +72 -22
- package/src/observability/instrument-handoff.test.ts +62 -0
- package/src/observability/instrument-handoff.ts +37 -6
- package/src/observability/relay-drop-tracker.test.ts +84 -0
- package/src/observability/relay-drop-tracker.ts +53 -0
- package/src/observability/telemetry.test.ts +18 -0
- package/src/observability/telemetry.ts +41 -6
- package/src/universal-wallet-connector.observability.test.ts +54 -8
- package/src/universal-wallet-connector.ts +22 -12
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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
|
|
30
|
-
// (config, networkRpcMap,
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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,
|
|
191
|
-
// onInitError
|
|
192
|
-
//
|
|
193
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
export const UWC_CORE_VERSION = '1.3.0'
|