Skip to content
Merged
6 changes: 6 additions & 0 deletions docs/platforms/dart/guides/flutter/configuration/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@ Controls whether the SDK should propagate the W3C `traceparent` HTTP header alon

</SdkOption>

<SdkOption name="enableStandaloneAppStartTracing" type="bool" defaultValue="false" availableSince="9.26.0">

Set this boolean to `true` to report the app start as its own `app.start` transaction instead of attaching it to the first `ui.load` transaction. Requires tracing to be enabled and is only supported on Android and iOS. This option is experimental. Learn more in our <PlatformLink to="/integrations/app-start-instrumentation/#standalone-app-start-tracing">App Start Instrumentation</PlatformLink> docs.

</SdkOption>

## Experimental Features

<SdkOption name="experimental" type="object">
Expand Down
19 changes: 19 additions & 0 deletions docs/platforms/dart/guides/flutter/tracing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,25 @@ Test out tracing by starting and finishing a transaction/service span, which you

While you're testing, set <PlatformIdentifier name="traces-sample-rate" /> to `1.0`, as that ensures that every transaction/service span will be sent to Sentry. Once testing is complete, you may want to set a lower <PlatformIdentifier name="traces-sample-rate" /> value, or switch to using <PlatformIdentifier name="traces-sampler" /> to selectively sample and filter your transactions/service spans, based on contextual data.

## Standalone App Start Tracing

<Alert level="info">

