cxorbi_flutter 1.7.1 copy "cxorbi_flutter: ^1.7.1" to clipboard
cxorbi_flutter: ^1.7.1 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: ios or android
  • framework — always flutter

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, never runtimeType strings

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 #

  1. Run the app — the console prints Cxorbi Flutter SDK starting — platform: ios, framework: flutter, then Cxorbi Flutter SDK consent accepted — session: …; waiting for first screenview.

  2. Navigate to the first named screen. Replay/error capture starts at the first screenview.

  3. Emit a deterministic test event after consent:

    Cxorbi.instance.track('cxorbi_integration_test', {
      'source': 'flutter_readme',
    });
    
  4. 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.debug and look for a [cxorbi] … → 4xx line 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.

  5. 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 #

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 TabController listener
  • 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 explicit maskingMode in CxorbiConfig overrides the dashboard.
  • Images and custom render output are placeholders by default. Enable captureImageAssets / captureRenderBoundaryAssets only 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.

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,
);
await Cxorbi.instance.optIn();  // start capture (required once per launch)
Cxorbi.instance.optOut();       // stop all capture immediately

For test harnesses or a host-controlled graceful shutdown, wait for a bounded drain and inspect the result:

final uploaded = await Cxorbi.instance.flush(
  timeout: const Duration(seconds: 5),
);

For a terminal test or host-controlled shutdown, call Cxorbi.instance.pauseTracking() before flush() so no new telemetry is produced while the durable outbox is draining. Normal app lifecycle handling already pauses capture and persists pending work automatically.

false means the deadline expired; remaining telemetry stays in the encrypted durable spool for the next foreground or launch. Normal application code does not need to call this when background lifecycle callbacks are available.

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 both optIn() 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 to 0 to disable background-time rotation.
  • Cxorbi.instance.sessionId returns 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 logCxorbi.init() not reached; ensure it runs in main() before runApp().
  • Session never appearsoptIn() was not called, or the device can't reach apiUrl (check for HTTP errors with debug: true).
  • Screens all named unknown — routes are unnamed and no manual screen() calls; add CxorbiNavigatorObserver and route names.
  • Text shows as **** — that's the default privacy mode; change it in Dashboard → Settings → Recording Privacy or via maskingMode.
32
likes
0
points
668
downloads

Publisher

unverified uploader

Weekly Downloads

Cxorbi analytics for Flutter: session replay, heatmaps, screens, gestures, funnels, performance, error capture and in-app surveys for iOS and Android.

Homepage

Topics

#analytics #session-replay #heatmaps #monitoring

License

unknown (license)

Dependencies

crypto, cryptography, ffi, flutter, flutter_secure_storage, http, path_provider, sqflite

More

Packages that depend on cxorbi_flutter

Packages that implement cxorbi_flutter