# @datagauge/edge

DataGauge for serverless & edge runtimes: **Cloudflare Workers, Vercel
Edge/Functions, Deno Deploy, Lambda.**

## The problem it solves

In these runtimes the process can be frozen the moment the response
returns, so a naive fire-and-forget `fetch` is silently dropped. Every
send must ride the platform's lifetime-extension hook:
`ctx.waitUntil(...)` on Workers, `waitUntil` from `@vercel/functions`,
`event.waitUntil` in service-worker-style runtimes. This wrapper does
exactly that and nothing more; a failed send never throws, so analytics
cannot break the caller's request path, but from 0.2.0 it is never silent
either.

## Install

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

This page documents **0.2.0**, which is where the wrapper started reading
ingest's response: the failure table below and `onError` arrive with it. On
`0.1.0` the response is discarded and every refusal is silent. 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/edge/index.ts
```

## Usage

```ts
import { datagauge } from "@datagauge/edge";
const dg = datagauge({ key: env.DATAGAUGE_KEY });

export default {
  async fetch(req, env, ctx) {
    dg.track({ name: "api_hit", user: userId }, ctx.waitUntil.bind(ctx));
    return new Response("ok"); // the send survives this return
  },
};
```

Pass the runtime's `waitUntil` and `track` returns immediately. Without
one, the returned promise must be awaited before your handler resolves.

## API

### `datagauge(opts: EdgeOptions): DataGaugeEdge`

Creates a tracker.

| Option | Type | Notes |
|---|---|---|
| `key` | `string` | Required. `sk_live_` on servers; `pk_live_` only for behavioral events. |
| `url` | `string` | Optional ingest base. Defaults to `https://in.datagauge.dev`. |
| `fetcher` | `typeof fetch` | Optional. Injected in tests; defaults to the global `fetch`. |
| `onError` | `(f: SendFailure) => void` | Optional. Called instead of `console.error` when a send fails. |

### `dg.track(event, waitUntil?)`

`track(event: DataGaugeEvent | DataGaugeEvent[], waitUntil?: (p: Promise<unknown>) => void): Promise<void>`

Queue an event (or array of events) to send. Pass the runtime's
`waitUntil` so the send outlives the response; otherwise `await` the
returned promise.

A `DataGaugeEvent` is `{ name, user?, id?, at?, props? }`, where `name` is
the only required field. See [POST /v1/events](/docs/api/events) for the full
field reference.

## What happens when ingest refuses the batch

`fetch()` rejects only on transport failure, so a wrapper that ignores the
response cannot tell a `401` from success. This one reads it:

| Response | What the wrapper does |
|---|---|
| `202` | Nothing, unless the body carries a non-empty `errors[]`, in which case the per-event rejections are reported. A `202` with rejections inside it is the only place you ever learn an event name or a prop was wrong. |
| Any 4xx | Reports the failure with the likely cause named, and returns. |
| `429`, `5xx` | Reports it as retriable. The batch was not stored. |
| Transport failure | Reports it with `status: 0` and the underlying error in the message. |

**Nothing here throws.** This wrapper runs inside your request: the promise
usually goes to `waitUntil`, where a rejection is an unhandled error in your
Worker, and when it doesn't it is awaited on the request path itself. So the
events in a refused batch are still gone, and a runtime that can freeze the
moment the response returns has nowhere to hold them for a retry. What changed
in 0.2.0 is that you are told. If these events must survive an outage, send them
through [`@datagauge/node`](/docs/sdks/node) or
[`@datagauge/client`](/docs/sdks/client), which keep a buffer.

### `onError`

Route failures somewhere other than `console.error`: your own reporter, an
Analytics Engine dataset, a log line you already parse:

```ts
const dg = datagauge({
  key: env.DATAGAUGE_KEY,
  onError: (f) => {
    // f: { status, body, events, message, retriable }
    env.ERRORS.writeDataPoint({ blobs: [f.message], doubles: [f.status, f.events] });
  },
});
```

The default lands in `wrangler tail` and in Logpush, which is where someone
debugging "my events stopped" is already looking. 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.

### A Worker with no key

`env` is only readable inside the handler, so a module-scope
`datagauge({ key: process.env.DATAGAUGE_KEY })` is constructed with `undefined`
and every event goes out as `Bearer undefined`. That is reported before anything
is sent, rather than becoming a wall of `401`s you have to go looking for.

## Key hygiene

Use an `sk_live_` server key here for anything involving money. A
`pk_live_` key works for behavioral events but is rejected for `payment`
events, and that's by design; see [Keys & scoping](/docs/concepts/keys). The
`sk_test_` / `pk_test_` prefixes do the same in your isolated test
environment.
