Flutter
rabolic_flutter is one pure-Dart SDK for iOS, Android and Flutter web, with no platform channels of its own. Events survive restarts and offline periods in a persisted queue, and are sent in batches with retry and backoff. Package: pub.dev/packages/rabolic_flutter. Source: github.com/rabolic/sdks/flutter.
Use the project's publishable key (rb_pub_…).
Install
flutter pub add rabolic_flutter
The SDK needs Dart 3.10 and Flutter 3.38 or later. npx rabolic init also adds the package and prints the init call for you.
Initialize
import 'package:rabolic_flutter/rabolic_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Rabolic.init(const RabolicConfig(projectKey: 'rb_pub_…'));
runApp(const MyApp());
}
// Anywhere in the app:
Rabolic.track('workout_started', properties: {'program_id': 'strength-30'});
You don't have to await init. Calls made while the SDK is loading are kept, with their original timestamps, and sent once it has loaded.
Turn on debug: true while you integrate. Every call and response is logged, and events appear on the dashboard's Live page within seconds.
Configuration
RabolicConfig(
projectKey: 'rb_pub_…', // required: the publishable key
host: 'https://ingest.rabolic.com', // or your first-party proxy
flushAt: 20, // send when this many events are queued
flushInterval: Duration(seconds: 15), // default: 15 s on mobile, 5 s on web
maxQueueSize: 10000, // default: 10,000 on mobile, 1,000 on web
trackLifecycle: true, // $app_* events (iOS and Android)
consent: RabolicConsent.granted, // starting consent state
debug: false, // log every call
)
Remote config is fetched from GET /v1/config at start. It is cached on the device and refreshed when the app returns to the foreground and the cache is more than 5 minutes old. Tracking never waits for it. Today the server sets only the session timeouts there, from your project settings.
API
All calls are static on Rabolic. None of them throws into your app: invalid input is logged (with debug: true) and ignored.
| Call | What it does |
|---|---|
Rabolic.init(RabolicConfig) | Starts the SDK. Call once. A second call is ignored |
Rabolic.track(name, {properties}) | Sends a custom event. Use snake_case, 1–200 characters. Names may not start with $ |
Rabolic.screen(name, {properties}) | Sends $screen, with the name in context.screen_name and properties.name |
Rabolic.identify(userId, {traits}) | Sets user_id on this and every later event, and sends $identify with traits |
Rabolic.reset() | Logout. Creates a new anonymous id and session, and clears the user id, super properties and appAccountToken |
Rabolic.register(props) / Rabolic.unregister(key) | Super properties, added to every event and kept across launches. Event properties win over them |
Rabolic.setConsent(RabolicConsent) | Changes consent at runtime |
Rabolic.optOut() / Rabolic.optIn() | Short for setConsent(denied) / setConsent(granted) |
Rabolic.flush() | Sends queued events now |
Rabolic.handleDeepLink(Uri) | Sends $deep_link |
Rabolic.anonymousId | This install's anonymous id (UUIDv4), or null until init completes |
Rabolic.appAccountToken | A stable UUID for the identified person, for in-app purchases |
Rabolic.close() | Stops the SDK, mostly for tests |
Property values may be strings, numbers, booleans, null, lists and maps. DateTime values are sent as RFC 3339 strings, enums as their name and Uris as strings. Any other value is dropped.
Screens
Add the observer to MaterialApp:
MaterialApp(
navigatorObservers: [RabolicNavigatorObserver()],
// …
);
go_router
Pass the observer to GoRouter. Screens are named after the route's name, or its path pattern (/users/:id) when it has none:
final router = GoRouter(
observers: [RabolicNavigatorObserver()],
routes: [
GoRoute(path: '/', name: 'home', builder: (_, __) => const HomeScreen()),
GoRoute(path: '/users/:id', builder: (_, state) => UserScreen(state.pathParameters['id']!)),
],
);
A ShellRoute or StatefulShellBranch has its own navigator, so give it its own observer: ShellRoute(observers: [RabolicNavigatorObserver()], …). A NavigatorObserver can only watch one navigator at a time, so don't reuse one instance.
auto_route
MaterialApp.router(
routerConfig: _appRouter.config(
navigatorObservers: () => [RabolicNavigatorObserver()],
),
);
Screens are named after the route name (HomeRoute). Tab changes inside AutoTabsRouter don't go through a navigator. Call Rabolic.screen for those yourself.
Naming and filtering screens
By default the observer tracks only PageRoutes, so dialogs, sheets and menus don't count, and it uses RouteSettings.name. Both are configurable:
RabolicNavigatorObserver(
nameExtractor: (settings) => settings.name?.replaceFirst('/', ''),
routeFilter: (route) => route is PageRoute && route.settings.name != '/splash',
);
Routes without a name are skipped. Use MaterialPageRoute(settings: RouteSettings(name: 'Checkout'), …) or named routes.
Automatic events
On iOS and Android, with trackLifecycle: true:
| Event | When | Properties |
|---|---|---|
$app_first_open | First launch after install | None |
$app_update | First launch on a new app version | previous_version |
$app_open | Launch, and every return to the foreground | from_background |
$app_background | The app leaves the foreground | foreground_ms |
A launch straight into the background (a push or background fetch) sends $app_open only when the app is first shown. Queued events are sent when the app goes to the background.
Sessions
- iOS and Android: a session starts at launch, and again when the app returns after more than 30 seconds in the background. The project setting can change the 30 seconds. Time in the background never counts as active time.
- Web: a session starts after 30 minutes without an event, or when the page is opened with campaign parameters that differ from the current session's. Tabs share the session.
reset()always starts a new session.
Identity
Call Rabolic.identify(userId) after signup and every login, with the same user id you use on the web and your server. Call Rabolic.reset() at logout. See Identity.
In-app purchases
Rabolic.appAccountToken is a stable UUID for the identified person. Pass it to in_app_purchase as applicationUserName:
final purchase = PurchaseParam(
productDetails: product,
applicationUserName: Rabolic.appAccountToken,
);
await InAppPurchase.instance.buyNonConsumable(purchaseParam: purchase);
The App Store stores it as the transaction's appAccountToken and Google Play as obfuscatedAccountId. It is created by identify, kept across launches, changes when a different user is identified, and is cleared by reset(). It is also sent as the app_account_token trait on $identify.
@rabolic/node's purchase, renewal and refund.Deep links
Call Rabolic.handleDeepLink with every link that opens the app. With app_links:
AppLinks().uriLinkStream.listen(Rabolic.handleDeepLink);
The SDK sends $deep_link with:
url;linked_anonymous_id, from the link'srb_aidparameter, which the web SDK'sappLink()adds. Rabolic stores it but doesn't join the web visitor to the app user from it yet;identifywith the same user id on web and app does;context.campaign, fromutm_source,utm_medium,utm_campaign,utm_content,utm_termand the click ids (gclid,fbclid,ttclid,msclkid).
The full URL is sent. If your links can carry secrets, such as magic-link login tokens, strip them before you pass the Uri.
Consent
| State | Behaviour |
|---|---|
granted (default) | Tracks and sends normally |
pending | Keeps new events in memory only (never on disk) and sends nothing. Switching to granted sends them; denied drops them |
denied | Drops every event and clears the queue on disk |
Pass the starting state as RabolicConfig.consent. A state you set at runtime (setConsent, optIn, optOut) is stored on the device and wins over RabolicConfig.consent on later launches.
Privacy
The SDK never collects advertising ids (IDFA, GAID), the Android id, form field values, the clipboard, location or device fingerprints.
The device context is limited to the platform; app version, build and bundle id; device model, manufacturer and type; OS name and version; locale and time zone; and screen size and density. On the web, it also sends the user agent. The anonymous id is a random UUID created on the device.
iOS privacy manifest
The SDK has no native code, so it has no PrivacyInfo.xcprivacy of its own. The plugins it uses (shared_preferences, path_provider, package_info_plus and device_info_plus) ship their own manifests. In your app's PrivacyInfo.xcprivacy, declare the data Rabolic collects for you:
- Product Interaction: analytics, not used for tracking.
- Device ID: the install's anonymous id. Analytics, not used for tracking.
- User ID: only if you call
identify. Analytics, linked to the user, not used for tracking.
NSPrivacyTracking stays false, and App Tracking Transparency isn't needed.
Google Play Data safety
Declare:
- App activity → App interactions: collected, for analytics.
- Device or other IDs: the anonymous install id, for analytics.
- Personal info → User IDs: only if you call
identify.
Data is encrypted in transit (HTTPS). Users can ask you to delete their data; delete the person in Rabolic (see Users).
Limits
| Limit | Value |
|---|---|
| Event name | 1–200 characters |
| Properties | 255 keys per level, nesting depth 5, keys up to 255 characters |
| Serialized event | 32 KB (32,768 bytes of UTF-8 JSON). Larger events are dropped; the server applies the same limit |
| User id | 1–255 characters |
| Queue | 10,000 events on iOS and Android, 1,000 on the web. When it is full, the oldest events are dropped |
| Event age | Events older than 7 days are dropped before sending. The server also rejects events older than 30 days and corrects device clocks that are off by more than 30 s |
| Batch | 500 events or 900 KB. Bodies over 1 KB are gzipped (not on the web) |
| Requests | One at a time, 10 s timeout. Backoff from 1 s to 5 min with jitter. Retry-After is honored |
Known limitations:
- Desktop (macOS, Windows, Linux) isn't supported yet. There, every call does nothing, and debug builds log one line saying so.
- Time zone: on iOS and Android,
context.timezoneis left out, because Dart only exposes ambiguous abbreviations such asCET. The web sends the IANA name. - Web page close: browsers may cancel a request when the tab closes. Unsent events stay in
localStorageand go out on the next visit. - Android Auto Backup restores
shared_preferenceson reinstall, so a restored install keeps its anonymous id and doesn't send$app_first_open. - One client per process: the queue file is shared, so don't run two
RabolicClients at once in an app.
React Native and Expo
Install @rabolic/react-native in Expo or bare React Native apps for iOS and Android. Options, app lifecycle events, screens with React Navigation and Expo Router, identity, deep links, consent and limits.
Node.js
Install @rabolic/node to send server-side events and verified revenue (purchases, renewals, refunds) with a secret key. Options, identity, idempotency, Dodo Payments checkout metadata, delivery and limits.