Flutter SDK
Log product telemetry from Flutter
Queue structured customer events with automatic runtime context, lifecycle flushes, and durable retry behavior.
Log a product event
Configure BTX at app startup and identify the customer with a stable external ID. The default feature set includes logs; telemetry-only apps can use the configuration below.
Btx.log(...) queues a structured event for the active customer. It waits for
pending configure and identity work, persists the entry, and schedules delivery.
Awaiting it does not wait for server acknowledgement.
await Btx.log(
'checkout_started',
level: BtxLogLevel.info,
message: 'Customer started checkout.',
properties: <String, Object?>{
'cartId': 'cart_123',
'itemCount': 3,
'subtotalUsd': 149.97,
'couponApplied': true,
},
);Keep events structured
eventTypeis the required, stable event name, such ascheckout_started.leveldefaults toBtxLogLevel.info; usewarningfor a recoverable problem anderrorfor a failed operation.messagedefaults to the event name. Supply a concise description when it adds useful detail.propertiesdefaults to an empty map. Use strings, numbers, booleans, null, lists, and maps with string keys.occurredAtis optional. Supply the original event time when recording an event later; the SDK converts it to UTC.
await Btx.log(
'device_sync_completed',
occurredAt: syncCompletedAt.toUtc(),
properties: <String, Object?>{'itemCount': 12},
);- Keep event names and property shapes stable between releases.
- Prefer product IDs and bounded values over free-form state dumps.
- Do not include passwords, access tokens, server credentials, or sensitive payloads.
- Use
Btx.flush(...)only for rare lifecycle or diagnostic waits; normal delivery is automatic.
Attach a stack trace
Capture both values in Dart's catch (error, stack) and pass
stack.toString() as the top-level stack entry in properties. Replace
connectBluetooth() with the operation your app performs.
try {
await connectBluetooth();
} catch (error, stack) {
await Btx.log(
'bluetooth_connection_failed',
level: BtxLogLevel.error,
message: error.toString(),
properties: <String, Object?>{
'stack': stack.toString(),
},
);
}BTX uses that string to populate the event's Stack trace section. An
error level or exception message alone does not capture a trace. A missing or
blank stack value shows No stack trace captured. A property named
stackTrace, or a stack nested inside another property, does not populate
that section.
Use the trace received in catch to preserve the failure location.
StackTrace.current describes the logging location instead. Btx.log(...)
does not install a global uncaught-error handler; report errors explicitly from
your app's existing error handling when needed. Keep your app's normal
recovery or rethrow behavior after logging.
Keep payloads bounded
The current Flutter SDK applies these limits before queueing an event:
- Event names: 128 characters. Messages: 2,048 characters. Both are trimmed; an empty event name or message skips the event.
- Property keys: 128 characters. String values, including
stack: 4,096 characters. Longer strings are truncated. - Maps and lists: up to 100 entries each, with up to eight levels of property values. Deeper values are omitted.
- Properties: 16 KiB of encoded JSON after normalization. If exceeded, the event keeps its name, level, message, and time but its properties become empty, including any stack trace.
Keep traces and diagnostic context focused. Remove sensitive data before logging; payload normalization does not redact credentials or personal data.
Configure telemetry only
Apps that do not need customer-facing surfaces can enable only logs. Feature flag state still loads through the telemetry session for an identified customer.
await Btx.configure(
BtxConfiguration(
publishableClientKey: 'cfk_...',
features: const <BtxFeature>{BtxFeature.logs},
),
);Identify the customer after configuration. No messenger widget is needed for telemetry-only apps.
Understand delivery
Logs persist in a durable queue across runtime recreation. Batches contain up
to 100 events and at most 1 MiB of UTF-8 JSON, including the request envelope.
The SDK sends a batch when it fills or its oldest event has waited 60 seconds.
New events do not reset that deadline. Brief focus loss (inactive) preserves
the queue and deadline. An app transition to background with
lifecycle binding enabled, an identity or session change, disposal, or an explicit
flush can send a smaller batch. Network availability, retry backoff, and OS
suspension can delay delivery.
The queue retains up to 1,000 events; when full, older events are removed to make
room for newer ones. Temporary network, rate-limit, and server failures retry
with bounded backoff. Permanent key, app-context, or request errors pause
delivery and appear through Btx.status.
For an occasional diagnostic wait, use:
await Btx.flush(timeout: const Duration(seconds: 5));This waits for a flush attempt, with a timeout that can throw. It does not guarantee delivery while identity is unavailable, delivery is disabled, or retry backoff is active. Normal logging does not need an explicit flush.
Use automatic runtime context
BTX automatically captures privacy-safe app, platform, operating-system, device-family or model, physical-device, and SDK version context. Android also captures API level and security patch when available.
The SDK intentionally excludes advertising identifiers, persistent phone IDs, serial numbers, user-assigned device names, storage readings, and memory readings. Metadata collection is best effort and never blocks configuration.
Use worker mode for telemetry only
Select worker mode when telemetry collection and delivery should run on a worker isolate. Customer messages, knowledge base UI, and push integration stay on the main isolate.
await Btx.configure(
BtxConfiguration(
publishableClientKey: 'cfk_...',
features: const <BtxFeature>{BtxFeature.logs},
telemetry: const BtxTelemetryConfiguration(
bindAppLifecycle: true,
runtimeMode: BtxTelemetryRuntimeMode.worker,
),
),
);Troubleshoot missing events
- Check
Btx.status.kindafter awaited configuration and identity work.inactiveneeds a customer identity;disabledincludesdisableReason.codeanddisableReason.message. - For
invalidPublishableClientKey, check the configured key. ForappContextMismatch, check that the key matches the app's iOS bundle ID or Android package name. - For
telemetryRequestRejected, inspect the disable reason and correct the rejected request or session configuration. Repeated flush calls do not clear a disabled state. - Allow time for automatic delivery and retries. An
activestatus describes the runtime, not acknowledgement of a particular event. - If the event appears without a trace, check the exact
stackproperty name, a nonempty string value, and the payload limits above.
Use Btx.statusListenable when your diagnostic UI needs to observe status
changes. See the API reference for public
status and configuration types.