telling_logger 1.4.3
telling_logger: ^1.4.3 copied to clipboard
Comprehensive crash reporting, error tracking, and analytics SDK for Flutter applications. Track errors, monitor performance, and gain insights into your app's behavior.
Telling Logger π #
A production-ready crash reporting, error tracking, and analytics SDK for Flutter applications. Monitor your app's health, track user behavior, and gain actionable insights with minimal setup.
β¨ Features #
Core Capabilities #
- π Automatic Crash Reporting β Captures unhandled Flutter framework and platform errors
- π Event Analytics β Track custom events, user actions, and business metrics
- π± Rich Device Context β Auto-collects platform, OS version, device model, and app info
- π Session Management β Automatic session tracking with app lifecycle hooks
- π Screen Tracking β Built-in NavigatorObserver for automatic screen view analytics
- π€ User Context β Associate logs with user IDs, names, and emails
- π― Widget-Level Tracking β
.nowTelling()extension for effortless view tracking - β‘ Smart Batching β Efficient log deduplication and batching to minimize network overhead
- οΏ½ Offline Support β Persists logs when offline, auto-sends when connection is restored
- π‘οΈ Rate Limiting β Built-in deduplication, throttling, and flood protection
- π Cross-Platform β Works on iOS, Android, Web, macOS, Windows, and Linux
Developer Experience #
- π 5-Minute Setup β Initialize with a single line of code
- π Production-Safe Logging β Debug logs automatically stripped from release builds
- π¨ Flexible API β Multiple log levels, types, and metadata support
π¦ Installation #
Add telling_logger to your pubspec.yaml:
dependencies:
telling_logger: ^1.4.3
Then install:
flutter pub get
π Get Your API Key #
To use Telling Logger, you need an API key.
- Go to the Telling Dashboard.
- Log in or create an account.
- Create a new project to obtain your API key.
π Quick Start #
1. Initialize the SDK #
In your main.dart, initialize Telling before running your app:
import 'package:flutter/material.dart';
import 'package:telling_logger/telling_logger.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Telling SDK
await Telling.instance.init(
'YOUR_API_KEY',
enableDebugLogs: true, // Optional: Control debug logs (defaults to true in debug mode)
);
// Enable automatic crash reporting
Telling.instance.enableCrashReporting();
runApp(MyApp());
}
2. Log Events #
// Simple info log
Telling.instance.log('User completed onboarding');
// Log with metadata
Telling.instance.log(
'Payment processed',
level: LogLevel.info,
metadata: {
'amount': 29.99,
'currency': 'USD',
'payment_method': 'stripe',
},
);
// Error logging
try {
await processPayment();
} catch (e, stack) {
Telling.instance.captureException(
error: e,
stackTrace: stack,
context: 'payment_processing',
metadata: {'amount': 29.99},
);
}
3. Track Analytics #
// Track custom events
Telling.instance.event(
'button_clicked',
properties: {
'button_name': 'Sign Up',
'screen': 'Landing Page',
'user_segment': 'free_trial',
},
);
// Track funnel steps (IMPORTANT: call setUser first!)
// Telling.instance.setUser(userId: 'user123'); // Must be called before funnel tracking
Telling.instance.trackFunnel(
funnelName: 'onboarding',
stepName: 'email_entered',
step: 1,
properties: {'source': 'google_ads'},
);
π Core Concepts #
Log Levels #
Control the severity and visibility of your logs:
| Level | Use Case | Severity |
|---|---|---|
LogLevel.trace |
Extremely detailed debugging | 0 |
LogLevel.debug |
Detailed diagnostic information | 1 |
LogLevel.info |
General informational messages | 2 |
LogLevel.warning |
Potentially harmful situations | 3 |
LogLevel.error |
Runtime errors that allow continuation | 4 |
LogLevel.fatal |
Critical errors causing termination | 5 |
Log Types #
Categorize logs for better filtering and analytics:
| Type | Purpose |
|---|---|
LogType.general |
Standard application logs, debug, operational |
LogType.analytics |
User events, funnels, screen views, tracking |
LogType.crash |
Errors, exceptions, and crashes |
LogType.performance |
Performance metrics and benchmarks |
π― Advanced Features #
Exception Capture #
Report handled exceptions from try-catch blocks without crashing your app:
try {
await riskyOperation();
} catch (e, stackTrace) {
Telling.instance.captureException(
error: e,
stackTrace: stackTrace,
context: 'checkout_flow', // Where did this happen?
metadata: {'orderId': '12345'},
);
// Handle gracefully for the user
showErrorDialog('Something went wrong');
}
TellingTryCatch Mixin #
For cleaner code, use the TellingTryCatch mixin to automatically capture exceptions:
class PaymentService with TellingTryCatch {
// Async operation that returns a value (or null on failure)
Future<Receipt?> processPayment(double amount) async {
return tryRun(
context: 'process_payment',
metadata: {'amount': amount},
func: () async {
final result = await api.charge(amount);
return Receipt.fromJson(result);
},
onSuccess: () => print('Payment succeeded!'),
onError: (e, stack) => showToast('Payment failed'),
);
}
// Async operation with no return value
Future<void> saveReceipt(Receipt receipt) async {
await tryRunVoid(
context: 'save_receipt',
func: () async => await storage.save(receipt),
);
}
// Synchronous operation
Config? parseConfig(String json) {
return tryRunSync(
context: 'parse_config',
func: () => Config.fromJson(jsonDecode(json)),
);
}
}
User Context Tracking #
Associate logs with specific users for better debugging and analytics:
// Set user context after login
Telling.instance.setUser(
userId: 'user_12345',
userName: 'Jane Doe',
userEmail: 'jane@example.com',
);
// Clear user context after logout
Telling.instance.clearUser();
All subsequent logs will automatically include user information until cleared.
User Properties #
Track custom user attributes for segmentation and personalization:
// Set individual properties
Telling.instance.setUserProperty('subscription_tier', 'premium');
Telling.instance.setUserProperty('plan_renewal_date', '2025-12-31');
// Set multiple properties at once
Telling.instance.setUserProperties({
'subscription_tier': 'premium',
'mrr': 99.99,
'seats': 5,
'industry': 'SaaS',
});
// Get property value
final tier = Telling.instance.getUserProperty('subscription_tier');
// Clear properties
Telling.instance.clearUserProperty('mrr');
Telling.instance.clearUserProperties(); // Clear all
User properties are automatically included in all log events, enabling powerful segmentation in your analytics dashboard.
Automatic Performance Tracking (Coming Soon) #
Automatic Screen Tracking #
With MaterialApp
MaterialApp(
navigatorObservers: [
Telling.instance.screenTracker,
],
home: HomeScreen(),
)
With go_router
final router = GoRouter(
observers: [
Telling.instance.goRouterScreenTracker,
],
routes: [...],
);
MaterialApp.router(
routerConfig: router,
)
Screen views are automatically logged with:
- Screen name
- Previous screen
- Time spent on previous screen
- Session context
Widget-Level Tracking #
Use the .nowTelling() extension to track any widget's visibility:
import 'package:telling_logger/telling_logger.dart';
// Basic usage - tracks when widget appears
Column(
children: [
Text('Welcome!'),
],
).nowTelling()
// With custom name
ProductCard(product: item).nowTelling(
name: 'Product Card Impression',
)
// With metadata for context
PremiumFeature().nowTelling(
name: 'Premium Feature Shown',
metadata: {
'feature_id': 'dark_mode',
'user_tier': 'free',
},
)
// Track every appearance (not just once)
AdBanner().nowTelling(
name: 'Banner Ad Impression',
trackOnce: false,
metadata: {'ad_id': 'banner_123'},
)
// Custom log type and level
CriticalAlert().nowTelling(
name: 'Security Alert Displayed',
type: LogType.security,
level: LogLevel.warning,
)
Parameters:
nameβ Custom name (defaults to widget's runtimeType)typeβ Log type (defaults toLogType.analytics)levelβ Log level (defaults toLogLevel.info)metadataβ Additional context datatrackOnceβ Track only first appearance (defaults totrue)
Funnel Tracking #
Track user journeys through multi-step flows to identify drop-off points and optimize conversion rates.
Caution
Critical: Set User Context First
Always call Telling.instance.setUser() BEFORE tracking funnel steps. If you call setUser() mid-funnel, the backend will treat events before and after as different users, breaking your conversion tracking.
Basic Usage
void trackFunnel({
required String funnelName,
required String stepName,
int? step,
Map<String, dynamic>? properties,
});
Parameters:
funnelNameβ Unique identifier for the entire flow (must be consistent across all steps)stepNameβ Descriptive name for this specific stepstepβ Optional but recommended: Sequential step number (1, 2, 3...)propertiesβ Optional additional metadata for this step
Example: User Onboarding
// Set user context first (critical!)
final tempUserId = 'anon_${DateTime.now().millisecondsSinceEpoch}';
Telling.instance.setUser(userId: tempUserId);
// Step 1: User lands on welcome screen
Telling.instance.trackFunnel(
funnelName: 'user_onboarding',
stepName: 'welcome_viewed',
step: 1,
);
// Step 2: User clicks "Get Started"
Telling.instance.trackFunnel(
funnelName: 'user_onboarding',
stepName: 'get_started_clicked',
step: 2,
);
// Step 3: User submits registration
Telling.instance.trackFunnel(
funnelName: 'user_onboarding',
stepName: 'registration_submitted',
step: 3,
properties: {'method': 'email'},
);
// Step 4: User completes profile (conversion!)
Telling.instance.trackFunnel(
funnelName: 'user_onboarding',
stepName: 'profile_completed',
step: 4,
);
Example: E-Commerce Checkout
final checkoutFunnel = 'checkout_flow';
// Step 1: Cart viewed
Telling.instance.trackFunnel(
funnelName: checkoutFunnel,
stepName: 'cart_viewed',
step: 1,
properties: {'item_count': 2, 'total_value': 49.99},
);
// Step 2: Shipping started
Telling.instance.trackFunnel(
funnelName: checkoutFunnel,
stepName: 'shipping_started',
step: 2,
);
// Step 3: Shipping completed
Telling.instance.trackFunnel(
funnelName: checkoutFunnel,
stepName: 'shipping_completed',
step: 3,
properties: {'address_length': 45},
);
// Step 4: Payment successful (conversion!)
Telling.instance.trackFunnel(
funnelName: checkoutFunnel,
stepName: 'payment_completed',
step: 4,
properties: {'payment_method': 'credit_card'},
);
Best Practices
- Consistent Naming: Use the exact same
funnelNameacross all steps (e.g.,'checkout_flow', not'checkout'then'checkout_flow') - Sequential Steps: Always provide
stepnumbers (1, 2, 3...) for reliable analysis - Descriptive Step Names: Use action-oriented names (
'payment_completed'not'step4') - Enrich with Properties: Add context that explains drop-offs (
{'cart_value': 99.99, 'payment_method': 'paypal'}) - Track Start & End: Capture the first step to establish baseline conversion rates
- Anonymous Users: Generate a temporary user ID for anonymous flows:
final tempUserId = 'anon_${uuid.v4()}'; Telling.instance.setUser(userId: tempUserId);
Tip
For detailed funnel tracking implementation patterns and troubleshooting, see the Funnel Tracking Guide.
Session Management #
Sessions are automatically managed based on app lifecycle:
- Session Start: When app launches or returns from background
- Session End: When app goes to background or terminates
- Session Data: Duration, user context, device info
// Session data is automatically included in all logs
{
"sessionId": "user_123_1700745600000",
"userId": "user_123",
"userName": "Jane Doe",
"userEmail": "jane@example.com"
}
Crash Reporting #
Enable automatic crash capture for both Flutter and platform errors:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Telling.instance.init('YOUR_API_KEY');
// Captures:
// - Flutter framework errors (FlutterError.onError)
// - Platform dispatcher errors (PlatformDispatcher.onError)
// - Render issues (marked as warnings)
Telling.instance.enableCrashReporting();
runApp(MyApp());
}
Crash Intelligence:
- Render/layout issues are logged as
warnings - Actual crashes are logged as
fatalerrors - Full stack traces included
- Automatic retry with exponential backoff
Remote Config & Force Update #
Manage your app's version and configuration remotely from the Telling Dashboard.
Check for Updates
// Check on app startup (e.g., in main.dart or Splash Screen)
final result = await Telling.instance.checkVersion();
if (result.requiresUpdate) {
if (result.isRequired) {
// Force update - show blocking UI, open store
} else {
// Optional update - user can skip
}
}
Snooze for Optional Updates
When the user skips an optional update, call snoozeUpdate() to suppress the prompt:
final result = await Telling.instance.checkVersion();
if (result.requiresUpdate && !result.isRequired) {
final skipped = await showUpdateDialog();
if (skipped && result.minVersion != null) {
await Telling.instance.snoozeUpdate(
days: 3, // 0 = no snooze, 1-3 = days to suppress
minVersion: result.minVersion!,
);
}
}
Snooze Behavior:
- 0 days: No snooze, prompt every app launch
- 1-3 days: Suppress prompt until snooze expires
- Version change: Snooze resets when you bump the minimum version
Track Update Acceptance
When the user accepts an update, call acceptUpdate() before opening the store URL:
if (shouldUpdate == true) {
await Telling.instance.acceptUpdate(minVersion: result.minVersion);
launchUrl(Uri.parse(result.storeUrl!));
}
Automatic Update Analytics
The SDK automatically tracks update-related events internally:
| Event | When Logged | Properties |
|---|---|---|
update_check_completed |
Every checkVersion() call |
requires_update, is_required, min_version, current_version, is_snoozed |
update_prompted |
When update is required (not snoozed) | is_required, min_version, current_version |
update_snoozed |
When snoozeUpdate() is called |
snooze_days, min_version, current_version |
update_accepted |
When acceptUpdate() is called |
min_version, current_version |
This gives you a complete funnel: check β prompt β (snooze OR accept) without any manual event logging.
π§ Configuration Options #
Initialization Parameters #
await Telling.instance.init(
String apiKey, // Required: Your API key
{
String? userId, // Initial user ID
String? userName, // Initial user name
String? userEmail, // Initial user email
bool? enableDebugLogs, // Control debug logs (defaults to kDebugMode)
}
);
Environment-Specific Setup #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
const isProduction = bool.fromEnvironment('dart.vm.product');
await Telling.instance.init(
isProduction ? 'PROD_API_KEY' : 'DEV_API_KEY',
);
runApp(MyApp());
}
Debug Logs Control #
// Disable debug logs even in debug mode
await Telling.instance.init(
'YOUR_API_KEY',
enableDebugLogs: false,
);
// Enable debug logs explicitly
await Telling.instance.init(
'YOUR_API_KEY',
enableDebugLogs: true,
);
// Default behavior (true in debug mode, false in release)
await Telling.instance.init(
'YOUR_API_KEY',
);
## π Best Practices
### 1. Initialize Early
```dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize BEFORE runApp
await Telling.instance.init('API_KEY');
Telling.instance.enableCrashReporting();
runApp(MyApp());
}
2. Use Appropriate Log Levels #
// β
Good
Telling.instance.log('User login successful', level: LogLevel.info);
Telling.instance.log('Database query slow', level: LogLevel.warning);
Telling.instance.log('Payment failed', level: LogLevel.error);
// β Avoid
Telling.instance.log('Button clicked', level: LogLevel.fatal); // Wrong severity
3. Add Context with Metadata #
// β
Good - rich context
Telling.instance.event('purchase_completed', properties: {
'product_id': 'premium_monthly',
'price': 9.99,
'currency': 'USD',
'payment_method': 'stripe',
'user_segment': 'trial_converted',
});
// β Poor - no context
Telling.instance.event('purchase');
4. Track User Context #
// Set user context after authentication
await signIn(email, password);
Telling.instance.setUser(
userId: user.id,
userName: user.name,
userEmail: user.email,
);
// Clear on logout
await signOut();
Telling.instance.clearUser();
5. Use Widget Tracking Wisely #
// β
Good - track important screens/components
HomeScreen().nowTelling(name: 'Home Screen');
PremiumPaywall().nowTelling(name: 'Paywall Viewed');
// β Avoid - don't track every tiny widget
Text('Hello').nowTelling(); // Too granular
Container().nowTelling(); // Not meaningful
6. Handle Sensitive Data #
// β Don't log PII or sensitive data
Telling.instance.event('login', properties: {
'password': '123456', // NEVER
'credit_card': '4111...', // NEVER
});
// β
Hash or omit sensitive fields
Telling.instance.event('login', properties: {
'email_hash': hashEmail(user.email),
'login_method': 'email',
});
Common Errors #
"Telling SDK not initialized"
- Call
await Telling.instance.init()before using the SDK
"Invalid API Key" (403)
- Verify your API key is correct
- Check backend is running and accessible
"Logs being dropped"
- Reduce log volume or increase limits
π License #
MIT License - see the LICENSE file for details.
π Support #
- οΏ½ Documentation
- π Report Issues
- οΏ½ Discussions
- π§ Email: kiishidart@gmail.com
π Show Your Support #
If Telling Logger helped you build better apps, please:
- β Star this repo
- π¦ Share on Twitter
- π Write a blog post
- π¬ Tell your friends
Made with π by Kiishi
Kiishi's Space β’ Telling Dashboard β’ Twitter β’ GitHub