Engineering

Catch

Crash detection, error tracking, transactions, and web vitals. Auto-rollback in under 60 seconds via halt webhooks. Native + JS + server-side capture, with full source-map symbolication.

Catch is Sankofa's error-tracking and performance-monitoring product. Every official Sankofa SDK ships Catch — it's the broadest product in the matrix.

Catch captures:

  • Errors: uncaught exceptions, unhandled rejections, native crashes, ANRs.
  • Breadcrumbs: low-noise events leading up to a failure (clicks, route changes, console logs, fetch responses).
  • Transactions: distributed-tracing spans for measuring server-side and (where instrumented) client-side latency.
  • Web vitals: LCP, FID, CLS, INP — for the web SDK.
  • Profiles: sampling profiler output (server SDKs).

The goal isn't only to alert when something breaks — it's to stop the regression in flight. Catch wires into Switch and Deploy via the halt webhook so that a spike in error rate auto-rolls back the OTA bundle that caused it, in under 60 seconds, before most users notice.

What you'll see in the dashboard

ViewPurpose
IssuesGrouped errors. Each issue is a fingerprint — a stack-trace + message family — with first/last seen, total count, affected users, and bonded release(s).
EventsIndividual error occurrences, with full payload + breadcrumbs + replay link (if Replay was active).
TransactionsTrace spans grouped by name (route, function, db query). Used for performance regression tracking.
VitalsWeb Vitals time series + percentiles, broken down by route + cohort.
ProfilesFlame graphs from sampled profilers (server SDKs).
AlertsConditions on issue / transaction / vital metrics that trigger webhooks, Slack messages, or PagerDuty pages.
Ownership rulesAuto-assign issues to teams + people based on stack-trace heuristics.

How issues are grouped

Catch fingerprints every captured event into an issue so you don't see 10,000 rows for the same crash. The fingerprint is computed from:

  1. The exception type (TypeError, NullPointerException, etc.)
  2. The normalized stack trace (top N user-code frames, with line/column stripped to allow refactor tolerance)
  3. The exception message (with PII patterns scrubbed)

Two events with the same fingerprint group into the same issue. You can override the fingerprint at the SDK call site:

TypeScript
Sankofa.captureException(err, {
fingerprint: ["billing-charge-failed"],  // collapse all charge errors into one issue
tags: { feature: "billing" },
extras: { amount, currency },
});

Custom fingerprints are useful for errors that the auto-grouper splits too aggressively (e.g. dynamically-named functions creating unique stacks).

SDK ergonomics

The Sankofa SDKs ship a unified Catch API across Web, Flutter, React Native, Android, iOS — Crashlytics + Sentry merged. Once you've called the SDK's main init function, every helper below works from anywhere in your app with no instance to thread through:

TypeScript
// Capture from anywhere — Sentry-style.
Sankofa.captureException(err);
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");

// 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);

// Sentry-style temporary scope — tags/extras attached to ONE
// capture without polluting the global scope.
Sankofa.withScope((scope) => {
scope.setTag("checkout_step", "payment");
scope.setLevel("warning");
Sankofa.captureException(err);
});

Automatic coverage (zero wiring)

The SDK installs platform-specific handlers when you call init:

PlatformCaptured
Webwindow.onerror, unhandledrejection, optional console.error capture, fetch/XHR breadcrumbs, Web Vitals.
FlutterDart-side FlutterError.onError, PlatformDispatcher.onError, isolate error listeners. The bundled Flutter plugin adds iOS NSException + POSIX signals + main-queue stalls, plus Android JVM-uncaught + ANRs — standalone, no Pod or Maven dependency on the native iOS/Android Sankofa SDKs.
React NativeJS-side ErrorUtils + unhandledrejection, plus native NSException / JVM-uncaught via the RN bridge wrapping SankofaIOS / sankofa_sdk_android.
Android (native)Chained Thread.UncaughtExceptionHandler + CatchAnrWatcher.
iOS (native)NSSetUncaughtExceptionHandler + POSIX signal handlers + main-queue stall detector (2s default threshold, configurable).

beforeSend — scrub PII / drop noise

Every SDK accepts a beforeSend hook at init time. It fires AFTER event composition but BEFORE the transport sends. Return the (possibly modified) event to ship, or null to drop entirely. Throws are swallowed — a buggy hook can never break the capture pipeline.

TypeScript
Sankofa.init({
// ...
plugins: [catchPlugin({
  beforeSend: (event) => {
    if (event.message?.includes("ResizeObserver loop limit")) return null;
    if (event.user?.email) {
      return { ...event, user: { ...event.user, email: undefined } };
    }
    return event;
  },
})],
});

Auto-discovered flag + config snapshots

