All questions

Analytics Event SDK

Premium

Analytics Event SDK

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.

Signature

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 };

Examples

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.

Notes

  • 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.
  • Two flush triggers — a flush happens when the queue reaches batchSize (checked on every track) or every flushInterval ms for whatever is buffered, whichever comes first.
  • Retry with backoff, then drop — if 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.
  • One send at a timeflush() 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.
  • Mid-send events belong to the next batch — anything tracked while a batch is being sent stays buffered and ships on the following flush, never joining the in-flight batch.
  • Don't worry about the real network, event schemas, persistence across reloads, or aborting a hung request — transport is injected and assumed to settle.

FAQ

How is this different from a plain batch event flusher?
A batch flusher just buffers events and hands them to a synchronous callback. An analytics SDK sends over an unreliable network, so it adds the async parts: the transport returns a promise, a failed send is retried with backoff, and a guard stops the same batch from being sent twice while a send is in flight.
What happens to a batch after every retry fails?
It is dropped, not re-queued, and passed to the onError callback so the caller can log or persist it. Dropping keeps a permanently failing batch from blocking every batch behind it forever; surfacing it via onError means the loss is observable rather than silent.
Why do events tracked during a flush go to the next batch?
Because the in-flight batch was already handed to the transport, so adding to it would either be ignored or risk sending those events twice on a retry. The flush swaps the queue out before awaiting the send, so any track that lands mid-send refills a fresh queue that ships on the following flush.

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.

Upgrade to Premium