@meshconnect/uwc-core 1.2.3-snapshot.dcfdbbf → 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 +40 -5
- 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/lifecycle-bridge.d.ts +13 -31
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -1
- package/dist/observability/lifecycle-bridge.js +92 -167
- package/dist/observability/lifecycle-bridge.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/services/connection-service.d.ts +0 -43
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +9 -163
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +1 -4
- package/dist/services/network-switch-service.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 +5 -5
- package/src/events.ts +50 -6
- 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/lifecycle-bridge.test.ts +0 -197
- package/src/observability/lifecycle-bridge.ts +99 -211
- 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/services/connection-service.test.ts +0 -272
- package/src/services/connection-service.ts +10 -190
- package/src/services/network-switch-service.test.ts +0 -33
- package/src/services/network-switch-service.ts +1 -4
- package/src/universal-wallet-connector.observability.test.ts +54 -8
- package/src/universal-wallet-connector.ts +22 -12
- package/src/version.ts +1 -1
package/src/events.ts
CHANGED
|
@@ -94,6 +94,24 @@ export interface UWCEventMap {
|
|
|
94
94
|
error: WalletError
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The WalletConnect relay transport socket dropped (relayer `disconnect`) — e.g.
|
|
99
|
+
* iOS suspending the WebView during a wallet deep-link silently kills the socket.
|
|
100
|
+
* Distinct from `relayError` (an abnormal close that surfaced an ERROR): this is
|
|
101
|
+
* the lifecycle FACT of the socket going down. Emitted once per outage (WC core
|
|
102
|
+
* re-emits `disconnect` on each failed reconnect); pair with `relayReconnected`
|
|
103
|
+
* to measure the dead window that leaves an in-flight sign/connect response
|
|
104
|
+
* undelivered (CARE-263). The relayer passes no payload, so this carries no
|
|
105
|
+
* reason. Telemetry-only.
|
|
106
|
+
*/
|
|
107
|
+
relayDisconnected: void
|
|
108
|
+
/**
|
|
109
|
+
* The WalletConnect relay transport reconnected after a `relayDisconnected`.
|
|
110
|
+
* The elapsed time since the paired disconnect is the socket's dead window.
|
|
111
|
+
* Telemetry-only.
|
|
112
|
+
*/
|
|
113
|
+
relayReconnected: void
|
|
114
|
+
|
|
97
115
|
/**
|
|
98
116
|
* `SignClient.init` (WalletConnect) rejected — the client could not be created, so
|
|
99
117
|
* no connect could proceed. Rejects on a storage (IndexedDB) or crypto init failure,
|
|
@@ -125,11 +143,8 @@ export interface UWCEventMap {
|
|
|
125
143
|
|
|
126
144
|
/** Network switching is in progress. Fires on start and end. */
|
|
127
145
|
networkSwitching: { isLoading: boolean; isWaitingForUserApproval: boolean }
|
|
128
|
-
/** Network switch succeeded.
|
|
129
|
-
|
|
130
|
-
* the lifecycle bridge to suppress the wallet's own echo of this same
|
|
131
|
-
* change (see attachLifecycle's echo-suppression window). */
|
|
132
|
-
networkSwitched: { network: Network; address?: string }
|
|
146
|
+
/** Network switch succeeded. */
|
|
147
|
+
networkSwitched: { network: Network }
|
|
133
148
|
|
|
134
149
|
/** Wallet capabilities were refreshed. */
|
|
135
150
|
capabilitiesUpdated: { capabilities: Record<string, EVMCapabilities> }
|
|
@@ -208,6 +223,11 @@ export interface UWCEventMap {
|
|
|
208
223
|
timeoutMs?: number | undefined
|
|
209
224
|
/** switchNetwork only: target chain already the active one (no-op switch). */
|
|
210
225
|
targetAlreadyActive?: boolean | undefined
|
|
226
|
+
/** Backgrounding during this handoff: cumulative hidden time (ms) + transition count. */
|
|
227
|
+
backgroundedMs?: number | undefined
|
|
228
|
+
backgroundCount?: number | undefined
|
|
229
|
+
/** WalletConnect only: relay-socket drops seen while this handoff was pending. */
|
|
230
|
+
relayDropCount?: number | undefined
|
|
211
231
|
}
|
|
212
232
|
/** The user explicitly rejected (4001 / WalletError.type === 'rejected'). Closes a handoff. */
|
|
213
233
|
walletRejected: {
|
|
@@ -225,6 +245,11 @@ export interface UWCEventMap {
|
|
|
225
245
|
errorCode?: string | undefined
|
|
226
246
|
failureKind?: FailureKind | undefined
|
|
227
247
|
source?: ErrorSource | undefined
|
|
248
|
+
/** Backgrounding during this handoff: cumulative hidden time (ms) + transition count. */
|
|
249
|
+
backgroundedMs?: number | undefined
|
|
250
|
+
backgroundCount?: number | undefined
|
|
251
|
+
/** WalletConnect only: relay-socket drops seen while this handoff was pending. */
|
|
252
|
+
relayDropCount?: number | undefined
|
|
228
253
|
}
|
|
229
254
|
/** The op failed for any non-rejection reason. Closes a handoff. */
|
|
230
255
|
walletFailed: {
|
|
@@ -243,6 +268,11 @@ export interface UWCEventMap {
|
|
|
243
268
|
errorCode?: string | undefined
|
|
244
269
|
failureKind?: FailureKind | undefined
|
|
245
270
|
source?: ErrorSource | undefined
|
|
271
|
+
/** Backgrounding during this handoff: cumulative hidden time (ms) + transition count. */
|
|
272
|
+
backgroundedMs?: number | undefined
|
|
273
|
+
backgroundCount?: number | undefined
|
|
274
|
+
/** WalletConnect only: relay-socket drops seen while this handoff was pending. */
|
|
275
|
+
relayDropCount?: number | undefined
|
|
246
276
|
}
|
|
247
277
|
/**
|
|
248
278
|
* Side marker: the deadline passed with the op still pending. OBSERVATIONAL —
|
|
@@ -262,6 +292,11 @@ export interface UWCEventMap {
|
|
|
262
292
|
timeoutMs: number
|
|
263
293
|
/** switchNetwork only: target chain already the active one (no-op switch). */
|
|
264
294
|
targetAlreadyActive?: boolean | undefined
|
|
295
|
+
/** Backgrounding so far (marker fires mid-flight): hidden time (ms) + transition count. */
|
|
296
|
+
backgroundedMs?: number | undefined
|
|
297
|
+
backgroundCount?: number | undefined
|
|
298
|
+
/** WalletConnect only: relay-socket drops seen so far while this handoff was pending. */
|
|
299
|
+
relayDropCount?: number | undefined
|
|
265
300
|
}
|
|
266
301
|
|
|
267
302
|
// ---- Provider lifecycle (telemetry-only) ----------------------------------
|
|
@@ -324,7 +359,12 @@ export interface UWCEventMap {
|
|
|
324
359
|
* handoff (a bfcache restore can fire both `pageshow` and `visibilitychange`
|
|
325
360
|
* for one physical return). Telemetry-only.
|
|
326
361
|
*/
|
|
327
|
-
pageVisibleDuringHandoff: {
|
|
362
|
+
pageVisibleDuringHandoff: {
|
|
363
|
+
handoffId: string
|
|
364
|
+
platform: Platform
|
|
365
|
+
/** Duration the page was hidden for THIS hide→show segment (ms). */
|
|
366
|
+
hiddenMs?: number | undefined
|
|
367
|
+
}
|
|
328
368
|
|
|
329
369
|
/**
|
|
330
370
|
* Back-compat catch-all. Fires after every other event above.
|
|
@@ -348,6 +388,8 @@ export const TELEMETRY_ONLY_EVENTS: ReadonlySet<UWCEventName> = new Set([
|
|
|
348
388
|
'walletDiscoveryFailed',
|
|
349
389
|
'bridgeError',
|
|
350
390
|
'relayError',
|
|
391
|
+
'relayDisconnected',
|
|
392
|
+
'relayReconnected',
|
|
351
393
|
'walletConnectInitError',
|
|
352
394
|
'walletAccountChanged',
|
|
353
395
|
'walletChainChanged',
|
|
@@ -375,6 +417,8 @@ const EVENT_NAME_FLAGS: Record<UWCEventName, true> = {
|
|
|
375
417
|
walletDiscoveryFailed: true,
|
|
376
418
|
bridgeError: true,
|
|
377
419
|
relayError: true,
|
|
420
|
+
relayDisconnected: true,
|
|
421
|
+
relayReconnected: true,
|
|
378
422
|
walletConnectInitError: true,
|
|
379
423
|
connecting: true,
|
|
380
424
|
connected: true,
|
|
@@ -88,17 +88,92 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
88
88
|
|
|
89
89
|
const { doc, fire } = makeFakeDoc()
|
|
90
90
|
const { win } = makeFakeWin()
|
|
91
|
-
|
|
91
|
+
let t = 1000
|
|
92
|
+
trackFocusDuringHandoff(em, 'h-1', 'mobile', doc, win, () => t)
|
|
92
93
|
|
|
93
94
|
doc.visibilityState = 'hidden'
|
|
94
95
|
doc.hidden = true
|
|
95
|
-
fire('visibilitychange')
|
|
96
|
+
fire('visibilitychange') // hidden at t=1000
|
|
97
|
+
t = 1700
|
|
96
98
|
doc.visibilityState = 'visible'
|
|
97
99
|
doc.hidden = false
|
|
98
|
-
fire('visibilitychange')
|
|
100
|
+
fire('visibilitychange') // visible at t=1700 → 700ms hidden
|
|
99
101
|
|
|
100
102
|
expect(hidden).toEqual([{ handoffId: 'h-1', platform: 'mobile' }])
|
|
101
|
-
expect(visible).toEqual([
|
|
103
|
+
expect(visible).toEqual([
|
|
104
|
+
{ handoffId: 'h-1', platform: 'mobile', hiddenMs: 700 }
|
|
105
|
+
])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('summary() reports background count + cumulative hidden time, including an open segment', () => {
|
|
109
|
+
const em = new EventManager()
|
|
110
|
+
const { doc, fire } = makeFakeDoc()
|
|
111
|
+
let t = 0
|
|
112
|
+
const tracker = trackFocusDuringHandoff(
|
|
113
|
+
em,
|
|
114
|
+
'h-sum',
|
|
115
|
+
'mobile',
|
|
116
|
+
doc,
|
|
117
|
+
undefined,
|
|
118
|
+
() => t
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
// First hide→show: 100 → 600 = 500ms hidden.
|
|
122
|
+
t = 100
|
|
123
|
+
doc.visibilityState = 'hidden'
|
|
124
|
+
fire('visibilitychange')
|
|
125
|
+
t = 600
|
|
126
|
+
doc.visibilityState = 'visible'
|
|
127
|
+
fire('visibilitychange')
|
|
128
|
+
expect(tracker.summary()).toEqual({
|
|
129
|
+
backgroundCount: 1,
|
|
130
|
+
backgroundedMs: 500
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Second hide, still hidden when summary() runs at t=1000 → +200 open segment.
|
|
134
|
+
t = 800
|
|
135
|
+
doc.visibilityState = 'hidden'
|
|
136
|
+
fire('visibilitychange')
|
|
137
|
+
t = 1000
|
|
138
|
+
expect(tracker.summary()).toEqual({
|
|
139
|
+
backgroundCount: 2,
|
|
140
|
+
backgroundedMs: 700
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('records an already-hidden start as a real hidden transition (pairs 1:1, counted)', () => {
|
|
145
|
+
const em = new EventManager()
|
|
146
|
+
const hidden: Hidden[] = []
|
|
147
|
+
const visible: Visible[] = []
|
|
148
|
+
em.on('pageHiddenDuringHandoff', e => hidden.push(e))
|
|
149
|
+
em.on('pageVisibleDuringHandoff', e => visible.push(e))
|
|
150
|
+
|
|
151
|
+
const { doc, fire } = makeFakeDoc()
|
|
152
|
+
doc.visibilityState = 'hidden'
|
|
153
|
+
doc.hidden = true
|
|
154
|
+
let t = 500
|
|
155
|
+
const tracker = trackFocusDuringHandoff(
|
|
156
|
+
em,
|
|
157
|
+
'h-seed',
|
|
158
|
+
'mobile',
|
|
159
|
+
doc,
|
|
160
|
+
undefined,
|
|
161
|
+
() => t
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
// Attaching while hidden emits a REAL pageHidden so the later visible pairs 1:1.
|
|
165
|
+
expect(hidden).toEqual([{ handoffId: 'h-seed', platform: 'mobile' }])
|
|
166
|
+
expect(visible).toEqual([])
|
|
167
|
+
expect(tracker.summary()).toEqual({ backgroundCount: 1, backgroundedMs: 0 })
|
|
168
|
+
|
|
169
|
+
// Returning at t=900 → paired pageVisible carrying the 400ms segment.
|
|
170
|
+
t = 900
|
|
171
|
+
doc.visibilityState = 'visible'
|
|
172
|
+
doc.hidden = false
|
|
173
|
+
fire('visibilitychange')
|
|
174
|
+
expect(visible).toEqual([
|
|
175
|
+
{ handoffId: 'h-seed', platform: 'mobile', hiddenMs: 400 }
|
|
176
|
+
])
|
|
102
177
|
})
|
|
103
178
|
|
|
104
179
|
it('treats a bfcache pageshow ON THE WINDOW as a "returned" signal', () => {
|
|
@@ -137,7 +212,13 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
137
212
|
em.on('pageVisibleDuringHandoff', e => visible.push(e))
|
|
138
213
|
|
|
139
214
|
const { win, fire } = makeFakeWin()
|
|
140
|
-
const stop = trackFocusDuringHandoff(
|
|
215
|
+
const { stop } = trackFocusDuringHandoff(
|
|
216
|
+
em,
|
|
217
|
+
'h-2c',
|
|
218
|
+
'mobile',
|
|
219
|
+
undefined,
|
|
220
|
+
win
|
|
221
|
+
)
|
|
141
222
|
fire('pageshow')
|
|
142
223
|
|
|
143
224
|
expect(visible).toEqual([{ handoffId: 'h-2c', platform: 'mobile' }])
|
|
@@ -150,7 +231,13 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
150
231
|
em.on('pageHiddenDuringHandoff', e => hidden.push(e))
|
|
151
232
|
|
|
152
233
|
const { doc, fire } = makeFakeDoc()
|
|
153
|
-
const stop = trackFocusDuringHandoff(
|
|
234
|
+
const { stop } = trackFocusDuringHandoff(
|
|
235
|
+
em,
|
|
236
|
+
'h-2d',
|
|
237
|
+
'desktop',
|
|
238
|
+
doc,
|
|
239
|
+
undefined
|
|
240
|
+
)
|
|
154
241
|
doc.visibilityState = 'hidden'
|
|
155
242
|
fire('visibilitychange')
|
|
156
243
|
|
|
@@ -166,7 +253,7 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
166
253
|
|
|
167
254
|
const { doc, fire: fireDoc } = makeFakeDoc()
|
|
168
255
|
const { win, listeners: winListeners, fire: fireWin } = makeFakeWin()
|
|
169
|
-
const stop = trackFocusDuringHandoff(em, 'h-3', 'desktop', doc, win)
|
|
256
|
+
const { stop } = trackFocusDuringHandoff(em, 'h-3', 'desktop', doc, win)
|
|
170
257
|
stop()
|
|
171
258
|
|
|
172
259
|
doc.visibilityState = 'hidden'
|
|
@@ -179,7 +266,7 @@ describe('trackFocusDuringHandoff', () => {
|
|
|
179
266
|
|
|
180
267
|
it('is a no-op (no throw) when there is neither document nor window (SSR)', () => {
|
|
181
268
|
const em = new EventManager()
|
|
182
|
-
const stop = trackFocusDuringHandoff(
|
|
269
|
+
const { stop } = trackFocusDuringHandoff(
|
|
183
270
|
em,
|
|
184
271
|
'h-4',
|
|
185
272
|
'unknown',
|
|
@@ -35,30 +35,58 @@ function resolveWindow(): FocusWindowLike | undefined {
|
|
|
35
35
|
return win && typeof win.addEventListener === 'function' ? win : undefined
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Handle returned by `trackFocusDuringHandoff`. */
|
|
39
|
+
export interface FocusHandoffTracker {
|
|
40
|
+
/** Remove the visibility listeners. Always call on settle (no-op if none attached). */
|
|
41
|
+
stop: () => void
|
|
42
|
+
/**
|
|
43
|
+
* Backgrounding summary at call time: how many times the page was hidden and the
|
|
44
|
+
* cumulative hidden duration (ms) during the handoff — including an in-progress
|
|
45
|
+
* hidden segment if the handoff settles while still backgrounded.
|
|
46
|
+
*/
|
|
47
|
+
summary: () => { backgroundCount: number; backgroundedMs: number }
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
/**
|
|
39
51
|
* While a wallet handoff is pending, emit a FACT on every tab-visibility
|
|
40
52
|
* transition — `pageHiddenDuringHandoff` when the page is backgrounded (the user
|
|
41
|
-
* likely left for the wallet app), `pageVisibleDuringHandoff`
|
|
42
|
-
* (including a bfcache `pageshow`, which iOS uses when
|
|
53
|
+
* likely left for the wallet app), `pageVisibleDuringHandoff` (with the segment's
|
|
54
|
+
* `hiddenMs`) when it returns (including a bfcache `pageshow`, which iOS uses when
|
|
55
|
+
* coming back from a wallet).
|
|
43
56
|
*
|
|
44
|
-
* This is the raw signal behind "did the user actually app-switch to the wallet
|
|
45
|
-
* for deep-link flows — it does NOT interpret abandonment. The
|
|
46
|
-
* scoped to the single handoff: attach right after `awaitingWallet`,
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
57
|
+
* This is the raw signal behind "did the user actually app-switch to the wallet,
|
|
58
|
+
* and for how long?" for deep-link flows — it does NOT interpret abandonment. The
|
|
59
|
+
* listeners are scoped to the single handoff: attach right after `awaitingWallet`,
|
|
60
|
+
* and `stop()` (called the instant the op settles) removes them, so the page's
|
|
61
|
+
* visibility is only observed during the blind window. SSR-safe (no-op when there
|
|
62
|
+
* is neither `document` nor `window`). `summary()` lets the handoff stamp the
|
|
63
|
+
* cumulative backgrounding onto its terminal.
|
|
50
64
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
65
|
+
* One non-transition case: if the tracker attaches while the page is ALREADY
|
|
66
|
+
* hidden (handoff initiated from a backgrounded tab), it emits an initial
|
|
67
|
+
* synthetic `pageHiddenDuringHandoff` at attach time so the eventual return pairs
|
|
68
|
+
* 1:1 and the hidden segment is counted. So `pageHiddenDuringHandoff` is "every
|
|
69
|
+
* transition into hidden, plus an already-hidden start", not strictly transitions.
|
|
53
70
|
*/
|
|
54
71
|
export function trackFocusDuringHandoff(
|
|
55
72
|
eventManager: EventManager,
|
|
56
73
|
handoffId: string,
|
|
57
74
|
platform: Platform,
|
|
58
75
|
doc: FocusDocumentLike | undefined = resolveDocument(),
|
|
59
|
-
win: FocusWindowLike | undefined = resolveWindow()
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
win: FocusWindowLike | undefined = resolveWindow(),
|
|
77
|
+
now: () => number = Date.now
|
|
78
|
+
): FocusHandoffTracker {
|
|
79
|
+
let backgroundCount = 0
|
|
80
|
+
let totalHiddenMs = 0
|
|
81
|
+
let hiddenSince: number | undefined
|
|
82
|
+
|
|
83
|
+
const summary = (): { backgroundCount: number; backgroundedMs: number } => ({
|
|
84
|
+
backgroundCount,
|
|
85
|
+
backgroundedMs:
|
|
86
|
+
totalHiddenMs + (hiddenSince !== undefined ? now() - hiddenSince : 0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (!doc && !win) return { stop: () => {}, summary }
|
|
62
90
|
|
|
63
91
|
// One physical return can fire BOTH `pageshow` (window) and a
|
|
64
92
|
// `visibilitychange`→visible (document) — an iOS bfcache restore does. Dedupe
|
|
@@ -67,12 +95,23 @@ export function trackFocusDuringHandoff(
|
|
|
67
95
|
const emitState = (state: 'hidden' | 'visible') => {
|
|
68
96
|
if (state === lastState) return
|
|
69
97
|
lastState = state
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
if (state === 'hidden') {
|
|
99
|
+
hiddenSince = now()
|
|
100
|
+
backgroundCount++
|
|
101
|
+
eventManager.emit('pageHiddenDuringHandoff', { handoffId, platform })
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
let hiddenMs: number | undefined
|
|
105
|
+
if (hiddenSince !== undefined) {
|
|
106
|
+
hiddenMs = now() - hiddenSince
|
|
107
|
+
totalHiddenMs += hiddenMs
|
|
108
|
+
hiddenSince = undefined
|
|
109
|
+
}
|
|
110
|
+
eventManager.emit('pageVisibleDuringHandoff', {
|
|
111
|
+
handoffId,
|
|
112
|
+
platform,
|
|
113
|
+
...(hiddenMs !== undefined ? { hiddenMs } : {})
|
|
114
|
+
})
|
|
76
115
|
}
|
|
77
116
|
|
|
78
117
|
const onVisibility = () => {
|
|
@@ -89,8 +128,19 @@ export function trackFocusDuringHandoff(
|
|
|
89
128
|
doc?.addEventListener('visibilitychange', onVisibility)
|
|
90
129
|
win?.addEventListener('pageshow', onPageShow)
|
|
91
130
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
131
|
+
// Handoff started while ALREADY hidden (initiated from a backgrounded tab):
|
|
132
|
+
// record it as a real hidden transition so the eventual `pageVisibleDuringHandoff`
|
|
133
|
+
// pairs 1:1 and summary()/hiddenMs count the segment. `emitState`'s dedup makes
|
|
134
|
+
// the first subsequent visibilitychange→hidden a no-op.
|
|
135
|
+
if (doc?.visibilityState === 'hidden' || doc?.hidden === true) {
|
|
136
|
+
emitState('hidden')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
stop: () => {
|
|
141
|
+
doc?.removeEventListener('visibilitychange', onVisibility)
|
|
142
|
+
win?.removeEventListener('pageshow', onPageShow)
|
|
143
|
+
},
|
|
144
|
+
summary
|
|
95
145
|
}
|
|
96
146
|
}
|
|
@@ -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
|
}
|