New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@statoss/sdk

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@statoss/sdk

Error tracking for StatOSS: browsers, Node, Bun, Deno, Cloudflare Workers and Vercel Edge. Speaks Sentry's protocol.

Source
npmnpm
Version
0.2.0
Version published
Weekly downloads
17
-93.97%
Maintainers
1
Weekly downloads
 
Created
Source

@statoss/sdk

Error tracking for StatOSS. One package for browsers, Node, Bun, Deno, Cloudflare Workers and Vercel Edge, with TypeScript types included and no dependencies.

The SDK speaks Sentry's protocol (envelopes, event schema, rate limits, client reports), so it can also send to Sentry, and Sentry's own SDKs can send to StatOSS.

Install

pnpm add @statoss/sdk     # or: npm install, yarn add, bun add
deno add npm:@statoss/sdk

Node 20.16 or later on 20, or 22.3 or later. Older Node lacks process.getBuiltinModule, which the SDK uses to reach AsyncLocalStorage without a static import that would break browser and edge bundles; without it, withScope could not keep concurrent requests apart (see Scopes).

Start

Call init once, as early as you can, with your app's DSN. The DSN is on the app's setup page in StatOSS and looks like https://<key>@ingest.statoss.com/<app number>.

import * as StatOSS from "@statoss/sdk";

StatOSS.init({
  dsn: "https://0123456789abcdef0123456789abcdef@ingest.statoss.com/42",
  environment: "production",
  release: "shop@1.4.0",
});

try {
  await chargeCard(order);
} catch (error) {
  StatOSS.captureException(error, { tags: { order: order.id } });
}

StatOSS.captureMessage("Nightly export finished late", "warning");

Without a DSN, with an invalid one, or with enabled: false, the SDK stays off: nothing is sent and nothing is patched. The SDK never throws into your app and never logs, unless you pass debug: true.

Browser

import * as StatOSS from "@statoss/sdk";

StatOSS.init({
  dsn: "https://<key>@ingest.statoss.com/<app number>",
  release: "web@5.0.0",
});

Uncaught errors and unhandled rejections are captured. Breadcrumbs record console calls, fetch requests, history navigation and clicks. Each event carries the page URL, the user agent, and the browser and OS read from it.

The key travels in the URL query and the body as text/plain, so sending needs no CORS preflight. Small envelopes use keepalive, so an error captured just before the page unloads still arrives.

Script tags from other origins only report "Script error." unless they are served with CORS headers and loaded with crossorigin="anonymous".

Node

import * as StatOSS from "@statoss/sdk";

StatOSS.init({
  dsn: process.env.STATOSS_DSN,
  environment: process.env.NODE_ENV,
  release: process.env.RELEASE,
});
  • An uncaught exception is captured at level fatal. If your app has no uncaughtException listener of its own, the SDK prints the error, sets the exit code to 1, sends the event (2 seconds at most) and exits with 1, as Node would have. If it has one, your listener decides.

  • An unhandled rejection is captured and then handled as Node's --unhandled-rejections mode says, which the SDK reads from the command line and NODE_OPTIONS:

    ModeAfter capturing
    throw (the default)Raised as an uncaught exception: your uncaughtException listener gets it, or the process exits 1
    strictThe same; Node raises it itself
    warn-with-error-codePrinted; the process keeps running and later exits with 1
    warn, noneThe process keeps running

    An unhandledRejection listener of your own decides instead. To keep running after unhandled rejections, as Sentry's default does, start Node with --unhandled-rejections=warn.

  • server_name is the host name. Events also carry the Node version, the OS and the CPU architecture.

  • withScope uses AsyncLocalStorage, so what you set inside one stays with that request across awaits.

app.use((req, res, next) => {
  StatOSS.withScope((scope) => {
    scope.setUser({ id: req.user?.id });
    scope.setTag("route", req.route?.path);
    next();
  });
});

Next.js

Server side, in instrumentation.ts at the project root (or in src/). This runs in both the Node.js and the Edge runtime.

import * as StatOSS from "@statoss/sdk";
import type { Instrumentation } from "next";

export function register() {
  StatOSS.init({
    dsn: process.env.STATOSS_DSN,
    environment: process.env.NODE_ENV,
  });
}

export const onRequestError: Instrumentation.onRequestError = async (
  error,
  request,
  context,
) => {
  StatOSS.captureException(error, {
    tags: {
      route: context.routePath,
      routeType: context.routeType,
      method: request.method,
    },
  });
  // The Edge runtime may stop once the response is sent.
  await StatOSS.flush(2000);
};

