@meshconnect/uwc-core 1.0.3 → 1.1.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/README.md +247 -76
- package/dist/events.d.ts +241 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +77 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/logger/create-logger.d.ts +32 -0
- package/dist/logger/create-logger.d.ts.map +1 -0
- package/dist/logger/create-logger.js +79 -0
- package/dist/logger/create-logger.js.map +1 -0
- package/dist/managers/event-manager.d.ts.map +1 -1
- package/dist/managers/event-manager.js +5 -1
- package/dist/managers/event-manager.js.map +1 -1
- package/dist/observability/connect-observer.d.ts +33 -0
- package/dist/observability/connect-observer.d.ts.map +1 -0
- package/dist/observability/connect-observer.js +115 -0
- package/dist/observability/connect-observer.js.map +1 -0
- package/dist/observability/focus-tracker.d.ts +42 -0
- package/dist/observability/focus-tracker.d.ts.map +1 -0
- package/dist/observability/focus-tracker.js +57 -0
- package/dist/observability/focus-tracker.js.map +1 -0
- package/dist/observability/instrument-handoff.d.ts +54 -0
- package/dist/observability/instrument-handoff.d.ts.map +1 -0
- package/dist/observability/instrument-handoff.js +173 -0
- package/dist/observability/instrument-handoff.js.map +1 -0
- package/dist/observability/lifecycle-bridge.d.ts +69 -0
- package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
- package/dist/observability/lifecycle-bridge.js +155 -0
- package/dist/observability/lifecycle-bridge.js.map +1 -0
- package/dist/observability/scrub.d.ts +31 -0
- package/dist/observability/scrub.d.ts.map +1 -0
- package/dist/observability/scrub.js +167 -0
- package/dist/observability/scrub.js.map +1 -0
- package/dist/observability/telemetry.d.ts +157 -0
- package/dist/observability/telemetry.d.ts.map +1 -0
- package/dist/observability/telemetry.js +373 -0
- package/dist/observability/telemetry.js.map +1 -0
- package/dist/services/connection-service.d.ts +23 -1
- package/dist/services/connection-service.d.ts.map +1 -1
- package/dist/services/connection-service.js +128 -19
- package/dist/services/connection-service.js.map +1 -1
- package/dist/services/network-switch-service.d.ts +2 -1
- package/dist/services/network-switch-service.d.ts.map +1 -1
- package/dist/services/network-switch-service.js +45 -16
- package/dist/services/network-switch-service.js.map +1 -1
- package/dist/services/signature-service.d.ts +14 -7
- package/dist/services/signature-service.d.ts.map +1 -1
- package/dist/services/signature-service.js +56 -58
- package/dist/services/signature-service.js.map +1 -1
- package/dist/services/transaction-service.d.ts +19 -5
- package/dist/services/transaction-service.d.ts.map +1 -1
- package/dist/services/transaction-service.js +55 -38
- package/dist/services/transaction-service.js.map +1 -1
- package/dist/universal-wallet-connector.d.ts +56 -1
- package/dist/universal-wallet-connector.d.ts.map +1 -1
- package/dist/universal-wallet-connector.js +207 -110
- package/dist/universal-wallet-connector.js.map +1 -1
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +41 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/resolve-session-provider.d.ts +10 -0
- package/dist/utils/resolve-session-provider.d.ts.map +1 -0
- package/dist/utils/resolve-session-provider.js +21 -0
- package/dist/utils/resolve-session-provider.js.map +1 -0
- package/dist/utils/to-wallet-error.d.ts +35 -0
- package/dist/utils/to-wallet-error.d.ts.map +1 -0
- package/dist/utils/to-wallet-error.js +130 -0
- package/dist/utils/to-wallet-error.js.map +1 -0
- package/dist/utils/validate-wallet-session.d.ts +15 -0
- package/dist/utils/validate-wallet-session.d.ts.map +1 -0
- package/dist/utils/validate-wallet-session.js +29 -0
- package/dist/utils/validate-wallet-session.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -6
- package/src/events.ts +326 -14
- package/src/index.ts +10 -0
- package/src/logger/create-logger.test.ts +126 -0
- package/src/logger/create-logger.ts +112 -0
- package/src/managers/event-manager.test.ts +55 -0
- package/src/managers/event-manager.ts +5 -1
- package/src/observability/connect-observer.test.ts +214 -0
- package/src/observability/connect-observer.ts +128 -0
- package/src/observability/focus-tracker.test.ts +191 -0
- package/src/observability/focus-tracker.ts +96 -0
- package/src/observability/instrument-handoff.test.ts +397 -0
- package/src/observability/instrument-handoff.ts +231 -0
- package/src/observability/lifecycle-bridge.test.ts +398 -0
- package/src/observability/lifecycle-bridge.ts +216 -0
- package/src/observability/scrub.test.ts +183 -0
- package/src/observability/scrub.ts +174 -0
- package/src/observability/telemetry.test.ts +549 -0
- package/src/observability/telemetry.ts +546 -0
- package/src/services/connection-service.test.ts +261 -1
- package/src/services/connection-service.ts +167 -38
- package/src/services/network-switch-service.test.ts +51 -1
- package/src/services/network-switch-service.ts +56 -24
- package/src/services/signature-service.test.ts +181 -76
- package/src/services/signature-service.ts +75 -85
- package/src/services/transaction-service.test.ts +270 -69
- package/src/services/transaction-service.ts +74 -51
- package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
- package/src/universal-wallet-connector.observability.test.ts +610 -0
- package/src/universal-wallet-connector.test.ts +27 -146
- package/src/universal-wallet-connector.ts +279 -142
- package/src/utils/id.test.ts +15 -0
- package/src/utils/id.ts +48 -0
- package/src/utils/resolve-session-provider.test.ts +54 -0
- package/src/utils/resolve-session-provider.ts +25 -0
- package/src/utils/to-wallet-error.test.ts +181 -0
- package/src/utils/to-wallet-error.ts +152 -0
- package/src/utils/validate-wallet-session.test.ts +77 -0
- package/src/utils/validate-wallet-session.ts +40 -0
- package/src/version.test.ts +11 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionMode,
|
|
3
|
+
WalletError,
|
|
4
|
+
WalletFlowType,
|
|
5
|
+
Platform,
|
|
6
|
+
LogLevel,
|
|
7
|
+
Namespace,
|
|
8
|
+
NetworkId,
|
|
9
|
+
FailureKind,
|
|
10
|
+
ErrorSource,
|
|
11
|
+
LifecycleUnavailableReason,
|
|
12
|
+
Session
|
|
13
|
+
} from '@meshconnect/uwc-types'
|
|
14
|
+
import type { UWCEventName, UWCEventMap, UWCOperation } from '../events'
|
|
15
|
+
import { UWC_CORE_VERSION } from '../version'
|
|
16
|
+
import { scrubMessage } from './scrub'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Telemetry record schema version. Bump on any breaking change to the
|
|
20
|
+
* `UWCTelemetryEvent` shape so a downstream consumer can branch on it.
|
|
21
|
+
*/
|
|
22
|
+
export const TELEMETRY_SCHEMA_VERSION = 1
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Egress bounds for `networkMismatch`. `walletNetworkIds` comes from catalog
|
|
26
|
+
* wallet metadata (`provider.supportedNetworkIds`), not live wallet input — but
|
|
27
|
+
* it is unbounded upstream, so cap cardinality and element length here as
|
|
28
|
+
* defense-in-depth. Elements are NOT scrubbed: a legitimate
|
|
29
|
+
* `solana:<32-char-base58-ref>` CAIP-2 id would be eaten by the base58 rule.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_NETWORK_MISMATCH_IDS = 20
|
|
32
|
+
const MAX_NETWORK_ID_LENGTH = 64
|
|
33
|
+
|
|
34
|
+
function capNetworkMismatch(
|
|
35
|
+
mismatch:
|
|
36
|
+
| { walletNetworkIds: string[]; configuredNetworkIds: string[] }
|
|
37
|
+
| undefined
|
|
38
|
+
): { walletNetworkIds: string[]; configuredNetworkIds: string[] } | undefined {
|
|
39
|
+
if (!mismatch) return undefined
|
|
40
|
+
const cap = (ids: string[]) =>
|
|
41
|
+
ids
|
|
42
|
+
.slice(0, MAX_NETWORK_MISMATCH_IDS)
|
|
43
|
+
.map(id => id.slice(0, MAX_NETWORK_ID_LENGTH))
|
|
44
|
+
return {
|
|
45
|
+
walletNetworkIds: cap(mismatch.walletNetworkIds),
|
|
46
|
+
configuredNetworkIds: cap(mismatch.configuredNetworkIds)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A normalized, PII-safe telemetry record derived from a `UWCEventMap` event.
|
|
52
|
+
*
|
|
53
|
+
* This is the ONLY shape that reaches `observer.onEvent`. It is the PII boundary:
|
|
54
|
+
* it has no field for a signing payload, signature, raw transaction, or wallet
|
|
55
|
+
* address — leaking those is structurally impossible, not merely discouraged.
|
|
56
|
+
*/
|
|
57
|
+
export interface UWCTelemetryEvent {
|
|
58
|
+
/**
|
|
59
|
+
* Record schema version — the literal type IS the version this package
|
|
60
|
+
* emits, so a consumer can branch exhaustively (`event.schemaVersion === 1`)
|
|
61
|
+
* and gets a compile break when a bump lands. Import the runtime value as
|
|
62
|
+
* `TELEMETRY_SCHEMA_VERSION`.
|
|
63
|
+
*/
|
|
64
|
+
schemaVersion: typeof TELEMETRY_SCHEMA_VERSION
|
|
65
|
+
/**
|
|
66
|
+
* The `@meshconnect/uwc-core` version that produced the record (generated
|
|
67
|
+
* from package.json at build time) — aggregate failures per released
|
|
68
|
+
* version to spot version-specific regressions.
|
|
69
|
+
*/
|
|
70
|
+
uwcVersion: string
|
|
71
|
+
/** The originating event name. */
|
|
72
|
+
name: UWCEventName
|
|
73
|
+
/** Per-connector-instance id (stamped on every event). */
|
|
74
|
+
sdkSessionId: string
|
|
75
|
+
/**
|
|
76
|
+
* Consumer-supplied correlation id (e.g. their `link_sess_id`), present only
|
|
77
|
+
* when supplied — never aliased to `sdkSessionId`. Lets UWC events join the
|
|
78
|
+
* consumer's existing telemetry without a separate mapping.
|
|
79
|
+
*/
|
|
80
|
+
correlationId?: string | undefined
|
|
81
|
+
/** ms since epoch when the record was produced. */
|
|
82
|
+
timestamp: number
|
|
83
|
+
/** Wallet-boundary op, when the event relates to one. */
|
|
84
|
+
operation?: UWCOperation | undefined
|
|
85
|
+
connectionMode?: ConnectionMode | undefined
|
|
86
|
+
/** CAIP namespace (eip155 | solana | tron | tvm …) — never an address. */
|
|
87
|
+
namespace?: Namespace | undefined
|
|
88
|
+
/** CAIP chain id (e.g. `eip155:1`) — never an address. */
|
|
89
|
+
chainId?: NetworkId | undefined
|
|
90
|
+
/** Catalog wallet id — never an address. */
|
|
91
|
+
walletId?: string | undefined
|
|
92
|
+
/** Correlates a handoff start with its terminal. */
|
|
93
|
+
handoffId?: string | undefined
|
|
94
|
+
durationMs?: number | undefined
|
|
95
|
+
walletFlowType?: WalletFlowType | undefined
|
|
96
|
+
platform?: Platform | undefined
|
|
97
|
+
/** A `walletTimedOut` marker fired before the terminal (late completion). */
|
|
98
|
+
timedOut?: boolean | undefined
|
|
99
|
+
/** Marker deadline — present on `walletTimedOut` and on a late terminal. */
|
|
100
|
+
timeoutMs?: number | undefined
|
|
101
|
+
/** switchNetwork only: target chain was already the active one (no-op switch). */
|
|
102
|
+
targetAlreadyActive?: boolean | undefined
|
|
103
|
+
/** Non-success outcomes: stringified error code when reliably known. */
|
|
104
|
+
errorCode?: string | undefined
|
|
105
|
+
/** Non-success outcomes: coarse cause bucket. `unknown` when not classifiable. */
|
|
106
|
+
failureKind?: FailureKind | undefined
|
|
107
|
+
/** Non-success outcomes: where the error originated, when inferable. */
|
|
108
|
+
source?: ErrorSource | undefined
|
|
109
|
+
/**
|
|
110
|
+
* Connect `no_common_network` failures only: the wallet's advertised vs the
|
|
111
|
+
* configured CAIP-2 id lists (non-PII). Lets a dashboard show "wallet offered
|
|
112
|
+
* X, token configured Y" — the diagnosis for the #1 prod connection error.
|
|
113
|
+
*/
|
|
114
|
+
networkMismatch?:
|
|
115
|
+
| { walletNetworkIds: string[]; configuredNetworkIds: string[] }
|
|
116
|
+
| undefined
|
|
117
|
+
/** Present on failure events. `message` is scrubbed before egress. */
|
|
118
|
+
error?: WalletError | undefined
|
|
119
|
+
/** Log level, present only for `name === 'log'`. */
|
|
120
|
+
level?: LogLevel | undefined
|
|
121
|
+
/**
|
|
122
|
+
* Per-chain supported capability names (e.g. `{ 'eip155:8453': ['atomic'] }`),
|
|
123
|
+
* present only for `capabilitiesUpdated`. Feature flags + chain ids — not PII.
|
|
124
|
+
*/
|
|
125
|
+
capabilities?: Record<string, string[]> | undefined
|
|
126
|
+
/**
|
|
127
|
+
* Per-namespace requested-vs-matched discovery summary, present only on
|
|
128
|
+
* `walletsDetected`. Counts only — never a wallet id or address.
|
|
129
|
+
*/
|
|
130
|
+
discovery?: Record<string, { requested: number; matched: number }> | undefined
|
|
131
|
+
/**
|
|
132
|
+
* Per-namespace count of the session's available addresses, present on
|
|
133
|
+
* `connected` / `sessionChanged`. Counts only — never an address.
|
|
134
|
+
*/
|
|
135
|
+
addressNamespaces?: Record<string, number> | undefined
|
|
136
|
+
/**
|
|
137
|
+
* Namespaces the connected wallet supports for this session's configured
|
|
138
|
+
* networks but returned NO address for — present on `connected` /
|
|
139
|
+
* `sessionChanged` only when non-empty. A RAW FACT (supported-but-absent), not
|
|
140
|
+
* an asserted bug: it's the observable symptom of a silently dropped
|
|
141
|
+
* secondary-namespace gather — e.g. injected-connector's framed-embed skip,
|
|
142
|
+
* where the wallet supports Solana but the session has zero Solana addresses.
|
|
143
|
+
* The consumer interprets it (link-v2 deposit ⇒ lost holdings; a single-
|
|
144
|
+
* namespace deposit ⇒ intended). Pairs with `discovery` for the full picture.
|
|
145
|
+
*/
|
|
146
|
+
missingNamespaces?: string[] | undefined
|
|
147
|
+
/** `lifecycleTelemetryUnavailable` only: why lifecycle telemetry couldn't attach. */
|
|
148
|
+
reason?: LifecycleUnavailableReason | undefined
|
|
149
|
+
/** `lifecycleTelemetryUnavailable` only: direct page vs production iframe. */
|
|
150
|
+
runtime?: 'direct' | 'iframe' | undefined
|
|
151
|
+
/**
|
|
152
|
+
* `error` only: a handoff terminal already reported this failure — this
|
|
153
|
+
* record is the back-compat duplicate. Count terminals + `error` where NOT
|
|
154
|
+
* handoffCovered to avoid double-counting.
|
|
155
|
+
*/
|
|
156
|
+
handoffCovered?: boolean | undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Vendor-agnostic telemetry sink. Consumers implement; UWC ships no vendor SDK.
|
|
161
|
+
* Both methods are optional and called best-effort (throws are swallowed upstream).
|
|
162
|
+
*
|
|
163
|
+
* CONTRACT — both methods are invoked **synchronously on the wallet-operation
|
|
164
|
+
* path**. A terminal event (e.g. `walletSucceeded`) fires between the wallet
|
|
165
|
+
* returning a result and that result reaching the caller, so a slow handler adds
|
|
166
|
+
* latency to connect / sign / sendTransaction. Keep handlers **non-blocking**:
|
|
167
|
+
* hand off to a queue-backed sink (Datadog `addAction`, Segment `track`, etc. are
|
|
168
|
+
* already non-blocking) or defer heavy work yourself — e.g.
|
|
169
|
+
* `onEvent: e => queueMicrotask(() => doHeavyWork(e))`. Do not perform synchronous
|
|
170
|
+
* network/IO or expensive serialization inline.
|
|
171
|
+
*/
|
|
172
|
+
export interface UWCObserver {
|
|
173
|
+
/** A normalized, PII-safe event. Forward to Datadog / Amplitude / Segment / … (non-blocking). */
|
|
174
|
+
onEvent?(event: UWCTelemetryEvent): void
|
|
175
|
+
/** A log line (all levels). `args` is already scrubbed. Keep non-blocking. */
|
|
176
|
+
onLog?(level: LogLevel, message: string, args: unknown[]): void
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Best-effort coarse runtime surface. HEURISTIC — user agents lie and in-app
|
|
181
|
+
* browsers are hard to detect; treat `webview` as a hint, not a guarantee.
|
|
182
|
+
* Never returns the raw UA (PII / high-cardinality).
|
|
183
|
+
*/
|
|
184
|
+
export function derivePlatform(): Platform {
|
|
185
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) return 'unknown'
|
|
186
|
+
const ua = navigator.userAgent
|
|
187
|
+
const isIOS = /iPhone|iPad|iPod/i.test(ua)
|
|
188
|
+
const isAndroid = /Android/i.test(ua)
|
|
189
|
+
// Android in-app browsers tag the UA with `; wv`; iOS WKWebViews lack `Safari`.
|
|
190
|
+
const isWebview =
|
|
191
|
+
(isAndroid && /; wv\)/i.test(ua)) || (isIOS && !/Safari/i.test(ua))
|
|
192
|
+
if (isWebview) return 'webview'
|
|
193
|
+
if (isIOS || isAndroid) return 'mobile'
|
|
194
|
+
return 'desktop'
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Derive the flow taxonomy from connection mode + surface. HEURISTIC — UWC does
|
|
199
|
+
* not always know whether a WalletConnect handoff used a deeplink vs a QR scan;
|
|
200
|
+
* we infer from the platform (mobile ⇒ deeplink, desktop ⇒ QR), matching link-v2's
|
|
201
|
+
* historical intent. Consumers with better context may override downstream.
|
|
202
|
+
*/
|
|
203
|
+
export function deriveWalletFlowType(
|
|
204
|
+
connectionMode: ConnectionMode,
|
|
205
|
+
platform: Platform
|
|
206
|
+
): WalletFlowType {
|
|
207
|
+
const isDesktop = platform === 'desktop'
|
|
208
|
+
switch (connectionMode) {
|
|
209
|
+
case 'injected':
|
|
210
|
+
// Injected on a phone almost always means we're inside a wallet's browser.
|
|
211
|
+
return isDesktop ? 'browser_extension_flow' : 'in_wallet_browser_flow'
|
|
212
|
+
case 'walletConnect':
|
|
213
|
+
return isDesktop
|
|
214
|
+
? 'wallet_connect_qr_flow'
|
|
215
|
+
: 'wallet_connect_deep_link_flow'
|
|
216
|
+
case 'tonConnect':
|
|
217
|
+
return isDesktop ? 'ton_connect_qr_flow' : 'ton_connect_deep_link_flow'
|
|
218
|
+
default:
|
|
219
|
+
return 'unknown'
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Per-namespace address coverage for a session — telemetry-only, non-PII
|
|
225
|
+
* (namespace names + counts, never an address). Derived purely from the session:
|
|
226
|
+
* `availableNetworks` is the connected wallet's supported networks already
|
|
227
|
+
* filtered to the session's configured set, and `availableAddresses` is what the
|
|
228
|
+
* connect actually gathered, so `missing` is precise per-connect (no cross-wallet
|
|
229
|
+
* discovery join that could false-positive on a different installed wallet).
|
|
230
|
+
*
|
|
231
|
+
* `missing` is the observable symptom of a dropped secondary-namespace gather:
|
|
232
|
+
* the wallet supports the namespace for a configured network, but no address for
|
|
233
|
+
* it came back.
|
|
234
|
+
*/
|
|
235
|
+
function summarizeAddressCoverage(session: Session): {
|
|
236
|
+
addressNamespaces: Record<string, number>
|
|
237
|
+
missing: string[]
|
|
238
|
+
} {
|
|
239
|
+
// Defensive `?? []`: this runs on the wallet path, so a partial/malformed
|
|
240
|
+
// session must never throw here (a real session always has both arrays).
|
|
241
|
+
const addressNamespaces: Record<string, number> = {}
|
|
242
|
+
const present = new Set<string>()
|
|
243
|
+
for (const { networkId } of session.availableAddresses ?? []) {
|
|
244
|
+
const namespace = networkId.split(':')[0]
|
|
245
|
+
if (!namespace) continue
|
|
246
|
+
present.add(namespace)
|
|
247
|
+
addressNamespaces[namespace] = (addressNamespaces[namespace] ?? 0) + 1
|
|
248
|
+
}
|
|
249
|
+
const supported = new Set(
|
|
250
|
+
(session.availableNetworks ?? []).map(n => n.namespace)
|
|
251
|
+
)
|
|
252
|
+
const missing = [...supported].filter(namespace => !present.has(namespace))
|
|
253
|
+
return { addressNamespaces, missing }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Map a raw `UWCEventMap` event to its PII-safe `UWCTelemetryEvent`.
|
|
258
|
+
*
|
|
259
|
+
* Per-event allow-listing: only known-safe fields are copied. Payloads carrying a
|
|
260
|
+
* `Session`/`Network` (which include the wallet address) are reduced to
|
|
261
|
+
* `namespace` + `chainId` + `walletId`. Unknown/noisy payloads collapse to just
|
|
262
|
+
* the name. `error.message` is scrubbed.
|
|
263
|
+
*/
|
|
264
|
+
function buildTelemetryEvent<K extends UWCEventName>(
|
|
265
|
+
name: K,
|
|
266
|
+
data: UWCEventMap[K],
|
|
267
|
+
sdkSessionId: string,
|
|
268
|
+
now: number,
|
|
269
|
+
correlationId?: string
|
|
270
|
+
): UWCTelemetryEvent {
|
|
271
|
+
// `correlationId` is added to the key only when supplied — never aliased to
|
|
272
|
+
// `sdkSessionId`, so a consumer can tell "I gave no id" from "id == session".
|
|
273
|
+
const base: UWCTelemetryEvent = {
|
|
274
|
+
schemaVersion: TELEMETRY_SCHEMA_VERSION,
|
|
275
|
+
uwcVersion: UWC_CORE_VERSION,
|
|
276
|
+
name,
|
|
277
|
+
sdkSessionId,
|
|
278
|
+
timestamp: now,
|
|
279
|
+
...(correlationId !== undefined ? { correlationId } : {})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
switch (name) {
|
|
283
|
+
case 'connecting': {
|
|
284
|
+
const d = data as UWCEventMap['connecting']
|
|
285
|
+
return {
|
|
286
|
+
...base,
|
|
287
|
+
operation: 'connect',
|
|
288
|
+
connectionMode: d.connectionMode,
|
|
289
|
+
walletId: d.walletId
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
case 'connected':
|
|
293
|
+
case 'sessionChanged': {
|
|
294
|
+
const d = data as UWCEventMap['connected']
|
|
295
|
+
const session = d.session
|
|
296
|
+
const { addressNamespaces, missing } = summarizeAddressCoverage(session)
|
|
297
|
+
return {
|
|
298
|
+
...base,
|
|
299
|
+
connectionMode: session.connectionMode ?? undefined,
|
|
300
|
+
namespace: session.activeNetwork?.namespace,
|
|
301
|
+
chainId: session.activeNetwork?.id,
|
|
302
|
+
walletId: session.activeWallet?.id,
|
|
303
|
+
addressNamespaces,
|
|
304
|
+
...(missing.length > 0 && { missingNamespaces: missing })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
case 'networkSwitched': {
|
|
308
|
+
const d = data as UWCEventMap['networkSwitched']
|
|
309
|
+
return {
|
|
310
|
+
...base,
|
|
311
|
+
operation: 'switchNetwork',
|
|
312
|
+
namespace: d.network.namespace,
|
|
313
|
+
chainId: d.network.id
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
case 'connectionUri': {
|
|
317
|
+
// Carry the mode but NEVER the URI itself (a relay/pairing string).
|
|
318
|
+
const d = data as UWCEventMap['connectionUri']
|
|
319
|
+
return { ...base, connectionMode: d.connectionMode }
|
|
320
|
+
}
|
|
321
|
+
case 'error': {
|
|
322
|
+
const d = data as UWCEventMap['error']
|
|
323
|
+
return {
|
|
324
|
+
...base,
|
|
325
|
+
operation: d.operation,
|
|
326
|
+
error: { type: d.error.type, message: scrubMessage(d.error.message) },
|
|
327
|
+
// Telemetry-only classification — undefined for ordinary errors, set for
|
|
328
|
+
// pre-wallet validation failures (e.g. no_common_network) that never
|
|
329
|
+
// reach the handoff funnel.
|
|
330
|
+
errorCode: d.errorCode,
|
|
331
|
+
failureKind: d.failureKind,
|
|
332
|
+
source: d.source,
|
|
333
|
+
networkMismatch: capNetworkMismatch(d.networkMismatch),
|
|
334
|
+
handoffCovered: d.handoffCovered
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
case 'awaitingWallet': {
|
|
338
|
+
const d = data as UWCEventMap['awaitingWallet']
|
|
339
|
+
return {
|
|
340
|
+
...base,
|
|
341
|
+
operation: d.operation,
|
|
342
|
+
connectionMode: d.connectionMode,
|
|
343
|
+
walletId: d.walletId,
|
|
344
|
+
namespace: d.namespace,
|
|
345
|
+
chainId: d.chainId,
|
|
346
|
+
handoffId: d.handoffId,
|
|
347
|
+
walletFlowType: d.walletFlowType,
|
|
348
|
+
platform: d.platform,
|
|
349
|
+
targetAlreadyActive: d.targetAlreadyActive
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
case 'walletSucceeded': {
|
|
353
|
+
const d = data as UWCEventMap['walletSucceeded']
|
|
354
|
+
return {
|
|
355
|
+
...base,
|
|
356
|
+
operation: d.operation,
|
|
357
|
+
connectionMode: d.connectionMode,
|
|
358
|
+
walletId: d.walletId,
|
|
359
|
+
namespace: d.namespace,
|
|
360
|
+
chainId: d.chainId,
|
|
361
|
+
handoffId: d.handoffId,
|
|
362
|
+
durationMs: d.durationMs,
|
|
363
|
+
timedOut: d.timedOut,
|
|
364
|
+
timeoutMs: d.timeoutMs,
|
|
365
|
+
targetAlreadyActive: d.targetAlreadyActive
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
case 'walletRejected': {
|
|
369
|
+
const d = data as UWCEventMap['walletRejected']
|
|
370
|
+
return {
|
|
371
|
+
...base,
|
|
372
|
+
operation: d.operation,
|
|
373
|
+
connectionMode: d.connectionMode,
|
|
374
|
+
walletId: d.walletId,
|
|
375
|
+
namespace: d.namespace,
|
|
376
|
+
chainId: d.chainId,
|
|
377
|
+
handoffId: d.handoffId,
|
|
378
|
+
durationMs: d.durationMs,
|
|
379
|
+
timedOut: d.timedOut,
|
|
380
|
+
timeoutMs: d.timeoutMs,
|
|
381
|
+
targetAlreadyActive: d.targetAlreadyActive,
|
|
382
|
+
errorCode: d.errorCode,
|
|
383
|
+
failureKind: d.failureKind,
|
|
384
|
+
source: d.source
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
case 'walletTimedOut': {
|
|
388
|
+
const d = data as UWCEventMap['walletTimedOut']
|
|
389
|
+
return {
|
|
390
|
+
...base,
|
|
391
|
+
operation: d.operation,
|
|
392
|
+
connectionMode: d.connectionMode,
|
|
393
|
+
walletId: d.walletId,
|
|
394
|
+
namespace: d.namespace,
|
|
395
|
+
chainId: d.chainId,
|
|
396
|
+
handoffId: d.handoffId,
|
|
397
|
+
durationMs: d.durationMs,
|
|
398
|
+
timeoutMs: d.timeoutMs,
|
|
399
|
+
targetAlreadyActive: d.targetAlreadyActive
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
case 'walletFailed': {
|
|
403
|
+
const d = data as UWCEventMap['walletFailed']
|
|
404
|
+
return {
|
|
405
|
+
...base,
|
|
406
|
+
operation: d.operation,
|
|
407
|
+
connectionMode: d.connectionMode,
|
|
408
|
+
walletId: d.walletId,
|
|
409
|
+
namespace: d.namespace,
|
|
410
|
+
chainId: d.chainId,
|
|
411
|
+
handoffId: d.handoffId,
|
|
412
|
+
durationMs: d.durationMs,
|
|
413
|
+
timedOut: d.timedOut,
|
|
414
|
+
timeoutMs: d.timeoutMs,
|
|
415
|
+
targetAlreadyActive: d.targetAlreadyActive,
|
|
416
|
+
errorCode: d.errorCode,
|
|
417
|
+
failureKind: d.failureKind,
|
|
418
|
+
source: d.source,
|
|
419
|
+
error: { type: d.error.type, message: scrubMessage(d.error.message) }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
case 'capabilitiesUpdated': {
|
|
423
|
+
const d = data as UWCEventMap['capabilitiesUpdated']
|
|
424
|
+
// Summarize to per-chain lists of the capability names that are not
|
|
425
|
+
// 'unsupported'. Compact + non-PII (chain ids + bounded feature names).
|
|
426
|
+
const capabilities: Record<string, string[]> = {}
|
|
427
|
+
for (const [chainId, caps] of Object.entries(d.capabilities)) {
|
|
428
|
+
const enabled = Object.entries(caps)
|
|
429
|
+
.filter(([, cap]) => cap && cap.status !== 'unsupported')
|
|
430
|
+
.map(([feature]) => feature)
|
|
431
|
+
if (enabled.length > 0) capabilities[chainId] = enabled
|
|
432
|
+
}
|
|
433
|
+
return { ...base, capabilities }
|
|
434
|
+
}
|
|
435
|
+
case 'walletsDetected': {
|
|
436
|
+
const d = data as UWCEventMap['walletsDetected']
|
|
437
|
+
// Carry only the compact requested-vs-matched summary; never the wallet list.
|
|
438
|
+
return { ...base, ...(d.discovery ? { discovery: d.discovery } : {}) }
|
|
439
|
+
}
|
|
440
|
+
case 'walletDiscoveryFailed': {
|
|
441
|
+
const d = data as UWCEventMap['walletDiscoveryFailed']
|
|
442
|
+
return {
|
|
443
|
+
...base,
|
|
444
|
+
connectionMode: d.connectionMode,
|
|
445
|
+
namespace: d.namespace,
|
|
446
|
+
error: { type: d.error.type, message: scrubMessage(d.error.message) }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
case 'pageHiddenDuringHandoff':
|
|
450
|
+
case 'pageVisibleDuringHandoff': {
|
|
451
|
+
const d = data as UWCEventMap['pageHiddenDuringHandoff']
|
|
452
|
+
return { ...base, handoffId: d.handoffId, platform: d.platform }
|
|
453
|
+
}
|
|
454
|
+
case 'bridgeError': {
|
|
455
|
+
const d = data as UWCEventMap['bridgeError']
|
|
456
|
+
return {
|
|
457
|
+
...base,
|
|
458
|
+
namespace: d.namespace,
|
|
459
|
+
source: 'bridge',
|
|
460
|
+
failureKind: 'bridge_error',
|
|
461
|
+
error: { type: d.error.type, message: scrubMessage(d.error.message) }
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
case 'relayError': {
|
|
465
|
+
const d = data as UWCEventMap['relayError']
|
|
466
|
+
// A relay-socket transport failure (WalletConnect only). The scrubbed message
|
|
467
|
+
// carries the diagnostic detail (e.g. "code: 3000 (JWT ... not yet valid)").
|
|
468
|
+
return {
|
|
469
|
+
...base,
|
|
470
|
+
connectionMode: 'walletConnect',
|
|
471
|
+
source: 'provider',
|
|
472
|
+
failureKind: 'network_error',
|
|
473
|
+
error: { type: d.error.type, message: scrubMessage(d.error.message) }
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
case 'walletAccountChanged':
|
|
477
|
+
case 'walletDisconnected':
|
|
478
|
+
case 'walletSessionExpired': {
|
|
479
|
+
const d = data as UWCEventMap['walletAccountChanged']
|
|
480
|
+
return {
|
|
481
|
+
...base,
|
|
482
|
+
connectionMode: d.connectionMode,
|
|
483
|
+
walletId: d.walletId,
|
|
484
|
+
namespace: d.namespace
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
case 'walletChainChanged': {
|
|
488
|
+
const d = data as UWCEventMap['walletChainChanged']
|
|
489
|
+
return {
|
|
490
|
+
...base,
|
|
491
|
+
connectionMode: d.connectionMode,
|
|
492
|
+
walletId: d.walletId,
|
|
493
|
+
namespace: d.namespace,
|
|
494
|
+
chainId: d.chainId
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
case 'lifecycleTelemetryUnavailable': {
|
|
498
|
+
const d = data as UWCEventMap['lifecycleTelemetryUnavailable']
|
|
499
|
+
return {
|
|
500
|
+
...base,
|
|
501
|
+
connectionMode: d.connectionMode,
|
|
502
|
+
walletId: d.walletId,
|
|
503
|
+
namespace: d.namespace,
|
|
504
|
+
reason: d.reason,
|
|
505
|
+
runtime: d.runtime
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
default:
|
|
509
|
+
// ready / disconnected / networkSwitching / change / log — name only
|
|
510
|
+
// (log goes via onLog).
|
|
511
|
+
return base
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Drop keys whose value is `undefined` so a record never carries a
|
|
517
|
+
* present-but-undefined optional (e.g. `errorCode: undefined` on a non-error
|
|
518
|
+
* event), which would make `'errorCode' in event` misleading. Shallow by design:
|
|
519
|
+
* nested objects (`error`, `networkMismatch`, `discovery`, `capabilities`,
|
|
520
|
+
* `addressNamespaces`) are only ever built with defined values.
|
|
521
|
+
*/
|
|
522
|
+
function compactUndefined(event: UWCTelemetryEvent): UWCTelemetryEvent {
|
|
523
|
+
const record = event as unknown as Record<string, unknown>
|
|
524
|
+
const out: Record<string, unknown> = {}
|
|
525
|
+
for (const key of Object.keys(record)) {
|
|
526
|
+
const value = record[key]
|
|
527
|
+
if (value !== undefined) out[key] = value
|
|
528
|
+
}
|
|
529
|
+
return out as unknown as UWCTelemetryEvent
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Normalize a bus event into a PII-safe `UWCTelemetryEvent`, dropping undefined
|
|
534
|
+
* optionals so the emitted shape is consistent across event kinds.
|
|
535
|
+
*/
|
|
536
|
+
export function normalizeEvent<K extends UWCEventName>(
|
|
537
|
+
name: K,
|
|
538
|
+
data: UWCEventMap[K],
|
|
539
|
+
sdkSessionId: string,
|
|
540
|
+
now: number,
|
|
541
|
+
correlationId?: string
|
|
542
|
+
): UWCTelemetryEvent {
|
|
543
|
+
return compactUndefined(
|
|
544
|
+
buildTelemetryEvent(name, data, sdkSessionId, now, correlationId)
|
|
545
|
+
)
|
|
546
|
+
}
|