@lavarage/telemetry 1.0.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 +540 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +623 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +2 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# @lavarage/telemetry
|
|
2
|
+
|
|
3
|
+
Production telemetry SDK for Lavarage and partner applications. Track user activity, network requests, and errors with comprehensive filtering and privacy controls.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @lavarage/telemetry
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { LavarageTelemetry } from '@lavarage/telemetry';
|
|
15
|
+
|
|
16
|
+
const telemetry = new LavarageTelemetry({
|
|
17
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
18
|
+
platform: 'lavarage-web',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Intercept network requests
|
|
22
|
+
telemetry.interceptFetch();
|
|
23
|
+
|
|
24
|
+
// Connect wallet (triggers login event)
|
|
25
|
+
telemetry.setWallet('YourWalletAddress123...');
|
|
26
|
+
|
|
27
|
+
// Track trading pair views
|
|
28
|
+
telemetry.trackPairView('SOL/USDC', { source: 'homepage' });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
### Basic Configuration
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const telemetry = new LavarageTelemetry({
|
|
37
|
+
apiEndpoint: string; // Required: Backend API endpoint
|
|
38
|
+
platform: string; // Required: Platform identifier (e.g., 'lavarage-web')
|
|
39
|
+
captureHosts?: HostFilterInput; // Optional: Host filtering configuration
|
|
40
|
+
errorFilters?: ErrorFilterConfig; // Optional: Error filtering configuration
|
|
41
|
+
batchSize?: number; // Optional: Max events per batch (default: 50)
|
|
42
|
+
flushInterval?: number; // Optional: Flush interval in ms (default: 5000)
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Host Filtering
|
|
47
|
+
|
|
48
|
+
Control which network requests are captured using flexible host filtering.
|
|
49
|
+
|
|
50
|
+
### String Configuration
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const telemetry = new LavarageTelemetry({
|
|
54
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
55
|
+
platform: 'lavarage-web',
|
|
56
|
+
captureHosts: 'api.lavarage.com', // Only capture requests to this host
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Array Configuration
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const telemetry = new LavarageTelemetry({
|
|
64
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
65
|
+
platform: 'lavarage-web',
|
|
66
|
+
captureHosts: [
|
|
67
|
+
'api.lavarage.com',
|
|
68
|
+
'partner-api.com',
|
|
69
|
+
'*.example.com' // Wildcard: matches any subdomain
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Object Configuration
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const telemetry = new LavarageTelemetry({
|
|
78
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
79
|
+
platform: 'lavarage-web',
|
|
80
|
+
captureHosts: {
|
|
81
|
+
mode: 'include', // 'all' | 'none' | 'include' | 'exclude'
|
|
82
|
+
hosts: ['api.lavarage.com', '*.partner.com'],
|
|
83
|
+
patterns: ['^api\\..*\\.lavarage\\.com$'] // Regex patterns
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Wildcard Matching
|
|
89
|
+
|
|
90
|
+
- `*.example.com` - Matches any subdomain (e.g., `api.example.com`, `www.example.com`)
|
|
91
|
+
- `example.com` - Matches the domain and all subdomains
|
|
92
|
+
|
|
93
|
+
### Update Host Filter at Runtime
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
telemetry.updateHostFilter({
|
|
97
|
+
mode: 'exclude',
|
|
98
|
+
hosts: ['analytics.google.com', '*.tracking.com']
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Error Filtering
|
|
103
|
+
|
|
104
|
+
Filter which errors are captured using include/exclude patterns.
|
|
105
|
+
|
|
106
|
+
### Example 1: Only Capture Specific Errors
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const telemetry = new LavarageTelemetry({
|
|
110
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
111
|
+
platform: 'lavarage-web',
|
|
112
|
+
errorFilters: {
|
|
113
|
+
include: [
|
|
114
|
+
'Failed to fetch',
|
|
115
|
+
'Network error',
|
|
116
|
+
'Transaction.*failed',
|
|
117
|
+
'RPC.*error'
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Example 2: Exclude Noisy Errors
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const telemetry = new LavarageTelemetry({
|
|
127
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
128
|
+
platform: 'lavarage-web',
|
|
129
|
+
errorFilters: {
|
|
130
|
+
exclude: [
|
|
131
|
+
'ResizeObserver',
|
|
132
|
+
'Extension context invalidated',
|
|
133
|
+
'Script error',
|
|
134
|
+
'Non-Error promise rejection'
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Example 3: Combined Filtering
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const telemetry = new LavarageTelemetry({
|
|
144
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
145
|
+
platform: 'lavarage-web',
|
|
146
|
+
errorFilters: {
|
|
147
|
+
include: ['.*'], // Capture everything
|
|
148
|
+
exclude: [
|
|
149
|
+
'chrome-extension://',
|
|
150
|
+
'moz-extension://',
|
|
151
|
+
'webkit-masked-url://',
|
|
152
|
+
'ResizeObserver loop',
|
|
153
|
+
'Script error\\.'
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Filter Logic:**
|
|
160
|
+
- If `include` patterns are provided, ONLY errors matching those patterns are captured
|
|
161
|
+
- If `exclude` patterns are provided, all errors EXCEPT those matching patterns are captured
|
|
162
|
+
- If both are provided, `include` is applied first, then `exclude`
|
|
163
|
+
|
|
164
|
+
## Axios Integration
|
|
165
|
+
|
|
166
|
+
If you're using Axios, you can intercept Axios requests:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import axios from 'axios';
|
|
170
|
+
import { LavarageTelemetry } from '@lavarage/telemetry';
|
|
171
|
+
|
|
172
|
+
const telemetry = new LavarageTelemetry({
|
|
173
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
174
|
+
platform: 'lavarage-web',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const axiosInstance = axios.create({
|
|
178
|
+
baseURL: 'https://api.lavarage.com',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Intercept Axios requests
|
|
182
|
+
telemetry.interceptAxios(axiosInstance);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Security Features
|
|
186
|
+
|
|
187
|
+
### Automatic Data Sanitization
|
|
188
|
+
|
|
189
|
+
The SDK automatically sanitizes sensitive data in all payloads and responses:
|
|
190
|
+
|
|
191
|
+
- `privateKey`
|
|
192
|
+
- `mnemonic`
|
|
193
|
+
- `password`
|
|
194
|
+
- `secret`
|
|
195
|
+
- `token`
|
|
196
|
+
- `apiKey`
|
|
197
|
+
- `authorization`
|
|
198
|
+
|
|
199
|
+
Sensitive fields are replaced with `[REDACTED]` recursively in nested objects and arrays.
|
|
200
|
+
|
|
201
|
+
### Silent Error Handling
|
|
202
|
+
|
|
203
|
+
All telemetry operations are wrapped in try-catch blocks. Telemetry errors will never disrupt your application.
|
|
204
|
+
|
|
205
|
+
## API Reference
|
|
206
|
+
|
|
207
|
+
### Methods
|
|
208
|
+
|
|
209
|
+
#### `setWallet(walletAddress: string)`
|
|
210
|
+
|
|
211
|
+
Set the current wallet address and trigger a login event.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
telemetry.setWallet('YourWalletAddress123...');
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### `interceptFetch()`
|
|
218
|
+
|
|
219
|
+
Intercept the global `fetch` API to capture network requests.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
telemetry.interceptFetch();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### `interceptAxios(axiosInstance: any)`
|
|
226
|
+
|
|
227
|
+
Intercept an Axios instance to capture network requests.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
telemetry.interceptAxios(axiosInstance);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### `trackPairView(pair: string, metadata?: object)`
|
|
234
|
+
|
|
235
|
+
Track a trading pair view event.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
telemetry.trackPairView('SOL/USDC', {
|
|
239
|
+
source: 'homepage',
|
|
240
|
+
category: 'trending'
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### `trackSystemEvent(category: string, message: string, level?: string, metadata?: object)`
|
|
245
|
+
|
|
246
|
+
Track a system event (not tied to any wallet address). System events are displayed in a separate panel in the dashboard.
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// Track app startup
|
|
250
|
+
telemetry.trackSystemEvent('app_start', 'Application initialized', 'info');
|
|
251
|
+
|
|
252
|
+
// Track feature usage
|
|
253
|
+
telemetry.trackSystemEvent('feature_used', 'User enabled dark mode', 'info', {
|
|
254
|
+
feature: 'dark_mode',
|
|
255
|
+
version: '1.2.3'
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Track configuration changes
|
|
259
|
+
telemetry.trackSystemEvent('config_change', 'API endpoint updated', 'warning', {
|
|
260
|
+
old_endpoint: 'https://api.example.com',
|
|
261
|
+
new_endpoint: 'https://api2.example.com'
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Track errors
|
|
265
|
+
telemetry.trackSystemEvent('system_error', 'Failed to load configuration', 'error', {
|
|
266
|
+
error_code: 'CONFIG_LOAD_FAILED'
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Parameters:**
|
|
271
|
+
- `category` - Event category (e.g., 'app_start', 'feature_used', 'config_change')
|
|
272
|
+
- `message` - Event message
|
|
273
|
+
- `level` - Event level: 'info' | 'warning' | 'error' | 'debug' (default: 'info')
|
|
274
|
+
- `metadata` - Optional additional metadata object
|
|
275
|
+
|
|
276
|
+
#### `updateHostFilter(captureHosts: HostFilterInput)`
|
|
277
|
+
|
|
278
|
+
Update the host filter configuration at runtime.
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
telemetry.updateHostFilter({
|
|
282
|
+
mode: 'exclude',
|
|
283
|
+
hosts: ['analytics.google.com']
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### `logWalletEvent(walletAddress: string, eventType: string, message: string, metadata?: object)`
|
|
288
|
+
|
|
289
|
+
Log a custom event with wallet address (backend-friendly method). Events are queued and sent in batches.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
telemetry.logWalletEvent(
|
|
293
|
+
'WalletAddress123...',
|
|
294
|
+
'transaction',
|
|
295
|
+
'Transaction completed',
|
|
296
|
+
{ txHash: '0x...', amount: '100' }
|
|
297
|
+
);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### `sendLog(walletAddress: string, eventType: string, message: string, metadata?: object): Promise<void>`
|
|
301
|
+
|
|
302
|
+
Send a log event immediately, bypassing the batch queue. Useful for critical logs.
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
await telemetry.sendLog(
|
|
306
|
+
'WalletAddress123...',
|
|
307
|
+
'critical',
|
|
308
|
+
'Security alert',
|
|
309
|
+
{ alertType: 'suspicious_activity' }
|
|
310
|
+
);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### `destroy()`
|
|
314
|
+
|
|
315
|
+
Clean up the telemetry instance, restore original functions, and flush remaining events.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
telemetry.destroy();
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Event Types
|
|
322
|
+
|
|
323
|
+
The SDK tracks the following event types:
|
|
324
|
+
|
|
325
|
+
- **login**: Triggered when `setWallet()` is called
|
|
326
|
+
- **pair_view**: Trading pair view events
|
|
327
|
+
- **error**: Console errors, uncaught exceptions, and unhandled promise rejections
|
|
328
|
+
- **request**: Network requests (fetch/Axios)
|
|
329
|
+
- **system_event**: System-level events not tied to any wallet address (displayed in separate dashboard panel)
|
|
330
|
+
- **log**: Custom log events (via `logWalletEvent()` or `sendLog()`)
|
|
331
|
+
|
|
332
|
+
## Batching
|
|
333
|
+
|
|
334
|
+
Events are automatically batched and sent to the backend:
|
|
335
|
+
|
|
336
|
+
- **Default batch size**: 50 events
|
|
337
|
+
- **Default flush interval**: 5 seconds
|
|
338
|
+
- **Automatic flush**: On page unload (using `keepalive` for reliable delivery)
|
|
339
|
+
|
|
340
|
+
## Backend Usage
|
|
341
|
+
|
|
342
|
+
The SDK works in both browser and Node.js environments. For backend applications, you can use a subset of features focused on manual logging.
|
|
343
|
+
|
|
344
|
+
### Installation for Backend
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
npm install @lavarage/telemetry
|
|
348
|
+
# For Node.js < 18, also install node-fetch:
|
|
349
|
+
npm install node-fetch@2
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Backend Quick Start
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { LavarageTelemetry } from '@lavarage/telemetry';
|
|
356
|
+
|
|
357
|
+
// Initialize telemetry for backend
|
|
358
|
+
const telemetry = new LavarageTelemetry({
|
|
359
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
360
|
+
platform: 'my-backend-service',
|
|
361
|
+
batchSize: 100, // Larger batches for backend
|
|
362
|
+
flushInterval: 10000, // Flush every 10 seconds
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Log events related to a wallet address
|
|
366
|
+
telemetry.logWalletEvent(
|
|
367
|
+
'DummyWallet123456789',
|
|
368
|
+
'transaction',
|
|
369
|
+
'Transaction completed successfully',
|
|
370
|
+
{
|
|
371
|
+
txHash: '0x123...',
|
|
372
|
+
amount: '100.5',
|
|
373
|
+
token: 'USDC'
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// Log errors with wallet context
|
|
378
|
+
telemetry.logWalletEvent(
|
|
379
|
+
'DummyWallet123456789',
|
|
380
|
+
'error',
|
|
381
|
+
'Failed to process transaction',
|
|
382
|
+
{
|
|
383
|
+
errorCode: 'INSUFFICIENT_FUNDS',
|
|
384
|
+
attemptedAmount: '200.0'
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Send critical logs immediately (bypasses batching)
|
|
389
|
+
await telemetry.sendLog(
|
|
390
|
+
'DummyWallet123456789',
|
|
391
|
+
'critical',
|
|
392
|
+
'Security alert: suspicious activity detected',
|
|
393
|
+
{
|
|
394
|
+
alertType: 'unusual_pattern',
|
|
395
|
+
severity: 'high'
|
|
396
|
+
}
|
|
397
|
+
);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Backend API Methods
|
|
401
|
+
|
|
402
|
+
#### `logWalletEvent(walletAddress: string, eventType: string, message: string, metadata?: object)`
|
|
403
|
+
|
|
404
|
+
Queue a log event with a wallet address. Events are batched and sent automatically.
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
telemetry.logWalletEvent(
|
|
408
|
+
'WalletAddress123...',
|
|
409
|
+
'transaction', // Event type (e.g., 'transaction', 'error', 'action')
|
|
410
|
+
'Transaction completed', // Log message
|
|
411
|
+
{ txHash: '0x...', amount: '100' } // Optional metadata
|
|
412
|
+
);
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
#### `sendLog(walletAddress: string, eventType: string, message: string, metadata?: object): Promise<void>`
|
|
416
|
+
|
|
417
|
+
Send a log event immediately, bypassing the batch queue. Useful for critical logs that need immediate delivery.
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
await telemetry.sendLog(
|
|
421
|
+
'WalletAddress123...',
|
|
422
|
+
'critical',
|
|
423
|
+
'Security alert',
|
|
424
|
+
{ alertType: 'suspicious_activity' }
|
|
425
|
+
);
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### `setWallet(walletAddress: string)`
|
|
429
|
+
|
|
430
|
+
Set the default wallet address for subsequent events. This is optional for backend use since you can specify the wallet in each log call.
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
telemetry.setWallet('WalletAddress123...');
|
|
434
|
+
// Now all events will use this wallet by default
|
|
435
|
+
telemetry.logWalletEvent(
|
|
436
|
+
null, // Will use the wallet set above
|
|
437
|
+
'action',
|
|
438
|
+
'User performed action'
|
|
439
|
+
);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Backend Example: Express.js Middleware
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import express from 'express';
|
|
446
|
+
import { LavarageTelemetry } from '@lavarage/telemetry';
|
|
447
|
+
|
|
448
|
+
const telemetry = new LavarageTelemetry({
|
|
449
|
+
apiEndpoint: process.env.TELEMETRY_ENDPOINT!,
|
|
450
|
+
platform: 'api-server',
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const app = express();
|
|
454
|
+
|
|
455
|
+
// Middleware to log wallet-related requests
|
|
456
|
+
app.use('/api/wallet/:walletAddress', (req, res, next) => {
|
|
457
|
+
const walletAddress = req.params.walletAddress;
|
|
458
|
+
|
|
459
|
+
// Log the request
|
|
460
|
+
telemetry.logWalletEvent(
|
|
461
|
+
walletAddress,
|
|
462
|
+
'api_request',
|
|
463
|
+
`${req.method} ${req.path}`,
|
|
464
|
+
{
|
|
465
|
+
method: req.method,
|
|
466
|
+
path: req.path,
|
|
467
|
+
userAgent: req.get('user-agent'),
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
next();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Log transaction events
|
|
475
|
+
app.post('/api/transactions', async (req, res) => {
|
|
476
|
+
const { walletAddress, txHash } = req.body;
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
// Process transaction...
|
|
480
|
+
|
|
481
|
+
telemetry.logWalletEvent(
|
|
482
|
+
walletAddress,
|
|
483
|
+
'transaction',
|
|
484
|
+
'Transaction processed successfully',
|
|
485
|
+
{ txHash, status: 'success' }
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
res.json({ success: true });
|
|
489
|
+
} catch (error) {
|
|
490
|
+
telemetry.logWalletEvent(
|
|
491
|
+
walletAddress,
|
|
492
|
+
'error',
|
|
493
|
+
'Transaction failed',
|
|
494
|
+
{ txHash, error: error.message }
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
res.status(500).json({ error: error.message });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Backend Example: Error Handler
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import { LavarageTelemetry } from '@lavarage/telemetry';
|
|
506
|
+
|
|
507
|
+
const telemetry = new LavarageTelemetry({
|
|
508
|
+
apiEndpoint: process.env.TELEMETRY_ENDPOINT!,
|
|
509
|
+
platform: 'api-server',
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Global error handler
|
|
513
|
+
process.on('uncaughtException', (error) => {
|
|
514
|
+
// Extract wallet from error context if available
|
|
515
|
+
const walletAddress = (error as any).walletAddress || null;
|
|
516
|
+
|
|
517
|
+
if (walletAddress) {
|
|
518
|
+
telemetry.logWalletEvent(
|
|
519
|
+
walletAddress,
|
|
520
|
+
'error',
|
|
521
|
+
`Uncaught exception: ${error.message}`,
|
|
522
|
+
{
|
|
523
|
+
stack: error.stack,
|
|
524
|
+
name: error.name,
|
|
525
|
+
}
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
## Browser Support
|
|
532
|
+
|
|
533
|
+
- Modern browsers with ES2020 support
|
|
534
|
+
- Node.js 18+ (native fetch support)
|
|
535
|
+
- Node.js < 18 requires `node-fetch` package
|
|
536
|
+
|
|
537
|
+
## License
|
|
538
|
+
|
|
539
|
+
MIT
|
|
540
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { TelemetryConfig, TelemetryEvent, ErrorEvent, RequestEvent, HostFilterInput, HostFilterConfig, ErrorFilterConfig, SystemEvent } from './types';
|
|
2
|
+
export type { TelemetryConfig, TelemetryEvent, ErrorEvent, RequestEvent, HostFilterInput, HostFilterConfig, ErrorFilterConfig, SystemEvent, };
|
|
3
|
+
export declare class LavarageTelemetry {
|
|
4
|
+
private apiEndpoint;
|
|
5
|
+
private platform;
|
|
6
|
+
private wallet;
|
|
7
|
+
private sessionId;
|
|
8
|
+
private eventQueue;
|
|
9
|
+
private flushInterval;
|
|
10
|
+
private batchSize;
|
|
11
|
+
private flushTimer;
|
|
12
|
+
private hostFilter;
|
|
13
|
+
private errorFilters;
|
|
14
|
+
private originalFetch;
|
|
15
|
+
private originalConsoleError;
|
|
16
|
+
private requestMap;
|
|
17
|
+
constructor(config: TelemetryConfig);
|
|
18
|
+
private generateSessionId;
|
|
19
|
+
private generateRequestId;
|
|
20
|
+
private normalizeHostFilter;
|
|
21
|
+
private shouldCaptureHost;
|
|
22
|
+
private matchesHost;
|
|
23
|
+
private shouldCaptureError;
|
|
24
|
+
private sanitizePayload;
|
|
25
|
+
private safeReadResponse;
|
|
26
|
+
private enqueue;
|
|
27
|
+
private startFlushTimer;
|
|
28
|
+
private getFetch;
|
|
29
|
+
private flush;
|
|
30
|
+
private initializeCapture;
|
|
31
|
+
private trackError;
|
|
32
|
+
setWallet(walletAddress: string): void;
|
|
33
|
+
interceptFetch(): void;
|
|
34
|
+
interceptAxios(axiosInstance: any): void;
|
|
35
|
+
trackPairView(pair: string, metadata?: object): void;
|
|
36
|
+
/**
|
|
37
|
+
* Track a system event (not tied to any wallet address)
|
|
38
|
+
* System events are displayed in a separate panel in the dashboard
|
|
39
|
+
* @param category - Event category (e.g., 'app_start', 'feature_used', 'config_change')
|
|
40
|
+
* @param message - Event message
|
|
41
|
+
* @param level - Event level (info, warning, error, debug)
|
|
42
|
+
* @param metadata - Optional additional metadata
|
|
43
|
+
*/
|
|
44
|
+
trackSystemEvent(category: string, message: string, level?: 'info' | 'warning' | 'error' | 'debug', metadata?: object): void;
|
|
45
|
+
/**
|
|
46
|
+
* Log a custom event with wallet address (backend-friendly method)
|
|
47
|
+
* @param walletAddress - The wallet address associated with this log
|
|
48
|
+
* @param eventType - Type of event (e.g., 'transaction', 'error', 'action')
|
|
49
|
+
* @param message - Log message
|
|
50
|
+
* @param metadata - Optional additional metadata
|
|
51
|
+
*/
|
|
52
|
+
logWalletEvent(walletAddress: string, eventType: string, message: string, metadata?: object): void;
|
|
53
|
+
/**
|
|
54
|
+
* Send a log event immediately (bypasses batching, useful for critical logs)
|
|
55
|
+
* @param walletAddress - The wallet address associated with this log
|
|
56
|
+
* @param eventType - Type of event
|
|
57
|
+
* @param message - Log message
|
|
58
|
+
* @param metadata - Optional additional metadata
|
|
59
|
+
*/
|
|
60
|
+
sendLog(walletAddress: string, eventType: string, message: string, metadata?: object): Promise<void>;
|
|
61
|
+
updateHostFilter(captureHosts: HostFilterInput): void;
|
|
62
|
+
destroy(): void;
|
|
63
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LavarageTelemetry = void 0;
|
|
4
|
+
class LavarageTelemetry {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.wallet = null;
|
|
7
|
+
this.eventQueue = [];
|
|
8
|
+
this.flushTimer = null;
|
|
9
|
+
this.originalFetch = null;
|
|
10
|
+
this.originalConsoleError = null;
|
|
11
|
+
this.requestMap = new Map();
|
|
12
|
+
this.apiEndpoint = config.apiEndpoint;
|
|
13
|
+
this.platform = config.platform;
|
|
14
|
+
this.sessionId = this.generateSessionId();
|
|
15
|
+
this.flushInterval = config.flushInterval ?? 5000;
|
|
16
|
+
this.batchSize = config.batchSize ?? 50;
|
|
17
|
+
this.hostFilter = this.normalizeHostFilter(config.captureHosts);
|
|
18
|
+
this.errorFilters = config.errorFilters ?? {};
|
|
19
|
+
// Initialize event capture
|
|
20
|
+
this.initializeCapture();
|
|
21
|
+
// Start flush timer
|
|
22
|
+
this.startFlushTimer();
|
|
23
|
+
// Setup beforeunload handler
|
|
24
|
+
if (typeof window !== 'undefined') {
|
|
25
|
+
window.addEventListener('beforeunload', () => {
|
|
26
|
+
this.flush(true);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
generateSessionId() {
|
|
31
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}-${Math.random().toString(36).substring(2, 15)}`;
|
|
32
|
+
}
|
|
33
|
+
generateRequestId() {
|
|
34
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
35
|
+
}
|
|
36
|
+
normalizeHostFilter(input) {
|
|
37
|
+
if (!input) {
|
|
38
|
+
return { mode: 'all' };
|
|
39
|
+
}
|
|
40
|
+
if (typeof input === 'string') {
|
|
41
|
+
return {
|
|
42
|
+
mode: 'include',
|
|
43
|
+
hosts: [input],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(input)) {
|
|
47
|
+
return {
|
|
48
|
+
mode: 'include',
|
|
49
|
+
hosts: input,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return input;
|
|
53
|
+
}
|
|
54
|
+
shouldCaptureHost(url) {
|
|
55
|
+
try {
|
|
56
|
+
const urlObj = new URL(url);
|
|
57
|
+
const hostname = urlObj.hostname;
|
|
58
|
+
const { mode, hosts = [], patterns = [] } = this.hostFilter;
|
|
59
|
+
if (mode === 'all')
|
|
60
|
+
return true;
|
|
61
|
+
if (mode === 'none')
|
|
62
|
+
return false;
|
|
63
|
+
// Check host patterns
|
|
64
|
+
for (const host of hosts) {
|
|
65
|
+
if (this.matchesHost(hostname, host)) {
|
|
66
|
+
return mode === 'include';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Check regex patterns
|
|
70
|
+
for (const pattern of patterns) {
|
|
71
|
+
try {
|
|
72
|
+
const regex = new RegExp(pattern, 'i');
|
|
73
|
+
if (regex.test(hostname)) {
|
|
74
|
+
return mode === 'include';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Invalid regex, skip
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// If include mode and no matches, don't capture
|
|
82
|
+
// If exclude mode and no matches, capture
|
|
83
|
+
return mode === 'exclude';
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Invalid URL, don't capture
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
matchesHost(hostname, pattern) {
|
|
91
|
+
// Exact match
|
|
92
|
+
if (hostname === pattern)
|
|
93
|
+
return true;
|
|
94
|
+
// Wildcard subdomain: *.example.com
|
|
95
|
+
if (pattern.startsWith('*.')) {
|
|
96
|
+
const domain = pattern.substring(2);
|
|
97
|
+
return hostname === domain || hostname.endsWith('.' + domain);
|
|
98
|
+
}
|
|
99
|
+
// Domain match (matches domain and all subdomains)
|
|
100
|
+
if (hostname === pattern || hostname.endsWith('.' + pattern)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
shouldCaptureError(errorMessage) {
|
|
106
|
+
const { include, exclude } = this.errorFilters;
|
|
107
|
+
// If no filters, capture everything
|
|
108
|
+
if (!include && !exclude)
|
|
109
|
+
return true;
|
|
110
|
+
// If include patterns specified, only capture matching errors
|
|
111
|
+
if (include && include.length > 0) {
|
|
112
|
+
const matchesInclude = include.some(pattern => {
|
|
113
|
+
try {
|
|
114
|
+
return new RegExp(pattern, 'i').test(errorMessage);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
console.warn('Invalid error filter pattern:', pattern);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (!matchesInclude)
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
// If exclude patterns specified, exclude matching errors
|
|
125
|
+
if (exclude && exclude.length > 0) {
|
|
126
|
+
const matchesExclude = exclude.some(pattern => {
|
|
127
|
+
try {
|
|
128
|
+
return new RegExp(pattern, 'i').test(errorMessage);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
console.warn('Invalid error filter pattern:', pattern);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
if (matchesExclude)
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
sanitizePayload(data) {
|
|
141
|
+
if (data === null || data === undefined) {
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
144
|
+
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') {
|
|
145
|
+
return data;
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(data)) {
|
|
148
|
+
return data.map(item => this.sanitizePayload(item));
|
|
149
|
+
}
|
|
150
|
+
if (typeof data === 'object') {
|
|
151
|
+
const sanitized = {};
|
|
152
|
+
const sensitiveKeys = ['privateKey', 'mnemonic', 'password', 'secret', 'token', 'apiKey', 'authorization'];
|
|
153
|
+
for (const [key, value] of Object.entries(data)) {
|
|
154
|
+
const lowerKey = key.toLowerCase();
|
|
155
|
+
const isSensitive = sensitiveKeys.some(sk => lowerKey.includes(sk.toLowerCase()));
|
|
156
|
+
if (isSensitive) {
|
|
157
|
+
sanitized[key] = '[REDACTED]';
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
sanitized[key] = this.sanitizePayload(value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return sanitized;
|
|
164
|
+
}
|
|
165
|
+
return data;
|
|
166
|
+
}
|
|
167
|
+
async safeReadResponse(response) {
|
|
168
|
+
try {
|
|
169
|
+
const cloned = response.clone();
|
|
170
|
+
const contentType = cloned.headers.get('content-type') || '';
|
|
171
|
+
if (contentType.includes('application/json')) {
|
|
172
|
+
const text = await cloned.text();
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(text);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return text;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (contentType.includes('text/')) {
|
|
181
|
+
return await cloned.text();
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
enqueue(event) {
|
|
190
|
+
try {
|
|
191
|
+
this.eventQueue.push(event);
|
|
192
|
+
// Flush if batch size reached
|
|
193
|
+
if (this.eventQueue.length >= this.batchSize) {
|
|
194
|
+
this.flush();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
// Silently fail - don't disrupt the app
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
startFlushTimer() {
|
|
202
|
+
if (this.flushTimer) {
|
|
203
|
+
clearInterval(this.flushTimer);
|
|
204
|
+
}
|
|
205
|
+
this.flushTimer = setInterval(() => {
|
|
206
|
+
this.flush();
|
|
207
|
+
}, this.flushInterval);
|
|
208
|
+
}
|
|
209
|
+
getFetch() {
|
|
210
|
+
// Use global fetch if available (Node.js 18+, modern browsers)
|
|
211
|
+
if (typeof fetch !== 'undefined') {
|
|
212
|
+
return fetch;
|
|
213
|
+
}
|
|
214
|
+
// Try to use node-fetch if available (for older Node.js)
|
|
215
|
+
try {
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
217
|
+
const nodeFetch = require('node-fetch');
|
|
218
|
+
// Handle both node-fetch v2 (default export) and v3 (named export)
|
|
219
|
+
return (nodeFetch.default || nodeFetch);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Silently fail - will be caught in flush method
|
|
223
|
+
throw new Error('fetch is not available. Please use Node.js 18+ or install node-fetch');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async flush(useKeepalive = false) {
|
|
227
|
+
if (this.eventQueue.length === 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const events = [...this.eventQueue];
|
|
231
|
+
this.eventQueue = [];
|
|
232
|
+
try {
|
|
233
|
+
const fetchFn = this.getFetch();
|
|
234
|
+
const requestInit = {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: {
|
|
237
|
+
'Content-Type': 'application/json',
|
|
238
|
+
},
|
|
239
|
+
body: JSON.stringify({ events }),
|
|
240
|
+
};
|
|
241
|
+
if (useKeepalive && typeof window !== 'undefined') {
|
|
242
|
+
requestInit.keepalive = true;
|
|
243
|
+
}
|
|
244
|
+
await fetchFn(`${this.apiEndpoint}/telemetry/batch`, requestInit);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
// Silently fail - don't disrupt the app
|
|
248
|
+
// Optionally, could re-queue events on failure, but for simplicity we'll drop them
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
initializeCapture() {
|
|
252
|
+
// Capture console errors
|
|
253
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
254
|
+
this.originalConsoleError = console.error.bind(console);
|
|
255
|
+
console.error = (...args) => {
|
|
256
|
+
this.originalConsoleError?.(...args);
|
|
257
|
+
try {
|
|
258
|
+
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
|
|
259
|
+
if (this.shouldCaptureError(message)) {
|
|
260
|
+
this.trackError({
|
|
261
|
+
type: 'console',
|
|
262
|
+
message,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Silently fail
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// Capture uncaught errors
|
|
272
|
+
if (typeof window !== 'undefined') {
|
|
273
|
+
window.addEventListener('error', (event) => {
|
|
274
|
+
try {
|
|
275
|
+
const message = event.message || 'Unknown error';
|
|
276
|
+
if (this.shouldCaptureError(message)) {
|
|
277
|
+
this.trackError({
|
|
278
|
+
type: 'uncaught',
|
|
279
|
+
message,
|
|
280
|
+
stack: event.error?.stack,
|
|
281
|
+
filename: event.filename,
|
|
282
|
+
lineno: event.lineno,
|
|
283
|
+
colno: event.colno,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Silently fail
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
// Capture unhandled promise rejections
|
|
292
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
293
|
+
try {
|
|
294
|
+
const reason = event.reason;
|
|
295
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
296
|
+
if (this.shouldCaptureError(message)) {
|
|
297
|
+
this.trackError({
|
|
298
|
+
type: 'promise',
|
|
299
|
+
message,
|
|
300
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
// Silently fail
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
trackError(error) {
|
|
311
|
+
this.enqueue({
|
|
312
|
+
type: 'error',
|
|
313
|
+
wallet: this.wallet,
|
|
314
|
+
platform: this.platform,
|
|
315
|
+
error,
|
|
316
|
+
timestamp: Date.now(),
|
|
317
|
+
sessionId: this.sessionId,
|
|
318
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
setWallet(walletAddress) {
|
|
322
|
+
try {
|
|
323
|
+
this.wallet = walletAddress;
|
|
324
|
+
// Track login event
|
|
325
|
+
this.enqueue({
|
|
326
|
+
type: 'login',
|
|
327
|
+
wallet: this.wallet,
|
|
328
|
+
platform: this.platform,
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
sessionId: this.sessionId,
|
|
331
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
// Silently fail
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
interceptFetch() {
|
|
339
|
+
// Only intercept in browser environments
|
|
340
|
+
if (typeof window === 'undefined') {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Check if fetch is available
|
|
344
|
+
if (typeof fetch === 'undefined') {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (this.originalFetch) {
|
|
348
|
+
return; // Already intercepted
|
|
349
|
+
}
|
|
350
|
+
this.originalFetch = window.fetch.bind(window);
|
|
351
|
+
window.fetch = async (input, init) => {
|
|
352
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
353
|
+
const method = init?.method || (typeof input === 'object' && 'method' in input ? input.method : 'GET');
|
|
354
|
+
// Check if we should capture this request
|
|
355
|
+
if (!this.shouldCaptureHost(url)) {
|
|
356
|
+
return this.originalFetch(input, init);
|
|
357
|
+
}
|
|
358
|
+
const requestId = this.generateRequestId();
|
|
359
|
+
const startTime = Date.now();
|
|
360
|
+
const payload = init?.body ? this.sanitizePayload(init.body) : undefined;
|
|
361
|
+
// Store request info
|
|
362
|
+
this.requestMap.set(requestId, { startTime, url, method });
|
|
363
|
+
// Track request start
|
|
364
|
+
this.enqueue({
|
|
365
|
+
type: 'request',
|
|
366
|
+
wallet: this.wallet,
|
|
367
|
+
platform: this.platform,
|
|
368
|
+
requestId,
|
|
369
|
+
url,
|
|
370
|
+
method,
|
|
371
|
+
payload: payload ? this.sanitizePayload(payload) : undefined,
|
|
372
|
+
timestamp: startTime,
|
|
373
|
+
sessionId: this.sessionId,
|
|
374
|
+
});
|
|
375
|
+
try {
|
|
376
|
+
const response = await this.originalFetch(input, init);
|
|
377
|
+
const duration = Date.now() - startTime;
|
|
378
|
+
// Read response safely
|
|
379
|
+
const responseData = await this.safeReadResponse(response);
|
|
380
|
+
// Track request completion
|
|
381
|
+
this.enqueue({
|
|
382
|
+
type: 'request',
|
|
383
|
+
wallet: this.wallet,
|
|
384
|
+
platform: this.platform,
|
|
385
|
+
requestId,
|
|
386
|
+
url,
|
|
387
|
+
method,
|
|
388
|
+
status: response.status,
|
|
389
|
+
response: this.sanitizePayload(responseData),
|
|
390
|
+
duration,
|
|
391
|
+
timestamp: Date.now(),
|
|
392
|
+
sessionId: this.sessionId,
|
|
393
|
+
});
|
|
394
|
+
this.requestMap.delete(requestId);
|
|
395
|
+
return response;
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
const duration = Date.now() - startTime;
|
|
399
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
400
|
+
// Track request error
|
|
401
|
+
this.enqueue({
|
|
402
|
+
type: 'request',
|
|
403
|
+
wallet: this.wallet,
|
|
404
|
+
platform: this.platform,
|
|
405
|
+
requestId,
|
|
406
|
+
url,
|
|
407
|
+
method,
|
|
408
|
+
error: errorMessage,
|
|
409
|
+
duration,
|
|
410
|
+
timestamp: Date.now(),
|
|
411
|
+
sessionId: this.sessionId,
|
|
412
|
+
});
|
|
413
|
+
this.requestMap.delete(requestId);
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
interceptAxios(axiosInstance) {
|
|
419
|
+
if (!axiosInstance || !axiosInstance.interceptors) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// Request interceptor
|
|
423
|
+
axiosInstance.interceptors.request.use((config) => {
|
|
424
|
+
const url = config.url || (config.baseURL ? `${config.baseURL}${config.url || ''}` : '');
|
|
425
|
+
const method = config.method?.toUpperCase() || 'GET';
|
|
426
|
+
if (!this.shouldCaptureHost(url)) {
|
|
427
|
+
return config;
|
|
428
|
+
}
|
|
429
|
+
const requestId = this.generateRequestId();
|
|
430
|
+
const startTime = Date.now();
|
|
431
|
+
// Store request metadata
|
|
432
|
+
config._telemetryRequestId = requestId;
|
|
433
|
+
config._telemetryStartTime = startTime;
|
|
434
|
+
// Track request start
|
|
435
|
+
this.enqueue({
|
|
436
|
+
type: 'request',
|
|
437
|
+
wallet: this.wallet,
|
|
438
|
+
platform: this.platform,
|
|
439
|
+
requestId,
|
|
440
|
+
url,
|
|
441
|
+
method,
|
|
442
|
+
payload: config.data ? this.sanitizePayload(config.data) : undefined,
|
|
443
|
+
timestamp: startTime,
|
|
444
|
+
sessionId: this.sessionId,
|
|
445
|
+
});
|
|
446
|
+
return config;
|
|
447
|
+
}, (error) => {
|
|
448
|
+
return Promise.reject(error);
|
|
449
|
+
});
|
|
450
|
+
// Response interceptor
|
|
451
|
+
axiosInstance.interceptors.response.use((response) => {
|
|
452
|
+
const config = response.config || {};
|
|
453
|
+
const requestId = config._telemetryRequestId;
|
|
454
|
+
const startTime = config._telemetryStartTime;
|
|
455
|
+
if (requestId && startTime) {
|
|
456
|
+
const duration = Date.now() - startTime;
|
|
457
|
+
const url = response.config?.url || response.request?.responseURL || '';
|
|
458
|
+
this.enqueue({
|
|
459
|
+
type: 'request',
|
|
460
|
+
wallet: this.wallet,
|
|
461
|
+
platform: this.platform,
|
|
462
|
+
requestId,
|
|
463
|
+
url,
|
|
464
|
+
method: config.method?.toUpperCase() || 'GET',
|
|
465
|
+
status: response.status,
|
|
466
|
+
response: this.sanitizePayload(response.data),
|
|
467
|
+
duration,
|
|
468
|
+
timestamp: Date.now(),
|
|
469
|
+
sessionId: this.sessionId,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return response;
|
|
473
|
+
}, (error) => {
|
|
474
|
+
const config = error.config || {};
|
|
475
|
+
const requestId = config._telemetryRequestId;
|
|
476
|
+
const startTime = config._telemetryStartTime;
|
|
477
|
+
if (requestId && startTime) {
|
|
478
|
+
const duration = Date.now() - startTime;
|
|
479
|
+
const url = config.url || error.request?.responseURL || '';
|
|
480
|
+
const errorMessage = error.message || 'Request failed';
|
|
481
|
+
this.enqueue({
|
|
482
|
+
type: 'request',
|
|
483
|
+
wallet: this.wallet,
|
|
484
|
+
platform: this.platform,
|
|
485
|
+
requestId,
|
|
486
|
+
url,
|
|
487
|
+
method: config.method?.toUpperCase() || 'GET',
|
|
488
|
+
error: errorMessage,
|
|
489
|
+
duration,
|
|
490
|
+
timestamp: Date.now(),
|
|
491
|
+
sessionId: this.sessionId,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return Promise.reject(error);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
trackPairView(pair, metadata) {
|
|
498
|
+
try {
|
|
499
|
+
this.enqueue({
|
|
500
|
+
type: 'pair_view',
|
|
501
|
+
wallet: this.wallet,
|
|
502
|
+
platform: this.platform,
|
|
503
|
+
pair,
|
|
504
|
+
metadata: metadata ? this.sanitizePayload(metadata) : undefined,
|
|
505
|
+
timestamp: Date.now(),
|
|
506
|
+
sessionId: this.sessionId,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
// Silently fail
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Track a system event (not tied to any wallet address)
|
|
515
|
+
* System events are displayed in a separate panel in the dashboard
|
|
516
|
+
* @param category - Event category (e.g., 'app_start', 'feature_used', 'config_change')
|
|
517
|
+
* @param message - Event message
|
|
518
|
+
* @param level - Event level (info, warning, error, debug)
|
|
519
|
+
* @param metadata - Optional additional metadata
|
|
520
|
+
*/
|
|
521
|
+
trackSystemEvent(category, message, level = 'info', metadata) {
|
|
522
|
+
try {
|
|
523
|
+
this.enqueue({
|
|
524
|
+
type: 'system_event',
|
|
525
|
+
wallet: null, // System events are not tied to wallets
|
|
526
|
+
platform: this.platform,
|
|
527
|
+
category,
|
|
528
|
+
message,
|
|
529
|
+
level,
|
|
530
|
+
metadata: metadata ? this.sanitizePayload(metadata) : undefined,
|
|
531
|
+
timestamp: Date.now(),
|
|
532
|
+
sessionId: this.sessionId,
|
|
533
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
// Silently fail
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Log a custom event with wallet address (backend-friendly method)
|
|
542
|
+
* @param walletAddress - The wallet address associated with this log
|
|
543
|
+
* @param eventType - Type of event (e.g., 'transaction', 'error', 'action')
|
|
544
|
+
* @param message - Log message
|
|
545
|
+
* @param metadata - Optional additional metadata
|
|
546
|
+
*/
|
|
547
|
+
logWalletEvent(walletAddress, eventType, message, metadata) {
|
|
548
|
+
try {
|
|
549
|
+
this.enqueue({
|
|
550
|
+
type: 'log', // Custom log type
|
|
551
|
+
wallet: walletAddress,
|
|
552
|
+
platform: this.platform,
|
|
553
|
+
logType: eventType,
|
|
554
|
+
message,
|
|
555
|
+
metadata: metadata ? this.sanitizePayload(metadata) : undefined,
|
|
556
|
+
timestamp: Date.now(),
|
|
557
|
+
sessionId: this.sessionId,
|
|
558
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
// Silently fail
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Send a log event immediately (bypasses batching, useful for critical logs)
|
|
567
|
+
* @param walletAddress - The wallet address associated with this log
|
|
568
|
+
* @param eventType - Type of event
|
|
569
|
+
* @param message - Log message
|
|
570
|
+
* @param metadata - Optional additional metadata
|
|
571
|
+
*/
|
|
572
|
+
async sendLog(walletAddress, eventType, message, metadata) {
|
|
573
|
+
try {
|
|
574
|
+
const event = {
|
|
575
|
+
type: 'log',
|
|
576
|
+
wallet: walletAddress,
|
|
577
|
+
platform: this.platform,
|
|
578
|
+
logType: eventType,
|
|
579
|
+
message,
|
|
580
|
+
metadata: metadata ? this.sanitizePayload(metadata) : undefined,
|
|
581
|
+
timestamp: Date.now(),
|
|
582
|
+
sessionId: this.sessionId,
|
|
583
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
584
|
+
};
|
|
585
|
+
const fetchFn = this.getFetch();
|
|
586
|
+
await fetchFn(`${this.apiEndpoint}/telemetry/batch`, {
|
|
587
|
+
method: 'POST',
|
|
588
|
+
headers: {
|
|
589
|
+
'Content-Type': 'application/json',
|
|
590
|
+
},
|
|
591
|
+
body: JSON.stringify({ events: [event] }),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
// Silently fail
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
updateHostFilter(captureHosts) {
|
|
599
|
+
try {
|
|
600
|
+
this.hostFilter = this.normalizeHostFilter(captureHosts);
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
// Silently fail
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
destroy() {
|
|
607
|
+
// Restore original functions
|
|
608
|
+
if (this.originalFetch && typeof window !== 'undefined') {
|
|
609
|
+
window.fetch = this.originalFetch;
|
|
610
|
+
}
|
|
611
|
+
if (this.originalConsoleError && typeof console !== 'undefined') {
|
|
612
|
+
console.error = this.originalConsoleError;
|
|
613
|
+
}
|
|
614
|
+
// Clear timer
|
|
615
|
+
if (this.flushTimer) {
|
|
616
|
+
clearInterval(this.flushTimer);
|
|
617
|
+
this.flushTimer = null;
|
|
618
|
+
}
|
|
619
|
+
// Flush remaining events
|
|
620
|
+
this.flush(true);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
exports.LavarageTelemetry = LavarageTelemetry;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface TelemetryConfig {
|
|
2
|
+
apiEndpoint: string;
|
|
3
|
+
platform: string;
|
|
4
|
+
captureHosts?: HostFilterInput;
|
|
5
|
+
errorFilters?: ErrorFilterConfig;
|
|
6
|
+
batchSize?: number;
|
|
7
|
+
flushInterval?: number;
|
|
8
|
+
}
|
|
9
|
+
export type HostFilterInput = string | string[] | HostFilterConfig;
|
|
10
|
+
export interface HostFilterConfig {
|
|
11
|
+
mode: 'all' | 'none' | 'include' | 'exclude';
|
|
12
|
+
hosts?: string[];
|
|
13
|
+
patterns?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface ErrorFilterConfig {
|
|
16
|
+
include?: string[];
|
|
17
|
+
exclude?: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface TelemetryEvent {
|
|
20
|
+
type: 'login' | 'pair_view' | 'error' | 'request' | 'system_event';
|
|
21
|
+
wallet: string | null;
|
|
22
|
+
platform: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
}
|
|
27
|
+
export interface SystemEvent {
|
|
28
|
+
category: string;
|
|
29
|
+
message: string;
|
|
30
|
+
level?: 'info' | 'warning' | 'error' | 'debug';
|
|
31
|
+
metadata?: object;
|
|
32
|
+
}
|
|
33
|
+
export interface ErrorEvent {
|
|
34
|
+
type: 'console' | 'uncaught' | 'promise';
|
|
35
|
+
message: string;
|
|
36
|
+
stack?: string;
|
|
37
|
+
filename?: string;
|
|
38
|
+
lineno?: number;
|
|
39
|
+
colno?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface RequestEvent {
|
|
42
|
+
requestId: string;
|
|
43
|
+
url: string;
|
|
44
|
+
method?: string;
|
|
45
|
+
status?: number;
|
|
46
|
+
payload?: any;
|
|
47
|
+
response?: any;
|
|
48
|
+
error?: string;
|
|
49
|
+
duration?: number;
|
|
50
|
+
timestamp: number;
|
|
51
|
+
type: 'complete' | 'error';
|
|
52
|
+
}
|
|
53
|
+
export interface BatchIngestRequest {
|
|
54
|
+
events: TelemetryEvent[];
|
|
55
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lavarage/telemetry",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production telemetry SDK for Lavarage and partner applications",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"test": "jest"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["telemetry", "analytics", "monitoring", "lavarage"],
|
|
14
|
+
"author": "Lavarage",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"node-fetch": "^2.6.0 || ^3.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"node-fetch": {
|
|
24
|
+
"optional": true
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.0.0",
|
|
29
|
+
"typescript": "^5.0.0",
|
|
30
|
+
"jest": "^29.0.0",
|
|
31
|
+
"@types/jest": "^29.0.0",
|
|
32
|
+
"ts-jest": "^29.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": ["dist", "README.md"]
|
|
35
|
+
}
|
|
36
|
+
|