dd-sdk-reactnative
dd-sdk-reactnative copied to clipboard
SessionReplay: Webview is empty on replay while RUM is instrumented on browser
Describe the bug
We have some webview-wrapped Hybrid web app with expo. Both Expo side and Browser-side RUM instrumentation is installed.
But with import { WebView } from "@datadog/mobile-react-native-webview";. WebView is rendered as empty on SessionReplay.
Using standard Webview(from react-native-webview) makes Data is not being connected between Native and Webview but Web-Content RUM is shown on WebView-app.
(Note that when using Webview from DataDog, Browser RUM is not showing up but only click events on Native RUM Logs)
Reproduction steps
Bare minimum:
export default function App() {
// .... some datadog RUM sdk init with sessionReplay with all sample rate to 100,
return (
<View style={containerStyle}>
<ProgressBar isLoading={isLoading} progress={loadingProgress} />
<WebView
{...webViewProps}
allowsBackForwardNavigationGestures={true}
pullToRefreshEnabled={true}
decelerationRate="normal"
bounces={true}
scrollEnabled={true}
directionalLockEnabled={false}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
// allowedHosts={ALLOWED_HOSTS}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollViewContent: {
flex: 1,
},
webview: {
flex: 1,
},
});
datadogRum.init({
// ...
sessionSampleRate: 100,
sessionReplaySampleRate: 100,
});
SDK logs
No response
Expected behavior
WebView content is rendered, at least seperated RUM session data showing up with connected-data
Affected SDK versions
2.12.4
Latest working SDK version
no
Did you confirm if the latest SDK version fixes the bug?
Yes
Integration Methods
NPM
React Native Version
0.81.5
Package.json Contents
{ "name": "private", "version": "1.0.0", "main": "index.ts", "scripts": { "start": "expo start", "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", "lint": "eslint . --ext .ts,.tsx", "postinstall": "patch-package" }, "dependencies": { "@datadog/mobile-react-native": "^2.12.4", "@datadog/mobile-react-native-session-replay": "^2.12.4", "@datadog/mobile-react-native-webview": "^2.12.4", "@datadog/mobile-react-navigation": "^2.12.4", "@react-native-firebase/analytics": "^23.4.0", "@react-native-firebase/app": "^23.4.0", "@react-native-firebase/crashlytics": "^23.4.0", "@react-native-firebase/messaging": "^23.4.0", "@react-native-seoul/kakao-login": "^5.4.1", "@react-native-seoul/naver-login": "^4.2.2", "@tosspayments/widget-sdk-react-native": "^1.5.0", "expo": "54.0.17", "expo-apple-authentication": "~8.0.7", "expo-application": "^7.0.7", "expo-build-properties": "~1.0.9", "expo-constants": "~18.0.10", "expo-datadog": "^54.0.0", "expo-dev-client": "~6.0.16", "expo-device": "~8.0.9", "expo-haptics": "^15.0.7", "expo-image-picker": "^17.0.8", "expo-localization": "^17.0.7", "expo-navigation-bar": "~5.0.9", "expo-notifications": "~0.32.12", "expo-sensors": "^15.0.7", "expo-splash-screen": "^31.0.10", "expo-status-bar": "~3.0.8", "expo-tracking-transparency": "^6.0.7", "expo-updates": "~29.0.12", "expo-web-browser": "~15.0.8", "i": "^0.3.7", "patch-package": "^8.0.0", "react": "19.1.0", "react-native": "0.81.5", "react-native-safe-area-context": "^5.6.1", "react-native-webview": "13.15.0" }, "devDependencies": { "@datadog/datadog-ci": "^3.21.4", "@types/react": "~19.1.0", "@typescript-eslint/eslint-plugin": "^8.44.0", "@typescript-eslint/parser": "^8.44.0", "eslint": "^9.36.0", "eslint-plugin-expo": "^1.0.0", "typescript": "~5.9.2" }, "private": true }
....
"@datadog/browser-logs": "^6.19.0", "@datadog/browser-rum": "^6.19.0", "@datadog/browser-rum-react": "^6.19.0",
iOS Setup
No response
Android Setup
No response
Device Information
Both android and iOS
Other relevant information
No response
Hi @kimwonj77 , thanks for reaching out, and sorry for the delay.
Have you tested it by setting allowedHosts with the urls you want to track?:
<WebView
source={{ uri: 'https://www.example.com' }}
allowedHosts={['example.com']}
/>
This is vital for session replay to properly record the webView contents.
Also, can you please share the contents of datadogRum.init just to make sure that everything is set correctly?
Thanks 🙇
Allowed host is set like this:
export const ALLOWED_HOSTS = [
"app.service.com", "app-dev.service.com"
// just in case, for login setups
"appleid.apple.com",
"accounts.google.com",
];
...
const [currentUrl, setCurrentUrl] = useState("https://app.service.com");
const handleNavigationStateChange = useCallback(
(navState: { url: string; }) => {
setCurrentUrl(navState.url);
},
[]
);
<WebView
source={{ uri: currentUrl }}, // it uses useState for tracking thing
onNavigationStateChange={handleNavigationStateChange},
// note: some javascript injections happens too.
allowedHosts={ALLOWED_HOSTS}
/>
on frontend-side. we set like this:
// Initialize RUM with React plugin
const rumConfig: RumInitConfiguration = {
applicationId: config.applicationId,
clientToken: config.clientToken,
site: config.site as RumInitConfiguration["site"],
service: config.service,
env,
version: config.version,
sessionSampleRate: 100,
sessionReplaySampleRate: 100,
trackUserInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: "mask-user-input",
allowedTracingUrls: [import.meta.env.VITE_API_BASE_URL],
beforeSend: (event) => {
// Filter out known non-critical errors if needed
if (
event.type === "error" &&
event.error?.message?.includes("ResizeObserver loop")
) {
return false;
}
return true;
},
// As we're using TanStack Router, router to false
plugins: [reactPlugin({ router: false })],
};
datadogRum.init(rumConfig);
Note in main issue description, has some typo that allowed host was not set, it was due to using normal web view for mitigation temporarily. When I tested with Datadog Webview, it is correctly set. Also tried setting applicationId/clientToken same as frontend but no effect.
Edit: import was import { WebView } from "@datadog/mobile-react-native-webview";
@kimwonj77 Alright, thanks for the info.
Can you also share the Datadog configuration you are using for the expo-app? (not for the page you are presenting on the web view, but the mobile app itself). It should be either calling DdSdkReactNative.initialize or using the DatadogProvider wrapper. Also, once it is initialized you should be calling SessionReplay.enable() and passing it its specific SR configuration.
Also, you should not use the same Datadog Credentials (Client Token, App Id) for the webpage and the expo app.
import {
SessionReplay,
} from "@datadog/mobile-react-native-session-replay";
import {
DdSdkReactNative,
DdSdkReactNativeConfiguration,
DdLogs,
DdTrace,
DdRum,
ErrorSource,
RumActionType,
UploadFrequency,
BatchSize,
SdkVerbosity,
} from "@datadog/mobile-react-native";
import { Platform } from "react-native";
import Constants from "expo-constants";
import { DATADOG_CONFIG } from "../config/datadog";
interface DatadogConfig {
clientToken: string;
environment: string;
applicationId: string;
trackInteractions?: boolean;
trackResources?: boolean;
trackErrors?: boolean;
}
class DatadogService {
private static instance: DatadogService;
private isInitialized = false;
static getInstance(): DatadogService {
if (!DatadogService.instance) {
DatadogService.instance = new DatadogService();
}
return DatadogService.instance;
}
async initialize(config: DatadogConfig): Promise<void> {
if (this.isInitialized) {
console.log("[Datadog] Already initialized");
return;
}
// Initialize Datadog SDK
try {
const ddConfig = new DdSdkReactNativeConfiguration(
config.clientToken,
config.environment,
config.applicationId,
config.trackInteractions ?? true,
config.trackResources ?? true,
config.trackErrors ?? true
);
// Set service name
ddConfig.serviceName = DATADOG_CONFIG.SERVICE_NAME;
// Set version
ddConfig.version = Constants.expoConfig?.version || "1.0.0";
// Set sample rate from config
ddConfig.sessionSamplingRate = DATADOG_CONFIG.SESSION_SAMPLE_RATE;
// Enable native crash reporting
ddConfig.nativeCrashReportEnabled = DATADOG_CONFIG.NATIVE_CRASH_REPORT;
// Set custom endpoints if needed (for EU datacenter, etc.)
ddConfig.site = DATADOG_CONFIG.SITE;
if (__DEV__) {
// Optional: Send data more frequently
ddConfig.uploadFrequency = UploadFrequency.FREQUENT;
// Optional: Send smaller batches of data
ddConfig.batchSize = BatchSize.SMALL;
// Optional: Enable debug logging
ddConfig.verbosity = SdkVerbosity.DEBUG;
}
// Initialize Datadog
await DdSdkReactNative.initialize(ddConfig)
.then(() => {
if (DATADOG_CONFIG.ENABLE_SESSION_REPLAY) {
SessionReplay.enable({
replaySampleRate:
// Session Replay will be available for all sessions already captured by the SDK
DATADOG_CONFIG.SESSION_REPLAY_SAMPLE_RATE,
textAndInputPrivacyLevel:
DATADOG_CONFIG.SESSION_REPLAY_TEXT_INPUT_PRIVACY_LEVEL,
imagePrivacyLevel:
DATADOG_CONFIG.SESSION_REPLAY_IMAGE_PRIVACY_LEVEL,
touchPrivacyLevel:
DATADOG_CONFIG.SESSION_REPLAY_TOUCH_PRIVACY_LEVEL,
startRecordingImmediately: true,
});
}
})
.catch((error) => {
console.error("[Datadog] Initialization error:", error);
});
// Set global attributes
this.setGlobalAttributes({
platform: Platform.OS,
environment: __DEV__ ? "development" : "production",
appVersion: Constants.expoConfig?.version || "1.0.0",
});
this.isInitialized = true;
console.log("[Datadog] Initialized successfully");
} catch (error) {
console.error("[Datadog] Failed to initialize:", error);
}
}
// Logging methods
debug(message: string, context?: Record<string, unknown>): void {
if (!this.isInitialized) {
console.log(`[DEBUG] ${message}`, context);
return;
}
DdLogs.debug(message, context);
}
...
}
export default DatadogService;
export const datadog = DatadogService.getInstance();
import {
ImagePrivacyLevel,
TextAndInputPrivacyLevel,
TouchPrivacyLevel,
} from "@datadog/mobile-react-native-session-replay";
export const DATADOG_CONFIG = {
// Enable/Disable Datadog
ENABLED: true,
CLIENT_TOKEN: "<redacted>
APPLICATION_ID: "<redacted>",
// Datacenter site (US1, US3, US5, EU1, AP1)
SITE: "US1",
// Service name
SERVICE_NAME: "app",
// Environment
ENVIRONMENT: __DEV__ ? "development" : "production",
// Session tracking
SESSION_SAMPLE_RATE: 100,
// Feature flags
TRACK_INTERACTIONS: true,
TRACK_RESOURCES: true,
TRACK_ERRORS: true,
NATIVE_CRASH_REPORT: true,
// Session Replay
ENABLE_SESSION_REPLAY: true,
SESSION_REPLAY_SAMPLE_RATE: 100,
SESSION_REPLAY_TEXT_INPUT_PRIVACY_LEVEL:
TextAndInputPrivacyLevel.MASK_SENSITIVE_INPUTS,
SESSION_REPLAY_IMAGE_PRIVACY_LEVEL: ImagePrivacyLevel.MASK_NONE,
SESSION_REPLAY_TOUCH_PRIVACY_LEVEL: TouchPrivacyLevel.SHOW,
};
import React, { useEffect, useState, useCallback } from "react";
import { StatusBar } from "expo-status-bar";
import { StyleSheet, View, Platform } from "react-native";
import { SafeAreaProvider } from "react-native-safe-area-context";
import * as SplashScreen from "expo-splash-screen";
import { WebViewContainer } from "./components/WebViewContainer";
import DatadogService from "./services/DatadogService";
import {
getAnalytics,
setAnalyticsCollectionEnabled,
} from "@react-native-firebase/analytics";
import { DATADOG_CONFIG } from "./app/config/datadog";
// Register Firebase background message handler
import "./app/services/firebase-background-handler";
SplashScreen.preventAutoHideAsync();
const WEB_URL = __DEV__ ? "https://app-dev.service.com" : "https://app.service.com";
export default function App() {
const [isAppInitialized, setIsAppInitialized] = useState(false);
const [isWebViewLoaded, setIsWebViewLoaded] = useState(false);
// Callback for when WebView finishes loading
const handleWebViewLoadCompleted = useCallback(() => {
setIsWebViewLoaded(true);
}, []);
// Initialize app, OAuth SDKs, Crashlytics, and Datadog
useEffect(() => {
const initializeApp = async () => {
try {
// ...
const datadogService = DatadogService.getInstance();
await datadogService.initialize({
clientToken: DATADOG_CONFIG.CLIENT_TOKEN,
environment: DATADOG_CONFIG.ENVIRONMENT,
applicationId: DATADOG_CONFIG.APPLICATION_ID,
trackInteractions: DATADOG_CONFIG.TRACK_INTERACTIONS,
trackResources: DATADOG_CONFIG.TRACK_RESOURCES,
trackErrors: DATADOG_CONFIG.TRACK_ERRORS,
});
setIsAppInitialized(true);
} catch (error) {
console.error("Failed to initialize app:", error);
setIsAppInitialized(true); // Continue even if initialization fails (for splash screen)
}
};
initializeApp();
}, []);
return (
<SafeAreaProvider>
<View style={styles.container}>
<StatusBar style="dark" />
<WebViewContainer /> {/* this component has web view *}
</View>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
});
useEffect was used for App Tracking Transparency check. (removed in current code). Note that instruments are working but not web view session replay playback (empty out the web view only. can see the touch points and etc). session replay is always happened when ATT is enabled (in private codes) like these above.
Thanks for the prompt reply 🙇
Your configuration looks fine and it should work as it is.
I'm going to try to reproduce the problem following your setup and will come back to you shortly.
Thanks again!
@kimwonj77 Hey again. I've managed to reproduce the issue, both on iOS and Android. I'm going to create an escalation so it gets dealt with ASAP.
Thanks for raising this and sorry for the inconvenience 🙇
I am getting the same results without expo (pure react-native), I use the DatadogProvider and I do see interactions from the user but the webview is a blank screen.
Logcat does show this warning:
Could not find generated setter for class com.datadog.reactnative.sessionreplay.views.DdPrivacyViewManager
Any updates about this?
Any updates about this?
Hi @kimwonj77, we are actively investigating the root cause on the issue and are aiming to fix it ASAP. We'll let you know as soon as we have more news regarding it.