Skip to content

launch_tracker_sdk Free

App launch time tracker with native platform timing, a debug implementation that pushes OTLP spans, and a zero-cost release noop, swapped via build_prepare.

Version: 2.0.0

Variables

VariableTypeDefaultDescription
appNamestringThe name of your application
isForMonorepobooleantrueWhether this is being generated as part of a monorepo

Usage

Automatic (via flutter_modular_monorepo)

Included unconditionally in Phase 3 of the monorepo post-gen hook — no selection required.

Standalone

bash
archipelago generate launch_tracker_sdk

Non-interactive (CI)

bash
archipelago generate launch_tracker_sdk --config my_config.json

Config Template

json
{
  "@appName": "The name of your application",
  "appName": "MyApp",
  "@isForMonorepo": "Whether this is being generated as part of a monorepo",
  "isForMonorepo": true
}

Architecture

Uses the Impl + Noop (unified class name) pattern with build_prepare swap.

There is no _api package. Both app_launch_tracker_impl and app_launch_tracker_noop export a class named AppLaunchTracker with identical method signatures. build_prepare rewrites the import path in the host app's .dart files — the class name never changes, so the host compiles with either package.

This differs from monitoring_impl / monitoring_noop, which share a contract via monitoring_api. The launch tracker is only ever referenced by the host app's HostAppWidget, so no shared contract package is needed.

See the architecture guide for the full decision tree.

Generated Structure

infrastructure/
├── app_launch_tracker_impl/          # Debug implementation (swapped out in release)
│   ├── android/
│   │   └── src/main/kotlin/com/archipelago/app_launch_tracker/
│   │       └── LaunchTimeTrackerPlugin.kt   # Native process start time (Android)
│   ├── ios/
│   │   └── Classes/
│   │       └── LaunchTimeTrackerPlugin.swift # Native process start time (iOS)
│   └── lib/src/
│       ├── app_launch_tracker_impl.dart      # AppLaunchTracker (debug)
│       ├── app_launch_metrics.dart           # AppLaunchMetrics + MetricTrace (package-private)
│       ├── launch_time_tracker_channel.dart  # Platform channel wrapper
│       ├── trace_utils.dart                  # traceAsync / traceSync / safeTraceProcessAsync
│       └── di/
│           ├── injector.dart                 # @InjectableInit.microPackage()
│           └── injector.module.dart          # AppLaunchTrackerPackageModule
└── app_launch_tracker_noop/          # Release stubs (zero overhead)
    └── lib/src/
        ├── app_launch_tracker_noop.dart      # AppLaunchTracker (noop — same class name)
        └── di/
            ├── injector.dart
            └── injector.module.dart          # AppLaunchTrackerPackageModule

Two-Phase Launch Lifecycle

The tracker is designed around the monorepo's two-phase init architecture:

MethodPhaseDescription
start()Pre-launchCalled in HostAppWidget.initState — fetches native timestamp via platform channel, starts stopwatch
markPreLaunchCompleted()Pre-launchCalled after initializerController.initialize() resolves
markFirstFrameRendered()Post frameCalled in addPostFrameCallback after first frame
startPostLaunch()Post-launchStarts post-launch stopwatch
markPostLaunchCompleted()Post-launchCalled after PostLaunchInitializer.initialize() — pushes OTLP spans
recordStep(String step)AnyRecords a named milestone timestamp
recordDuration(String name, int ms)AnyRecords a named duration (e.g. from traceAsync)
seedNativeTimestamps({required int nativeStartMs, required int hostStartMs})TestingInjects pre-captured timestamps (used in tests and by the platform channel callback)

Native Timing

app_launch_tracker_impl is a Flutter plugin. The platform channel com.archipelago/launch_time_tracker exposes a single method getStartupTimestamps that returns:

KeyDescription
nativeStartMsProcess creation time (ms since epoch)
hostStartMsTime when the Flutter engine attached to the plugin (ms since epoch)

Android

nativeStartMs is computed from Process.getStartUptimeMillis() (API 24+), converted to wall-clock time using SystemClock.elapsedRealtime() and System.currentTimeMillis(). On API < 24, falls back to System.currentTimeMillis() at engine-attach time.

iOS

nativeStartMs is read via sysctl(CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()) from kinfo_proc.kp_proc.p_starttime (seconds + microseconds since epoch). Falls back to Date().timeIntervalSince1970 * 1000 if sysctl fails.

hostStartMs on both platforms is captured at engine-attach time (onAttachedToEngine / plugin register(with:)).

OTLP Push via AppMonitoring

When markPostLaunchCompleted() is called, the impl pushes timing spans to Grafana (or any configured OTLP backend) via AppMonitoring.recordTransaction. The impl soft-checks GetIt.instance.isRegistered<AppMonitoring>() — if monitoring is not registered, the push is skipped silently.

Spans emitted:

Span nameAttributes
app_launchParent span wrapping all phases
native_to_hostduration_ms, phase: native_to_host, flavor, platform
pre_launchduration_ms, phase: pre_launch
first_frameduration_ms, phase: first_frame
post_launchduration_ms, phase: post_launch

Trace Utilities

trace_utils.dart provides wrapping helpers:

dart
// Wrap an async operation — records to tracker and prints to debug console
final result = await traceAsync('load_config', () => loadConfig());

// Wrap a sync operation
final value = traceSync('parse_response', () => parseResponse(data));

// Wrap an async operation and emit an OTLP span if monitoring is available
final result = await safeTraceProcessAsync(
  key: 'fetch_remote_config',
  block: () => remoteConfig.fetchAndActivate(),
  monitoring: getIt<AppMonitoring>(),
);

safeTraceProcessAsync is a fire-and-forget-safe wrapper: if monitoring is null, it simply executes the block without any tracing overhead.

DI Registration

Both _impl and _noop register the same DI module name: AppLaunchTrackerPackageModule. The build_prepare swap is transparent to the host app's injector.

Register it as the first external module so it's available before any other SDK initializes:

dart
@InjectableInit(
  externalPackageModulesBefore: [
    ExternalModule(AppLaunchTrackerPackageModule),
    // ... other modules
  ],
)
Future<void> initializeInjector() => $initGetIt(getIt);

HostAppWidget resolves the tracker via GetIt.I<AppLaunchTracker>() immediately on state creation, starting the clock before the first frame.

build_prepare Swap

build_prepare.yaml at workspace root includes:

yaml
build_prepare:
  mappings:
    - debug: app_launch_tracker_impl
      release: app_launch_tracker_noop

  target_paths:
    - apps/your_app

build_prepare performs two passes when swapping:

  1. Replaces app_launch_tracker_impl with app_launch_tracker_noop (and vice versa) in every pubspec.yaml under target_paths
  2. Rewrites import 'package:app_launch_tracker_impl/...'import 'package:app_launch_tracker_noop/...' in every .dart file under each target's lib/ and test/

The swap is fully reversible: build-prepare:debug restores all original imports exactly.

Run before release builds:

bash
melos run build-prepare:release

Restore after builds (or if the build fails):

bash
melos run build-prepare:debug

Built by Banua Coder