StatOSS

Send errors

Any Sentry SDK sends to StatOSS with an app's DSN, as do @statoss/sdk, the Python statoss package and the Rust statoss crate.

The DSN

An app is made under Apps in the dashboard. Each of its keys is a DSN, listed under the app's Keys:

https://<key>@ingest.statoss.com/<app number>

Any Sentry SDK takes it where Sentry's DSN goes, and sends to /api/<app number>/envelope/ or /store/ as it does for Sentry. Browsers may send from any website, unless the key lists the ones it allows. A revoked key's events are refused. The app's Set up page has these snippets with its DSN filled in.

What is kept, and for how long, is on Errors.

Browser JavaScript: @statoss/sdk

npm install @statoss/sdk

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

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

Node.js: @statoss/sdk

npm install @statoss/sdk

const StatOSS = require("@statoss/sdk");

StatOSS.init({
  dsn: "https://<key>@ingest.statoss.com/<app number>",
  environment: process.env.NODE_ENV,
});

Next.js: @statoss/sdk

npm install @statoss/sdk

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

export function register() {
  StatOSS.init({
    dsn: "https://<key>@ingest.statoss.com/<app number>",
    environment: process.env.NODE_ENV,
  });
}

export const onRequestError: Instrumentation.onRequestError = async (error) => {
  StatOSS.captureException(error);
  await StatOSS.flush(2000);
};

// instrumentation-client.ts
import * as StatOSS from "@statoss/sdk";

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

React: @statoss/sdk

npm install @statoss/sdk

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

StatOSS.init({
  dsn: "https://<key>@ingest.statoss.com/<app number>",
  environment: import.meta.env.MODE,
});

Python: statoss

Over sentry-sdk; it takes sentry-sdk's options.

pip install statoss

import statoss

statoss.init(
    dsn="https://<key>@ingest.statoss.com/<app number>",
    environment="production",
)

Python: the Sentry SDK

pip install sentry-sdk

import sentry_sdk

sentry_sdk.init(
    dsn="https://<key>@ingest.statoss.com/<app number>",
    environment="production",
)

Go: the Sentry SDK

go get github.com/getsentry/sentry-go

err := sentry.Init(sentry.ClientOptions{
	Dsn:         "https://<key>@ingest.statoss.com/<app number>",
	Environment: "production",
})

PHP: the Sentry SDK

composer require sentry/sentry

\Sentry\init([
    'dsn' => 'https://<key>@ingest.statoss.com/<app number>',
    'environment' => 'production',
]);

Ruby: the Sentry SDK

gem install sentry-ruby

Sentry.init do |config|
  config.dsn = "https://<key>@ingest.statoss.com/<app number>"
  config.environment = "production"
end

Rust: statoss

Over the sentry crate; it takes the crate's options.

cargo add statoss

let _statoss = statoss::init(
    statoss::Options::new()
        .dsn("https://<key>@ingest.statoss.com/<app number>")
        .release(statoss::release_name!())
        .environment("production"),
);

Protecting a browser DSN

A DSN in a web page is public, as it is with Sentry: anyone who reads the page can send events with it. Three settings limit what they can do. They are under the app's Settings, Keys and Monthly errors, and in the API and MCP as allowedWebsites, ratePerMinute and errorsShare.

  • Allowed websites, per key. Host names such as example.com, or *.example.com for the hosts below it (not example.com itself). A request whose Origin, or Referer when it has no Origin, names another host gets 403 and counts as filtered, origin_not_allowed. A request with neither header, as a server SDK sends it, is taken: the list keeps other websites from using the key, and does not stop a script that leaves the headers out. Empty allows any website.
  • Events a minute, per key: 10 to 1,200. Past it the key gets 429, and the app's other keys are not held back.
  • Share of the team's monthly errors, per app: 1% to 100%, 50% unless set. Past its share the app gets 429 (app_quota) until the month turns, and the team's other apps, a server's for one, keep sending. The team's own quota still holds. A team with one app is not held by it: the share only guards the other apps.

Against a script that leaves the headers out, the rate and the share are what hold: a flood stops at the key's rate, and at the app's share of the month instead of the whole team's.

Keeping the DSN off the page

Sentry's browser SDKs take a tunnel option, a URL on your own site that the SDK posts its envelopes to. @statoss/sdk has no such option; its transport option does the same. Your route sends the envelope on to /api/<app number>/envelope/. Intake takes the key from the envelope's first line, its dsn, when the request carries none, so the route sets the real DSN there and the page holds a placeholder with the same app number. A request from your server names no website, so it passes the allowed websites.

// POST /monitoring on your server: sets the real DSN, sends the envelope on.
export async function POST(request: Request) {
  const body = await request.text();
  const end = body.indexOf("\n");
  const header = JSON.parse(body.slice(0, end === -1 ? undefined : end));
  header.dsn = process.env.STATOSS_DSN; // https://<key>@ingest.statoss.com/<app number>
  const rest = end === -1 ? "" : body.slice(end);
  // 42 is the app number.
  const res = await fetch("https://ingest.statoss.com/api/42/envelope/", {
    method: "POST",
    body: JSON.stringify(header) + rest,
  });
  const headers = new Headers();
  for (const name of ["retry-after", "x-sentry-rate-limits"]) {
    const value = res.headers.get(name);
    if (value) headers.set(name, value);
  }
  return new Response(null, { status: res.status, headers });
}
// In the page: a placeholder key and the app's number.
import * as Sentry from "@sentry/browser";

Sentry.init({
  dsn: "https://[email protected]/42",
  tunnel: "/monitoring",
});

// With @statoss/sdk, a transport that posts to the same route.
import * as StatOSS from "@statoss/sdk";

StatOSS.init({
  dsn: "https://[email protected]/42",
  transport: async (request) => {
    const res = await fetch("/monitoring", {
      method: "POST",
      body: request.body,
      keepalive: request.keepalive,
    });
    return { status: res.status, headers: res.headers };
  },
});

Moving from Sentry

Set the SDK's DSN to the app's; the rest of the code stays as it is. Tags, fingerprints, releases and environments work as they do with Sentry.

sentry-cli uploads source maps, makes and finalizes releases and records deploys against StatOSS with these settings:

export SENTRY_URL=https://statoss.com
export SENTRY_AUTH_TOKEN=<a StatOSS API key that writes>
export SENTRY_ORG=statoss
export SENTRY_PROJECT=<the app's slug>

The key names the team; the organization can be any name. The commands are on Errors, Source maps. Not served: projects list, organizations list, releases set-commits, listing a release's deploys, and releases list without a project. Checked with sentry-cli 2.58 and 3.8.

Answers

StatusMeans
200Read. Answers {"id"} with the event's id, or {}.
400Not an envelope, or a body that does not decompress.
401No key.
403Not a live key, not the app's, or a website the key does not allow.
413Over 8 MiB, before or after decompression.
415A Content-Encoding other than gzip, deflate, br or zstd.
429Over the key's events a minute (1,200 at most), the app's share of the month or the team's month; with Retry-After and X-Sentry-Rate-Limits.
503Busy, or starting.

Errors answer as Sentry's do, so the SDKs back off on a 429 until Retry-After.