bug_recorder 0.1.0
bug_recorder: ^0.1.0 copied to clipboard
Rolling-buffer screen recording for bug reports. Continuously keeps the last N seconds of screen activity, so a tester who shakes the phone after noticing a bug captures the steps that caused it - not [...]
example/lib/main.dart
import 'package:bug_recorder/bug_recorder.dart';
import 'package:flutter/material.dart';
import 'screens/checkout_screen.dart';
import 'screens/home_screen.dart';
import 'widgets/recording_indicator.dart';
import 'widgets/recording_sheet.dart';
/// Demonstrates the full rolling-buffer flow:
///
/// ```
/// start buffering -> tester uses the app -> a bug happens -> tester shakes
/// -> [20s before the shake] + [10s after] -> one MP4 -> preview and share
/// ```
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final recorder = BugRecorder.instance;
// Initialising never throws on an unsupported platform; it reports
// capabilities instead, so no Platform.isX guard is needed here.
final capabilities = await recorder.initialize(
config: const BugRecorderConfig(
bufferDuration: Duration(seconds: 20),
postTriggerDuration: Duration(seconds: 10),
// 2s segments: ~10 files for the window, and at most 2s of video lost if
// the app crashes before the tester shakes.
segmentDuration: Duration(seconds: 2),
maxDimension: 720,
frameRate: 24,
notification: AndroidNotificationConfig(
title: 'Bug recorder is on',
body: 'Shake your phone when you see something wrong.',
),
),
);
// Ask about a crashed previous run *before* starting a new session, otherwise
// the new session's directory is the newest one and the old one is pruned.
final recovered = await _tryRecover(recorder);
runApp(ExampleApp(capabilities: capabilities, recovered: recovered));
}
Future<RecordingResult?> _tryRecover(BugRecorder recorder) async {
if (!(recorder.capabilities?.supported ?? false)) return null;
try {
return await recorder.recoverPreviousSession();
} on BugRecorderException catch (error) {
debugPrint('crash recovery skipped: $error');
return null;
}
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key, required this.capabilities, this.recovered});
final PlatformCapabilities capabilities;
final RecordingResult? recovered;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bug Recorder',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6750A4)),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.dark,
),
useMaterial3: true,
),
routes: {CheckoutScreen.route: (_) => const CheckoutScreen()},
home: _Root(capabilities: capabilities, recovered: recovered),
);
}
}
/// Hosts the [BugRecorderScope] so a finished recording can be surfaced from one
/// place no matter which screen the tester was on when they shook the phone.
class _Root extends StatefulWidget {
const _Root({required this.capabilities, this.recovered});
final PlatformCapabilities capabilities;
final RecordingResult? recovered;
@override
State<_Root> createState() => _RootState();
}
class _RootState extends State<_Root> {
final _navigatorKey = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
final recovered = widget.recovered;
if (recovered != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_present(recovered, crashRecovery: true);
});
}
}
void _present(RecordingResult result, {bool crashRecovery = false}) {
final context = _navigatorKey.currentContext ?? this.context;
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder: (_) =>
RecordingSheet(result: result, crashRecovery: crashRecovery),
);
}
@override
Widget build(BuildContext context) {
return BugRecorderScope(
onRecording: _present,
child: Stack(
children: [
Navigator(
key: _navigatorKey,
onGenerateRoute: (settings) => MaterialPageRoute<void>(
builder: (_) => switch (settings.name) {
CheckoutScreen.route => const CheckoutScreen(),
_ => HomeScreen(capabilities: widget.capabilities),
},
settings: settings,
),
),
// The system provides disclosure on both platforms (a mandatory
// notification on Android, a consent alert on iOS), but an in-app
// indicator is the honest thing to add: it is visible at the moment
// recording is actually happening.
const Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(child: RecordingIndicator()),
),
],
),
);
}
}