analytics_sdk Free
Multi-vendor analytics abstraction with real interceptor chain, per-vendor routing, shared sanitization, consent/opt-out, and vendor self-registration.
Version: 2.0.0
Variables
| Variable | Type | Default | Description |
|---|---|---|---|
| appName | string | — | The name of your application |
| isForMonorepo | boolean | true | Whether this is being generated as part of a monorepo |
Architecture
The analytics system uses a multi-vendor dispatch pattern with an interceptor chain that sits between the caller and all registered vendors:
- analytics_api — Vendor-agnostic contracts (
AnalyticTracker,AnalyticVendor, event interfaces, interceptor chain, test harness) - analytics_impl — Orchestrator (
AnalyticTrackerImpl) that runs events through the interceptor chain, applies super-properties, enforces consent/opt-out, and dispatches to all registered vendors with error isolation - Vendor bricks — Per-vendor implementations that self-register into
AnalyticVendorRegistryvia theirRegisterModuleconstructor
Vendor Bricks
| Brick | Vendor | Key Feature |
|---|---|---|
firebase_analytics_impl | Firebase Analytics | Event/param sanitization (40 char limit) |
appsflyer_analytics_impl | AppsFlyer | Direct SDK pass-through |
mixpanel_analytics_impl | Mixpanel | People API + flush support |
moengage_analytics_impl | MoEngage | Properties builder pattern |
Multi-Select
During flutter_modular_monorepo generation, users select vendors via analyticsVendors (type: array):
? Which analytics vendors do you want? (Use space to select)
❯ ◉ firebase
◯ appsflyer
◯ mixpanel
◯ moengage
◯ noneOnly selected vendor bricks are generated. The orchestrator dispatches events to all registered vendors.
Generated Structure
infrastructure/
├── analytics_api/ # Contracts (always generated)
│ └── lib/src/
│ ├── data/ # AnalyticVendor, TrackData, AnalyticVendorRegistry
│ ├── events/ # AnalyticEvent + specialised event interfaces
│ ├── tracker/ # AnalyticTracker, AnalyticInterceptor, AnalyticInterceptorChain
│ ├── testing/ # InMemoryAnalyticTracker, RecordingAnalyticVendor
│ └── utils/ # AnalyticInjectorKey, SharedEventSanitizer
├── analytics_impl/ # Orchestrator (always generated)
│ └── lib/src/
│ ├── analytic_tracker_impl.dart # Multi-vendor dispatch + interceptor chain
│ ├── analytics_initializer.dart # DI bootstrap
│ └── di/ # Injectable config
└── <vendor>_analytics_impl/ # Per-vendor (only selected)
└── lib/src/
├── <vendor>_analytics_vendor.dart
└── di/
├── injector.dart
└── register_module.dartAnalyticTracker API
All 11 methods are available on the AnalyticTracker interface injected via GetIt:
final tracker = getIt<AnalyticTracker>();
// Track a custom event
await tracker.track(LoginEvent(method: 'email'));
// Track a screen view
await tracker.trackScreen('HomeScreen', params: {'source': 'push'});
// Identity
await tracker.setUserId('user_123');
await tracker.setUserProperty('plan', 'pro');
await tracker.setUserProperties({'plan': 'pro', 'country': 'ID'});
// Super-properties (merged into every subsequent event)
await tracker.registerSuperProperties({'app_version': '2.1.0'});
// Consent / opt-out
await tracker.setConsent(granted: true); // GDPR/CCPA consent signal
await tracker.optOut(); // Stop tracking immediately
await tracker.optIn(); // Resume tracking
// Lifecycle
await tracker.flush(); // Flush buffered events (vendors that support it)
await tracker.reset(); // Clear all data — call on logoutInterceptor Chain
Interceptors form a chain that wraps every track call. Each interceptor must call chain.proceed(data) to continue, or return null to drop the event silently.
class LoggingInterceptor extends AnalyticInterceptor {
@override
Future<TrackData?> intercept(TrackData data, AnalyticInterceptorChain chain) async {
debugPrint('[Analytics] ${data.name}: ${data.params}');
return chain.proceed(data); // must call proceed() or return null to drop
}
}An enriching interceptor can modify TrackData before passing it downstream:
class SessionInterceptor extends AnalyticInterceptor {
final String sessionId;
SessionInterceptor(this.sessionId);
@override
Future<TrackData?> intercept(TrackData data, AnalyticInterceptorChain chain) =>
chain.proceed(data.withProperties({'session_id': sessionId}));
}Additional lifecycle hooks available on AnalyticInterceptor (all default to no-op):
| Hook | Triggered by |
|---|---|
onSetUserId | tracker.setUserId |
onSetUserProperty | tracker.setUserProperty |
onSetUserProperties | tracker.setUserProperties |
onRegisterSuperProperties | tracker.registerSuperProperties |
onSetConsent | tracker.setConsent |
onOptOut | tracker.optOut |
onOptIn | tracker.optIn |
onFlush | tracker.flush |
onReset | tracker.reset |
onError | any vendor throws (informational only — do not re-throw) |
Event Types
All event types are interfaces. Implement them alongside AnalyticEvent to unlock vendor-specific behaviour.
AnalyticEvent (base)
class LoginEvent with AnalyticEventMixin implements AnalyticEvent {
const LoginEvent({required this.method, this.isFirstLogin = false});
final String method;
final bool isFirstLogin;
@override
String get name => 'login';
@override
Map<String, dynamic> toJson() => {
'method': method,
'is_first_login': isFirstLogin,
};
}RoutableAnalyticEvent — per-vendor routing
Restrict which vendors receive an event by implementing RoutableAnalyticEvent. If targetVendors returns null, the event goes to all active vendors.
class FirebaseOnlyEvent with AnalyticEventMixin
implements AnalyticEvent, RoutableAnalyticEvent {
@override
String get name => 'promo_viewed';
@override
Map<String, dynamic> toJson() => {'promo_id': 'SUMMER24'};
@override
List<String>? get targetVendors => [
AnalyticInjectorKey.firebaseImpl,
AnalyticInjectorKey.moEngageImpl,
];
}ScreenViewEvent
Implement to enable native screen-tracking APIs (e.g. Firebase's logScreenView).
abstract interface class ScreenViewEvent implements AnalyticEvent {
String get screenName;
String? get screenClass;
}TimingEvent
Measure how long operations take (API calls, image loads, etc.).
abstract interface class TimingEvent implements AnalyticEvent {
String get category; // e.g. 'network'
String get timingLabel; // e.g. 'products_endpoint'
int get durationMs;
}RevenueEvent
Trigger purchase-specific tracking APIs (e.g. AppsFlyer logPurchase, Firebase logPurchase).
abstract interface class RevenueEvent implements AnalyticEvent {
double get amount;
String get currency; // ISO 4217 (e.g. 'USD', 'IDR')
String? get transactionId;
}ErrorEvent
Track errors for correlation with crash tools.
abstract interface class ErrorEvent implements AnalyticEvent {
String get errorDescription;
bool get isFatal;
}Shared Sanitization
SharedEventSanitizer enforces constraints common to all vendors:
- Replaces non-alphanumeric/underscore characters with
_, collapses runs - Truncates event/property names to 100 characters
- Truncates string property values to 500 characters
- Drops unsupported value types (maps, lists, custom objects)
Vendor-specific sanitizers (e.g. Firebase's 40-character limit) extend SharedEventSanitizer and call super before applying their own rules:
const sanitizer = SharedEventSanitizer();
final safeName = sanitizer.sanitizeName(event.name);
final safeParams = sanitizer.sanitizeProperties(event.toJson());Vendor Self-Registration
Vendors self-register into AnalyticVendorRegistry from their RegisterModule constructor. No manual editing of analytics_impl is required when adding a new vendor.
// In your vendor's RegisterModule:
@module
abstract class MyVendorRegisterModule {
MyVendorRegisterModule() {
AnalyticVendorRegistry.instance.register(AnalyticInjectorKey.myVendorImpl);
}
// ... provider methods ...
}AnalyticsInitializer reads AnalyticVendorRegistry.instance.keys at boot time to discover which named vendor bindings to pull from GetIt — no hardcoded list.
Test Harness
Two test doubles are provided in analytics_api under lib/src/testing/:
InMemoryAnalyticTracker
Drop-in replacement for AnalyticTracker in feature-layer unit tests:
final tracker = InMemoryAnalyticTracker();
getIt.registerSingleton<AnalyticTracker>(tracker);
// ... trigger code under test ...
expect(tracker.trackedEvents.where((e) => e.name == 'purchase'), hasLength(1));
expect(tracker.screenViews, contains('CheckoutScreen'));
expect(tracker.superProperties['app_version'], '2.1.0');
expect(tracker.isOptedOut, isFalse);
expect(tracker.currentUserId, 'user_123');
// Reset between tests
tracker.clear();RecordingAnalyticVendor
Use when testing AnalyticTrackerImpl directly (infrastructure layer):
final vendor = RecordingAnalyticVendor();
final tracker = AnalyticTrackerImpl(vendors: [vendor]);
await tracker.track(LoginEvent(method: 'email'));
expect(vendor.trackedEvents.first.name, 'login');
expect(vendor.userIds, contains('user_123'));
expect(vendor.flushCount, 1);
vendor.clear(); // reset between testsIn tests that exercise AnalyticVendorRegistry, call AnalyticVendorRegistry.instance.clear() in setUp/tearDown to avoid state leaking between tests.
Key Features
- Real interceptor chain — Interceptors can enrich, filter, or drop events before they reach vendors
- Per-vendor routing —
RoutableAnalyticEvent.targetVendorssends events only to selected vendors - Vendor self-registration — Adding a vendor brick requires zero changes to
analytics_impl - Error isolation — One vendor failure does not block others; errors surface via
onErrorhook - Consent & opt-out — First-class GDPR/CCPA consent signal and opt-out gate in the orchestrator
- Super-properties — Merged into every event automatically after
registerSuperProperties - Shared sanitization — Common name/value constraints applied before dispatch; vendor subclasses extend for stricter limits
- Named DI —
@Named(AnalyticInjectorKey.firebaseImpl)uniquely identifies each vendor binding in GetIt - Test harness built-in —
InMemoryAnalyticTrackerandRecordingAnalyticVendorship withanalytics_api