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 build.
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_API_KEY'),
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 1.3.1 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 Integrations in the dashboard and click Verify installation. The status should move to SDK connected after the first event arrives. If row-level debugging is needed, a super admin can confirm the screen view, taps, and
cxorbi_integration_testevent in the SDK event feed. -
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_API_KEY',
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 path templates:
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
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. - Returning after 30 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) | Organization API key from Settings → API Key |
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 |
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 |
captureFrameVitals |
true |
Aggregated Flutter slow/frozen frame vitals sent through performance telemetry |
captureNativeDiagnostics |
true |
Android process-exit diagnostics and iOS MetricKit crash/hang diagnostics |
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 |
30 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, bounded to a supported range |
backgroundSessionTimeoutMs |
1800000 (30 min) |
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, only after
Flutter actually painted a new frame, and only re-emits when screen content
changed. Frame trees are bounded, and the adaptive quality governor can lower
ordinary capture fidelity under sustained jank while preserving periodic
keyframes.
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.