boltZharp Docs
smartphoneSDKs

Flutter

zharp_flutter sends a Flutter app's logs, crashes and errors with the session and device they came from, and traces its screens and HTTP requests into your API.

info
zharp_flutter 0.1.0

This SDK is new, and its API may still change before 1.0. Requires Flutter 3.19 or later (Dart 3.3).

Install

flutter pub add zharp_flutter

Initialize

Before runApp:

import 'package:flutter/material.dart';
import 'package:zharp_flutter/zharp_flutter.dart';

Future<void> main() async {
  await Zharp.init(ZharpOptions(
    key: "zpk_…",
    service: "shop-app",
    environment: "production",
    tracingOrigins: ["api.example.com"], // your API: its requests carry traceparent
  ));
  runApp(const MyApp());
}

The key is safe to ship in the app: it can only send. Turn the SDK off in debug builds with enabled: kReleaseMode.

Logs

Zharp.log.info('Checkout opened', {'items': 3});   // debug, info, warn, error

Each log carries the session, user, device, current screen and trace. print and debugPrint aren't sent.

Logs only

If you only want logs, turn tracing off. Logs, errors, and the user and session they belong to are still sent; no spans are recorded and no trace headers are added to outgoing requests.

await Zharp.init(ZharpOptions(
  key: "zpk_…",
  service: "shop-app",
  tracing: false,
));

The logs show on the Logs page under the service name, and on the app's Logs tab with the user and session of each line. The key comes from an app — see Get a key. To add tracing later, remove the option; nothing else changes.

Errors

Uncaught Flutter and platform errors are sent with their Dart stack, as unhandled — init hooks Flutter's and the platform's error handlers, keeping any you set earlier. Report one you caught yourself:

try {
  await checkout(cart);
} catch (error, stack) {
  Zharp.captureError(error, stackTrace: stack, attributes: {'orderId': order.id});   // handled
}

Errors in your own zones

For errors in zones you create, run the app inside Zharp.guard:

void main() {
  Zharp.guard(() async {
    await Zharp.init(ZharpOptions(key: "zpk_…", service: "shop-app"));
    runApp(const MyApp());
  });
}

Readable stack traces

Release builds made with --obfuscate --split-debug-info report stacks as addresses. Upload the build's symbols and Zharp shows — and groups — those errors by your original functions, files and lines:

flutter build apk --obfuscate --split-debug-info=build/symbols \
  --extra-gen-snapshot-options=--save-obfuscation-map=build/obfuscation.json     # or appbundle, ipa, …
npx @zharp/cli dart-symbols upload build/symbols --app <application id>
  • build/symbols holds one app.<os>-<arch>.symbols file per architecture. Upload them all, from the same build you ship: each is matched by the snapshot's build ID.
  • --save-obfuscation-map writes the map of obfuscated names; with it, Zharp also restores the class and method names in error types and messages (otherwise your own error class reads like a5). An obfuscation.json in or next to the symbols folder is found and uploaded with the symbols; pass --obfuscation-map <file> for another place.
  • The upload needs ZHARP_AUTH_TOKEN, a workspace key with the artifacts:write scope — CI only, never in the app. See The upload key.
  • Nothing changes in the app: zharp_flutter sends the stack as the Dart VM writes it, with its build_id header.

Tracing

Screens

Add ZharpNavigatorObserver. Each named page route becomes a screen <name> span lasting until the screen is drawn and its requests have finished, and everything recorded meanwhile carries the screen name.

MaterialApp(
  navigatorObservers: [ZharpNavigatorObserver()],
  // …
);

// go_router
GoRouter(observers: [ZharpNavigatorObserver()], routes: [ /* … */ ]);
info

Routes without a name aren't recorded, and taps aren't recorded as spans.

HTTP

Requests become spans only through Zharp's client or interceptor. Requests to hosts in tracingOrigins carry traceparent and baggage, so your API's spans join the same trace; other hosts get no extra headers.

// package:http
final client = ZharpHttpClient();           // or ZharpHttpClient(yourClient) to wrap one
final response = await client.get(Uri.parse("https://api.example.com/orders"));

Dio

flutter pub add zharp_dio

final dio = Dio()..interceptors.add(ZharpDioInterceptor());

Spans of your own

await Zharp.startSpan('load orders', (span) async {
  await api.loadOrders();
});

Zharp.setTag('plan', 'pro');   // on everything after

User context

Zharp.setUser(id: user.id, email: user.email);   // once someone signs in
Zharp.clearUser();                                 // on sign-out: also starts a new session
  • A session ID on everything: a new session after 30 minutes in the background or idle, and on clearUser().
  • Device model and manufacturer, OS and version, and a device.id — a random ID created on first launch and kept in the app's storage. It isn't a hardware or advertising ID, and it resets on reinstall.

Options

OptionDefaultNotes
key—Required.
service—service.name
environmentnonedeployment.environment.name
releaseThe app's version and builde.g. com.shop.app@2.3.0+41
endpointhttps://api.zharp.io/api/v1/otlp
tracingtruefalse is logs only
tracingOrigins[]Hosts (api.example.com, *.example.com), origins or RegExps
sampleRate1.0Share of traces kept; errors and logs are always sent
enabledtruee.g. kReleaseMode
captureErrorstrue
reportSilentFlutterErrorsfalse
sessionTimeout30 minutesIn the background or idle
debugfalsePrint what the SDK does

Telemetry is batched every 5 seconds or 100 records, gzipped, retried with backoff, kept in memory (capped) while offline, and sent when the app goes to the background. Zharp.flush() sends now. The SDK never throws into your app.

Troubleshooting

Nothing arrives

On the app's setup page (App Telemetry → the app → settings → Setup instructions), the verify step shows the first event as soon as it lands, and Send a test event checks the key on its own. If the test event arrives but your app's don't, the SDK isn't starting or isn't reaching Zharp:

  • Is init called, and before the code you expect to see? Without a key, the SDK starts nothing and says so once.
  • Does the service name match? With an app key everything goes to that app whatever it says; with a workspace key, service decides the app — see Keys.
  • Data is sent in batches every few seconds. A process that exits straight away should call flush first.
  • Logs and errors arrive but no traces? Check that tracing isn't turned off — see Logs only.
  • On an Android emulator, localhost is the emulator itself; a local Zharp is at 10.0.2.2.
  • Pass debug: true to print what the SDK does and why something wasn't sent.

401 or 403

Ingest answers 401 when the key is missing, mistyped, revoked or rolled, and 403 when the request comes from an origin or IP address the key doesn't allow. Neither is retried: the SDK warns once and drops what was refused, and it never throws into your code. Copy the key again from the app's settings.