Web (multi-package)
@sankofa/catch
Error capture, breadcrumbs, and source-mapped stack traces for the web. Layers on @sankofa/browser via the plugin protocol.
@sankofa/catch is the web error-tracking package. It hooks into the browser's window.onerror / unhandledrejection events, captures structured stack traces, batches breadcrumbs leading up to the error, and uploads everything to the Catch product on the engine.
It's a plugin to @sankofa/browser — once registered, error capture happens automatically. You only write code when you want to capture custom errors or refine fingerprinting.
Install
npm install @sankofa/catchRegister the plugin
import { Sankofa } from "@sankofa/browser";
import { catchPlugin } from "@sankofa/catch";
import { switchPlugin } from "@sankofa/switch";
import { configPlugin } from "@sankofa/config";
Sankofa.init({
apiKey: "sk_live_...",
endpoint: "https://api.sankofa.dev",
plugins: [
// Switch + Config register themselves with the cross-module
// registry — catchPlugin auto-discovers them at capture time
// so every error carries the active flag + config state.
switchPlugin({ defaults: { /* ... */ } }),
configPlugin({ defaults: { /* ... */ } }),
catchPlugin({
release: process.env.GIT_SHA,
// Optional Sentry-style hook to scrub PII / drop noise.
// Return null to drop the event, or a modified event to ship it.
beforeSend: (event) => {
if (event.message?.includes("ResizeObserver loop limit")) return null;
return event;
},
}),
],
});The release option is critical for source-map matching — it's the bundle identifier Catch uses to look up your source maps. Pass your commit SHA, build number, or version tag.
Sankofa.captureException, Sankofa.log, etc. are static helpers on the Sankofa namespace itself — no getCatch() instance to thread through your call sites.
What gets captured automatically
Once catchPlugin() is registered, all of these route to Catch with no further wiring:
| Source | Behavior |
|---|---|
| Uncaught exceptions | window.onerror listener captures, fingerprints, uploads. |
| Unhandled promise rejections | window.unhandledrejection listener does the same. |
| Console errors | console.error calls become breadcrumbs (not their own crash report). |
Failed fetch / XHR requests | 4xx and 5xx responses become breadcrumbs. |
| Page navigations | $pageview events become breadcrumbs. |
| Custom events | Any Sankofa.track(...) call becomes a breadcrumb on the next error. |
Capture an error manually
import { Sankofa } from "@sankofa/browser";
// Capture a handled exception from anywhere — Sentry-style.
try {
await chargeCard(amount);
} catch (err) {
Sankofa.captureException(err);
}
// Non-error event (warning-level).
Sankofa.captureMessage("Payment retry attempted");
// Crashlytics-style breadcrumb log — rides on the next captured
// event. Doesn't bill on its own.
Sankofa.log("checkout: applying coupon SUMMER25");Breadcrumbs, user, tags
import { Sankofa } from "@sankofa/browser";
// Attach ambient context. Sticky — every subsequent event carries it.
Sankofa.setUser({ id: "user_123", email: "ada@example.com" });
Sankofa.setTag("flow", "checkout");
Sankofa.setExtra("cart_id", cart.id);
// Structured breadcrumb.
Sankofa.addBreadcrumb({
category: "user-action",
message: "Clicked the submit button",
level: "info",
data: { form_id: "checkout-form" },
});withScope — Sentry-style temporary scope
When you want tags/extras attached to ONE capture without polluting the global scope:
Sankofa.withScope((scope) => {
scope.setTag("checkout_step", "payment");
scope.setExtra("cart_id", cart.id);
scope.setLevel("warning");
Sankofa.captureException(err);
});
// Outside the closure, those tags / extras are gone.Scopes are stack-scoped to the closure — async captures deferred past the closure's return won't see the scope.
beforeSend — scrub PII / drop noise
Pass beforeSend to catchPlugin(). The hook fires AFTER an event is composed but BEFORE the transport sends. Return the (possibly modified) event to ship it, or null to drop entirely.
catchPlugin({
beforeSend: (event) => {
// Drop framework noise.
if (event.message?.includes("ResizeObserver loop limit exceeded")) return null;
// PII scrubbing — strip email.
if (event.user?.email) {
return { ...event, user: { ...event.user, email: undefined } };
}
return event;
},
})Throws inside beforeSend are swallowed — the original event ships unchanged. A buggy hook can never break the capture pipeline.
Fingerprinting
By default, Catch generates a fingerprint from the error's normalized stack trace + message. Errors with the same fingerprint are grouped into one issue, so the dashboard shows you "this issue has fired 423 times across 187 users" instead of 423 individual rows.
You can override the fingerprint when you need a different grouping than the default. The exact API is documented in the SDK's @sankofa/catch source — most apps don't need to override.
Source maps
Production bundles are minified. To get readable stack traces in the dashboard:
Generate source maps with your bundler
Vite, Next.js, Remix all support
sourcemap: true(or equivalent) in production builds. The.mapfile should sit next to the bundle.Upload source maps with the CLI
bashnpx sankofa-cli catch symbols upload \ --kind js_sourcemap \ --release "$GIT_SHA" \ --dir ./distThe CLI walks
./dist, finds every.mapfile, and uploads them tagged with the release. Run this in CI after the build, before deploying.Verify in the dashboard
Trigger an error in production. The dashboard's stack trace should show your original source files and line numbers, not minified gibberish.
Privacy
captureException sends the error message and stack trace. Don't put PII in error messages. If your code does (new Error("user@example.com not found")), redact at the source — replace dynamic email/SSN/phone fragments with generic placeholders before throwing.
API summary
| Symbol | Description |
|---|---|
catchPlugin(options?) | Plugin to register at Sankofa.init. Options include release, appVersion, environment, beforeSend, captureUnhandled, captureRejections, captureConsoleError, autocapture, capturePerformance, readFlagSnapshot, readConfigSnapshot. |
getCatch() | Returns the singleton catch client (rarely needed — prefer the Sankofa.* statics below). |
Sankofa.captureException(err, options?) | Capture an error (Sentry-style static). Returns event ID. |
Sankofa.captureMessage(msg, options?) | Capture a non-error event. |
Sankofa.log(msg, category?) | Crashlytics-style breadcrumb. Doesn't bill on its own. |
Sankofa.setUser(user) | Set ambient user context. |
Sankofa.setTag(k, v) / Sankofa.setTags({...}) | Set sticky tags. |
Sankofa.setExtra(k, v) | Set a sticky extra field. |
Sankofa.addBreadcrumb(crumb) | Push a breadcrumb onto the ring buffer. |
Sankofa.withScope(fn) | Sentry-style temporary scope overlay. |
Sankofa.flushCatch() | Force-flush queued Catch events. |