SDKs & Code Examples
@datius-io/node is a zero-dependency Node.js/TypeScript SDK for emitting signals without calling the Ingestion API directly. It buffers events in memory and dispatches them as batched HTTP requests, with automatic retries, backoff, and a graceful shutdown drain — none of it ever blocks or throws in the host application.
Package: @datius-io/node on npm, MIT licence.
Install
npm install @datius-io/node
Direct usage
import { Datius } from "@datius-io/node";const datius = new Datius({apiKey: process.env.DATIUS_API_KEY!,});datius.emit("order_completed", {value: 149.99,tags: { currency: "USD", customerTier: "gold" },});
Decorator usage (NestJS, Express controllers, service classes)
Requires experimentalDecorators: true in tsconfig.json. Emits a signal automatically after the decorated method resolves, derived from its arguments and return value. If the method throws, nothing is sent, so only completed work is counted.
import { DatiusEmit } from "@datius-io/node";export class CheckoutService {constructor(private readonly payments: PaymentsClient,private readonly orders: OrdersRepository,) {}// Once the order is paid, send what was charged: the arguments give the// store, and the method's result gives the amount and payment method.@DatiusEmit("order_revenue", ([request]: [CheckoutRequest], paid: PaidOrder) => ({value: paid.amountCharged,tags: { store: request.storeId, paymentMethod: paid.paymentMethod },}))async processCheckout(request: CheckoutRequest): Promise<PaidOrder> {const payment = await this.payments.charge(request.order);return this.orders.markPaid(request.order.id, payment);}}
Route handler usage (Next.js App Router, Fastify, Lambda)
import { NextResponse } from "next/server";import { withDatius } from "@datius-io/node";// Counts every checkout attempt, tagged by store.export const POST = withDatius("checkout_attempts",async (req: Request) => {const body = await req.json();return { value: 1, tags: { store: body.storeId } };},async (req: Request) => {const body = await req.json();const order = await placeOrder(body);return NextResponse.json({ orderId: order.id });},);
The extract function receives a clone of the request, so it and the handler can each independently read the body. The signal is sent as each request arrives, before the handler runs and whether or not it succeeds, so this suits counting what comes in. To record only what succeeded, such as paid orders, use the decorator or call emit from the handler.
Signal shape
datius.emit(signalKey: string, {value: number,tags?: Record<string, string | number | boolean>,timestamp?: string,});
This mirrors the Ingestion API's payload exactly — see Ingestion API for the full field reference. Use tags for the small sets you group and filter by, such as a store or a channel, not a value unique to each event like an order ID.
Resolving the default client
@DatiusEmit and withDatius both fall back to the first Datius instance constructed in the process, unless a client is passed explicitly via { client }. Call Datius.setDefault(instance) to override, or construct multiple clients and pass { client } per call site for multi-tenant or multi-endpoint setups.
How sending works
emit never sends right away. It adds the event to a queue in memory, inside the process that's running the SDK, and returns at once, so it never slows your code down. The SDK sends what's queued to Datius in one request as soon as either of these happens:
flushIntervalMspasses (2 seconds by default), so an event waits at most about 2 seconds;maxBatchSizeevents pile up (50 by default), so busy periods send without waiting for the timer.
Each process has its own queue: run your app on four servers and there are four queues. When a process shuts down normally (SIGINT or SIGTERM), the SDK sends what's left before it exits; if the process crashes, events from its last couple of seconds can be lost. The queue holds up to 5,000 events, and past that the oldest are dropped, so memory stays bounded while Datius can't be reached.
Serverless functions (AWS Lambda, Vercel functions) can be frozen as soon as they return, before the timer fires. Call await datius.flush() before the function returns so its events are sent.
Configuration
Only apiKey is needed: a workspace-scoped key with the signals:emit scope.
Everything else is optional tuning for how your process batches and retries, with defaults that suit most services. Datius itself slows the SDK down when it needs to (see Retries and backpressure), so you rarely need to change these.
| Option | Default | Description |
|---|---|---|
flushIntervalMs | 2000 | How often queued events are sent, in milliseconds. |
maxBatchSize | 50 | Send at once when this many events are queued. |
maxRetries | 8 | Retry attempts per failed batch. |
maxRetryWindowMs | 60000 | How long one batch keeps being retried before it's dropped. |
retryBaseDelayMs | 500 | The first retry's wait when the server doesn't say; it doubles each time, up to 10 seconds, with jitter. |
requestTimeoutMs | 10000 | One request's timeout. |
onDrop | — | Called with { count, status, reason } whenever a batch is dropped, e.g. to count lost events. |
debug | false | Log lifecycle events to the console. |
handleProcessSignals | true | Flush and detach on SIGINT/SIGTERM. |
Retries and backpressure
A batch that fails in a way that may pass (no answer, a timeout, 408, 425, 429 or a 5xx) is retried within maxRetryWindowMs. When Datius answers 429 or 503 with a Retry-After header, as it does when its ingest queue is under pressure, the SDK waits exactly that long, so it slows down instead of losing events. Any other 4xx (an invalid batch, a wrong or expired key) isn't retried.
A dropped batch is never silent: the SDK warns on the console once per kind of failure, logs every drop with debug: true, and reports each one to onDrop. The buffer caps out at 5,000 queued events; beyond that, the oldest are dropped to protect memory during a long outage. shutdown() waits for batches already being sent.
Other languages
Only the ingestion payload shape matters to talk to Datius — any HTTP client can call POST /api/v1/ingest directly. Additional language SDKs may follow the Node.js SDK in the future. Already instrumenting with OpenTelemetry instead? See OpenTelemetry — no Datius SDK needed at all, any language's standard OTel exporter works.