replayfy_flutter 0.0.10
replayfy_flutter: ^0.0.10 copied to clipboard
Session replay, product analytics and error monitoring for your Flutter apps on iOS and Android.
Replayfy for Flutter #
Session replay, product analytics & error monitoring for your Flutter apps — iOS and Android.
Replayfy records what your users actually experience — every screen, tap, and error — so you can see how your product is really used, understand drop-off, and fix bugs from a real reproduction instead of a guess.
Features #
- Session replay — pixel-accurate playback of real user sessions on iOS and Android.
- Product analytics — funnels, custom events, and user journeys from
identifyandtrack. - Error & crash monitoring — automatic capture of unhandled errors and crashes, plus manual
captureException. - Automatic capture — screens, taps, network requests, console output, performance vitals, and device info, out of the box.
- Privacy-first masking — cover, blur, or pixelate any sensitive widget; platform views (WebView, maps, video) are covered for you; nothing sensitive leaves the device.
- Remote configuration — tune capture from the dashboard without shipping a new build.
- GDPR-ready — a persistent per-user opt-out kill switch.
Install #
flutter pub add replayfy_flutter
Or add it to your pubspec.yaml:
dependencies:
replayfy_flutter: ^0.0.10
Android (JitPack) #
The native Android recording engine is distributed through JitPack. Add its
repository to your app's android/settings.gradle so Gradle can resolve the
transitive native SDK:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Quick start #
Start Replayfy as early as possible — ideally inside Replay.runZoned so
console output and uncaught async errors are captured too:
import 'package:flutter/widgets.dart';
import 'package:replayfy_flutter/replayfy_flutter.dart';
void main() => Replay.runZoned(() async {
WidgetsFlutterBinding.ensureInitialized();
await Replay.start(const ReplayConfig(
apiKey: 'rpl_pk_xxx',
apiHost: 'https://us.replayfy.app', // or your self-hosted API URL
));
runApp(const MyApp());
});
To enable session replay, use the provided app builder so Replayfy can capture each frame:
MaterialApp(
builder: Replay.appBuilder,
navigatorObservers: [ReplayNavigatorObserver()], // auto-tags screens
home: const HomePage(),
);
Configuration #
Every option on ReplayConfig:
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
String |
— (required) | Your project API key from the dashboard. |
apiHost |
String |
— (required) | Your Replayfy ingest host, e.g. https://us.replayfy.app (or your self-hosted API URL). |
distinctId |
String? |
null |
Known user id at start; otherwise an install-stable anonymous id is used. |
recordScreen |
bool |
true |
Capture screen frames for replay. |
recordNetwork |
bool |
true |
Capture network requests and responses. |
recordConsole |
bool |
true |
Capture console output (print / debugPrint). Requires Replay.runZoned. |
recordErrors |
bool |
true |
Capture unhandled errors and crashes. |
recordPerformance |
bool |
true |
Capture performance vitals (cold start, frame drops, memory, thermal, battery). |
maskAllInputs |
bool |
false |
Deprecated. No effect on Flutter widgets. |
maskPlatformViews |
bool |
true |
Covers platform views (WebView, maps, camera/video textures) with a solid placeholder; Flutter cannot image them. |
autoScreenName |
bool |
true |
Automatically tag screens from the navigator. |
useRemoteConfig |
bool |
true |
Let the dashboard override capture settings remotely. |
debug |
bool |
false |
Verbose SDK logging. |
API #
All methods are static on Replay. Most return Future<void>.
Lifecycle #
await Replay.start(config); // boot the SDK and start recording
await Replay.stop(); // stop and finalize the session
await Replay.pauseRecording(); // pause capture without ending the session
await Replay.resumeRecording(); // resume after a pause
await Replay.startNewSession(); // end the current session, begin a fresh one
await Replay.cancelSession(); // discard the in-flight session without uploading
final recording = await Replay.isRecording(); // bool
final sessionId = await Replay.currentSessionId(); // String?
Replay.flush() equivalent — force-upload everything immediately (e.g. right before a forced logout):
await Replay.stopApplicationAndUploadData();
On Android the future completes once the session's data has been sent (or after 5 s), so await it before the process exits.
Identify a user #
await Replay.identify('user_123', properties: {
'email': 'ada@example.com',
'plan': 'pro',
});
Track an event #
await Replay.track('checkout_completed', properties: {
'value': 49.0,
'currency': 'USD',
});
Capture an exception #
try {
await risky();
} catch (e, stack) {
await Replay.captureException(e, stackTrace: stack); // handled: true by default
}
Track a text input #
Flutter manages its own text widgets, so record a field's value from its
callback. Pass masked: true for sensitive fields — the value is dropped to
"***" and never leaves the device:
TextField(onSubmitted: (v) => Replay.trackInput('Email', v));
TextField(
obscureText: true,
onSubmitted: (v) => Replay.trackInput('Password', v, masked: true),
);
Screens, tags & properties #
await Replay.tagScreenName('Checkout');
await Replay.addTagWithProperties('promo_shown', properties: {'variant': 'B'});
await Replay.setUserProperty('plan', 'pro'); // sticks to the user
await Replay.setSessionProperty('experiment', 'A'); // this session only
await Replay.setMetadata('build_channel', 'beta');
await Replay.markSessionAsFavorite();
await Replay.reportBugEvent('Broken layout', description: 'Overflow on the cart row');
await Replay.log('order placed', level: 'info'); // into the console timeline
Exclude a screen #
Stop capturing frames for a screen by name. While an excluded screen is in the foreground the periodic screenshot capture pauses — taps, network, console, and performance events keep flowing — and it resumes on the next non-excluded screen. Names are matched case-insensitively against the tagged (or auto-tagged) screen name.
await Replay.excludeScreen('Checkout'); // stop capturing frames of this screen
await Replay.unexcludeScreen('Checkout'); // resume capturing it
await Replay.setExcludedScreens(['Checkout', 'Card']); // replace the whole exclusion list
This is distinct from occludeSensitiveScreen (below), which keeps recording
the screen but blanks the frame — exclusion captures no frames of that screen at
all.
Runtime configuration #
await Replay.setAutomaticScreenNameTagging(true);
await Replay.enableAdvancedGestureRecognizer(true); // also capture pinch + rotate (default off)
await Replay.setMultiSessionRecord(true); // many sessions per launch (default); false = one, then no more this launch
await Replay.allowShortBreakForAnotherApp(true, breakWindowMs: 30000); // a switch away shorter than the window keeps the session; longer starts a fresh one
await Replay.setAppVersion('1.4.0', build: '204');
await Replay.setPushNotificationToken(token, platform: 'fcm');
Deep links #
final sessionUrl = await Replay.urlForCurrentSession(); // String?
final userUrl = await Replay.urlForCurrentUser(); // String?
Privacy & masking #
Replayfy is built to keep sensitive data off the wire.
Mask a widget — wrap anything sensitive; it is covered with a solid box (default), blurred, or pixelated in the recording. A tap inside it is recorded without its text:
ReplayMask(
maskStyle: ReplayMaskStyle.overlay, // .overlay (default) | .blur | .pixelate
child: Text('•••• •••• •••• 4242'),
);
A mask follows its widget as it scrolls and moves. While a page transition draws a masked page as a snapshot image (the zoom page transition does), the whole frame is covered instead.
Text fields, including obscureText password fields, are not masked automatically. Flutter briefly shows each typed password character on Android and iOS, so wrap every sensitive field in ReplayMask.
ReplayMask(child: TextField(obscureText: true));
In debug builds, Replayfy logs a warning for each obscureText field it
records outside a ReplayMask.
Set the default mask style for every ReplayMask that sets no
maskStyle:
await Replay.setMaskStyle(ReplayMaskStyle.pixelate);
Whole-screen masking — cover the entire recorded frame with a solid box
(a payment or account screen, say). Touches are not recorded meanwhile unless
you pass hideGestures: false; then they keep their position but not the text
they hit. It stays on, across sessions, until you turn it off:
await Replay.occludeSensitiveScreen(true); // the entire screen
await Replay.occludeSensitiveScreen(false); // back to normal
Platform views — Flutter cannot image a platform view (WebView, maps,
camera and video textures), so the part of each one that shows is covered with
a solid placeholder. Turn it off with maskPlatformViews: false in
ReplayConfig.
occludeAllTextFields, occludeAllTextView and maskAllInputs are
deprecated: they mask the app's native text views only, and Flutter text
fields are not native views.
GDPR opt-out — a persistent kill switch that survives relaunches:
await Replay.optOut(true); // stop all recording
await Replay.optIn(); // resume (same as optOut(false))
final out = await Replay.isOptedOut(); // bool
Links #
- Docs: https://docs.replayfy.app/platforms/flutter
- Dashboard: https://app.replayfy.app
Native SDK distribution & build troubleshooting #
replayfy_flutter is a thin plugin over the native Replayfy SDKs, pulled at
build time:
- Android →
app.replayfy:android-sdkfrom Maven Central (primary; JitPackcom.github.replayfy:android-sdkis a fallback). - iOS → the
Replayfypod from CocoaPods trunk (the Flutter default).
iOS: use CocoaPods, not Swift Package Manager #
Flutter's iOS integration uses CocoaPods by default, which pulls a published
pod — reliable. Do not enable Flutter's SwiftPM integration for this plugin:
Xcode's SwiftPM resolves the iOS SDK by doing a live git ls-remote --tags,
which frequently fails in Xcode with Couldn't get the list of tags (Xcode uses
its own git/cache/credentials, and GitHub rate-limits unauthenticated tag
listing) even when the repo is reachable from your shell. If it's already on:
flutter config --no-enable-swift-package-manager
flutter clean && cd ios && pod install --repo-update
Fixing the two common build failures #
Android — No route to host fetching from jitpack.io: your build host
can't reach JitPack (corporate/CI firewall, proxy, or IPv6). Maven Central
(now the primary channel) avoids this. If a build still hits JitPack:
- confirm reachability on the build host:
curl -I https://jitpack.io - behind a proxy → set
systemProp.https.proxyHost/proxyPortingradle.properties - IPv6 issue →
org.gradle.jvmargs=-Djava.net.preferIPv4Stack=true
iOS — Couldn't get the list of tags resolving a Swift package: you're on
the SwiftPM path — switch to CocoaPods (above). If you must stay on SwiftPM,
prime the cache with the shell git that works, or clear the SPM cache:
cd ios && xcodebuild -resolvePackageDependencies
# or
rm -rf ~/Library/Caches/org.swift.swiftpm ~/Library/Developer/Xcode/DerivedData