SDKs and API

Flutter

Install rabolic_flutter for iOS, Android and Flutter web. Configuration, lifecycle events, screens with Navigator, go_router and auto_route, identity, in-app purchase tokens, deep links, consent, app store privacy forms and limits.

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

Terminal
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

lib/main.dart
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.

CallWhat 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.anonymousIdThis install's anonymous id (UUIDv4), or null until init completes
Rabolic.appAccountTokenA 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:

EventWhenProperties
$app_first_openFirst launch after installNone
$app_updateFirst launch on a new app versionprevious_version
$app_openLaunch, and every return to the foregroundfrom_background
$app_backgroundThe app leaves the foregroundforeground_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 doesn't receive App Store or Google Play transactions yet, so it can't link store purchases to the person from this token today. To record revenue now, report verified purchases from your server with @rabolic/node's purchase, renewal and refund.

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's rb_aid parameter, which the web SDK's appLink() adds. Rabolic stores it but doesn't join the web visitor to the app user from it yet; identify with the same user id on web and app does;
  • context.campaign, from utm_source, utm_medium, utm_campaign, utm_content, utm_term and 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.

StateBehaviour
granted (default)Tracks and sends normally
pendingKeeps new events in memory only (never on disk) and sends nothing. Switching to granted sends them; denied drops them
deniedDrops 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

LimitValue
Event name1–200 characters
Properties255 keys per level, nesting depth 5, keys up to 255 characters
Serialized event32 KB (32,768 bytes of UTF-8 JSON). Larger events are dropped; the server applies the same limit
User id1–255 characters
Queue10,000 events on iOS and Android, 1,000 on the web. When it is full, the oldest events are dropped
Event ageEvents 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
Batch500 events or 900 KB. Bodies over 1 KB are gzipped (not on the web)
RequestsOne 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.timezone is left out, because Dart only exposes ambiguous abbreviations such as CET. The web sends the IANA name.
  • Web page close: browsers may cancel a request when the tab closes. Unsent events stay in localStorage and go out on the next visit.
  • Android Auto Backup restores shared_preferences on 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.