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
| View | Purpose |
|---|---|
| Issues | Grouped errors. Each issue is a fingerprint — a stack-trace + message family — with first/last seen, total count, affected users, and bonded release(s). |
| Events | Individual error occurrences, with full payload + breadcrumbs + replay link (if Replay was active). |
| Transactions | Trace spans grouped by name (route, function, db query). Used for performance regression tracking. |
| Vitals | Web Vitals time series + percentiles, broken down by route + cohort. |
| Profiles | Flame graphs from sampled profilers (server SDKs). |
| Alerts | Conditions on issue / transaction / vital metrics that trigger webhooks, Slack messages, or PagerDuty pages. |
| Ownership rules | Auto-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:
- The exception type (
TypeError,NullPointerException, etc.) - The normalized stack trace (top N user-code frames, with line/column stripped to allow refactor tolerance)
- 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:
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:
// 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:
| Platform | Captured |
|---|---|
| Web | window.onerror, unhandledrejection, optional console.error capture, fetch/XHR breadcrumbs, Web Vitals. |
| Flutter | Dart-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 Native | JS-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.
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.
Catch detects the spike
An alert rule watches
error_rateon a key route or feature. When the rate crosses the threshold (configurable per project), the rule fires.Halt-webhook fires
Catch posts to
POST /api/switch/halt-webhookwith 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.Switch revokes the flag
The flag transitions to
halted. Within ~30 seconds, every connected client SDK receives the new decision (default value applies).Deploy reverts the bundle
For React Native OTA bundles gated on the flag, the next call to
deploy.reportError(or todeploy.checkForUpdate) detects the halt and triggersrollbackToPrevious.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:
| Platform | Format | Upload command |
|---|---|---|
| Web | JavaScript source maps | npx sankofa-cli catch symbols upload --kind js_sourcemap --release "$RELEASE_SHA" --dir ./dist |
| iOS | dSYM bundles | ... --kind dsym --release "$RELEASE_SHA" --dir ./build/dSYMs |
| Android | ProGuard / R8 mapping files | ... --kind proguard_mapping --release "$RELEASE_SHA" --file ./build/outputs/mapping/release/mapping.txt |
| Android NDK | NDK symbols | ... --kind ndk --release "$RELEASE_SHA" --dir ./obj |
| Flutter | Flutter symbols | ... --kind flutter --release "$RELEASE_SHA" --dir ./build/symbols |
| Server (Node) | JavaScript source maps | Same 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:
sankofa catch symbolicate --release "$RELEASE_SHA" --stack ./crash.txtWeb vitals (web only)
Catch on the web ships a Web Vitals capture path:
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:
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
CODEOWNERSfile 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)
| Endpoint | Purpose |
|---|---|
POST /api/catch/events | Error events |
POST /api/catch/transactions | Transactions + spans |
POST /api/catch/vitals | Web vitals |
POST /api/catch/profiles | Sampling profiler output |
Dashboard / management
| Endpoint | Purpose |
|---|---|
GET /api/v1/catch/config | Read project config (sample rates, ignore rules). |
GET /api/v1/catch/issues | List + search issues. |
GET /api/v1/catch/events | Drill into individual events. |
GET /api/v1/catch/ownership | Read + write ownership rules. |
Catch limits by tier
| Plan | Events / month | Retention | Vitals | Profiles | Ownership rules |
|---|---|---|---|---|---|
| Hobby | 100K | 30 days | ✓ | — | basic |
| Pro | 5M | 90 days | ✓ | ✓ | unlimited |
| Growth | 25M | 180 days | ✓ | ✓ | unlimited + SLA alerts |
| Enterprise | custom | custom (up to indefinite) | ✓ | ✓ | unlimited + per-rule audit export |