ispect 4.8.0-dev02
ispect: ^4.8.0-dev02 copied to clipboard
Logging and inspector tool for Flutter development and testing
example/lib/main.dart
import 'dart:async';
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:ispect/ispect.dart';
import 'package:ispectify_db/ispectify_db.dart';
import 'package:ispectify_dio/ispectify_dio.dart';
import 'package:ispectify_http/ispectify_http.dart';
import 'package:open_filex/open_filex.dart';
import 'package:share_plus/share_plus.dart';
// ---------------------------------------------------------------------------
// Observer example
// ---------------------------------------------------------------------------
class SentryISpectObserver implements ISpectObserver {
@override
void onError(ISpectLogData err) => log('Sentry onError: ${err.message}');
@override
void onException(ISpectLogData err) =>
log('Sentry onException: ${err.message}');
@override
void onLog(ISpectLogData data) => log('Sentry onLog: ${data.message}');
}
// ---------------------------------------------------------------------------
// Theme presets
// ---------------------------------------------------------------------------
class _ThemePreset {
const _ThemePreset(this.label, this.seed, this.primary, this.background);
final String label;
final Color seed;
final ISpectDynamicColor? primary;
final ISpectDynamicColor? background;
}
const _themePresets = <_ThemePreset>[
_ThemePreset('Default', Colors.deepPurple, null, null),
_ThemePreset(
'Ocean',
Colors.blue,
ISpectDynamicColor(light: Color(0xFF1565C0), dark: Color(0xFF64B5F6)),
ISpectDynamicColor(light: Color(0xFFF5F8FF), dark: Color(0xFF0D1B2A)),
),
_ThemePreset(
'Forest',
Colors.green,
ISpectDynamicColor(light: Color(0xFF2E7D32), dark: Color(0xFF81C784)),
ISpectDynamicColor(light: Color(0xFFF1F8E9), dark: Color(0xFF1B2E1B)),
),
_ThemePreset(
'Sunset',
Colors.deepOrange,
ISpectDynamicColor(light: Color(0xFFD84315), dark: Color(0xFFFF8A65)),
ISpectDynamicColor(light: Color(0xFFFFF3E0), dark: Color(0xFF2E1A0E)),
),
_ThemePreset(
'Mono',
Colors.grey,
ISpectDynamicColor(light: Color(0xFF424242), dark: Color(0xFFBDBDBD)),
ISpectDynamicColor(light: Color(0xFFFAFAFA), dark: Color(0xFF121212)),
),
];
// ---------------------------------------------------------------------------
// Locales
// ---------------------------------------------------------------------------
class _LocaleOption {
const _LocaleOption(this.locale, this.label);
final Locale locale;
final String label;
}
const _localeOptions = <_LocaleOption>[
_LocaleOption(Locale('en'), 'English'),
_LocaleOption(Locale('ru'), 'Русский'),
_LocaleOption(Locale('kk'), 'Қазақша'),
_LocaleOption(Locale('de'), 'Deutsch'),
_LocaleOption(Locale('es'), 'Español'),
_LocaleOption(Locale('fr'), 'Français'),
_LocaleOption(Locale('pt'), 'Português'),
_LocaleOption(Locale('zh'), '中文'),
_LocaleOption(Locale('ja'), '日本語'),
_LocaleOption(Locale('ko'), '한국어'),
_LocaleOption(Locale('hi'), 'हिन्दी'),
_LocaleOption(Locale('ar'), 'العربية'),
];
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
void main() {
final logger = ISpectFlutter.init();
logger.addObserver(SentryISpectObserver());
ISpect.run(logger: logger, () => runApp(const MyApp()));
}
// ---------------------------------------------------------------------------
// App
// ---------------------------------------------------------------------------
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _observer = ISpectNavigatorObserver();
ThemeMode _themeMode = ThemeMode.system;
_ThemePreset _preset = _themePresets.first;
Locale _locale = const Locale('en');
@override
Widget build(BuildContext context) {
return MaterialApp(
locale: _locale,
supportedLocales: _localeOptions.map((o) => o.locale),
localizationsDelegates: ISpectLocalizations.delegates(),
navigatorObservers:
ISpectNavigatorObserver.observers(additional: [_observer]),
themeMode: _themeMode,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: _preset.seed,
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: _preset.seed,
brightness: Brightness.dark,
),
),
builder: (_, child) => ISpectBuilder.wrap(
child: child!,
options: ISpectOptions(
observer: _observer,
onOpenFile: (path) async => OpenFilex.open(path),
onShare: (req) async => SharePlus.instance.share(ShareParams(
text: req.text,
subject: req.subject,
files: req.filePaths.map(XFile.new).toList(),
)),
),
theme: ISpectTheme(
primary: _preset.primary,
background: _preset.background,
),
),
home: _HomePage(
themeMode: _themeMode,
preset: _preset,
locale: _locale,
onThemeModeChanged: (v) => setState(() => _themeMode = v),
onPresetChanged: (v) => setState(() => _preset = v),
onLocaleChanged: (v) => setState(() => _locale = v),
),
);
}
}
// ---------------------------------------------------------------------------
// Home page
// ---------------------------------------------------------------------------
class _HomePage extends StatefulWidget {
const _HomePage({
required this.themeMode,
required this.preset,
required this.locale,
required this.onThemeModeChanged,
required this.onPresetChanged,
required this.onLocaleChanged,
});
final ThemeMode themeMode;
final _ThemePreset preset;
final Locale locale;
final ValueChanged<ThemeMode> onThemeModeChanged;
final ValueChanged<_ThemePreset> onPresetChanged;
final ValueChanged<Locale> onLocaleChanged;
@override
State<_HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<_HomePage> {
// Stress test
double _logCount = 10;
double _listSize = 5;
double _nestingDepth = 2;
bool _isGenerating = false;
// Periodic logger
Timer? _periodicTimer;
int _periodicCounter = 0;
@override
void dispose() {
_periodicTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('ISpect Example'),
actions: [
// Theme mode
IconButton(
icon: Icon(switch (widget.themeMode) {
ThemeMode.light => Icons.light_mode,
ThemeMode.dark => Icons.dark_mode,
ThemeMode.system => Icons.settings_brightness,
}),
onPressed: () {
const modes = ThemeMode.values;
final next =
modes[(modes.indexOf(widget.themeMode) + 1) % modes.length];
widget.onThemeModeChanged(next);
},
tooltip: 'Theme: ${widget.themeMode.name}',
),
// Locale
PopupMenuButton<Locale>(
icon: const Icon(Icons.language),
tooltip: 'Language',
onSelected: widget.onLocaleChanged,
itemBuilder: (_) => [
for (final opt in _localeOptions)
PopupMenuItem(
value: opt.locale,
child: Text(opt.label),
),
],
),
],
),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
children: [
// Status
Card(
color: cs.primaryContainer,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
kISpectEnabled ? Icons.check_circle : Icons.cancel,
color: cs.onPrimaryContainer,
),
const SizedBox(width: 12),
Text(
'ISpect ${kISpectEnabled ? "ENABLED" : "DISABLED"}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: cs.onPrimaryContainer,
),
),
],
),
),
),
const SizedBox(height: 16),
// Color presets
_SectionHeader(title: 'Color Preset'),
const SizedBox(height: 8),
SizedBox(
height: 40,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _themePresets.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (_, i) {
final p = _themePresets[i];
final selected = widget.preset.label == p.label;
return ChoiceChip(
label: Text(p.label),
avatar: CircleAvatar(backgroundColor: p.seed, radius: 8),
selected: selected,
onSelected: (_) => widget.onPresetChanged(p),
);
},
),
),
const SizedBox(height: 20),
// Quick logs
_SectionHeader(title: 'Quick Logs'),
const SizedBox(height: 8),
_QuickLogsGrid(),
const SizedBox(height: 20),
// Scenarios
_SectionHeader(title: 'Scenarios'),
const SizedBox(height: 8),
_ScenarioCard(
icon: Icons.timer,
title: 'Periodic Logger',
subtitle: _periodicTimer != null
? 'Running ($_periodicCounter logs)'
: 'Logs every second',
trailing: Switch(
value: _periodicTimer != null,
onChanged: (_) => _togglePeriodicLogger(),
),
),
const SizedBox(height: 8),
_ScenarioCard(
icon: Icons.navigation,
title: 'Navigation Test',
subtitle: 'Push a detail page',
trailing: const Icon(Icons.chevron_right),
onTap: () => _navigateToDetail(context),
),
const SizedBox(height: 8),
_ScenarioCard(
icon: Icons.error_outline,
title: 'Crash Simulation',
subtitle: 'Unhandled exception via Zone',
trailing: const Icon(Icons.chevron_right),
onTap: _simulateCrash,
),
const SizedBox(height: 8),
_ScenarioCard(
icon: Icons.data_object,
title: 'Complex Payload',
subtitle: 'Log deeply nested JSON',
trailing: const Icon(Icons.chevron_right),
onTap: _logComplexPayload,
),
const SizedBox(height: 20),
// Network & DB
_SectionHeader(title: 'Network & Database'),
const SizedBox(height: 8),
_NetworkDbSection(),
const SizedBox(height: 20),
// Stress test
_SectionHeader(title: 'Stress Test'),
const SizedBox(height: 8),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_SliderControl(
label: 'Count',
value: _logCount,
min: 1,
max: 10000,
divisions: 99,
displayValue: _logCount.round().toString(),
onChanged: (v) => setState(() => _logCount = v),
),
_SliderControl(
label: 'List size',
value: _listSize,
min: 1,
max: 100,
divisions: 99,
displayValue: _listSize.round().toString(),
onChanged: (v) => setState(() => _listSize = v),
),
_SliderControl(
label: 'Nesting',
value: _nestingDepth,
min: 0,
max: 6,
divisions: 6,
displayValue: _nestingDepth.round().toString(),
onChanged: (v) => setState(() => _nestingDepth = v),
),
const SizedBox(height: 12),
if (_isGenerating)
const LinearProgressIndicator()
else
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
for (final e in [
('Info', Colors.blue, 'info'),
('Debug', Colors.green, 'debug'),
('Warning', Colors.orange, 'warning'),
('Error', Colors.red, 'error'),
('Mixed', Colors.purple, 'mixed'),
])
FilledButton.tonal(
onPressed: () => _generateLogs(type: e.$3),
style: FilledButton.styleFrom(
foregroundColor: e.$2,
),
child: Text(e.$1),
),
],
),
],
),
),
),
],
),
);
}
// -- Actions --
void _togglePeriodicLogger() {
setState(() {
if (_periodicTimer != null) {
_periodicTimer!.cancel();
_periodicTimer = null;
ISpect.logger.info(
'Periodic logger stopped after $_periodicCounter logs',
);
_periodicCounter = 0;
} else {
_periodicCounter = 0;
_periodicTimer = Timer.periodic(const Duration(seconds: 1), (_) {
_periodicCounter++;
final types = ['info', 'debug', 'warning', 'verbose', 'good'];
final type = types[_periodicCounter % types.length];
switch (type) {
case 'info':
ISpect.logger.info('Periodic #$_periodicCounter');
case 'debug':
ISpect.logger.debug('Periodic #$_periodicCounter: state OK');
case 'warning':
ISpect.logger.warning(
'Periodic #$_periodicCounter: memory usage high',
);
case 'verbose':
ISpect.logger.verbose('Periodic #$_periodicCounter: tick');
case 'good':
ISpect.logger.good('Periodic #$_periodicCounter: healthy');
}
setState(() {});
});
ISpect.logger.info('Periodic logger started');
}
});
}
void _navigateToDetail(BuildContext context) {
ISpect.logger.info('Navigating to detail page');
Navigator.of(context).push(
MaterialPageRoute<void>(
settings: const RouteSettings(name: '/detail'),
builder: (_) => Scaffold(
appBar: AppBar(title: const Text('Detail Page')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('This tests navigation observer logging.'),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
ISpect.logger.info('Going back from detail');
Navigator.of(context).pop();
},
child: const Text('Go Back'),
),
],
),
),
),
),
);
}
void _simulateCrash() {
ISpect.logger.warning('About to simulate crash...');
Future<void>.delayed(const Duration(milliseconds: 100), () {
throw StateError('Simulated crash for ISpect testing');
});
}
void _logComplexPayload() {
ISpect.logger.info(
'Complex payload with deep nesting',
additionalData: {
'user': {
'id': 42,
'name': 'John Doe',
'roles': ['admin', 'editor'],
'preferences': {
'theme': 'dark',
'notifications': {
'email': true,
'push': false,
'sms': {'enabled': true, 'number': '+1234567890'},
},
},
},
'items': List.generate(
10,
(i) => {
'id': i,
'title': 'Item #$i',
'tags': List.generate(3, (j) => 'tag_${i}_$j'),
'metadata': {
'created':
DateTime.now().subtract(Duration(hours: i)).toIso8601String(),
'weight': (i * 1.5).toStringAsFixed(2),
},
},
),
'pagination': {
'page': 1,
'perPage': 10,
'total': 156,
'hasNext': true,
},
},
);
}
Future<void> _generateLogs({required String type}) async {
setState(() => _isGenerating = true);
final count = _logCount.round();
final listSize = _listSize.round();
final nestingDepth = _nestingDepth.round();
final data = _buildList(listSize, nestingDepth);
final nested = _buildNestedMap(nestingDepth, listSize.clamp(1, 5));
for (var i = 0; i < count; i++) {
final n = i + 1;
switch (type) {
case 'info':
ISpect.logger.info('Info #$n', additionalData: {'items': data});
case 'debug':
ISpect.logger.debug('Debug #$n', additionalData: {'nested': nested});
case 'warning':
ISpect.logger.warning('Warning #$n', additionalData: {'rows': data});
case 'error':
ISpect.logger.error(
'Error #$n',
exception: Exception('BatchError #$n'),
stackTrace: StackTrace.current,
additionalData: {'input': data},
);
case 'mixed':
final types = ['info', 'debug', 'warning', 'error', 'verbose'];
final t = types[i % types.length];
switch (t) {
case 'info':
ISpect.logger
.info('Mixed #$n [INFO]', additionalData: {'d': data});
case 'debug':
ISpect.logger
.debug('Mixed #$n [DEBUG]', additionalData: {'d': nested});
case 'warning':
ISpect.logger
.warning('Mixed #$n [WARN]', additionalData: {'d': data});
case 'error':
ISpect.logger.error(
'Mixed #$n [ERROR]',
exception: Exception('MixedError #$n'),
stackTrace: StackTrace.current,
);
case 'verbose':
ISpect.logger.verbose('Mixed #$n [VERBOSE]',
additionalData: {'d': nested});
}
}
if (i % 100 == 99) await Future<void>.delayed(Duration.zero);
}
setState(() => _isGenerating = false);
}
// -- Data builders --
Map<String, dynamic> _buildNestedMap(int depth, int breadth) {
if (depth <= 0) {
return {
'value': 'leaf',
'ts': DateTime.now().toIso8601String(),
'tags': List.generate(breadth, (i) => 'tag_$i'),
};
}
return {
'level': depth,
'items': List.generate(
breadth,
(i) => {'item_$i': _buildNestedMap(depth - 1, breadth.clamp(1, 3))},
),
};
}
List<Map<String, dynamic>> _buildList(int size, int nestingDepth) {
return List.generate(
size,
(i) => {
'index': i,
'id': 'item_${i.toString().padLeft(4, '0')}',
'name': 'Element #$i',
'active': i.isEven,
if (nestingDepth > 0)
'nested':
_buildNestedMap(nestingDepth, (size ~/ 3).clamp(1, 3)),
});
}
}
// ---------------------------------------------------------------------------
// Quick log buttons grid
// ---------------------------------------------------------------------------
class _QuickLogsGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
final logs = <(String, IconData, Color, VoidCallback)>[
(
'Info',
Icons.info_outline,
Colors.blue,
() => ISpect.logger.info('Info message')
),
(
'Debug',
Icons.bug_report_outlined,
Colors.green,
() => ISpect.logger.debug('Debug message')
),
(
'Warning',
Icons.warning_amber,
Colors.orange,
() => ISpect.logger.warning('Warning message')
),
(
'Error',
Icons.error_outline,
Colors.red,
() => ISpect.logger.error('Error message')
),
(
'Exception',
Icons.dangerous_outlined,
Colors.red.shade800,
() => ISpect.logger.handle(
exception: Exception('Test exception'),
stackTrace: StackTrace.current,
)
),
(
'Critical',
Icons.local_fire_department,
Colors.deepOrange,
() => ISpect.logger.critical(
'Critical failure',
exception: Exception('CriticalTest'),
stackTrace: StackTrace.current,
)
),
(
'Good',
Icons.check_circle_outline,
Colors.teal,
() => ISpect.logger.good('All systems operational')
),
(
'Verbose',
Icons.text_snippet_outlined,
Colors.grey,
() => ISpect.logger.verbose('Verbose trace')
),
(
'Print',
Icons.terminal,
Colors.blueGrey,
() => ISpect.logger.print('Print log')
),
];
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final (label, icon, color, onTap) in logs)
ActionChip(
avatar: Icon(icon, size: 18, color: color),
label: Text(label),
onPressed: onTap,
),
],
);
}
}
// ---------------------------------------------------------------------------
// Scenario card
// ---------------------------------------------------------------------------
class _ScenarioCard extends StatelessWidget {
const _ScenarioCard({
required this.icon,
required this.title,
required this.subtitle,
required this.trailing,
this.onTap,
});
final IconData icon;
final String title;
final String subtitle;
final Widget trailing;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
child: ListTile(
leading: Icon(icon),
title: Text(title),
subtitle: Text(subtitle),
trailing: trailing,
onTap: onTap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
}
// ---------------------------------------------------------------------------
// Section header
// ---------------------------------------------------------------------------
class _SectionHeader extends StatelessWidget {
const _SectionHeader({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w600,
),
);
}
}
// ---------------------------------------------------------------------------
// Network & Database section
// ---------------------------------------------------------------------------
class _NetworkDbSection extends StatefulWidget {
@override
State<_NetworkDbSection> createState() => _NetworkDbSectionState();
}
class _NetworkDbSectionState extends State<_NetworkDbSection> {
late final Dio _dio;
late final InterceptedClient _httpClient;
bool _isLoading = false;
String? _accessToken;
String? _refreshToken;
@override
void initState() {
super.initState();
final logger = ISpect.logger;
_dio = Dio();
_dio.interceptors.add(
ISpectDioInterceptor(
logger: logger,
settings: const ISpectDioInterceptorSettings(
printRequestHeaders: true,
printResponseHeaders: true,
enableRedaction: true,
),
),
);
_httpClient = InterceptedClient.build(
interceptors: [
ISpectHttpInterceptor(
logger: logger,
settings: ISpectHttpInterceptorSettingsBuilder()
.withAllHeaders()
.withRedaction()
.build(),
),
],
);
ISpectDbCore.config = ISpectDbConfig(
sampleRate: 1.0,
redact: true,
attachStackOnError: true,
enableTransactionMarkers: true,
slowQueryThreshold: Duration(milliseconds: 250),
);
}
@override
void dispose() {
_dio.close();
_httpClient.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_isLoading) const LinearProgressIndicator(),
if (_isLoading) const SizedBox(height: 12),
// Dio
Text(
'Dio HTTP',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_chip('GET todo', Icons.download, Colors.blue, () {
_dio.get('https://dummyjson.com/todos/1');
}),
_chip('GET products', Icons.list, Colors.blue, () {
_dio.get('https://dummyjson.com/products?limit=5');
}),
_chip('POST product', Icons.upload, Colors.green, () {
_dio.post(
'https://dummyjson.com/products/add',
data: {
'title': 'ISpect Test Product',
'description': 'Testing Dio interceptor',
'price': 99,
},
);
}),
_chip('PUT product', Icons.edit, Colors.orange, () {
_dio.put(
'https://dummyjson.com/products/1',
data: {'title': 'Updated Title'},
);
}),
_chip('DELETE product', Icons.delete, Colors.red, () {
_dio.delete('https://dummyjson.com/products/1');
}),
_chip('GET 404', Icons.error, Colors.red.shade800, () {
_dio.get('https://dummyjson.com/products/0');
}),
_chip('Invalid URL', Icons.link_off, Colors.grey, () {
_dio.get('htt://invalid-url');
}),
],
),
const SizedBox(height: 16),
// HTTP package
Text(
'HTTP Package',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_chip('GET users', Icons.people, Colors.blue, () {
_httpClient.get(
Uri.parse('https://dummyjson.com/users?limit=3'),
);
}),
_chip('GET recipes', Icons.restaurant, Colors.teal, () {
_httpClient.get(
Uri.parse('https://dummyjson.com/recipes?limit=5'),
);
}),
_chip('POST login', Icons.login, Colors.green, () {
_httpClient.post(
Uri.parse('https://dummyjson.com/auth/login'),
body: '{"username":"emilys","password":"emilyspass"}',
headers: {'Content-Type': 'application/json'},
);
}),
],
),
const SizedBox(height: 16),
// Redaction examples
Text(
'Redaction (Auth & Sensitive Data)',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 4),
Text(
_accessToken != null
? 'Logged in (token cached)'
: 'Tap "Login" first to get real tokens',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: _accessToken != null ? Colors.green : Colors.grey,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
// Step 1: Login to get real tokens
_chip(
'Login (get tokens)',
Icons.login,
Colors.deepPurple,
() async {
try {
final response = await _dio.post(
'https://dummyjson.com/auth/login',
data: {
'username': 'emilys',
'password': 'emilyspass',
},
);
final data = response.data as Map<String, dynamic>;
setState(() {
_accessToken = data['accessToken'] as String?;
_refreshToken = data['refreshToken'] as String?;
});
} catch (e, st) {
ISpect.logger.handle(exception: e, stackTrace: st);
}
},
),
// Step 2: Use real access token
_chip(
'GET /auth/me',
Icons.person,
Colors.deepPurple.shade300,
() {
_dio.get(
'https://dummyjson.com/auth/me',
options: Options(
headers: {
'Authorization':
'Bearer ${_accessToken ?? 'NO_TOKEN'}',
},
),
);
},
),
// Step 3: Refresh token flow
_chip(
'Refresh token',
Icons.refresh,
Colors.deepPurple.shade200,
() async {
try {
final response = await _dio.post(
'https://dummyjson.com/auth/refresh',
data: {
'refreshToken': _refreshToken ?? 'NO_TOKEN',
},
);
final data = response.data as Map<String, dynamic>;
setState(() {
_accessToken = data['accessToken'] as String?;
_refreshToken = data['refreshToken'] as String?;
});
} catch (e, st) {
ISpect.logger.handle(exception: e, stackTrace: st);
}
},
),
// API key + client secret headers
_chip(
'API Key header',
Icons.vpn_key,
Colors.indigo,
() {
_dio.get(
'https://dummyjson.com/products/1',
options: Options(
headers: {
'Authorization':
'Bearer ${_accessToken ?? 'NO_TOKEN'}',
'X-Api-Key': 'sk-proj-abc123def456ghi789',
'X-Client-Secret': 'cs_live_xR7kL9mP2qW5nV8',
},
),
);
},
),
// Cookie header
_chip(
'Cookie header',
Icons.cookie,
Colors.brown,
() {
_dio.get(
'https://dummyjson.com/users/1',
options: Options(
headers: {
'Cookie': 'session=abc123xyz; '
'accessToken=${_accessToken ?? 'NO_TOKEN'}',
},
),
);
},
),
// Sensitive body (PII, financial)
_chip(
'Sensitive body',
Icons.security,
Colors.red.shade700,
() {
_dio.post(
'https://dummyjson.com/users/add',
data: {
'firstName': 'John',
'lastName': 'Doe',
'email': 'john@example.com',
'phone': '+1-555-0123',
'password': 'SuperSecret123!',
'ssn': '123-45-6789',
'credit_card': '4111111111111111',
'cvv': '123',
'bank_account': '9876543210',
},
options: Options(
headers: {
'Authorization':
'Bearer ${_accessToken ?? 'NO_TOKEN'}',
},
),
);
},
),
// Nested secrets in body
_chip(
'Nested secrets',
Icons.account_tree,
Colors.teal.shade700,
() {
_dio.post(
'https://dummyjson.com/products/add',
data: {
'title': 'Test',
'metadata': {
'api_key': 'ak_test_51234567890',
'private_key': '-----BEGIN RSA PRIVATE KEY-----',
'config': {
'client_secret': 'whsec_abcdef123456',
'access_token': _accessToken ?? 'NO_TOKEN',
'refresh_token': _refreshToken ?? 'NO_TOKEN',
},
},
},
);
},
),
// Query params with secrets
_chip(
'Query params',
Icons.link,
Colors.indigo.shade400,
() {
_dio.get(
'https://dummyjson.com/products/search'
'?q=phone'
'&api_key=sk-12345'
'&token=${_accessToken ?? 'NO_TOKEN'}',
);
},
),
// HTTP package with real token
_chip(
'HTTP + auth',
Icons.http,
Colors.deepPurple.shade400,
() {
_httpClient.get(
Uri.parse('https://dummyjson.com/auth/me'),
headers: {
'Authorization': 'Bearer ${_accessToken ?? 'NO_TOKEN'}',
'X-Api-Key': 'sk-proj-abc123def456',
},
);
},
),
// HTTP package login with creds
_chip(
'HTTP + creds body',
Icons.password,
Colors.red.shade400,
() {
_httpClient.post(
Uri.parse('https://dummyjson.com/auth/login'),
body: '{'
'"username":"emilys",'
'"password":"emilyspass",'
'"expiresInMins":1'
'}',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic dXNlcjpwYXNzd29yZA==',
},
);
},
),
],
),
const SizedBox(height: 16),
// Database
Text(
'Database Queries',
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_chip('SELECT users', Icons.storage, Colors.indigo, () {
_runDbExample();
}),
_chip('INSERT user', Icons.person_add, Colors.green, () {
_runDbInsert();
}),
_chip('UPDATE user', Icons.edit_note, Colors.orange, () {
_runDbUpdate();
}),
_chip('DELETE user', Icons.person_remove, Colors.red, () {
_runDbDelete();
}),
_chip('Transaction', Icons.swap_horiz, Colors.purple, () {
_runDbTransaction();
}),
_chip('KV get', Icons.key, Colors.brown, () {
_runDbKeyValue();
}),
_chip('Slow query', Icons.hourglass_bottom, Colors.amber, () {
_runDbSlowQuery();
}),
],
),
],
),
),
);
}
Widget _chip(
String label,
IconData icon,
Color color,
VoidCallback onTap,
) {
return ActionChip(
avatar: Icon(icon, size: 18, color: color),
label: Text(label),
onPressed: () {
setState(() => _isLoading = true);
onTap();
Future<void>.delayed(const Duration(seconds: 1), () {
if (mounted) setState(() => _isLoading = false);
});
},
);
}
// -- DB examples --
Future<void> _runDbExample() async {
await ISpect.logger.dbTrace<List<Map<String, Object?>>>(
source: 'drift',
operation: 'query',
table: 'users',
statement: 'SELECT * FROM users WHERE active = ? ORDER BY name LIMIT 10',
args: [true],
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 15));
return [
{'id': 1, 'name': 'Alice', 'active': true},
{'id': 2, 'name': 'Bob', 'active': true},
{'id': 3, 'name': 'Charlie', 'active': true},
];
},
projectResult: (rows) => {'rows': rows.length},
);
}
Future<void> _runDbInsert() async {
await ISpect.logger.dbTrace<int>(
source: 'drift',
operation: 'insert',
table: 'users',
statement: "INSERT INTO users (name, email, active) VALUES (?, ?, ?)",
args: ['Dave', 'dave@example.com', true],
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 8));
return 4;
},
projectResult: (id) => {'insertedId': id},
);
}
Future<void> _runDbUpdate() async {
await ISpect.logger.dbTrace<int>(
source: 'drift',
operation: 'update',
table: 'users',
statement: 'UPDATE users SET name = ?, email = ? WHERE id = ?',
args: ['Dave Updated', 'dave_new@example.com', 4],
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 10));
return 1;
},
projectResult: (affected) => {'affectedRows': affected},
);
}
Future<void> _runDbDelete() async {
await ISpect.logger.dbTrace<int>(
source: 'drift',
operation: 'delete',
table: 'users',
statement: 'DELETE FROM users WHERE id = ?',
args: [4],
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 5));
return 1;
},
projectResult: (affected) => {'deletedRows': affected},
);
}
Future<void> _runDbTransaction() async {
await ISpect.logger.dbTransaction(
source: 'drift',
logMarkers: true,
run: () async {
await ISpect.logger.dbTrace<int>(
source: 'drift',
operation: 'update',
table: 'accounts',
statement: 'UPDATE accounts SET balance = balance - ? WHERE id = ?',
args: [100, 1],
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 5));
return 1;
},
);
await ISpect.logger.dbTrace<int>(
source: 'drift',
operation: 'update',
table: 'accounts',
statement: 'UPDATE accounts SET balance = balance + ? WHERE id = ?',
args: [100, 2],
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 5));
return 1;
},
);
},
);
}
Future<void> _runDbKeyValue() async {
await ISpect.logger.dbTrace<String?>(
source: 'hive',
operation: 'get',
key: 'session_token',
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 3));
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
},
);
await ISpect.logger.dbTrace<bool>(
source: 'shared_prefs',
operation: 'write',
key: 'onboarding_done',
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 2));
return true;
},
);
}
Future<void> _runDbSlowQuery() async {
await ISpect.logger.dbTrace<List<Map<String, Object?>>>(
source: 'drift',
operation: 'query',
table: 'analytics',
statement:
'SELECT user_id, COUNT(*) as cnt, AVG(duration) as avg_dur FROM analytics GROUP BY user_id HAVING cnt > ? ORDER BY avg_dur DESC',
args: [10],
run: () async {
await Future<void>.delayed(const Duration(milliseconds: 500));
return [
{'user_id': 1, 'cnt': 42, 'avg_dur': 1250.5},
{'user_id': 7, 'cnt': 28, 'avg_dur': 980.3},
];
},
projectResult: (rows) => {'rows': rows.length},
);
}
}
// ---------------------------------------------------------------------------
// Slider control
// ---------------------------------------------------------------------------
class _SliderControl extends StatelessWidget {
const _SliderControl({
required this.label,
required this.value,
required this.min,
required this.max,
required this.divisions,
required this.displayValue,
required this.onChanged,
});
final String label;
final double value;
final double min;
final double max;
final int divisions;
final String displayValue;
final ValueChanged<double> onChanged;
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(
width: 72,
child: Text(label, style: Theme.of(context).textTheme.bodyMedium),
),
Expanded(
child: Slider(
value: value,
min: min,
max: max,
divisions: divisions,
label: displayValue,
onChanged: onChanged,
),
),
SizedBox(
width: 48,
child: Text(
displayValue,
style: Theme.of(context).textTheme.titleSmall,
textAlign: TextAlign.end,
),
),
],
);
}
}