Mobile

React Native SDK

One package, all products — @sankofa/react-native bridges to the native iOS and Android SDKs and ships analytics, errors, flags, config, surveys, replay, and the only mobile Deploy/OTA surface in the matrix.

@sankofa/react-native wraps the native iOS and Android Sankofa SDKs and exposes a single TypeScript surface. It's the only mobile SDK that ships Deploy/OTA at the package level — Flutter, native iOS, and native Android don't expose Deploy.

For installation and project setup, see Install on React Native.

Initialize

TSXApp.tsx
import { Sankofa } from "@sankofa/react-native";

Sankofa.initialize(process.env.EXPO_PUBLIC_SANKOFA_KEY!, {
endpoint: "https://api.sankofa.dev",
recordSessions: true,
maskAllInputs: true,
debug: __DEV__,
});

Sankofa.initialize options

apiKeystringRequired
Project API key (live or test).
endpointstringdefault https://api.sankofa.dev
Server base URL.
recordSessionsbooleandefault true
Enable native session replay capture.
maskAllInputsbooleandefault true
Auto-mask all <TextInput> components in replays.
trackLifecycleEventsbooleandefault true
Auto-track $app_opened, $app_foregrounded, $app_backgrounded.
flushIntervalSecondsnumberdefault 30
Foreground flush cadence.
batchSizenumberdefault 50
Maximum events per upload batch.
debugbooleandefault false
Verbose console logging.
enableCatchbooleandefault true
Auto-install JS-side SankofaCatch + native NSException/JVM-uncaught handlers. Set false to defer Catch boot.
catchEnvironmentstringdefault live
Environment tag stamped on every captured Catch event.
releasestring?
Release identifier (e.g. '[email protected]') forwarded to both JS Catch + native crash reporters so JS errors and native crashes share one release dimension.
appVersionstring?
App version override sent in the Catch device context.
beforeSendBeforeSendFn?
Synchronous hook fired AFTER event composition but BEFORE the transport sends. Return the (possibly modified) event to ship it, or null to drop. Only applies to the JS-side capture path.

Analytics — track, identify, screen

TSX
import { Sankofa, useSankofaScreen } from "@sankofa/react-native";

function CheckoutScreen() {
// Hook tags the current screen on mount; powers heatmaps + replay context.
useSankofaScreen("Checkout");

return (
  <Pressable onPress={() => Sankofa.track("pay_clicked", { amount: 29.99 })}>
    <Text>Pay</Text>
  </Pressable>
);
}

// On sign-in
Sankofa.identify("user_123");

// Update profile traits
Sankofa.setPerson({ email: "[email protected]", plan: "pro" });

// On logout
Sankofa.reset();

// Force-flush
Sankofa.flush();

Auto-tag screens from React Navigation

If your app uses @react-navigation/native, drop useSankofaNavigationTracking(navRef) in your app shell and every screen change tags into the heatmap pipeline automatically — no per-screen useSankofaScreen call needed.

TSXApp.tsx
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
import { Sankofa, useSankofaNavigationTracking } from "sankofa-react-native";

export default function App() {
const navRef = useNavigationContainerRef();
useSankofaNavigationTracking(navRef);

return (
  <NavigationContainer ref={navRef}>
    <RootStack />
  </NavigationContainer>
);
}

What the hook does:

  • Subscribes to React Navigation's state event and re-tags the active route via Sankofa.screen(name) on every change.
  • Tags the initial route synchronously on mount (so cold-start frames carry the right screen, not the framework host fallback).
  • Dedupes redundant retags — a state event for the same route is a no-op.
  • Tolerates null/undefined refs (e.g. while the container is still mounting).
  • Survives a getCurrentRoute() that throws (pre-mount race in the container).

Use useSankofaScreen(name) directly only when you want per-component manual tagging instead of (or alongside) the navigation hook.

Catch — error capture

Catch auto-boots inside Sankofa.initialize on both the JS side AND the underlying iOS/Android native side. Once init resolves, every Sankofa.captureException / Sankofa.log call routes to the singleton — no new SankofaCatch(...) boilerplate needed.

TSX
import { Sankofa } from "@sankofa/react-native";

// Capture a caught 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");

// 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 (HTTP, navigation, console, etc.).
Sankofa.addBreadcrumb({ category: "user-action", message: "Tapped checkout" });

Automatic coverage

Sankofa.initialize installs:

  • ErrorUtils.setGlobalHandler (RN ErrorUtils) — JS-side uncaught exceptions.
  • unhandledrejection — unhandled promise rejections.
  • iOS NSSetUncaughtExceptionHandler + POSIX signal handlers — NSException and SIGSEGV/SIGABRT/SIGBUS/etc. via the bundled SankofaIOS Pod.
  • Android chained Thread.UncaughtExceptionHandler + ANR watcher — JVM-uncaught exceptions and main-thread hangs.

All four sources POST to the same /api/catch/events endpoint. Sessions correlate by distinct_id.

withScope — Sentry-style temporary scope

TSX
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

TSX
Sankofa.initialize("sk_live_...", {
endpoint: "https://api.sankofa.dev",
beforeSend: (event) => {
  // Drop ResizeObserver-loop-limit-style noise.
  if (event.message?.includes("setState() called after unmount")) return null;
  // PII scrubbing — strip email from the user context.
  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. Only applies to the JS-side capture path; native NSException + JVM crashes are composed by the native SDKs.

React error boundaries

For class-component boundaries, call the static helper from componentDidCatch:

TSXapp/_layout.tsx
import React from "react";
import { Sankofa } from "@sankofa/react-native";

class RootBoundary extends React.Component<{ children: React.ReactNode }> {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
  Sankofa.withScope((scope) => {
    scope.setExtra("componentStack", errorInfo.componentStack ?? "");
    Sankofa.captureException(error);
  });
}
// ... render fallback
}
Static helperDescription
Sankofa.captureException(err, [opts])Capture a handled exception. Returns event ID or '' if Catch isn't started.
Sankofa.captureMessage(msg, [opts])Non-error variant.
Sankofa.log(msg, [category])Crashlytics-style breadcrumb. Doesn't bill.
Sankofa.setUser(user) / setUser(null)Set / clear ambient user.
Sankofa.setTag(k, v) / setTags({...})Sticky tags.
Sankofa.setExtra(k, v)Sticky extra context.
Sankofa.addBreadcrumb(crumb)Push to ring buffer.
Sankofa.withScope(fn)Temporary scope overlay.
Sankofa.flushCatch()Force-flush queued Catch events.

Switch — feature flags

Construct SankofaSwitch before Sankofa.initialize:

TSX
import { Sankofa, SankofaSwitch } from "@sankofa/react-native";

const flags = new SankofaSwitch({
defaults: { new_checkout: false, dark_mode_default: false },
});

Sankofa.initialize("sk_live_...", { endpoint: "https://api.sankofa.dev" });

// Boolean
if (flags.getFlag("new_checkout")) showNewCheckout();

// Variant
const variant = flags.getVariant("checkout_redesign", "control");

// Full envelope
const decision = flags.getDecision("new_checkout");
console.log(decision?.reason);  // "rollout", "cohort:pro", "default", "halted", etc.

// Subscribe to changes (e.g. halt-webhook fire)
const unsubscribe = flags.onChange("new_checkout", (decision) => {
setNewCheckoutEnabled(decision.value as boolean);
});

Config — remote config

TSX
import { Sankofa, SankofaConfig } from "@sankofa/react-native";

const config = new SankofaConfig({
defaults: { max_upload_mb: 25, support_email: "[email protected]" },
});

Sankofa.initialize("sk_live_...");

// Generic typed read
const maxMB = config.get<number>("max_upload_mb", 25);
const email = config.get<string>("support_email", "[email protected]");

// Decision envelope
const decision = config.getDecision("max_upload_mb");

// Subscribe to changes
const unsubscribe = config.onChange("max_upload_mb", (decision) => {
setMaxMB(decision.value as number);
});

Pulse — surveys

SankofaPulse is imperative (no <SankofaPulseProvider> context):

TSX
import { SankofaPulse, SurveyModal } from "@sankofa/react-native";

const pulse = new SankofaPulse();

// Show a survey
await pulse.show("srv_post_purchase_feedback");

// Programmatic dismiss
pulse.dismiss();

// Listen for events
const unsubscribe = pulse.on("completed", (event) => {
console.log("Survey", event.surveyId, "completed");
});

// Set per-user eligibility context (passed back to engine on next handshake)
pulse.setContext({ tenantId: "acme", plan: "pro" });

Render <SurveyModal> once at your app's root for the SDK to mount surveys into:

TSXApp.tsx
import { SurveyModal } from "@sankofa/react-native";

export default function App() {
return (
  <NavigationContainer>
    {/* ... your screens */}
    <SurveyModal />
  </NavigationContainer>
);
}

Deploy — OTA releases (RN-only)

SankofaDeploy ships JavaScript updates without an App Store / Play Store review. This is the only mobile SDK that exports a Deploy class.

TSX
import { SankofaDeploy } from "@sankofa/react-native";

const deploy = new SankofaDeploy({
apiKey: "sk_live_...",
serverUrl: "https://api.sankofa.dev",
checkOnResume: true,
});

// Confirm successful boot — prevents auto-rollback.
deploy.notifyAppReady();

// Check for updates
const update = await deploy.checkForUpdate();

if (update.updateAvailable) {
if (update.isMandatory) {
  await deploy.downloadAndApply(update);
} else {
  await deploy.downloadInBackground(update);
}
}

Deploy API

checkForUpdate()Promise<UpdateCheckResult>
Polls the server for a new bundle.
downloadAndApply(update)Promise<void>
Downloads and restarts to apply immediately.
downloadInBackground(update)Promise<void>
Downloads silently; applies on next launch.
notifyAppReady()void
Marks the bundle healthy. Call after first successful render.
applyPending()Promise<void>
Apply a previously downloaded update on demand.
reportError(err, opts?)void
Report an error for crash-rate tracking — feeds auto-rollback.
getStatus()Promise<DeployStatus>
Current deploy state — last check, last apply, etc.

Session replay

Replay capture happens at the native layer — the SDK forwards your masking config to the native iOS / Android boundaries. Recording is enabled when recordSessions: true (default).

For per-component masking, mark sensitive components by setting their testID to a sentinel value the native layer recognizes, or rely on the auto-mask behavior for <TextInput> (default maskAllInputs: true).

API summary

SymbolDescription
Sankofa.initialize(apiKey, config?)Initialize the SDK.
Sankofa.track(event, props?)Record an event.
Sankofa.screen(name, props?)Tag the current screen.
Sankofa.identify(userId)Stitch anonymous → known.
Sankofa.setPerson(traits)Update profile traits.
Sankofa.reset()Rotate session + clear identity.
Sankofa.flush()Force-drain the queue.
useSankofaScreen(name)Hook — tags the current screen on mount.
useSankofaNavigationTracking(navRef)Hook — auto-tags every React Navigation screen change. Drop once in your app shell; no per-screen wiring needed.
Sankofa.captureException(err, [opts])Capture an error (auto-routes to Catch singleton).
Sankofa.captureMessage(msg, [opts])Capture a non-error event.
Sankofa.log(msg, [category])Crashlytics-style breadcrumb.
Sankofa.setUser / setTag(s) / setExtra / addBreadcrumbAmbient context.
Sankofa.withScope(fn)Temporary scope overlay.
Sankofa.flushCatch()Force-flush Catch events.
new SankofaSwitch({defaults?})Construct a flag client.
new SankofaConfig({defaults?})Construct a config client.
new SankofaPulse({...})Construct a Pulse client.
new SankofaDeploy({...})Construct a Deploy/OTA client.
<SurveyModal> componentMount once at root for Pulse to render into.

What's next

Edit this page on GitHub