/** * Event Queue and Batching Module */ import type { IngestEventPayload, NetworkStatus } from './types'; export declare class EventQueue { private queue; private offlineQueue; private batchTimer; private periodicFlushInterval; private flushPromise; private networkStatus; private config; private recentEventIds; private MAX_RECENT_EVENT_IDS; private OFFLINE_QUEUE_KEY; private flushLock; private offlineQueueLock; private offlineProcessing; private inFlight; private enabled; private consentCheck?; private rateLimitedUntil; private droppedEventCount; private lastDropStatus; constructor(config: any); /** * Add event to queue */ enqueue(event: IngestEventPayload): void; /** * Check if event is duplicate * Fixed Issue #32: Use content-based hash instead of UUID */ private isDuplicateEvent; /** * Create content-based hash for duplicate detection (Issue #32) */ private createEventHash; /** * Check if we should flush the queue */ private shouldFlush; /** * Flush the queue * FIXED (DATA-03): Enhanced protection against concurrent flushes */ flush(): Promise; /** * Internal flush implementation * FIXED (CRITICAL-06): Don't remove events from queue until send succeeds */ private _flush; /** * Send batch of events */ private sendBatch; /** * Setup network status listeners */ private setupNetworkListeners; /** * Start periodic flush timer */ private startPeriodicFlush; /** * Stop periodic flush timer */ private stopPeriodicFlush; /** * Move events to offline queue * Fixed Issue #33: Enforce size limit when adding, not just when saving * FIXED (DATA-03): Added mutex lock to prevent race conditions * FIXED (CRITICAL-06): Can now accept specific events or move entire queue */ private moveToOfflineQueue; /** * Persist a critical event to the offline queue BEFORE its immediate send, so it * survives an unload during the send/backoff window. Deduped on event_id so a * re-enqueue can't double-store it. (FSR-16) */ private persistCriticalEvent; /** * Remove a delivered/dropped event from the offline queue by event_id, and clear the * persisted copy when the queue empties. (FSR-16) */ private removeFromOfflineQueue; /** * Load offline queue from storage */ private loadOfflineQueue; /** * Save offline queue to storage */ private saveOfflineQueue; /** * Process offline queue */ private processOfflineQueue; /** * Get queue size */ getQueueSize(): number; /** * Get offline queue size */ getOfflineQueueSize(): number; /** * WEB-23: queue health, including events the SDK gave up on. * * `droppedEvents` is the number this session discarded as permanently rejected * (a 400/401). It exists because a silent drop is precisely the failure mode * that let the 2026-07-13 `allowed_origins` outage run for eleven days looking * exactly like a healthy queue. A non-zero value here means data was lost. */ getStats(): { queued: number; offline: number; droppedEvents: number; lastDropStatus: number | null; backoffUntil: number; }; /** * Get network status */ getNetworkStatus(): NetworkStatus; /** * Build a batch payload from a set of events. */ private buildBatch; /** * Force flush. Called on visibilitychange:hidden (NON-terminal — an ordinary tab * switch / mobile background) and on pagehide+beforeunload (terminal unload). * * FSR-15: the old single path beaconed BOTH queues on EVERY visibilitychange and then * storage.remove()'d the persisted offline copy on sendBeacon()===true. But sendBeacon * true only means the browser ENQUEUED the request — not that it was delivered. The * offline queue exists precisely because earlier sends failed (ingest down / 5xx / 429); * a single alt-tab during that outage beaconed the whole backlog into the dead endpoint * and ERASED the persisted copy, so nothing replayed when ingest recovered. It also * ignored rateLimitedUntil, firing beacons straight into the Retry-After window. * * Now: * - Non-terminal (tab switch): deliver the LIVE queue via a response-checked keepalive * fetch (flush) and response-check-drain the offline backlog (processOfflineQueue) — * neither erases the backlog on failure. The destructive beacon path is not used. * - Terminal (unload): PERSIST everything first (live + the already-persisted offline * backlog) and NEVER storage.remove() on a mere beacon enqueue. Beacon ONLY the LIVE * queue best-effort — NOT the offline backlog (TR-13): the backlog is already durable and * the next-load drain delivers it exactly once, so beaconing it here too risked a * delivered-now-AND-re-drained-next-load double-send that, when >6h apart, outlives * ingest's dedup window → a duplicate purchase. The next page load's response-checked * drain (normal fetch, no 64KB cap) is the source of truth and clears the copy. * - Both paths honor rateLimitedUntil (don't beacon/flush into the backoff window). * * Still excludes the in-flight _flush batch (its keepalive fetch already carries it, * HIGH-1), and detaches synchronously so a repeat unload event this lifecycle re-enters * with nothing to re-beacon (HIGH-2). */ forceFlush(isTerminal?: boolean): Promise; /** * Clear queue */ clear(): void; /** * Enable/disable all sending. Used by opt-out / withdrawn analytics consent so that * events persisted BEFORE opt-out aren't drained afterwards (the periodic drain and * the on-load drain both honor this). */ setEnabled(enabled: boolean): void; /** * TR-15: register a live consent predicate the offline drain re-evaluates at drain time. * `enabled` is latched at init to a fail-open value (Shopify's customerPrivacy loads * async), and a returning DECLINED visitor fires no visitorConsentCollected event — so * without this, their persisted backlog would drain before the decline is known. */ setConsentCheck(fn: () => boolean): void; /** * Clear the offline queue and its persisted copy (used by opt-out to purge any * PII-bearing events that were parked before the user opted out). */ clearOffline(): void; /** * Debug logging */ private log; /** * Cleanup resources */ destroy(): void; } //# sourceMappingURL=queue.d.ts.map