An analytics SDK collects the events your app tracks — clicks, page views, purchases — and delivers them to a server both efficiently and reliably: batched, so it sends one request per many events instead of one per event, and resilient, so a failed send is retried rather than silently dropped. You will build analyticsEventSdk, a factory that wraps an injected transport (the function that actually ships a batch) and returns a small { track, flush, stop } API.
Batching has two triggers — a size cap and a periodic timer — and sending has to survive a flaky network, so the interesting part is the coordination: retry a failed batch with backoff, never send the same batch twice, and keep events that arrive mid-send out of the batch already in flight.
function analyticsEventSdk(options: {
transport: (events: Event[]) => Promise<unknown>; // ships one batch; may reject
batchSize?: number; // auto-flush once this many events queue (default 10)
flushInterval?: number; // ms between periodic partial flushes (default 5000)
maxRetries?: number; // retries AFTER the first failed send (default 3)
backoff?: (attempt: number) => number; // ms before retry `attempt`, 1-based
now?: () => number; // clock used to stamp each event's ts (default Date.now)
onError?: (dropped: Event[], error: unknown) => void; // called when a batch is dropped
}): {
track: (name: string, props?: object) => void;
flush: () => Promise<void>; // resolves when the current flush settles
stop: () => void; // stops the periodic flush timer
};
type Event = { name: string; props: object; ts: number };
const sent = [];
const sdk = analyticsEventSdk({
transport: (events) => { sent.push(events); return Promise.resolve(); },
batchSize: 2,
now: () => 1000,
});
sdk.track('page_view', { path: '/home' });
sdk.track('click', { id: 'buy' }); // reaches batchSize → flushes now
// sent is now [[
// { name: 'page_view', props: { path: '/home' }, ts: 1000 },
// { name: 'click', props: { id: 'buy' }, ts: 1000 },
// ]]
// A flaky transport: the first send rejects, the retry succeeds.
let calls = 0;
const sdk = analyticsEventSdk({
transport: () => (++calls === 1 ? Promise.reject(new Error('offline')) : Promise.resolve()),
batchSize: 1,
maxRetries: 3,
backoff: (attempt) => attempt * 100, // wait 100ms before the first retry
});
sdk.track('signup'); // send #1 rejects...
// ...100ms later the batch is retried and delivered, so calls ends at 2.
track queues, it does not send — it appends { name, props, ts } to a buffer, defaulting props to {} and stamping ts from now(). The buffer leaves only on a flush.batchSize (checked on every track) or every flushInterval ms for whatever is buffered, whichever comes first.transport rejects, retry the same batch up to maxRetries times, waiting backoff(attempt) ms before each retry. After the last failure the batch is dropped and passed to onError; it is not re-queued.flush() returns a promise that resolves when the current flush settles. Calling it while a send is in flight must not start a second send of the same events.transport is injected and assumed to settle.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.