# @datagauge/node

DataGauge for **short-lived processes**: CLIs, cron jobs, one-shot scripts.

## The problem it solves

A CLI exits before any timer-based batcher fires, so a background queue
would silently lose its buffer at exit. This SDK buffers events
synchronously and makes `flush()` the one obligation before the process
ends: no timers, no daemon threads, nothing to lose. A metrics failure
never fails the CLI.

## Install

```sh
npm install @datagauge/node@^0.2.0
```

This page documents **0.2.0**, which is where `flush()` started returning
`reason`: the failure table below and `onError` arrive with it. On `0.1.0` a
refused batch returns `{ ok: false }` with nothing attached to say why.
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:

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

## Usage

```ts
import { datagauge } from "@datagauge/node";
const dg = datagauge({ key: process.env.DATAGAUGE_KEY! });
dg.track({ name: "cli_run", user: machineId });
await dg.flush(); // the one obligation, before exit
```

In Rust, Go, or anything else without this wrapper: send synchronously
before exit; it's one HTTP call.

## API

### `datagauge(opts: NodeOptions): DataGaugeNode`

| Option | Type | Notes |
|---|---|---|
| `key` | `string` | Required. Use an `sk_live_` server key. |
| `url` | `string` | Ingest base. Defaults to `https://in.datagauge.dev`. |
| `fetcher` | `typeof fetch` | Injectable for tests. |
| `onError` | `(f: SendFailure) => void` | Optional. Called for every failure, in addition to `reason` on the result. Nothing is printed by default: a CLI's stderr is its own product. |

### `dg.track(event): void`

Buffer an event synchronously. `event` is `{ name, user?, id?, at?, props? }`;
see [POST /v1/events](/docs/api/events) for the field reference.

### `dg.flush(): Promise<FlushResult>`

Send the whole buffer, in requests of up to 500 events each. `await` this
before the process exits.

| Field | Type | Notes |
|---|---|---|
| `sent` | `number` | Events the server accepted this flush. |
| `ok` | `boolean` | True only when the buffer is now empty. A flush that sends 500 of 700 and is then refused returns `{ sent: 500, ok: false }`. |
| `capped` | `{ resetsAt: number \| null }` | Your organization's [hard cap](/docs/api/events) refused the batch. `resetsAt` is epoch ms. |
| `suspended` | `true` | Ingest is paused because the account has no live subscription: either an unpaid invoice's grace period expired, or the subscription ended. Nothing stored has been deleted. |
| `reason` | `SendFailure` | Why the flush stopped. Present whenever `ok` is false, including for `capped` and `suspended`, so there is never a failure with no explanation attached. |

A failed or errored send returns `{ ok: false }` without throwing, so a metrics
problem can't crash the CLI, and the events stay buffered. `capped` and
`suspended` are the two refusals a script should branch on; `reason` is the one
a human reads.

### Why a flush failed

`fetch()` rejects only on transport failure, so an SDK that ignores the response
cannot tell a `401` from success. `reason` is `{ status, body, events, message,
retriable }`, where `status` is `0` for a transport failure and `retriable` says
whether sending the identical bytes again could work:

```ts
const res = await dg.flush();
if (!res.ok) {
  console.error(res.reason?.message);
  // → ingest returned 403 for 12 event(s): the key was refused for this
  //   write: either the wrong plane (an sk_test_ key cannot write live data,
  //   or the reverse), a missing scope, or the organization's own hard event
  //   cap. Response: {"ok":false,"error":"…"}
  if (res.reason?.retriable) process.exitCode = 75; // EX_TEMPFAIL
}
```

`retriable` is true for `429`, `5xx` and transport failures, and the events are
still in the buffer, so a wrapper script can flush again or exit in a way its
scheduler will retry. It is false for every other refusal: the same bytes get
the same answer, and the message names the fix instead.

A `202` never sets `reason`, because the batch was accepted. If it carried per-event
rejections in `errors[]`, those go to `onError`, because `ok: false` would be a
lie about the buffer.

### `dg.pending: number`

Number of buffered events not yet sent.

### `installExitFlush(dg): void`

Registers a best-effort flush on `process.on("beforeExit")`. A safety net,
**not** a substitute for an explicit `await dg.flush()`; the awaited flush
remains the contract.

## Key hygiene

Short-lived processes run server-side (or on a developer's machine), so use
an `sk_live_` key here: it can write payments and read metrics. An
`sk_test_` key behaves identically against your isolated test environment;
swap the prefix to rehearse a job before pointing it at live.

Don't bundle this SDK with an `sk_live_` key inside a distributed CLI: a
secret key can write revenue. For anything users can extract the key from,
use a publishable `pk_live_` key. See [Keys & scoping](/docs/concepts/keys).
