Cxorbi Flutter SDK
Session replay, heatmaps, journeys, performance and error analytics for Flutter apps — iOS and Android from a single integration.
The SDK captures wireframe replays (no screenshots ever leave the device), gestures with target elements, screen views, app performance and errors. Image and custom-render assets are placeholders by default and can be uploaded only through explicit opt-in privacy settings. Records use the industry-standard two-dimension identity model:
platform— the host OS the session ran on:iosorandroidframework— alwaysflutter
So a Flutter session shows up under iOS or Android in the dashboard with a Flutter badge, exactly like React Native sessions do.
📚 Full documentation lives in the Docs section of your Cxorbi dashboard and at cxorbi.com — getting started, screen tracking, identity, events, transactions, session replay, heatmaps, customer journeys, funnels, error analysis, privacy & masking, API reference, data collection, performance impact, production checklist, compatibility, troubleshooting.
Requirements
- Flutter 3.27+ / Dart 3.5+
- iOS and Android (web/desktop are not captured)
- Works with
flutter build --obfuscate— all widget detection uses compile-time type checks, neverruntimeTypestrings
Installation
flutter pub add cxorbi_flutter
No manual native (Pod/Gradle) setup is required for the default integration — standard Flutter dependency resolution wires the iOS/Android plugin.
Quick start
For production apps, gate optIn() behind your consent/legal-basis flow. This
quick start opts in immediately so you can verify the integration in a
development or staging source.
import 'package:cxorbi_flutter/cxorbi_flutter.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Cxorbi.init(CxorbiConfig(
apiKey: const String.fromEnvironment('CXORBI_SOURCE_TOKEN'),
environment: CxorbiEnvironment.development,
debugMode: true,
logLevel: LogLevel.debug,
));
// The SDK is opted OUT by default — nothing is captured or sent until
// optIn() is called. In production, call this from your consent flow.
await Cxorbi.instance.optIn();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [CxorbiNavigatorObserver()], // automatic screen tracking
// ...
);
}
}
AI agent setup
This package ships a Flutter Agent Skill
(skills/cxorbi_flutter-integrate). With an agent that supports the standard
(Claude, Cursor, Copilot, …), install it from your dependencies and let the
assistant do the wiring:
dart pub global activate skills # once
skills get # installs cxorbi_flutter's skill into your IDE
Then prompt: "Integrate Cxorbi in this application." The skill is consent-gated by design — it never auto opts-in users and never fabricates an API key or user id.
Verify your integration
-
Run the app — the console prints
Cxorbi Flutter SDK 0.2.0 starting — platform: ios, framework: flutter, thenCxorbi Flutter SDK consent accepted — session: …; waiting for first screenview. -
Navigate to the first named screen. Replay/error capture starts at the first screenview.
-
Emit a deterministic test event after consent:
Cxorbi.instance.track('cxorbi_integration_test', { 'source': 'flutter_readme', }); -
Open Live Events in the dashboard and confirm the screen view, taps, and
cxorbi_integration_testevent arrive within seconds. -
Open Session Replay — the session appears within a minute under the iOS/Android tab with a Flutter badge. Screens appear in Heatmaps → iOS/Android once a screen has interactions.
Before shipping, run the release-readiness checks in
docs/production-checklist.md. Dashboard setup and copy-paste integration
patterns live in docs/dashboard-onboarding.md and
docs/reference-integrations.md.
Screen tracking
Automatic (recommended)
CxorbiNavigatorObserver tracks every named route pushed, popped or
replaced. Unnamed routes are ignored rather than guessed — give your routes
names (RouteSettings(name: '/checkout')) or use the callbacks below.
MaterialApp(
navigatorObservers: [
CxorbiNavigatorObserver(
// Skip dialogs / splash screens:
excludeRoute: (route) => route.settings.name == '/splash',
// Rename or derive names dynamically (return null = use route name):
screenNameProvider: (route) =>
route.settings.name == '/p' ? '/product-detail' : null,
// Attach route-level variables:
customVarsProvider: (route) => const [
CustomVar(index: 1, name: 'area', value: 'checkout'),
],
),
],
)
Manual
Call Cxorbi.instance.screen(name) whenever a screen becomes visible. Manual
calls always win over the observer. You need manual calls for:
- PageView — call in
onPageChanged - TabBar — call from a
TabControllerlistener - Modals / bottom sheets you want treated as screens
Cxorbi.instance.screen('/checkout');
If your app starts with home: and no named initial route, call screen() when
the first view appears; otherwise replay/error/performance capture will keep
waiting for a real screen name.
Naming guidance: keep names short
and template-based, separate words with /, - or _, and encode state in
the name when it changes the layout — e.g. /cart-empty vs /cart.
In-app surveys (Ask)
The SDK can fetch, render and submit server-driven surveys natively. It is
inert by default — turn it on with surveysEnabled: true, then wrap your
app once so the SDK can present over your UI without owning the Navigator:
await Cxorbi.init(CxorbiConfig(
apiKey: 'YOUR_PUBLIC_SOURCE_TOKEN',
surveysEnabled: true, // opt-in; default false
));
MaterialApp(
navigatorObservers: [
CxorbiNavigatorObserver(checkSurveyOnNavigation: true),
],
builder: (context, child) => CxorbiSurveyHost(child: child!),
// ...
);
After optIn(), eligible surveys auto-display on launch, app-foreground,
navigator screen changes when checkSurveyOnNavigation is true, and on
Cxorbi.instance.trackEvent(...). You can also drive them manually:
await Cxorbi.instance.showSurvey(); // fetch + present if eligible
final s = await Cxorbi.instance.fetchSurvey(); // fetch only, no UI
Cxorbi.instance.closeSurvey(); // dismiss the active survey
Cxorbi.instance.setSurveyCallbacks(CxorbiSurveyCallbacks(
onSurveyDisplayed: (s) {},
onSurveyCompleted: (s, answers) {},
));
All 11 question types, skip logic, dashboard theming and a thank-you card are
supported. For regulated apps: consent gates both fetch and submit, the survey
overlay never enters session replay, free-text answers are PII-scrubbed before
egress (scrubFreeTextPii, default on), and allowedApiHosts can pin egress to
approved hosts (fail-closed).
Privacy & masking
Replays are wireframes: layout boxes, text, colors — never pixels or screenshots. On top of that:
- Input fields are masked by default. Only explicitly unmask fields after confirming your privacy policy allows it.
- All other text is masked by default (
MaskingMode.text), with the mode controlled from the dashboard (Settings → Recording Privacy). An explicitmaskingModeinCxorbiConfigoverrides the dashboard. - Images and custom render output are placeholders by default. Enable
captureImageAssets/captureRenderBoundaryAssetsonly for reviewed, non-sensitive surfaces. - Modes:
none(capture text),digits(mask digits only),text/full(mask all text, length-preserving*).
To mask a specific widget regardless of the global mode, wrap it in
CxorbiMask:
CxorbiMask(
child: Text(user.cardNumber),
)
Everything inside a CxorbiMask subtree is masked by default, even when the
global mode is none. For scoped overrides:
CxorbiMask(
config: const CxorbiMaskingConfig(maskTexts: false),
child: const Text('Public label'),
)
Identify users
Cxorbi.instance.identify('user-123', {
'plan': 'premium',
'country': 'IN',
});
Cxorbi.instance.addUserProperties(properties: {'campaign': 'summer'});
// On logout — clears identity + user/event properties and rotates the session
// so the next user is never mixed into the previous one's replay/heatmaps:
Cxorbi.instance.reset();
Custom events
Cxorbi.instance.track('feature_used', {'feature': 'dark_mode'});
// Attach properties to every subsequent event:
Cxorbi.instance.addEventProperties(properties: {'app_version': '2.1.4'});
Events feed journeys and funnels alongside automatic screen views.
Transactions
Cxorbi.instance.trackTransaction(
orderId: 'ord_001',
revenue: 4999, // minor units
currency: 'USD',
items: [
{'sku': 'SKU-1', 'qty': 1},
],
);
Errors
Uncaught Flutter framework errors and 4xx/5xx Dart HttpClient API errors are
captured automatically after the first screenview. Query strings are stripped
from API error URLs. Mask path segments with CS-style patterns:
Cxorbi.instance.setURLMaskingPatterns(patterns: [
'https://api.example.com/users/:user_id/address/:address',
]);
Report handled errors yourself:
try {
await api.submit();
} catch (e, st) {
Cxorbi.instance.reportError(e, st);
}
For native plugin, WebView or custom-client API failures that do not flow
through Dart HttpClient, report the failed request explicitly:
Cxorbi.instance.reportNetworkError(
method: 'POST',
url: Uri.parse('https://api.example.com/orders/123'),
statusCode: 500,
);
Consent
await Cxorbi.instance.optIn(); // start capture (required once per launch)
Cxorbi.instance.optOut(); // stop all capture immediately
One-token onboarding
Prefer to start with a single source token? StartConfig builds a
CxorbiConfig from one credential and feeds the same engine:
await Cxorbi.init(
StartConfig.withDatasourceId(id: 'YOUR_PUBLIC_SOURCE_TOKEN').toCxorbiConfig(),
);
await Cxorbi.instance.optIn();
StartConfig.withDatasourceId() is the recommended one-token onboarding path.
The token is source-scoped and ingest-only; it cannot access dashboard or
administrative APIs. Existing CxorbiConfig(apiKey: ...) and
StartConfig.withApiKey() integrations remain supported.
Replay sampling and event-triggered replay
The dashboard's Replay collection rate applies only to full replay frames. Screen analytics, funnels, journeys, errors, performance, and enabled heatmap data continue at 100%.
When Event-Triggered Replay is enabled, unsampled sessions send masked, short-lived candidate frames. Candidates are not visible in Session Replay and expire unless your app promotes them:
await Cxorbi.instance.triggerReplayForCurrentSession('payment_failed');
await Cxorbi.instance.triggerReplayForCurrentScreen('checkout_validation_failed');
The session trigger retains the whole candidate session, including frames from before the trigger. The screen trigger retains only the current screen-view instance. Because full-session ETR captures pre-trigger context, it can create replay network traffic for sessions that are later discarded.
Sessions
- A session id is created at
init(). Mobile replay/error/perf capture starts only after bothoptIn()and the first screenview. - During replay testing, returning after 2 minutes in background starts a
new session by default. Shorter app switches resume the existing session.
Configure this explicitly with
backgroundSessionTimeoutMs; set it to0to disable background-time rotation. Cxorbi.instance.sessionIdreturns the current id.- To join a session minted by your backend analytics, pass
CxorbiConfig(sessionId: ...).
Configuration reference
CxorbiConfig field |
Default | Purpose |
|---|---|---|
apiKey |
— (required) | SDK credential; prefer the public source token from Capture Settings → Data sources |
ingestKey |
null |
Source-scoped public ingest token when using dual-header compatibility |
apiUrl |
https://api.cxorbi.com/api |
API base URL |
dashboardUrl |
null |
Enables metadata.sessionReplayUrl |
sessionId |
auto (fl_…) |
Externally minted session id |
userId |
null |
Initial user id (else call identify) |
maskingMode |
dashboard setting | Explicit masking override |
maskingConfig |
inherit | Fine-grained text/input/image/interaction masking |
captureFrames |
true |
Wireframe replay frames |
captureGestures |
true |
Taps / swipes / scrolls |
enableInteractionsAutocapture |
true |
CS-compatible alias for gesture/heatmap interaction autocapture; set to false only when intentionally disabling interaction capture |
captureErrors |
true |
Automatic error capture |
capturePerformance |
true |
App/screen/network performance samples |
captureNetworkErrors |
true |
Automatic 4xx/5xx API errors through HttpOverrides |
captureNativeNetworkErrors |
true |
Native/WebView bridge for API error reports |
captureImageAssets |
false |
Opt-in unmasked image asset upload for replay |
captureRenderBoundaryAssets |
false |
Opt-in CxorbiCaptureBoundary custom-render upload |
sessionReplayAutoStart |
true |
Start replay automatically after first screenview |
urlMaskingPatterns |
[] |
API error URL path masking patterns |
offlineQueueEnabled |
true |
Encrypted bounded disk queue on iOS/Android |
offlineQueueMaxEntries |
200 |
Max pending requests before oldest-drop |
offlineQueueMaxBytes |
5 MB |
Max pending bytes before oldest-drop |
maxReplayAssetBytes |
512 KB |
Max single visual replay asset |
logLevel |
warn |
Diagnostic verbosity (none/error/warn/info/debug/verbose); none silences all SDK output |
debug |
false |
Alias for logLevel: LogLevel.debug — verbose [cxorbi] request logs |
environment |
production |
production/staging/development; stamped on every session |
debugMode |
kDebugMode |
Flags test/debug traffic; set explicitly for staging/profile builds when needed |
captureCadenceMs |
300 |
Base replay tree-walk cadence in milliseconds, clamped 50..10000 |
backgroundSessionTimeoutMs |
120000 (2 min) |
Temporary testing default: background inactivity before the next foreground starts a new session; 0 disables this rotation |
sessionHeartbeatIntervalMs |
15000 (15 sec) |
Lightweight foreground activity marker used for accurate duration when unchanged frames are deduplicated; 0 disables it |
gzipRequests |
false |
Gzip SDK request bodies at or above gzipMinBytes |
gzipMinBytes |
1024 |
Minimum uncompressed body size before gzip is applied |
qualityConfig |
enabled defaults | Adaptive quality governor config; disables or tunes jank-based replay throttling |
Session-list duration is cumulative foreground-active time, not simply
lastTimestamp - firstTimestamp. The lifecycle close marker records an exact
short visit (for example, 14 seconds) even when it ends before the first
15-second heartbeat. Heartbeats are crash/force-termination checkpoints and
live-update signals; they are not the duration measurement granularity. An OS
kill that delivers no lifecycle/crash callback can only be reported through the
last durable checkpoint, so its final duration is a lower bound.
How capture works (and what it costs)
The frame walker runs at the configured captureCadenceMs interval (default
300 ms, clamped 50..10000 ms), only after Flutter actually painted a new frame,
and only re-emits when screen content changed (an idle screen sends a keyframe
every 10 s). Trees are capped at 800 nodes / depth 60, and the adaptive quality
governor can lower ordinary capture fidelity under sustained jank while keeping
the 10-second keyframe floor.
Gestures are observed on the global pointer router — no GestureDetector
wrapping, no interference with your app's gesture handling.
Troubleshooting
- No startup log —
Cxorbi.init()not reached; ensure it runs inmain()beforerunApp(). - Session never appears —
optIn()was not called, or the device can't reachapiUrl(check for HTTP errors withdebug: true). - Screens all named
unknown— routes are unnamed and no manualscreen()calls; addCxorbiNavigatorObserverand route names. - Text shows as
****— that's the default privacy mode; change it in Dashboard → Settings → Recording Privacy or viamaskingMode.
Libraries
- csq
- cxorbi_flutter
- Cxorbi analytics SDK for Flutter.