replayfy_flutter 0.0.7
replayfy_flutter: ^0.0.7 copied to clipboard
Session replay, product analytics and error monitoring for your Flutter apps on iOS and Android.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:replayfy_flutter/replayfy_flutter.dart';
void main() {
// runZoned lets Replayfy capture print/debugPrint ($console) and uncaught
// async errors ($exception) — the Flutter analogue of patching console.*.
Replay.runZoned(() async {
WidgetsFlutterBinding.ensureInitialized();
// Android emulator reaches the host at 10.0.2.2; iOS simulator shares
// the host network, so localhost works.
final String apiHost = Platform.isAndroid
? 'http://10.0.2.2:4000'
: 'http://localhost:4000';
// Boot the SDK. The native engine takes over screenshots, taps,
// performance, and crashes; the Dart layer adds masking + network +
// console + error capture. recordNetwork/Console/Errors default on.
await Replay.start(ReplayConfig(
apiKey: 'rpl_pk_ef7e2fc8c7f952bcd0a69466e1a42a0625f9',
apiHost: apiHost,
maskAllInputs: false,
));
// Headless crash-test hook. Run with
// flutter run --dart-define=REPLAY_AUTOCRASH=dart_fatal
// to fire a crash a few seconds after start (enough for the session to
// register) without needing a UI tap. Values: dart_fatal | dart_handled.
const String autoCrash = String.fromEnvironment('REPLAY_AUTOCRASH');
if (autoCrash.isNotEmpty) {
Future<void>.delayed(const Duration(seconds: 6), () {
debugPrint('[replayfy] auto-crash: $autoCrash');
switch (autoCrash) {
case 'dart_handled':
// → FlutterError.onError → $exception (fatal:false)
throw StateError('autocrash: handled dart error');
case 'dart_fatal':
default:
// → PlatformDispatcher.onError → $exception (fatal:true)
throw StateError('autocrash: fatal dart error');
}
});
}
runApp(const ExampleApp());
});
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Replayfy Flutter',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
// Auto-tag screens from named routes (native swizzling can't see
// Flutter's Navigator).
navigatorObservers: <NavigatorObserver>[ReplayNavigatorObserver()],
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String? _sessionId;
@override
void initState() {
super.initState();
Replay.tagScreenName('Home');
// === Engine-stub bridge tests — each forwards to native; watch [ReplaySdk] ===
Replay.enableAdvancedGestureRecognizer(true); // → facade: enableAdvancedGestureRecognizer(true)
Replay.allowShortBreakForAnotherApp(true, breakWindowMs: 5000); // → facade: allowShortBreakForAnotherApp(true, 5000ms)
Replay.setMultiSessionRecord(true); // → facade: setMultiSessionRecord(true)
Replay.setExcludedScreens(<String>['Secret']); // → facade: setExcludedScreens([Secret])
Replay.excludeScreen('Vault'); // → facade: excludeScreen(Vault)
// Screen-exclusion round-trip through the channel: enter 'Secret' (excluded
// → PAUSED frames) then leave to 'Home' (→ RESUMED frames).
Future<void>.delayed(const Duration(seconds: 5),
() => Replay.tagScreenName('Secret'));
Future<void>.delayed(const Duration(seconds: 8),
() => Replay.tagScreenName('Home'));
_refreshSession();
}
Future<void> _refreshSession() async {
final String? id = await Replay.currentSessionId();
if (mounted) setState(() => _sessionId = id);
}
// Demonstrates Dart HTTP capture: this request goes through dart:io's
// HttpClient (invisible to the native interceptors), so the Dart-side proxy
// records it. The ingest host is excluded, so we hit a different endpoint.
Future<void> _makeRequest() async {
final HttpClient client = HttpClient();
try {
final HttpClientRequest req =
await client.getUrl(Uri.parse('https://api.github.com/zen'));
final HttpClientResponse resp = await req.close();
await resp.drain<void>();
} catch (_) {
// Network errors are fine for a demo button.
} finally {
client.close();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Replayfy Flutter Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Session: ${_sessionId ?? "—"}'),
const SizedBox(height: 24),
// Anything inside <ReplayMask> is masked in playback per its
// style — blur (default), overlay (solid box), or pixelate
// (mosaic). Bounds are tracked in Dart and pulled by the engine.
const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: <Widget>[
ReplayMask(
child: Text('blur 4242 4242 4242 4242',
style: TextStyle(fontSize: 18, letterSpacing: 1)),
),
SizedBox(height: 8),
ReplayMask(
maskStyle: ReplayMaskStyle.overlay,
child: Text('overlay 012-34-5678',
style: TextStyle(fontSize: 18, letterSpacing: 1)),
),
SizedBox(height: 8),
ReplayMask(
maskStyle: ReplayMaskStyle.pixelate,
child: Text('pixelate hunter2',
style: TextStyle(fontSize: 18, letterSpacing: 1)),
),
],
),
),
),
const SizedBox(height: 24),
FilledButton(
onPressed: () => Replay.track('cta_tapped',
properties: <String, dynamic>{'screen': 'Home'}),
child: const Text('Track event'),
),
OutlinedButton(
onPressed: _makeRequest,
child: const Text('Make network request'),
),
OutlinedButton(
// Captured as a $console event via runZoned's print hook.
onPressed: () => debugPrint('Replayfy demo log line'),
child: const Text('Log a message'),
),
OutlinedButton(
// A throw in a callback is reported via FlutterError.onError,
// which ReplayErrorCapture forwards as a $exception (fatal:false).
onPressed: () => throw StateError('demo: handled error'),
child: const Text('Trigger handled error'),
),
OutlinedButton(
// An uncaught async error escapes to PlatformDispatcher.onError,
// forwarded as a $exception (fatal:true) — the Dart analogue of
// an unhandled crash. The app keeps running (the engine catches
// native signals separately).
onPressed: () => Future<void>.delayed(
Duration.zero,
() => throw StateError('demo: fatal async error'),
),
child: const Text('Trigger fatal async error'),
),
TextButton(
onPressed: () async {
await Replay.startNewSession();
await _refreshSession();
},
child: const Text('Start new session'),
),
TextButton(
onPressed: () => Replay.cancelSession(),
child: const Text('cancelSession (discard)'),
),
],
),
),
);
}
}