Skip to content

Reproduce RUMS-5458: WebView blank screen with New Architecture#1208

Closed
mariusc83 wants to merge 1 commit into
developfrom
mconstantin/RUMS-5458/white-blank-screen-webviews
Closed

Reproduce RUMS-5458: WebView blank screen with New Architecture#1208
mariusc83 wants to merge 1 commit into
developfrom
mconstantin/RUMS-5458/white-blank-screen-webviews

Conversation

@mariusc83

Copy link
Copy Markdown
Member

Reproduction for RUMS-5458

Jira: RUMS-5458

Issue Summary

WebView tracking broken in bare RN app with New Architecture - blank screens and WEBVIEW error source. The @datadog/mobile-react-native-webview wrapper injects // #allowedHosts=[] when an empty allowedHosts array is provided, causing the native side (New Architecture) to call configureWebViewTracking with zero hosts. This interferes with WebView rendering by replacing the standard WebView with DdReactNativeWebView but without a properly configured tracking bridge.

Reproduction Tests

  • Unit tests: 1 failing (in WebviewDatadogNewArchBlankScreen.test.tsx)
  • Integration tests: 1 failing (in WebviewDatadogNewArchIntegration.test.ts)

What the Tests Prove

  1. Empty allowedHosts produces invalid tracking comment: When allowedHosts=[], the JS side still injects // #allowedHosts=[] as a comment, and the native side extracts an empty array. This causes WebViewTracking.enable() to be called with no hosts, which can interfere with WebView rendering without providing any actual tracking benefit.
  2. Round-trip encoding/extraction allows empty hosts: The native regex extraction successfully parses [] as an empty array and passes it to configureWebViewTracking, which is unnecessary and may contribute to blank screens when the Datadog native WebView component replaces the standard one.

Failure Output

FAIL WebviewDatadogNewArchBlankScreen.test.tsx
  expect(received).not.toContain(expected)
  Expected substring: not "#allowedHosts=[]"
  Received string: "// #allowedHosts=[]\n"

FAIL WebviewDatadogNewArchIntegration.test.ts
  expect(received).toBeNull()
  Received: []

Generated by rum:tee-triage-insights

@datadog-official

datadog-official Bot commented Mar 17, 2026

Copy link
Copy Markdown

⚠️ Tests

Fix all issues with BitsAI or with Cursor

⚠️ Warnings

🧪 2 Tests failed

RUMS-5458: WebView blank screen with New Architecture allowedHosts propagation in New Architecture should handle empty allowedHosts gracefully without causing blank screen from packages/react-native-webview/src/__tests__/WebviewDatadogNewArchBlankScreen.test.tsx (Datadog) (Fix with Cursor)
expect(received).not.toContain(expected) // indexOf

Expected substring: not "#allowedHosts=[]"
Received string:        "// #allowedHosts=[]
"
RUMS-5458: New Architecture allowedHosts encoding/extraction chain Edge cases that may cause blank screens should return null extraction when allowedHosts is empty array from packages/react-native-webview/src/__tests__/WebviewDatadogNewArchIntegration.test.ts (Datadog) (Fix with Cursor)
expect(received).toBeNull()

Received: []

ℹ️ Info

No other issues found (see more)

❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 5ed7760 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!


// Verify the JS is syntactically valid by attempting to parse it
// eslint-disable-next-line no-eval
expect(() => eval(`(function() { ${injectedJs} })`)).not.toThrow();

Check warning

Code scanning / CodeQL

Improper code sanitization Medium test

Code construction depends on an
improperly sanitized value
.

Copilot Autofix

AI 4 months ago