Browser side, in instrumentation-client.ts:

import * as StatOSS from "@statoss/sdk";

StatOSS.init({ dsn: process.env.NEXT_PUBLIC_STATOSS_DSN });

Errors that an error boundary catches never reach the browser's global handler, so report them from app/error.tsx and app/global-error.tsx:

"use client";

import * as StatOSS from "@statoss/sdk";
import { useEffect } from "react";

export default function Error({
  error,
}: {
  error: Error & { digest?: string };
}) {
  useEffect(() => {
    StatOSS.captureException(error, { tags: { digest: error.digest } });
  }, [error]);
  return <p>Something went wrong.</p>;
}

Bun

import * as StatOSS from "@statoss/sdk";

StatOSS.init({ dsn: Bun.env.STATOSS_DSN });

Bun behaves as Node does above, including uncaught exceptions and unhandled rejections.

Deno

import * as StatOSS from "npm:@statoss/sdk";

StatOSS.init({ dsn: Deno.env.get("STATOSS_DSN") });

Deno.serve((request) => {
  return StatOSS.withScope((scope) => {
    scope.setTag("path", new URL(request.url).pathname);
    return handle(request);
  });
});

Run with --allow-net=ingest.statoss.com (and --allow-env for the DSN). An uncaught error or unhandled rejection is captured at level fatal and sent, then Deno prints it and exits as it would have. A listener of yours that calls event.preventDefault() first keeps the process running. server_name is set only when the sys permission is already granted; the SDK never asks.

Cloudflare Workers

Workers have no global error hook, so capture in your handler and hand the sending to waitUntil, which keeps the Worker alive until it is done.

import * as StatOSS from "@statoss/sdk";

let started = false;

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    if (!started) {
      StatOSS.init({ dsn: env.STATOSS_DSN, release: env.RELEASE });
      started = true;
    }
    try {
      return await handle(request, env);
    } catch (error) {
      StatOSS.captureException(error, {
        tags: { path: new URL(request.url).pathname },
      });
      return new Response("Something went wrong", { status: 500 });
    } finally {
      ctx.waitUntil(StatOSS.flush(2000));
    }
  },
};

init does no I/O, timers or random numbers, so it may also run at the top of the module, where Workers forbid those. Turn on the nodejs_compat compatibility flag for withScope to keep requests apart across awaits (see Scopes).

Vercel Edge

The Edge runtime has no global error hook either: capture where you handle errors and await StatOSS.flush(2000) (or pass it to waitUntil) before returning. In Next.js, the instrumentation.ts above covers it.

Source maps

After the build, give the bundles and their maps debug ids, then upload the maps with Sentry's CLI:

npx @sentry/cli sourcemaps inject ./dist

SENTRY_URL=https://statoss.com \
SENTRY_AUTH_TOKEN=<a StatOSS API key with write access> \
npx @sentry/cli sourcemaps upload --org statoss --project <app slug> ./dist

Adding --release <release> to the upload is optional. Deploy the bundles as inject left them: the SDK reads their debug ids and sends them with each event. Frames are then shown as source and grouped by their source names.

Custom levels and categories

Sentry has five levels: fatal, error, warning, info and debug. StatOSS lets you use your own words as well, and adds a category. Both are sent as tags next to the nearest Sentry level, so Sentry and any Sentry-based tool still read the event normally.

StatOSS.init({
  dsn: "...",
  levels: {
    "payment-failed": "error",
    "slow-query": "warning",
  },
});

StatOSS.captureMessage("Card declined", "payment-failed");
// level "error", tags { "statoss.level": "payment-failed" }

StatOSS.captureException(error, {
  level: "slow-query",
  category: "db.postgres",
});
// level "warning", tags { "statoss.level": "slow-query", "statoss.category": "db.postgres" }
  • level takes a Sentry level or any name of your own. A name that is not in levels is sent as error.
  • category sets the statoss.category tag.
  • Names are 1 to 64 characters of lowercase letters, digits, dots, dashes and underscores. Others are ignored; with debug: true the SDK logs each one.
  • StatOSS filters and groups on these tags like any other.

What is captured

IntegrationRuntimesWhat it does
globalHandlersbrowser, Node, Bun, DenoUncaught errors and unhandled rejections
consoleallA breadcrumb per console call
fetchallA breadcrumb per request: method, URL, status
navigationbrowserA breadcrumb per pushState, replaceState and popstate
clicksbrowserA breadcrumb per click, with a short selector
dedupeallDrops an event identical to the one just before it

