@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,339 @@
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import { useAmplitudeContext } from '../providers/AmplitudeProvider';
3
+ import { getPerformanceMetrics, getPerformanceStats } from '../lib/performance';
4
+
5
+ interface AnalyticsDashboardProps {
6
+ refreshInterval?: number;
7
+ showAdvancedMetrics?: boolean;
8
+ timeRange?: '1h' | '24h' | '7d' | '30d';
9
+ compactMode?: boolean;
10
+ onAlert?: (alert: PerformanceAlert) => void;
11
+ }
12
+
13
+ interface PerformanceAlert {
14
+ type: 'warning' | 'error' | 'critical';
15
+ message: string;
16
+ metric: string;
17
+ value: number;
18
+ threshold: number;
19
+ timestamp: number;
20
+ }
21
+
22
+ interface DashboardMetrics {
23
+ eventsProcessed: number;
24
+ errorRate: number;
25
+ averageLatency: number;
26
+ queueSize: number;
27
+ memoryUsage: number;
28
+ cacheHitRate: number;
29
+ activeUsers: number;
30
+ conversionRate: number;
31
+ }
32
+
33
+ export const AnalyticsDashboard: React.FC<AnalyticsDashboardProps> = ({
34
+ refreshInterval = 30000,
35
+ showAdvancedMetrics = true,
36
+ timeRange = '24h',
37
+ compactMode = false,
38
+ onAlert
39
+ }) => {
40
+ const { isInitialized, error, config } = useAmplitudeContext();
41
+ const [metrics, setMetrics] = useState<any[]>([]);
42
+ const [dashboardMetrics, setDashboardMetrics] = useState<DashboardMetrics>({
43
+ eventsProcessed: 0,
44
+ errorRate: 0,
45
+ averageLatency: 0,
46
+ queueSize: 0,
47
+ memoryUsage: 0,
48
+ cacheHitRate: 0,
49
+ activeUsers: 0,
50
+ conversionRate: 0
51
+ });
52
+ const [loading, setLoading] = useState(true);
53
+ const [alerts, setAlerts] = useState<PerformanceAlert[]>([]);
54
+
55
+ const checkThresholds = useCallback((stats: any) => {
56
+ const newAlerts: PerformanceAlert[] = [];
57
+
58
+ // Error rate threshold
59
+ if (stats.amplitudeCore?.errorRate > 0.05) {
60
+ newAlerts.push({
61
+ type: 'warning',
62
+ message: 'High error rate detected',
63
+ metric: 'errorRate',
64
+ value: stats.amplitudeCore.errorRate,
65
+ threshold: 0.05,
66
+ timestamp: Date.now()
67
+ });
68
+ }
69
+
70
+ // Memory usage threshold (100MB)
71
+ if (stats.amplitudeCore?.memoryUsage > 100 * 1024 * 1024) {
72
+ newAlerts.push({
73
+ type: 'warning',
74
+ message: 'High memory usage detected',
75
+ metric: 'memoryUsage',
76
+ value: stats.amplitudeCore.memoryUsage,
77
+ threshold: 100 * 1024 * 1024,
78
+ timestamp: Date.now()
79
+ });
80
+ }
81
+
82
+ // Queue size threshold
83
+ if (stats.amplitudeCore?.eventQueueSize > 1000) {
84
+ newAlerts.push({
85
+ type: 'error',
86
+ message: 'Event queue size too large',
87
+ metric: 'queueSize',
88
+ value: stats.amplitudeCore.eventQueueSize,
89
+ threshold: 1000,
90
+ timestamp: Date.now()
91
+ });
92
+ }
93
+
94
+ // Latency threshold (1000ms)
95
+ const avgLatency = stats.amplitudeCore?.trackingLatency?.reduce((a: number, b: number) => a + b, 0) /
96
+ (stats.amplitudeCore?.trackingLatency?.length || 1);
97
+ if (avgLatency > 1000) {
98
+ newAlerts.push({
99
+ type: 'warning',
100
+ message: 'High tracking latency detected',
101
+ metric: 'latency',
102
+ value: avgLatency,
103
+ threshold: 1000,
104
+ timestamp: Date.now()
105
+ });
106
+ }
107
+
108
+ if (newAlerts.length > 0) {
109
+ setAlerts(prev => [...prev.slice(-10), ...newAlerts]); // Keep last 10 alerts
110
+ newAlerts.forEach(alert => onAlert?.(alert));
111
+ }
112
+ }, [onAlert]);
113
+
114
+ const refreshMetrics = useCallback(async () => {
115
+ if (!isInitialized) return;
116
+
117
+ try {
118
+ setLoading(true);
119
+
120
+ // Get performance metrics
121
+ const collectedMetrics = getPerformanceMetrics();
122
+ setMetrics(collectedMetrics);
123
+
124
+ // Get performance stats
125
+ const stats = getPerformanceStats();
126
+
127
+ // Update dashboard metrics
128
+ setDashboardMetrics({
129
+ eventsProcessed: collectedMetrics.length,
130
+ errorRate: stats.amplitudeCore?.errorRate || 0,
131
+ averageLatency: stats.amplitudeCore?.trackingLatency?.reduce((a: number, b: number) => a + b, 0) /
132
+ (stats.amplitudeCore?.trackingLatency?.length || 1) || 0,
133
+ queueSize: stats.amplitudeCore?.eventQueueSize || 0,
134
+ memoryUsage: stats.amplitudeCore?.memoryUsage || 0,
135
+ cacheHitRate: Math.random() * 100, // Placeholder
136
+ activeUsers: Math.floor(Math.random() * 1000), // Placeholder
137
+ conversionRate: Math.random() * 10 // Placeholder
138
+ });
139
+
140
+ // Check thresholds for alerts
141
+ checkThresholds(stats);
142
+
143
+ } catch (error) {
144
+ console.error('[Analytics Dashboard] Failed to refresh metrics:', error);
145
+ } finally {
146
+ setLoading(false);
147
+ }
148
+ }, [isInitialized, checkThresholds]);
149
+
150
+ useEffect(() => {
151
+ refreshMetrics();
152
+ const interval = setInterval(refreshMetrics, refreshInterval);
153
+ return () => clearInterval(interval);
154
+ }, [refreshMetrics, refreshInterval]);
155
+
156
+ const formatBytes = (bytes: number): string => {
157
+ if (bytes === 0) return '0 Bytes';
158
+ const k = 1024;
159
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
160
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
161
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
162
+ };
163
+
164
+ const formatDuration = (ms: number): string => {
165
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
166
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
167
+ return `${(ms / 60000).toFixed(1)}m`;
168
+ };
169
+
170
+ if (loading && metrics.length === 0) {
171
+ return (
172
+ <div className="amplitude-dashboard amplitude-dashboard--loading">
173
+ <div className="amplitude-dashboard__spinner"></div>
174
+ <p>Loading analytics data...</p>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ if (error) {
180
+ return (
181
+ <div className="amplitude-dashboard amplitude-dashboard--error">
182
+ <h3>Error Loading Dashboard</h3>
183
+ <p>{error.message}</p>
184
+ <button onClick={refreshMetrics} className="amplitude-dashboard__retry-button">
185
+ Retry
186
+ </button>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ if (!isInitialized) {
192
+ return (
193
+ <div className="amplitude-dashboard amplitude-dashboard--not-initialized">
194
+ <h3>Amplitude Plugin Not Initialized</h3>
195
+ <p>Check configuration and ensure the plugin is properly loaded.</p>
196
+ </div>
197
+ );
198
+ }
199
+
200
+ return (
201
+ <div className={`amplitude-dashboard ${compactMode ? 'amplitude-dashboard--compact' : ''}`}>
202
+ <div className="amplitude-dashboard__header">
203
+ <h2 className="amplitude-dashboard__title">Amplitude Analytics Dashboard</h2>
204
+ <div className="amplitude-dashboard__controls">
205
+ <button
206
+ onClick={refreshMetrics}
207
+ className="amplitude-dashboard__refresh-button"
208
+ disabled={loading}
209
+ >
210
+ {loading ? 'Refreshing...' : 'Refresh'}
211
+ </button>
212
+ </div>
213
+ </div>
214
+
215
+ {/* Alerts Section */}
216
+ {alerts.length > 0 && (
217
+ <div className="amplitude-dashboard__alerts">
218
+ <h3>Recent Alerts</h3>
219
+ {alerts.slice(-3).map((alert, index) => (
220
+ <div key={index} className={`amplitude-dashboard__alert amplitude-dashboard__alert--${alert.type}`}>
221
+ <span className="amplitude-dashboard__alert-icon">
222
+ {alert.type === 'critical' ? '🚨' : alert.type === 'error' ? '❌' : '⚠️'}
223
+ </span>
224
+ <span className="amplitude-dashboard__alert-message">{alert.message}</span>
225
+ <span className="amplitude-dashboard__alert-value">
226
+ {alert.value} (threshold: {alert.threshold})
227
+ </span>
228
+ </div>
229
+ ))}
230
+ </div>
231
+ )}
232
+
233
+ {/* Main Metrics Grid */}
234
+ <div className="amplitude-dashboard__metrics-grid">
235
+ <div className="amplitude-dashboard__metric-card">
236
+ <h3>Events Processed</h3>
237
+ <span className="amplitude-dashboard__metric-value">{dashboardMetrics.eventsProcessed.toLocaleString()}</span>
238
+ <span className="amplitude-dashboard__metric-label">Total events</span>
239
+ </div>
240
+
241
+ <div className="amplitude-dashboard__metric-card">
242
+ <h3>Error Rate</h3>
243
+ <span className="amplitude-dashboard__metric-value">
244
+ {(dashboardMetrics.errorRate * 100).toFixed(2)}%
245
+ </span>
246
+ <span className="amplitude-dashboard__metric-label">Error percentage</span>
247
+ </div>
248
+
249
+ <div className="amplitude-dashboard__metric-card">
250
+ <h3>Average Latency</h3>
251
+ <span className="amplitude-dashboard__metric-value">
252
+ {formatDuration(dashboardMetrics.averageLatency)}
253
+ </span>
254
+ <span className="amplitude-dashboard__metric-label">Response time</span>
255
+ </div>
256
+
257
+ <div className="amplitude-dashboard__metric-card">
258
+ <h3>Queue Size</h3>
259
+ <span className="amplitude-dashboard__metric-value">{dashboardMetrics.queueSize}</span>
260
+ <span className="amplitude-dashboard__metric-label">Pending events</span>
261
+ </div>
262
+
263
+ {showAdvancedMetrics && (
264
+ <>
265
+ <div className="amplitude-dashboard__metric-card">
266
+ <h3>Memory Usage</h3>
267
+ <span className="amplitude-dashboard__metric-value">
268
+ {formatBytes(dashboardMetrics.memoryUsage)}
269
+ </span>
270
+ <span className="amplitude-dashboard__metric-label">RAM usage</span>
271
+ </div>
272
+
273
+ <div className="amplitude-dashboard__metric-card">
274
+ <h3>Cache Hit Rate</h3>
275
+ <span className="amplitude-dashboard__metric-value">
276
+ {dashboardMetrics.cacheHitRate.toFixed(1)}%
277
+ </span>
278
+ <span className="amplitude-dashboard__metric-label">Cache efficiency</span>
279
+ </div>
280
+
281
+ <div className="amplitude-dashboard__metric-card">
282
+ <h3>Active Users</h3>
283
+ <span className="amplitude-dashboard__metric-value">
284
+ {dashboardMetrics.activeUsers.toLocaleString()}
285
+ </span>
286
+ <span className="amplitude-dashboard__metric-label">Current session</span>
287
+ </div>
288
+
289
+ <div className="amplitude-dashboard__metric-card">
290
+ <h3>Conversion Rate</h3>
291
+ <span className="amplitude-dashboard__metric-value">
292
+ {dashboardMetrics.conversionRate.toFixed(2)}%
293
+ </span>
294
+ <span className="amplitude-dashboard__metric-label">Goal completion</span>
295
+ </div>
296
+ </>
297
+ )}
298
+ </div>
299
+
300
+ {/* Performance Metrics Table */}
301
+ {showAdvancedMetrics && (
302
+ <div className="amplitude-dashboard__performance-table">
303
+ <h3>Recent Performance Metrics</h3>
304
+ <div className="amplitude-dashboard__table-container">
305
+ <table className="amplitude-dashboard__table">
306
+ <thead>
307
+ <tr>
308
+ <th>Metric</th>
309
+ <th>Value</th>
310
+ <th>Unit</th>
311
+ <th>Timestamp</th>
312
+ </tr>
313
+ </thead>
314
+ <tbody>
315
+ {metrics.slice(-10).map((metric, index) => (
316
+ <tr key={index}>
317
+ <td>{metric.name}</td>
318
+ <td>{metric.value.toFixed(2)}</td>
319
+ <td>{metric.unit}</td>
320
+ <td>{new Date(metric.timestamp).toLocaleTimeString()}</td>
321
+ </tr>
322
+ ))}
323
+ </tbody>
324
+ </table>
325
+ </div>
326
+ </div>
327
+ )}
328
+
329
+ <div className="amplitude-dashboard__footer">
330
+ <p className="amplitude-dashboard__footer-text">
331
+ Last updated: {new Date().toLocaleTimeString()} |
332
+ Refresh interval: {refreshInterval / 1000}s |
333
+ Time range: {timeRange}
334
+ </p>
335
+ </div>
336
+ </div>
337
+ );
338
+ };
339
+
@@ -0,0 +1,265 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { useAmplitudeContext } from '../providers/AmplitudeProvider';
3
+ import { ConsentState, ConsentCategory } from '../types/amplitude.types';
4
+
5
+ interface ConsentManagerProps {
6
+ onConsentChange?: (consent: ConsentState) => void;
7
+ initialConsent?: ConsentState;
8
+ showBadge?: boolean;
9
+ position?: 'bottom-left' | 'bottom-right' | 'center' | 'top';
10
+ theme?: 'light' | 'dark' | 'auto';
11
+ compactMode?: boolean;
12
+ }
13
+
14
+ export const ConsentManager: React.FC<ConsentManagerProps> = ({
15
+ onConsentChange,
16
+ initialConsent,
17
+ showBadge = true,
18
+ position = 'bottom-right',
19
+ theme = 'auto',
20
+ compactMode = false
21
+ }) => {
22
+ const { consent: currentConsent, updateConsent } = useAmplitudeContext();
23
+ const [localConsent, setLocalConsent] = useState<ConsentState>(initialConsent || currentConsent);
24
+ const [showModal, setShowModal] = useState(false);
25
+ const [hasInteracted, setHasInteracted] = useState(false);
26
+
27
+ // Check if user has previously interacted with consent
28
+ useEffect(() => {
29
+ const storedConsent = localStorage.getItem('amplitude_consent');
30
+ const hasStoredConsent = !!storedConsent;
31
+ setHasInteracted(hasStoredConsent);
32
+
33
+ if (storedConsent) {
34
+ try {
35
+ const parsed = JSON.parse(storedConsent);
36
+ setLocalConsent(parsed);
37
+ } catch (error) {
38
+ console.warn('[Consent Manager] Failed to parse stored consent:', error);
39
+ }
40
+ } else if (initialConsent) {
41
+ setLocalConsent(initialConsent);
42
+ } else {
43
+ // Show modal if no previous consent
44
+ setShowModal(true);
45
+ }
46
+ }, [initialConsent]);
47
+
48
+ // Sync consent changes
49
+ useEffect(() => {
50
+ updateConsent(localConsent);
51
+ localStorage.setItem('amplitude_consent', JSON.stringify(localConsent));
52
+ if (onConsentChange) {
53
+ onConsentChange(localConsent);
54
+ }
55
+ }, [localConsent, updateConsent, onConsentChange]);
56
+
57
+ const handleToggle = useCallback((category: ConsentCategory) => {
58
+ setLocalConsent(prev => ({ ...prev, [category]: !prev[category] }));
59
+ setHasInteracted(true);
60
+ }, []);
61
+
62
+ const handleAcceptAll = useCallback(() => {
63
+ setLocalConsent({
64
+ analytics: true,
65
+ sessionReplay: true,
66
+ experiments: true,
67
+ performance: true
68
+ });
69
+ setShowModal(false);
70
+ setHasInteracted(true);
71
+ }, []);
72
+
73
+ const handleRejectAll = useCallback(() => {
74
+ setLocalConsent({
75
+ analytics: false,
76
+ sessionReplay: false,
77
+ experiments: false,
78
+ performance: false
79
+ });
80
+ setShowModal(false);
81
+ setHasInteracted(true);
82
+ }, []);
83
+
84
+ const handleSavePreferences = useCallback(() => {
85
+ setShowModal(false);
86
+ setHasInteracted(true);
87
+ }, []);
88
+
89
+ const getConsentSummary = () => {
90
+ const categories = Object.keys(localConsent) as ConsentCategory[];
91
+ const granted = categories.filter(cat => localConsent[cat]);
92
+ return {
93
+ total: categories.length,
94
+ granted: granted.length,
95
+ percentage: Math.round((granted.length / categories.length) * 100)
96
+ };
97
+ };
98
+
99
+ const consentSummary = getConsentSummary();
100
+
101
+ if (!showModal && showBadge && hasInteracted) {
102
+ return (
103
+ <button
104
+ onClick={() => setShowModal(true)}
105
+ className={`amplitude-consent-badge amplitude-consent-badge--${position} amplitude-consent-badge--${theme}`}
106
+ aria-label="Manage Privacy Preferences"
107
+ >
108
+ <span className="amplitude-consent-badge__icon">🛡️</span>
109
+ <span className="amplitude-consent-badge__text">
110
+ Privacy ({consentSummary.granted}/{consentSummary.total})
111
+ </span>
112
+ </button>
113
+ );
114
+ }
115
+
116
+ if (!showModal) return null;
117
+
118
+ return (
119
+ <div className={`amplitude-consent-modal amplitude-consent-modal--${position} amplitude-consent-modal--${theme}`}>
120
+ <div className="amplitude-consent-modal__backdrop" onClick={() => setShowModal(false)} />
121
+ <div className={`amplitude-consent-modal__content ${compactMode ? 'amplitude-consent-modal__content--compact' : ''}`}>
122
+ <div className="amplitude-consent-modal__header">
123
+ <h2 className="amplitude-consent-modal__title">Privacy Preferences</h2>
124
+ <button
125
+ className="amplitude-consent-modal__close"
126
+ onClick={() => setShowModal(false)}
127
+ aria-label="Close"
128
+ >
129
+ ×
130
+ </button>
131
+ </div>
132
+
133
+ <div className="amplitude-consent-modal__body">
134
+ <p className="amplitude-consent-modal__description">
135
+ We use cookies and other tracking technologies to improve your experience.
136
+ Please review your preferences below.
137
+ </p>
138
+
139
+ <div className="amplitude-consent-modal__categories">
140
+ <div className="amplitude-consent-category">
141
+ <label className="amplitude-consent-category__label">
142
+ <input
143
+ type="checkbox"
144
+ checked={localConsent.analytics}
145
+ onChange={() => handleToggle('analytics')}
146
+ className="amplitude-consent-category__checkbox"
147
+ />
148
+ <span className="amplitude-consent-category__name">Analytics</span>
149
+ </label>
150
+ <p className="amplitude-consent-category__description">
151
+ Collects anonymous usage data to help us improve our service.
152
+ </p>
153
+ </div>
154
+
155
+ <div className="amplitude-consent-category">
156
+ <label className="amplitude-consent-category__label">
157
+ <input
158
+ type="checkbox"
159
+ checked={localConsent.sessionReplay}
160
+ onChange={() => handleToggle('sessionReplay')}
161
+ className="amplitude-consent-category__checkbox"
162
+ />
163
+ <span className="amplitude-consent-category__name">Session Replay</span>
164
+ </label>
165
+ <p className="amplitude-consent-category__description">
166
+ Records user sessions for debugging and user experience improvements.
167
+ </p>
168
+ </div>
169
+
170
+ <div className="amplitude-consent-category">
171
+ <label className="amplitude-consent-category__label">
172
+ <input
173
+ type="checkbox"
174
+ checked={localConsent.experiments}
175
+ onChange={() => handleToggle('experiments')}
176
+ className="amplitude-consent-category__checkbox"
177
+ />
178
+ <span className="amplitude-consent-category__name">A/B Testing</span>
179
+ </label>
180
+ <p className="amplitude-consent-category__description">
181
+ Participate in experiments to help us test new features and improvements.
182
+ </p>
183
+ </div>
184
+
185
+ <div className="amplitude-consent-category">
186
+ <label className="amplitude-consent-category__label">
187
+ <input
188
+ type="checkbox"
189
+ checked={localConsent.performance}
190
+ onChange={() => handleToggle('performance')}
191
+ className="amplitude-consent-category__checkbox"
192
+ />
193
+ <span className="amplitude-consent-category__name">Performance</span>
194
+ </label>
195
+ <p className="amplitude-consent-category__description">
196
+ Collects app performance metrics to monitor and improve system performance.
197
+ </p>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ <div className="amplitude-consent-modal__actions">
203
+ <button
204
+ onClick={handleRejectAll}
205
+ className="amplitude-consent-modal__button amplitude-consent-modal__button--secondary"
206
+ >
207
+ Reject All
208
+ </button>
209
+ <button
210
+ onClick={handleSavePreferences}
211
+ className="amplitude-consent-modal__button amplitude-consent-modal__button--primary"
212
+ >
213
+ Save Preferences
214
+ </button>
215
+ <button
216
+ onClick={handleAcceptAll}
217
+ className="amplitude-consent-modal__button amplitude-consent-modal__button--accent"
218
+ >
219
+ Accept All
220
+ </button>
221
+ </div>
222
+
223
+ <div className="amplitude-consent-modal__footer">
224
+ <p className="amplitude-consent-modal__footer-text">
225
+ You can change these preferences at any time in your privacy settings.
226
+ </p>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ );
231
+ };
232
+
233
+ // Hook for easier consent management
234
+ export const useConsent = () => {
235
+ const { consent, updateConsent } = useAmplitudeContext();
236
+ const [isConsentModalOpen, setIsConsentModalOpen] = useState(false);
237
+
238
+ const openConsentModal = useCallback(() => setIsConsentModalOpen(true), []);
239
+ const closeConsentModal = useCallback(() => setIsConsentModalOpen(false), []);
240
+
241
+ const hasConsent = useCallback((category: ConsentCategory): boolean => {
242
+ return consent[category];
243
+ }, [consent]);
244
+
245
+ const getConsentStatus = useCallback(() => {
246
+ const categories = Object.keys(consent) as ConsentCategory[];
247
+ const granted = categories.filter(cat => consent[cat]);
248
+ return {
249
+ total: categories.length,
250
+ granted: granted.length,
251
+ percentage: Math.round((granted.length / categories.length) * 100)
252
+ };
253
+ }, [consent]);
254
+
255
+ return {
256
+ consent,
257
+ updateConsent,
258
+ isConsentModalOpen,
259
+ openConsentModal,
260
+ closeConsentModal,
261
+ hasConsent,
262
+ getConsentStatus,
263
+ };
264
+ };
265
+