Skip to content

Setting Up the Launch Tracker SDK

What you'll learn

  • Generating the Launch Tracker SDK with the Impl + Noop pattern
  • Registering it as the first DI module for accurate pre-launch timing
  • Instrumenting the two-phase launch lifecycle
  • Recording named steps and custom durations for performance analysis
  • Pushing OTLP spans to Grafana or any OpenTelemetry-compatible backend

Prerequisites

  • An existing Archipelago monorepo (see Monorepo Scaffolding)
  • Monitoring SDK already generated — spans are forwarded via AppMonitoring

Step 1: Generate the Launch Tracker SDK

The Launch Tracker SDK is a Free tier brick.

bash
archipelago generate launch_tracker_sdk

You will be prompted for:

  • appNameMyApp (must match your monorepo app name)
  • isForMonorepotrue

Or use a config file:

json
{
  "appName": "MyApp",
  "isForMonorepo": true
}
bash
archipelago generate launch_tracker_sdk --config launch_tracker_config.json

If you scaffolded your monorepo with flutter_modular_monorepo, the Launch Tracker SDK is auto-included as part of Phase 3 and you can skip this step.

Step 2: Understand the Generated Structure

The Launch Tracker SDK uses the Impl + Noop pattern — no _api package. Both packages export a class named AppLaunchTracker, so your app code never needs to change between builds.

infrastructure/
├── app_launch_tracker_impl/    # Real OTLP-backed implementation
│   └── lib/
│       ├── app_launch_tracker_impl.dart
│       └── src/
│           ├── app_launch_tracker.dart
│           └── di/app_launch_tracker_package_module.dart
└── app_launch_tracker_noop/    # No-op — does nothing, zero overhead
    └── lib/
        ├── app_launch_tracker_noop.dart
        └── src/
            ├── app_launch_tracker.dart
            └── di/app_launch_tracker_package_module.dart

build_prepare rewrites the import at build time — in debug builds the noop is used, in release builds the impl is used. No if/else in your app code.

Step 3: Configure build_prepare

Edit build_prepare.yaml at your monorepo root:

yaml
flavors:
  development:
    app_launch_tracker:
      dependency: app_launch_tracker_noop
  staging:
    app_launch_tracker:
      dependency: app_launch_tracker_noop
  production:
    app_launch_tracker:
      dependency: app_launch_tracker_impl

Before each build, run:

bash
dart run devtools/build_prepare.dart --flavor production

Step 4: Register as the First DI Module

Timing accuracy depends on the tracker being registered before any other external module. Use externalPackageModulesBefore in your @InjectableInit:

dart
// apps/my_app/lib/bootstrap.dart
@InjectableInit(
  externalPackageModulesBefore: [
    ExternalModule(AppLaunchTrackerPackageModule),
    ExternalModule(MonitoringSdkPackageModule),
    // ... all other modules follow
  ],
)
Future<void> initializeInjector() => $initGetIt(getIt);

The tracker is registered first so its start() call captures true cold-start time before any other SDK initializes.

Step 5: Instrument the Launch Lifecycle

Wire the lifecycle methods into your HostAppWidget:

dart
// apps/my_app/lib/host_app_widget.dart
class HostAppWidget extends StatefulWidget {
  const HostAppWidget({super.key});

  @override
  State<HostAppWidget> createState() => _HostAppWidgetState();
}

class _HostAppWidgetState extends State<HostAppWidget> {
  @override
  void initState() {
    super.initState();

    // Mark the start of the pre-launch phase
    getIt<AppLaunchTracker>().start();

    _initialize();
  }

  Future<void> _initialize() async {
    await initializerController.initialize();

    // Pre-launch complete — DI, routes, critical config done
    getIt<AppLaunchTracker>().markPreLaunchCompleted();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      // First frame is visible to the user
      getIt<AppLaunchTracker>().markFirstFrameRendered();

      // Begin timing the post-launch phase
      getIt<AppLaunchTracker>().startPostLaunch();

      PostLaunchInitializer.initialize().then((_) {
        // Post-launch complete — analytics, flags, non-critical setup done
        // Pushes OTLP spans to your configured backend
        getIt<AppLaunchTracker>().markPostLaunchCompleted();
      });
    });
  }
}

Step 6: Record Named Milestones

Use recordStep to capture named checkpoints within the launch sequence:

dart
getIt<AppLaunchTracker>().recordStep('remote_config_fetched');
await remoteConfig.fetchAndActivate();
getIt<AppLaunchTracker>().recordStep('remote_config_applied');

Use recordDuration when you already have an elapsed time (for example, from a third-party SDK callback):

dart
getIt<AppLaunchTracker>().recordDuration('firebase_init_ms', firebaseInitDuration.inMilliseconds);

Step 7: Trace Async and Sync Work

The SDK includes three tracing utilities that wrap work in named spans:

dart
// Async work — returns the result of the block
final config = await traceAsync(
  'load_remote_config',
  () => remoteConfig.fetchAndActivate(),
);

// Sync work — returns the result of the block
final parsed = traceSync(
  'parse_app_config',
  () => AppConfig.fromJson(rawJson),
);

// Async work with error forwarding to AppMonitoring
final activated = await safeTraceProcessAsync(
  key: 'fetch_remote_config',
  block: () => remoteConfig.fetchAndActivate(),
  monitoring: getIt<AppMonitoring>(),
);

safeTraceProcessAsync soft-checks GetIt before accessing AppMonitoring, so it is safe to call before monitoring is fully initialized.

Step 8: View Spans in Grafana

When using app_launch_tracker_impl, completed spans are pushed as OTLP traces via AppMonitoring. Configure your OTLP endpoint in the monitoring SDK's impl package:

dart
// infrastructure/monitoring_sdk_impl/lib/src/...
options.dsn = const String.fromEnvironment('OTEL_EXPORTER_OTLP_ENDPOINT');

In Grafana, filter by the app_launch service name to see a waterfall of pre-launch, first-frame, and post-launch spans.

Common Customizations

GoalWhat to do
Track a feature flag fetch timetraceAsync('feature_flags', () => flags.fetch())
Record SDK init durationrecordDuration('analytics_init_ms', elapsed)
Add a named milestonerecordStep('user_profile_loaded')
Disable in all non-production buildsSet noop in development + staging in build_prepare.yaml

Next Steps

Built by Banua Coder