hindsight_flutter 0.9.8
hindsight_flutter: ^0.9.8 copied to clipboard
Session replay for Flutter tree/state-delta capture with compact scene-graph encoding.
hindsight_flutter #
rrweb-style session replay for Flutter: the SDK captures the app as a structured scene graph (text, visuals, scroll containers, overlays) and streams compact deltas to the hindsight platform; the companion web player reconstructs the session pixel-faithfully, including scrolling, route transitions and keyboards.
Quick start #
Future<void> main() async {
await Hindsight.init(HindsightConfig(
projectId: 'proj_abc123', // the proj_… id from your dashboard
apiKey: 'hs_live_xxx', // the hs_… ingest key for that project
));
runApp(HindsightWidget(child: const MyApp()));
}
That's it — sessions stream to the hindsight platform. Business customers
with a dedicated ingest or replay environment point ingestUrl /
replayBaseUrl at their own endpoints.
Route changes are captured by adding the SDK's observer to your navigator:
MaterialApp(
navigatorObservers: [Hindsight.navigatorObserver],
)
An observer can only attach to one navigator, so nested navigators (tab scaffolds, auth shells, ...) each get their own:
Navigator(
observers: [Hindsight.createNavigatorObserver()],
)
Runtime controls #
All controls live on the static Hindsight surface:
Hindsight.identify('user-42', attributes: {'plan': 'pro'});
Hindsight.addEvent('checkout_started', {'cart_total': 42900});
await Hindsight.flush();
// Deep link to this session in the replay viewer — attach it to error
// reports, support tickets, logs.
final url = Hindsight.replayUrl; // <base>/?session=<sid>&project_id=<pid>
// Error marker on the replay timeline + forced flush, so the moment
// survives a crashing app.
Hindsight.captureError(error, stackTrace: stack, handled: false);
// Foreign events (analytics, navigation, network, ...) as breadcrumbs.
Hindsight.addBreadcrumb('http', {'url': '/cart', 'status_code': 500});
// Config can change at runtime; identity-level changes (userId,
// externalSessionId, ...) apply in place, pipeline-level changes restart
// the recording session.
Hindsight.updateConfig(HindsightConfig(
projectId: 'proj_abc123',
apiKey: 'hs_live_xxx',
userId: 'user-42',
));
Vendor adapters #
replayUrl / captureError / addBreadcrumb are the hooks vendor
adapter packages build on, so error reports in your existing tooling
deep-link straight to the replay. hindsight_sentry
is the reference adapter:
options.addIntegration(HindsightSentryIntegration());
Adapters resolve the Hindsight singleton when their host SDK starts
them; Datadog / PostHog / Mixpanel adapters follow the same shape.
If your app uses flutter_svg, install the optional
hindsight_flutter_svg
adapter before Hindsight.init() to capture bundled SvgPicture.asset
icons as vector UI chrome:
HindsightFlutterSvg.install();
await Hindsight.init(config);
Advanced: explicit clients #
The static surface fronts a single HindsightClient. Tests, harnesses
and multi-client setups can construct clients directly and inject them,
bypassing the singleton entirely:
final client = HindsightClient(config: HindsightConfig(...));
runApp(HindsightWidget(client: client, child: const MyApp()));
// Descendants reach the mounted client without a global:
final client = HindsightWidget.of(context); // throws without an ancestor
final maybe = HindsightWidget.maybeOf(context); // null without an ancestor
Privacy #
Hindsight masks only what must be masked: static UI text is captured as-is
so replays stay legible, while everything the user types is masked by
default (maskAllInputs: true). Obscured fields (passwords/PINs) are always
masked, with no opt-out.
Redact sensitive static content, or reveal a specific subtree:
HindsightRedact(redactText: true, child: CreditCardSummary()) // mask this subtree
HindsightPrivacyScope(redactText: false, child: PublicBanner()) // opt out
Hindsight.mask<TransactionRow>(); // mask by widget type
Tune the global defaults via HindsightPrivacyOptions:
const HindsightConfig(
projectId: 'proj_...',
apiKey: 'hs_...',
privacy: HindsightPrivacyOptions(
maskAllText: false, // default: static text visible
maskAllInputs: true, // default: typed input masked (passwords always)
),
);
Sampling #
HindsightConfig.sessionSampleRate decides once per session start whether
the session records at all; sampled-out sessions cost nothing.
How capture works (overview) #
- One self-contained
fullsnapshot at start and everytreeFullSnapshotInterval(default 60s) — a lost chunk only costs the deltas up to the next full. - Between fulls, per-capture deltas: upserts/patches/removes plus scroll rebases (one translation per container instead of per-node patches) and payload-level fractional ordering keys.
- Scroll positions stream as compact per-container traces
(
[epoch_ms, pixels]pairs); the player interpolates between keyframes and replays scroll motion from the traces.