To fix the problem, we should sanitize the string produced by JSON.stringify(allowedHosts) before embedding it into JavaScript source. Specifically, we should apply an escaping function that replaces characters that could break out of JavaScript/HTML contexts (<, >, /, \, control characters, \u2028, \u2029) with their safe escape sequences (e.g., \u003C), following the pattern in the background section. Then we must use this sanitized value both when creating the comment (// #allowedHosts=...) and when returning the formatted hosts string from formatAllowedHosts, to keep behavior consistent and ensure the comment content and the parsed value match.

Concretely:

  • In packages/react-native-webview/src/utils/webview-js-utils.ts, define a small escapeUnsafeForJsEmbedding helper (similar to the example’s escapeUnsafeChars) that operates on a string and returns a safely escaped version.
  • In formatAllowedHosts, after JSON.stringify(allowedHosts) returns jsonHosts, pass it through escapeUnsafeForJsEmbedding and return the escaped string. This means formatAllowedHosts now returns the already-sanitized value to be embedded.
  • In wrapJsCodeWithAllowedHosts, instead of directly using JSON.stringify(allowedHosts) again, reuse hosts (the sanitized string from formatAllowedHosts) when building the comment line: // #allowedHosts=${hosts}\n. This avoids duplicate logic, keeps the comment consistent with what formatAllowedHosts validated, and ensures the comment payload is the escaped JSON.

No changes are needed in the test file’s logic; it will continue to call wrapJsCodeWithAllowedHosts and eval the result, but now the constructed code will be hardened against malformed or malicious allowedHosts content.

Suggested changeset 1
packages/react-native-webview/src/utils/webview-js-utils.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/react-native-webview/src/utils/webview-js-utils.ts b/packages/react-native-webview/src/utils/webview-js-utils.ts
--- a/packages/react-native-webview/src/utils/webview-js-utils.ts
+++ b/packages/react-native-webview/src/utils/webview-js-utils.ts
@@ -41,7 +41,8 @@
     let jsCode = '';
     const hosts = formatAllowedHosts(allowedHosts);
     if (hosts) {
-        jsCode = `// #allowedHosts=${JSON.stringify(allowedHosts)}\n`;
+        // hosts is already JSON-stringified and sanitized for safe embedding
+        jsCode = `// #allowedHosts=${hosts}\n`;
     }
 
     return javascriptCode
@@ -81,7 +82,8 @@
                 "JSON.stringify returned 'undefined' for the given hosts"
             );
         }
