export interface AnalyticsEvent {
name: string;
props?: Record<string, unknown>;
ts: number;
}
export interface Transport {
send(batch: AnalyticsEvent[]): Promise<boolean>;
}
interface QueueOptions {
batchSize: number;
maxBufferSize: number;
transport: Transport;
}
export class EventQueue {
private buffer: AnalyticsEvent[] = [];
private flushing = false;
dropped = 0;
constructor(private readonly opts: QueueOptions) {}
enqueue(event: AnalyticsEvent): void {
this.buffer.push(event);
if (this.buffer.length > this.opts.maxBufferSize) {
const overflow = this.buffer.length - this.opts.maxBufferSize;
this.buffer.splice(0, overflow);
this.dropped += overflow;
}
if (this.buffer.length >= this.opts.batchSize) {
void this.flush();
}
}
async flush(): Promise<void> {
if (this.flushing || this.buffer.length === 0) return;
this.flushing = true;
try {
while (this.buffer.length > 0) {
const batch = this.buffer.splice(0, this.opts.batchSize);
const ok = await this.opts.transport.send(batch);
if (!ok) {
this.buffer.unshift(...batch);
break;
}
}
} finally {
this.flushing = false;
}
}
}
import type { AnalyticsEvent, Transport } from './EventQueue';
export class BeaconTransport implements Transport {
constructor(private readonly endpoint: string) {}
async send(batch: AnalyticsEvent[]): Promise<boolean> {
const payload = JSON.stringify({ events: batch });
if (typeof navigator !== 'undefined' && document.visibilityState === 'hidden') {
const blob = new Blob([payload], { type: 'application/json' });
return navigator.sendBeacon(this.endpoint, blob);
}
try {
const res = await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
keepalive: true,
});
return res.ok;
} catch {
return false;
}
}
}
import { EventQueue, type AnalyticsEvent } from './EventQueue';
import { BeaconTransport } from './BeaconTransport';
interface AnalyticsConfig {
endpoint: string;
flushIntervalMs?: number;
batchSize?: number;
maxBufferSize?: number;
}
export class Analytics {
private readonly queue: EventQueue;
private timer: ReturnType<typeof setInterval> | null = null;
private readonly onHidden = () => {
if (document.visibilityState === 'hidden') void this.queue.flush();
};
constructor(private readonly config: AnalyticsConfig) {
this.queue = new EventQueue({
batchSize: config.batchSize ?? 20,
maxBufferSize: config.maxBufferSize ?? 500,
transport: new BeaconTransport(config.endpoint),
});
}
start(): void {
if (this.timer) return;
const interval = this.config.flushIntervalMs ?? 5000;
this.timer = setInterval(() => void this.queue.flush(), interval);
document.addEventListener('visibilitychange', this.onHidden);
}
track(name: string, props?: Record<string, unknown>): void {
const event: AnalyticsEvent = { name, props, ts: Date.now() };
this.queue.enqueue(event);
}
async stop(): Promise<void> {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
document.removeEventListener('visibilitychange', this.onHidden);
await this.queue.flush();
}
}
This snippet shows a small client-side analytics pipeline that buffers events in memory and flushes them to a collector endpoint in batches, either when a size threshold is reached or when a periodic timer fires. Batching matters because sending one HTTP request per track call is wasteful and slow; coalescing many events into a single request reduces overhead, and an interval flush guarantees events still leave the buffer even during quiet periods.
In EventQueue, the core is a bounded FIFO array buffer guarded by maxBufferSize. When enqueue is called the event is pushed; if the buffer overflows the oldest entries are dropped and counted in dropped so telemetry loss is observable rather than silent. This is the backpressure trade-off: under a burst the queue favors bounded memory over completeness. When the buffer reaches batchSize, enqueue triggers flush immediately instead of waiting for the timer, keeping latency low for active sessions. The flush method splices out a batch, calls the injected transport, and on failure re-queues the events at the front so a transient network error does not lose data. A flushing guard prevents overlapping flushes from racing on the same buffer.
BeaconTransport implements the actual send. It prefers navigator.sendBinary-style fetch with keepalive so requests survive brief navigations, and falls back to navigator.sendBeacon during page unload, which the browser delivers reliably even as the tab closes. The send method returns a boolean the queue uses to decide whether to re-queue.
Analytics wires everything together: it constructs the queue with a transport, starts a setInterval timer in start, and registers a visibilitychange listener so a final flush runs when the page is hidden — the single most common moment to lose buffered events. track is the public API that stamps each event with a timestamp and forwards it to the queue. stop clears the timer and performs one last drain, making the lifecycle explicit. Together the files demonstrate durable batching: bounded buffers, immediate flush on threshold, periodic flush on idle, retry on failure, and a guaranteed flush on unload.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
Share this code
Here's the card — post it anywhere.