@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.
- package/CODE_REVIEW_REPORT.md +462 -0
- package/README.md +619 -0
- package/__tests__/amplitude-core.test.ts +279 -0
- package/__tests__/hooks.test.ts +478 -0
- package/__tests__/validation.test.ts +393 -0
- package/components/AnalyticsDashboard.tsx +339 -0
- package/components/ConsentManager.tsx +265 -0
- package/components/ExperimentWrapper.tsx +440 -0
- package/components/PerformanceMonitor.tsx +578 -0
- package/hooks/useAmplitude.ts +132 -0
- package/hooks/useAmplitudeEvents.ts +100 -0
- package/hooks/useExperiment.ts +195 -0
- package/hooks/useSessionReplay.ts +238 -0
- package/jest.setup.ts +276 -0
- package/lib/amplitude-core.ts +178 -0
- package/lib/cache.ts +181 -0
- package/lib/performance.ts +319 -0
- package/lib/queue.ts +389 -0
- package/lib/security.ts +188 -0
- package/package.json +15 -0
- package/plugin.config.ts +58 -0
- package/providers/AmplitudeProvider.tsx +113 -0
- package/styles/amplitude.css +593 -0
- package/translations/en.json +45 -0
- package/translations/es.json +45 -0
- package/tsconfig.json +47 -0
- package/types/amplitude.types.ts +105 -0
- package/utils/debounce.ts +133 -0
|
@@ -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
|
+
};
|