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.

Dartlib/main.dart
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

apiKeyStringRequired
Project API key (live or test). The engine resolves the environment from the key.
endpointStringdefault https://api.sankofa.dev
Server base URL. Pass a regional endpoint to pin data residency.
debugbooldefault false
Verbose logging. Disable in production.
trackLifecycleEventsbooldefault true
Auto-track $app_opened, $app_foregrounded, $app_backgrounded.
enableSessionReplaybooldefault false
Wrap your app in <SankofaReplayBoundary> to actually capture replays.
replayModeReplayModedefault ReplayMode.wireframe
Either wireframe (lightweight JSON) or screenshot (pixel-perfect, ~10x bandwidth).
replayFpsintdefault 1
Frame capture rate for replay.
enableCatchbooldefault true
Auto-install the Catch singleton + chained FlutterError.onError / PlatformDispatcher.onError handlers. Set false only to defer Catch boot for integration tests.
catchEnvironmentStringdefault live
Environment tag stamped on every captured event (live, staging, dev, custom).
releaseString?
Release identifier (e.g. '[email protected]+42') sent on every Catch event for grouping.
appVersionString?
App version override sent in the Catch device context.
beforeSendBeforeSendFn?
Synchronous hook fired AFTER event composition but BEFORE enqueue. Return the (possibly modified) event to ship it, or null to drop. Throws are swallowed.

Analytics — events, identify, setPerson

Dart
// 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: '[email protected]',
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:

Dart
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.

Dart
// 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: '[email protected]'));
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:

Dart
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:

Dart
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 helperDescription
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:

Dart
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:

Dart
final config = SankofaConfig(defaults: {
'max_upload_mb': ItemDecision(value: 25, version: 1, reason: 'default'),
'support_email': ItemDecision(value: '[email protected]', 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', '[email protected]');

// 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:

Dart
final registered = await SankofaPulse.instance.register();
if (!registered) {
print('Sankofa Core must be initialized before Pulse.');
}

Show a survey:

Dart
await SankofaPulse.instance.show(context, 'srv_post_purchase_feedback');

// Programmatic dismiss
SankofaPulse.instance.dismiss();

Listen for events:

Dart
final unsubscribe = SankofaPulse.instance.on(PulseEvent.completed, (event) {
print('Survey ${event.surveyId} completed');
});

Inspect eligibility:

Dart
final eligible = await SankofaPulse.instance.activeMatchingSurveys();
print('Eligible:', eligible.map((s) => s.id).toList());

Session replay

Two recording modes:

ModePayloadBest for
ReplayMode.wireframeLightweight JSON skeletons of the UI tree (~5 KB/s)Default. Good UX for replays of structural flow.
ReplayMode.screenshotPixel-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:

Dart
void main() {
runApp(
  SankofaReplayBoundary(
    child: MyApp(),
  ),
);
}

Privacy

By default every TextField is masked. Manually mark sensitive widgets:

Dart
SankofaMask(
child: Text('Account balance: \$4,238'),
)

API summary

MethodDescription
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 / setExtraSticky 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.

What's next

Edit this page on GitHub