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.
archipelago generate launch_tracker_sdkYou will be prompted for:
- appName —
MyApp(must match your monorepo app name) - isForMonorepo —
true
Or use a config file:
{
"appName": "MyApp",
"isForMonorepo": true
}archipelago generate launch_tracker_sdk --config launch_tracker_config.jsonIf 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.dartbuild_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:
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_implBefore each build, run:
dart run devtools/build_prepare.dart --flavor productionStep 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:
// 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:
// 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:
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):
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:
// 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:
// 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
| Goal | What to do |
|---|---|
| Track a feature flag fetch time | traceAsync('feature_flags', () => flags.fetch()) |
| Record SDK init duration | recordDuration('analytics_init_ms', elapsed) |
| Add a named milestone | recordStep('user_profile_loaded') |
| Disable in all non-production builds | Set noop in development + staging in build_prepare.yaml |
Next Steps
- Set up monitoring to receive the OTLP spans pushed by the tracker
- Configure push notifications as part of your post-launch initialization