Sankofa Flutter SDK ๐
The official Flutter SDK for Sankofa โ seven products in one package: Analytics, Catch, Switch, Config, Pulse, Replay, and Deploy (App-Store-compliant Flutter OTA updates). Plus a standalone native crash bridge that captures iOS NSException + POSIX signals and Android JVM-uncaught + ANR with zero dependency on the iOS / Android SDKs.
โจ Features
- Analytics: events, identify, peopleSet, deep-link attribution, offline-first queueing.
- Catch (Crashlytics + Sentry merged): Dart-level errors via
FlutterError.onError/PlatformDispatcher.onError/ isolate listeners โ plus iOS NSException + POSIX signals + main-queue stalls and Android JVM-uncaught + ANR via the bundled Flutter plugin. - Switch: feature flags with bundled defaults + onChange listeners.
- Config: remote-config with typed
get<T>accessors. - Pulse: in-app surveys (NPS, CSAT, custom).
- Session Replay: wireframe + screenshot modes, automatic input masking,
SankofaMaskwidget. - Deploy: Flutter OTA updates โ fix bugs and tweak UI without a new App Store / Play Store release. App Store compliant under Apple PLA ยง 3.3.2 and Google Play DNA's interpreted-code carve-out.
๐ Quick start
1. Install
Add the dependency to your pubspec.yaml:
dependencies:
sankofa_flutter: ^0.2.8
2. Initialize
One call wires every product. Analytics, Catch, Switch, Config, Pulse,
Replay and Deploy all come up from a single init() โ no per-product
new SankofaX() boilerplate, no register() step. Each product is gated
server-side via the handshake, so a flag you don't subscribe to is simply
a no-op. Both Dart errors AND iOS/Android native crashes flow through
Sankofa.captureException.
import 'package:sankofa_flutter/sankofa_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Sankofa.instance.init(
apiKey: 'YOUR_PROJECT_API_KEY',
endpoint: 'https://api.sankofa.dev',
debug: true,
// โโ Product switches (all default true except Deploy) โโ
enableAnalytics: true, // events, screens, lifecycle, presence
enableCatch: true, // errors + native crashes
enableFlags: true, // Switch โ feature flags
enableConfig: true, // remote config
enablePulse: true, // in-app surveys (auto-shows, no extra wiring)
enableDeploy: true, // OTA updates
// โโ Optional per-product config โโ
catchEnvironment: 'production',
release: 'myapp@1.4.0',
flagDefaults: {'new_checkout': FlagDecision(value: false, reason: 'default')},
configDefaults: {'max_upload_mb': ItemDecision(value: 25, version: 1, reason: 'default')},
// Sentry-style hook to scrub PII / drop noise.
beforeSend: (event) {
if (event.message?.contains('setState called after dispose') ?? false) return null;
return event;
},
);
runApp(const MyApp());
}
After init(), reach any product through the client:
Sankofa.instance.flags, .config, .pulse, .errors, .deploy.
Turning a product off: pass
enableFlags: false(etc.) to skip constructing it entirely.enableAnalytics: falseships a build that sends zero analytics events while keeping Catch/Switch/Config/Pulse live.
๐ Usage
Analytics
// Events
Sankofa.instance.track('completed_purchase', {
'item_name': 'Vintage Camera',
'price': 120.50,
'currency': 'USD',
});
// Identity
Sankofa.instance.identify('user_99');
Sankofa.instance.setPerson(
name: 'Jane Doe',
email: 'jane@example.com',
properties: {'membership': 'Gold'},
);
Catch โ error capture
Once init() resolves, every helper below works from anywhere. No
SankofaCatch.instance to thread through your widget tree.
// Capture a handled exception
try {
await chargeCard(amount);
} catch (err, stack) {
Sankofa.captureException(err, stack);
}
// Non-error event
Sankofa.captureMessage('payment retry attempted');
// Crashlytics-style breadcrumb log โ rides on next capture, doesn't bill.
Sankofa.log('checkout: applying coupon SUMMER25');
// Ambient context
Sankofa.setUser(CatchUserContext(id: 'u_42', email: 'ada@example.com'));
Sankofa.setTag('flow', 'checkout');
Sankofa.setExtra('cart_id', cart.id);
// Sentry-style temporary scope โ tags only on this capture.
Sankofa.withScope((scope) {
scope.setTag('checkout_step', 'payment');
scope.setLevel(CatchLevel.warning);
Sankofa.captureException(err);
});
Session replay
MaterialApp(
navigatorObservers: [SankofaNavigatorObserver()],
home: const SankofaReplayBoundary(
child: MyHomePage(),
),
);
// Hide sensitive UI from replays โ replaced with solid black in the
// upload, untouched in the live UI.
SankofaMask(
child: TextField(controller: _passwordController, obscureText: true),
);
// Suppress capture for screens that host external textures (video,
// maps, web views) โ protects the surrounding scroll position + avoids
// "Invalid external texture" log spam on Android.
SankofaReplaySuppress(
child: BetterPlayer(controller: _videoController),
);
Switch โ feature flags
Auto-constructed by init(enableFlags: true). Read from anywhere via
Sankofa.instance.flags:
final flags = Sankofa.instance.flags!; // pass flagDefaults to init() for offline-first values
if (flags.getFlag('new_checkout')) showNewCheckout();
final variant = flags.getVariant('checkout_redesign', defaultValue: 'control');
Config โ remote config
Auto-constructed by init(enableConfig: true). Read via
Sankofa.instance.config:
final config = Sankofa.instance.config!; // pass configDefaults to init() for offline-first values
final maxUploads = config.get<int>('max_uploads_per_day', 25);
Pulse โ surveys
Auto-registered by init(enablePulse: true) โ no register() call needed.
Surveys flagged auto-show in the dashboard appear on their own; Pulse
discovers your app's navigator automatically, so there's no navigator key
to wire up. Present one manually with:
await Sankofa.instance.pulse.show(context, surveyId: 'nps-2024');
// React to lifecycle events
Sankofa.instance.pulse.on(PulseEvent.surveyCompleted, (e) {
print('Survey ${e.surveyId} completed');
});
Targeting: rules are AND-ed and evaluated on-device. For Screen
rules, tag screens with Sankofa.instance.screen('โฆ') (or
SankofaNavigatorObserver). User-property rules read traits from
setPerson, Event rules read on-device track() counts, Cohort
rules are resolved server-side via the handshake, and Feature-flag
rules read SankofaSwitch. URL rules are web-only and ignored on Flutter
โ use a Screen rule. Supply data the SDK can't see itself via
Sankofa.instance.pulse.setDefaultTargetingContext(userProperties: โฆ, cohorts: โฆ, flagValues: โฆ) or per-call show(..., properties: โฆ, cohorts: โฆ).
Deploy โ Flutter OTA updates
Ship bug fixes and UI tweaks without an App Store / Play Store release. Sankofa Deploy is built to comply with both stores' policies on runtime updates (Apple PLA ยง 3.3.2, Google Play's DNA policy).
Setup: run the Sankofa CLI once โ it wires everything for you: creates
sankofa.yaml, adds the OTA runtime binding to your project, lists
sankofa.yaml under flutter.assets, and initializes the loader in main().
npm i -g sankofa-cli
sankofa init --deploy
Then paste your project's api_key into sankofa.yaml:
app_id: proj_xxxxxxxxxxxxx
api_key: sk_live_xxxxxxxxxxxxxxxx
After setup, your main() looks like this (the CLI writes it for you):
import 'package:dynamic_modules/dynamic_modules.dart';
import 'package:sankofa_flutter/sankofa_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SankofaUpdater.registerLoader(loadModuleFromBytes);
await SankofaUpdater.preFlight(); // applies any staged patch on cold launch
runApp(const MyApp());
}
After registerLoader, every other API call is loader-free. Engine version +
signing keys live in sankofa.yaml (managed by the CLI); customer code never
touches them.
Check + download from anywhere in your app:
final updater = SankofaUpdater();
// Currently-installed patch โ null on baseline.
final current = await updater.readCurrentPatch();
print(current?.label ?? 'baseline');
// Is a new patch available?
final result = await updater.checkForUpdate();
if (result.hasUpdate) {
final update = result.update!;
if (update.isMandatory) {
// Skip the prompt โ download immediately.
await updater.downloadUpdate(update);
} else {
// Gate behind a user confirmation.
final yes = await showUpdateDialog(context, update);
if (yes) {
await updater.downloadUpdate(
update,
onProgress: (received, total) {
setState(() => _progress = total > 0 ? received / total : 0);
},
);
}
}
}
After downloadUpdate returns, the patch is staged on disk. The next cold
launch applies it automatically (the preFlight() call in step 3 picks it
up). Optionally prompt for restart so the patch takes effect immediately.
Auto-rollback: the SDK auto-disables any patch that crashes the app twice within 30 seconds of launch. The last known-good patch is restored automatically, and the bad label is added to a local ban list so the same patch isn't re-downloaded. No host code required.
๐ชค Native crash bridge
The Flutter SDK ships a standalone native crash reporter built into the plugin (iOS + Android) โ no separate native SDK to install.
| Layer | Captured |
|---|---|
| Dart | FlutterError.onError, PlatformDispatcher.onError, isolate listeners |
| iOS plugin | NSSetUncaughtExceptionHandler, POSIX signals (SIGSEGV/SIGABRT/SIGBUS/SIGILL/SIGFPE/SIGTRAP/SIGSYS), main-queue stalls |
| Android plugin | Chained Thread.UncaughtExceptionHandler, ANR watcher (main thread > 5s) |
All three report through the same Catch pipeline. Sankofa.setUser /
setTag(s) / flushCatch mirror to the native side automatically.
๐ฉบ Troubleshooting
OTA updates aren't applying after restart
Verify in this order:
SankofaUpdater.preFlight()is called beforerunApp(). Without it, the staged patch isn't read off disk on cold launch.sankofa.yamlis listed inpubspec.yaml'sflutter.assets:. Otherwise the SDK can't find yourapi_keyat runtime.- The dashboard's rollout for the release is 100% (or explicitly
includes your device's
distinct_id). app_versionmatches. The server matches the release'starget_binary_versionto the device's app version verbatim โ no fuzzy semver matching.
๐ Documentation
For full API references and integration guides, visit the Sankofa docs.
๐ก License
Distributed under the MIT License. See LICENSE for more information.
Libraries
- deploy
- Subpath import for Sankofa Deploy only.
- sankofa_flutter
- Flutter client SDK for Sankofa Analytics, Deploy, Switch, and Config โ offline queueing, session replay, feature flags, and remote config.