@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 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
+
@@ -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;
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+