@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,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
|
+
|