@datagauge/client

Last updated Aug 12, 2026View as MarkdownAgent setup

DataGauge for client & offline runtimes: Expo/React Native, Electron, browser PWAs.

The problem it solves

Clients go offline, apps get killed mid-session, and retries will happen. So every event gets a durable id at enqueue time, the queue survives restarts via pluggable storage, and replays are sent as arrays with per-event at plus a flush-time sent_at, which lets the server correct the device's clock skew. Ship it with a pk_live_ key: it is public by design and can never write money.

Install

npm install @datagauge/client@^0.2.0
0.2.0 and later

This page documents 0.2.0, which is where the queue started telling you why it paused or dropped: the failure table below and onError arrive with it. On 0.1.x flush() returns the same cappedUntil, pausedUntil and dropped, but with nothing that says what the server actually said. Upgrading is a version bump and nothing else.

Prefer zero dependencies? Every SDK is a single file, so vendor it and point your import at the local copy:

curl -o lib/datagauge.ts \
  https://raw.githubusercontent.com/raymond-UI/datagauge/main/sdks/client/index.ts

Usage

import AsyncStorage from "@react-native-async-storage/async-storage";
import { DataGaugeClient } from "@datagauge/client";
 
const dg = new DataGaugeClient({ key: "pk_live_...", storage: AsyncStorage });
await dg.track({ name: "screen_view", user: deviceId }); // durable immediately
await dg.flush(); // on start, on reconnect, on an interval, all safe

track returns once the event is persisted. flush is safe to call repeatedly; on failure the queue is kept, and per-event ids make the eventual replay idempotent.

API

new DataGaugeClient(opts: ClientOptions)

OptionTypeNotes
keystringRequired. pk_live_, safe to ship in the binary.
storageKeyValueStorageAsyncStorage, localStorage, a file-backed shim. Without it the queue is memory-only.
urlstringIngest base. Defaults to https://in.datagauge.dev.
maxQueuenumberOldest events drop beyond this. Default 1000.
fetchertypeof fetchInjectable for tests.
now() => numberInjectable clock for tests.
randomId() => stringInjectable id generator (defaults to crypto.randomUUID).
onError(f: SendFailure) => voidOptional. Called instead of console.error when events are dropped or sending stops. Not called for a refusal the client will simply ask about again.

KeyValueStorage is { getItem(key), setItem(key, value) }, and each may be sync or async. A corrupt or unavailable store never bricks the app; the in-memory queue is kept.

dg.track(event): Promise<void>

Enqueue durably. event is { name, user?, props? }, and the SDK assigns the id and at itself, so replays stay idempotent.

dg.flush(): Promise<FlushResult>

Send everything queued, in batches of up to 500. Safe to call on app start, on connectivity regained, or on an interval. On failure the queue is kept for the next attempt.

FieldTypeNotes
sentnumberEvents the server accepted this flush.
remainingnumberEvents still queued.
cappedUntilnumberYour organization's hard cap is refusing events; epoch ms at which it lifts. The queue is kept and replays when it does.
pausedUntilnumberSending has stopped because the server refused the key, not the events; epoch ms at which it tries again (5 minutes). The queue is kept: a key rotated on the server, or an invoice paid, must not need an app restart.
droppednumberEvents discarded this flush because the server said they will never be accepted.

What happens when ingest refuses the batch

Not every refusal means the same thing to a queue, and treating them alike is how a client either loses good data or replays a doomed batch forever:

ResponseWhat the queue does
408, 425, 429, 5xx, offlineKeeps everything and asks again on the next flush. Not reported: this is the ordinary weather of a client SDK, and a channel that fires on every bad minute is a channel you filter out.
413Halves the batch and retries, down to a single event. Only an event that is over the 1 MB cap on its own is dropped.
403 with event_cap_reachedKeeps everything, sets cappedUntil, and reports it.
401, 402, other 403, 404Keeps everything, sets pausedUntil, and reports it.
400, 415, 422Drops that batch and reports it.

The drop is the one that cannot be undone, so it is never silent. It happens because the queue evicts its oldest events past maxQueue: a batch the server will refuse identically forever would otherwise outlive the events that could still have been sent, sitting at the head of the queue and blocking everything behind it.

A 202 carrying a non-empty errors[] is also reported. That is the only place you learn an event name or a prop was wrong, and a 202 reads as success.

onError

Route failures somewhere other than console.error:

const dg = new DataGaugeClient({
  key: "pk_live_...",
  storage: AsyncStorage,
  onError: (f) => {
    // f: { status, body, events, message, retriable }
    Sentry.captureMessage(f.message, { level: "warning" });
  },
});

If your reporter itself throws, the failure it was called about is logged to the console instead, and the reporter's own error alongside it.

dg.pending: number

Number of events waiting to be sent.

Key hygiene

This SDK is the one place a public pk_live_ key belongs: it's built to be extracted from an IPA or bundle and still be harmless, because a public key can only write behavioral events. Never put an sk_live_ key in client code; see Keys & scoping.

A pk_test_ key is the same key, aimed at your isolated test environment. Ship it in a test build to keep those events out of your live numbers.