@lavarage/telemetry 1.1.0 → 1.2.1
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 +188 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +130 -10
- package/dist/react.d.ts +73 -0
- package/dist/react.js +149 -0
- package/dist/types.d.ts +8 -1
- package/package.json +36 -9
package/README.md
CHANGED
|
@@ -263,6 +263,22 @@ Track a system event (not tied to any wallet address). System events are display
|
|
|
263
263
|
```typescript
|
|
264
264
|
// Track app startup
|
|
265
265
|
telemetry.trackSystemEvent('app_start', 'Application initialized', 'info');
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### `trackStateChange(stateName: string, previousValue?: any, newValue?: any, action?: string, metadata?: object)`
|
|
269
|
+
|
|
270
|
+
Track a React state change. Useful for debugging and understanding user interactions.
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// Track a state change manually
|
|
274
|
+
telemetry.trackStateChange(
|
|
275
|
+
'userPreferences',
|
|
276
|
+
{ theme: 'light' },
|
|
277
|
+
{ theme: 'dark' },
|
|
278
|
+
'TOGGLE_THEME',
|
|
279
|
+
{ source: 'settings-panel' }
|
|
280
|
+
);
|
|
281
|
+
```
|
|
266
282
|
|
|
267
283
|
// Track feature usage
|
|
268
284
|
telemetry.trackSystemEvent('feature_used', 'User enabled dark mode', 'info', {
|
|
@@ -333,6 +349,177 @@ Clean up the telemetry instance, restore original functions, and flush remaining
|
|
|
333
349
|
telemetry.destroy();
|
|
334
350
|
```
|
|
335
351
|
|
|
352
|
+
## React State Tracking
|
|
353
|
+
|
|
354
|
+
The SDK provides React hooks for automatically tracking state changes in your React components.
|
|
355
|
+
|
|
356
|
+
### Installation
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
# Install the SDK
|
|
360
|
+
npm install @lavarage/telemetry
|
|
361
|
+
|
|
362
|
+
# If you want to use React hooks, also install React (peer dependency)
|
|
363
|
+
npm install react
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Quick Start
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import { LavarageTelemetry } from '@lavarage/telemetry';
|
|
370
|
+
// Import React hooks from the separate entry point
|
|
371
|
+
import { useTrackedState } from '@lavarage/telemetry/react';
|
|
372
|
+
|
|
373
|
+
const telemetry = new LavarageTelemetry({
|
|
374
|
+
apiEndpoint: 'https://telemetry.lavarage.com',
|
|
375
|
+
platform: 'lavarage-web',
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
function MyComponent() {
|
|
379
|
+
// Automatically tracks all state changes
|
|
380
|
+
const [count, setCount] = useTrackedState(0, {
|
|
381
|
+
stateName: 'counter',
|
|
382
|
+
telemetry: telemetry,
|
|
383
|
+
action: 'SET_COUNT'
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<button onClick={() => setCount(count + 1)}>
|
|
388
|
+
Count: {count}
|
|
389
|
+
</button>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Available Hooks
|
|
395
|
+
|
|
396
|
+
#### `useTrackedState(initialState, config)`
|
|
397
|
+
|
|
398
|
+
Wraps React's `useState` and automatically tracks state changes.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
const [state, setState] = useTrackedState(initialValue, {
|
|
402
|
+
stateName: 'userPreferences', // Required: name for this state
|
|
403
|
+
telemetry: telemetryInstance, // Required: telemetry instance
|
|
404
|
+
trackInitial: false, // Optional: track initial state (default: false)
|
|
405
|
+
action: 'UPDATE_PREFERENCES', // Optional: action name
|
|
406
|
+
metadata: { source: 'settings' }, // Optional: additional metadata
|
|
407
|
+
shouldTrack: (prev, next) => { // Optional: filter which changes to track
|
|
408
|
+
return prev !== next; // Only track if values actually changed
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### `useTrackedReducer(reducer, initialState, config)`
|
|
414
|
+
|
|
415
|
+
Wraps React's `useReducer` and automatically tracks state changes with action information.
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
const [state, dispatch] = useTrackedReducer(reducer, initialState, {
|
|
419
|
+
stateName: 'cart',
|
|
420
|
+
telemetry: telemetryInstance,
|
|
421
|
+
trackInitial: false,
|
|
422
|
+
metadata: { userId: '123' }
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Dispatch actions normally - they'll be tracked automatically
|
|
426
|
+
dispatch({ type: 'ADD_ITEM', payload: { id: 1, name: 'Product' } });
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### `useStateTracker(telemetry)`
|
|
430
|
+
|
|
431
|
+
Returns a function to manually track state changes with more control.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
const trackState = useStateTracker(telemetry);
|
|
435
|
+
|
|
436
|
+
const handleChange = (newValue) => {
|
|
437
|
+
trackState('theme', currentValue, newValue, 'SET_THEME', {
|
|
438
|
+
source: 'user-action'
|
|
439
|
+
});
|
|
440
|
+
setCurrentValue(newValue);
|
|
441
|
+
};
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Configuration Options
|
|
445
|
+
|
|
446
|
+
- **stateName** (required): A unique identifier for this state (e.g., 'cart', 'userPreferences', 'theme')
|
|
447
|
+
- **telemetry** (required): Your telemetry instance
|
|
448
|
+
- **trackInitial** (optional): Whether to track the initial state value (default: false)
|
|
449
|
+
- **action** (optional): Action name to use when tracking (default: 'STATE_CHANGE')
|
|
450
|
+
- **metadata** (optional): Additional context to include with each state change
|
|
451
|
+
- **shouldTrack** (optional): Function to filter which changes to track - useful for avoiding noise
|
|
452
|
+
|
|
453
|
+
### Example: Shopping Cart
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { useTrackedState } from '@lavarage/telemetry/react';
|
|
457
|
+
|
|
458
|
+
function ShoppingCart() {
|
|
459
|
+
const [items, setItems] = useTrackedState([], {
|
|
460
|
+
stateName: 'cartItems',
|
|
461
|
+
telemetry: telemetry,
|
|
462
|
+
action: 'CART_UPDATE',
|
|
463
|
+
shouldTrack: (prev, next) => {
|
|
464
|
+
// Only track if cart actually changed (not just reference)
|
|
465
|
+
return JSON.stringify(prev) !== JSON.stringify(next);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const addItem = (item) => {
|
|
470
|
+
setItems([...items, item]);
|
|
471
|
+
// State change is automatically tracked!
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
<div>
|
|
476
|
+
{items.map(item => <div key={item.id}>{item.name}</div>)}
|
|
477
|
+
<button onClick={() => addItem({ id: 1, name: 'Product' })}>
|
|
478
|
+
Add Item
|
|
479
|
+
</button>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Example: User Preferences with useReducer
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { useTrackedReducer } from '@lavarage/telemetry/react';
|
|
489
|
+
|
|
490
|
+
function preferencesReducer(state, action) {
|
|
491
|
+
switch (action.type) {
|
|
492
|
+
case 'SET_THEME':
|
|
493
|
+
return { ...state, theme: action.payload };
|
|
494
|
+
case 'SET_LANGUAGE':
|
|
495
|
+
return { ...state, language: action.payload };
|
|
496
|
+
default:
|
|
497
|
+
return state;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function UserPreferences() {
|
|
502
|
+
const [prefs, dispatch] = useTrackedReducer(
|
|
503
|
+
preferencesReducer,
|
|
504
|
+
{ theme: 'light', language: 'en' },
|
|
505
|
+
{
|
|
506
|
+
stateName: 'userPreferences',
|
|
507
|
+
telemetry: telemetry,
|
|
508
|
+
metadata: { userId: currentUser.id }
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<div>
|
|
514
|
+
<button onClick={() => dispatch({ type: 'SET_THEME', payload: 'dark' })}>
|
|
515
|
+
Dark Mode
|
|
516
|
+
</button>
|
|
517
|
+
{/* State changes are automatically tracked with action type! */}
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
336
523
|
## Event Types
|
|
337
524
|
|
|
338
525
|
The SDK tracks the following event types:
|
|
@@ -343,6 +530,7 @@ The SDK tracks the following event types:
|
|
|
343
530
|
- **request**: Network requests (fetch/Axios)
|
|
344
531
|
- **system_event**: System-level events not tied to any wallet address (displayed in separate dashboard panel)
|
|
345
532
|
- **log**: Custom log events (via `logWalletEvent()` or `sendLog()`)
|
|
533
|
+
- **state_change**: React state changes (via `trackStateChange()` or React hooks)
|
|
346
534
|
|
|
347
535
|
## Batching
|
|
348
536
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TelemetryConfig, TelemetryEvent, ErrorEvent, RequestEvent, HostFilterInput, HostFilterConfig, ErrorFilterConfig, SystemEvent } from './types';
|
|
2
2
|
export type { TelemetryConfig, TelemetryEvent, ErrorEvent, RequestEvent, HostFilterInput, HostFilterConfig, ErrorFilterConfig, SystemEvent, };
|
|
3
|
+
export type { StateChangeEvent } from './types';
|
|
3
4
|
export declare class LavarageTelemetry {
|
|
4
5
|
private apiEndpoint;
|
|
5
6
|
private platform;
|
|
@@ -64,5 +65,15 @@ export declare class LavarageTelemetry {
|
|
|
64
65
|
*/
|
|
65
66
|
sendLog(walletAddress: string, eventType: string, message: string, metadata?: object): Promise<void>;
|
|
66
67
|
updateHostFilter(captureHosts: HostFilterInput): void;
|
|
68
|
+
/**
|
|
69
|
+
* Track a React state change
|
|
70
|
+
* Use this method to manually log state changes, or use the useTrackedState/useTrackedReducer hooks for automatic tracking
|
|
71
|
+
* @param stateName - Name/identifier for the state (e.g., 'userPreferences', 'cartItems', 'theme')
|
|
72
|
+
* @param previousValue - Previous state value (optional, will be sanitized)
|
|
73
|
+
* @param newValue - New state value (optional, will be sanitized)
|
|
74
|
+
* @param action - Action that caused the change (e.g., 'SET_USER', 'ADD_TO_CART', 'TOGGLE_THEME')
|
|
75
|
+
* @param metadata - Additional context about the state change
|
|
76
|
+
*/
|
|
77
|
+
trackStateChange(stateName: string, previousValue?: any, newValue?: any, action?: string, metadata?: object): void;
|
|
67
78
|
destroy(): void;
|
|
68
79
|
}
|
package/dist/index.js
CHANGED
|
@@ -124,16 +124,57 @@ class LavarageTelemetry {
|
|
|
124
124
|
}
|
|
125
125
|
shouldCaptureHost(url) {
|
|
126
126
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
// Skip empty or invalid URLs
|
|
128
|
+
if (!url || url.trim() === '') {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// Resolve relative URLs to absolute URLs
|
|
132
|
+
let absoluteUrl;
|
|
133
|
+
try {
|
|
134
|
+
// Try to parse as absolute URL first
|
|
135
|
+
new URL(url);
|
|
136
|
+
absoluteUrl = url;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// If it fails, it's likely a relative URL - resolve it using current origin
|
|
140
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
141
|
+
try {
|
|
142
|
+
absoluteUrl = new URL(url, window.location.origin).href;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Still can't resolve, don't capture
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Not in browser environment, can't resolve relative URLs
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const urlObj = new URL(absoluteUrl);
|
|
155
|
+
let hostname = urlObj.hostname;
|
|
156
|
+
// Handle special URL types that don't have a hostname (data:, blob:, etc.)
|
|
157
|
+
if (!hostname || hostname === '') {
|
|
158
|
+
// For include mode, don't capture URLs without hostnames
|
|
159
|
+
// For exclude mode, these would be captured (but they're unlikely to be in exclude list)
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
// Remove port number if present (hostname property should already exclude it, but be defensive)
|
|
163
|
+
// Also normalize: lowercase and trim
|
|
164
|
+
hostname = hostname.split(':')[0].toLowerCase().trim();
|
|
129
165
|
const { mode, hosts = [], patterns = [] } = this.hostFilter;
|
|
130
166
|
if (mode === 'all')
|
|
131
167
|
return true;
|
|
132
168
|
if (mode === 'none')
|
|
133
169
|
return false;
|
|
134
|
-
//
|
|
170
|
+
// In include mode, if no hosts or patterns are specified, don't capture anything
|
|
171
|
+
if (mode === 'include' && hosts.length === 0 && patterns.length === 0) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
// Check host patterns (normalize them too)
|
|
135
175
|
for (const host of hosts) {
|
|
136
|
-
|
|
176
|
+
const normalizedHost = host.toLowerCase().trim();
|
|
177
|
+
if (this.matchesHost(hostname, normalizedHost)) {
|
|
137
178
|
return mode === 'include';
|
|
138
179
|
}
|
|
139
180
|
}
|
|
@@ -159,16 +200,26 @@ class LavarageTelemetry {
|
|
|
159
200
|
}
|
|
160
201
|
}
|
|
161
202
|
matchesHost(hostname, pattern) {
|
|
203
|
+
// Normalize inputs (should already be normalized, but be defensive)
|
|
204
|
+
hostname = hostname.toLowerCase().trim();
|
|
205
|
+
pattern = pattern.toLowerCase().trim();
|
|
162
206
|
// Exact match
|
|
163
207
|
if (hostname === pattern)
|
|
164
208
|
return true;
|
|
165
209
|
// Wildcard subdomain: *.example.com
|
|
166
210
|
if (pattern.startsWith('*.')) {
|
|
167
|
-
const domain = pattern.substring(2);
|
|
211
|
+
const domain = pattern.substring(2).toLowerCase().trim();
|
|
212
|
+
// Match exact domain or any subdomain
|
|
168
213
|
return hostname === domain || hostname.endsWith('.' + domain);
|
|
169
214
|
}
|
|
170
215
|
// Domain match (matches domain and all subdomains)
|
|
171
|
-
|
|
216
|
+
// e.g., 'lavarave.wtf' matches 'lavarave.wtf' and 'api.lavarave.wtf'
|
|
217
|
+
if (hostname === pattern) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
// Check if hostname is a subdomain of pattern
|
|
221
|
+
// e.g., 'api.lavarave.wtf' ends with '.lavarave.wtf'
|
|
222
|
+
if (hostname.endsWith('.' + pattern)) {
|
|
172
223
|
return true;
|
|
173
224
|
}
|
|
174
225
|
return false;
|
|
@@ -502,16 +553,53 @@ class LavarageTelemetry {
|
|
|
502
553
|
}
|
|
503
554
|
// Request interceptor
|
|
504
555
|
axiosInstance.interceptors.request.use((config) => {
|
|
505
|
-
|
|
556
|
+
// Construct full URL from axios config
|
|
557
|
+
let url;
|
|
558
|
+
try {
|
|
559
|
+
if (config.url) {
|
|
560
|
+
// If url is absolute (starts with http:// or https://), use it directly
|
|
561
|
+
if (config.url.startsWith('http://') || config.url.startsWith('https://')) {
|
|
562
|
+
url = config.url;
|
|
563
|
+
}
|
|
564
|
+
else if (config.baseURL) {
|
|
565
|
+
// Relative URL with baseURL - use URL constructor to properly combine them
|
|
566
|
+
try {
|
|
567
|
+
url = new URL(config.url, config.baseURL).href;
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Fallback to manual concatenation if URL constructor fails
|
|
571
|
+
const base = config.baseURL.endsWith('/') ? config.baseURL.slice(0, -1) : config.baseURL;
|
|
572
|
+
const path = config.url.startsWith('/') ? config.url : '/' + config.url;
|
|
573
|
+
url = base + path;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
// Relative URL without baseURL - will be resolved in shouldCaptureHost
|
|
578
|
+
url = config.url;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
else if (config.baseURL) {
|
|
582
|
+
url = config.baseURL;
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
// No URL and no baseURL - skip capture
|
|
586
|
+
return config;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// If URL construction fails, skip capture
|
|
591
|
+
return config;
|
|
592
|
+
}
|
|
506
593
|
const method = config.method?.toUpperCase() || 'GET';
|
|
507
594
|
if (!this.shouldCaptureHost(url)) {
|
|
508
595
|
return config;
|
|
509
596
|
}
|
|
510
597
|
const requestId = this.generateRequestId();
|
|
511
598
|
const startTime = Date.now();
|
|
512
|
-
// Store request metadata
|
|
599
|
+
// Store request metadata (including full URL for response interceptor)
|
|
513
600
|
config._telemetryRequestId = requestId;
|
|
514
601
|
config._telemetryStartTime = startTime;
|
|
602
|
+
config._telemetryUrl = url; // Store full URL for response interceptor
|
|
515
603
|
// Track request start
|
|
516
604
|
this.enqueue({
|
|
517
605
|
type: 'request',
|
|
@@ -535,7 +623,8 @@ class LavarageTelemetry {
|
|
|
535
623
|
const startTime = config._telemetryStartTime;
|
|
536
624
|
if (requestId && startTime) {
|
|
537
625
|
const duration = Date.now() - startTime;
|
|
538
|
-
|
|
626
|
+
// Use stored URL from request interceptor, fallback to response URL
|
|
627
|
+
const url = config._telemetryUrl || response.config?.url || response.request?.responseURL || '';
|
|
539
628
|
this.enqueue({
|
|
540
629
|
type: 'request',
|
|
541
630
|
wallet: this.wallet,
|
|
@@ -557,7 +646,8 @@ class LavarageTelemetry {
|
|
|
557
646
|
const startTime = config._telemetryStartTime;
|
|
558
647
|
if (requestId && startTime) {
|
|
559
648
|
const duration = Date.now() - startTime;
|
|
560
|
-
|
|
649
|
+
// Use stored URL from request interceptor, fallback to error config URL
|
|
650
|
+
const url = config._telemetryUrl || config.url || error.request?.responseURL || '';
|
|
561
651
|
const errorMessage = error.message || 'Request failed';
|
|
562
652
|
this.enqueue({
|
|
563
653
|
type: 'request',
|
|
@@ -684,6 +774,36 @@ class LavarageTelemetry {
|
|
|
684
774
|
// Silently fail
|
|
685
775
|
}
|
|
686
776
|
}
|
|
777
|
+
/**
|
|
778
|
+
* Track a React state change
|
|
779
|
+
* Use this method to manually log state changes, or use the useTrackedState/useTrackedReducer hooks for automatic tracking
|
|
780
|
+
* @param stateName - Name/identifier for the state (e.g., 'userPreferences', 'cartItems', 'theme')
|
|
781
|
+
* @param previousValue - Previous state value (optional, will be sanitized)
|
|
782
|
+
* @param newValue - New state value (optional, will be sanitized)
|
|
783
|
+
* @param action - Action that caused the change (e.g., 'SET_USER', 'ADD_TO_CART', 'TOGGLE_THEME')
|
|
784
|
+
* @param metadata - Additional context about the state change
|
|
785
|
+
*/
|
|
786
|
+
trackStateChange(stateName, previousValue, newValue, action, metadata) {
|
|
787
|
+
try {
|
|
788
|
+
const event = {
|
|
789
|
+
type: 'state_change',
|
|
790
|
+
wallet: this.wallet,
|
|
791
|
+
platform: this.platform,
|
|
792
|
+
stateName,
|
|
793
|
+
previousValue: previousValue !== undefined ? this.sanitizePayload(previousValue) : undefined,
|
|
794
|
+
newValue: newValue !== undefined ? this.sanitizePayload(newValue) : undefined,
|
|
795
|
+
action: action || 'STATE_CHANGE',
|
|
796
|
+
metadata: metadata ? this.sanitizePayload(metadata) : undefined,
|
|
797
|
+
timestamp: Date.now(),
|
|
798
|
+
sessionId: this.sessionId,
|
|
799
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
800
|
+
};
|
|
801
|
+
this.enqueue(event);
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
// Silently fail
|
|
805
|
+
}
|
|
806
|
+
}
|
|
687
807
|
destroy() {
|
|
688
808
|
// Restore original functions
|
|
689
809
|
if (this.originalFetch && typeof window !== 'undefined') {
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks for automatic state change tracking
|
|
3
|
+
*
|
|
4
|
+
* These hooks wrap React's useState and useReducer to automatically
|
|
5
|
+
* track state changes to the telemetry system.
|
|
6
|
+
*/
|
|
7
|
+
import { Dispatch, SetStateAction, Reducer } from 'react';
|
|
8
|
+
import { LavarageTelemetry } from './index';
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for tracked state
|
|
11
|
+
*/
|
|
12
|
+
export interface TrackedStateConfig {
|
|
13
|
+
/** Name/identifier for this state (e.g., 'userPreferences', 'cartItems') */
|
|
14
|
+
stateName: string;
|
|
15
|
+
/** Telemetry instance to use for tracking */
|
|
16
|
+
telemetry: LavarageTelemetry;
|
|
17
|
+
/** Whether to track the initial state value (default: false) */
|
|
18
|
+
trackInitial?: boolean;
|
|
19
|
+
/** Action name to use when tracking (default: 'STATE_CHANGE') */
|
|
20
|
+
action?: string;
|
|
21
|
+
/** Additional metadata to include with each state change */
|
|
22
|
+
metadata?: object;
|
|
23
|
+
/** Function to determine if a state change should be tracked (useful for filtering) */
|
|
24
|
+
shouldTrack?: (previousValue: any, newValue: any) => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Hook that wraps useState and automatically tracks state changes
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const [count, setCount] = useTrackedState(0, {
|
|
32
|
+
* stateName: 'counter',
|
|
33
|
+
* telemetry: telemetryInstance,
|
|
34
|
+
* action: 'SET_COUNT'
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function useTrackedState<T>(initialState: T | (() => T), config: TrackedStateConfig): [T, Dispatch<SetStateAction<T>>];
|
|
39
|
+
/**
|
|
40
|
+
* Action type for tracked reducer
|
|
41
|
+
*/
|
|
42
|
+
export interface TrackedReducerAction {
|
|
43
|
+
type: string;
|
|
44
|
+
payload?: any;
|
|
45
|
+
[key: string]: any;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Hook that wraps useReducer and automatically tracks state changes
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* const [state, dispatch] = useTrackedReducer(reducer, initialState, {
|
|
53
|
+
* stateName: 'cart',
|
|
54
|
+
* telemetry: telemetryInstance
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function useTrackedReducer<TState, TAction extends TrackedReducerAction>(reducer: Reducer<TState, TAction>, initialState: TState, config: TrackedStateConfig): [TState, Dispatch<TAction>];
|
|
59
|
+
/**
|
|
60
|
+
* Hook to manually track state changes with more control
|
|
61
|
+
* Useful when you want to track state changes but don't want to use the tracked hooks
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* const trackState = useStateTracker(telemetryInstance);
|
|
66
|
+
*
|
|
67
|
+
* const handleChange = (newValue) => {
|
|
68
|
+
* trackState('userPreferences', currentValue, newValue, 'UPDATE_PREFERENCES');
|
|
69
|
+
* setCurrentValue(newValue);
|
|
70
|
+
* };
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export declare function useStateTracker(telemetry: LavarageTelemetry): (stateName: string, previousValue?: any, newValue?: any, action?: string, metadata?: object) => void;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* React hooks for automatic state change tracking
|
|
4
|
+
*
|
|
5
|
+
* These hooks wrap React's useState and useReducer to automatically
|
|
6
|
+
* track state changes to the telemetry system.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.useTrackedState = useTrackedState;
|
|
10
|
+
exports.useTrackedReducer = useTrackedReducer;
|
|
11
|
+
exports.useStateTracker = useStateTracker;
|
|
12
|
+
const react_1 = require("react");
|
|
13
|
+
/**
|
|
14
|
+
* Hook that wraps useState and automatically tracks state changes
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const [count, setCount] = useTrackedState(0, {
|
|
19
|
+
* stateName: 'counter',
|
|
20
|
+
* telemetry: telemetryInstance,
|
|
21
|
+
* action: 'SET_COUNT'
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function useTrackedState(initialState, config) {
|
|
26
|
+
const { stateName, telemetry, trackInitial = false, action, metadata, shouldTrack } = config;
|
|
27
|
+
const [state, setState] = (0, react_1.useState)(initialState);
|
|
28
|
+
const previousStateRef = (0, react_1.useRef)(state);
|
|
29
|
+
const isInitialMount = (0, react_1.useRef)(true);
|
|
30
|
+
// Track initial state if configured
|
|
31
|
+
(0, react_1.useEffect)(() => {
|
|
32
|
+
if (trackInitial && isInitialMount.current) {
|
|
33
|
+
isInitialMount.current = false;
|
|
34
|
+
telemetry.trackStateChange(stateName, undefined, state, action || 'INITIAL_STATE', metadata);
|
|
35
|
+
}
|
|
36
|
+
}, []);
|
|
37
|
+
// Track state changes
|
|
38
|
+
(0, react_1.useEffect)(() => {
|
|
39
|
+
// Skip tracking on initial mount unless trackInitial is true
|
|
40
|
+
if (isInitialMount.current && !trackInitial) {
|
|
41
|
+
isInitialMount.current = false;
|
|
42
|
+
previousStateRef.current = state;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (isInitialMount.current) {
|
|
46
|
+
isInitialMount.current = false;
|
|
47
|
+
previousStateRef.current = state;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const previousValue = previousStateRef.current;
|
|
51
|
+
// Check if we should track this change
|
|
52
|
+
if (shouldTrack && !shouldTrack(previousValue, state)) {
|
|
53
|
+
previousStateRef.current = state;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Track the state change
|
|
57
|
+
telemetry.trackStateChange(stateName, previousValue, state, action, metadata);
|
|
58
|
+
previousStateRef.current = state;
|
|
59
|
+
}, [state, stateName, telemetry, action, metadata, shouldTrack]);
|
|
60
|
+
// Wrapper for setState that ensures tracking
|
|
61
|
+
const trackedSetState = (value) => {
|
|
62
|
+
setState(value);
|
|
63
|
+
};
|
|
64
|
+
return [state, trackedSetState];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Hook that wraps useReducer and automatically tracks state changes
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* const [state, dispatch] = useTrackedReducer(reducer, initialState, {
|
|
72
|
+
* stateName: 'cart',
|
|
73
|
+
* telemetry: telemetryInstance
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
function useTrackedReducer(reducer, initialState, config) {
|
|
78
|
+
const { stateName, telemetry, trackInitial = false, metadata, shouldTrack } = config;
|
|
79
|
+
const [state, dispatch] = (0, react_1.useReducer)(reducer, initialState);
|
|
80
|
+
const previousStateRef = (0, react_1.useRef)(state);
|
|
81
|
+
const isInitialMount = (0, react_1.useRef)(true);
|
|
82
|
+
// Track initial state if configured
|
|
83
|
+
(0, react_1.useEffect)(() => {
|
|
84
|
+
if (trackInitial && isInitialMount.current) {
|
|
85
|
+
isInitialMount.current = false;
|
|
86
|
+
telemetry.trackStateChange(stateName, undefined, state, 'INITIAL_STATE', metadata);
|
|
87
|
+
}
|
|
88
|
+
}, []);
|
|
89
|
+
// Track state changes
|
|
90
|
+
(0, react_1.useEffect)(() => {
|
|
91
|
+
// Skip tracking on initial mount unless trackInitial is true
|
|
92
|
+
if (isInitialMount.current && !trackInitial) {
|
|
93
|
+
isInitialMount.current = false;
|
|
94
|
+
previousStateRef.current = state;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (isInitialMount.current) {
|
|
98
|
+
isInitialMount.current = false;
|
|
99
|
+
previousStateRef.current = state;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const previousValue = previousStateRef.current;
|
|
103
|
+
// Check if we should track this change
|
|
104
|
+
if (shouldTrack && !shouldTrack(previousValue, state)) {
|
|
105
|
+
previousStateRef.current = state;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Track the state change (action type will be included in the reducer action)
|
|
109
|
+
telemetry.trackStateChange(stateName, previousValue, state, undefined, // Action will be determined by the reducer
|
|
110
|
+
metadata);
|
|
111
|
+
previousStateRef.current = state;
|
|
112
|
+
}, [state, stateName, telemetry, metadata, shouldTrack]);
|
|
113
|
+
// Wrapper for dispatch that tracks the action
|
|
114
|
+
const trackedDispatch = (action) => {
|
|
115
|
+
// Track before dispatching
|
|
116
|
+
const previousValue = previousStateRef.current;
|
|
117
|
+
dispatch(action);
|
|
118
|
+
// Note: The actual state change will be tracked in the useEffect above
|
|
119
|
+
// But we can also track the action here for immediate feedback
|
|
120
|
+
if (!shouldTrack || shouldTrack(previousValue, state)) {
|
|
121
|
+
telemetry.trackStateChange(stateName, previousValue, state, // This will be the old state, useEffect will track the new one
|
|
122
|
+
action.type || 'REDUCER_ACTION', {
|
|
123
|
+
...metadata,
|
|
124
|
+
actionPayload: action.payload,
|
|
125
|
+
...action
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
return [state, trackedDispatch];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Hook to manually track state changes with more control
|
|
133
|
+
* Useful when you want to track state changes but don't want to use the tracked hooks
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* const trackState = useStateTracker(telemetryInstance);
|
|
138
|
+
*
|
|
139
|
+
* const handleChange = (newValue) => {
|
|
140
|
+
* trackState('userPreferences', currentValue, newValue, 'UPDATE_PREFERENCES');
|
|
141
|
+
* setCurrentValue(newValue);
|
|
142
|
+
* };
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
function useStateTracker(telemetry) {
|
|
146
|
+
return (stateName, previousValue, newValue, action, metadata) => {
|
|
147
|
+
telemetry.trackStateChange(stateName, previousValue, newValue, action, metadata);
|
|
148
|
+
};
|
|
149
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface ErrorFilterConfig {
|
|
|
17
17
|
exclude?: string[];
|
|
18
18
|
}
|
|
19
19
|
export interface TelemetryEvent {
|
|
20
|
-
type: 'login' | 'pair_view' | 'error' | 'request' | 'system_event';
|
|
20
|
+
type: 'login' | 'pair_view' | 'error' | 'request' | 'system_event' | 'state_change';
|
|
21
21
|
wallet: string | null;
|
|
22
22
|
platform: string;
|
|
23
23
|
timestamp: number;
|
|
@@ -53,3 +53,10 @@ export interface RequestEvent {
|
|
|
53
53
|
export interface BatchIngestRequest {
|
|
54
54
|
events: TelemetryEvent[];
|
|
55
55
|
}
|
|
56
|
+
export interface StateChangeEvent {
|
|
57
|
+
stateName: string;
|
|
58
|
+
previousValue?: any;
|
|
59
|
+
newValue?: any;
|
|
60
|
+
action?: string;
|
|
61
|
+
metadata?: object;
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lavarage/telemetry",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Production telemetry SDK for Lavarage and partner applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -10,29 +10,56 @@
|
|
|
10
10
|
"node-fetch": false
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "tsc",
|
|
13
|
+
"build": "tsc && tsc --project tsconfig.react.json",
|
|
14
|
+
"build:main": "tsc",
|
|
15
|
+
"build:react": "tsc --project tsconfig.react.json",
|
|
14
16
|
"dev": "tsc --watch",
|
|
15
17
|
"prepublishOnly": "npm run build",
|
|
16
18
|
"test": "jest"
|
|
17
19
|
},
|
|
18
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"telemetry",
|
|
22
|
+
"analytics",
|
|
23
|
+
"monitoring",
|
|
24
|
+
"lavarage"
|
|
25
|
+
],
|
|
19
26
|
"author": "Lavarage",
|
|
20
27
|
"license": "MIT",
|
|
21
28
|
"peerDependencies": {
|
|
22
|
-
"node-fetch": "^2.6.0 || ^3.0.0"
|
|
29
|
+
"node-fetch": "^2.6.0 || ^3.0.0",
|
|
30
|
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
|
23
31
|
},
|
|
24
32
|
"peerDependenciesMeta": {
|
|
25
33
|
"node-fetch": {
|
|
26
34
|
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"react": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"import": "./dist/index.js",
|
|
43
|
+
"require": "./dist/index.js",
|
|
44
|
+
"types": "./dist/index.d.ts"
|
|
45
|
+
},
|
|
46
|
+
"./react": {
|
|
47
|
+
"import": "./dist/react.js",
|
|
48
|
+
"require": "./dist/react.js",
|
|
49
|
+
"types": "./dist/react.d.ts"
|
|
27
50
|
}
|
|
28
51
|
},
|
|
29
52
|
"devDependencies": {
|
|
53
|
+
"@types/jest": "^29.0.0",
|
|
30
54
|
"@types/node": "^20.0.0",
|
|
31
|
-
"
|
|
55
|
+
"@types/react": "^19.2.8",
|
|
56
|
+
"@types/react-dom": "^19.2.3",
|
|
32
57
|
"jest": "^29.0.0",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
58
|
+
"ts-jest": "^29.0.0",
|
|
59
|
+
"typescript": "^5.0.0"
|
|
35
60
|
},
|
|
36
|
-
"files": [
|
|
61
|
+
"files": [
|
|
62
|
+
"dist",
|
|
63
|
+
"README.md"
|
|
64
|
+
]
|
|
37
65
|
}
|
|
38
|
-
|