Skip to main content

flawsnap 0.4.0 copy "flawsnap: ^0.4.0" to clipboard
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'),
            ),
          ],
        ),
      ),
    );
  }
}
3
likes
160
points
853
downloads

Documentation

Documentation
API reference

Publisher

verified publisherflawsnap.com

Weekly Downloads

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.

Homepage
View/report issues

Topics

#bug-reporting #feedback #debugging #screenshot #analytics

License

MIT (license)

Dependencies

device_info_plus, flutter, http, package_info_plus, sensors_plus, shared_preferences, url_launcher

More

Packages that depend on flawsnap