dd-sdk-reactnative icon indicating copy to clipboard operation
dd-sdk-reactnative copied to clipboard

SessionReplay: Webview is empty on replay while RUM is instrumented on browser

Open kimwonj77 opened this issue 1 month ago • 9 comments

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.

Image

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

kimwonj77 avatar Oct 22 '25 00:10 kimwonj77

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 🙇

sbarrio avatar Oct 24 '25 09:10 sbarrio

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 avatar Oct 24 '25 10:10 kimwonj77

@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.

sbarrio avatar Oct 24 '25 10:10 sbarrio

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.

kimwonj77 avatar Oct 24 '25 10:10 kimwonj77

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!

sbarrio avatar Oct 24 '25 11:10 sbarrio

@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 🙇

sbarrio avatar Oct 24 '25 14:10 sbarrio

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.

Image

Logcat does show this warning: Could not find generated setter for class com.datadog.reactnative.sessionreplay.views.DdPrivacyViewManager

BillyBlaze avatar Oct 28 '25 15:10 BillyBlaze

Any updates about this?

kimwonj77 avatar Nov 11 '25 12:11 kimwonj77

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.

sbarrio avatar Nov 12 '25 13:11 sbarrio