Skip to content
Back to archipelago.banuacoder.com

Infrastructure SDKs

analytics_sdk Free ​

Tier
Freeversion 4.0.6
Command
archipelago generate analytics_sdk
Generates
  • infrastructure/analytics_api
  • infrastructure/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 ​

VariableTypeDefaultDescription
appNamestring—The name of your application
isForMonorepobooleantrueWhether this is being generated as part of a monorepo

Install ​

Interactive ​

bash
archipelago generate analytics_sdk

Non-interactive (CI) ​

bash
archipelago generate analytics_sdk --config my_config.json

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 AnalyticVendorRegistry via their RegisterModule constructor

Vendor Bricks ​

BrickVendorKey Feature
firebase_analytics_implFirebase AnalyticsEvent/param sanitization (40 char limit)
appsflyer_analytics_implAppsFlyerDirect SDK pass-through
mixpanel_analytics_implMixpanelPeople API + flush support
moengage_analytics_implMoEngageProperties 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
  ◯ none

Only selected vendor bricks are generated. The orchestrator dispatches events to all registered vendors.

Generated Structure ​

text
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.dart

AnalyticTracker API ​

All 11 methods are available on the AnalyticTracker interface injected via GetIt:

dart
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 logout

Built-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:

  1. ConsentAnalyticInterceptor — drops every event while the analytics consent purpose is denied (reads a ConsentGate, defaulting to ConsentGateRegistry.instance.gate if none is registered in getIt).
  2. ScrubbingAnalyticInterceptor — redacts personal data from event properties (via a TelemetryScrubber, 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.

dart
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:

dart
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):

HookTriggered by
onSetUserIdtracker.setUserId
onSetUserPropertytracker.setUserProperty
onSetUserPropertiestracker.setUserProperties
onRegisterSuperPropertiestracker.registerSuperProperties
onSetConsenttracker.setConsent
onOptOuttracker.optOut
onOptIntracker.optIn
onFlushtracker.flush
onResettracker.reset
onErrorany 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) ​

dart
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.

dart
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).

dart
abstract interface class ScreenViewEvent implements AnalyticEvent {
  String get screenName;
  String? get screenClass;
}

TimingEvent ​

Measure how long operations take (API calls, image loads, etc.).

dart
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).

dart
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.

dart
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:

dart
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.

dart
// 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:

dart
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):

dart
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 tests

In 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.targetVendors sends 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 onError hook
  • 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 — InMemoryAnalyticTracker and RecordingAnalyticVendor ship with analytics_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.