If you've registered switchPlugin / configPlugin (web), or constructed SankofaSwitch / SankofaConfig (Flutter/RN/native), every captured event automatically carries the active flag_snapshot + config_snapshot. The dashboard shows "which flags were ON when this error fired" without any host wiring.

Auto-rollback (the <60s loop)

This is what makes Catch operationally different.

  1. Catch detects the spike

    An alert rule watches error_rate on a key route or feature. When the rate crosses the threshold (configurable per project), the rule fires.

  2. Halt-webhook fires

    Catch posts to POST /api/switch/halt-webhook with the affected flag's key. The halt is identified either by an explicit flag-key configured on the alert, or by introspecting the most recent decision-handshake snapshot.

  3. Switch revokes the flag

    The flag transitions to halted. Within ~30 seconds, every connected client SDK receives the new decision (default value applies).

  4. Deploy reverts the bundle

    For React Native OTA bundles gated on the flag, the next call to deploy.reportError (or to deploy.checkForUpdate) detects the halt and triggers rollbackToPrevious.

  5. Audit + ticket

    The halt is logged in the audit log. If you've configured a Plan integration, a ticket is auto-created in the affected board with the issue link, the affected release, and the rollback timestamp.

End-to-end this completes in < 60 seconds for the alert → halt → rollback path; the dashboard's banner reflects the rolled-back state immediately.

Source-map and symbol upload

Production bundles + binaries are minified / stripped. To get readable stack traces:

PlatformFormatUpload command
WebJavaScript source mapsnpx sankofa-cli catch symbols upload --kind js_sourcemap --release "$RELEASE_SHA" --dir ./dist
iOSdSYM bundles... --kind dsym --release "$RELEASE_SHA" --dir ./build/dSYMs
AndroidProGuard / R8 mapping files... --kind proguard_mapping --release "$RELEASE_SHA" --file ./build/outputs/mapping/release/mapping.txt
Android NDKNDK symbols... --kind ndk --release "$RELEASE_SHA" --dir ./obj
FlutterFlutter symbols... --kind flutter --release "$RELEASE_SHA" --dir ./build/symbols
Server (Node)JavaScript source mapsSame command as Web.

Run these in CI after build, before deploy. Catch matches uploaded symbols against release on the captured event.

For ad-hoc symbolication of an obfuscated stack you've collected manually:

bash
sankofa catch symbolicate --release "$RELEASE_SHA" --stack ./crash.txt

Web vitals (web only)

Catch on the web ships a Web Vitals capture path:

TypeScript
Sankofa.init({ apiKey: "...", endpoint: "...", plugins: [catchPlugin()] });
// Web Vitals are captured automatically when @sankofa/catch is registered.

The dashboard shows LCP / FID / CLS / INP at the p50, p75, p90, and p99 percentiles, broken down by route and cohort, with regression alerts available.

Transactions and profiling (server SDKs)

Server SDKs (Node, Go, Java, Python) ship distributed-tracing primitives:

TypeScript
import { startTransaction } from "@sankofa/node";

const tx = startTransaction({ name: "checkout_handler", op: "http.server" });
const span = tx.startChild({ name: "db.query.orders", op: "db" });
// ... do work ...
span.finish();
tx.finish();

Transactions appear in the Transactions view, with p50/p95/p99 latency and error correlation per name.

For sampling profilers, see the per-SDK reference (NodeProfiler, Profiler on Python, etc.).

Issue ownership

Configure rules at /dashboard/catch/ownership to auto-assign issues to teams or individuals. Rules match against:

  • Stack-trace path globs (web/billing/** → billing team)
  • Tag values (feature=billing → billing team)
  • File / module ownership from a CODEOWNERS file uploaded to the dashboard

Multiple rules can match; the most-specific path wins.

API surface

Catch exposes:

Ingestion (called by the SDK; not for direct use)

EndpointPurpose
POST /api/catch/eventsError events
POST /api/catch/transactionsTransactions + spans
POST /api/catch/vitalsWeb vitals
POST /api/catch/profilesSampling profiler output

Dashboard / management

EndpointPurpose
GET /api/v1/catch/configRead project config (sample rates, ignore rules).
GET /api/v1/catch/issuesList + search issues.
GET /api/v1/catch/eventsDrill into individual events.
GET /api/v1/catch/ownershipRead + write ownership rules.

Catch limits by tier

PlanEvents / monthRetentionVitalsProfilesOwnership rules
Hobby100K30 daysbasic
Pro5M90 daysunlimited
Growth25M180 daysunlimited + SLA alerts
Enterprisecustomcustom (up to indefinite)unlimited + per-rule audit export

What's next

Edit this page on GitHub