drengr_flutter_sdk 0.4.1 copy "drengr_flutter_sdk: ^0.4.1" to clipboard
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.

example/lib/main.dart

/// 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)),
          ),
        ],
      ),
    );
  }
}
0
likes
160
points
376
downloads

Documentation

API reference

Publisher

verified publisherdrengr.dev

Weekly Downloads

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.

Homepage
Repository (GitHub)
View/report issues

Topics

#networking #http #observability #analytics #monitoring

License

Apache-2.0 (license)

Dependencies

cryptography, flutter, shared_preferences

More

Packages that depend on drengr_flutter_sdk