Skip to content
Last9
Book demo

WebView Session Correlation

Correlate native iOS/Android RUM sessions with Browser RUM spans running inside a WebView. Single session timeline across native and web surfaces.

WebView session correlation links native iOS/Android RUM sessions with Browser RUM spans running inside a WebView. Once set up, the session detail panel shows a single unified timeline — native view spans alongside WebView network calls and page navigations — queryable by the same session.id.

How it works

When a native app navigates into a WebView, the SDK injects the current session.id, view.id, and native.view.id as JavaScript globals. The Browser RUM SDK running inside the WebView reads these globals on every span and adopts the native session ID instead of generating its own.

On the native side, the host view span is tagged with view.type = webview and native.view.id (equal to the native view span’s own trace ID). The dashboard uses native.view.id to join Browser RUM spans — which live in a separate trace — back to the native view without a cross-trace lookup.

Native app
└─ View span [session.id=abc, view.type=webview, native.view.id=xyz]
└─ WebView
└─ Browser RUM spans [session.id=abc, native.view.id=xyz]
└─ XHR/fetch spans [session.id=abc, native.view.id=xyz]

The Browser SDK fires a l9rum:session_rollover event when it adopts a native session. The original browser-generated session ID is preserved as previous_id on the session span.

Prerequisites

ComponentMinimum version
iOS SDK (Last9RUM)0.5.0
Android SDK (io.last9:rum-android)0.5.0
React Native SDK (@last9/rum-react-native)0.5.0
Flutter SDK (last9_rum_flutter)0.5.0
Browser SDK (@last9/rum)2.5.0

Use Browser 2.8.0 and mobile SDK 1.6.0 artifacts. The mobile stable/v1 channels currently serve 1.6.0.

The Browser SDK does not require any additional configuration — session adoption happens automatically when native context is detected.

Setup

Call L9Rum.shared.instrument(_:) after creating your WKWebView. The SDK attaches a WKNavigationDelegate that re-injects native context on every navigation, so session and view IDs stay current as the user moves between pages inside the WebView.

import WebKit
import Last9RUM
// In your UIViewController or SwiftUI UIViewRepresentable:
let webView = WKWebView(frame: .zero, configuration: configuration)
L9Rum.shared.instrument(webView)
// That's it — injection happens on every navigation automatically.
webView.load(URLRequest(url: url))

Legacy static-script path

If you cannot use instrument(_:) (for example, when building the WKWebView outside of your code), you can inject the script manually:

