Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/react-native-webview/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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']
}
}
}

Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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=<JSON>` 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)
Comment thread
sbarrio marked this conversation as resolved.
}

private fun configureWebViewTracking(webView: RNCWebView, allowedHosts: List<String>) {
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<String>? {
// 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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ViewManager<*,*>> {
return mutableListOf(
RNCWebViewManager()
DdSdkReactNativeWebViewManager(reactContext)
)
}

override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,16 @@ 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.
*/
@Volatile private var _datadogCore: SdkCore? = null
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
Expand Down Expand Up @@ -92,21 +84,15 @@ class DdSdkReactNativeWebViewManager(
sdkCore: SdkCore,
allowedHosts: List<String>
) {
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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
// =========
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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<WebViewTracking>

@BeforeEach
fun `set up`() {
whenever(themedReactContext.runOnUiQueueThread(any())).thenAnswer { answer ->
answer.getArgument<Runnable>(0).run()
true
}

webViewTrackingMockedStatic = Mockito.mockStatic(WebViewTracking::class.java)
webViewTrackingMockedStatic.`when`<Unit> {
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)
}
}
}
Loading
Loading