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
npm install @datagauge/edge@^0.2.0This 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:
curl -o lib/datagauge.ts \
https://raw.githubusercontent.com/raymond-UI/datagauge/main/sdks/edge/index.tsUsage
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 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 or
@datagauge/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:
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 401s 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. The
sk_test_ / pk_test_ prefixes do the same in your isolated test
environment.