From c119d13b9a4e92dd41ae4d1282d44e4ef61feabd Mon Sep 17 00:00:00 2001 From: nimishgj Date: Mon, 27 Jul 2026 13:59:56 +0530 Subject: [PATCH 1/2] add flutter kotlin instrumentation doc --- docs/instrument/mobile/hybrid.md | 308 +++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 docs/instrument/mobile/hybrid.md diff --git a/docs/instrument/mobile/hybrid.md b/docs/instrument/mobile/hybrid.md new file mode 100644 index 0000000..838cfef --- /dev/null +++ b/docs/instrument/mobile/hybrid.md @@ -0,0 +1,308 @@ +--- +title: Hybrid Instrumentation - Native + Flutter RUM with one unified session +sidebar_label: Hybrid (Native + Flutter) +sidebar_position: 24 +description: + Instrument add-to-app hybrids — a native Android/iOS app embedding Flutter — + with Scout so native and Flutter telemetry stitch into one unified session, + same-process or in a separate process. +keywords: + [ + hybrid mobile observability, + add-to-app flutter, + flutter embed native, + native flutter session, + scout hybrid, + android process flutter, + unified session rum, + ] +--- + +# Hybrid (Native + Flutter) instrumentation + +Many production apps are **add-to-app** hybrids: a native Android (Kotlin) or +iOS (Swift) app that embeds Flutter for some screens. Instrumenting each layer +in isolation gives you two disconnected views of the same user — a native +session and a Flutter session that never join up. + +Scout instruments **both** layers and stitches them into **one session**, so a +journey that crosses the native ↔ Flutter boundary is a single, continuous +session in your backend. + +## How it works + +- The **native SDK** (`scout-android` on Android; the Kotlin/Native engine that + ships inside `scout_flutter` on iOS) and the **Flutter SDK** (`scout_flutter`) + both run inside your app. +- `scout_flutter` **delegates to the native SDK**: it forwards its spans, logs, + and metrics to the native side, which is the **single exporter** to your + collector. You get one export pipeline, not two. +- The native side **owns the session**; the Flutter side **adopts that + `session.id`**. Breadcrumbs and user/session attributes are shared. + +In your backend you see **one service** (the shared service name) with **two +instrumentation scopes**, all under **one `session.id`**: + +| Platform | Native scope | Flutter scope | +| --- | --- | --- | +| Android | `base14.scout.android` | `base14.scout.flutter` | +| iOS | `base14.scout.ios` | `base14.scout.flutter` | + +### Process models + +| Platform | Same-process | Different-process | +| --- | --- | --- | +| **Android** | ✅ Default (no `android:process`) | ✅ `android:process=":flutter"` | +| **iOS** | ✅ Only mode — iOS apps are single-process | — Not applicable | + +:::note scout_flutter version + +The hybrid bridge lands in **`scout_flutter` 0.2.0** (the `0.2.x` line). The +`0.1.x` line does not include the native delegation bridge — use `0.2.0` or +later for hybrid apps. + +::: + +## Prerequisites + +- **Same `serviceName` and `endpoint` on both the native and Flutter init.** + This is what ties the two layers to one service; the bridge then unifies the + session automatically. +- Android native SDK: `io.base14:scout-android:0.1.7` (Maven Central). +- Flutter SDK: `scout_flutter` **`^0.2.0`** — the `0.2.x` line includes the + native delegation bridge. +- The iOS native engine ships **inside** `scout_flutter` — you do not add a + separate iOS native dependency for the bridge. + +--- + +## Android + +On Android you initialise the native `scout-android` SDK in your native host and +initialise `scout_flutter` in Dart. The native SDK becomes the session owner and +the sole exporter; `scout_flutter` detects it and bridges automatically. + +### 1. Dependencies + +`android/app/build.gradle.kts` (native host): + +```kotlin +dependencies { + implementation("io.base14:scout-android:0.1.7") +} +``` + +`pubspec.yaml` (Flutter module): + +```yaml +dependencies: + scout_flutter: ^0.2.0 +``` + +### 2. Initialize the native SDK (session owner) + +In your launcher `Activity` (or `Application`), before presenting Flutter: + +```kotlin +import io.base14.scout.android.Scout +import io.base14.scout.core.ScoutConfig + +Scout.initialize( + this, + ScoutConfig( + serviceName = "my-hybrid-app", // MUST match Flutter + endpoint = "https:///otlp", // MUST match Flutter + headers = mapOf("Authorization" to "Bearer "), + // role defaults to ScoutRole.AUTO — leave it; the bridge resolves ownership. + ), +) +``` + +This also auto-instruments your **native** screens (Compose / Views), taps, +HTTP, crashes, ANR, and frame metrics — see [Android](./android.md). + +### 3. Initialize scout_flutter (same service name + endpoint) + +In your Flutter module's `main()`: + +```dart +import 'package:scout_flutter/scout_flutter.dart'; + +void main() async { + await ScoutFlutter.initialize( + config: ScoutFlutterConfig( + serviceName: 'my-hybrid-app', // MUST match native + endpoint: 'https:///otlp', // MUST match native + headers: const {'Authorization': 'Bearer '}, + ), + ); + runApp(const MyApp()); +} +``` + +`scout_flutter` detects the already-initialised native SDK and **delegates** to +it — forwarding all Flutter telemetry through the bridge and adopting the native +`session.id`. No extra wiring is required. + +### 4. Launch Flutter from the native host + +Add a `FlutterActivity` subclass for the Flutter UI (a bare subclass is enough; +add a `MethodChannel` only if you call native code from Dart): + +```kotlin +package com.example.myapp + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() +``` + +Then present it from your native host: + +```kotlin +startActivity(Intent(this, MainActivity::class.java)) +``` + +:::note Init ordering +You don't need to hand-sequence the two SDKs. Initialise each at its own entry +point — native in the host's `onCreate`, Flutter in `main()`. Because the native +host is shown first and initialises Scout **before** it launches Flutter, the +native session owner is always in place by the time the Flutter side attaches. +(Same-process is order-independent regardless — whichever SDK initialises first +establishes the shared owner. In different-process, keep native init in the host, +which naturally runs before the Flutter process starts.) +::: + +### Same-process (default, recommended) + +The Flutter `Activity` runs in the **same OS process** as the native host. +Declare it in `AndroidManifest.xml` **without** an `android:process` attribute: + +```xml + +``` + +Unification happens **in-memory** (the native and Flutter SDKs share the same +process, so the Flutter side reads the native session directly). This is the +default and the lowest-overhead option. + +### Different-process + +If your Flutter `Activity` must run in a **separate OS process** (for memory +isolation), add `android:process=":flutter"`: + +```xml + +``` + +Here the Flutter process cannot read the native session in-memory, so the bridge +falls back to a **persisted cross-process owner record**: the Flutter process +reads the native session context and **adopts** the same `session.id`. The +result is identical in the backend — one session across both scopes — at the +cost of a slightly heavier bridge and a second process. + +:::tip Which process model? +Prefer **same-process** unless you have a specific reason to isolate Flutter +(e.g. a memory-heavy Flutter surface you want the OS to reclaim independently). +Both produce one unified session. +::: + +--- + +## iOS + +iOS apps are **single-process**, so there is only one process model. You do +**not** write any native Scout initialisation for the bridge — `scout_flutter` +starts the native iOS engine in bridge mode for you. + +### 1. Dependency + +Add `scout_flutter` to your Flutter module. The iOS native engine is bundled +with the plugin (no separate native dependency). + +```yaml +dependencies: + scout_flutter: ^0.2.0 +``` + +### 2. Initialize scout_flutter + +In your Flutter module's `main()` — same as Android: + +```dart +await ScoutFlutter.initialize( + config: ScoutFlutterConfig( + serviceName: 'my-hybrid-app', + endpoint: 'https:///otlp', + headers: const {'Authorization': 'Bearer '}, + ), +); +``` + +On iOS, `scout_flutter` starts the native Kotlin/Native engine in **bridge mode** +internally (via `Scout.startBridge`). That engine emits the `base14.scout.ios` +scope (session, crash, hang, vitals) and shares one `session.id` with the +Flutter layer. **No native Swift init code is required.** + +### 3. Present Flutter from the native host + +Standard add-to-app: keep a cached `FlutterEngine` and present a +`FlutterViewController`: + +```swift +lazy var flutterEngine = FlutterEngine(name: "my_engine") + +func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: ...) -> Bool { + flutterEngine.run() + GeneratedPluginRegistrant.register(with: flutterEngine) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) +} + +func presentFlutter() { + let vc = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil) + vc.modalPresentationStyle = .fullScreen + keyWindow?.rootViewController?.present(vc, animated: true) +} +``` + +:::note Native iOS screens +In this hybrid setup, Scout instruments the **Flutter** UI plus the shared +native engine (session, crashes, app-hangs, vitals). Your app's own **native +SwiftUI/UIKit screens are not auto-instrumented** unless you additionally +initialise the native iOS SDK yourself — see [iOS](./ios.md). +::: + +--- + +## Notes + +- **Service name + endpoint must match** on both sides. A mismatch produces two + separate services and no session unification. +- The **native SDK is the exporter**. Batching/retry settings on the native + config (`exportIntervalSeconds`, `maxExportBatchSize`, `maxQueueSize`, + `maxRetries`) govern what actually leaves the device; the Flutter side + forwards to it rather than exporting directly. +- Crash and ANR/app-hang reporting is handled by the native side in hybrid mode. +- For the per-layer configuration options, see the platform pages: + [Android](./android.md), [iOS](./ios.md), and [Flutter](./flutter.md). + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| **Two separate services** in the backend instead of one | The native and Flutter inits used a different `serviceName` or `endpoint` | Make `serviceName` **and** `endpoint` identical on both sides — this is what ties them to one service. | +| **Flutter screens/events don't appear** (native data does) | `ScoutFlutter.initialize(...)` never ran, or ran with a non-hybrid build of `scout_flutter` | Ensure `ScoutFlutter.initialize` runs in `main()` before `runApp`, using the hybrid `scout_flutter` version. | +| **Flutter data lands but under a different `session.id`** than native (not unified) | The bridge didn't attach — in different-process, the Flutter process started before the native host initialised Scout | Keep native `Scout.initialize` in the host and launch Flutter **from** the host (so native init runs first). For same-process this can't happen. | +| **Native screens/events don't appear** | On Android the native SDK was never initialised in the host (on iOS the native side starts automatically with `scout_flutter`) | Android: call `Scout.initialize` in the host `Activity`/`Application`. | From 0399c75c062df2dc4c30c4f00653330b68d9a9a0 Mon Sep 17 00:00:00 2001 From: Nilakanta Mallick Date: Mon, 27 Jul 2026 20:03:14 +0530 Subject: [PATCH 2/2] fix hybrid doc accuracy and structure, bump scout_flutter to 0.2.0 --- docs/guides/flutter-mobile-observability.md | 2 +- docs/instrument/mobile/flutter.md | 12 +- docs/instrument/mobile/hybrid.md | 354 ++++++++++++++------ 3 files changed, 255 insertions(+), 113 deletions(-) diff --git a/docs/guides/flutter-mobile-observability.md b/docs/guides/flutter-mobile-observability.md index 853e557..5473e72 100644 --- a/docs/guides/flutter-mobile-observability.md +++ b/docs/guides/flutter-mobile-observability.md @@ -97,7 +97,7 @@ hand-build. It is published on ```yaml title="pubspec.yaml" dependencies: - scout_flutter: ^0.1.23 + scout_flutter: ^0.2.0 ``` Then install: diff --git a/docs/instrument/mobile/flutter.md b/docs/instrument/mobile/flutter.md index 7dd249c..7293139 100644 --- a/docs/instrument/mobile/flutter.md +++ b/docs/instrument/mobile/flutter.md @@ -95,7 +95,7 @@ Add it to your `pubspec.yaml`: ```yaml # pubspec.yaml dependencies: - scout_flutter: ^0.1.23 + scout_flutter: ^0.2.0 ``` Or: @@ -104,6 +104,16 @@ Or: flutter pub add scout_flutter ``` +:::note What changed in 0.2.0 +The native engine now starts alongside the Flutter SDK on both platforms. This +is additive for a pure-Flutter app: Flutter telemetry is unchanged, and you also +get native crash capture and CPU/memory vitals under a native scope +(`base14.scout.android` or `base14.scout.ios`) next to `base14.scout.flutter`. + +The same mechanism lets a native host app share one session with embedded +Flutter. See [Hybrid (Native + Flutter)](./hybrid.md). +::: + ### iOS — CocoaPods install The first build after adding scout_flutter triggers a pod install for diff --git a/docs/instrument/mobile/hybrid.md b/docs/instrument/mobile/hybrid.md index 838cfef..808d569 100644 --- a/docs/instrument/mobile/hybrid.md +++ b/docs/instrument/mobile/hybrid.md @@ -1,47 +1,61 @@ --- -title: Hybrid Instrumentation - Native + Flutter RUM with one unified session +title: Hybrid Instrumentation - Native + Flutter RUM in one session sidebar_label: Hybrid (Native + Flutter) -sidebar_position: 24 +sidebar_position: 27 description: - Instrument add-to-app hybrids — a native Android/iOS app embedding Flutter — - with Scout so native and Flutter telemetry stitch into one unified session, + Instrument add-to-app hybrids, a native Android/iOS app embedding Flutter, + with Scout so native and Flutter OpenTelemetry RUM stitch into one session, same-process or in a separate process. keywords: [ hybrid mobile observability, + hybrid opentelemetry, add-to-app flutter, flutter embed native, native flutter session, - scout hybrid, android process flutter, unified session rum, + scout-android, + scout-flutter, + base14, ] --- -# Hybrid (Native + Flutter) instrumentation +# Hybrid (Native + Flutter) -Many production apps are **add-to-app** hybrids: a native Android (Kotlin) or -iOS (Swift) app that embeds Flutter for some screens. Instrumenting each layer -in isolation gives you two disconnected views of the same user — a native -session and a Flutter session that never join up. +An add-to-app hybrid is a native Android (Kotlin) or iOS (Swift) app that embeds +Flutter for some screens. To instrument one, initialize the native SDK and +`scout_flutter` with the same `serviceName` and `endpoint`. The Flutter SDK +detects the native SDK and delegates to it, and both layers report under one +`session.id`. -Scout instruments **both** layers and stitches them into **one session**, so a -journey that crosses the native ↔ Flutter boundary is a single, continuous -session in your backend. +## What You Get + +| Capability | Owner | Notes | +|---|---|---| +| Unified session | Native | The native side mints the session; the Flutter side adopts its `session.id`. One session across both scopes | +| Export pipeline | Native | Flutter forwards spans, logs, and metrics over the bridge. Same-process, one exporter for both layers; different-process, one per process | +| Breadcrumbs, user and session attributes | Shared | Set on either side, visible to both | +| Native Android telemetry | `scout-android` | Screens, taps, HTTP, JVM and NDK crashes, ANR, jank, vitals. See [Android](./android.md) | +| Native iOS telemetry | Bridge engine | Screens, KSCrash native crashes, app hangs, HTTP, jank, vitals. Taps, startup tracking, and MetricKit are off in bridge mode | +| Flutter telemetry | `scout_flutter` | Screens, taps, HTTP, errors, and jank from the Flutter layer, forwarded to the native side. See [Flutter](./flutter.md) | +| Crash de-duplication | Native | In hybrid mode `scout_flutter` drains and discards its own crash files, so a crash is reported once | ## How it works -- The **native SDK** (`scout-android` on Android; the Kotlin/Native engine that - ships inside `scout_flutter` on iOS) and the **Flutter SDK** (`scout_flutter`) +- The native SDK (`scout-android` on Android; the Kotlin/Native engine that + ships inside `scout_flutter` on iOS) and the Flutter SDK (`scout_flutter`) both run inside your app. -- `scout_flutter` **delegates to the native SDK**: it forwards its spans, logs, - and metrics to the native side, which is the **single exporter** to your - collector. You get one export pipeline, not two. -- The native side **owns the session**; the Flutter side **adopts that - `session.id`**. Breadcrumbs and user/session attributes are shared. +- `scout_flutter` delegates to the native SDK, forwarding its spans, logs, and + metrics to the native side, which owns the export pipeline. Same-process, that + is a single OTLP exporter for both layers. In a separate `:flutter` process + each process runs its own exporter, described in + [Different-process](#different-process). +- The native side owns the session; the Flutter side adopts that `session.id`. + Breadcrumbs and user/session attributes are shared. -In your backend you see **one service** (the shared service name) with **two -instrumentation scopes**, all under **one `session.id`**: +In your backend you see one service (the shared service name) with two +OpenTelemetry instrumentation scopes, all under one `session.id`: | Platform | Native scope | Flutter scope | | --- | --- | --- | @@ -52,34 +66,29 @@ instrumentation scopes**, all under **one `session.id`**: | Platform | Same-process | Different-process | | --- | --- | --- | -| **Android** | ✅ Default (no `android:process`) | ✅ `android:process=":flutter"` | -| **iOS** | ✅ Only mode — iOS apps are single-process | — Not applicable | +| Android | Default (no `android:process`) | `android:process=":flutter"` | +| iOS | Only mode; iOS apps are single-process | Not applicable | -:::note scout_flutter version +## Prerequisites -The hybrid bridge lands in **`scout_flutter` 0.2.0** (the `0.2.x` line). The -`0.1.x` line does not include the native delegation bridge — use `0.2.0` or -later for hybrid apps. +| Requirement | Version | +| --- | --- | +| Android native SDK | `io.base14:scout-android:0.1.7` (Maven Central) | +| Flutter SDK | `scout_flutter` `^0.2.0` | +| iOS native engine | Ships inside `scout_flutter`; no separate dependency | -::: - -## Prerequisites +The hybrid bridge lands in `scout_flutter` 0.2.0. The `0.1.x` line does not +include the native delegation bridge, so use `0.2.0` or later for hybrid apps. -- **Same `serviceName` and `endpoint` on both the native and Flutter init.** - This is what ties the two layers to one service; the bridge then unifies the - session automatically. -- Android native SDK: `io.base14:scout-android:0.1.7` (Maven Central). -- Flutter SDK: `scout_flutter` **`^0.2.0`** — the `0.2.x` line includes the - native delegation bridge. -- The iOS native engine ships **inside** `scout_flutter` — you do not add a - separate iOS native dependency for the bridge. +Both inits must use the same `serviceName` and `endpoint`. That is what ties the +two layers to one service; the bridge then unifies the session automatically. --- ## Android -On Android you initialise the native `scout-android` SDK in your native host and -initialise `scout_flutter` in Dart. The native SDK becomes the session owner and +On Android you initialize the native `scout-android` SDK in your native host and +initialize `scout_flutter` in Dart. The native SDK becomes the session owner and the sole exporter; `scout_flutter` detects it and bridges automatically. ### 1. Dependencies @@ -101,34 +110,55 @@ dependencies: ### 2. Initialize the native SDK (session owner) -In your launcher `Activity` (or `Application`), before presenting Flutter: +Initialize Scout in `Application.onCreate`, once per process, before any +`Activity` starts, whether that is your native host or the embedded +`FlutterActivity`: ```kotlin +package com.example.myapp + +import android.app.Application import io.base14.scout.android.Scout import io.base14.scout.core.ScoutConfig -Scout.initialize( - this, - ScoutConfig( - serviceName = "my-hybrid-app", // MUST match Flutter - endpoint = "https:///otlp", // MUST match Flutter - headers = mapOf("Authorization" to "Bearer "), - // role defaults to ScoutRole.AUTO — leave it; the bridge resolves ownership. - ), -) +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + Scout.initialize( + this, + ScoutConfig( + serviceName = "my-hybrid-app", // MUST match Flutter + endpoint = "https:///otlp", // MUST match Flutter + headers = mapOf("Authorization" to "Bearer "), + // Leave `role` at its ScoutRole.AUTO default; see Different-process. + ), + ) + } +} +``` + +Register it in `AndroidManifest.xml`: + +```xml + ``` -This also auto-instruments your **native** screens (Compose / Views), taps, -HTTP, crashes, ANR, and frame metrics — see [Android](./android.md). +This also auto-instruments your native screens (Compose and Views), taps, HTTP, +crashes, ANR, and frame metrics. See [Android](./android.md). ### 3. Initialize scout_flutter (same service name + endpoint) In your Flutter module's `main()`: ```dart +import 'package:flutter/widgets.dart'; import 'package:scout_flutter/scout_flutter.dart'; -void main() async { +Future main() async { + // Required: initialize() talks to the native side over a platform channel, + // which needs the binding in place. + WidgetsFlutterBinding.ensureInitialized(); + await ScoutFlutter.initialize( config: ScoutFlutterConfig( serviceName: 'my-hybrid-app', // MUST match native @@ -140,8 +170,8 @@ void main() async { } ``` -`scout_flutter` detects the already-initialised native SDK and **delegates** to -it — forwarding all Flutter telemetry through the bridge and adopting the native +`scout_flutter` detects the already-initialized native SDK and delegates to it, +forwarding all Flutter telemetry through the bridge and adopting the native `session.id`. No extra wiring is required. ### 4. Launch Flutter from the native host @@ -164,38 +194,45 @@ startActivity(Intent(this, MainActivity::class.java)) ``` :::note Init ordering -You don't need to hand-sequence the two SDKs. Initialise each at its own entry -point — native in the host's `onCreate`, Flutter in `main()`. Because the native -host is shown first and initialises Scout **before** it launches Flutter, the -native session owner is always in place by the time the Flutter side attaches. -(Same-process is order-independent regardless — whichever SDK initialises first -establishes the shared owner. In different-process, keep native init in the host, -which naturally runs before the Flutter process starts.) +You don't need to hand-sequence the two SDKs. Initialize each at its own entry +point: native in `Application.onCreate`, Flutter in `main()`. `onCreate` runs +before any `Activity`, so the native session owner is always in place by the +time the Flutter side attaches. Same-process is order-independent regardless: +whichever SDK initializes first establishes the shared owner. ::: -### Same-process (default, recommended) +### 5. Choose a process model + +#### Same-process (default, recommended) -The Flutter `Activity` runs in the **same OS process** as the native host. -Declare it in `AndroidManifest.xml` **without** an `android:process` attribute: +The Flutter `Activity` runs in the same OS process as the native host. Declare +it in `AndroidManifest.xml` without an `android:process` attribute: ```xml + android:windowSoftInputMode="adjustResize"> + + + ``` -Unification happens **in-memory** (the native and Flutter SDKs share the same -process, so the Flutter side reads the native session directly). This is the -default and the lowest-overhead option. +Unification happens in memory: the native and Flutter SDKs share the same +process, so the Flutter side reads the native session directly. This is the +default. -### Different-process +#### Different-process -If your Flutter `Activity` must run in a **separate OS process** (for memory -isolation), add `android:process=":flutter"`: +If your Flutter `Activity` must run in a separate OS process, usually for memory +isolation, add `android:process=":flutter"`: ```xml ``` -Here the Flutter process cannot read the native session in-memory, so the bridge -falls back to a **persisted cross-process owner record**: the Flutter process -reads the native session context and **adopts** the same `session.id`. The -result is identical in the backend — one session across both scopes — at the -cost of a slightly heavier bridge and a second process. +`Application.onCreate` runs again in the `:flutter` process, so `Scout.initialize` +runs there too. That process cannot read the main process's in-memory owner, so +the bridge queries `ScoutBridgeProvider` instead. This is a `ContentProvider` +that `scout-android` declares in its own manifest with authority +`${applicationId}.scout.bridge`, `android:exported="false"`, and +`android:multiprocess="false"`, so a single instance runs in the main process. +It returns the live session context, and the `:flutter` process adopts the same +`session.id`. You do not declare the provider yourself; the manifest merger +pulls it in. + +:::warning Keep `role` at `ScoutRole.AUTO` here +The Android bridge only performs the cross-process lookup when `role` is not +`OWNER`. Setting `role = ScoutRole.OWNER` skips it, so the `:flutter` process +mints its own session and you get two sessions. `AUTO`, the default, resolves +ownership correctly in both process models. + +The `flutter-android-example` app in the `scout-kotlin-multiplatform` repo does +set `ScoutRole.OWNER`. That is safe there because it runs same-process, where +the lookup is not needed. Do not copy it into a different-process app. +::: + +In the backend you still get one session across both scopes, but each process +runs its own exporter: the main process exports native telemetry, the +`:flutter` process exports the forwarded Flutter telemetry. That is two export +pipelines and two processes, which is the overhead this mode costs you. -:::tip Which process model? -Prefer **same-process** unless you have a specific reason to isolate Flutter -(e.g. a memory-heavy Flutter surface you want the OS to reclaim independently). -Both produce one unified session. +:::tip Choosing a process model +Prefer same-process unless you have a specific reason to isolate Flutter, such +as a memory-heavy Flutter surface you want the OS to reclaim independently. Both +produce one unified session. ::: --- ## iOS -iOS apps are **single-process**, so there is only one process model. You do -**not** write any native Scout initialisation for the bridge — `scout_flutter` -starts the native iOS engine in bridge mode for you. +iOS apps are single-process, so there is only one process model. You do not +write any native Scout initialization for the bridge. `scout_flutter` starts the +native iOS engine in bridge mode for you. ### 1. Dependency @@ -238,22 +295,27 @@ dependencies: ### 2. Initialize scout_flutter -In your Flutter module's `main()` — same as Android: +In your Flutter module's `main()`, same as Android: ```dart -await ScoutFlutter.initialize( - config: ScoutFlutterConfig( - serviceName: 'my-hybrid-app', - endpoint: 'https:///otlp', - headers: const {'Authorization': 'Bearer '}, - ), -); +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await ScoutFlutter.initialize( + config: ScoutFlutterConfig( + serviceName: 'my-hybrid-app', + endpoint: 'https:///otlp', + headers: const {'Authorization': 'Bearer '}, + ), + ); + runApp(const MyApp()); +} ``` -On iOS, `scout_flutter` starts the native Kotlin/Native engine in **bridge mode** +On iOS, `scout_flutter` starts the native Kotlin/Native engine in bridge mode internally (via `Scout.startBridge`). That engine emits the `base14.scout.ios` -scope (session, crash, hang, vitals) and shares one `session.id` with the -Flutter layer. **No native Swift init code is required.** +scope and shares one `session.id` with the Flutter layer. No native Swift init +code is required. ### 3. Present Flutter from the native host @@ -277,23 +339,39 @@ func presentFlutter() { } ``` -:::note Native iOS screens -In this hybrid setup, Scout instruments the **Flutter** UI plus the shared -native engine (session, crashes, app-hangs, vitals). Your app's own **native -SwiftUI/UIKit screens are not auto-instrumented** unless you additionally -initialise the native iOS SDK yourself — see [iOS](./ios.md). +:::note What bridge mode leaves out on iOS +The engine `scout_flutter` starts for you covers sessions, screen views, +KSCrash native crashes, app hangs, HTTP, jank, and vitals on the native side. +Three things are off in bridge mode: + +- Tap tracking (`enableTapTracking` defaults to `false`). +- Startup and cold-start tracking (`enableStartupTracking` defaults to `false`). +- MetricKit diagnostics. `Scout.startBridge` does not subscribe + `MetricKitSubscriber`, so `MXCrashDiagnostic` and `MXHangDiagnostic` reports + never arrive. KSCrash still captures native crashes in-process. + +To get all three, call `Scout.start(...)` yourself in +`application(_:didFinishLaunchingWithOptions:)` **before** `flutterEngine.run()`, +using the same `serviceName` and `endpoint` as the Flutter config. The engine is +a first-wins singleton: whichever of `Scout.start` and the plugin's +`Scout.startBridge` runs first configures it, and the later call is a no-op. +Calling `Scout.start` after Flutter has started has no effect. See +[iOS](./ios.md). ::: --- -## Notes +## Configuration in hybrid mode -- **Service name + endpoint must match** on both sides. A mismatch produces two +- Service name and endpoint must match on both sides. A mismatch produces two separate services and no session unification. -- The **native SDK is the exporter**. Batching/retry settings on the native - config (`exportIntervalSeconds`, `maxExportBatchSize`, `maxQueueSize`, - `maxRetries`) govern what actually leaves the device; the Flutter side - forwards to it rather than exporting directly. +- The native SDK is the exporter. Batching and retry settings on the native + config govern what actually leaves the device; the Flutter side forwards to it + rather than exporting directly. In different-process, each process applies + these settings to its own exporter. +- The Flutter config is forwarded to the native side through the bridge: + `exportIntervalSeconds`, `maxExportBatchSize`, `maxQueueSize`, `maxRetries`, + `metricExportIntervalSeconds`, `firstPartyHosts`, and `debugLogging`. - Crash and ANR/app-hang reporting is handled by the native side in hybrid mode. - For the per-layer configuration options, see the platform pages: [Android](./android.md), [iOS](./ios.md), and [Flutter](./flutter.md). @@ -302,7 +380,61 @@ initialise the native iOS SDK yourself — see [iOS](./ios.md). | Symptom | Cause | Fix | | --- | --- | --- | -| **Two separate services** in the backend instead of one | The native and Flutter inits used a different `serviceName` or `endpoint` | Make `serviceName` **and** `endpoint` identical on both sides — this is what ties them to one service. | -| **Flutter screens/events don't appear** (native data does) | `ScoutFlutter.initialize(...)` never ran, or ran with a non-hybrid build of `scout_flutter` | Ensure `ScoutFlutter.initialize` runs in `main()` before `runApp`, using the hybrid `scout_flutter` version. | -| **Flutter data lands but under a different `session.id`** than native (not unified) | The bridge didn't attach — in different-process, the Flutter process started before the native host initialised Scout | Keep native `Scout.initialize` in the host and launch Flutter **from** the host (so native init runs first). For same-process this can't happen. | -| **Native screens/events don't appear** | On Android the native SDK was never initialised in the host (on iOS the native side starts automatically with `scout_flutter`) | Android: call `Scout.initialize` in the host `Activity`/`Application`. | +| **Two separate services** in the backend instead of one | The native and Flutter inits used a different `serviceName` or `endpoint` | Make `serviceName` and `endpoint` identical on both sides. That is what ties them to one service. | +| **Flutter screens/events don't appear** (native data does) | `ScoutFlutter.initialize(...)` never ran, or ran with a non-hybrid build of `scout_flutter` | Ensure `ScoutFlutter.initialize` runs in `main()` before `runApp`, using `scout_flutter` `0.2.0` or later. | +| **`main()` throws on startup** before any telemetry appears | `ScoutFlutter.initialize` reaches the native side over a platform channel, which needs the binding | Call `WidgetsFlutterBinding.ensureInitialized()` as the first line of `main()`. | +| **Flutter data lands but under a different `session.id`** than native (not unified) | Different-process only: `role` was set to `ScoutRole.OWNER`, so the `:flutter` process skipped the cross-process lookup and minted its own session | Leave `role` at its `ScoutRole.AUTO` default. Same-process is unaffected. | +| **Native screens/events don't appear on Android** | The native SDK was never initialized | Call `Scout.initialize` in `Application.onCreate`. | +| **No native taps, cold-start spans, or MetricKit reports on iOS** | Bridge mode leaves tap tracking, startup tracking, and the MetricKit subscriber off | Call `Scout.start(...)` in `application(_:didFinishLaunchingWithOptions:)` before `flutterEngine.run()`. Screens, KSCrash crashes, and hangs are already covered without it. | + +## FAQ + +**Do I need to change my Flutter code for hybrid?** + +No. The same `ScoutFlutter.initialize` call works. It detects the native SDK and +delegates automatically. Matching `serviceName` and `endpoint` is the only +requirement. + +**What happens if I use `scout_flutter` 0.1.x in a hybrid app?** + +The `0.1.x` line has no delegation bridge, so both SDKs export independently. +You get two sessions under one service instead of one unified session. + +**Does the bridge change anything for pure-Flutter apps?** + +Yes. From 0.2.0 the native engine starts alongside the Flutter SDK on both +platforms, even with no native host. A pure-Flutter app gains a native scope +with native crash capture and CPU/memory vitals. Flutter telemetry is unchanged. + +**Can the two layers use different service names?** + +No. A different `serviceName` or `endpoint` on either side produces two separate +services and no session unification. + +**Does iOS need a separate native dependency?** + +No. The iOS engine ships inside `scout_flutter` through the +`scout-kotlin-multiplatform` Swift package. + +## What's next + +- [Configure your collector](/instrument/collector-setup/docker-compose-example/) + to receive OTLP-HTTP on `:4318` +- Explore the data in [RUM](/operate/rum/getting-started) - both layers appear + under the one session you just unified +- Read the per-layer pages for the configuration options this page does not + cover: [Android](/instrument/mobile/android), [iOS](/instrument/mobile/ios), + and [Flutter](/instrument/mobile/flutter) +- Read [RUM with OpenTelemetry](/instrument/mobile/rum-opentelemetry) if you need + the span names and attributes behind those views + +## References + +- scout_flutter repo: + [github.com/base-14/scout-flutter](https://github.com/base-14/scout-flutter) +- scout-kotlin-multiplatform repo: + [github.com/base-14/scout-kotlin-multiplatform](https://github.com/base-14/scout-kotlin-multiplatform) +- Flutter add-to-app: + [docs.flutter.dev/add-to-app](https://docs.flutter.dev/add-to-app) +- Android manifest `android:process`: + [developer.android.com/guide/topics/manifest/activity-element#proc](https://developer.android.com/guide/topics/manifest/activity-element#proc)