@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,319 @@
1
+ import { EventType } from '../types/amplitude.types';
2
+
3
+ export interface PerformanceMetric {
4
+ name: string;
5
+ value: number;
6
+ unit: string;
7
+ timestamp: number;
8
+ properties?: Record<string, any>;
9
+ }
10
+
11
+ export interface PerformanceStats {
12
+ amplitudeCore: {
13
+ initTime: number;
14
+ trackingLatency: number[];
15
+ errorRate: number;
16
+ eventQueueSize: number;
17
+ memoryUsage: number;
18
+ };
19
+ webVitals: {
20
+ cls: number;
21
+ fid: number;
22
+ fcp: number;
23
+ lcp: number;
24
+ ttfb: number;
25
+ inp: number;
26
+ };
27
+ browserMetrics: {
28
+ memoryUsage: number;
29
+ connectionType: string;
30
+ devicePixelRatio: number;
31
+ screenResolution: string;
32
+ viewportSize: string;
33
+ };
34
+ }
35
+
36
+ const performanceMetrics: PerformanceMetric[] = [];
37
+ const performanceObservers: PerformanceObserver[] = [];
38
+ const MAX_METRICS = 10000;
39
+ const CLEANUP_THRESHOLD = 12000;
40
+
41
+ // Internal performance stats
42
+ let amplitudeCoreStats = {
43
+ initTime: 0,
44
+ trackingLatency: [] as number[],
45
+ errorCount: 0,
46
+ successCount: 0,
47
+ eventQueueSize: 0,
48
+ memoryUsage: 0,
49
+ };
50
+
51
+ export function trackPerformanceMetric(name: EventType | string, value: number, unit: string = 'ms', properties?: Record<string, any>): void {
52
+ if (typeof window === 'undefined') return;
53
+
54
+ const metric: PerformanceMetric = {
55
+ name: name as string,
56
+ value,
57
+ unit,
58
+ timestamp: Date.now(),
59
+ properties,
60
+ };
61
+
62
+ performanceMetrics.push(metric);
63
+
64
+ // Update internal stats based on metric type
65
+ updateInternalStats(name as string, value);
66
+
67
+ // Cleanup old metrics if we exceed threshold
68
+ if (performanceMetrics.length > CLEANUP_THRESHOLD) {
69
+ performanceMetrics.splice(0, performanceMetrics.length - MAX_METRICS);
70
+ }
71
+
72
+ // Log to console in debug mode
73
+ if (process.env.NODE_ENV === 'development') {
74
+ console.debug(`[Performance] ${name}: ${value}${unit}`, properties);
75
+ }
76
+ }
77
+
78
+ function updateInternalStats(name: string, value: number): void {
79
+ switch (name) {
80
+ case 'amplitude_init_success':
81
+ amplitudeCoreStats.initTime = value;
82
+ break;
83
+ case 'amplitude_track_latency':
84
+ amplitudeCoreStats.trackingLatency.push(value);
85
+ if (amplitudeCoreStats.trackingLatency.length > 100) {
86
+ amplitudeCoreStats.trackingLatency = amplitudeCoreStats.trackingLatency.slice(-50);
87
+ }
88
+ break;
89
+ case 'amplitude_track_success':
90
+ case 'amplitude_identify_success':
91
+ case 'amplitude_user_properties_success':
92
+ amplitudeCoreStats.successCount += value;
93
+ break;
94
+ case 'amplitude_track_error':
95
+ case 'amplitude_identify_error':
96
+ case 'amplitude_user_properties_error':
97
+ case 'amplitude_init_error':
98
+ amplitudeCoreStats.errorCount += value;
99
+ break;
100
+ case 'amplitude_queue_size':
101
+ amplitudeCoreStats.eventQueueSize = value;
102
+ break;
103
+ case 'amplitude_memory_usage':
104
+ amplitudeCoreStats.memoryUsage = value;
105
+ break;
106
+ }
107
+ }
108
+
109
+ export function getPerformanceMetrics(): PerformanceMetric[] {
110
+ return [...performanceMetrics].sort((a, b) => b.timestamp - a.timestamp);
111
+ }
112
+
113
+ export function getPerformanceStats(): PerformanceStats {
114
+ const stats: PerformanceStats = {
115
+ amplitudeCore: {
116
+ ...amplitudeCoreStats,
117
+ errorRate: amplitudeCoreStats.errorCount / Math.max(amplitudeCoreStats.successCount + amplitudeCoreStats.errorCount, 1),
118
+ },
119
+ webVitals: {
120
+ cls: 0,
121
+ fid: 0,
122
+ fcp: 0,
123
+ lcp: 0,
124
+ ttfb: 0,
125
+ inp: 0,
126
+ },
127
+ browserMetrics: {
128
+ memoryUsage: 0,
129
+ connectionType: 'unknown',
130
+ devicePixelRatio: 1,
131
+ screenResolution: '0x0',
132
+ viewportSize: '0x0',
133
+ },
134
+ };
135
+
136
+ if (typeof window !== 'undefined') {
137
+ // Browser metrics
138
+ stats.browserMetrics = {
139
+ memoryUsage: (performance as any).memory?.usedJSHeapSize || 0,
140
+ connectionType: (navigator as any).connection?.effectiveType || 'unknown',
141
+ devicePixelRatio: window.devicePixelRatio || 1,
142
+ screenResolution: `${window.screen.width}x${window.screen.height}`,
143
+ viewportSize: `${window.innerWidth}x${window.innerHeight}`,
144
+ };
145
+
146
+ // Web Vitals from recent metrics
147
+ const recentMetrics = performanceMetrics.filter(m => Date.now() - m.timestamp < 60000); // Last minute
148
+ stats.webVitals = {
149
+ cls: getLatestMetricValue(recentMetrics, 'CLS') || 0,
150
+ fid: getLatestMetricValue(recentMetrics, 'FID') || 0,
151
+ fcp: getLatestMetricValue(recentMetrics, 'FCP') || 0,
152
+ lcp: getLatestMetricValue(recentMetrics, 'LCP') || 0,
153
+ ttfb: getLatestMetricValue(recentMetrics, 'TTFB') || 0,
154
+ inp: getLatestMetricValue(recentMetrics, 'INP') || 0,
155
+ };
156
+ }
157
+
158
+ return stats;
159
+ }
160
+
161
+ function getLatestMetricValue(metrics: PerformanceMetric[], name: string): number | null {
162
+ const metric = metrics.filter(m => m.name === name).sort((a, b) => b.timestamp - a.timestamp)[0];
163
+ return metric?.value || null;
164
+ }
165
+
166
+ export function clearPerformanceMetrics(): void {
167
+ performanceMetrics.length = 0;
168
+ amplitudeCoreStats = {
169
+ initTime: 0,
170
+ trackingLatency: [],
171
+ errorCount: 0,
172
+ successCount: 0,
173
+ eventQueueSize: 0,
174
+ memoryUsage: 0,
175
+ };
176
+ }
177
+
178
+ export function setupPerformanceObservers(): void {
179
+ if (typeof window === 'undefined' || !window.PerformanceObserver) return;
180
+
181
+ try {
182
+ // Largest Contentful Paint (LCP)
183
+ const lcpObserver = new PerformanceObserver((entryList) => {
184
+ for (const entry of entryList.getEntries()) {
185
+ trackPerformanceMetric('LCP', entry.startTime + entry.duration, 'ms', {
186
+ element: (entry as any).element?.tagName,
187
+ url: window.location.href,
188
+ });
189
+ }
190
+ });
191
+ lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
192
+ performanceObservers.push(lcpObserver);
193
+
194
+ // Cumulative Layout Shift (CLS)
195
+ const clsObserver = new PerformanceObserver((entryList) => {
196
+ let cls = 0;
197
+ for (const entry of entryList.getEntries()) {
198
+ if (!(entry as any).hadRecentInput) {
199
+ cls += (entry as any).value;
200
+ }
201
+ }
202
+ if (cls > 0) {
203
+ trackPerformanceMetric('CLS', cls, 'score', { url: window.location.href });
204
+ }
205
+ });
206
+ clsObserver.observe({ type: 'layout-shift', buffered: true });
207
+ performanceObservers.push(clsObserver);
208
+
209
+ // First Contentful Paint (FCP)
210
+ const fcpObserver = new PerformanceObserver((entryList) => {
211
+ for (const entry of entryList.getEntries()) {
212
+ if (entry.name === 'first-contentful-paint') {
213
+ trackPerformanceMetric('FCP', entry.startTime, 'ms', { url: window.location.href });
214
+ }
215
+ }
216
+ });
217
+ fcpObserver.observe({ type: 'paint', buffered: true });
218
+ performanceObservers.push(fcpObserver);
219
+
220
+ // Navigation Timing
221
+ const navigationObserver = new PerformanceObserver((entryList) => {
222
+ for (const entry of entryList.getEntries()) {
223
+ const navEntry = entry as PerformanceNavigationTiming;
224
+ trackPerformanceMetric('TTFB', navEntry.responseStart - navEntry.requestStart, 'ms');
225
+ trackPerformanceMetric('DOMContentLoaded', navEntry.domContentLoadedEventEnd - navEntry.domContentLoadedEventStart, 'ms');
226
+ trackPerformanceMetric('Load', navEntry.loadEventEnd - navEntry.loadEventStart, 'ms');
227
+ }
228
+ });
229
+ navigationObserver.observe({ type: 'navigation', buffered: true });
230
+ performanceObservers.push(navigationObserver);
231
+
232
+ // Long Tasks
233
+ const longTaskObserver = new PerformanceObserver((entryList) => {
234
+ for (const entry of entryList.getEntries()) {
235
+ trackPerformanceMetric('Long Task', entry.duration, 'ms', {
236
+ name: entry.name,
237
+ startTime: entry.startTime,
238
+ });
239
+ }
240
+ });
241
+ longTaskObserver.observe({ type: 'longtask', buffered: true });
242
+ performanceObservers.push(longTaskObserver);
243
+
244
+ // Resource Timing
245
+ const resourceObserver = new PerformanceObserver((entryList) => {
246
+ for (const entry of entryList.getEntries()) {
247
+ const resourceEntry = entry as PerformanceResourceTiming;
248
+ trackPerformanceMetric('Resource Load', entry.duration, 'ms', {
249
+ name: entry.name,
250
+ initiatorType: resourceEntry.initiatorType,
251
+ transferSize: resourceEntry.transferSize,
252
+ responseStatus: (resourceEntry as any).responseStatus,
253
+ });
254
+ }
255
+ });
256
+ resourceObserver.observe({ type: 'resource', buffered: true });
257
+ performanceObservers.push(resourceObserver);
258
+
259
+ } catch (error) {
260
+ console.warn('[Performance] Failed to setup performance observers:', error);
261
+ }
262
+ }
263
+
264
+ export function disconnectPerformanceObservers(): void {
265
+ performanceObservers.forEach(observer => {
266
+ try {
267
+ observer.disconnect();
268
+ } catch (error) {
269
+ console.warn('[Performance] Failed to disconnect observer:', error);
270
+ }
271
+ });
272
+ performanceObservers.length = 0;
273
+ }
274
+
275
+ // Memory monitoring
276
+ export function startMemoryMonitoring(intervalMs: number = 30000): () => void {
277
+ if (typeof window === 'undefined') return () => {};
278
+
279
+ const interval = setInterval(() => {
280
+ if ((performance as any).memory) {
281
+ const memory = (performance as any).memory;
282
+ trackPerformanceMetric('JS Heap Used', memory.usedJSHeapSize, 'bytes');
283
+ trackPerformanceMetric('JS Heap Total', memory.totalJSHeapSize, 'bytes');
284
+ trackPerformanceMetric('JS Heap Limit', memory.jsHeapSizeLimit, 'bytes');
285
+ }
286
+ }, intervalMs);
287
+
288
+ return () => clearInterval(interval);
289
+ }
290
+
291
+ // Connection monitoring
292
+ export function monitorConnection(): () => void {
293
+ if (typeof window === 'undefined' || !(navigator as any).connection) return () => {};
294
+
295
+ const connection = (navigator as any).connection;
296
+
297
+ const handleConnectionChange = () => {
298
+ trackPerformanceMetric('Connection Type', 1, 'event', {
299
+ effectiveType: connection.effectiveType,
300
+ downlink: connection.downlink,
301
+ rtt: connection.rtt,
302
+ saveData: connection.saveData,
303
+ });
304
+ };
305
+
306
+ connection.addEventListener('change', handleConnectionChange);
307
+
308
+ // Initial reading
309
+ handleConnectionChange();
310
+
311
+ return () => connection.removeEventListener('change', handleConnectionChange);
312
+ }
313
+
314
+ // Export for cleanup
315
+ export function shutdownPerformanceMonitoring(): void {
316
+ disconnectPerformanceObservers();
317
+ clearPerformanceMetrics();
318
+ }
319
+
package/lib/queue.ts ADDED
@@ -0,0 +1,389 @@
1
+ import { EventType, EventProperties } from '../types/amplitude.types';
2
+ import { trackPerformanceMetric } from './performance';
3
+
4
+ interface QueuedEvent {
5
+ id: string;
6
+ eventType: EventType;
7
+ properties?: EventProperties;
8
+ timestamp: number;
9
+ retries: number;
10
+ priority: 'low' | 'normal' | 'high' | 'critical';
11
+ userId?: string;
12
+ }
13
+
14
+ interface QueueOptions {
15
+ flushInterval?: number;
16
+ batchSize?: number;
17
+ maxRetries?: number;
18
+ retryDelayMs?: number;
19
+ maxQueueSize?: number;
20
+ persistenceEnabled?: boolean;
21
+ priorityEnabled?: boolean;
22
+ }
23
+
24
+ export class EventQueue {
25
+ private queue: QueuedEvent[] = [];
26
+ private isProcessing = false;
27
+ private flushInterval: number;
28
+ private batchSize: number;
29
+ private maxRetries: number;
30
+ private retryDelayMs: number;
31
+ private maxQueueSize: number;
32
+ private flushTimer: NodeJS.Timeout | null = null;
33
+ private sendBatchCallback: (events: QueuedEvent[]) => Promise<void>;
34
+ private persistenceEnabled: boolean;
35
+ private priorityEnabled: boolean;
36
+ private storageKey = 'amplitude_event_queue';
37
+
38
+ constructor(
39
+ sendBatchCallback: (events: QueuedEvent[]) => Promise<void>,
40
+ options: QueueOptions = {}
41
+ ) {
42
+ this.sendBatchCallback = sendBatchCallback;
43
+ this.flushInterval = options.flushInterval || 10000; // 10 seconds
44
+ this.batchSize = options.batchSize || 30;
45
+ this.maxRetries = options.maxRetries || 3;
46
+ this.retryDelayMs = options.retryDelayMs || 1000;
47
+ this.maxQueueSize = options.maxQueueSize || 10000;
48
+ this.persistenceEnabled = options.persistenceEnabled || true;
49
+ this.priorityEnabled = options.priorityEnabled || true;
50
+
51
+ this.loadPersistedEvents();
52
+ this.startFlushTimer();
53
+ this.setupOnlineListener();
54
+ }
55
+
56
+ public enqueue(
57
+ eventType: EventType,
58
+ properties?: EventProperties,
59
+ priority: 'low' | 'normal' | 'high' | 'critical' = 'normal',
60
+ userId?: string
61
+ ): string {
62
+ const event: QueuedEvent = {
63
+ id: this.generateEventId(),
64
+ eventType,
65
+ properties,
66
+ timestamp: Date.now(),
67
+ retries: 0,
68
+ priority,
69
+ userId,
70
+ };
71
+
72
+ // Check queue size limit
73
+ if (this.queue.length >= this.maxQueueSize) {
74
+ this.removeOldestLowPriorityEvent();
75
+ }
76
+
77
+ // Insert based on priority if enabled
78
+ if (this.priorityEnabled) {
79
+ this.insertByPriority(event);
80
+ } else {
81
+ this.queue.push(event);
82
+ }
83
+
84
+ // Persist to storage
85
+ if (this.persistenceEnabled) {
86
+ this.persistEvents();
87
+ }
88
+
89
+ // Track queue metrics
90
+ trackPerformanceMetric('amplitude_queue_size', this.queue.length, 'count');
91
+
92
+ // Trigger immediate processing for critical events
93
+ if (priority === 'critical') {
94
+ this.processQueue();
95
+ } else {
96
+ // Normal processing
97
+ this.scheduleProcessing();
98
+ }
99
+
100
+ return event.id;
101
+ }
102
+
103
+ private generateEventId(): string {
104
+ return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
105
+ }
106
+
107
+ private insertByPriority(event: QueuedEvent): void {
108
+ const priorityOrder = { critical: 0, high: 1, normal: 2, low: 3 };
109
+ const eventPriority = priorityOrder[event.priority];
110
+
111
+ let insertIndex = this.queue.length;
112
+ for (let i = 0; i < this.queue.length; i++) {
113
+ const queuePriority = priorityOrder[this.queue[i].priority];
114
+ if (eventPriority < queuePriority) {
115
+ insertIndex = i;
116
+ break;
117
+ }
118
+ }
119
+
120
+ this.queue.splice(insertIndex, 0, event);
121
+ }
122
+
123
+ private removeOldestLowPriorityEvent(): void {
124
+ // Find and remove the oldest low priority event
125
+ for (let i = this.queue.length - 1; i >= 0; i--) {
126
+ if (this.queue[i].priority === 'low') {
127
+ this.queue.splice(i, 1);
128
+ return;
129
+ }
130
+ }
131
+
132
+ // If no low priority events, remove oldest normal priority
133
+ for (let i = this.queue.length - 1; i >= 0; i--) {
134
+ if (this.queue[i].priority === 'normal') {
135
+ this.queue.splice(i, 1);
136
+ return;
137
+ }
138
+ }
139
+
140
+ // Fallback: remove oldest event
141
+ if (this.queue.length > 0) {
142
+ this.queue.shift();
143
+ }
144
+ }
145
+
146
+ private scheduleProcessing(): void {
147
+ // Don't schedule if already processing or if queue is small
148
+ if (this.isProcessing || this.queue.length < this.batchSize) {
149
+ return;
150
+ }
151
+
152
+ // Use setTimeout for immediate processing to avoid blocking
153
+ setTimeout(() => this.processQueue(), 0);
154
+ }
155
+
156
+ private startFlushTimer(): void {
157
+ if (this.flushTimer) {
158
+ clearInterval(this.flushTimer);
159
+ }
160
+ this.flushTimer = setInterval(() => this.processQueue(true), this.flushInterval);
161
+ }
162
+
163
+ private stopFlushTimer(): void {
164
+ if (this.flushTimer) {
165
+ clearInterval(this.flushTimer);
166
+ this.flushTimer = null;
167
+ }
168
+ }
169
+
170
+ private async processQueue(forceFlush = false): Promise<void> {
171
+ if (this.isProcessing) return;
172
+
173
+ // Don't process if offline (will be handled by online listener)
174
+ if (!navigator.onLine && !forceFlush) return;
175
+
176
+ // Don't process small queues unless forced
177
+ if (this.queue.length === 0 || (this.queue.length < this.batchSize && !forceFlush)) {
178
+ return;
179
+ }
180
+
181
+ this.isProcessing = true;
182
+ const startTime = Date.now();
183
+
184
+ try {
185
+ // Take events to process (prioritize critical/high priority)
186
+ const eventsToSend = this.selectEventsForBatch();
187
+
188
+ if (eventsToSend.length === 0) {
189
+ return;
190
+ }
191
+
192
+ // Remove from queue before sending
193
+ this.removeEventsFromQueue(eventsToSend.map(e => e.id));
194
+
195
+ // Send batch
196
+ await this.sendBatchCallback(eventsToSend);
197
+
198
+ // Track successful batch
199
+ trackPerformanceMetric('amplitude_batch_sent', eventsToSend.length, 'count');
200
+ trackPerformanceMetric('amplitude_batch_latency', Date.now() - startTime, 'ms');
201
+
202
+ console.debug(`[Event Queue] Successfully sent ${eventsToSend.length} events`);
203
+
204
+ } catch (error) {
205
+ console.error('[Event Queue] Failed to send event batch:', error);
206
+
207
+ // Re-queue events that failed, with retry logic
208
+ const failedEvents = this.queue.splice(-this.batchSize); // Get last batch that was attempted
209
+ await this.handleFailedEvents(failedEvents);
210
+
211
+ // Track failed batch
212
+ trackPerformanceMetric('amplitude_batch_error', 1, 'count');
213
+
214
+ } finally {
215
+ this.isProcessing = false;
216
+
217
+ // Update persisted events
218
+ if (this.persistenceEnabled) {
219
+ this.persistEvents();
220
+ }
221
+
222
+ // Continue processing if there are more events
223
+ if (this.queue.length > 0) {
224
+ setTimeout(() => this.processQueue(), 100);
225
+ }
226
+ }
227
+ }
228
+
229
+ private selectEventsForBatch(): QueuedEvent[] {
230
+ const batch: QueuedEvent[] = [];
231
+ const criticalAndHigh = this.queue.filter(e => e.priority === 'critical' || e.priority === 'high');
232
+
233
+ // Prioritize critical and high priority events
234
+ if (criticalAndHigh.length > 0) {
235
+ batch.push(...criticalAndHigh.slice(0, this.batchSize));
236
+ }
237
+
238
+ // Fill remaining space with normal/low priority events
239
+ const remaining = this.batchSize - batch.length;
240
+ if (remaining > 0) {
241
+ const normalAndLow = this.queue.filter(e => e.priority === 'normal' || e.priority === 'low');
242
+ batch.push(...normalAndLow.slice(0, remaining));
243
+ }
244
+
245
+ return batch;
246
+ }
247
+
248
+ private removeEventsFromQueue(eventIds: string[]): void {
249
+ this.queue = this.queue.filter(event => !eventIds.includes(event.id));
250
+ }
251
+
252
+ private async handleFailedEvents(failedEvents: QueuedEvent[]): Promise<void> {
253
+ for (const event of failedEvents) {
254
+ event.retries += 1;
255
+
256
+ if (event.retries < this.maxRetries) {
257
+ // Calculate exponential backoff delay
258
+ const delay = this.retryDelayMs * Math.pow(2, event.retries - 1);
259
+
260
+ setTimeout(() => {
261
+ // Re-add to queue with priority adjustment
262
+ const adjustedPriority = event.retries >= 2 ? 'low' : event.priority;
263
+ this.queue.unshift({ ...event, priority: adjustedPriority });
264
+
265
+ if (this.persistenceEnabled) {
266
+ this.persistEvents();
267
+ }
268
+ }, delay);
269
+
270
+ console.debug(`[Event Queue] Retrying event ${event.id} in ${delay}ms (attempt ${event.retries})`);
271
+ } else {
272
+ console.warn(`[Event Queue] Discarding event ${event.id} after ${this.maxRetries} retries`);
273
+ trackPerformanceMetric('amplitude_event_discarded', 1, 'count');
274
+ }
275
+ }
276
+ }
277
+
278
+ private loadPersistedEvents(): void {
279
+ if (!this.persistenceEnabled || typeof localStorage === 'undefined') return;
280
+
281
+ try {
282
+ const stored = localStorage.getItem(this.storageKey);
283
+ if (stored) {
284
+ const events = JSON.parse(stored) as QueuedEvent[];
285
+ this.queue = events.filter(event => {
286
+ // Remove very old events (older than 24 hours)
287
+ return Date.now() - event.timestamp < 24 * 60 * 60 * 1000;
288
+ });
289
+ console.debug(`[Event Queue] Loaded ${this.queue.length} persisted events`);
290
+ }
291
+ } catch (error) {
292
+ console.warn('[Event Queue] Failed to load persisted events:', error);
293
+ // Clear corrupted storage
294
+ localStorage.removeItem(this.storageKey);
295
+ }
296
+ }
297
+
298
+ private persistEvents(): void {
299
+ if (!this.persistenceEnabled || typeof localStorage === 'undefined') return;
300
+
301
+ try {
302
+ // Only persist recent events to avoid storage bloat
303
+ const recentEvents = this.queue.filter(event =>
304
+ Date.now() - event.timestamp < 12 * 60 * 60 * 1000 // 12 hours
305
+ );
306
+ localStorage.setItem(this.storageKey, JSON.stringify(recentEvents));
307
+ } catch (error) {
308
+ console.warn('[Event Queue] Failed to persist events:', error);
309
+ }
310
+ }
311
+
312
+ private setupOnlineListener(): void {
313
+ if (typeof window === 'undefined') return;
314
+
315
+ const handleOnline = () => {
316
+ console.debug('[Event Queue] Connection restored, processing queued events');
317
+ if (this.queue.length > 0) {
318
+ setTimeout(() => this.processQueue(true), 1000); // Give connection a moment to stabilize
319
+ }
320
+ };
321
+
322
+ const handleOffline = () => {
323
+ console.debug('[Event Queue] Connection lost, events will be queued');
324
+ };
325
+
326
+ window.addEventListener('online', handleOnline);
327
+ window.addEventListener('offline', handleOffline);
328
+ }
329
+
330
+ public getQueueSize(): number {
331
+ return this.queue.length;
332
+ }
333
+
334
+ public getQueueStats(): {
335
+ total: number;
336
+ byPriority: Record<string, number>;
337
+ oldestTimestamp: number | null;
338
+ averageAge: number;
339
+ } {
340
+ const byPriority = { critical: 0, high: 0, normal: 0, low: 0 };
341
+ let oldestTimestamp: number | null = null;
342
+ let totalAge = 0;
343
+
344
+ for (const event of this.queue) {
345
+ byPriority[event.priority]++;
346
+ if (!oldestTimestamp || event.timestamp < oldestTimestamp) {
347
+ oldestTimestamp = event.timestamp;
348
+ }
349
+ totalAge += Date.now() - event.timestamp;
350
+ }
351
+
352
+ return {
353
+ total: this.queue.length,
354
+ byPriority,
355
+ oldestTimestamp,
356
+ averageAge: this.queue.length > 0 ? totalAge / this.queue.length : 0,
357
+ };
358
+ }
359
+
360
+ public clear(): void {
361
+ this.queue = [];
362
+ if (this.persistenceEnabled && typeof localStorage !== 'undefined') {
363
+ localStorage.removeItem(this.storageKey);
364
+ }
365
+ trackPerformanceMetric('amplitude_queue_cleared', 1, 'count');
366
+ }
367
+
368
+ public shutdown(): void {
369
+ this.stopFlushTimer();
370
+
371
+ // Attempt to send any remaining critical events
372
+ const criticalEvents = this.queue.filter(e => e.priority === 'critical');
373
+ if (criticalEvents.length > 0) {
374
+ console.debug(`[Event Queue] Flushing ${criticalEvents.length} critical events before shutdown`);
375
+ // Fire and forget - don't wait for completion
376
+ this.sendBatchCallback(criticalEvents).catch(error =>
377
+ console.warn('[Event Queue] Failed to send critical events during shutdown:', error)
378
+ );
379
+ }
380
+
381
+ // Persist remaining events
382
+ if (this.persistenceEnabled) {
383
+ this.persistEvents();
384
+ }
385
+
386
+ console.debug('[Event Queue] Shutdown complete');
387
+ }
388
+ }
389
+