Rybbit Flutter SDK
⚠️ Note: This is an unofficial, community-maintained Flutter SDK for Rybbit Analytics. While functional, this package may have incomplete features or limitations compared to the official web SDK. Use at your own discretion.
A Flutter client SDK for Rybbit Analytics - a modern, open-source web & product analytics platform. Track events, pageviews, and user interactions in your Flutter applications across mobile, web, and desktop.
Features
✨ Comprehensive Analytics Tracking
- 📊 Pageview tracking with automatic screen navigation detection
- 🎯 Custom event tracking with arbitrary properties
- 🔗 Outbound link tracking (external URLs, deep links, app store links)
- ❌ Error tracking with stack traces and context
- 👤 User identification with traits (custom user properties)
- 📱 App lifecycle tracking (foreground/background)
- 📦 Offline event queueing — events are persisted locally and sent automatically when connectivity is restored
Installation
Add rybbit_flutter to your pubspec.yaml:
dependencies:
rybbit_flutter: ^0.6.0
Run:
flutter pub get
Quick Start
1. Get Your Credentials
- Sign up at Rybbit Analytics
- Create a new site/project
- Copy your Site ID
Create the site as an app/mobile site, not a website: Rybbit skips its
browser-shaped bot detection layers for app sites. The Site ID is all the SDK
needs. An API Key is not required, and if you use one anyway it should carry
only the ingest:write scope. See Authentication.
2. Initialize the SDK
import 'package:rybbit_flutter/rybbit_flutter.dart';
// Initialize in your main() function or app startup
await RybbitFlutter.instance.initialize(
RybbitConfig(
siteId: 'your_site_id',
enableLogging: true, // Enable for development
),
);
3. Add Route Observer (Optional)
For automatic screen tracking, add the route observer to your MaterialApp:
MaterialApp(
navigatorObservers: [
RybbitFlutter.instance.routeObserver,
],
// ... rest of your app
)
4. Start Tracking
// Track a pageview manually
await RybbitFlutter.instance.trackPageView(
pathname: '/home',
pageTitle: 'Home Screen',
queryParams: {
'utm_source': 'google',
'utm_medium': 'cpc',
'utm_campaign': 'spring_sale',
},
);
// Track custom events
await RybbitFlutter.instance.trackEvent(
'button_clicked',
properties: {
'button_id': 'login_button',
'user_type': 'premium',
'value': 29.99,
},
);
// Identify users (with optional traits)
await RybbitFlutter.instance.identify('user123', traits: {
'name': 'John Doe',
'plan': 'premium',
});
// Track outbound links (web URLs, deep links, app store links)
await RybbitFlutter.instance.trackOutboundLink(
'https://play.google.com/store/apps/details?id=com.example.app',
text: 'Download Our App',
);
// Track errors with context
try {
await riskyOperation();
} catch (e, stackTrace) {
await RybbitFlutter.instance.trackError(
'NetworkError',
'Failed to load user data: ${e.toString()}',
stackTrace: stackTrace.toString(),
fileName: 'user_service.dart',
lineNumber: 42,
);
}
Authentication
An API key is not required. POST /api/track and POST /api/identify are
public ingestion endpoints: they identify the site from site_id in the
payload, and a request with no Authorization header is accepted normally. This
is verified against the Rybbit server, where an under-scoped or absent bearer
token degrades to ordinary client-side traffic rather than a 4xx.
Leaving apiKey unset is therefore the recommended setup, and means no
credential ships inside your app binary.
What an API key actually changes
A key that carries the ingest:write scope for the site marks the request as
trusted server-side ingestion, which lets the payload speak for someone else:
ip_addressanduser_agentfrom the payload are used instead of the ones on the HTTP request. Without a key, both are taken from the request itself.- Bot detection treats the request as first-party traffic and only classifies the reported user agent.
That matters when you forward events from your own backend, not when a device reports its own activity. A Flutter app sends its real request headers, so it does not need any of it.
If you do use a key, create it with only the ingest:write scope. Anything
in an APK or IPA can be extracted from it, so a personal or organization key
with broad read/write access must never be bundled into an app. The alternative
is to point analyticsHost at a backend of your own and attach the key there,
so nothing sensitive ships with the app at all.
// Recommended: no credential in the binary
const config = RybbitConfig(siteId: 'your_site_id');
// Only if you have a reason to: a key scoped to ingest:write
const config = RybbitConfig(
siteId: 'your_site_id',
apiKey: 'rb_ingest_write_key',
);
// Or keep the key server side entirely
const config = RybbitConfig(
siteId: 'your_site_id',
analyticsHost: 'https://analytics-proxy.yourcompany.com',
);
Create the site as an app site
Rybbit's bot detection has two browser-shaped layers: it reads a native HTTP client's user agent as a scripting framework, and treats missing browser-only headers as suspicious. Both are skipped for sites whose type is app/mobile, so create your site that way in Rybbit rather than as a website.
The SDK also sends Accept and Accept-Language on every request, which keeps
it under the header heuristic threshold on sites that are typed as websites.
Rejected events still return {"success":true}, so if events never show up in
your dashboard this is worth checking before anything else.
Usage Examples
Configuration Options
const config = RybbitConfig(
siteId: 'your_site_id',
// Optional: not required for tracking (see Authentication)
apiKey: 'rb_your_api_key_here',
// Optional: Custom analytics server
analyticsHost: 'https://analytics.yourcompany.com',
// Optional: Network and retry settings
requestTimeout: Duration(seconds: 15),
maxRetries: 5,
// Optional: Tracking behavior
trackScreenViews: true, // Auto-track route changes
trackAppLifecycle: true, // Track app foreground/background
trackQuerystring: true, // Include query parameters in pageviews
autoTrackPageview: true, // Track initial pageview on init
// Optional: Skip patterns (simple wildcards supported)
skipPatterns: [
'/debug/*', // Skip all debug pages
'/admin/internal/*', // Skip internal admin pages
'*/temp', // Skip any temp pages
],
// Optional: Offline queueing
enableOfflineQueue: true, // Persist events when offline (default: true)
maxQueueSize: 1000, // Max queued events before oldest are dropped
// Optional: Debug settings
enableLogging: false, // Debug logging
);
Manual Screen Tracking
If you prefer manual control over screen tracking:
// Disable automatic tracking and skip certain patterns
const config = RybbitConfig(
siteId: 'your_site_id',
trackScreenViews: false,
skipPatterns: ['/debug/*', '/internal/*'],
);
// Track screens manually in your State class
class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
RybbitFlutter.instance.trackPageView(
pathname: '/home',
pageTitle: 'Home Screen',
queryParams: {
'tab': 'featured',
'source': 'navigation',
},
);
}
}
E-commerce Tracking
// Track purchases
await RybbitFlutter.instance.trackEvent(
'purchase',
properties: {
'transaction_id': 'txn_123',
'revenue': 99.99,
'currency': 'USD',
'items': [
{
'item_id': 'prod_123',
'item_name': 'Premium Plan',
'category': 'subscription',
'quantity': 1,
'price': 99.99,
}
],
},
);
// Track cart actions
await RybbitFlutter.instance.trackEvent(
'add_to_cart',
properties: {
'item_id': 'prod_456',
'item_name': 'Widget Pro',
'category': 'widgets',
'value': 29.99,
},
);
Outbound Link Tracking
Note: Unlike the web SDK, outbound link tracking in Flutter requires explicit manual calls. This is because Flutter apps launch URLs programmatically (via url_launcher) rather than through DOM click events. Call trackOutboundLink() before launching URLs with launchUrl().
// Track external website visits
await RybbitFlutter.instance.trackOutboundLink(
'https://docs.flutter.dev',
text: 'Flutter Documentation',
);
// Track app store links
await RybbitFlutter.instance.trackOutboundLink(
'https://apps.apple.com/app/id123456789',
text: 'Download from App Store',
);
// Track deep links to other apps
await RybbitFlutter.instance.trackOutboundLink(
'instagram://user?username=yourcompany',
text: 'Follow on Instagram',
);
// Track email/phone links
await RybbitFlutter.instance.trackOutboundLink(
'mailto:support@example.com',
text: 'Contact Support',
);
// Track map/navigation links
await RybbitFlutter.instance.trackOutboundLink(
'https://maps.google.com/?q=coffee+near+me',
text: 'Find Coffee Nearby',
);
User Management
// Identify logged-in users with traits
await RybbitFlutter.instance.identify('user_12345', traits: {
'name': 'Jane Smith',
'email': 'jane@example.com',
'plan': 'premium',
});
// Update traits later without creating a new alias
await RybbitFlutter.instance.setTraits({
'plan': 'enterprise', // Update existing trait
'company': 'Acme Corp', // Add new trait
'old_field': null, // Remove a trait by setting to null
});
// Track user properties with events
await RybbitFlutter.instance.trackEvent(
'profile_updated',
properties: {
'plan_type': 'premium',
'account_age_days': 45,
'features_enabled': ['advanced_search', 'export'],
},
);
// Clear user ID on logout
RybbitFlutter.instance.clearUserId();
Error Tracking
// Track errors with full context
try {
await authenticateUser(credentials);
} catch (e, stackTrace) {
await RybbitFlutter.instance.trackError(
'AuthenticationError',
'Login failed: ${e.toString()}',
stackTrace: stackTrace.toString(),
fileName: 'auth_service.dart',
lineNumber: 156,
pathname: '/login',
pageTitle: 'Login Screen',
);
rethrow;
}
// Track different error types
await RybbitFlutter.instance.trackError(
'ValidationError',
'Email format is invalid',
fileName: 'validators.dart',
lineNumber: 23,
pathname: '/signup',
);
// Track network errors
await RybbitFlutter.instance.trackError(
'NetworkError',
'Request timeout after 30 seconds',
stackTrace: stackTrace?.toString(),
fileName: 'api_client.dart',
lineNumber: 89,
pathname: '/dashboard',
);
// Track custom business logic errors
await RybbitFlutter.instance.trackError(
'PaymentError',
'Insufficient funds for transaction',
fileName: 'payment_service.dart',
lineNumber: 234,
);
Offline Queueing
Events are persisted to local storage (Hive) when the device is offline and flushed automatically when connectivity is restored or the app resumes from the background. This happens transparently — no changes to your tracking calls are required.
// This event will be queued if the device is offline and sent later
await RybbitFlutter.instance.trackEvent('purchase_completed', properties: {
'order_id': 'ord_789',
'revenue': 49.99,
});
To disable offline queueing:
const config = RybbitConfig(
siteId: 'your_site_id',
enableOfflineQueue: false,
);
To cap the queue at a lower number (e.g., for memory-constrained devices):
const config = RybbitConfig(
siteId: 'your_site_id',
maxQueueSize: 100,
);
Note: The offline queue requires the
connectivity_plusplatform plugin. On Android this needs theACCESS_NETWORK_STATEpermission, which is automatically declared by the plugin.
Performance Tracking
// Track performance metrics with custom events
final stopwatch = Stopwatch()..start();
await loadData();
stopwatch.stop();
await RybbitFlutter.instance.trackEvent(
'data_load_performance',
properties: {
'duration_ms': stopwatch.elapsedMilliseconds,
'data_size': dataSize,
'cache_hit': wasCacheHit,
},
);
API Reference
RybbitFlutter
Methods
initialize(RybbitConfig config)- Initialize the SDK with configurationtrackPageView({required String pathname, String? pageTitle, String? referrer, Map<String, String>? queryParams})- Track a page/screen view with optional query parameterstrackEvent(String eventName, {Map<String, dynamic>? properties, String? pathname, String? pageTitle})- Track a custom eventtrackOutboundLink(String url, {String? text, String? pathname})- Track external link clickstrackError(String errorName, String message, {String? stackTrace, String? fileName, int? lineNumber, int? columnNumber, String? pathname, String? pageTitle})- Track application errors with contextidentify(String userId, {Map<String, dynamic>? traits})- Associate events with a user ID and optionally store custom traits on the serversetTraits(Map<String, dynamic> traits)- Update traits for the current identified user without creating a new aliasclearUserId()- Clear the current user IDdispose()- Clean up resources (call when app is disposed)
Query Parameters
The queryParams parameter accepts a Map<String, String> and automatically converts it to a proper query string format:
await RybbitFlutter.instance.trackPageView(
pathname: '/products',
queryParams: {
'utm_source': 'google',
'utm_medium': 'cpc',
'utm_campaign': 'spring_sale',
'category': 'electronics',
},
);
// Automatically becomes: ?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale&category=electronics
Properties
isInitialized- Whether the SDK has been initializeduserId- Current user ID (if set)routeObserver- Route observer for automatic screen tracking
RybbitConfig
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
siteId |
String | ✅ | - | Your Rybbit site ID |
apiKey |
String? | ❌ | null |
Not required for tracking. Sent as a Bearer token when set; no Authorization header is sent when null |
analyticsHost |
String | ❌ | https://app.rybbit.io |
Analytics server URL |
enableLogging |
bool | ❌ | false |
Enable debug logging |
requestTimeout |
Duration | ❌ | 10s |
Network request timeout |
maxRetries |
int | ❌ | 3 |
Max retry attempts |
trackScreenViews |
bool | ❌ | true |
Auto-track route changes |
trackAppLifecycle |
bool | ❌ | true |
Track app state changes |
trackQuerystring |
bool | ❌ | true |
Include query params in pageviews |
autoTrackPageview |
bool | ❌ | true |
Track initial pageview on init |
skipPatterns |
List | ❌ | [] |
URL patterns to skip (supports * wildcards) |
enableOfflineQueue |
bool | ❌ | true |
Persist events locally when offline and retry on reconnect |
maxQueueSize |
int | ❌ | 1000 |
Maximum events to hold in the offline queue |
Platform Support
| Platform | Support | Notes |
|---|---|---|
| ✅ Android | Full | Device info, screen metrics, deep links |
| ✅ iOS | Full | Device info, screen metrics, deep links |
| ✅ Web | Full | WASM support, full outbound link tracking |
| ✅ macOS | Full | Desktop support |
| ✅ Windows | Full | Desktop support |
| ✅ Linux | Full | Desktop support |
Requirements:
- Flutter 3.41.0 or higher
Troubleshooting
Common Issues
Events not appearing in dashboard:
- Verify your Site ID is correct
- Check whether the events were classified as bot traffic. Rybbit answers
{"success":true}either way and files rejected events underbot_eventsinstead ofevents, so a 200 is not proof an event was stored. Creating the site as an app/mobile site avoids the browser-shaped detection layers (see Authentication) - Check network connectivity
- Enable logging to see debug information
- Ensure you're calling
initialize()before tracking events
Route observer not working:
- Make sure you've added the route observer to
MaterialApp - Verify routes have
settings.namedefined - Check that
trackScreenViewsis enabled
Build errors:
- Run
flutter pub getafter adding the dependency - Check that your Flutter SDK version meets requirements (≥3.32.0)
- For web builds, ensure CORS is properly configured on your analytics server
Debug Mode
Enable debug logging to troubleshoot issues:
const config = RybbitConfig(
siteId: 'your_site_id',
enableLogging: true,
);
This will print detailed information about:
- Initialization status
- Event tracking attempts
- Network requests and responses
- Error details
Contributing
Contributions are welcome! I have no specific contribution guide, but please raise an issue or PR with proper description
Development Setup
# Clone the repository
git clone https://github.com/stijnie2210/rybbit-flutter.git
cd rybbit-flutter
# Install dependencies
flutter pub get
# Run tests
flutter test
# Run analysis
flutter analyze
Support
- 📖 Documentation: rybbit.io/docs
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
License
This project is licensed under the MIT License - see the LICENSE file for details.