
Threat Research
/Security News
Searchjacked: Hidden Browser Extensions Hijack Searches for 13 Million Users
319 browser extensions installed by 13.4 million users hijack searches, capture queries, and collect referral payments from Yahoo and Bing.
@statoss/sdk
Advanced tools
Error tracking for StatOSS: browsers, Node, Bun, Deno, Cloudflare Workers and Vercel Edge. Speaks Sentry's protocol.
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.
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).
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.
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".
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:
| Mode | After capturing |
|---|---|
throw (the default) | Raised as an uncaught exception: your uncaughtException listener gets it, or the process exits 1 |
strict | The same; Node raises it itself |
warn-with-error-code | Printed; the process keeps running and later exits with 1 |
warn, none | The 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();
});
});
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>;
}
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.
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.
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).
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.
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.
Sentry has five levels: fatal, error, warning, info and debug.
StatOSS adds levels of your own, categories and fields. They travel as tags
next to the nearest Sentry level, so Sentry and any Sentry-based tool still
read the event normally.
Declare them with defineSchema and give the schema to init. The capture
functions init returns take only the schema's names, so a wrong one fails
to compile.
import * as StatOSS from "@statoss/sdk";
export const schema = StatOSS.defineSchema({
levels: {
"payment-failed": { like: "error", label: "Payment failed" },
"slow-query": "warning",
},
categories: {
billing: { label: "Billing", color: "plum" },
auth: {},
},
fields: {
route: { label: "Route", filterable: true },
duration_ms: { type: "number", unit: "ms", label: "Duration" },
},
});
const statoss = StatOSS.init({ dsn: process.env.STATOSS_DSN, schema });
statoss.captureException(error, {
level: "payment-failed",
category: "billing",
fields: { route: "/pay" },
});
// level "error", tags { "statoss.level": "payment-failed",
// "statoss.category": "billing", route: "/pay" }
statoss.captureMessage("Search took 2.4 s", {
level: "slow-query",
fields: { duration_ms: 2400 },
});
// @ts-expect-error: not a level of this schema
statoss.captureException(error, { level: "payment-faild" });
fatal, error, warning, info, debug or
trace: give that alone, or like with an optional severity (1 to 24,
inside its range), label and color. It is sent as the nearest Sentry
level (trace as debug; error when none is given).label, color, owner (a team member's
email) and hidden.label, type (text or
number), unit and filterable. Field values are strings, numbers or
booleans.rules set a level or a category for events that match, such as
{ subject: "logger", value: "payments", category: "billing" }. They name
the schema's own levels and categories.crimson, red, orange, amber, yellow, green, aqua,
teal, blue, violet, indigo, magenta, plum and grey.statoss..defineSchema refuses a wrong name, range, severity, colour or setting at
compile time (TypeScript 5.0 or later). It returns the schema as given:
plain data in the shape StatOSS's schema API takes. A function of yours
that hands a schema to init types it as
<const S extends SchemaSpec & CheckSchema<S>>(schema: S).
The module-level functions take any level or category name, and levels in
init maps your names to Sentry levels:
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" }
levels nor the schema is sent as error. Given both,
levels wins.debug: true it logs each
one.| Integration | Runtimes | What it does |
|---|---|---|
globalHandlers | browser, Node, Bun, Deno | Uncaught errors and unhandled rejections |
console | all | A breadcrumb per console call |
fetch | all | A breadcrumb per request: method, URL, status |
navigation | browser | A breadcrumb per pushState, replaceState and popstate |
clicks | browser | A breadcrumb per click, with a short selector |
dedupe | all | Drops 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.
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
});
init(options): handle // handle.captureException, captureMessage, captureEvent
defineSchema(schema): schema // the schema as given, its names as types
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 },
and on the handle init returns also fields. It wins over the scope for
that one event.
| Option | Default | Meaning |
|---|---|---|
dsn | Where to send. Without it the SDK stays off. | |
environment | "production" | |
release, dist | ||
serverName | host name | Node and Bun only, unless set |
sampleRate | 1 | Share of events sent, 0 to 1 |
beforeSend | (event, hint) => event | null, may be async | |
beforeBreadcrumb | (breadcrumb, hint) => breadcrumb | null | |
maxBreadcrumbs | 100 | 100 at most |
sendDefaultPii | false | Record the sender's IP address |
tags | Tags on every event | |
initialScope | { tags, extra, contexts, user, level, fingerprint } | |
schema | From defineSchema: your levels, categories, fields | |
levels | Your level names and the Sentry level each is sent as | |
integrations | all on | See above |
enabled | true | |
debug | false | Log what the SDK does |
transport | fetch | (request) => Promise<{ status, headers }>, for tests |
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.
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.
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.
MIT
FAQs
Error tracking and logs for StatOSS: browsers, Node, Bun, Deno, Cloudflare Workers and Vercel Edge. Speaks Sentry's protocol.
The npm package @statoss/sdk receives a total of 292 weekly downloads. As such, @statoss/sdk popularity was classified as not popular.
We found that @statoss/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Threat Research
/Security News
319 browser extensions installed by 13.4 million users hijack searches, capture queries, and collect referral payments from Yahoo and Bing.

Threat Research
Malicious Firefox extensions mimic Phantom, Jupiter, and Bitget wallets to steal recovery phrases and private keys, with one fetching its payload remotely.

Product
Socket Threat Intel now shows which malware campaigns were blocked or detected in your organization, with links to scans and Firewall events.