@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 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
- const urlObj = new URL(url);
128
- const hostname = urlObj.hostname;
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
- // Check host patterns
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
- if (this.matchesHost(hostname, host)) {
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
- if (hostname === pattern || hostname.endsWith('.' + pattern)) {
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
- const url = config.url || (config.baseURL ? `${config.baseURL}${config.url || ''}` : '');
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
- const url = response.config?.url || response.request?.responseURL || '';
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
- const url = config.url || error.request?.responseURL || '';
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') {
@@ -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.0",
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": ["telemetry", "analytics", "monitoring", "lavarage"],
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
- "typescript": "^5.0.0",
55
+ "@types/react": "^19.2.8",
56
+ "@types/react-dom": "^19.2.3",
32
57
  "jest": "^29.0.0",
33
- "@types/jest": "^29.0.0",
34
- "ts-jest": "^29.0.0"
58
+ "ts-jest": "^29.0.0",
59
+ "typescript": "^5.0.0"
35
60
  },
36
- "files": ["dist", "README.md"]
61
+ "files": [
62
+ "dist",
63
+ "README.md"
64
+ ]
37
65
  }
38
-