farol_flutter 0.1.0 copy "farol_flutter: ^0.1.0" to clipboard
farol_flutter: ^0.1.0 copied to clipboard

Flutter auto-instrumentation for the Faro RUM client — lifecycle, errors, navigation.

farol_flutter #

Flutter auto-instrumentation for farol: app lifecycle events, cold-start timing, uncaught-error reporting, and route/view tracking — wired up with a single initialize() call.

Implements the Grafana Faro wire protocol. Not affiliated with or endorsed by Grafana Labs.

Install #

dependencies:
  farol: ^0.1.0
  farol_flutter: ^0.1.0

Or, to track this repo directly instead of pub.dev:

dependencies:
  farol:
    git:
      url: https://github.com/thegorangers/farol.git
      path: packages/farol
  farol_flutter:
    git:
      url: https://github.com/thegorangers/farol.git
      path: packages/farol_flutter

Usage #

import 'package:farol/farol.dart';
import 'package:farol_flutter/farol_flutter.dart';
import 'package:flutter/material.dart';

void main() {
  FaroFlutter.initialize(FaroConfig(
    collectorUrl: Uri.parse('https://faro-collector.example.com/collect'),
    app: const FaroApp(name: 'my-app', version: '1.0.0'),
    headersProvider: () async => {'x-api-key': await readApiKey()},
  ));

  runApp(MaterialApp(
    navigatorObservers: [FaroNavigatorObserver()],
    home: const HomePage(),
  ));
}

