Infrastructure SDKs
analytics_sdk Free
- Tier
- Freeversion 4.0.6
- Command
archipelago generate analytics_sdk- Generates
infrastructure/analytics_apiinfrastructure/analytics_impl
- Depends on
- shared_consentgenerate these first
Multi-vendor analytics abstraction with real interceptor chain, per-vendor routing, shared sanitization, consent/opt-out, and vendor self-registration.
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 |
Install
Interactive
archipelago generate analytics_sdkNon-interactive (CI)
archipelago generate analytics_sdk --config my_config.jsonArchitecture
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 # register()/initialize() bootstrap
│ └── di/ # AnalyticsImplPackageModule (di_modules manifest)
└── <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 logoutBuilt-in Privacy Interceptors
AnalyticsInitializer.register(getIt) (called from the pre-launch phase — see DI & Wiring below) always prepends two interceptors ahead of anything registered via AnalyticInterceptorRegistry.instance.register() or passed through register()'s own interceptors parameter, and their position is not configurable:
ConsentAnalyticInterceptor— drops every event while the analytics consent purpose is denied (reads aConsentGate, defaulting toConsentGateRegistry.instance.gateif none is registered ingetIt).ScrubbingAnalyticInterceptor— redacts personal data from event properties (via aTelemetryScrubber, same registry-or-DI resolution).
Vendor initialize() itself is deferred until analytics consent is actually granted, and retried once the consent gate flips — so a vendor SDK never starts collecting before consent, independent of the interceptor chain.
Interceptor 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.
Every vendor must implement setCollectionEnabled(bool) (no default). AnalyticsInitializer calls it with false when analytics consent is revoked and true when it is granted again, so the SDK stops its own automatic collection. AnalyticsInitializer builds each vendor in its own try/catch, so one that fails to construct (e.g. Firebase before flutterfire configure) is skipped rather than breaking the tracker; consent changes are applied to every started vendor in isolation. Typical mapping: Mixpanel optOutTracking/optInTracking, MoEngage disableDataTracking/enableDataTracking, AppsFlyer stop(true)/stop(false), Firebase setAnalyticsCollectionEnabled.
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
DI & Wiring
analytics_impl depends only on analytics_api. It declares a hand-written AnalyticsImplPackageModule under archipelago: di_modules in its own pubspec.yaml, which the app's di_modules wiring registers into the host injector (apps/<app>/lib/di/injector.dart), same as any other infra package. Its init() calls AnalyticsInitializer.register(getIt), which registers AnalyticTracker as a lazy singleton in the base GetIt scope during pre-launch — it is never dropped when a feature scope is pushed/popped.
The tracker itself is built on first resolution: its vendor list comes from AnalyticVendorRegistry.instance.keys, and its interceptor chain is [ConsentAnalyticInterceptor, ScrubbingAnalyticInterceptor, ...AnalyticInterceptorRegistry.instance.interceptors, ...extra] (consent + scrubbing always first; the registry is read on every call, so late registrations still apply). Until AnalyticsInitializer.initialize has started the vendors, the tracker holds vendor calls (up to 100, oldest dropped) and replays them afterwards, dropping them if consent was withdrawn meanwhile. Vendor bricks declare their own <Vendor>AnalyticsImplPackageModule under their own archipelago: di_modules and wire it into the app injector, exactly like a monitoring vendor — their RegisterModule constructor calls AnalyticVendorRegistry.instance.register(<key>) as a side effect of that module running. analytics_impl never depends on vendor packages; it only reads the registry that vendors populate.
AnalyticsInitializer.initialize(getIt), invoked from the host app's post_launch_initializer.dart (post-launch phase), only starts vendors — vendor initialize() itself is still deferred until analytics consent is granted.
Dependencies
analytics_api: meta only, no Flutter dependency. analytics_impl: consent_api (workspace) for the consent-gate/scrubber types above, plus meta.
Platform Setup
None required — analytics dispatch is pure Dart; platform setup lives in each vendor brick.
Changelog
Run archipelago brick info analytics_sdk to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.