@nextsparkjs/plugin-amplitude 0.1.0-beta.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.
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Performance Monitor component for real-time plugin performance tracking
3
+ * Displays key metrics, alerts, and performance insights
4
+ */
5
+
6
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
7
+ import { useAmplitudeContext } from '../providers/AmplitudeProvider';
8
+ import { getPerformanceStats, getPerformanceMetrics } from '../lib/performance';
9
+ import { PerformanceMetric, PerformanceStats } from '../lib/performance';
10
+
11
+ // Component props
12
+ export interface PerformanceMonitorProps {
13
+ refreshInterval?: number;
14
+ showAlerts?: boolean;
15
+ showCharts?: boolean;
16
+ compactMode?: boolean;
17
+ alertThresholds?: PerformanceThresholds;
18
+ onAlert?: (alert: PerformanceAlert) => void;
19
+ className?: string;
20
+ style?: React.CSSProperties;
21
+ }
22
+
23
+ // Performance thresholds for alerts
24
+ export interface PerformanceThresholds {
25
+ errorRate: number;
26
+ memoryUsageMB: number;
27
+ latencyMs: number;
28
+ queueSize: number;
29
+ cacheHitRate: number;
30
+ }
31
+
32
+ // Performance alert
33
+ export interface PerformanceAlert {
34
+ id: string;
35
+ type: 'error_rate' | 'memory' | 'latency' | 'queue' | 'cache';
36
+ severity: 'warning' | 'critical';
37
+ message: string;
38
+ value: number;
39
+ threshold: number;
40
+ timestamp: number;
41
+ }
42
+
43
+ // Chart data point
44
+ export interface ChartDataPoint {
45
+ timestamp: number;
46
+ value: number;
47
+ label?: string;
48
+ }
49
+
50
+ const DEFAULT_THRESHOLDS: PerformanceThresholds = {
51
+ errorRate: 0.05, // 5%
52
+ memoryUsageMB: 100,
53
+ latencyMs: 1000,
54
+ queueSize: 1000,
55
+ cacheHitRate: 0.8, // 80%
56
+ };
57
+
58
+ /**
59
+ * Performance Monitor Component
60
+ */
61
+ export const PerformanceMonitor: React.FC<PerformanceMonitorProps> = ({
62
+ refreshInterval = 5000, // 5 seconds
63
+ showAlerts = true,
64
+ showCharts = true,
65
+ compactMode = false,
66
+ alertThresholds = DEFAULT_THRESHOLDS,
67
+ onAlert,
68
+ className,
69
+ style,
70
+ }) => {
71
+ const { isInitialized, config } = useAmplitudeContext();
72
+
73
+ const [stats, setStats] = useState<PerformanceStats | null>(null);
74
+ const [metrics, setMetrics] = useState<PerformanceMetric[]>([]);
75
+ const [alerts, setAlerts] = useState<PerformanceAlert[]>([]);
76
+ const [isLoading, setIsLoading] = useState(true);
77
+ const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
78
+ const [chartData, setChartData] = useState<Map<string, ChartDataPoint[]>>(new Map());
79
+
80
+ // Fetch performance data
81
+ const fetchPerformanceData = useCallback(async () => {
82
+ if (!isInitialized) return;
83
+
84
+ try {
85
+ setIsLoading(true);
86
+
87
+ const currentStats = getPerformanceStats();
88
+ const currentMetrics = getPerformanceMetrics();
89
+
90
+ setStats(currentStats);
91
+ setMetrics(currentMetrics);
92
+ setLastUpdate(new Date());
93
+
94
+ // Update chart data
95
+ const now = Date.now();
96
+ const newChartData = new Map(chartData);
97
+
98
+ // Add current data points
99
+ const metricsToChart = ['latency', 'memory_usage', 'error_rate', 'queue_size'];
100
+ metricsToChart.forEach(metricName => {
101
+ if (!newChartData.has(metricName)) {
102
+ newChartData.set(metricName, []);
103
+ }
104
+
105
+ const chartPoints = newChartData.get(metricName)!;
106
+ let value = 0;
107
+
108
+ // Map stats to chart values
109
+ switch (metricName) {
110
+ case 'latency':
111
+ value = currentStats.amplitudeCore.trackingLatency.reduce((a, b) => a + b, 0) / Math.max(currentStats.amplitudeCore.trackingLatency.length, 1);
112
+ break;
113
+ case 'memory_usage':
114
+ value = currentStats.amplitudeCore.memoryUsage / (1024 * 1024); // Convert to MB
115
+ break;
116
+ case 'error_rate':
117
+ value = currentStats.amplitudeCore.errorRate * 100; // Convert to percentage
118
+ break;
119
+ case 'queue_size':
120
+ value = currentStats.amplitudeCore.eventQueueSize;
121
+ break;
122
+ }
123
+
124
+ chartPoints.push({ timestamp: now, value });
125
+
126
+ // Keep only last 50 data points
127
+ if (chartPoints.length > 50) {
128
+ chartPoints.shift();
129
+ }
130
+ });
131
+
132
+ setChartData(newChartData);
133
+
134
+ // Check for alerts
135
+ if (showAlerts) {
136
+ checkForAlerts(currentStats);
137
+ }
138
+ } catch (error) {
139
+ console.error('[Performance Monitor] Failed to fetch data:', error);
140
+ } finally {
141
+ setIsLoading(false);
142
+ }
143
+ }, [isInitialized, chartData, showAlerts, alertThresholds]);
144
+
145
+ // Check for performance alerts
146
+ const checkForAlerts = useCallback((currentStats: PerformanceStats) => {
147
+ const newAlerts: PerformanceAlert[] = [];
148
+ const now = Date.now();
149
+
150
+ // Error rate alert
151
+ if (currentStats.amplitudeCore.errorRate > alertThresholds.errorRate) {
152
+ newAlerts.push({
153
+ id: `error_rate_${now}`,
154
+ type: 'error_rate',
155
+ severity: currentStats.amplitudeCore.errorRate > alertThresholds.errorRate * 2 ? 'critical' : 'warning',
156
+ message: `High error rate detected: ${(currentStats.amplitudeCore.errorRate * 100).toFixed(2)}%`,
157
+ value: currentStats.amplitudeCore.errorRate,
158
+ threshold: alertThresholds.errorRate,
159
+ timestamp: now,
160
+ });
161
+ }
162
+
163
+ // Memory usage alert
164
+ const memoryUsageMB = currentStats.amplitudeCore.memoryUsage / (1024 * 1024);
165
+ if (memoryUsageMB > alertThresholds.memoryUsageMB) {
166
+ newAlerts.push({
167
+ id: `memory_${now}`,
168
+ type: 'memory',
169
+ severity: memoryUsageMB > alertThresholds.memoryUsageMB * 1.5 ? 'critical' : 'warning',
170
+ message: `High memory usage: ${memoryUsageMB.toFixed(2)} MB`,
171
+ value: memoryUsageMB,
172
+ threshold: alertThresholds.memoryUsageMB,
173
+ timestamp: now,
174
+ });
175
+ }
176
+
177
+ // Average latency alert
178
+ const avgLatency = currentStats.amplitudeCore.trackingLatency.reduce((a, b) => a + b, 0) / Math.max(currentStats.amplitudeCore.trackingLatency.length, 1);
179
+ if (avgLatency > alertThresholds.latencyMs) {
180
+ newAlerts.push({
181
+ id: `latency_${now}`,
182
+ type: 'latency',
183
+ severity: avgLatency > alertThresholds.latencyMs * 2 ? 'critical' : 'warning',
184
+ message: `High tracking latency: ${avgLatency.toFixed(0)}ms`,
185
+ value: avgLatency,
186
+ threshold: alertThresholds.latencyMs,
187
+ timestamp: now,
188
+ });
189
+ }
190
+
191
+ // Queue size alert
192
+ if (currentStats.amplitudeCore.eventQueueSize > alertThresholds.queueSize) {
193
+ newAlerts.push({
194
+ id: `queue_${now}`,
195
+ type: 'queue',
196
+ severity: currentStats.amplitudeCore.eventQueueSize > alertThresholds.queueSize * 2 ? 'critical' : 'warning',
197
+ message: `Large event queue: ${currentStats.amplitudeCore.eventQueueSize} events`,
198
+ value: currentStats.amplitudeCore.eventQueueSize,
199
+ threshold: alertThresholds.queueSize,
200
+ timestamp: now,
201
+ });
202
+ }
203
+
204
+ // Add new alerts and notify
205
+ if (newAlerts.length > 0) {
206
+ setAlerts(prev => [...newAlerts, ...prev].slice(0, 10)); // Keep only 10 most recent alerts
207
+ newAlerts.forEach(alert => onAlert?.(alert));
208
+ }
209
+ }, [alertThresholds, onAlert]);
210
+
211
+ // Auto-refresh data
212
+ useEffect(() => {
213
+ fetchPerformanceData();
214
+
215
+ const interval = setInterval(fetchPerformanceData, refreshInterval);
216
+ return () => clearInterval(interval);
217
+ }, [fetchPerformanceData, refreshInterval]);
218
+
219
+ // Format numbers for display
220
+ const formatNumber = useCallback((num: number, decimals = 0): string => {
221
+ if (num >= 1000000) return `${(num / 1000000).toFixed(decimals)}M`;
222
+ if (num >= 1000) return `${(num / 1000).toFixed(decimals)}K`;
223
+ return num.toFixed(decimals);
224
+ }, []);
225
+
226
+ // Format bytes
227
+ const formatBytes = useCallback((bytes: number): string => {
228
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
229
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
230
+ return `${bytes} B`;
231
+ }, []);
232
+
233
+ // Get status color
234
+ const getStatusColor = useCallback((value: number, threshold: number, invert = false): string => {
235
+ const isGood = invert ? value < threshold : value > threshold;
236
+ return isGood ? '#10b981' : value > threshold * 1.5 ? '#ef4444' : '#f59e0b';
237
+ }, []);
238
+
239
+ // Simple chart component
240
+ const SimpleChart: React.FC<{ data: ChartDataPoint[]; color: string; height?: number }> = ({
241
+ data,
242
+ color,
243
+ height = 60
244
+ }) => {
245
+ const points = useMemo(() => {
246
+ if (data.length < 2) return '';
247
+
248
+ const minValue = Math.min(...data.map(d => d.value));
249
+ const maxValue = Math.max(...data.map(d => d.value));
250
+ const range = maxValue - minValue || 1;
251
+
252
+ const width = 200;
253
+ const stepX = width / (data.length - 1);
254
+
255
+ return data.map((point, index) => {
256
+ const x = index * stepX;
257
+ const y = height - ((point.value - minValue) / range) * height;
258
+ return `${x},${y}`;
259
+ }).join(' ');
260
+ }, [data, height]);
261
+
262
+ return (
263
+ <svg width="200" height={height} style={{ overflow: 'visible' }}>
264
+ <polyline
265
+ points={points}
266
+ fill="none"
267
+ stroke={color}
268
+ strokeWidth="2"
269
+ style={{ filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.1))' }}
270
+ />
271
+ {data.length > 0 && (
272
+ <circle
273
+ cx={200 * (data.length - 1) / Math.max(data.length - 1, 1)}
274
+ cy={height - ((data[data.length - 1].value - Math.min(...data.map(d => d.value))) / (Math.max(...data.map(d => d.value)) - Math.min(...data.map(d => d.value)) || 1)) * height}
275
+ r="3"
276
+ fill={color}
277
+ />
278
+ )}
279
+ </svg>
280
+ );
281
+ };
282
+
283
+ if (!isInitialized) {
284
+ return (
285
+ <div className={`amplitude-performance-monitor ${className || ''}`} style={style}>
286
+ <div style={{ padding: '16px', textAlign: 'center', color: '#6b7280' }}>
287
+ Amplitude plugin not initialized
288
+ </div>
289
+ </div>
290
+ );
291
+ }
292
+
293
+ if (isLoading && !stats) {
294
+ return (
295
+ <div className={`amplitude-performance-monitor ${className || ''}`} style={style}>
296
+ <div style={{ padding: '16px', textAlign: 'center', color: '#6b7280' }}>
297
+ Loading performance data...
298
+ </div>
299
+ </div>
300
+ );
301
+ }
302
+
303
+ return (
304
+ <div
305
+ className={`amplitude-performance-monitor ${compactMode ? 'compact' : ''} ${className || ''}`}
306
+ style={{
307
+ backgroundColor: '#ffffff',
308
+ borderRadius: '8px',
309
+ border: '1px solid #e5e7eb',
310
+ padding: compactMode ? '12px' : '16px',
311
+ fontFamily: 'system-ui, -apple-system, sans-serif',
312
+ fontSize: '14px',
313
+ ...style,
314
+ }}
315
+ >
316
+ {/* Header */}
317
+ <div style={{
318
+ display: 'flex',
319
+ justifyContent: 'space-between',
320
+ alignItems: 'center',
321
+ marginBottom: compactMode ? '8px' : '16px'
322
+ }}>
323
+ <h3 style={{
324
+ margin: 0,
325
+ fontSize: compactMode ? '14px' : '16px',
326
+ fontWeight: '600',
327
+ color: '#111827'
328
+ }}>
329
+ Plugin Performance
330
+ </h3>
331
+ <div style={{ fontSize: '12px', color: '#6b7280' }}>
332
+ Updated: {lastUpdate.toLocaleTimeString()}
333
+ </div>
334
+ </div>
335
+
336
+ {/* Alerts */}
337
+ {showAlerts && alerts.length > 0 && (
338
+ <div style={{ marginBottom: compactMode ? '8px' : '16px' }}>
339
+ {alerts.slice(0, compactMode ? 2 : 3).map(alert => (
340
+ <div
341
+ key={alert.id}
342
+ style={{
343
+ padding: '8px 12px',
344
+ backgroundColor: alert.severity === 'critical' ? '#fef2f2' : '#fffbeb',
345
+ border: `1px solid ${alert.severity === 'critical' ? '#fecaca' : '#fed7aa'}`,
346
+ borderRadius: '6px',
347
+ marginBottom: '4px',
348
+ fontSize: '12px',
349
+ color: alert.severity === 'critical' ? '#991b1b' : '#92400e',
350
+ }}
351
+ >
352
+ <strong>{alert.severity.toUpperCase()}</strong>: {alert.message}
353
+ </div>
354
+ ))}
355
+ </div>
356
+ )}
357
+
358
+ {/* Metrics Grid */}
359
+ {stats && (
360
+ <div style={{
361
+ display: 'grid',
362
+ gridTemplateColumns: compactMode ? 'repeat(2, 1fr)' : 'repeat(auto-fit, minmax(200px, 1fr))',
363
+ gap: compactMode ? '8px' : '12px',
364
+ marginBottom: showCharts ? (compactMode ? '8px' : '16px') : 0,
365
+ }}>
366
+ {/* Error Rate */}
367
+ <div style={{
368
+ padding: compactMode ? '8px' : '12px',
369
+ backgroundColor: '#f9fafb',
370
+ borderRadius: '6px',
371
+ border: '1px solid #f3f4f6',
372
+ }}>
373
+ <div style={{
374
+ fontSize: '12px',
375
+ color: '#6b7280',
376
+ marginBottom: '4px'
377
+ }}>
378
+ Error Rate
379
+ </div>
380
+ <div style={{
381
+ fontSize: compactMode ? '18px' : '20px',
382
+ fontWeight: '600',
383
+ color: getStatusColor(stats.amplitudeCore.errorRate, alertThresholds.errorRate),
384
+ marginBottom: '2px'
385
+ }}>
386
+ {(stats.amplitudeCore.errorRate * 100).toFixed(2)}%
387
+ </div>
388
+ <div style={{ fontSize: '10px', color: '#9ca3af' }}>
389
+ Threshold: {(alertThresholds.errorRate * 100).toFixed(1)}%
390
+ </div>
391
+ </div>
392
+
393
+ {/* Memory Usage */}
394
+ <div style={{
395
+ padding: compactMode ? '8px' : '12px',
396
+ backgroundColor: '#f9fafb',
397
+ borderRadius: '6px',
398
+ border: '1px solid #f3f4f6',
399
+ }}>
400
+ <div style={{
401
+ fontSize: '12px',
402
+ color: '#6b7280',
403
+ marginBottom: '4px'
404
+ }}>
405
+ Memory Usage
406
+ </div>
407
+ <div style={{
408
+ fontSize: compactMode ? '18px' : '20px',
409
+ fontWeight: '600',
410
+ color: getStatusColor(stats.amplitudeCore.memoryUsage / (1024 * 1024), alertThresholds.memoryUsageMB),
411
+ marginBottom: '2px'
412
+ }}>
413
+ {formatBytes(stats.amplitudeCore.memoryUsage)}
414
+ </div>
415
+ <div style={{ fontSize: '10px', color: '#9ca3af' }}>
416
+ Limit: {alertThresholds.memoryUsageMB} MB
417
+ </div>
418
+ </div>
419
+
420
+ {/* Average Latency */}
421
+ <div style={{
422
+ padding: compactMode ? '8px' : '12px',
423
+ backgroundColor: '#f9fafb',
424
+ borderRadius: '6px',
425
+ border: '1px solid #f3f4f6',
426
+ }}>
427
+ <div style={{
428
+ fontSize: '12px',
429
+ color: '#6b7280',
430
+ marginBottom: '4px'
431
+ }}>
432
+ Avg Latency
433
+ </div>
434
+ <div style={{
435
+ fontSize: compactMode ? '18px' : '20px',
436
+ fontWeight: '600',
437
+ color: getStatusColor(
438
+ stats.amplitudeCore.trackingLatency.reduce((a, b) => a + b, 0) / Math.max(stats.amplitudeCore.trackingLatency.length, 1),
439
+ alertThresholds.latencyMs
440
+ ),
441
+ marginBottom: '2px'
442
+ }}>
443
+ {Math.round(stats.amplitudeCore.trackingLatency.reduce((a, b) => a + b, 0) / Math.max(stats.amplitudeCore.trackingLatency.length, 1))}ms
444
+ </div>
445
+ <div style={{ fontSize: '10px', color: '#9ca3af' }}>
446
+ Threshold: {alertThresholds.latencyMs}ms
447
+ </div>
448
+ </div>
449
+
450
+ {/* Queue Size */}
451
+ <div style={{
452
+ padding: compactMode ? '8px' : '12px',
453
+ backgroundColor: '#f9fafb',
454
+ borderRadius: '6px',
455
+ border: '1px solid #f3f4f6',
456
+ }}>
457
+ <div style={{
458
+ fontSize: '12px',
459
+ color: '#6b7280',
460
+ marginBottom: '4px'
461
+ }}>
462
+ Event Queue
463
+ </div>
464
+ <div style={{
465
+ fontSize: compactMode ? '18px' : '20px',
466
+ fontWeight: '600',
467
+ color: getStatusColor(stats.amplitudeCore.eventQueueSize, alertThresholds.queueSize),
468
+ marginBottom: '2px'
469
+ }}>
470
+ {formatNumber(stats.amplitudeCore.eventQueueSize)}
471
+ </div>
472
+ <div style={{ fontSize: '10px', color: '#9ca3af' }}>
473
+ Limit: {formatNumber(alertThresholds.queueSize)}
474
+ </div>
475
+ </div>
476
+ </div>
477
+ )}
478
+
479
+ {/* Charts */}
480
+ {showCharts && !compactMode && chartData.size > 0 && (
481
+ <div style={{
482
+ display: 'grid',
483
+ gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
484
+ gap: '16px',
485
+ }}>
486
+ {Array.from(chartData.entries()).map(([metricName, data]) => (
487
+ <div key={metricName} style={{
488
+ padding: '12px',
489
+ backgroundColor: '#f9fafb',
490
+ borderRadius: '6px',
491
+ border: '1px solid #f3f4f6',
492
+ }}>
493
+ <div style={{
494
+ fontSize: '12px',
495
+ color: '#6b7280',
496
+ marginBottom: '8px',
497
+ textTransform: 'capitalize'
498
+ }}>
499
+ {metricName.replace('_', ' ')} Trend
500
+ </div>
501
+ <SimpleChart
502
+ data={data}
503
+ color={
504
+ metricName === 'error_rate' ? '#ef4444' :
505
+ metricName === 'memory_usage' ? '#f59e0b' :
506
+ metricName === 'latency' ? '#8b5cf6' :
507
+ '#10b981'
508
+ }
509
+ />
510
+ </div>
511
+ ))}
512
+ </div>
513
+ )}
514
+
515
+ {/* Footer Actions */}
516
+ {!compactMode && (
517
+ <div style={{
518
+ marginTop: '16px',
519
+ paddingTop: '12px',
520
+ borderTop: '1px solid #f3f4f6',
521
+ display: 'flex',
522
+ gap: '8px',
523
+ fontSize: '12px',
524
+ }}>
525
+ <button
526
+ onClick={fetchPerformanceData}
527
+ style={{
528
+ padding: '4px 8px',
529
+ backgroundColor: '#3b82f6',
530
+ color: 'white',
531
+ border: 'none',
532
+ borderRadius: '4px',
533
+ cursor: 'pointer',
534
+ fontSize: '12px',
535
+ }}
536
+ >
537
+ Refresh
538
+ </button>
539
+ <button
540
+ onClick={() => {
541
+ const metrics = getPerformanceMetrics();
542
+ const stats = getPerformanceStats();
543
+ const data = { metrics, stats };
544
+ console.log('Performance Data:', data);
545
+ }}
546
+ style={{
547
+ padding: '4px 8px',
548
+ backgroundColor: '#6b7280',
549
+ color: 'white',
550
+ border: 'none',
551
+ borderRadius: '4px',
552
+ cursor: 'pointer',
553
+ fontSize: '12px',
554
+ }}
555
+ >
556
+ Export Data
557
+ </button>
558
+ <button
559
+ onClick={() => setAlerts([])}
560
+ style={{
561
+ padding: '4px 8px',
562
+ backgroundColor: '#ef4444',
563
+ color: 'white',
564
+ border: 'none',
565
+ borderRadius: '4px',
566
+ cursor: 'pointer',
567
+ fontSize: '12px',
568
+ }}
569
+ >
570
+ Clear Alerts
571
+ </button>
572
+ </div>
573
+ )}
574
+ </div>
575
+ );
576
+ };
577
+
578
+ export default PerformanceMonitor;
@@ -0,0 +1,132 @@
1
+ import { useCallback, useRef, useEffect, useState } from 'react';
2
+ import { useAmplitudeContext } from '../providers/AmplitudeProvider';
3
+ import { EventProperties, EventType, UserProperties, UserId, isAmplitudeEvent, isValidUserId } from '../types/amplitude.types';
4
+ import { trackPerformanceMetric } from '../lib/performance';
5
+
6
+ const REQUEST_CACHE_TTL = 5000;
7
+
8
+ export const useAmplitude = () => {
9
+ const { amplitude, isInitialized, config, consent, error: providerError } = useAmplitudeContext();
10
+ const requestCache = useRef(new Map<string, { timestamp: number; promise: Promise<void> }>());
11
+ const isOnline = true; // Placeholder
12
+ const [lastError, setLastError] = useState<Error | null>(null);
13
+
14
+ useEffect(() => {
15
+ const cleanupCache = () => {
16
+ const now = Date.now();
17
+ for (const [key, entry] of requestCache.current.entries()) {
18
+ if (now - entry.timestamp > REQUEST_CACHE_TTL) {
19
+ requestCache.current.delete(key);
20
+ }
21
+ }
22
+ };
23
+
24
+ const interval = setInterval(cleanupCache, REQUEST_CACHE_TTL);
25
+ return () => clearInterval(interval);
26
+ }, []);
27
+
28
+ const checkPreconditions = useCallback((feature: string) => {
29
+ if (!isInitialized) {
30
+ throw new Error(`Amplitude not initialized - cannot ${feature}`);
31
+ }
32
+ if (!isOnline) {
33
+ throw new Error(`Network offline - cannot ${feature}`);
34
+ }
35
+ if (providerError) {
36
+ throw new Error(`Amplitude error: ${providerError.message}`);
37
+ }
38
+ }, [isInitialized, isOnline, providerError]);
39
+
40
+ const track = useCallback(async (eventType: EventType, properties?: EventProperties) => {
41
+ try {
42
+ checkPreconditions('track event');
43
+
44
+ if (!consent.analytics) {
45
+ console.warn('[Amplitude] Analytics consent not granted, skipping track');
46
+ return;
47
+ }
48
+
49
+ const cacheKey = `track_${eventType}_${JSON.stringify(properties)}`;
50
+ const cached = requestCache.current.get(cacheKey);
51
+
52
+ if (cached && Date.now() - cached.timestamp < REQUEST_CACHE_TTL) {
53
+ return cached.promise;
54
+ }
55
+
56
+ const promise = amplitude!.track(eventType, properties);
57
+ requestCache.current.set(cacheKey, { timestamp: Date.now(), promise });
58
+
59
+ await promise;
60
+ trackPerformanceMetric('amplitude_track_success', 1, 'counter');
61
+ } catch (error) {
62
+ const err = error instanceof Error ? error : new Error('Track failed');
63
+ setLastError(err);
64
+ trackPerformanceMetric('amplitude_track_error', 1, 'counter');
65
+ throw err;
66
+ }
67
+ }, [amplitude, checkPreconditions, consent.analytics]);
68
+
69
+ const identify = useCallback(async (userId: UserId, properties?: UserProperties) => {
70
+ try {
71
+ checkPreconditions('identify user');
72
+
73
+ if (!consent.analytics) {
74
+ console.warn('[Amplitude] Analytics consent not granted, skipping identify');
75
+ return;
76
+ }
77
+
78
+ if (!isValidUserId(userId)) {
79
+ throw new Error('Invalid user ID');
80
+ }
81
+
82
+ await amplitude!.identify(userId, properties);
83
+ trackPerformanceMetric('amplitude_identify_success', 1, 'counter');
84
+ } catch (error) {
85
+ const err = error instanceof Error ? error : new Error('Identify failed');
86
+ setLastError(err);
87
+ trackPerformanceMetric('amplitude_identify_error', 1, 'counter');
88
+ throw err;
89
+ }
90
+ }, [amplitude, checkPreconditions, consent.analytics]);
91
+
92
+ const setUserProperties = useCallback(async (properties: UserProperties) => {
93
+ try {
94
+ checkPreconditions('set user properties');
95
+
96
+ if (!consent.analytics) {
97
+ console.warn('[Amplitude] Analytics consent not granted, skipping setUserProperties');
98
+ return;
99
+ }
100
+
101
+ await amplitude!.setUserProperties(properties);
102
+ trackPerformanceMetric('amplitude_user_properties_success', 1, 'counter');
103
+ } catch (error) {
104
+ const err = error instanceof Error ? error : new Error('SetUserProperties failed');
105
+ setLastError(err);
106
+ trackPerformanceMetric('amplitude_user_properties_error', 1, 'counter');
107
+ throw err;
108
+ }
109
+ }, [amplitude, checkPreconditions, consent.analytics]);
110
+
111
+ const reset = useCallback(() => {
112
+ try {
113
+ checkPreconditions('reset');
114
+ amplitude!.reset();
115
+ trackPerformanceMetric('amplitude_reset', 1, 'counter');
116
+ } catch (error) {
117
+ const err = error instanceof Error ? error : new Error('Reset failed');
118
+ setLastError(err);
119
+ throw err;
120
+ }
121
+ }, [amplitude, checkPreconditions]);
122
+
123
+ return {
124
+ track,
125
+ identify,
126
+ setUserProperties,
127
+ reset,
128
+ isInitialized,
129
+ context: { config, consent, error: providerError || lastError },
130
+ lastError,
131
+ };
132
+ };