* Flush batch of events to server
()
| 47 | * Flush batch of events to server |
| 48 | */ |
| 49 | function flushBatch(): void { |
| 50 | if (eventBatch.length === 0) return |
| 51 | |
| 52 | const batch = eventBatch.splice(0, eventBatch.length) |
| 53 | if (batchTimer) { |
| 54 | clearTimeout(batchTimer) |
| 55 | batchTimer = null |
| 56 | } |
| 57 | |
| 58 | const sanitizedBatch = batch.map(sanitizeEventData) |
| 59 | |
| 60 | const payload = JSON.stringify({ |
| 61 | category: 'batch', |
| 62 | action: 'client_events', |
| 63 | events: sanitizedBatch, |
| 64 | timestamp: Date.now(), |
| 65 | }) |
| 66 | |
| 67 | const payloadSize = new Blob([payload]).size |
| 68 | const MAX_BEACON_SIZE = 64 * 1024 // 64KB |
| 69 | |
| 70 | if (navigator.sendBeacon && payloadSize < MAX_BEACON_SIZE) { |
| 71 | const sent = navigator.sendBeacon('/api/telemetry', payload) |
| 72 | |
| 73 | if (!sent) { |
| 74 | // boundary-raw-fetch: pre-bootstrap instrumentation |
| 75 | fetch('/api/telemetry', { |
| 76 | method: 'POST', |
| 77 | headers: { 'Content-Type': 'application/json' }, |
| 78 | body: payload, |
| 79 | keepalive: true, |
| 80 | }).catch(() => { |
| 81 | // Silently fail |
| 82 | }) |
| 83 | } |
| 84 | } else { |
| 85 | // boundary-raw-fetch: pre-bootstrap instrumentation |
| 86 | fetch('/api/telemetry', { |
| 87 | method: 'POST', |
| 88 | headers: { 'Content-Type': 'application/json' }, |
| 89 | body: payload, |
| 90 | keepalive: true, |
| 91 | }).catch(() => { |
| 92 | // Silently fail |
| 93 | }) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | window.addEventListener('beforeunload', flushBatch) |
| 98 | window.addEventListener('visibilitychange', () => { |
no outgoing calls
no test coverage detected