Skip to content
Telemetry

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.

Product event
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,
  },
);
01

Keep events structured

  • eventType is the required, stable event name, such as checkout_started.
  • level defaults to BtxLogLevel.info; use warning for a recoverable problem and error for a failed operation.
  • message defaults to the event name. Supply a concise description when it adds useful detail.
  • properties defaults to an empty map. Use strings, numbers, booleans, null, lists, and maps with string keys.
  • occurredAt is optional. Supply the original event time when recording an event later; the SDK converts it to UTC.
Original event time
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.
02

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.

Error with stack trace
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.

03

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.

04

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.

Logs-only integration
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.

05

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:

Explicit flush
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.

06

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.

07

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.

Worker telemetry
await Btx.configure(
  BtxConfiguration(
    publishableClientKey: 'cfk_...',
    features: const <BtxFeature>{BtxFeature.logs},
    telemetry: const BtxTelemetryConfiguration(
      bindAppLifecycle: true,
      runtimeMode: BtxTelemetryRuntimeMode.worker,
    ),
  ),
);
08

Troubleshoot missing events

  • Check Btx.status.kind after awaited configuration and identity work. inactive needs a customer identity; disabled includes disableReason.code and disableReason.message.
  • For invalidPublishableClientKey, check the configured key. For appContextMismatch, 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 active status describes the runtime, not acknowledgement of a particular event.
  • If the event appears without a trace, check the exact stack property 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.