FaroFlutter.initialize calls Faro.initialize internally, then:

  • measures cold-start time (from initialize() to first frame),
  • reports app_background / app_foreground lifecycle events (flushing on background, so the batch isn't lost if the process is later killed),
  • installs FlutterError.onError and PlatformDispatcher.instance.onError hooks for uncaught error reporting (deduplicated across the two channels so a framework error and its rethrow-as-async-error aren't double-counted).

Add FaroNavigatorObserver() to navigatorObservers to get automatic setView/view_changed tracking on named-route push/pop/replace.

Call FaroFlutter.dispose() to remove the lifecycle observer (e.g. in tests or a hot-reload teardown path). Calling initialize() again after dispose() re-wires everything from scratch; calling it a second time without an intervening dispose() is a no-op — it does not re-apply a new config/options.

Signals overview #

FaroFlutter.initialize auto-collects the following, each individually toggleable via FaroFlutterOptions:

Signal Wire name(s) Option
Cold start cold_start measurement (cold_start_ms) always on
App lifecycle app_background / app_foreground events always on
Foreground resume foreground_resume measurement (foreground_resume_ms) collectForegroundResume
Uncaught errors pushError (FlutterError + PlatformDispatcher, deduplicated) always on
View/route changes view_changed event + setView via FaroNavigatorObserver
Time-to-initial-display ttid measurement (ttid_ms) collectTtid
Time-to-fully-displayed ttfd measurement (ttfd_ms) manual call, see below
Frame jank frames measurement (frames_total/frames_slow/frames_frozen/frame_delay_ms), context: {'view': ...} collectFrameJank / collectFrameJankInDebug
Rage taps rage_tap event (count) mount FaroRageTapDetector, see below

Options #

FaroFlutter.initialize(
  config,
  options: const FaroFlutterOptions(
    collectFrameJank: true,
    collectFrameJankInDebug: false,
    collectForegroundResume: true,
    detectRageTaps: true,
    collectTtid: true,
    longScreenFlushInterval: Duration(seconds: 30),
    slowFrameThreshold: Duration(microseconds: 16667),
    frozenFrameThreshold: Duration(milliseconds: 700),
    rageTapCount: 4,
    rageTapWindow: Duration(milliseconds: 1000),
    rageTapRadius: 48.0,
    rageTapCooldown: Duration(seconds: 3),
  ),
);

The values shown above are the defaults. collectFrameJank, collectForegroundResume, and collectTtid are wired directly by initialize(). detectRageTaps and the rageTap* fields are not hard-wired — see "Rage-tap mounting" below for why, and how to use them.

Debug-mode no-op for frame jank #

By default (collectFrameJankInDebug: false), the frame-jank collector is not installed in debug builds (kDebugMode), even when collectFrameJank: true. Debug-mode frame timings are dominated by JIT compilation and assertion overhead and would produce noisy, unrepresentative jank numbers. Set collectFrameJankInDebug: true to opt in anyway (e.g. to smoke-test the pipeline locally).

120Hz / high-refresh-rate caveat #

slowFrameThreshold's default (16667µs ≈ one 60Hz frame budget) assumes a 60Hz target. On a 90Hz/120Hz device a "slow" 60Hz-budget frame is a normal frame at the device's native refresh rate — pass a tighter slowFrameThreshold if you want jank to reflect the device's actual refresh rate rather than a fixed 60Hz baseline.

frames context attribution note #

frames measurements carry an explicit context: {'view': ...} rather than relying on Faro's ambient "current view" meta. addTimingsCallback batches are delivered on a delay (observed ~1s in release builds); by the time a batch arrives, the ambient current view may already be the next screen. Each frame is instead attributed to the view whose tracked interval actually contained the frame's vsync timestamp.

Rage-tap mounting #

FaroRageTapDetector is a widget, not something initialize() can wire into your tree automatically — mount it once, high in your widget tree (e.g. wrapping MaterialApp.builder):

MaterialApp(
  navigatorObservers: [FaroNavigatorObserver()],
  builder: (context, child) => FaroRageTapDetector(child: child!),
  home: const HomePage(),
)

FaroFlutterOptions.detectRageTaps and the rageTap* fields are not hard-wired into FaroRageTapDetector — the widget's own constructor parameters (count, window, radius, cooldown) are the single source of truth, set at the mounting site:

FaroRageTapDetector(
  count: options.rageTapCount,
  window: options.rageTapWindow,
  radius: options.rageTapRadius,
  cooldown: options.rageTapCooldown,
  child: child!,
)

This keeps FaroFlutterOptions a plain data holder (no hidden global wiring for a widget-shaped signal) — detectRageTaps is documentation of intent, not an implicit switch.

Wrap any widget where fast, repeated taps are legitimate (steppers, keypads, +/- buttons) in FaroRageTapIgnore so it's excluded from rage-tap detection without changing its hit-testing behavior:

FaroRageTapIgnore(
  child: IconButton(onPressed: increment, icon: const Icon(Icons.add)),
)

Time-to-fully-displayed (TTFD) #

TTID (above) measures "first frame of the new screen"; TTFD measures "the screen's real content — e.g. data loaded from the network — is actually on screen." Call FaroFlutter.reportFullyDisplayed() once your screen's meaningful content has rendered:

@override
void initState() {
  super.initState();
  loadData().then((_) {
    setState(() { /* ... */ });
    FaroFlutter.reportFullyDisplayed();
  });
}

reportFullyDisplayed() emits a ttfd measurement (ttfd_ms, measured from the view's startView timestamp) at most once per view — a second call before the next navigation is silently ignored, so callers don't need to guard against calling it from multiple code paths (e.g. a data-loaded callback and a timeout fallback both racing to call it). It's also a no-op if Faro isn't initialized.

Endpoint & auth model #

Identical to farol: collectorUrl + a pluggable headersProvider on FaroConfig for auth (API key, bearer token, etc). farol_flutter adds no auth of its own — it only feeds Flutter-specific signals into the same Faro instance.

No-PII rule #

  • FaroNavigatorObserver reports route.settings.name — use static route names, not raw user data, as route names (this is standard Flutter practice; avoid encoding user IDs or free text into route names).
  • Anonymous routes (settings.name == null) are skipped, not reported with a generated toString(), to avoid noisy/non-deterministic view names.
  • Uncaught-error reporting forwards error.toString() and the stack trace — don't throw exceptions whose toString() embeds PII.
  • As with farol, use FaroConfig.beforeSend for centralized scrubbing if needed.
1
likes
160
points
135
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter auto-instrumentation for the Faro RUM client — lifecycle, errors, navigation.

Repository (GitHub)
View/report issues

Topics

#observability #rum #faro #flutter

License

Apache-2.0 (license)

Dependencies

farol, flutter

More

Packages that depend on farol_flutter