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: '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 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": "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 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 Space β€’ Telling Dashboard β€’ Twitter β€’ GitHub

Libraries

telling_logger