-        return jsonHosts;
+        // Sanitize the JSON string so it is safe to embed into JavaScript/HTML
+        return escapeUnsafeForJsEmbedding(jsonHosts);
     } catch (e: any) {
         if (NativeDdSdk) {
             NativeDdSdk.telemetryError(
@@ -97,6 +99,29 @@
     }
 }
 
+const unsafeCharMap: { [key: string]: string } = {
+    '<': '\\u003C',
+    '>': '\\u003E',
+    '/': '\\u002F',
+    '\\': '\\\\',
+    '\b': '\\b',
+    '\f': '\\f',
+    '\n': '\\n',
+    '\r': '\\r',
+    '\t': '\\t',
+    '\0': '\\0',
+    '\u2028': '\\u2028',
+    '\u2029': '\\u2029'
+};
+
+function escapeUnsafeForJsEmbedding(str: string): string {
+    // Replace characters that could break out of a JS/HTML context
+    return str.replace(
+        /[<>\/\\\b\f\n\r\t\0\u2028\u2029]/g,
+        (ch) => unsafeCharMap[ch] || ch
+    );
+}
+
 const getErrorMessage = (error: any | undefined): string => {
     const EMPTY_MESSAGE = 'Unknown Error';
     let message = EMPTY_MESSAGE;
EOF
@@ -41,7 +41,8 @@
let jsCode = '';
const hosts = formatAllowedHosts(allowedHosts);
if (hosts) {
jsCode = `// #allowedHosts=${JSON.stringify(allowedHosts)}\n`;
// hosts is already JSON-stringified and sanitized for safe embedding
jsCode = `// #allowedHosts=${hosts}\n`;
}

return javascriptCode
@@ -81,7 +82,8 @@
"JSON.stringify returned 'undefined' for the given hosts"
);
}
return jsonHosts;
// Sanitize the JSON string so it is safe to embed into JavaScript/HTML
return escapeUnsafeForJsEmbedding(jsonHosts);
} catch (e: any) {
if (NativeDdSdk) {
NativeDdSdk.telemetryError(
@@ -97,6 +99,29 @@
}
}

const unsafeCharMap: { [key: string]: string } = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\0',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};

function escapeUnsafeForJsEmbedding(str: string): string {
// Replace characters that could break out of a JS/HTML context
return str.replace(
/[<>\/\\\b\f\n\r\t\0\u2028\u2029]/g,
(ch) => unsafeCharMap[ch] || ch
);
}

const getErrorMessage = (error: any | undefined): string => {
const EMPTY_MESSAGE = 'Unknown Error';
let message = EMPTY_MESSAGE;
Copilot is powered by AI and may make mistakes. Always verify output.

expect(injectedJs).toBeDefined();
// eslint-disable-next-line no-eval
expect(() => eval(`(function() { ${injectedJs} })`)).not.toThrow();

Check warning

Code scanning / CodeQL

Improper code sanitization Medium test

Code construction depends on an
improperly sanitized value
.

Copilot Autofix

AI 4 months ago

To fix the problem, we should ensure that the string we derive from allowedHosts is additionally sanitized before being concatenated into JavaScript code. The recommended pattern is to JSON‑stringify the data (already done) and then escape characters that could prematurely terminate a <script> tag or otherwise alter the JavaScript parsing context (such as <, >, /, certain control characters, and Unicode line separators). This preserves existing functionality (the native side still sees parseable JSON) while preventing injected values from breaking out of the intended context.

Concretely, in packages/react-native-webview/src/utils/webview-js-utils.ts:

  • Add a small helper, e.g. escapeUnsafeCharsForJsComment, that replaces unsafe characters in a string with safe Unicode escape sequences, following the pattern described in the background (mapping <\u003C, etc.).
  • Use formatAllowedHosts(allowedHosts) (which already returns a JSON string or undefined) instead of re‑calling JSON.stringify(allowedHosts) in wrapJsCodeWithAllowedHosts.
  • Apply the escaping helper to the JSON string before embedding it in the comment, e.g.
    jsCode = // #allowedHosts=${escapeUnsafeCharsForJsComment(hosts)}\n;
  • Leave other behavior (try/catch wrapper, telemetry, etc.) unchanged.

The test file packages/react-native-webview/src/__tests__/WebviewDatadogNewArchIntegration.test.ts does not require logic changes; it will continue to work because the escaped JSON is still valid JSON and can be parsed by extractAllowedHostsFromInjectedJs. No additional imports are needed; the helper can be defined in the same module as wrapJsCodeWithAllowedHosts.

Suggested changeset 1
packages/react-native-webview/src/utils/webview-js-utils.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/react-native-webview/src/utils/webview-js-utils.ts b/packages/react-native-webview/src/utils/webview-js-utils.ts
--- a/packages/react-native-webview/src/utils/webview-js-utils.ts
+++ b/packages/react-native-webview/src/utils/webview-js-utils.ts
@@ -34,6 +34,28 @@
  * @param javascriptCode The JS Code to wrap in a try and catch block.
  * @returns the wrapped JS code.
  */
+const unsafeCharMap: { [key: string]: string } = {
+    '<': '\\u003C',
+    '>': '\\u003E',
+    '/': '\\u002F',
+    '\\': '\\\\',
+    '\b': '\\b',
+    '\f': '\\f',
+    '\n': '\\n',
+    '\r': '\\r',
+    '\t': '\\t',
+    '\0': '\\0',
+    '\u2028': '\\u2028',
+    '\u2029': '\\u2029'
+};
+
+function escapeUnsafeCharsForJsComment(str: string): string {
+    return str.replace(
+        /[<>/\\\b\f\n\r\t\0\u2028\u2029]/g,
+        (ch) => unsafeCharMap[ch] ?? ch
+    );
+}
+
 export function wrapJsCodeWithAllowedHosts(
     javascriptCode?: string,
     allowedHosts?: string[]
@@ -41,7 +63,8 @@
     let jsCode = '';
     const hosts = formatAllowedHosts(allowedHosts);
     if (hosts) {
-        jsCode = `// #allowedHosts=${JSON.stringify(allowedHosts)}\n`;
+        const safeHosts = escapeUnsafeCharsForJsComment(hosts);
+        jsCode = `// #allowedHosts=${safeHosts}\n`;
     }
 
     return javascriptCode
EOF
@@ -34,6 +34,28 @@
* @param javascriptCode The JS Code to wrap in a try and catch block.
* @returns the wrapped JS code.
*/
const unsafeCharMap: { [key: string]: string } = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\0',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};

function escapeUnsafeCharsForJsComment(str: string): string {
return str.replace(
/[<>/\\\b\f\n\r\t\0\u2028\u2029]/g,
(ch) => unsafeCharMap[ch] ?? ch
);
}

export function wrapJsCodeWithAllowedHosts(
javascriptCode?: string,
allowedHosts?: string[]
@@ -41,7 +63,8 @@
let jsCode = '';
const hosts = formatAllowedHosts(allowedHosts);
if (hosts) {
jsCode = `// #allowedHosts=${JSON.stringify(allowedHosts)}\n`;
const safeHosts = escapeUnsafeCharsForJsComment(hosts);
jsCode = `// #allowedHosts=${safeHosts}\n`;
}

return javascriptCode
Copilot is powered by AI and may make mistakes. Always verify output.
@mariusc83 mariusc83 closed this Mar 17, 2026
@mariusc83 mariusc83 deleted the mconstantin/RUMS-5458/white-blank-screen-webviews branch March 17, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants