flawsnap 0.4.0
flawsnap: ^0.4.0 copied to clipboard
In-app bug reports for Flutter: shake or tap, annotate a screenshot and send it with device details, logs and network calls. Plus Insights: user flow, API timing and errors.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flawsnap/flawsnap.dart';
// Run with: flutter run --dart-define-from-file=dart_defines.env
const _apiUrl = String.fromEnvironment('SNAPLOG_API', defaultValue: 'http://localhost:8080/api/v1');
const _projectKey = String.fromEnvironment('SNAPLOG_KEY');
// Insights (user flow, API timing, errors); the project's switch must be on too.
const _insights = bool.fromEnvironment('SNAPLOG_INSIGHTS', defaultValue: true);
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Flawsnap.init(apiUrl: _apiUrl, projectKey: _projectKey, insights: _insights);
Flawsnap.identify(name: 'Example Tester');
runApp(Flawsnap.wrap(const ExampleApp()));
}
final _http = Flawsnap.httpClient();
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BrightCart',
theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
navigatorObservers: [Flawsnap.navigatorObserver],
initialRoute: '/shop',
routes: {
'/shop': (_) => const ShopScreen(),
'/checkout': (_) => const CheckoutScreen(),
'/account': (_) => const AccountScreen(),
},
);
}
}
class ShopScreen extends StatelessWidget {
const ShopScreen({super.key});
@override
Widget build(BuildContext context) {
const items = [('Sun hat', 24.0), ('Beach towel', 18.0), ('Sandals', 39.0)];
return Scaffold(
appBar: AppBar(
title: const Text('BrightCart'),
actions: [
IconButton(
tooltip: 'Account',
icon: const Icon(Icons.person_outline),
onPressed: () => Navigator.pushNamed(context, '/account'),
),
// Pushed without a route name: Insights shows it as "(unnamed)".
IconButton(
tooltip: 'Help',
icon: const Icon(Icons.help_outline),
onPressed: () => Navigator.push(
context,
MaterialPageRoute<void>(builder: (_) => const HelpScreen()),
),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
for (final (name, price) in items)
Card(
child: ListTile(
title: Text(name),
subtitle: Text('\$${price.toStringAsFixed(2)}'),
onTap: () => Navigator.push(
context,
MaterialPageRoute<void>(
settings: const RouteSettings(name: '/product'),
builder: (_) => ProductScreen(name: name, price: price),
),
),
trailing: FilledButton(
onPressed: () {
debugPrint('Added $name to cart');
Flawsnap.track('product_added'); // an Insights action on this screen
},
child: const Text('Add'),
),
),
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () async {
// Recorded in the report's network tab (404 from the local API).
await _http.get(Uri.parse('http://localhost:8080/api/v1/cart/does-not-exist'));
debugPrint('Cart sync failed: server returned 404');
},
child: const Text('Sync cart (fails)'),
),
const SizedBox(height: 8),
FilledButton.tonal(
onPressed: () => Navigator.pushNamed(context, '/checkout'),
child: const Text('Go to checkout'),
),
],
),
);
}
}
class ProductScreen extends StatefulWidget {
const ProductScreen({super.key, required this.name, required this.price});
final String name;
final double price;
@override
State<ProductScreen> createState() => _ProductScreenState();
}
class _ProductScreenState extends State<ProductScreen> {
String _status = 'Loading details…';
@override
void initState() {
super.initState();
_load();
}
// A successful call for Insights' API timing (the API serves widget.js).
Future<void> _load() async {
try {
final res = await _http.get(Uri.parse(_apiUrl).resolve('/widget.js'));
if (mounted) setState(() => _status = 'Details loaded (${res.statusCode})');
} catch (e) {
if (mounted) setState(() => _status = 'Could not load details');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.name)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('\$${widget.price.toStringAsFixed(2)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Text(_status),
const SizedBox(height: 16),
FilledButton(
onPressed: () => Navigator.pushNamed(context, '/checkout'),
child: const Text('Buy now'),
),
],
),
),
);
}
}
/// Tabs don't change routes, so each tab names itself with Flawsnap.screen().
class AccountScreen extends StatefulWidget {
const AccountScreen({super.key});
@override
State<AccountScreen> createState() => _AccountScreenState();
}
class _AccountScreenState extends State<AccountScreen> with SingleTickerProviderStateMixin {
static const _tabs = ['Orders', 'Profile'];
late final TabController _controller = TabController(length: _tabs.length, vsync: this)
..addListener(() {
if (!_controller.indexIsChanging) Flawsnap.screen(_tabs[_controller.index]);
});
@override
void initState() {
super.initState();
Flawsnap.screen(_tabs.first);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Account'),
bottom: TabBar(controller: _controller, tabs: [for (final t in _tabs) Tab(text: t)]),
),
body: TabBarView(
controller: _controller,
children: [
const Center(child: Text('No orders yet')),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Example Tester'),
const SizedBox(height: 12),
// Forgets the Insights user id; the next launch gets a new one.
OutlinedButton(
onPressed: () async {
await Flawsnap.resetUser();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Insights user id reset')),
);
}
},
child: const Text('Reset user'),
),
],
),
),
],
),
);
}
}
class HelpScreen extends StatelessWidget {
const HelpScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Help')),
body: const Padding(
padding: EdgeInsets.all(16),
child: Text('Shake the phone or tap "Report a problem" on checkout to send a bug report.'),
),
);
}
}
class CheckoutScreen extends StatelessWidget {
const CheckoutScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Checkout')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Total: NaN', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600)),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
Flawsnap.log('Pay tapped with total NaN', level: 'error');
Flawsnap.track('checkout_done');
},
child: const Text('Pay now'),
),
const SizedBox(height: 8),
TextButton(
onPressed: Flawsnap.show,
child: const Text('Report a problem'),
),
],
),
),
);
}
}