cxorbi_flutter 1.6.2
cxorbi_flutter: ^1.6.2 copied to clipboard
Cxorbi analytics for Flutter: session replay, heatmaps, screens, gestures, funnels, performance, error capture and in-app surveys for iOS and Android.
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; normal end-user sessions do not upload screenshots. A separately armed, host-opted-in analyst flow can upload one viewport screenshot as a mobile heatmap ReferenceCanvas. Gestures with target elements, screen views, app performance and errors remain structural data. 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 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 the status does not change, re-run with
logLevel: LogLevel.debugand look for a[cxorbi] … → 4xxline in the console — that points to a rejected API key or an unreachable base URL. If requests are succeeding but the status stays unchanged, contact support with the session id. -
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 checklist in the Docs section of your Cxorbi dashboard or at cxorbi.com — it also covers dashboard setup and copy-paste integration patterns.
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',
// Set these only when target membership/layout genuinely changes. Scrolling
// is not a new presentation.
screenStateKey: 'review',
experimentKey: 'checkout-layout-b',
);
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 route names short and template-based, separating words
with /, - or _. Keep one route for the same logical screen and use
screenStateKey, experimentKey, or hostLayoutKey when a declared state,
experiment, or host layout changes its target inventory. Each screen() call
replaces these optional keys; omitting one resets it for the new occurrence.
Mobile ReferenceCanvas and optional semantic zones #
Mobile heatmaps work with ordinary Flutter widgets. The SDK records each interaction in the coordinate system that existed when it happened, including the viewport and vertical scroll offset, and projects it onto an analyst-authored ReferenceCanvas. Client applications do not need to wrap their screens or controls for long screenshots or coordinate heatmaps.
CxorbiSemanticZone is an optional analytics annotation. Use it only when you
want a durable business-owned name for a section or control in reports, such as
checkout_primary_action. It does not stabilize Flutter layout, enable capture,
or make an otherwise valid interaction paintable.
CxorbiSemanticZone(
zoneId: 'checkout_primary_action',
zoneLabel: 'Place order',
zoneType: 'cta',
child: ElevatedButton(
onPressed: submitOrder,
child: const Text('Place order'),
),
)
If you choose to add zones, prefer meaningful sections (cta, nav, tab,
control, list, content, and similar product-owned types). They add named
section and exposure analysis; they are not a coverage requirement.
Reference screenshots are explicit analyst/test-device captures, not background
end-user collection. To accept the dashboard's cxorbi:// capture link, opt in
only in a reviewed QA/beta build:
CxorbiConfig(inAppCaptureDeepLinkEnabled: true)
The capture control honors the dashboard-requested mode. Long mode traverses one vertical Flutter scroll view once, stores the captured slice offsets as the immutable reference frame, restores the user's scroll position, and publishes a scrollable long canvas. A different extent reported after restoration is diagnostic because lazy slivers can refine their extent while being traversed; it does not discard already captured slices. Automatic mode falls back to a viewport reference when the route cannot produce a safe long canvas.
Live text or image changes continue to produce replay frames but do not create a new heatmap layout when the widget geometry is unchanged.
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).
In-app Nudges #
Nudges are a separate, opt-in runtime for native banners, modals, and inline messages. The host flag and the selected project's server config must both be enabled; consent is still required.
await Cxorbi.init(CxorbiConfig(
apiKey: 'YOUR_API_KEY',
nudgesEnabled: true,
nudgeCustomActionKeys: const ['open_upgrade'],
));
MaterialApp(
navigatorObservers: [CxorbiNavigatorObserver()],
builder: (context, child) => CxorbiSurveyHost(
child: CxorbiNudgeHost(child: child!),
),
// ...
);
Cxorbi.instance.setNudgeCallbacks(CxorbiNudgeCallbacks(
onNudgeAction: (nudge, action) async {
if (action.actionKey == 'open_upgrade') {
// Run the corresponding host-owned action.
}
},
));
The Survey host is only needed when Ask is also enabled. Survey/Feedback keeps priority over interruptive Nudges; the hosts share only a presentation arbiter, not records, caps, storage, or telemetry.
Inline campaigns require an exact host-owned placement and never become an overlay when the placement is absent:
const CxorbiNudgePlacement(
placementId: 'checkout.summary',
fallback: SizedBox.shrink(),
)
Named screen changes and tracked events drive the corresponding triggers. The
SDK validates published content again on-device, uses native accessible widgets,
and excludes Nudge content from replay by default. showNudge() and
closeNudge() provide manual evaluation/dismissal without bypassing consent,
the server gate, targeting, reservations, or frequency caps.
Privacy & masking #
Replays are wireframes: layout boxes, text and colors, not screen pixels. The only screenshot path is the separately armed analyst ReferenceCanvas flow described above. 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'),
)
Masking redacts content, not behaviour — a tap inside a masked subtree is still counted, so the region keeps its tap heatmap and funnel steps, just unlabelled. Where the tap coordinate is itself the value (a fixed-layout keypad, a signature or pattern grid, an ordered sensitive list), also block the gestures:
CxorbiMask(
config: CxorbiMaskingConfig.maskAllAndBlockGestures,
child: PinPad(),
)
Store privacy declarations and heatmap QA #
The host app owns its Apple privacy labels/manifest and Google Play Data Safety answers because the required declaration depends on enabled Cxorbi features, identity configuration, consent/legal basis, retention, and the host's own data use. Do not copy a blanket declaration from the SDK.
For a heatmap-only review, account for pseudonymous session/visitor identifiers, device/app/OS metadata, logical screen identity, gesture coordinates, scroll reach, lifecycle-bounded visibility time, errors, and any custom event properties. Text, input values, image pixels, and analyst reference screenshots require a separate review according to the masking and reference-capture settings above. Verify both stores' current questionnaires at release time.
QA builds can enable enablePerfMetrics and inspect
CxorbiPerfMetrics.instance.heatmapSloEvaluation(). The versioned
mobile_heatmap_slo_v1 gate reports measured pass/fail and leaves missing
CPU, RAM, battery, bytes/minute, frame-delta, or queue-drop gauges as
not_measured—never as an implicit pass. Its thresholds are SDK release
budgets, not universal industry standards, and must be validated on the release
device matrix before publishing an achieved-performance claim.
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.
Carousel and paged-content state #
Raw horizontal movement cannot reveal which item the app accepted or how many
items exist. When a carousel, PageView, onboarding pager, or card stack changes
its selected item, report the app-owned state explicitly:
await Cxorbi.instance.trackPagedContent(
containerId: 'home-promotions',
index: selectedIndex,
itemCount: promotions.length,
forwardSwipeDirection: 'left',
);
Send the initial visible index and every accepted index change. Cxorbi links a
nearby horizontal gesture in the same session to the transition, classifies the
gesture as responsive with high confidence, and calculates forward/reverse
movement, item exposure, and final-item completion. It never guesses an item
count or completion from swipe distance. Wrap the pager in a CxorbiZone whose
zone ID exactly matches containerId. Supplying forwardSwipeDirection lets
the backend classify a reachable swipe with no index change as unresponsive;
without both explicit signals, the outcome remains unknown.
For a non-paged swipeable component, report a non-sensitive state token when it appears and whenever accepted state changes:
await Cxorbi.instance.trackSwipeComponentState(
componentId: 'watchlist-row-actions', // matching CxorbiZone id
stateKey: isOpen ? 'open' : 'closed',
respondsToDirections: isOpen ? {'right'} : {'left'},
);
Cxorbi links only same-session gestures targeting the matching zone. A state
change is a high-confidence accepted/responsive swipe. No change is called
unresponsive only when the preceding declaration explicitly listed that swipe
direction; otherwise the outcome remains unknown. Never put user data in
stateKey.
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 |
captureErrorsBeforeFirstScreen |
false |
Hook the Dart error handlers at optIn() instead of at the first screenview, so startup crashes are captured. Buffered until the first screenview; nothing is sent before consent or before the session exists. Off by default because enabling it raises error volume for sessions that previously reported none |
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. The enum has six values but only three are distinguishable today: none silences all SDK output; error/warn/info print SDK diagnostics only; debug/verbose additionally print the [cxorbi] request/transport logs. Values inside a group behave identically — the finer steps are reserved, not yet honoured |
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.