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

bash
npm install @sankofa/catch

Register the plugin

TypeScript
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:

SourceBehavior
Uncaught exceptionswindow.onerror listener captures, fingerprints, uploads.
Unhandled promise rejectionswindow.unhandledrejection listener does the same.
Console errorsconsole.error calls become breadcrumbs (not their own crash report).
Failed fetch / XHR requests4xx and 5xx responses become breadcrumbs.
Page navigations$pageview events become breadcrumbs.
Custom eventsAny Sankofa.track(...) call becomes a breadcrumb on the next error.

Capture an error manually

TypeScript
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");
TypeScript
import { Sankofa } from "@sankofa/browser";

// Attach ambient context. Sticky — every subsequent event carries it.
Sankofa.setUser({ id: "user_123", email: "[email protected]" });
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:

TypeScript
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.

TypeScript
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:

  1. Generate source maps with your bundler

    Vite, Next.js, Remix all support sourcemap: true (or equivalent) in production builds. The .map file should sit next to the bundle.

  2. Upload source maps with the CLI

    bash
    npx sankofa-cli catch symbols upload \
    --kind js_sourcemap \
    --release "$GIT_SHA" \
    --dir ./dist

    The CLI walks ./dist, finds every .map file, and uploads them tagged with the release. Run this in CI after the build, before deploying.

  3. 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("[email protected] not found")), redact at the source — replace dynamic email/SSN/phone fragments with generic placeholders before throwing.

API summary

SymbolDescription
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.

What's next

Edit this page on GitHub