let js = try L9Rum.shared.getWebViewInjectedJavaScript()
let script = WKUserScript(
source: js,
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
webView.configuration.userContentController.addUserScript(script)

With the static-script path you are responsible for re-injecting when the view or session changes. instrument(_:) handles this automatically.

Native view tracking on WebView screens

WebView correlation produces two correlated but separate views per navigation:

  1. Native host view — the iOS UIViewController / Android Activity that contains the WebView (auto-tracked by the mobile SDK).
  2. Web route view — each in-WebView navigation tracked by Browser RUM startView() (for example, an SPA route change inside the WebView).

This is intentional. A WebView can occupy only part of a native screen, so route-level tracking inside the WebView is separate from the native screen view. The dashboard joins them via session.id and native.view.id.

Optionally name the native host with setViewName()

UIKit and Activity lifecycle tracking already creates the native host view span. You can call setViewName() once to give that span a fixed, friendly name. Do not call native startView() on a WebView host screen, and do not call setViewName() again when its URL changes.

On SDK 1.5.0 and later, startView ends the auto-tracked host view before opening a new one, so it no longer leaves a duplicate native view. It still replaces the auto-tracked span. setViewName() is the API that renames the auto-tracked view in place.

On SDK versions before 1.5.0, calling startView() on an auto-tracked screen opened a second native view span alongside the auto-tracked one.

Auto-name the host from the WebView URL (1.6.0+)

From mobile SDK 1.6.0, a WebView instrumented with native instrument(webView) / L9Rum.instrumentWebView(...) auto-names the native host view from the current main-frame URL:

  • app.screen.name is the folded pathname (e.g. https://www.example.com/health-record?webViewFrame=edgeToEdge/health-record; /order/12345/order/?).
  • view.url carries the full URL (scheme, host, path, and query) for the session detail panel.

Android updates the name on full loads and in-WebView history changes. iOS updates on navigation commit — client-side SPA route changes (pushState / replaceState / hash) do not re-name the iOS host view.

An explicit startView / setViewName still wins; the URL-derived name never overwrites an app-provided one. view.url is recorded either way. Do not use either native API to name each WebView URL. setViewName() renames the currently active native view immediately; the later navigation commit creates the actual WebView page view. Calling it per navigation can therefore rename the previous native screen and produce two rows with the upcoming URL.

This does not apply to Flutter, which uses getWebViewInjectedJavaScript() rather than native instrument(webView). Name Flutter WebView host views with L9Rum.startView(...).

Recommended pattern:

// 1. Instrument the WKWebView (once, after creation)
L9Rum.shared.instrument(webView: webView)
// 2. Optional: set one fixed host label. Do not repeat this on URL changes.
L9Rum.shared.setViewName("WebViewActivity")
// 3. Track in-WebView SPA routes via Browser RUM only
// (inside your WebView JS bridge or page code)
L9RUM.startView({ name: routePath })

Screen load time is measured from when the native view span starts (viewDidAppear / onResume for auto-tracked screens). Browser RUM inside the WebView tracks its own route-level timings separately.

Auto-load Browser RUM

By default, the native SDK injects context globals but does not load the Browser RUM SDK. Your WebView HTML must already include @last9/rum and initialize it.

To have the native SDK auto-load the Browser RUM bundle from Last9’s CDN into every WebView, set webViewAutoLoadBrowserRum = true in the SDK config:

let config = L9RumConfig(
baseUrl: "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>",
clientToken: "your-client-token",
serviceName: "my-ios-app",
serviceVersion: "1.0.0",
deploymentEnvironment: "production"
)
config.webViewAutoLoadBrowserRum = true
// Optional: override the CDN URL (defaults to Last9's stable CDN)
// config.webViewBrowserRumCdnUrl = "https://your-cdn.example.com/l9.umd.js"
L9Rum.shared.initialize(config: config)

Verification

  1. Open your app and navigate to a screen that contains a WebView

  2. Perform a network request inside the WebView (load a page, trigger an API call)

  3. In Last9, open Discover > Applications and find the session

  4. The session detail panel should show both native view spans and WebView spans in a single timeline

  5. Native host view spans carry view.type = webview and native.view.id. Browser RUM spans carry the same native.view.id, confirming the join. On 1.6.0+, the native host also carries view.url (full URL) and a folded app.screen.name unless you set the name yourself.


Troubleshooting

  • WebView spans appear in web RUM instead of mobile RUM

    The Browser SDK inside a WebView emits spans with telemetry.sdk.name: opentelemetry. If browser.mobile: "true" is not set as a resource attribute, the dashboard may classify the session as web. Ensure the Browser SDK is initialized after the native context globals are present — instrument(webView) / instrumentWebView() handles injection before page load, so timing is correct with that path. On Flutter, inject with getWebViewInjectedJavaScript() from onPageStarted. The static-script path can have timing issues if the script runs before the globals are written.

  • Native context not being picked up

    The Browser SDK reads __L9RumNativeWebViewContext from the global scope on each span. If the WebView content is served from a different origin than the app’s origin config value, the injected script may be blocked by the WebView’s content security policy. Verify that the WebView’s CSP allows inline scripts from the native injection.

  • Session ID changes mid-WebView session

    If the native session expires (max duration or idle timeout) while the user is inside a WebView, the native SDK writes a new session ID to the global. The Browser SDK reads fresh on each span, so subsequent spans automatically use the new session ID. The l9rum:session_rollover event fires on the browser side when this happens.

  • Duplicate view records on WebView screens

    Seeing one native view plus one Browser RUM route view per navigation is expected. If both rows are native views, remove any native startView() or per-navigation setViewName() calls. Instrumentation already names full-page WebView navigations. Use setViewName() at most once for a fixed host label, and use Browser RUM startView() inside the WebView for SPA routes. A per-navigation setViewName() can rename the previous native view to the upcoming URL before instrumentation creates the real page view. See Native view tracking on WebView screens above.

Please get in touch with us on Discord or Email if you have any questions.