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
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
apiKeystringRequiredendpointstringdefault https://api.sankofa.devrecordSessionsbooleandefault truemaskAllInputsbooleandefault truetrackLifecycleEventsbooleandefault trueflushIntervalSecondsnumberdefault 30batchSizenumberdefault 50debugbooleandefault falseenableCatchbooleandefault truecatchEnvironmentstringdefault livereleasestring?appVersionstring?beforeSendBeforeSendFn?Analytics — track, identify, screen
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: "ada@example.com", 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.
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
stateevent and re-tags the active route viaSankofa.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/undefinedrefs (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.
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: "ada@example.com" });
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 bundledSankofaIOSPod. - 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
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
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:
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 helper | Description |
|---|---|
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:
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
import { Sankofa, SankofaConfig } from "@sankofa/react-native";
const config = new SankofaConfig({
defaults: { max_upload_mb: 25, support_email: "help@example.com" },
});
Sankofa.initialize("sk_live_...");
// Generic typed read
const maxMB = config.get<number>("max_upload_mb", 25);
const email = config.get<string>("support_email", "help@example.com");
// 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):
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:
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.
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>downloadAndApply(update)Promise<void>downloadInBackground(update)Promise<void>notifyAppReady()voidapplyPending()Promise<void>reportError(err, opts?)voidgetStatus()Promise<DeployStatus>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
| Symbol | Description |
|---|---|
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 / addBreadcrumb | Ambient 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> component | Mount once at root for Pulse to render into. |