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.
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, errorEach 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/symbolsholds oneapp.<os>-<arch>.symbolsfile per architecture. Upload them all, from the same build you ship: each is matched by the snapshot's build ID.--save-obfuscation-mapwrites 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 likea5). Anobfuscation.jsonin 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 theartifacts:writescope — 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_idheader.
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: [ /* … */ ]);
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 afterUser 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
| Option | Default | Notes |
|---|---|---|
key | — | Required. |
service | — | service.name |
environment | none | deployment.environment.name |
release | The app's version and build | e.g. com.shop.app@2.3.0+41 |
endpoint | https://api.zharp.io/api/v1/otlp | |
tracing | true | false is logs only |
tracingOrigins | [] | Hosts (api.example.com, *.example.com), origins or RegExps |
sampleRate | 1.0 | Share of traces kept; errors and logs are always sent |
enabled | true | e.g. kReleaseMode |
captureErrors | true | |
reportSilentFlutterErrors | false | |
sessionTimeout | 30 minutes | In the background or idle |
debug | false | Print 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
initcalled, 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,
servicedecides the app — see Keys. - Data is sent in batches every few seconds. A process that exits straight away should call
flushfirst. - Logs and errors arrive but no traces? Check that tracing isn't turned off — see Logs only.
- On an Android emulator,
localhostis the emulator itself; a local Zharp is at10.0.2.2. - Pass
debug: trueto 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.