diff --git a/packages/react-native-webview/src/__tests__/WebviewDatadogNewArchBlankScreen.test.tsx b/packages/react-native-webview/src/__tests__/WebviewDatadogNewArchBlankScreen.test.tsx
new file mode 100644
index 000000000..9d696b1a9
--- /dev/null
+++ b/packages/react-native-webview/src/__tests__/WebviewDatadogNewArchBlankScreen.test.tsx
@@ -0,0 +1,341 @@
+/*
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
+ * Copyright 2016-Present Datadog, Inc.
+ */
+
+/**
+ * Reproduction tests for RUMS-5458:
+ * White Blank Screen Across All WebViews with RUM Errors
+ *
+ * The customer uses a bare React Native app with New Architecture (Fabric/TurboModules)
+ * and the @datadog/mobile-react-native-webview wrapper. Despite having both the native
+ * Datadog WebView wrapper and Browser SDK instrumented, WebViews render as blank screens
+ * with error source labeled "WEBVIEW" in Session Replay.
+ *
+ * These tests verify the WebView wrapper behavior in scenarios matching the customer's
+ * setup, particularly around message handling, allowedHosts propagation, and the
+ * interaction between the Datadog bridge and user WebView content.
+ */
+
+import { render } from '@testing-library/react-native';
+import { WebView as RNWebView } from 'react-native-webview';
+import React from 'react';
+
+import { NativeDdSdk } from '../ext-specs/NativeDdSdk';
+import { WebView } from '../index';
+
+jest.mock('react-native-webview', () => {
+ return {
+ WebView: jest.fn()
+ };
+});
+
+jest.mock('../specs/NativeDdWebView', () => {
+ return {
+ NativeDdWebView: jest.fn()
+ };
+});
+
+jest.mock('../ext-specs/NativeDdSdk', () => {
+ return {
+ NativeDdSdk: {
+ consumeWebviewEvent: jest.fn().mockResolvedValue(undefined),
+ telemetryError: jest.fn().mockResolvedValue(undefined)
+ }
+ };
+});
+
+jest.mock('../ext-specs/NativeDdLogs', () => {
+ return {
+ __esModule: true,
+ default: {
+ error: jest.fn().mockResolvedValue(undefined)
+ }
+ };
+});
+
+describe('RUMS-5458: WebView blank screen with New Architecture', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Message handling should not interfere with WebView content', () => {
+ it('should forward non-Datadog messages to user onMessage handler even when message is valid JSON', () => {
+ /**
+ * In the customer's setup, the Epic MyChart WebView sends its own
+ * postMessage calls. These must be forwarded to the user's onMessage
+ * handler and not swallowed by the Datadog message interceptor.
+ */
+ const userOnMessage = jest.fn();
+ const allowedHosts = ['epicmychart.example.com'];
+
+ render(
+
+ );
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const datadogOnMessage = mockedWebView.mock.calls[0][0].onMessage;
+
+ // Simulate a WebView postMessage with arbitrary JSON (not Datadog)
+ const userJsonMessage = JSON.stringify({
+ type: 'navigation',
+ url: 'https://epicmychart.example.com/dashboard'
+ });
+
+ datadogOnMessage?.({
+ nativeEvent: { data: userJsonMessage }
+ } as any);
+
+ expect(userOnMessage).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not swallow messages when WebView sends empty or null data', () => {
+ /**
+ * Blank screen scenario: when WebView fails to load properly,
+ * it may send null or empty messages. The handler should not throw.
+ */
+ const userOnMessage = jest.fn();
+ const allowedHosts = ['epicmychart.example.com'];
+
+ render(
+
+ );
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const datadogOnMessage = mockedWebView.mock.calls[0][0].onMessage;
+
+ // Null data should be handled gracefully
+ expect(() => {
+ datadogOnMessage?.({
+ nativeEvent: { data: null }
+ } as any);
+ }).not.toThrow();
+
+ // Empty string should be forwarded to user handler
+ datadogOnMessage?.({
+ nativeEvent: { data: '' }
+ } as any);
+
+ // User handler should be called for non-null, non-Datadog messages
+ expect(userOnMessage).toHaveBeenCalledTimes(1);
+ });
+
+ it('should correctly route Datadog NATIVE_EVENT messages to consumeWebviewEvent', () => {
+ /**
+ * When the Browser SDK in the WebView sends RUM events through the
+ * bridge, they should reach consumeWebviewEvent on the native side.
+ * If this fails, WebView tracking data is lost and Session Replay
+ * shows the "WEBVIEW" error source.
+ */
+ const userOnMessage = jest.fn();
+ const allowedHosts = ['epicmychart.example.com'];
+
+ render(
+
+ );
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const datadogOnMessage = mockedWebView.mock.calls[0][0].onMessage;
+
+ const ddEvent = JSON.stringify({
+ source: 'DATADOG',
+ type: 'NATIVE_EVENT',
+ message: '{"eventType":"view","session":{"id":"abc123"}}'
+ });
+
+ datadogOnMessage?.({
+ nativeEvent: { data: ddEvent }
+ } as any);
+
+ expect(NativeDdSdk?.consumeWebviewEvent).toHaveBeenCalledWith(
+ '{"eventType":"view","session":{"id":"abc123"}}'
+ );
+ expect(userOnMessage).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('allowedHosts propagation in New Architecture', () => {
+ it('should include allowedHosts comment in injectedJavaScriptBeforeContentLoaded even without user JS code', () => {
+ /**
+ * In New Architecture, the native side extracts allowedHosts from
+ * the injectedJavaScriptBeforeContentLoaded prop via a regex comment.
+ * If allowedHosts is provided but no user JS code is given, the
+ * comment MUST still be injected for native-side WebView tracking to
+ * be configured.
+ *
+ * This is critical because without the comment, configureWebViewTracking
+ * is never called on the native side under New Architecture, and the
+ * WebView is tracked by the Datadog native component but without the
+ * bridge being set up — potentially causing blank screens.
+ */
+ const allowedHosts = ['epicmychart.example.com', 'www.epic.com'];
+
+ render();
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const injectedJs =
+ mockedWebView.mock.calls[0][0]
+ .injectedJavaScriptBeforeContentLoaded;
+
+ // The injected JS must contain the allowedHosts comment for native extraction
+ expect(injectedJs).toBeDefined();
+ expect(injectedJs).not.toBeNull();
+ expect(injectedJs).toContain('#allowedHosts=');
+ expect(injectedJs).toContain('epicmychart.example.com');
+ expect(injectedJs).toContain('www.epic.com');
+ });
+
+ it('should propagate allowedHosts through nativeConfig props for Old Architecture fallback', () => {
+ /**
+ * In Old Architecture, allowedHosts is passed as a native prop via
+ * nativeConfig. Verify it's always set.
+ */
+ const allowedHosts = ['epicmychart.example.com'];
+
+ render();
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const nativeProps = mockedWebView.mock.calls[0][0].nativeConfig
+ ?.props as any;
+
+ expect(nativeProps?.allowedHosts).toEqual(allowedHosts);
+ });
+
+ it('should handle empty allowedHosts gracefully without causing blank screen', () => {
+ /**
+ * If the customer initially forgot to set allowedHosts (as described
+ * in the ticket), the WebView should still render content — it just
+ * won't have Datadog tracking. The Datadog wrapper must not interfere
+ * with normal WebView rendering when allowedHosts is empty.
+ */
+ const allowedHosts: string[] = [];
+
+ render();
+
+ const mockedWebView = jest.mocked(RNWebView);
+
+ // The WebView should still be rendered
+ expect(mockedWebView).toHaveBeenCalled();
+
+ // injectedJavaScriptBeforeContentLoaded should either be undefined
+ // or contain valid (non-interfering) JS — not broken JS that causes
+ // the WebView to fail
+ const injectedJs =
+ mockedWebView.mock.calls[0][0]
+ .injectedJavaScriptBeforeContentLoaded;
+
+ if (injectedJs !== undefined && injectedJs !== null) {
+ // If any JS is injected, it should not contain broken allowedHosts
+ // that would cause the native side to misconfigure tracking
+ expect(injectedJs).not.toContain('#allowedHosts=[]');
+ }
+ });
+
+ it('should not inject broken allowedHosts comment when allowedHosts is undefined', () => {
+ /**
+ * When allowedHosts is not provided at all, no tracking comment
+ * should be injected. The WebView should work as a plain WebView.
+ */
+ render();
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const injectedJs =
+ mockedWebView.mock.calls[0][0]
+ .injectedJavaScriptBeforeContentLoaded;
+
+ // Should be undefined — no Datadog injection when no hosts configured
+ expect(injectedJs).toBeUndefined();
+ });
+ });
+
+ describe('WebView rendering with Datadog wrapper should not cause blank screen', () => {
+ it('should not override user-provided injectedJavaScriptBeforeContentLoaded', () => {
+ /**
+ * The customer may have their own JS injected before content loads.
+ * The Datadog wrapper must preserve it while adding its own tracking
+ * comment. If the user's JS is overwritten, it could cause blank screens.
+ */
+ const userJs = "window.__APP_CONFIG__ = { theme: 'dark' };";
+ const allowedHosts = ['epicmychart.example.com'];
+
+ render(
+
+ );
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const injectedJs =
+ mockedWebView.mock.calls[0][0]
+ .injectedJavaScriptBeforeContentLoaded;
+
+ // Must contain both the allowedHosts comment AND the user's JS code
+ expect(injectedJs).toBeDefined();
+ expect(injectedJs).toContain('#allowedHosts=');
+ expect(injectedJs).toContain('__APP_CONFIG__');
+ });
+
+ it('should preserve user injectedJavaScript when Datadog wrapper is used', () => {
+ /**
+ * User-provided injectedJavaScript must still be passed through
+ * to the underlying WebView.
+ */
+ const userJs = "document.body.style.backgroundColor = 'white';";
+ const allowedHosts = ['epicmychart.example.com'];
+
+ render(
+
+ );
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const injectedJs =
+ mockedWebView.mock.calls[0][0].injectedJavaScript;
+
+ expect(injectedJs).toBeDefined();
+ expect(injectedJs).toContain('backgroundColor');
+ });
+
+ it('should pass all standard WebView props through to the underlying RNWebView', () => {
+ /**
+ * The Datadog wrapper must be transparent for standard WebView props.
+ * If props like source, style, or scrollEnabled are dropped, the
+ * WebView may not render content (blank screen).
+ */
+ const source = { uri: 'https://epicmychart.example.com' };
+ const style = { flex: 1 };
+ const allowedHosts = ['epicmychart.example.com'];
+
+ render(
+
+ );
+
+ const mockedWebView = jest.mocked(RNWebView);
+ const passedProps = mockedWebView.mock.calls[0][0];
+
+ expect(passedProps.source).toEqual(source);
+ expect(passedProps.style).toEqual(style);
+ expect(passedProps.scrollEnabled).toBe(true);
+ expect(passedProps.javaScriptEnabled).toBe(true);
+ });
+ });
+});
diff --git a/packages/react-native-webview/src/__tests__/WebviewDatadogNewArchIntegration.test.ts b/packages/react-native-webview/src/__tests__/WebviewDatadogNewArchIntegration.test.ts
new file mode 100644
index 000000000..fd194a71e
--- /dev/null
+++ b/packages/react-native-webview/src/__tests__/WebviewDatadogNewArchIntegration.test.ts
@@ -0,0 +1,219 @@
+/*
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
+ * Copyright 2016-Present Datadog, Inc.
+ */
+
+/**
+ * Integration tests for RUMS-5458:
+ * Verifies the interaction between JS-side allowedHosts encoding and the
+ * native-side extraction logic used in New Architecture.
+ *
+ * In the New Architecture, the native ViewManager cannot receive custom props
+ * directly from React. Instead, allowedHosts is encoded as a comment in
+ * injectedJavaScriptBeforeContentLoaded and extracted on the native side
+ * via regex. If this encoding/extraction chain breaks, WebView tracking is
+ * not configured, leading to blank screens and "WEBVIEW" error sources.
+ */
+
+import { wrapJsCodeWithAllowedHosts } from '../utils/webview-js-utils';
+
+/**
+ * Simulates the native-side regex extraction of allowedHosts from
+ * injectedJavaScriptBeforeContentLoaded. This mirrors the Kotlin code in
+ * DdSdkReactNativeWebViewManager.extractAllowedHosts (newarch variant).
+ */
+function extractAllowedHostsFromInjectedJs(
+ input: string | undefined
+): string[] | null {
+ if (!input) {
+ return null;
+ }
+
+ // Matches the Kotlin regex: """//\s*#allowedHosts\s*=\s*(.+)"""
+ const regex = /\/\/\s*#allowedHosts\s*=\s*(.+)/;
+ const match = input.match(regex);
+ if (!match) {
+ return null;
+ }
+
+ const jsonString = match[1].trim();
+ try {
+ return JSON.parse(jsonString);
+ } catch {
+ return null;
+ }
+}
+
+describe('RUMS-5458: New Architecture allowedHosts encoding/extraction chain', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('allowedHosts round-trip: JS encoding -> native extraction', () => {
+ it('should encode and extract single host correctly', () => {
+ const allowedHosts = ['epicmychart.example.com'];
+ const injectedJs = wrapJsCodeWithAllowedHosts(
+ undefined,
+ allowedHosts
+ );
+
+ const extracted = extractAllowedHostsFromInjectedJs(injectedJs);
+ expect(extracted).toEqual(allowedHosts);
+ });
+
+ it('should encode and extract multiple hosts correctly', () => {
+ const allowedHosts = [
+ 'epicmychart.example.com',
+ 'www.epic.com',
+ 'portal.optum.com'
+ ];
+ const injectedJs = wrapJsCodeWithAllowedHosts(
+ undefined,
+ allowedHosts
+ );
+
+ const extracted = extractAllowedHostsFromInjectedJs(injectedJs);
+ expect(extracted).toEqual(allowedHosts);
+ });
+
+ it('should encode and extract hosts with user JS code present', () => {
+ const allowedHosts = ['epicmychart.example.com'];
+ const userJs = 'window.__init__ = true;';
+ const injectedJs = wrapJsCodeWithAllowedHosts(userJs, allowedHosts);
+
+ const extracted = extractAllowedHostsFromInjectedJs(injectedJs);
+ expect(extracted).toEqual(allowedHosts);
+
+ // User JS should also be present
+ expect(injectedJs).toContain('__init__');
+ });
+
+ it('should handle hosts with special characters in URLs', () => {
+ const allowedHosts = [
+ 'my-app.example.com',
+ 'app.sub.domain.co.uk',
+ 'localhost:3000'
+ ];
+ const injectedJs = wrapJsCodeWithAllowedHosts(
+ undefined,
+ allowedHosts
+ );
+
+ const extracted = extractAllowedHostsFromInjectedJs(injectedJs);
+ expect(extracted).toEqual(allowedHosts);
+ });
+ });
+
+ describe('Edge cases that may cause blank screens', () => {
+ it('should return null extraction when allowedHosts is empty array', () => {
+ /**
+ * BUG REPRODUCTION: When allowedHosts is an empty array [],
+ * the JS side encodes it as `// #allowedHosts=[]`, but the native
+ * side should NOT call configureWebViewTracking with an empty list,
+ * as this could interfere with WebView rendering.
+ *
+ * Expected: empty array should NOT produce an allowedHosts comment,
+ * because tracking with 0 hosts is meaningless and the native
+ * component override (DdReactNativeWebView) may still interfere
+ * with normal rendering.
+ */
+ const allowedHosts: string[] = [];
+ const injectedJs = wrapJsCodeWithAllowedHosts(
+ undefined,
+ allowedHosts
+ );
+
+ if (injectedJs !== undefined) {
+ const extracted = extractAllowedHostsFromInjectedJs(injectedJs);
+ // If extraction succeeds, it should NOT return an empty array
+ // because enabling WebView tracking with empty hosts is wrong
+ expect(extracted).toBeNull();
+ }
+ });
+
+ it('should not produce allowedHosts comment when allowedHosts is undefined', () => {
+ /**
+ * When allowedHosts is undefined, no tracking comment should exist.
+ * The native side should see no comment and skip tracking setup.
+ */
+ const injectedJs = wrapJsCodeWithAllowedHosts(undefined, undefined);
+
+ expect(injectedJs).toBeUndefined();
+
+ const extracted = extractAllowedHostsFromInjectedJs(injectedJs);
+ expect(extracted).toBeNull();
+ });
+
+ it('should produce valid JS code that does not break WebView rendering', () => {
+ /**
+ * The injected JS must be syntactically valid. If it contains syntax
+ * errors, the WebView's JavaScript context will fail, potentially
+ * causing a blank screen.
+ */
+ const allowedHosts = ['epicmychart.example.com'];
+ const userJs = "document.addEventListener('load', function() {});";
+ const injectedJs = wrapJsCodeWithAllowedHosts(userJs, allowedHosts);
+
+ expect(injectedJs).toBeDefined();
+
+ // Verify the JS is syntactically valid by attempting to parse it
+ // eslint-disable-next-line no-eval
+ expect(() => eval(`(function() { ${injectedJs} })`)).not.toThrow();
+ });
+
+ it('should produce valid JS even with complex user code', () => {
+ /**
+ * Complex user-injected JS should be safely wrapped without
+ * breaking the overall JS syntax.
+ */
+ const allowedHosts = ['epicmychart.example.com'];
+ const complexUserJs = `
+ (function() {
+ var config = { api: "https://api.example.com" };
+ window.postMessage(JSON.stringify(config), "*");
+ })();
+ `;
+ const injectedJs = wrapJsCodeWithAllowedHosts(
+ complexUserJs,
+ allowedHosts
+ );
+
+ expect(injectedJs).toBeDefined();
+ // eslint-disable-next-line no-eval
+ expect(() => eval(`(function() { ${injectedJs} })`)).not.toThrow();
+ });
+ });
+
+ describe('DatadogEventBridge injection behavior', () => {
+ it('should not inject DatadogEventBridge setup code in injectedJavaScriptBeforeContentLoaded', () => {
+ /**
+ * BUG INVESTIGATION: The RFC for WebView tracking on React Native
+ * states that the JS-side should inject a DatadogEventBridge object
+ * before the WebView loads. However, in the current implementation,
+ * this bridge is set up on the native side via WebViewTracking.enable().
+ *
+ * If the native side fails to call WebViewTracking.enable() (e.g.,
+ * because allowedHosts extraction failed), the Browser SDK in the
+ * WebView will not find the bridge and may behave unexpectedly.
+ *
+ * Expected: The injected JS should NOT contain DatadogEventBridge
+ * setup -- that's the native side's job. But we verify the comment
+ * is present so the native side can do its job.
+ */
+ const allowedHosts = ['epicmychart.example.com'];
+ const injectedJs = wrapJsCodeWithAllowedHosts(
+ undefined,
+ allowedHosts
+ );
+
+ expect(injectedJs).toBeDefined();
+ // The JS-side should NOT inject DatadogEventBridge
+ // (that's done natively by WebViewTracking.enable)
+ expect(injectedJs).not.toContain('DatadogEventBridge');
+
+ // But it MUST contain the allowedHosts for native extraction
+ expect(injectedJs).toContain('#allowedHosts=');
+ });
+ });
+});