All are on by default. Turn one off with integrations: { console: false }.

Breadcrumbs keep the last 100. URLs in breadcrumbs and in the page request lose their credentials, fragment, and the values of query parameters whose names look secret (token, key, session, password and similar). The sender's IP address is recorded only with sendDefaultPii: true (browsers send {{auto}}, and the server reads the address).

Values in breadcrumbs, extra and contexts are copied only so far: 5 levels deep, 1,000 entries per array or object, 8 KiB per string (1 KiB for a console breadcrumb). Buffers, typed arrays, ArrayBuffers, Maps and Sets are written as their type and size, such as [Uint8Array(30000000)], without reading them, so logging a large value costs the app next to nothing.

Scopes

setUser, setTag and the other setters change the current scope, which every later event carries. withScope(callback) gives the callback its own copy, so what it sets stays with the events captured inside it.

Where the runtime has AsyncLocalStorage (Node 20.16+ and 22.3+, Bun, Deno, Vercel Edge, Next.js, Workers with nodejs_compat), the copy follows the callback through every await, and concurrent requests never see each other's user or tags.

Elsewhere (browsers, Workers without nodejs_compat) the copy is current only until the callback first awaits. It is never left current for other work to pick up, so one request's user cannot end up on another's error, but after an await the module-level functions see the outer scope. Capture through the scope the callback gets instead; with debug: true the SDK warns once.

await StatOSS.withScope(async (scope) => {
  scope.setUser({ id: user.id });
  await loadCart();
  scope.captureMessage("Cart was empty", "warning"); // carries the user anywhere
});

API

init(options): void
captureException(error, context?): string            // returns the event id
captureMessage(message, levelOrContext?): string
captureEvent(event, context?): string
setUser(user | null), setTag(key, value), setTags(tags)
setContext(name, context | null), setExtra(key, value)
addBreadcrumb(breadcrumb)
withScope(callback): result                          // callback gets an isolated scope
flush(timeoutMs = 2000): Promise<boolean>            // true once nothing is left to send
close(timeoutMs = 2000): Promise<boolean>            // flush, then stop and unpatch
lastEventId(): string | undefined

// on the scope withScope passes
scope.setUser, setTag, setTags, setExtra, setExtras, setContext, setLevel,
  setFingerprint, addBreadcrumb, clearBreadcrumbs, clear
scope.captureException, captureMessage, captureEvent // with this scope

context is { level, tags, extra, contexts, user, fingerprint, category }. It wins over the scope for that one event.

Options

OptionDefaultMeaning
dsnWhere to send. Without it the SDK stays off.
environment"production"
release, dist
serverNamehost nameNode and Bun only, unless set
sampleRate1Share of events sent, 0 to 1
beforeSend(event, hint) => event | null, may be async
beforeBreadcrumb(breadcrumb, hint) => breadcrumb | null
maxBreadcrumbs100100 at most
sendDefaultPiifalseRecord the sender's IP address
tagsTags on every event
initialScope{ tags, extra, contexts, user, level, fingerprint }
levelsYour level names and the Sentry level each is sent as
integrationsall onSee above
enabledtrue
debugfalseLog what the SDK does
transportfetch(request) => Promise<{ status, headers }>, for tests

Sending to Sentry

Use a Sentry DSN instead. Events, client reports, rate limits (including a used-up quota) and the X-Sentry-Auth header all follow Sentry's protocol. Custom levels arrive as Sentry levels with the statoss.level tag.

Network behaviour

Envelopes go to https://<host>/api/<app number>/envelope/, at most 5 at a time and 30 queued or in flight; more are dropped and counted. When the server answers 429, the SDK holds back the limited categories for as long as Retry-After or X-Sentry-Rate-Limits says. Events it could not send (queue full, rate limited, network error, sampled out, dropped by beforeSend) are counted and reported as Sentry client reports, on flush and otherwise at most every 30 seconds.

Building

From the repository root:

pnpm --dir sdk build    # ESM in sdk/dist/esm, CommonJS in sdk/dist/cjs, .d.ts in both
pnpm test               # runs sdk/test with the rest of the repository's tests

The build uses the repository's TypeScript; the package has no dependencies.

License

MIT

Keywords

statoss

FAQs

Package last updated on 08 Oct 2026

Related posts