diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 973b3ec5f..a0c407316 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -134,7 +134,13 @@ android { } test { - java.srcDir("src/test/kotlin") + java.srcDir('src/test/kotlin') + + if (isNewArchitectureEnabled()) { + java.srcDirs += ['src/testNewArch/kotlin'] + } else { + java.srcDirs += ['src/testOldArch/kotlin'] + } } } @@ -208,6 +214,7 @@ dependencies { testImplementation "com.github.xgouchet.Elmyr:jvm:1.3.1" testImplementation "org.mockito.kotlin:mockito-kotlin:5.1.0" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + testImplementation 'org.json:json:20160810' detektPlugins "io.gitlab.arturbosch.detekt:detekt-formatting:1.23.8" detektPlugins "io.gitlab.arturbosch.detekt:detekt-rules-libraries:1.23.8" diff --git a/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt new file mode 100644 index 000000000..71c30c406 --- /dev/null +++ b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt @@ -0,0 +1,125 @@ +/* +* 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. +*/ + +package com.datadog.reactnative.webview + +import android.annotation.SuppressLint +import com.datadog.android.api.SdkCore +import com.datadog.android.webview.WebViewTracking +import com.datadog.reactnative.DatadogSDKWrapperStorage +import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.ThemedReactContext +import com.reactnativecommunity.webview.RNCWebView +import com.reactnativecommunity.webview.RNCWebViewClient +import com.reactnativecommunity.webview.RNCWebViewManager +import com.reactnativecommunity.webview.RNCWebViewWrapper +import org.json.JSONArray + +/** + * The entry point to use Datadog auto-instrumented WebView feature. + */ +class DdSdkReactNativeWebViewManager( + private val reactContext: ReactContext +) : RNCWebViewManager() { + // The name used to reference this custom View from React Native. + override fun getName(): String { + return VIEW_NAME + } + + /** + * The instance of Datadog SDK Core. + */ + @Volatile private var _datadogCore: SdkCore? = null + val datadogCore: SdkCore? + get() = _datadogCore + + init { + DatadogSDKWrapperStorage.addOnInitializedListener { core -> + _datadogCore = core + } + } + + /** + * Intercepts the WebView wrapper instance before it is returned and ensures that + * JavaScript is enabled on the underlying WebView. JavaScript must be enabled + * for Datadog WebView tracking to function correctly. + */ + @SuppressLint("SetJavaScriptEnabled") + override fun createViewInstance(context: ThemedReactContext): RNCWebViewWrapper { + val viewInstance = super.createViewInstance(context) + viewInstance.webView.settings.javaScriptEnabled = true + return viewInstance + } + + /** + * Intercepts the JavaScript injected before the WebView loads. + * + * In the New Architecture, WebView props from React Native are ignored, + * so this callback is the only reliable place to extract the + * `// #allowedHosts=` configuration and apply Datadog WebView tracking. + */ + override fun setInjectedJavaScriptBeforeContentLoaded( + view: RNCWebViewWrapper?, + value: String? + ) { + val allowedHosts = value?.let { extractAllowedHosts(it) } + val webView = view?.webView + + if (allowedHosts != null && webView != null) { + configureWebViewTracking(webView, allowedHosts) + } + + super.setInjectedJavaScriptBeforeContentLoaded(view, value) + } + + private fun configureWebViewTracking(webView: RNCWebView, allowedHosts: List) { + val datadogCore = _datadogCore + if (datadogCore != null) { + WebViewTracking.enable( + webView, + allowedHosts = allowedHosts, + sdkCore = datadogCore + ) + } else { + DatadogSDKWrapperStorage.addOnInitializedListener { core -> + reactContext.runOnUiQueueThread { + WebViewTracking.enable( + webView, + allowedHosts = allowedHosts, + sdkCore = core + ) + } + } + } + } + + override fun addEventEmitters( + reactContext: ThemedReactContext, + view: RNCWebViewWrapper + ) { + view.webView.webViewClient = RNCWebViewClient() + } + + companion object { + // The name used to reference this custom View from React Native. + const val VIEW_NAME = "DdReactNativeWebView" + + private fun extractAllowedHosts(input: String): List? { + // Regex that captures everything after "// #allowedHosts=" + val regex = Regex("""//\s*#allowedHosts\s*=\s*(.+)""") + + val match = regex.find(input) ?: return null + val jsonString = match.groupValues[1].trim() + + return try { + val jsonArray = JSONArray(jsonString) + (0 until jsonArray.length()).map { jsonArray.getString(it) } + } catch (e: Exception) { + null + } + } + } +} diff --git a/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt index ada4cc97c..be3b22ee2 100644 --- a/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt +++ b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt @@ -3,7 +3,6 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ - package com.datadog.reactnative.webview import com.facebook.react.TurboReactPackage @@ -12,17 +11,18 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager -import com.reactnativecommunity.webview.RNCWebViewManager +import com.reactnativecommunity.webview.RNCWebViewModule +import com.reactnativecommunity.webview.RNCWebViewModuleImpl class DdSdkReactNativeWebViewPackage : TurboReactPackage() { override fun createViewManagers( reactContext: ReactApplicationContext ): MutableList> { return mutableListOf( - RNCWebViewManager() + DdSdkReactNativeWebViewManager(reactContext) ) } - + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { return null } diff --git a/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt index 911ccb989..094e95b91 100644 --- a/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt +++ b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt @@ -26,10 +26,9 @@ class DdSdkReactNativeWebViewManager( private val reactContext: ReactContext ) : RNCWebViewManager() { // The name used to reference this custom View from React Native. - companion object { - const val VIEW_NAME = "DdReactNativeWebView" + override fun getName(): String { + return VIEW_NAME } - /** * The instance of Datadog SDK Core. */ @@ -37,13 +36,6 @@ class DdSdkReactNativeWebViewManager( val datadogCore: SdkCore? get() = _datadogCore - /** - * Whether WebView tracking has been enabled or not. - */ - @Volatile private var _isWebViewTrackingEnabled: Boolean = false - val isWebViewTrackingEnabled: Boolean - get() = _isWebViewTrackingEnabled - init { DatadogSDKWrapperStorage.addOnInitializedListener { core -> _datadogCore = core @@ -92,21 +84,15 @@ class DdSdkReactNativeWebViewManager( sdkCore: SdkCore, allowedHosts: List ) { - if (_isWebViewTrackingEnabled) { - return - } - WebViewTracking.enable( webView, allowedHosts = allowedHosts, sdkCore = sdkCore ) - - _isWebViewTrackingEnabled = true } - // The name used to reference this custom View from React Native. - override fun getName(): String { - return VIEW_NAME + companion object { + // The name used to reference this custom View from React Native. + const val VIEW_NAME = "DdReactNativeWebView" } } diff --git a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt b/packages/react-native-webview/android/src/test/kotlin/main/com/datadog/reactnative/tools/unit/GenericAssert.kt similarity index 92% rename from packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt rename to packages/react-native-webview/android/src/test/kotlin/main/com/datadog/reactnative/tools/unit/GenericAssert.kt index 8248b6224..4756fb86b 100644 --- a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt +++ b/packages/react-native-webview/android/src/test/kotlin/main/com/datadog/reactnative/tools/unit/GenericAssert.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.reactnative.tools.unit +package main.reactnative.tools.unit import org.assertj.core.api.AbstractAssert diff --git a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt b/packages/react-native-webview/android/src/test/webview/DatadogWebViewTest.kt similarity index 92% rename from packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt rename to packages/react-native-webview/android/src/test/webview/DatadogWebViewTest.kt index e9cf9ce03..8bd514379 100644 --- a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt +++ b/packages/react-native-webview/android/src/test/webview/DatadogWebViewTest.kt @@ -10,7 +10,8 @@ import com.datadog.android.api.SdkCore import com.datadog.android.core.InternalSdkCore import com.datadog.android.webview.WebViewTracking import com.datadog.reactnative.DatadogSDKWrapperStorage -import com.datadog.reactnative.tools.unit.GenericAssert.Companion.assertThat +import com.datadog.reactnative.webview.DdSdkReactNativeWebViewManager +import main.reactnative.tools.unit.GenericAssert.Companion.assertThat import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.uimanager.ThemedReactContext import com.reactnativecommunity.webview.RNCWebView @@ -90,9 +91,6 @@ internal class DatadogWebViewTest { // When first initialized, the WebView manager core should be null assertThat(manager.datadogCore).isNull() - // When first initialized, the WebView tracking should be disabled - assertThat(manager.isWebViewTrackingEnabled).isEqualTo(false) - // ========= // When // ========= @@ -119,8 +117,5 @@ internal class DatadogWebViewTest { webViewTrackingMockedStatic.verify { WebViewTracking.enable(rncWebView, listOf("example.com"), 100.0f, datadogCore) } - - // At this point 'isWebViewTrackingEnabled' should be true. - assertThat(manager.isWebViewTrackingEnabled).isEqualTo(true) } } diff --git a/packages/react-native-webview/android/src/testNewArch/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt b/packages/react-native-webview/android/src/testNewArch/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt new file mode 100644 index 000000000..581add1f7 --- /dev/null +++ b/packages/react-native-webview/android/src/testNewArch/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt @@ -0,0 +1,122 @@ +/* + * 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. + */ + +package com.datadog.reactnative.webview + +import com.datadog.android.api.SdkCore +import com.datadog.android.core.InternalSdkCore +import com.datadog.android.webview.WebViewTracking +import com.datadog.reactnative.DatadogSDKWrapperStorage +import main.reactnative.tools.unit.GenericAssert.Companion.assertThat +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.uimanager.ThemedReactContext +import com.reactnativecommunity.webview.RNCWebView +import com.reactnativecommunity.webview.RNCWebViewWrapper +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.MockedStatic +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +internal class DatadogWebViewTest { + + @Mock + lateinit var themedReactContext: ThemedReactContext + + @Mock + lateinit var datadogCore: InternalSdkCore + + private lateinit var webViewTrackingMockedStatic: MockedStatic + + @BeforeEach + fun `set up`() { + whenever(themedReactContext.runOnUiQueueThread(any())).thenAnswer { answer -> + answer.getArgument(0).run() + true + } + + webViewTrackingMockedStatic = Mockito.mockStatic(WebViewTracking::class.java) + webViewTrackingMockedStatic.`when` { + WebViewTracking.enable( + webView = any(), // Mock the WebView parameter + allowedHosts = any(), // Mock the list of allowed hosts + logsSampleRate = any(), // Mock the logsSampleRate parameter + sdkCore = any() // Mock the SdkCore parameter + ) + }.then {} // Return Unit as the function has no return value + } + + @AfterEach + fun `tear down`() { + webViewTrackingMockedStatic.close() + } + + @Test + fun `Datadog Core is set once initialized`() { + val manager = DdSdkReactNativeWebViewManager(themedReactContext) + assertThat(manager.datadogCore).isNull() + + DatadogSDKWrapperStorage.notifyOnInitializedListeners(datadogCore) + + assertThat(manager.datadogCore).isNotNull() + assertThat(manager.datadogCore).isInstanceOf(SdkCore::class.java) + } + + @Test + fun `Registers to SdkCore listener if the SDK is not initialized`() { + // ========= + // Given + // ========= + val manager = DdSdkReactNativeWebViewManager(themedReactContext) + + // When first initialized, the WebView manager core should be null + assertThat(manager.datadogCore).isNull() + + // ========= + // When + // ========= + val rncWebView = mock(RNCWebView::class.java) + val rncWebViewWrapper = mock(RNCWebViewWrapper::class.java) + whenever(rncWebViewWrapper.webView) doReturn rncWebView + + // When JS sends allowedHosts through 'injectedJavaScriptBeforeContentLoaded' prop + manager.setInjectedJavaScriptBeforeContentLoaded( + rncWebViewWrapper, + "// #allowedHosts=[\"example.com\",\"test.com\"]" + ) + + // ========= + // Then + // ========= + + // When we notify listeners that the core is available... + DatadogSDKWrapperStorage.notifyOnInitializedListeners(datadogCore) + + // ...the WebView should enable WebView tracking in the UI Thread. + verify(themedReactContext).runOnUiQueueThread(any()) + + // Native WebView tracking should be called + val arrayList = ArrayList(listOf("example.com", "test.com")) + webViewTrackingMockedStatic.verify { + WebViewTracking.enable(rncWebView, arrayList, 100.0f, datadogCore) + } + } +} diff --git a/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx b/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx index f3627a3c8..9feb45a22 100644 --- a/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx +++ b/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx @@ -3,13 +3,11 @@ * This product includes software developed at Datadog (https://www.datadoghq.com/). * Copyright 2016-Present Datadog, Inc. */ - import { render } from '@testing-library/react-native'; import { WebView as RNWebView } from 'react-native-webview'; import React from 'react'; import { WebView } from '../index'; -import { isNewArchitecture } from '../utils/env-utils'; import { dedent } from './__utils__/string-utils'; @@ -28,7 +26,7 @@ jest.mock('react-native-webview', () => { const callfunction = jest.fn(); const postMessageMock = jest.fn(); -window['ReactNativeWebView'] = { +(window as any)['ReactNativeWebView'] = { postMessage: postMessageMock }; @@ -129,19 +127,18 @@ describe('Webview', () => { const realInjectedJs = dedent( mockedWebView.mock.calls[0][0].injectedJavaScript ?? '' ); - const expected = dedent(` - try{ - testInjectedJavaScript() - } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - }`); + const expected = dedent(`try{ + testInjectedJavaScript() +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}`); expect(realInjectedJs).toBe(expected); }); @@ -167,51 +164,20 @@ describe('Webview', () => { mockedWebView.mock.calls[0][0] .injectedJavaScriptBeforeContentLoaded ?? '' ); - let expected; - - if (isNewArchitecture()) { - expected = dedent(` - window.DatadogEventBridge = { - send(msg) { - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'NATIVE_EVENT', - message: msg - })); - true; - }, - getAllowedWebViewHosts() { - return '["localhost","example.com"]' - } - }; - try{ - testInjectedJavaScript() - } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - } - `); - } else { - expected = dedent(` - try{ - testInjectedJavaScript() - } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - }`); - } + const expected = dedent(`// #allowedHosts=["localhost","example.com"] + +try{ + testInjectedJavaScript() +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}`); expect(realInjectedJs).toBe(expected); }); diff --git a/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts b/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts index b09ea8dbc..6091ebd53 100644 --- a/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts +++ b/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts @@ -4,7 +4,10 @@ * Copyright 2016-Present Datadog, Inc. */ -import { wrapJsCodeInTryAndCatch } from '../utils/webview-js-utils'; +import { + wrapJsCodeInTryAndCatch, + wrapJsCodeWithAllowedHosts +} from '../utils/webview-js-utils'; import { dedent } from './__utils__/string-utils'; @@ -13,13 +16,73 @@ describe('WebView JS Utils', () => { jest.clearAllMocks(); }); - describe('M wrapJsCodeInTryCatch wraps JS code in try & catch with DD messaging W jsCode is not null', () => { - it('M returns the JS code wrapped in try and catch', () => { + describe('M wrapJsCodeWithAllowedHosts wraps JS code in try & catch with DD messaging W jsCode is not null', () => { + it('M returns the allowedHosts JS comment W { jsCode = undefined & valid allowedHosts }', () => { // Given + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + const jsCode = undefined; + + // When + const wrappedCode = wrapJsCodeWithAllowedHosts(jsCode, [ + 'example.com', + 'test.com' + ]); + + // Then + const expected = '// #allowedHosts=["example.com","test.com"]\n'; + expect(wrappedCode).toBeDefined(); + expect(dedent(wrappedCode as string)).toBe(expected); + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); + + it('M returns the JS code wrapped in try and catch with allowedHosts comment W { custom JS code & valid allowedHosts }', () => { + // Given + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); const jsCode = "console.log('test')"; // When - const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + const wrappedCode = wrapJsCodeWithAllowedHosts(jsCode, [ + 'example.com', + 'test.com' + ]); + + // Then + const expected = dedent(`// #allowedHosts=["example.com","test.com"] + +try{ + console.log('test') +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}`); + expect(wrappedCode).toBeDefined(); + expect(dedent(wrappedCode as string)).toBe(expected); + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + }); + + it('M returns the JS code wrapped in try and catch W { custom JS code & allowedHosts = undefined }', () => { + // Given + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + const jsCode = "console.log('test')"; + + // When + const wrappedCode = wrapJsCodeWithAllowedHosts(jsCode, undefined); // Then const expected = dedent(` @@ -37,15 +100,97 @@ describe('WebView JS Utils', () => { }`); expect(wrappedCode).toBeDefined(); expect(dedent(wrappedCode as string)).toBe(expected); + expect(warnSpy).toHaveBeenCalledWith( + "[@datadog/mobile-react-native-webview] Invalid 'allowedHosts' format: Error: allowedHosts is undefined" + ); + + warnSpy.mockRestore(); }); - it('M returns undefined W { jsCode = undefined }', () => { + it('M returns the JS code wrapped in try and catch W { custom JS code & invalid JSON allowedHosts }', () => { + // Given + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + const jsCode = "console.log('test')"; + + // When + const wrappedCode = wrapJsCodeWithAllowedHosts( + jsCode, + (() => 'incompatible') as any + ); + + // Then + const expected = dedent(` + try{ + console.log('test') + } + catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; + }`); + expect(wrappedCode).toBeDefined(); + expect(dedent(wrappedCode as string)).toBe(expected); + expect(warnSpy).toHaveBeenCalledWith( + "[@datadog/mobile-react-native-webview] Invalid 'allowedHosts' format: Error: JSON.stringify returned 'undefined' for the given hosts" + ); + + warnSpy.mockRestore(); + }); + + it('M returns undefined W { jsCode = undefined & allowedHosts = undefined }', () => { // Given const jsCode = undefined; + const allowedHosts = undefined; // When - const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + const wrappedCode = wrapJsCodeWithAllowedHosts( + jsCode, + allowedHosts + ); // Then expect(wrappedCode).toBe(undefined); }); }); + + describe('M wrapJsCodeInTryAndCatch wraps the given JS code in a try & catch block with DD messaging', () => { + it('M returns the JS code wrapped in try and catch W { custom JS code is defined }', () => { + // Given + const jsCode = "console.log('test')"; + + // When + const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + + // Then + const expected = dedent(`try{ + console.log('test') +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}`); + expect(wrappedCode).toBeDefined(); + expect(dedent(wrappedCode as string)).toBe(expected); + }); + + it('M returns undefined W { custom JS code is undefined }', () => { + // Given + const jsCode = undefined; + + // When + const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + + // Then + expect(wrappedCode).toBeUndefined(); + }); + }); }); diff --git a/packages/react-native-webview/src/index.tsx b/packages/react-native-webview/src/index.tsx index 3812e952c..775dd9ab9 100644 --- a/packages/react-native-webview/src/index.tsx +++ b/packages/react-native-webview/src/index.tsx @@ -5,15 +5,14 @@ */ import type { WebViewMessageEvent, WebViewProps } from 'react-native-webview'; import { WebView as RNWebView } from 'react-native-webview'; -import React, { forwardRef, useCallback } from 'react'; +import React, { forwardRef, useCallback, useMemo } from 'react'; import NativeDdLogs from './ext-specs/NativeDdLogs'; import { NativeDdSdk } from './ext-specs/NativeDdSdk'; import { NativeDdWebView } from './specs/NativeDdWebView'; -import { isNewArchitecture } from './utils/env-utils'; import { - getWebViewEventBridgingJS, - wrapJsCodeInTryAndCatch + wrapJsCodeInTryAndCatch, + wrapJsCodeWithAllowedHosts } from './utils/webview-js-utils'; import type { DatadogMessageFormat } from './utils/webview-js-utils'; @@ -71,18 +70,16 @@ const WebViewComponent = (props: Props, ref: React.Ref>) => { [userDefinedOnMessage, props.logUserCodeErrors] ); - const getInjectedJavascriptBeforeContentLoaded = (): string | undefined => { - if (isNewArchitecture()) { - return getWebViewEventBridgingJS( - props.allowedHosts, - props.injectedJavaScriptBeforeContentLoaded - ); - } else { - return wrapJsCodeInTryAndCatch( - props.injectedJavaScriptBeforeContentLoaded - ); - } - }; + const injectedJavascript = useMemo(() => { + return wrapJsCodeInTryAndCatch(props.injectedJavaScript); + }, [props.injectedJavaScript]); + + const injectedJavascriptBeforeContentLoaded = useMemo(() => { + return wrapJsCodeWithAllowedHosts( + props.injectedJavaScriptBeforeContentLoaded, + props.allowedHosts + ); + }, [props.injectedJavaScriptBeforeContentLoaded, props.allowedHosts]); return ( >) => { allowedHosts: props.allowedHosts } }} - injectedJavaScript={wrapJsCodeInTryAndCatch( - props.injectedJavaScript - )} - injectedJavaScriptBeforeContentLoaded={getInjectedJavascriptBeforeContentLoaded()} + injectedJavaScript={injectedJavascript} + injectedJavaScriptBeforeContentLoaded={ + injectedJavascriptBeforeContentLoaded + } ref={ref} /> ); diff --git a/packages/react-native-webview/src/specs/NativeDdWebView.ts b/packages/react-native-webview/src/specs/NativeDdWebView.ts index 351b26a16..a3c3ed28a 100644 --- a/packages/react-native-webview/src/specs/NativeDdWebView.ts +++ b/packages/react-native-webview/src/specs/NativeDdWebView.ts @@ -7,10 +7,8 @@ import type { CommonNativeWebViewProps } from 'react-native-webview/lib/WebViewTypes'; import { requireNativeComponent } from 'react-native'; -import { isNewArchitecture } from '../utils/env-utils'; - -const NativeDdWebView = !isNewArchitecture() - ? requireNativeComponent('DdReactNativeWebView') - : undefined; +const NativeDdWebView = requireNativeComponent( + 'DdReactNativeWebView' +); export { NativeDdWebView }; diff --git a/packages/react-native-webview/src/utils/env-utils.ts b/packages/react-native-webview/src/utils/env-utils.ts deleted file mode 100644 index d30542bc6..000000000 --- a/packages/react-native-webview/src/utils/env-utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * 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. - */ - -export const isNewArchitecture = (): boolean => { - return (global as any)?.nativeFabricUIManager !== undefined; -}; diff --git a/packages/react-native-webview/src/utils/webview-js-utils.ts b/packages/react-native-webview/src/utils/webview-js-utils.ts index 101ea1873..c6c9be3b4 100644 --- a/packages/react-native-webview/src/utils/webview-js-utils.ts +++ b/packages/react-native-webview/src/utils/webview-js-utils.ts @@ -29,70 +29,59 @@ export type DatadogMessageFormat = { }; /** - * Wraps the given JS Code in a try and catch block. + * Wraps the given JS Code in a try and catch block, including a + * comment with the given allowedHosts in JSON format * @param javascriptCode The JS Code to wrap in a try and catch block. * @returns the wrapped JS code. */ -export const wrapJsCodeInTryAndCatch = ( - javascriptCode?: string -): string | undefined => - javascriptCode - ? ` - try{ - ${javascriptCode} +export function wrapJsCodeWithAllowedHosts( + javascriptCode?: string, + allowedHosts?: string[] +): string | undefined { + let jsCode = ''; + const hosts = formatAllowedHosts(allowedHosts); + if (hosts) { + jsCode = `// #allowedHosts=${JSON.stringify(allowedHosts)}\n`; } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - }` + + return javascriptCode + ? `${jsCode}\n${wrapJsCodeInTryAndCatch(javascriptCode)}` + : jsCode.trim().length > 0 + ? jsCode : undefined; +} -/** - * Legacy JS code for bridging the WebView events to DataDog native SDKs for consumption. - * @param allowedHosts The list of allowed hosts. - * @param customJavaScriptCode Custom user JS code to inject along with the Datadog bridging logic. - * @returns The JS code block as a string. - */ -export const getWebViewEventBridgingJS = ( - allowedHosts?: string[], - customJavaScriptCode?: string -): string => - ` - window.DatadogEventBridge = { - send(msg) { - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'NATIVE_EVENT', - message: msg - })); - true; - }, - getAllowedWebViewHosts() { - return ${formatAllowedHosts(allowedHosts)} - } - }; - try{ - ${customJavaScriptCode} - } - catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - window.ReactNativeWebView.postMessage(JSON.stringify({ - source: 'DATADOG', - type: 'ERROR', - message: errorMsg - })); - true; - } - `; +export function wrapJsCodeInTryAndCatch( + javascriptCode: string | undefined +): string | undefined { + return javascriptCode + ? `try{ + ${javascriptCode} +} +catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + window.ReactNativeWebView.postMessage(JSON.stringify({ + source: 'DATADOG', + type: 'ERROR', + message: errorMsg + })); + true; +}` + : undefined; +} -function formatAllowedHosts(allowedHosts?: string[]): string { +function formatAllowedHosts(allowedHosts?: string[]): string | undefined { try { - return `'${JSON.stringify(allowedHosts)}'`; + if (!allowedHosts) { + throw new Error('allowedHosts is undefined'); + } + const jsonHosts = JSON.stringify(allowedHosts); + if (!jsonHosts) { + throw new Error( + "JSON.stringify returned 'undefined' for the given hosts" + ); + } + return jsonHosts; } catch (e: any) { if (NativeDdSdk) { NativeDdSdk.telemetryError( @@ -101,7 +90,10 @@ function formatAllowedHosts(allowedHosts?: string[]): string { 'AllowedHostsError' ); } - return "'[]'"; + console.warn( + `[@datadog/mobile-react-native-webview] Invalid 'allowedHosts' format: ${e}` + ); + return undefined; } }