Mobile
Flutter SDK
Single Flutter package — sankofa_flutter on pub.dev — bundling six products. Analytics, Catch, Switch, Config, Pulse, Replay.
sankofa_flutter is the official Flutter SDK. Unlike the web SDK, which splits into seven packages, every product on Flutter ships in the same pub.dev package.
The product set on Flutter is six: Analytics, Catch, Switch, Config, Pulse, Replay. Deploy / OTA is not exposed at the Dart API surface today — the engine recognises the Deploy module on handshake but the Flutter SDK doesn't ship a SankofaDeploy class. For OTA on Flutter, use the React Native SDK in a hybrid app.
For installation and project setup, see Install on Flutter.
Initialize
Initialize before runApp so default properties (device, OS, locale) are captured from the first frame.
import 'package:flutter/material.dart';
import 'package:sankofa_flutter/sankofa_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Sankofa.instance.init(
apiKey: const String.fromEnvironment('SANKOFA_KEY'),
endpoint: 'https://api.sankofa.dev',
debug: !const bool.fromEnvironment('dart.vm.product'),
enableSessionReplay: true,
replayMode: ReplayMode.wireframe, // or ReplayMode.screenshot
);
runApp(const MyApp());
}Sankofa.instance.init parameters
apiKeyStringRequiredendpointStringdefault https://api.sankofa.devdebugbooldefault falsetrackLifecycleEventsbooldefault trueenableSessionReplaybooldefault falsereplayModeReplayModedefault ReplayMode.wireframereplayFpsintdefault 1enableCatchbooldefault truecatchEnvironmentStringdefault livereleaseString?appVersionString?beforeSendBeforeSendFn?Analytics — events, identify, setPerson
// Track an event
await Sankofa.instance.track('checkout_started', {
'cart_value': 49.99,
'item_count': 3,
});
// Tag a screen explicitly (auto-tracked when using SankofaNavigatorObserver)
await Sankofa.instance.screen('Checkout');
// Identify after sign-in
await Sankofa.instance.identify('user_123');
// Update profile via the dedicated People setter
await Sankofa.instance.peopleSet({
'plan': 'growth',
'role': 'operator',
});
// Or set the named common traits
await Sankofa.instance.setPerson(
name: 'Ada Lovelace',
email: 'ada@example.com',
avatar: null,
properties: {'company': 'Sankofa Ltd'},
);
// Logout — rotates session, clears identity
await Sankofa.instance.reset();
// Force-flush
await Sankofa.instance.flush();For automatic screen tracking with go_router or any Navigator-based router, register the observer:
MaterialApp(
navigatorObservers: [SankofaNavigatorObserver()],
// ...
)Catch — error capture
Catch auto-boots inside Sankofa.instance.init. Once init resolves, every Sankofa.captureException / Sankofa.log call routes to the singleton — no separate SankofaCatch setup needed and no instance to thread through your widget tree.
// Capture a caught exception from anywhere — Sentry-style.
try {
await chargeCard(amount);
} catch (err, stack) {
Sankofa.captureException(err, stack);
}
// Capture a 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(CatchUserContext(id: 'user_123', email: 'ada@example.com'));
Sankofa.setTag('flow', 'checkout');
Sankofa.setExtra('cart_id', cart.id);
// Push a structured breadcrumb (HTTP, navigation, console, etc.).
Sankofa.addBreadcrumb(CatchBreadcrumb(
category: 'user-action',
message: 'Tapped checkout button',
));Automatic coverage
Out of the box Sankofa.instance.init installs:
FlutterError.onError— every framework-level error (build/layout/paint).PlatformDispatcher.onError— async + zone-uncaught Dart errors.- Isolate error listener — errors from background isolates.
- iOS
NSSetUncaughtExceptionHandler+ POSIX signal handlers — NSException, SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE, SIGTRAP, SIGSYS. Wired by the Flutter plugin's iOS code with no Pod dependency on the standalone iOS SDK. - Android chained
Thread.UncaughtExceptionHandler+ ANR watcher — JVM-uncaught exceptions and main-thread hangs > 5s. Wired by the Flutter plugin's Android code with no Maven dependency on the standalone Android SDK.
Both Dart-side and native-side captures POST to the same /api/catch/events endpoint. Sessions correlate by distinct_id.
withScope — Sentry-style temporary scope
Use Sankofa.withScope when you want tags/extras attached to ONE capture without polluting the global scope:
Sankofa.withScope((scope) {
scope.setTag('checkout_step', 'payment');
scope.setExtra('cart_id', cart.id);
scope.setLevel(CatchLevel.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 Sankofa.instance.init for a synchronous hook that runs AFTER event composition but BEFORE enqueue. Return the (possibly modified) event to ship it, or null to drop entirely:
await Sankofa.instance.init(
apiKey: '...',
endpoint: '...',
beforeSend: (event) {
// Drop ResizeObserver-loop-limit-style noise.
if (event.message?.contains('setState() called after dispose') ?? false) {
return null;
}
// PII scrubbing — strip email from the user context.
if (event.user?.email != null) {
return event.copyWith(user: event.user!.copyWith(email: null));
}
return event;
},
);Throws inside beforeSend are swallowed — the original event ships through unchanged. A buggy hook can never break the capture pipeline.
Auto-discovered flag + config snapshots
If you've already constructed SankofaSwitch / SankofaConfig, every captured Catch event automatically carries a flag_snapshot and config_snapshot of the active decisions. The dashboard shows "which flags were ON when this error fired" without any host wiring.
| Static helper | Description |
|---|---|
Sankofa.captureException(err, [stack], [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) / Sankofa.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 both Dart + native queues. |
Switch — feature flags
Construct SankofaSwitch with bundled defaults before Sankofa.instance.init so the first decision handshake routes its payload correctly:
import 'package:sankofa_flutter/sankofa_flutter.dart';
final flags = SankofaSwitch(defaults: {
'new_checkout': FlagDecision(value: false, reason: 'default'),
'dark_mode_default': FlagDecision(value: false, reason: 'default'),
});
await Sankofa.instance.init(
apiKey: 'sk_live_...',
endpoint: 'https://api.sankofa.dev',
);
// Boolean
if (flags.getFlag('new_checkout')) showNewCheckout();
// Variant
final variant = flags.getVariant('checkout_redesign', defaultValue: 'control');
// Full envelope
final decision = flags.getDecision('new_checkout');
print(decision?.reason); // "rollout", "cohort:pro", "default", "halted", etc.
// Subscribe to changes
final unsubscribe = flags.onChange('new_checkout', (decision) {
setState(() => _newCheckoutEnabled = decision.value as bool);
});
// Later
unsubscribe();Config — remote config
SankofaConfig shares the same lifecycle pattern as SankofaSwitch:
final config = SankofaConfig(defaults: {
'max_upload_mb': ItemDecision(value: 25, version: 1, reason: 'default'),
'support_email': ItemDecision(value: 'help@example.com', version: 1, reason: 'default'),
});
await Sankofa.instance.init(/* ... */);
// Generic typed read — infers T from defaultValue
final maxMB = config.get<int>('max_upload_mb', 25);
final email = config.get<String>('support_email', 'help@example.com');
// Decision envelope
final decision = config.getDecision('max_upload_mb');
// Inspect
final allKeys = config.getAllKeys();
final allValues = config.getAll();
// Subscribe to changes
final unsubscribe = config.onChange('max_upload_mb', (decision) {
setState(() => _maxMB = decision.value as int);
});config.get<T> is generic — pass the expected type's default and the SDK validates at decode time.
Pulse — surveys
Register Pulse after Sankofa.instance.init:
final registered = await SankofaPulse.instance.register();
if (!registered) {
print('Sankofa Core must be initialized before Pulse.');
}Show a survey:
await SankofaPulse.instance.show(context, 'srv_post_purchase_feedback');
// Programmatic dismiss
SankofaPulse.instance.dismiss();Listen for events:
final unsubscribe = SankofaPulse.instance.on(PulseEvent.completed, (event) {
print('Survey ${event.surveyId} completed');
});Inspect eligibility:
final eligible = await SankofaPulse.instance.activeMatchingSurveys();
print('Eligible:', eligible.map((s) => s.id).toList());Session replay
Two recording modes:
| Mode | Payload | Best for |
|---|---|---|
ReplayMode.wireframe | Lightweight JSON skeletons of the UI tree (~5 KB/s) | Default. Good UX for replays of structural flow. |
ReplayMode.screenshot | Pixel-perfect snapshots throttled by frame rate (~50 KB/s) | High-fidelity debugging of visual bugs. |
Wrap your app in SankofaReplayBoundary and pass enableSessionReplay: true to init:
void main() {
runApp(
SankofaReplayBoundary(
child: MyApp(),
),
);
}Privacy
By default every TextField is masked. Manually mark sensitive widgets:
SankofaMask(
child: Text('Account balance: \$4,238'),
)API summary
| Method | Description |
|---|---|
Sankofa.instance.init(...) | Initialize the SDK. |
Sankofa.instance.track(event, props?) | Record an event. |
Sankofa.instance.screen(name, props?) | Tag the current screen. |
Sankofa.instance.identify(userId) | Stitch anonymous → known. |
Sankofa.instance.peopleSet(traits) | Update profile traits (verbose name). |
Sankofa.instance.setPerson(name, email, avatar, properties) | Update profile traits (named common traits). |
Sankofa.instance.reset() | Rotate session + clear identity. |
Sankofa.instance.flush() | Force-drain the queue. |
Sankofa.instance.dispose() | Release SDK resources. |
Sankofa.captureException(err, [stack], [opts]) | Capture a handled exception (Sentry-style static). |
Sankofa.captureMessage(msg, [opts]) | Capture a non-error event. |
Sankofa.log(msg, [category]) | Crashlytics-style breadcrumb log (no event emitted). |
Sankofa.setUser(user) / setTag / setTags / setExtra | Sticky ambient context. |
Sankofa.withScope(fn) | Sentry-style temporary scope overlay. |
Sankofa.flushCatch() | Force-flush Dart + native Catch queues. |
SankofaSwitch(defaults).getFlag(key, defaultValue?) | Boolean feature flag. |
SankofaSwitch.getVariant(key, defaultValue?) | Variant feature flag. |
SankofaConfig(defaults).get<T>(key, defaultValue) | Typed remote config. |
SankofaPulse.instance.register() | Initialize Pulse (async). |
SankofaPulse.instance.show(context, surveyId) | Show a survey. |