This feature is experimental and available since [version 9.26.0](https://github.com/getsentry/sentry-dart/blob/main/CHANGELOG.md#9260). The API is subject to change and may introduce breaking changes in future releases.

</Alert>

By default, app start data is attached to the first `ui.load` transaction in your app. Standalone app start tracing sends the app start as its own transaction instead, which gives you more accurate app start measurements because they no longer depend on a screen transaction being started. It's supported on Android and iOS.

```dart
await SentryFlutter.init((options) {
options.tracesSampleRate = 1.0;
options.enableStandaloneAppStartTracing = true;
});
```

For more details, including how to extend the app start past the first frame, see <PlatformLink to="/integrations/app-start-instrumentation/#standalone-app-start-tracing">App Start Instrumentation</PlatformLink>.

## Next Steps

<PageGrid />
175 changes: 168 additions & 7 deletions includes/dart-integrations/app-start-instrumentation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ Before diving into the configuration, it's important to understand how app start
App start instrumentation tracks the duration between the earliest native process initialization and the first frame rendered (as reported by [addTimingsCallback](https://api.flutter.dev/flutter/scheduler/SchedulerBinding/addTimingsCallback.html)). Once the app start is processed, the callback is removed to avoid additional overhead.

When the SDK receives the start and end times of the app launch, the SDK:

- Creates a transaction named `ui.load`
- Attaches a span with either `app.start.cold` or `app.start.warm` operation
- Adds app start metrics to the transaction

If you'd rather have the app start reported as its own transaction, see [Standalone App Start Tracing](#standalone-app-start-tracing).

<Alert>

Sentry's App Start instrumentation aims to be as comprehensive and representative of the user experience as possible, and adheres to guidelines by the platform vendors. For this reason, App Starts reported by Sentry might be longer than what you see in other tools.
Expand Down Expand Up @@ -68,17 +71,175 @@ Open the [sentry.io performance page](https://sentry.io/performance), find, and

Select the event within your transaction. Sentry displays the app start metrics on the right side of the screen in the **Mobile Vitals** section.

## Standalone App Start Tracing

<Alert level="info">

This feature is experimental and available since [version 9.26.0](https://github.com/getsentry/sentry-dart/blob/main/CHANGELOG.md#9260). The API is subject to change and may introduce breaking changes in future releases.

</Alert>

By default, app start data is attached to the first `ui.load` transaction in your app, which mixes startup timing with screen-display timing. Standalone app start tracing sends the app start as its own `App Start` transaction with the `app.start` operation instead. This gives you more accurate measurements, because they no longer depend on a screen transaction being started, and it lets you sample app starts independently.

To enable it:

```dart
await SentryFlutter.init((options) {
options.tracesSampleRate = 1.0;
options.enableStandaloneAppStartTracing = true;
});
```

Standalone app start tracing requires tracing to be enabled and is only supported on Android and iOS. On every other platform the SDK keeps attaching app start data to the first `ui.load` transaction.

Because the app start uses the `app.start` operation, you can use `tracesSampler` to give app starts a dedicated sample rate without raising your overall sample rate. Where you read that operation depends on your trace lifecycle: transaction mode exposes it on the transaction context, while <PlatformLink to="/tracing/streamed-spans">stream mode</PlatformLink> carries it as the span's `sentry.op` attribute.

```dart {tabTitle:Transaction Mode (Default)} {mdExpandTabs}
await SentryFlutter.init((options) {
options.enableStandaloneAppStartTracing = true;
options.tracesSampler = (samplingContext) {
if (samplingContext.transactionContext.operation == 'app.start') {
return 1.0;
}
return 0.1;
};
});
```

```dart {tabTitle:Stream Mode}
await SentryFlutter.init((options) {
options.enableStandaloneAppStartTracing = true;
options.tracesSampler = (samplingContext) {
final operation = samplingContext.spanContext.attributes['sentry.op']?.value;
if (operation == 'app.start') {
return 1.0;
}
return 0.1;
};
});
```

### Extending the App Start

The app start ends when the first frame renders. If your app does startup work that runs past that point — loading initial data from a server or a database, for example — call `SentryFlutter.extendAppStart()` to include that work in the reported duration, then `SentryFlutter.finishExtendedAppStart()` once it's done.

The only requirement is that `extendAppStart()` runs before the first frame renders, so both the `appRunner` callback of `SentryFlutter.init` and your root widget's `initState` work. Reach for `initState` when the startup work belongs to your widget tree, and `appRunner` when it doesn't. Either way, pair the two calls in a `try`/`finally` so an early return or a thrown exception can't leave the app start open.

```dart {tabTitle:appRunner} {mdExpandTabs}
await SentryFlutter.init(
(options) {
options.tracesSampleRate = 1.0;
options.enableStandaloneAppStartTracing = true;
},
appRunner: () async {
SentryFlutter.extendAppStart();

try {
runApp(const MyApp());
await loadStartupConfiguration();
} finally {
await SentryFlutter.finishExtendedAppStart();
}
},
Comment thread
Copilot marked this conversation as resolved.
);
```

```dart {tabTitle:initState}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_loadStartupConfiguration();
}

Future<void> _loadStartupConfiguration() async {
// Extend first, before any await, so the call can't slip past the first frame.
SentryFlutter.extendAppStart();

try {
await loadStartupConfiguration();
} finally {
await SentryFlutter.finishExtendedAppStart();
}
}

// ...
}
```

`initState` runs during the first build, which happens before the first frame is rasterized. Call `extendAppStart()` before the first `await` in that method, though — awaiting first gives the frame a chance to render, and the extension is then refused.

This adds an `Extended App Start` span with the `app.start.extended` operation, covering the time between the two calls.

#### Breaking Down the Extension

To see which part of your startup work took the time, retrieve the extension span and attach children to it. Use the getter that matches your trace lifecycle: `getExtendedAppStartSpan()` in transaction mode, `getExtendedAppStartSpanV2()` in stream mode. The other one returns `null`.
Comment thread
buenaflor marked this conversation as resolved.

```dart {tabTitle:Transaction Mode (Default)} {mdExpandTabs}
SentryFlutter.extendAppStart();

ISentrySpan? child;
try {
child = SentryFlutter.getExtendedAppStartSpan()?.startChild(
'http.client',
description: 'Fetch remote config',
);
await fetchRemoteConfig();
} finally {
await child?.finish();
await SentryFlutter.finishExtendedAppStart();
}
```

```dart {tabTitle:Stream Mode}
SentryFlutter.extendAppStart();

try {
await Sentry.startSpan(
'Fetch remote config',
(span) => fetchRemoteConfig(),
parentSpan: SentryFlutter.getExtendedAppStartSpanV2(),
);
Comment thread
sentry[bot] marked this conversation as resolved.
} finally {
await SentryFlutter.finishExtendedAppStart();
}
```

In stream mode, `startSpan` ends the child for you once the callback completes, so it only needs the parent. The extension span isn't the active span, which is why you have to pass it as `parentSpan` rather than relying on automatic nesting.

Both getters return `null` when the app start isn't extended. In transaction mode the null-aware calls take care of that. In stream mode, passing `parentSpan: null` means "start a root span", so guard the call if a stray root would be a problem.

Spans you start under the extension keep the app start open until they finish, so finish them too if they shouldn't delay it.

<Alert level="warning">

Always finish what you extend. While an extension is open the app start is still in progress, and if it hits its 30-second deadline first, the extension is dropped and the reported duration falls back to the first frame.

</Alert>

<Alert>

Extending requires standalone app start tracing to be enabled. `extendAppStart()` does nothing when standalone app start tracing is off, when the first frame has already rendered, or when the app start is already extended. Each of those cases is logged rather than reported back to the caller.

</Alert>

## Disable App Start Instrumentation

To disable the app start instrumentation, you can remove the `NativeAppStartIntegration` from the `integrations` list in your `SentryFlutter.init` call.
App start ships as two integrations: `NativeAppStartIntegration` for the default `ui.load`-attached path, and `StandaloneAppStartIntegration` for standalone app start tracing. The SDK registers both and each one stands down at runtime depending on your configuration, so remove both to turn app start off no matter how it's configured.

```dart
// ignore: implementation_imports
import 'package:sentry_flutter/src/integrations/native_app_start_integration.dart';
// ignore_for_file: implementation_imports
import 'package:sentry_flutter/src/app_start/standalone/standalone_app_start_integration.dart';
import 'package:sentry_flutter/src/app_start/ui_load_attached/native_app_start_integration.dart';

await SentryFlutter.init((options) {
final integration = options.integrations.firstWhere(
(integration) => integration is NativeAppStartIntegration);
options.removeIntegration(integration);
for (final integration in options.integrations) {
if (integration is NativeAppStartIntegration ||
integration is StandaloneAppStartIntegration) {
options.removeIntegration(integration);
}
}
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
buenaflor marked this conversation as resolved.
});
```
```

App start integrations are only registered on the platforms that support them, so don't assume either one is present — looking them up with `firstWhere` throws when they aren't.
Loading