Connect a Flutter app
- appId — the exact app slug, for example my-cool-app
- apiKey — the API key, which starts with hk_ (required in production)
- apiUrl — the hosted dashboard URL: https://saraayab.shop
The app-specific key is included in the mobile build and can be extracted. Treat it as public; never use your dashboard admin password in the app.
File paths are relative to your project root. Use the matching files in your app's folder structure.
Quick path — Flutter 3.13+, no plugin
Add the dependencies
your project/pubspec.yamlflutter pub add http shared_preferences flutter pub add device_info_plus package_info_plus
shared_preferences holds the persistent anonymous Tester ID and the offline event queue. http posts the telemetry.
Set the hosted dashboard URL and app values
Open a terminal at your project root, then run:
flutter run \ --dart-define=TESTER_API_URL=https://saraayab.shop \ --dart-define=TESTER_APP_ID=my-flutter-app \ --dart-define=TESTER_API_KEY=hk_your_key_here \ --dart-define=TESTER_APP_VERSION=1.0.3
Copy the API key from Developer portal → your app → Copy key (it looks like hk_.... The x-api-key header is required in production; it may be empty in local dev. appId is the exact appId from the Developer portal. Create the app there first.
Use https://saraayab.shop. HTTPS avoids Android cleartext HTTP blocking.
Create lib/tester_analytics.dart
your project/lib/tester_analytics.dartDownload the complete client Download the complete Flutter client and save it as lib/tester_analytics.dart. It creates the UUID v4 tester ID, sends register / app_opened, tracks screens, app foreground/background, crashes and bug reports, runs a ~60s heartbeat and queues events while offline.
Modify main.dart to boot the client
your project/lib/main.dartimport 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'tester_analytics.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await TesterAnalytics.init(
config: TesterConfig(
apiUrl: const String.fromEnvironment('TESTER_API_URL'),
appId: const String.fromEnvironment('TESTER_APP_ID'),
apiKey: const String.fromEnvironment('TESTER_API_KEY'),
appVersion: (await PackageInfo.fromPlatform()).version,
device: await _buildDevice(),
),
);
runApp(const MyApp());
}
Future<Map<String, dynamic>> _buildDevice() async {
final a = await DeviceInfoPlugin().androidInfo;
return <String, dynamic>{
'os': 'android',
'osVersion': a.version.release,
'systemVersion': a.version.sdkInt.toString(),
'deviceModel': a.model,
'deviceBrand': a.manufacturer,
'manufacturer': a.manufacturer,
'model': a.model,
'brand': a.brand,
};
}Track screens in main.dart
your project/lib/main.dartMaterialApp(
navigatorObservers: [TesterNavigatorObserver()],
initialRoute: '/home',
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (_) => HomeScreen(),
settings: settings,
),
);
// Or track a screen manually:
TesterAnalytics.setScreen('Checkout');Crashes, bug reports, opt-out and erase
your project/lib/main.dartTesterAnalytics.reportCrash('boom', stack.toString());
TesterAnalytics.reportBug(
'Checkout button does nothing',
params: <String, dynamic>{
'title': 'Checkout dead button',
'steps': 'Open cart -> tap Checkout',
'screenName': 'CartScreen',
},
);
Future<void> testerPrivacyExample() async {
final id = TesterAnalytics.testerId; // show this in Settings if needed
await TesterAnalytics.setContactName('Sara');
await TesterAnalytics.setOptedOut(true);
await TesterAnalytics.eraseMyData();
}Crash capture is automatic: the client installs FlutterError.onError and PlatformDispatcher.instance.onError. Wrap runApp in runZonedGuardedto catch errors outside the Flutter zone.
Run against the hosted dashboard
Open a terminal at your project root, then run:
flutter run \ --dart-define=TESTER_API_URL=https://saraayab.shop \ --dart-define=TESTER_APP_ID=my-flutter-app \ --dart-define=TESTER_API_KEY=hk_your_key_here \ --dart-define=TESTER_APP_VERSION=1.0.3
- Open the app once on the device or emulator.
- Events appear under the app you created.
- The tester shows as Active with version, device, opens, and activity history.
Local testing only: For local-only testing, use http://10.0.2.2:3000 on the Android emulator and allow cleartext HTTP in a debug build. Never use cleartext HTTP in production.
Payload reference
POST {apiUrl}/api/events with the header x-api-key: <app api key>.
- appId — required, lowercase app slug (1–40 letters, numbers, or hyphens)
- testerId — required, UUID v4
- device — strict object: only os, osVersion, systemVersion, deviceModel, deviceBrand, manufacturer, model, brand
- screens — ≤ 10 items, ≤ 80 chars each; message ≤ 500; stack ≤ 2600
- params — values must be string / number / boolean
Events: register, app_opened, app_foreground, app_background, screen_view, crash, bug_report, session_start, session_resume, session_pause, session_heartbeat
Active usage tracking
The client only sends signals (session_start, session_resume, session_pause, session_heartbeat). The server measures the elapsed active time with its own clock — it never trusts client timestamps. Background time is excluded because a pause/close signal ends the measured interval.
Force-close / crash / no network:sessions auto-close after a no-signal window, so a lost pause or heartbeat can't inflate totals. Duplicate signals are ignored and a single continuous session is capped to a maximum length.
View it:theUsagetab shows today's totals, per-tester daily history, and date-range sums.
Privacy & opt-out
The client uses a stable anonymous Tester ID (a random UUID stored in app storage) — no name, email, phone, or personal information is required. The only optional personal fields are the contact name/channel the tester chooses to enter.
Testers can opt out at any time with setOptedOut(true), and can erase their local + server-side data with eraseMyData().
Privacy policy for a temporary testing app
Add a clear, accurate disclosure to your privacy policy and store listing. Adapt this sample to your actual data, test period, hosting and retention:
During this app's [closed/open testing period], [developer/organization] uses TesterFlow to evaluate test builds, diagnose crashes and bug reports, and understand test-session activity. The app sends a random tester ID, app version, device information, screen/activity events, and foreground session signals to [hosting provider/region]. Crash details and bug-report information are sent when generated or submitted. Testers may optionally provide contact details. This data is used only to operate this testing program, not for advertising or production-user analytics. Collection will stop when [test end condition/date]. Data is retained for [actual retention period]. To stop future collection, use [in-app opt-out instructions]; to request deletion, use [erase instructions or contact details].
Only claim the collection is temporary or not used in production if that is true and you will stop or change collection when testing ends. TesterFlow does not automatically delete data when a test ends. Keep the privacy policy, Play Store listing/Data safety disclosures, and app behavior consistent. This sample cannot guarantee Google Play approval; check current Play policies for your app and testing track.
Troubleshooting
- 401 Missing or invalid x-api-key header — the dashboard runs in production mode; pass TESTER_API_KEY.
- 400 testerId must be a valid UUID v4 — the stored ID is corrupt; clear app data or call eraseMyData().
- 400 device: Unrecognized key — the device object is strict; only send the 8 allowed keys.
- 429 Too many requests — 120 requests/minute per key; keep the heartbeat at 60s.
- Reinstall → new Tester ID. The ID lives in app storage; reinstalling or clearing app data creates a new ID.