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
| Component | Minimum 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 |
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 WebKitimport 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.
Call L9Rum.instrument(webView) after inflating or constructing your WebView. The SDK wraps the WebViewClient with a forwarding client that re-injects native context on every onPageStarted, keeping session and view IDs accurate across in-WebView navigations.
import android.webkit.WebViewimport io.last9.rum.L9Rum
// In your Activity or Fragment:val webView: WebView = findViewById(R.id.webView)webView.settings.javaScriptEnabled = true
L9Rum.instrument(webView)// Navigation-time injection is handled automatically.
webView.loadUrl(url)Legacy static-script path
val js = L9Rum.getWebViewInjectedJavaScript()webView.evaluateJavascript(js, null)As with iOS, the static path does not re-inject on navigation. Use instrument(webView) unless you have a specific reason to manage injection manually.
Pass a react-native-webview ref to L9Rum.instrumentWebView. The SDK resolves the underlying native WKWebView (iOS) or android.webkit.WebView (Android) and delegates to the native instrument() call, so navigation-time re-injection applies on both platforms.
-
Install
react-native-webviewif not already present:npm install react-native-webviewcd ios && pod install -
Wire up the ref and call
instrumentWebView:import React, { useRef } from 'react';import { WebView } from 'react-native-webview';import { L9Rum } from '@last9/rum-react-native';export function MyWebViewScreen() {const webViewRef = useRef<WebView>(null);return (<WebViewref={webViewRef}source={{ uri: 'https://app.example.com' }}onLoad={() => {if (webViewRef.current) {L9Rum.instrumentWebView(webViewRef);}}}/>);}
Call L9Rum.instrumentWebView(controller) after the WebViewController is initialized.
import 'package:webview_flutter/webview_flutter.dart';import 'package:last9_rum_flutter/last9_rum_flutter.dart';
class MyWebViewWidget extends StatefulWidget { const MyWebViewWidget({super.key});
@override State<MyWebViewWidget> createState() => _MyWebViewWidgetState();}
class _MyWebViewWidgetState extends State<MyWebViewWidget> { late final WebViewController _controller;
@override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..loadRequest(Uri.parse('https://app.example.com'));
L9Rum.instrumentWebView(_controller); }
@override Widget build(BuildContext context) { return WebViewWidget(controller: _controller); }}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)val config = L9RumConfig( baseUrl = "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>", clientToken = "your-client-token", serviceName = "my-android-app", serviceVersion = "1.0.0", deploymentEnvironment = "production", webViewAutoLoadBrowserRum = true, // webViewBrowserRumCdnUrl = "https://your-cdn.example.com/l9.umd.js")L9Rum.initialize(application, config)L9Rum.initialize({ baseUrl: "https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>", clientToken: "your-client-token", serviceName: "my-rn-app", serviceVersion: "1.0.0", deploymentEnvironment: "production", webViewAutoLoadBrowserRum: true, // webViewBrowserRumCdnUrl: "https://your-cdn.example.com/l9.umd.js",});await L9Rum.initialize(L9RumConfig( baseUrl: 'https://otlp-ext-aps1.last9.io/v1/otlp/organizations/<org>', clientToken: 'your-client-token', serviceName: 'my-flutter-app', serviceVersion: '1.0.0', deploymentEnvironment: 'production', webViewAutoLoadBrowserRum: true, // webViewBrowserRumCdnUrl: 'https://your-cdn.example.com/l9.umd.js',));Verification
-
Open your app and navigate to a screen that contains a WebView
-
Perform a network request inside the WebView (load a page, trigger an API call)
-
In Last9, open Discover > Applications and find the session
-
The session detail panel should show both native view spans and WebView spans in a single timeline
-
Native host view spans carry
view.type = webviewandnative.view.id. Browser RUM spans carry the samenative.view.id, confirming the join.
Troubleshooting
-
WebView spans appear in web RUM instead of mobile RUM
The Browser SDK inside a WebView emits spans with
telemetry.sdk.name: opentelemetry. Ifbrowser.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. 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
__L9RumNativeWebViewContextfrom the global scope on each span. If the WebView content is served from a different origin than the app’soriginconfig 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_rolloverevent fires on the browser side when this happens.
Please get in touch with us on Discord or Email if you have any questions.