drengr_flutter_sdk 0.4.1
drengr_flutter_sdk: ^0.4.1 copied to clipboard
Zero-code mobile analytics for Flutter — one line captures and ships every HTTP request/response (http, Dio, dart:io) to Drengr with secret/PII redaction, no networking changes.
/// Drengr Demo Shop — a minimal 3-screen e-commerce app.
///
/// The ONLY Drengr integration is the single `Drengr.start(...)` call below and
/// the `navigatorObservers: [Drengr.navigatorObserver]` on MaterialApp. Every
/// HTTP request the app makes (see mock_api.dart) is captured transparently.
library;
import 'package:flutter/material.dart';
import 'package:drengr_flutter_sdk/drengr_flutter_sdk.dart';
import 'drengr_config.dart';
import 'mock_api.dart';
import 'products.dart';
void main() {
// ── The one-liner: capture + redact + ship. Placed before runApp. ──────────
// Captures every http/Dio/dart:io request, redacts secrets/PII on device,
// and ships events to your Drengr ingest endpoint. Also turns on behavior
// capture (screen_view / tap / rage_tap / crash) with zero per-widget code.
Drengr.start(
publishableKey: DrengrConfig.publishableKey,
ingestUrl: DrengrConfig.ingestUrl,
appPackage: DrengrConfig.appPackage,
);
runApp(const DemoShopApp());
}
class DemoShopApp extends StatelessWidget {
const DemoShopApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Drengr Demo Shop',
debugShowCheckedModeBanner: false,
// Feeds screen_view events (and per-screen tap context) to Drengr.
navigatorObservers: [Drengr.navigatorObserver],
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFFB8860B),
),
home: const CatalogScreen(),
);
}
}
// ── Screen 1: Catalog ────────────────────────────────────────────────────────
class CatalogScreen extends StatelessWidget {
const CatalogScreen({super.key});
Future<void> _openProduct(BuildContext context, Product product) async {
// Fires GET /anything/catalog/{id} → Drengr: catalog_item_viewed.
await MockApi.viewCatalogItem(product.id);
if (!context.mounted) return;
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => ProductScreen(product: product)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Drengr Demo Shop')),
body: Column(
children: [
const _CaptureBanner(),
Expanded(
child: ListView.separated(
itemCount: kProducts.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final p = kProducts[i];
return ListTile(
leading: const Icon(Icons.shopping_bag_outlined),
title: Text(p.name),
subtitle: Text('\$${p.price.toStringAsFixed(2)}'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _openProduct(context, p),
);
},
),
),
],
),
);
}
}
// ── Screen 2: Product + Add to cart ─────────────────────────────────────────
class ProductScreen extends StatefulWidget {
const ProductScreen({super.key, required this.product});
final Product product;
@override
State<ProductScreen> createState() => _ProductScreenState();
}
class _ProductScreenState extends State<ProductScreen> {
bool _added = false;
bool _busy = false;
Future<void> _addToCart() async {
setState(() => _busy = true);
// Fires POST /cart {product_id, qty} → Drengr: cart_updated.
await MockApi.addToCart(widget.product.id, 1);
if (!mounted) return;
setState(() {
_busy = false;
_added = true;
});
}
void _goToCheckout() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CheckoutScreen(product: widget.product),
),
);
}
@override
Widget build(BuildContext context) {
final p = widget.product;
return Scaffold(
appBar: AppBar(title: Text(p.name)),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(p.name, style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 8),
Text('\$${p.price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 24),
const Text(
'A tidy little product for demonstrating Drengr capture. '
'Adding to cart fires a real POST /cart request.',
),
const Spacer(),
FilledButton.icon(
onPressed: _busy ? null : _addToCart,
icon: _busy
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.add_shopping_cart),
label: Text(_added ? 'Added to cart' : 'Add to cart'),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: _added ? _goToCheckout : null,
icon: const Icon(Icons.arrow_forward),
label: const Text('Go to checkout'),
),
],
),
),
);
}
}
// ── Screen 3: Checkout ───────────────────────────────────────────────────────
class CheckoutScreen extends StatefulWidget {
const CheckoutScreen({super.key, required this.product});
final Product product;
@override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
final _formKey = GlobalKey<FormState>();
final _name = TextEditingController(text: 'Ada Lovelace');
final _email = TextEditingController(text: 'ada@example.com');
final _card = TextEditingController(text: '4242424242424242');
bool _busy = false;
@override
void dispose() {
_name.dispose();
_email.dispose();
_card.dispose();
super.dispose();
}
Future<void> _placeOrder() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() => _busy = true);
// Fires POST /checkout carrying PII (email + card) → Drengr: order_created.
// email + card are sealed ON DEVICE by the SDK before the event ships.
await MockApi.checkout(
name: _name.text,
email: _email.text,
card: _card.text,
amount: widget.product.price,
item: widget.product.name,
);
if (!mounted) return;
setState(() => _busy = false);
await showDialog<void>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Order placed'),
content: Text(
'Thanks! Your order for ${widget.product.name} is in.\n\n'
'Drengr captured this as order_created; the email and card were '
'sealed on device.',
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).popUntil((route) => route.isFirst);
},
child: const Text('Done'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Checkout')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'${widget.product.name} — \$${widget.product.price.toStringAsFixed(2)}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
TextFormField(
controller: _name,
decoration: const InputDecoration(labelText: 'Name'),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Required' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: _email,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email (PII — sealed on device)',
),
validator: (v) =>
(v != null && v.contains('@')) ? null : 'Invalid email',
),
const SizedBox(height: 12),
TextFormField(
controller: _card,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Card number (PII — sealed on device)',
),
validator: (v) =>
(v != null && v.replaceAll(' ', '').length >= 12)
? null
: 'Invalid card',
),
const Spacer(),
FilledButton.icon(
onPressed: _busy ? null : _placeOrder,
icon: _busy
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.lock_outline),
label: const Text('Place order'),
),
],
),
),
),
);
}
}
// ── Honest banner: is capture live, or demo-local? ──────────────────────────
class _CaptureBanner extends StatelessWidget {
const _CaptureBanner();
@override
Widget build(BuildContext context) {
final live = DrengrConfig.isConfigured;
final color = live ? const Color(0xFF1B5E20) : const Color(0xFF8D6E00);
final text = live
? 'Drengr capture LIVE → shipping to ingest'
: 'Demo key not set — capture runs, events buffer locally';
return Container(
width: double.infinity,
color: color.withOpacity(0.10),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
Icon(live ? Icons.cloud_done_outlined : Icons.info_outline,
size: 18, color: color),
const SizedBox(width: 8),
Expanded(
child: Text(text, style: TextStyle(color: color, fontSize: 12.5)),
),
],
),
);
}
}