Telling Logger 📊

pub package License: MIT Flutter

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.

  1. Go to the Telling Dashboard.
  2. Log in or create an account.
  3. 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: '[email protected]',
);

// 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 to LogType.analytics)
  • level – Log level (defaults to LogLevel.info)
  • metadata – Additional context data
  • trackOnce – Track only first appearance (defaults to true)

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 step
  • step – 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

  1. Consistent Naming: Use the exact same funnelName across all steps (e.g., 'checkout_flow', not 'checkout' then 'checkout_flow')
  2. Sequential Steps: Always provide step numbers (1, 2, 3...) for reliable analysis
  3. Descriptive Step Names: Use action-oriented names ('payment_completed' not 'step4')
  4. Enrich with Properties: Add context that explains drop-offs ({'cart_value': 99.99, 'payment_method': 'paypal'})
  5. Track Start & End: Capture the first step to establish baseline conversion rates
  6. 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": "[email protected]"
}

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 fatal errors
  • 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

🌟 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 SpaceTelling DashboardTwitterGitHub

Libraries

telling_logger