cx_flutter_plugin 0.9.1
cx_flutter_plugin: ^0.9.1 copied to clipboard
The Coralogix SDK for Flutter is designed to support various Flutter targets by leveraging the numerous platforms supported by Coralogix's native SDKs.
Official Coralogix SDK for Flutter. #
The Coralogix RUM Mobile SDK is library (plugin) for Flutter The SDK provides mobile Telemetry instrumentation that captures:
- HTTP requests
- Unhandled / handled exceptions
- Custom Log
- Crashes (iOS Native - using PLCrashReporter)
- Views
- Session Replay (record and replay user sessions)
Coralogix captures data by using an SDK within your application's runtime. These are platform-specific and allow Coralogix to have a deep understanding of how your application works.
Installaion #
Step 1 :Add Coralogix dependency #
In the root folder of your flutter app add the Coralogix package: flutter pub add cx_flutter_plugin.
Step 2 :Integration #
Inorder to initailized the RUM SDK, please supply both CXExporterOptions and CXDomain.
import 'package:cx_flutter_plugin/cx_http_client.dart';
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
var coralogixDomain = **< Coralogix Domain >**;
var options = CXExporterOptions(
coralogixDomain: <CXDomain>,
userContext: null,
environment: '<Environment>',
application: '<App Name>',
version: '<App Version>',
publicKey: '<PublicKey>',
ignoreUrls: [],
ignoreErrors: [],
customDomainUrl: '',
labels: {'item': 'playstation 5', 'itemPrice': 1999},
debug: false,
);
await CxFlutterPlugin.initSdk(options);
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
}
Step 3: Android setup #
Add the INTERNET permission to your app's main Android manifest:
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<!-- ...your <application> block... -->
</manifest>
Why this matters. New Flutter projects ship with INTERNET declared only in the
debug/ and profile/ manifest variants (for hot-reload). Release builds inherit only
from main/, so without this entry your release app has no network access at all —
the Coralogix RUM SDK silently can't reach the ingest endpoint, and Image.network /
HTTP calls fail. iOS has no equivalent permission gate, which is why this only bites on
Android release builds.
If you're on targetSdk 28+, also make sure any cleartext HTTP endpoints you call are
allowed via android:usesCleartextTraffic or a network-security-config.xml. HTTPS
endpoints (including all Coralogix ingest domains) work without extra config.
Exclude instrumentations from session sampling #
sdkSampler (alias sessionSampleRate) gates the entire SDK by default. Use
excludeFromSampling to keep specific instrumentation categories emitting events even when
the session is sampled out — useful when you want to ship sdkSampler: 0 (or a low rate)
for cost control but still capture all errors and logs.
import 'package:cx_flutter_plugin/cx_excludable_instrumentation.dart';
var options = CXExporterOptions(
// ...other options...
sdkSampler: 0,
excludeFromSampling: const [
CXExcludableInstrumentation.errors,
CXExcludableInstrumentation.logs,
],
);
Supported categories: errors, logs, network, userInteractions, mobileVitals,
customSpan, customMeasurement. Defaults to an empty list — the sampler gates everything
exactly as before. Requires iOS SDK ≥ 3.8.0 and Android SDK ≥ 2.12.0.
Network Requests #
CxHttpClient (dart:http)
By Using CxHttpClient The RUM SDK can catch / monitor the http traffic.
final client = CxHttpClient(http.Client());
await client.get(Uri.parse(url));
CxDioInterceptor (Dio)
If your app uses the Dio HTTP client, add CxDioInterceptor to your Dio instance to automatically capture network requests and generate RUM spans — no migration from your existing networking layer required.
Step 1: Add dio to your pubspec.yaml:
dependencies:
dio: ^5.7.0
Step 2: Attach the interceptor to your Dio instance:
import 'package:dio/dio.dart';
import 'package:cx_flutter_plugin/cx_dio_interceptor.dart';
final dio = Dio();
dio.interceptors.add(CxDioInterceptor());
The interceptor automatically captures for every request:
| Field | Description |
|---|---|
url |
Full request URL |
host |
Hostname |
method |
HTTP method (GET, POST, …) |
status_code |
HTTP status code (0 on connection error) |
status_text |
HTTP status message |
duration |
Request duration in milliseconds |
http_response_body_size |
Response body size in bytes |
schema |
URL scheme (https / http) |
fragments |
URL fragment |
request_headers |
Request headers map (only when a matching CxNetworkCaptureRule with reqHeaders is configured) |
response_headers |
Response headers map (only when a matching CxNetworkCaptureRule with resHeaders is configured) |
request_payload |
Request body (only when a matching CxNetworkCaptureRule with collectReqPayload: true is configured) |
response_payload |
Response body (only when a matching CxNetworkCaptureRule with collectResPayload: true is configured) |
error_message |
Error description (on failure) |
traceId / spanId |
W3C traceparent IDs (when tracing is enabled) |
W3C Traceparent injection
To automatically inject traceparent headers and correlate RUM spans with backend traces, enable traceParentInHeader in your CXExporterOptions:
var options = CXExporterOptions(
// ...other options...
traceParentInHeader: {
'enable': true,
'options': {
'allowedTracingUrls': ['api.example.com', 'backend.example.com'],
},
},
);
await CxFlutterPlugin.initSdk(options);
Only requests whose host matches an entry in allowedTracingUrls will receive the traceparent header.
Network Capture Rules
By default, no headers or payloads are captured. Use networkCaptureConfig in CXExporterOptions to opt in to capturing headers and payloads on a per-URL basis — useful for collecting diagnostic data while keeping sensitive endpoints clean.
import 'package:cx_flutter_plugin/cx_network_capture_rule.dart';
var options = CXExporterOptions(
// ...other options...
networkCaptureConfig: [
CxNetworkCaptureRule(
urlPattern: r'.*api\.example\.com.*',
reqHeaders: ['Accept', 'Content-Type'],
resHeaders: ['Content-Type', 'Content-Length'],
collectReqPayload: true,
collectResPayload: true,
),
CxNetworkCaptureRule(
url: 'https://analytics.example.com/track',
// No headers, no payload captured for this URL.
),
],
);
Rules are evaluated in list order — the first matching rule wins. Use url for exact matches or urlPattern (a Dart RegExp-compatible string) for pattern matches. When networkCaptureConfig is set, URLs that match no rule have their headers and payloads suppressed entirely.
| Field | Type | Description |
|---|---|---|
url |
String? |
Exact URL to match |
urlPattern |
String? |
Regex pattern to match against the full URL |
reqHeaders |
List<String>? |
Allowlist of request header names to capture (case-insensitive) |
resHeaders |
List<String>? |
Allowlist of response header names to capture (case-insensitive) |
collectReqPayload |
bool |
Capture the request body (default: false) |
collectResPayload |
bool |
Capture the response body (default: false) |
Payload size limit: Request and response bodies longer than 1024 characters are dropped entirely — not truncated. If a body exceeds the limit,
request_payload/response_payloadwill be absent from the span.
Dart-layer vs. native-layer rules:
networkCaptureConfigenriches only requests made throughCxHttpClientorCxDioInterceptor. Requests intercepted by the native iOS/Android SDK (via swizzling / OkHttp) use the native-side rules configured vianetworkExtraConfigduringinitSdk.
Custom Spans #
Custom spans let you mark manual business flows and correlate them with Dart-side network/interaction/error instrumentation.
Prerequisite: initialize the SDK with traceParentInHeader.enable = true.
Without this, CxFlutterPlugin.getCustomTracer() returns null.
import 'package:cx_flutter_plugin/cx_flutter_plugin.dart';
import 'package:cx_flutter_plugin/cx_http_client.dart';
Future<void> runCheckoutFlow() async {
final tracer = CxFlutterPlugin.getCustomTracer(
ignoredInstruments: const [
// Optional: disable linkage for specific Dart instruments.
CoralogixIgnoredInstrument.userInteractions,
],
);
if (tracer == null) return;
final global = await tracer.startGlobalSpan(
'checkout.flow',
labels: {'screen': 'checkout'},
);
if (global == null) return; // Native rejected (e.g., another global span is active).
try {
await global.withContext(() async {
final client = CxHttpClient();
try {
await client.get(Uri.parse('https://api.example.com/checkout'));
} finally {
client.close();
}
});
final child = await global.startCustomSpan('checkout.payment');
await child.endSpan();
} finally {
await global.endSpan();
}
}
withContext uses runZonedGuarded under the hood, so the active global span id
is preserved across await chains in that flow and not leaked to unrelated async
tasks.
Time Measurement #
Time arbitrary spans of work with startTimeMeasure / endTimeMeasure. The duration is
reported by the native SDK as a custom-measurement span (milliseconds) — useful when you
need to measure something the SDK can't auto-instrument (checkout flows, custom render
passes, asset loading, etc.).
await CxFlutterPlugin.startTimeMeasure('checkout', labels: {'cart_size': 3});
// ... do work ...
await CxFlutterPlugin.endTimeMeasure('checkout');
| Method | Argument | Behavior |
|---|---|---|
startTimeMeasure |
name: String |
Unique identifier. Empty / whitespace-only names are ignored. Duplicate start for an in-flight name is ignored (first wins). |
startTimeMeasure |
labels: Map<String, dynamic>? |
Optional. Merged with SDK-level labels at end; start labels win on key collision. |
endTimeMeasure |
name: String |
Must match a prior start. No-op when never started, already ended, or the session has gone idle. |
The bridge is a pure pass-through — no Dart-side state is kept, the native SDK owns the
in-flight registry. You are responsible for pairing every start with exactly one end
(leaked starts persist in memory until the session ends).
Traces Exporter #
Use tracesExporter in CXExporterOptions to receive native-exported trace
batches as OTLP-style JSON maps over the Flutter bridge.
final options = CXExporterOptions(
// ...other options...
tracesExporter: (Map<String, dynamic> batch) {
// Example: batch['resourceSpans'] -> scopeSpans -> spans
debugPrint('Received trace batch with keys: ${batch.keys.toList()}');
},
enableSwizzling: true,
);
Typical payload shape (simplified):
resourceSpans(array)resourceSpans[].scopeSpans(array)resourceSpans[].scopeSpans[].spans(array of span objects liketraceId,spanId,name, timestamps, attributes)
Unhandled / handled exceptions #
For handled exceptions Use Try / Catch scheme with the reportError API.
If you have a Stack Trace you can route it as follows. Pass data alongside the
stack trace to attach custom attributes to the emitted RUM event on both iOS and
Android:
try {
throw StateError('state error try catch');
} catch (error, stackTrace) {
if (error is StateError) {
await CxFlutterPlugin.reportError(
error.message,
{'fruit': 'banana', 'price': 1.30},
stackTrace.toString(),
);
}
}
⚠️ Platform note:
reportError(message, data, '')(empty stack trace) attachesdataon iOS only — the Android native SDK has no data-only entry point, sodatais dropped on Android in that shape. Always supply a stack trace when you need attributes attached cross-platform.
For Unhandled exceptions
you need to wrap you runAPP function ad follow
void main() {
runZonedGuarded(() {
runApp(const MaterialApp(
title: 'Navigation Basics',
home: MyApp(),
));
}, (error, stackTrace) {
CxFlutterPlugin.reportError(error.toString(), {}, stackTrace.toString());
});
}
Custom Log
await CxFlutterPlugin.log(CxLogSeverity.error, 'this is an error', {'fruit': 'banna', 'price': 1.30});
Views
To monitor page / views, report the current screen with setView:
await CxFlutterPlugin.setView(viewName);
Each setView also drives the SDK's product-analytics fields on the emitted
RUM events: view_number (a per-session counter that increments on each unique
view change) and isNavigationEvent. These are owned and computed by the native
SDK — you only need to report the view.
Automatic view tracking
Instead of calling setView in every page, drop CxNavigatorObserver into your
app's navigator and route changes are tracked for you:
MaterialApp(
navigatorObservers: [CxNavigatorObserver()],
// ...
);
Only PageRoutes are tracked (dialogs, bottom sheets and popups are not treated
as a screen change). The view name is taken from RouteSettings.name, so name
your routes:
MaterialPageRoute(
settings: const RouteSettings(name: 'Cart'),
builder: (_) => const CartPage(),
);
Unnamed routes are skipped. To derive the name differently (or skip a route by
returning null), pass a nameExtractor:
CxNavigatorObserver(
nameExtractor: (route) => route.settings.name ?? route.runtimeType.toString(),
);
Set Labels
Sets the labels for the Coralogix exporter.
final labels = {'stock': 'NVDA', 'price': 104};
await CxFlutterPlugin.setLabels(labels);
Set User Context
Setting User Context
var userContext = UserContext(
userId: '456',
userName: 'Robert Davis',
userEmail: 'robert.davis@example.com',
userMetadata: {'car': 'tesla'},
);
await CxFlutterPlugin.setUserContext(userContext);
Shutdown
Shuts down the Coralogix exporter and marks it as uninitialized.
await CxFlutterPlugin.shutdown();
Session Replay #
Session Replay allows you to record and replay user sessions for debugging and analytics purposes.
Setup #
First, initialize the Session Replay masking handler in your main() function:
import 'package:cx_flutter_plugin/cx_session_replay_masking.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SessionReplayMasking.initialize();
runZonedGuarded(() {
runApp(const MyApp());
}, (error, stackTrace) {
CxFlutterPlugin.reportError(error.toString(), {}, stackTrace.toString());
});
}
Initialize Session Replay #
import 'package:cx_flutter_plugin/cx_session_replay_options.dart';
final options = CXSessionReplayOptions(
captureScale: 1.0, // Screenshot scale (0.0-1.0)
captureCompressQuality: 0.8, // JPEG compression quality (0.0-1.0)
sessionRecordingSampleRate: 100, // Percentage of sessions to record (0-100)
autoStartSessionRecording: true, // Start recording automatically
maskAllTexts: false, // Mask all text in screenshots
textsToMask: ['password', 'credit'], // Regex patterns — mask text containing these words
maskAllImages: false, // Mask all images in screenshots
);
// Masking for Flutter content is performed in DART on both iOS and Android: the
// native SDK requests a pre-masked bitmap of the Flutter view on each capture, and
// the plugin walks the render tree to black out text/images before handing the bytes
// to native. CXSessionReplayOptions is the single source of truth for masking —
// SessionReplayMasking.initialize() in main() only registers the handler and takes
// no masking arguments.
// textsToMask patterns are RegExp strings, case-sensitive by default ('password'
// will not match "Password"). Dart's RegExp has no inline (?i) flag — use a character
// class instead: '[Pp]assword'. Text fields (RenderEditable) are always masked while
// masking is active; their live content is not read, to avoid inspecting sensitive input.
await CxFlutterPlugin.initializeSessionReplay(options);
Masking #
All masking is configured through CXSessionReplayOptions — that single object is the source of truth. SessionReplayMasking.initialize() in main() only registers the handler; it takes no masking arguments. To change masking later, just call initializeSessionReplay(newOptions) again.
Mask everything (text + images):
CXSessionReplayOptions(
maskAllTexts: true,
maskAllImages: true,
// ...other options
)
Mask only specific text (regex), leave the rest visible:
CXSessionReplayOptions(
maskAllTexts: false, // turn off "mask all text"
textsToMask: [r'\d{3}-\d{2}-\d{4}', // e.g. SSNs
'^Session.*', // text starting with "Session"
'password'], // RegExp strings, case-sensitive
maskAllImages: false,
)
Mask only images:
CXSessionReplayOptions(maskAllTexts: false, maskAllImages: true)
Mask one specific widget (regardless of the flags above) — wrap it in MaskedWidget:
import 'package:cx_flutter_plugin/cx_session_replay_masking.dart';
MaskedWidget(child: Image.network(avatarUrl))
// Conditional masking:
MaskedWidget(
isMasked: true, // set to false to temporarily disable
child: TextField(decoration: InputDecoration(labelText: 'Credit Card')),
)
Notes:
maskAllTextsandmaskAllImagesare independent — enable either or both.maskAllImagescoversImage.*,RawImage, andContainer/CircleAvatarbackground images (DecorationImage). For content the render tree can't expose (platform views, native maps/WebViews), wrap it inMaskedWidget.- Icons (
Icon, glyph fonts) are treated as neither text nor images and stay visible.
Check Status #
// Check if Session Replay is initialized
final isInitialized = await CxFlutterPlugin.isSessionReplayInitialized();
// Check if currently recording
final isRecording = await CxFlutterPlugin.isRecording();
Control Recording #
// Start recording
await CxFlutterPlugin.startSessionRecording();
// Stop recording
await CxFlutterPlugin.stopSessionRecording();
// Shutdown Session Replay
await CxFlutterPlugin.shutdownSessionReplay();
Capture Manual Screenshot #
await CxFlutterPlugin.captureScreenshot();
Get Session Replay Folder Path #
final folderPath = await CxFlutterPlugin.getSessionReplayFolderPath();
For more info check https://github.com/coralogix/cx-ios-sdk/tree/master/Coralogix/Docs.