diff --git a/example/android/build.gradle b/example/android/build.gradle
index 4ebd7d5d4..1e8154a10 100644
--- a/example/android/build.gradle
+++ b/example/android/build.gradle
@@ -1,7 +1,7 @@
buildscript {
ext {
buildToolsVersion = "34.0.0"
- minSdkVersion = 23
+ minSdkVersion = 21
compileSdkVersion = 34
targetSdkVersion = 34
ndkVersion = "25.1.8937393"
diff --git a/example/ios/DdSdkReactNativeExample.xcodeproj/xcshareddata/xcschemes/DdSdkReactNativeExample.xcscheme b/example/ios/DdSdkReactNativeExample.xcodeproj/xcshareddata/xcschemes/DdSdkReactNativeExample.xcscheme
index 57eb4ed27..bd67eb659 100644
--- a/example/ios/DdSdkReactNativeExample.xcodeproj/xcshareddata/xcschemes/DdSdkReactNativeExample.xcscheme
+++ b/example/ios/DdSdkReactNativeExample.xcodeproj/xcshareddata/xcschemes/DdSdkReactNativeExample.xcscheme
@@ -48,6 +48,16 @@
ReferencedContainer = "container:Pods/Pods.xcodeproj">
+
+
+
+
'../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests']
pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests']
+ pod 'DatadogSDKReactNativeWebView', :path => '../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec', :testspecs => ['Tests']
config = use_native_modules!
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 60dbf6b79..2e4877e13 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -35,6 +35,18 @@ PODS:
- DatadogSessionReplay (~> 2.18.0)
- React-Core
- React-RCTText
+ - DatadogSDKReactNativeWebView (2.4.4):
+ - DatadogInternal (~> 2.18.0)
+ - DatadogSDKReactNative
+ - DatadogWebViewTracking (~> 2.18.0)
+ - React-Core
+ - DatadogSDKReactNativeWebView/Tests (2.4.4):
+ - DatadogInternal (~> 2.18.0)
+ - DatadogSDKReactNative
+ - DatadogWebViewTracking (~> 2.18.0)
+ - React-Core
+ - react-native-webview
+ - React-RCTText
- DatadogSessionReplay (2.18.0):
- DatadogInternal (= 2.18.0)
- DatadogTrace (2.18.0):
@@ -984,6 +996,27 @@ PODS:
- React-Core
- react-native-safe-area-context (4.10.8):
- React-Core
+ - react-native-webview (13.12.2):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Codegen
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
- React-nativeconfig (0.74.3)
- React-NativeModulesApple (0.74.3):
- glog
@@ -1262,6 +1295,8 @@ DEPENDENCIES:
- DatadogSDKReactNative/Tests (from `../../packages/core/DatadogSDKReactNative.podspec`)
- DatadogSDKReactNativeSessionReplay (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`)
- DatadogSDKReactNativeSessionReplay/Tests (from `../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec`)
+ - DatadogSDKReactNativeWebView (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`)
+ - DatadogSDKReactNativeWebView/Tests (from `../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
@@ -1295,6 +1330,7 @@ DEPENDENCIES:
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- react-native-crash-tester (from `../node_modules/react-native-crash-tester`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
+ - react-native-webview (from `../node_modules/react-native-webview`)
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -1347,6 +1383,8 @@ EXTERNAL SOURCES:
:path: "../../packages/core/DatadogSDKReactNative.podspec"
DatadogSDKReactNativeSessionReplay:
:path: "../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec"
+ DatadogSDKReactNativeWebView:
+ :path: "../../packages/react-native-webview/DatadogSDKReactNativeWebView.podspec"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
FBLazyVector:
@@ -1410,6 +1448,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-crash-tester"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
+ react-native-webview:
+ :path: "../node_modules/react-native-webview"
React-nativeconfig:
:path: "../node_modules/react-native/ReactCommon"
React-NativeModulesApple:
@@ -1478,6 +1518,7 @@ SPEC CHECKSUMS:
DatadogRUM: 8461273acc87ddfe4a8bbc6e6672ae9542471f86
DatadogSDKReactNative: 1bdbebae33aa0b9dd4e9054a6a9bb09d8ca875c8
DatadogSDKReactNativeSessionReplay: b44da5bca3a58700b2add61e0ddc554c71da6fba
+ DatadogSDKReactNativeWebView: c63eda46a8060a126ed309d52a64c44c03b93e2e
DatadogSessionReplay: c08f0df752e916639b04251eb3bf06dcda80591a
DatadogTrace: cc8ae210c274aa40658da83c9cc15de85bde405a
DatadogWebViewTracking: eeb8ea3fa13983752d2cc4dc0994664bf8388e15
@@ -1515,6 +1556,7 @@ SPEC CHECKSUMS:
React-Mapbuffer: 9f68550e7c6839d01411ac8896aea5c868eff63a
react-native-crash-tester: 8b270c754febeab8a5761a8f50bc89ed26985f10
react-native-safe-area-context: b7daa1a8df36095a032dff095a1ea8963cb48371
+ react-native-webview: 8d746f921964c87b87b190bf6a46fa148d40cd0f
React-nativeconfig: fa5de9d8f4dbd5917358f8ad3ad1e08762f01dcb
React-NativeModulesApple: 585d1b78e0597de364d259cb56007052d0bda5e5
React-perflogger: 7bb9ba49435ff66b666e7966ee10082508a203e8
@@ -1546,6 +1588,6 @@ SPEC CHECKSUMS:
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: 04f1db30bb810187397fa4c37dd1868a27af229c
-PODFILE CHECKSUM: 82a277ceb4b375dcd70cc36bab9e782fc3365650
+PODFILE CHECKSUM: 54f3e178906b491e7b20bd99da6e8788777ec12e
COCOAPODS: 1.15.2
diff --git a/example/package.json b/example/package.json
index 8618e32ac..21979ced9 100644
--- a/example/package.json
+++ b/example/package.json
@@ -25,7 +25,8 @@
"react-native-gesture-handler": "^1.10.1",
"react-native-navigation": "7.40.1",
"react-native-safe-area-context": "4.10.8",
- "react-native-screens": "3.29.0"
+ "react-native-screens": "3.29.0",
+ "react-native-webview": "^13.12.2"
},
"devDependencies": {
"@babel/core": "^7.20.0",
diff --git a/package.json b/package.json
index 36a622058..c13fbd446 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
"react-native": "0.73.9",
"react-native-builder-bob": "0.26.0",
"react-native-gradle-plugin": "^0.71.19",
- "react-native-webview": "11.26.1",
+ "react-native-webview": "13.12.2",
"react-test-renderer": "18.1.0",
"typescript": "5.0.4"
},
diff --git a/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec
new file mode 100644
index 000000000..f18b92c55
--- /dev/null
+++ b/packages/react-native-webview/DatadogSDKReactNativeWebView.podspec
@@ -0,0 +1,46 @@
+require "json"
+
+package = JSON.parse(File.read(File.join(__dir__, "package.json")))
+
+Pod::Spec.new do |s|
+ s.name = "DatadogSDKReactNativeWebView"
+ s.version = package["version"]
+ s.summary = package["description"]
+ s.homepage = package["homepage"]
+ s.license = package["license"]
+ s.authors = package["author"]
+
+ s.platforms = { :ios => "12.0", :tvos => "12.0" }
+ s.source = { :git => "https://github.com/DataDog/dd-sdk-reactnative.git", :tag => "#{s.version}" }
+
+
+ s.source_files = "ios/Sources/*.{h,m,mm,swift}"
+
+ s.dependency "React-Core"
+
+ # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec
+ s.dependency 'DatadogWebViewTracking', '~> 2.18.0'
+ s.dependency 'DatadogInternal', '~> 2.18.0'
+ s.dependency 'DatadogSDKReactNative'
+
+ s.test_spec 'Tests' do |test_spec|
+ test_spec.dependency "react-native-webview"
+ test_spec.dependency "React-RCTText"
+
+ test_spec.source_files = 'ios/Tests/*.swift'
+ test_spec.platforms = { :ios => "13.4", :tvos => "13.4" }
+ end
+
+
+ # This guard prevents installing the dependencies when we run `pod install` in the old architecture.
+ # The `install_modules_dependencies` function is only available from RN 0.71, the new architecture is not
+ # supported on earlier RN versions.
+ if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
+ s.pod_target_xcconfig = {
+ "DEFINES_MODULE" => "YES",
+ "OTHER_CPLUSPLUSFLAGS" => "-DRCT_NEW_ARCH_ENABLED=1"
+ }
+
+ install_modules_dependencies(s)
+ end
+end
diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle
new file mode 100644
index 000000000..6bf908d43
--- /dev/null
+++ b/packages/react-native-webview/android/build.gradle
@@ -0,0 +1,249 @@
+import java.nio.file.Paths
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+buildscript {
+ // Buildscript is evaluated before everything else so we can't use getExtOrDefault
+ def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['DatadogSDKReactNativeWebView_kotlinVersion']
+
+ repositories {
+ mavenCentral()
+ google()
+ gradlePluginPortal()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.2.2'
+ // noinspection DifferentKotlinGradleVersion
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath "org.jlleitschuh.gradle:ktlint-gradle:11.5.1"
+ classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.18.0"
+ classpath 'com.github.bjoernq:unmockplugin:0.7.9'
+ }
+}
+
+
+apply plugin: 'de.mobilej.unmock'
+
+def isNewArchitectureEnabled() {
+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
+}
+
+apply plugin: 'com.android.library'
+if (isNewArchitectureEnabled()) {
+ apply plugin: 'com.facebook.react'
+}
+apply plugin: 'kotlin-android'
+apply plugin: 'org.jlleitschuh.gradle.ktlint'
+apply plugin: "io.gitlab.arturbosch.detekt"
+
+/**
+ * Finds the path of the installed npm package with the given name using Node's
+ * module resolution algorithm, which searches "node_modules" directories up to
+ * the file system root.
+ * This enables us to handle monorepos without requiring the node executable.
+ *
+ * The search begins at the given base directory (a File object). The returned
+ * path is a string.
+ */
+static def findNodeModulePath(baseDir, packageName) {
+ def basePath = baseDir.toPath().normalize()
+ // Node's module resolution algorithm searches up to the root directory,
+ // after which the base path will be null
+ while (basePath) {
+ def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
+ if (candidatePath.toFile().exists()) {
+ return candidatePath.toString()
+ }
+ basePath = basePath.getParent()
+ }
+ return null
+}
+
+def resolveReactNativeDirectory() {
+ def reactNativeLocation = rootProject.hasProperty("reactNativeDir") ? rootProject.getProperty("reactNativeDir") : null
+
+ if (reactNativeLocation != null) {
+ return file(reactNativeLocation)
+ }
+
+ def reactNativePath = findNodeModulePath(projectDir, "react-native")
+
+ if (reactNativePath != null) {
+ return file(reactNativePath)
+ }
+
+ throw new Exception(
+ "${project.name}: Failed to resolve 'react-native' in the project. " +
+ "Altenatively, you can specify 'reactNativeDir' with the path to 'react-native' in your 'gradle.properties' file."
+ )
+}
+
+def reactNativeRootDir = resolveReactNativeDirectory()
+def reactProperties = new Properties()
+file("$reactNativeRootDir/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) }
+
+def reactNativeVersion = reactProperties.getProperty("VERSION_NAME")
+def (reactNativeMajorVersion, reactNativeMinorVersion) = reactNativeVersion.split("\\.").collect { it.isInteger() ? it.toInteger() : it }
+
+def getExtOrDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['DatadogSDKReactNativeWebView_' + name]
+}
+
+def getExtOrIntegerDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['DatadogSDKReactNativeWebView_' + name]).toInteger()
+}
+
+android {
+ compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
+ buildToolsVersion getExtOrDefault('buildToolsVersion')
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
+ if (agpVersion.tokenize('.')[0].toInteger() >= 7) {
+ namespace = "com.datadog.reactnative.webview"
+ }
+ if (agpVersion.tokenize('.')[0].toInteger() >= 8) {
+ buildFeatures {
+ buildConfig = true
+ }
+ }
+ if (agpVersion.tokenize('.')[0].toInteger() < 8) {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
+ }
+ }
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
+ versionCode 1
+ versionName "1.0"
+ buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString())
+ }
+
+ sourceSets {
+ main {
+ if (isNewArchitectureEnabled()) {
+ java.srcDirs += ['src/newarch']
+ } else {
+ java.srcDirs += ['src/oldarch']
+ }
+ }
+
+ test {
+ java.srcDir("src/test/kotlin")
+ }
+ }
+
+ testOptions {
+ unitTests {
+ returnDefaultValues = true
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+ lintOptions {
+ disable 'GradleCompatible'
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+}
+
+repositories {
+ mavenCentral()
+ google()
+ maven { url "https://jitpack.io" }
+ mavenLocal()
+
+ if (reactNativeMajorVersion == 0 && reactNativeMinorVersion < 71) {
+ def androidSourcesDir = file("$reactNativeRootDir/android")
+ def androidSourcesName = "React Native sources"
+
+ if (androidSourcesDir.exists()) {
+ maven {
+ url androidSourcesDir.toString()
+ name androidSourcesName
+ }
+ }
+ }
+}
+
+def kotlin_version = getExtOrDefault('kotlinVersion')
+
+dependencies {
+ if (reactNativeMajorVersion == 0 && reactNativeMinorVersion < 71) {
+ // noinspection GradleDynamicVersion
+ api 'com.facebook.react:react-native:+'
+ } else {
+ // We specify the $reactNativeVersion, like it's done on the react-native-gradle-plugin, as the plugin is not applied in tests.
+ // There is no impact for apps as we apply the same logic:
+ // https://github.com/facebook/react-native/blob/e1a1e6aa8030bf11d691c3dcf7abd13b25175027/packages/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/DependencyUtils.kt
+ implementation "com.facebook.react:react-android:$reactNativeVersion"
+ }
+
+ implementation "com.datadoghq:dd-sdk-android-webview:2.14.0"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
+ implementation project(path: ':datadog_mobile-react-native')
+ implementation project(path: ':react-native-webview')
+
+ testImplementation "org.junit.platform:junit-platform-launcher:1.6.2"
+ testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2"
+ testImplementation "org.junit.jupiter:junit-jupiter-engine:5.6.2"
+ testImplementation "org.junit.jupiter:junit-jupiter-params:5.6.2"
+ testImplementation "org.mockito:mockito-junit-jupiter:3.4.6"
+ testImplementation "org.assertj:assertj-core:3.18.1"
+ testImplementation "com.github.xgouchet.Elmyr:core:1.3.1"
+ testImplementation "com.github.xgouchet.Elmyr:inject:1.3.1"
+ testImplementation "com.github.xgouchet.Elmyr:junit5:1.3.1"
+ 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"
+ unmock 'org.robolectric:android-all:4.4_r1-robolectric-r2'
+}
+
+tasks.withType(Test) {
+ useJUnitPlatform {
+ includeEngines("spek", "junit-jupiter", "junit-vintage")
+ }
+ reports {
+ junitXml.required.set(true)
+ html.required.set(true)
+ }
+}
+
+tasks.named("check") {
+ dependsOn("ktlintCheck")
+ dependsOn("detekt")
+}
+
+ktlint {
+ debug.set(false)
+ android.set(true)
+ outputToConsole.set(true)
+ ignoreFailures.set(false)
+ enableExperimentalRules.set(false)
+ filter {
+ exclude("**/generated/**")
+ include("**/kotlin/**")
+ }
+}
+
+detekt {
+ input = files("$projectDir/src/main/kotlin")
+ config = files("$projectDir/detekt.yml")
+ reports {
+ xml {
+ enabled = true
+ destination = file("build/reports/detekt.xml")
+ }
+ }
+}
diff --git a/packages/react-native-webview/android/detekt.yml b/packages/react-native-webview/android/detekt.yml
new file mode 100644
index 000000000..c1376f3bf
--- /dev/null
+++ b/packages/react-native-webview/android/detekt.yml
@@ -0,0 +1,572 @@
+build:
+ maxIssues: 0
+ weights:
+ # complexity: 2
+ # LongParameterList: 1
+ # style: 1
+ # comments: 1
+
+processors:
+ active: true
+ exclude:
+ # - 'DetektProgressListener'
+ # - 'FunctionCountProcessor'
+ # - 'PropertyCountProcessor'
+ # - 'ClassCountProcessor'
+ # - 'PackageCountProcessor'
+ # - 'KtFileCountProcessor'
+
+console-reports:
+ active: true
+ exclude:
+ # - 'ProjectStatisticsReport'
+ # - 'ComplexityReport'
+ # - 'NotificationReport'
+ # - 'FindingsReport'
+ - 'FileBasedFindingsReport'
+ # - 'BuildFailureReport'
+
+comments:
+ active: true
+ CommentOverPrivateFunction:
+ active: true
+ CommentOverPrivateProperty:
+ active: true
+ EndOfSentenceFormat:
+ active: true
+ endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!:]$)
+ UndocumentedPublicClass:
+ active: true
+ excludes: [ "**/test/**","**/androidTest/**","**/*.Test.kt","**/*.Spec.kt","**/*.Spek.kt" ]
+ searchInNestedClass: true
+ searchInInnerClass: true
+ searchInInnerObject: true
+ searchInInnerInterface: true
+ UndocumentedPublicFunction:
+ active: true
+ excludes: [ "**/test/**","**/androidTest/**","**/*.Test.kt","**/*.Spec.kt","**/*.Spek.kt" ]
+ UndocumentedPublicProperty:
+ active: true
+ excludes: [ "**/test/**","**/androidTest/**","**/*.Test.kt","**/*.Spec.kt","**/*.Spek.kt" ]
+
+complexity:
+ active: true
+ ComplexCondition:
+ active: true
+ threshold: 4
+ ComplexInterface:
+ active: true
+ threshold: 10
+ includeStaticDeclarations: false
+ ComplexMethod:
+ active: true
+ threshold: 10
+ ignoreSingleWhenExpression: true
+ ignoreSimpleWhenEntries: true
+ LabeledExpression:
+ active: true
+ ignoredLabels: ""
+ LargeClass:
+ active: true
+ threshold: 600
+ LongMethod:
+ active: true
+ threshold: 60
+ LongParameterList:
+ active: true
+ threshold: 6
+ ignoreDefaultParameters: true
+ MethodOverloading:
+ active: true
+ threshold: 6
+ NestedBlockDepth:
+ active: true
+ threshold: 4
+ StringLiteralDuplication:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ threshold: 3
+ ignoreAnnotation: true
+ excludeStringsWithLessThan5Characters: true
+ ignoreStringsRegex: '$^'
+ TooManyFunctions:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ thresholdInFiles: 11
+ thresholdInClasses: 11
+ thresholdInInterfaces: 11
+ thresholdInObjects: 11
+ thresholdInEnums: 11
+ ignoreDeprecated: true
+ ignorePrivate: true
+ ignoreOverridden: true
+
+empty-blocks:
+ active: true
+ EmptyCatchBlock:
+ active: true
+ allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
+ EmptyClassBlock:
+ active: true
+ EmptyDefaultConstructor:
+ active: true
+ EmptyDoWhileBlock:
+ active: true
+ EmptyElseBlock:
+ active: true
+ EmptyFinallyBlock:
+ active: true
+ EmptyForBlock:
+ active: true
+ EmptyFunctionBlock:
+ active: true
+ ignoreOverridden: true
+ EmptyIfBlock:
+ active: true
+ EmptyInitBlock:
+ active: true
+ EmptyKtFile:
+ active: true
+ EmptySecondaryConstructor:
+ active: true
+ EmptyWhenBlock:
+ active: true
+ EmptyWhileBlock:
+ active: true
+
+exceptions:
+ active: true
+ ExceptionRaisedInUnexpectedLocation:
+ active: true
+ methodNames: 'toString,hashCode,equals,finalize'
+ InstanceOfCheckForException:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ NotImplementedDeclaration:
+ active: true
+ PrintStackTrace:
+ active: true
+ RethrowCaughtException:
+ active: true
+ ReturnFromFinally:
+ active: true
+ ignoreLabeled: true
+ SwallowedException:
+ active: true
+ ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException'
+ allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
+ ThrowingExceptionFromFinally:
+ active: true
+ ThrowingExceptionInMain:
+ active: true
+ ThrowingExceptionsWithoutMessageOrCause:
+ active: true
+ exceptions: 'IllegalArgumentException,IllegalStateException,IOException'
+ ThrowingNewInstanceOfSameException:
+ active: true
+ TooGenericExceptionCaught:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ exceptionNames:
+ - ArrayIndexOutOfBoundsException
+ - Error
+ - Exception
+ - IllegalMonitorStateException
+ - NullPointerException
+ - IndexOutOfBoundsException
+ - RuntimeException
+ - Throwable
+ allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
+ TooGenericExceptionThrown:
+ active: true
+ exceptionNames:
+ - Error
+ - Exception
+ - Throwable
+ - RuntimeException
+
+formatting:
+ active: false
+ android: false
+ autoCorrect: true
+ AnnotationOnSeparateLine:
+ active: true
+ autoCorrect: true
+ ChainWrapping:
+ active: true
+ autoCorrect: true
+ CommentSpacing:
+ active: true
+ autoCorrect: true
+ Filename:
+ active: true
+ FinalNewline:
+ active: true
+ autoCorrect: true
+ ImportOrdering:
+ active: true
+ autoCorrect: true
+ Indentation:
+ active: true
+ autoCorrect: true
+ indentSize: 4
+ continuationIndentSize: 4
+ MaximumLineLength:
+ active: true
+ maxLineLength: 120
+ ModifierOrdering:
+ active: true
+ autoCorrect: true
+ MultiLineIfElse:
+ active: true
+ autoCorrect: true
+ NoBlankLineBeforeRbrace:
+ active: true
+ autoCorrect: true
+ NoConsecutiveBlankLines:
+ active: true
+ autoCorrect: true
+ NoEmptyClassBody:
+ active: true
+ autoCorrect: true
+ NoLineBreakAfterElse:
+ active: true
+ autoCorrect: true
+ NoLineBreakBeforeAssignment:
+ active: true
+ autoCorrect: true
+ NoMultipleSpaces:
+ active: true
+ autoCorrect: true
+ NoSemicolons:
+ active: true
+ autoCorrect: true
+ NoTrailingSpaces:
+ active: true
+ autoCorrect: true
+ NoUnitReturn:
+ active: true
+ autoCorrect: true
+ NoUnusedImports:
+ active: true
+ autoCorrect: true
+ NoWildcardImports:
+ active: true
+ autoCorrect: true
+ PackageName:
+ active: true
+ autoCorrect: true
+ ParameterListWrapping:
+ active: true
+ autoCorrect: true
+ indentSize: 4
+ SpacingAroundColon:
+ active: true
+ autoCorrect: true
+ SpacingAroundComma:
+ active: true
+ autoCorrect: true
+ SpacingAroundCurly:
+ active: true
+ autoCorrect: true
+ SpacingAroundDot:
+ active: true
+ autoCorrect: true
+ SpacingAroundKeyword:
+ active: true
+ autoCorrect: true
+ SpacingAroundOperators:
+ active: true
+ autoCorrect: true
+ SpacingAroundParens:
+ active: true
+ autoCorrect: true
+ SpacingAroundRangeOperator:
+ active: true
+ autoCorrect: true
+ StringTemplate:
+ active: true
+ autoCorrect: true
+
+naming:
+ active: true
+ ClassNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ classPattern: '[A-Z$][a-zA-Z0-9$]*'
+ ConstructorParameterNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ privateParameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ EnumNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*'
+ ForbiddenClassName:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ forbiddenName: ''
+ FunctionMaxLength:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ maximumFunctionNameLength: 30
+ FunctionMinLength:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ minimumFunctionNameLength: 3
+ FunctionNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
+ excludeClassPattern: '$^'
+ ignoreOverridden: true
+ FunctionParameterNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ ignoreOverridden: true
+ InvalidPackageDeclaration:
+ active: true
+ rootPackage: ''
+ MatchingDeclarationName:
+ active: true
+ MemberNameEqualsClassName:
+ active: true
+ ignoreOverridden: true
+ ObjectPropertyNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ constantPattern: '[A-Za-z][_A-Za-z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
+ PackageNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$'
+ TopLevelPropertyNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ constantPattern: '[A-Z][_A-Z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
+ VariableMaxLength:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ maximumVariableNameLength: 64
+ VariableMinLength:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ minimumVariableNameLength: 1
+ VariableNaming:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ variablePattern: '[a-z][A-Za-z0-9]*'
+ privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ ignoreOverridden: true
+
+performance:
+ active: true
+ ArrayPrimitive:
+ active: true
+ ForEachOnRange:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ SpreadOperator:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ UnnecessaryTemporaryInstantiation:
+ active: true
+
+potential-bugs:
+ active: true
+ Deprecation:
+ active: true
+ DuplicateCaseInWhenExpression:
+ active: true
+ EqualsAlwaysReturnsTrueOrFalse:
+ active: true
+ EqualsWithHashCodeExist:
+ active: true
+ ExplicitGarbageCollectionCall:
+ active: true
+ HasPlatformType:
+ active: true
+ InvalidRange:
+ active: true
+ IteratorHasNextCallsNextMethod:
+ active: true
+ IteratorNotThrowingNoSuchElementException:
+ active: true
+ LateinitUsage:
+ active: false
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ excludeAnnotatedProperties: ""
+ ignoreOnClassesPattern: ""
+ MissingWhenCase:
+ active: true
+ RedundantElseInWhen:
+ active: true
+ UnconditionalJumpStatementInLoop:
+ active: true
+ UnreachableCode:
+ active: true
+ UnsafeCallOnNullableType:
+ active: true
+ UnsafeCast:
+ active: true
+ UselessPostfixExpression:
+ active: true
+ WrongEqualsTypeParameter:
+ active: true
+
+style:
+ active: false
+ CollapsibleIfStatements:
+ active: true
+ DataClassContainsFunctions:
+ active: true
+ conversionFunctionPrefix: 'to'
+ DataClassShouldBeImmutable:
+ active: true
+ EqualsNullCall:
+ active: true
+ EqualsOnSignatureLine:
+ active: true
+ ExplicitItLambdaParameter:
+ active: true
+ ExpressionBodySyntax:
+ active: false
+ includeLineWrapping: false
+ ForbiddenComment:
+ active: true
+ values: 'TODO:,FIXME:,STOPSHIP:'
+ allowedPatterns: ""
+ ForbiddenImport:
+ active: true
+ imports: ''
+ forbiddenPatterns: ""
+ ForbiddenVoid:
+ active: true
+ ignoreOverridden: false
+ ignoreUsageInGenerics: true
+ FunctionOnlyReturningConstant:
+ active: true
+ ignoreOverridableFunction: true
+ excludedFunctions: 'describeContents'
+ excludeAnnotatedFunction: "dagger.Provides"
+ LibraryCodeMustSpecifyReturnType:
+ active: true
+ LoopWithTooManyJumpStatements:
+ active: true
+ maxJumpCount: 1
+ MagicNumber:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ ignoreNumbers: '-1,0,1,2'
+ ignoreHashCodeFunction: true
+ ignorePropertyDeclaration: false
+ ignoreConstantDeclaration: true
+ ignoreCompanionObjectPropertyDeclaration: true
+ ignoreAnnotation: false
+ ignoreNamedArgument: true
+ ignoreEnums: false
+ ignoreRanges: false
+ MandatoryBracesIfStatements:
+ active: true
+ MaxLineLength:
+ active: true
+ maxLineLength: 120
+ excludePackageStatements: true
+ excludeImportStatements: true
+ excludeCommentStatements: false
+ MayBeConst:
+ active: true
+ ModifierOrder:
+ active: true
+ NestedClassesVisibility:
+ active: true
+ NewLineAtEndOfFile:
+ active: false
+ NoTabs:
+ active: true
+ OptionalAbstractKeyword:
+ active: true
+ OptionalUnit:
+ active: true
+ OptionalWhenBraces:
+ active: true
+ PreferToOverPairSyntax:
+ active: true
+ ProtectedMemberInFinalClass:
+ active: true
+ RedundantExplicitType:
+ active: true
+ RedundantVisibilityModifierRule:
+ active: true
+ ReturnCount:
+ active: true
+ max: 2
+ excludedFunctions: "equals"
+ excludeLabeled: false
+ excludeReturnFromLambda: true
+ excludeGuardClauses: true
+ SafeCast:
+ active: true
+ SerialVersionUIDInSerializableClass:
+ active: true
+ SpacingBetweenPackageAndImports:
+ active: true
+ ThrowsCount:
+ active: true
+ max: 2
+ TrailingWhitespace:
+ active: true
+ UnderscoresInNumericLiterals:
+ active: true
+ acceptableDecimalLength: 5
+ UnnecessaryAbstractClass:
+ active: true
+ excludeAnnotatedClasses: "dagger.Module"
+ UnnecessaryApply:
+ active: true
+ UnnecessaryInheritance:
+ active: true
+ UnnecessaryLet:
+ active: true
+ UnnecessaryParentheses:
+ active: true
+ UntilInsteadOfRangeTo:
+ active: true
+ UnusedImports:
+ active: true
+ UnusedPrivateClass:
+ active: true
+ UnusedPrivateMember:
+ active: true
+ allowedNames: "(_|ignored|expected|serialVersionUID)"
+ UseArrayLiteralsInAnnotations:
+ active: true
+ UseCheckOrError:
+ active: true
+ UseDataClass:
+ active: true
+ excludeAnnotatedClasses: ""
+ allowVars: true
+ UseIfInsteadOfWhen:
+ active: true
+ UseRequire:
+ active: true
+ UselessCallOnNotNull:
+ active: true
+ UtilityClassWithPublicConstructor:
+ active: true
+ VarCouldBeVal:
+ active: true
+ WildcardImport:
+ active: true
+ excludes: "**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"
+ excludeImports: 'java.util.*,kotlinx.android.synthetic.*'
diff --git a/packages/react-native-webview/android/gradle.properties b/packages/react-native-webview/android/gradle.properties
new file mode 100644
index 000000000..1f8a7c826
--- /dev/null
+++ b/packages/react-native-webview/android/gradle.properties
@@ -0,0 +1,5 @@
+DatadogSDKReactNativeWebView_kotlinVersion=1.7.21
+DatadogSDKReactNativeWebView_compileSdkVersion=33
+DatadogSDKReactNativeWebView_buildToolsVersion=33.0.0
+DatadogSDKReactNativeWebView_targetSdkVersion=33
+android.useAndroidX=true
diff --git a/packages/react-native-webview/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-webview/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..62d4c0535
Binary files /dev/null and b/packages/react-native-webview/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/packages/react-native-webview/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-webview/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..8cb39cca3
--- /dev/null
+++ b/packages/react-native-webview/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Nov 20 08:39:09 CET 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
diff --git a/packages/react-native-webview/android/gradlew b/packages/react-native-webview/android/gradlew
new file mode 100755
index 000000000..fbd7c5158
--- /dev/null
+++ b/packages/react-native-webview/android/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/packages/react-native-webview/android/gradlew.bat b/packages/react-native-webview/android/gradlew.bat
new file mode 100644
index 000000000..5093609d5
--- /dev/null
+++ b/packages/react-native-webview/android/gradlew.bat
@@ -0,0 +1,104 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/react-native-webview/android/settings.gradle b/packages/react-native-webview/android/settings.gradle
new file mode 100644
index 000000000..f23344bcd
--- /dev/null
+++ b/packages/react-native-webview/android/settings.gradle
@@ -0,0 +1,7 @@
+// This is only used for building locally and testing the package.
+// If the package is used in an app that contains ":datadog_mobile-react-native" it will be resolved correctly.
+include ':datadog_mobile-react-native'
+project(':datadog_mobile-react-native').projectDir = new File('../../core/android')
+
+include ':react-native-webview'
+project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-webview/android')
diff --git a/packages/react-native-webview/android/src/main/AndroidManifest.xml b/packages/react-native-webview/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..7dbcc8d23
--- /dev/null
+++ b/packages/react-native-webview/android/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
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
new file mode 100644
index 000000000..ada4cc97c
--- /dev/null
+++ b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt
@@ -0,0 +1,35 @@
+/*
+* 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.facebook.react.TurboReactPackage
+import com.facebook.react.bridge.NativeModule
+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
+
+class DdSdkReactNativeWebViewPackage : TurboReactPackage() {
+ override fun createViewManagers(
+ reactContext: ReactApplicationContext
+ ): MutableList> {
+ return mutableListOf(
+ RNCWebViewManager()
+ )
+ }
+
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
+ return null
+ }
+
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
+ return ReactModuleInfoProvider {
+ mapOf()
+ }
+ }
+}
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
new file mode 100644
index 000000000..911ccb989
--- /dev/null
+++ b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt
@@ -0,0 +1,112 @@
+/*
+* 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.bridge.ReadableArray
+import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.annotations.ReactProp
+import com.reactnativecommunity.webview.RNCWebView
+import com.reactnativecommunity.webview.RNCWebViewClient
+import com.reactnativecommunity.webview.RNCWebViewManager
+import com.reactnativecommunity.webview.RNCWebViewWrapper
+
+/**
+ * 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.
+ companion object {
+ const val VIEW_NAME = "DdReactNativeWebView"
+ }
+
+ /**
+ * 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
+ }
+ }
+
+ // The Custom WebView exposed properties.
+ @ReactProp(name = "allowedHosts")
+ fun setAllowedHosts(view: RNCWebViewWrapper, allowedHosts: ReadableArray) {
+ // TODO: RUM-7218 (Log failures w Telemetry)
+ val webView = view.webView as? RNCWebView ?: return
+ val datadogCore = _datadogCore
+ val hosts = toStringList(allowedHosts)
+ if (datadogCore != null) {
+ this.enableWebViewTracking(webView, datadogCore, hosts)
+ } else {
+ DatadogSDKWrapperStorage.addOnInitializedListener { core ->
+ reactContext.runOnUiQueueThread {
+ this.enableWebViewTracking(webView, core, hosts)
+ }
+ }
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun createViewInstance(context: ThemedReactContext): RNCWebViewWrapper {
+ val webView = RNCWebView(context)
+ webView.settings.javaScriptEnabled = true
+ return super.createViewInstance(context, webView)
+ }
+
+ override fun addEventEmitters(
+ reactContext: ThemedReactContext,
+ view: RNCWebViewWrapper
+ ) {
+ view.webView.webViewClient = RNCWebViewClient()
+ }
+
+ private fun toStringList(props: ReadableArray): List {
+ return props.toArrayList().filterIsInstance()
+ }
+
+ // Utility function to enable WebView tracking
+ private fun enableWebViewTracking(
+ webView: RNCWebView,
+ 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
+ }
+}
diff --git a/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt
new file mode 100644
index 000000000..26e0d7831
--- /dev/null
+++ b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt
@@ -0,0 +1,36 @@
+/*
+* 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.facebook.react.TurboReactPackage
+import com.facebook.react.bridge.NativeModule
+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
+
+class DdSdkReactNativeWebViewPackage : TurboReactPackage() {
+ override fun createViewManagers(
+ reactContext: ReactApplicationContext
+ ): MutableList> {
+ return mutableListOf(
+ DdSdkReactNativeWebViewManager(
+ reactContext
+ )
+ )
+ }
+
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
+ return null
+ }
+
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
+ return ReactModuleInfoProvider {
+ mapOf()
+ }
+ }
+}
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/com/datadog/reactnative/tools/unit/GenericAssert.kt
new file mode 100644
index 000000000..8248b6224
--- /dev/null
+++ b/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt
@@ -0,0 +1,18 @@
+/*
+ * 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.tools.unit
+
+import org.assertj.core.api.AbstractAssert
+
+class GenericAssert(actual: Any?) :
+ AbstractAssert(actual, GenericAssert::class.java) {
+ companion object {
+ fun assertThat(actual: Any?): GenericAssert {
+ return GenericAssert(actual)
+ }
+ }
+}
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/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt
new file mode 100644
index 000000000..e9cf9ce03
--- /dev/null
+++ b/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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 com.datadog.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 first initialized, the WebView tracking should be disabled
+ assertThat(manager.isWebViewTrackingEnabled).isEqualTo(false)
+
+ // =========
+ // When
+ // =========
+ val rncWebView = mock(RNCWebView::class.java)
+ val rncWebViewWrapper = mock(RNCWebViewWrapper::class.java)
+ whenever(rncWebViewWrapper.webView) doReturn rncWebView
+
+ // When tracking is enabled with a null core...
+ val allowedHosts = JavaOnlyArray()
+ allowedHosts.pushString("example.com")
+ manager.setAllowedHosts(rncWebViewWrapper, allowedHosts)
+
+ // =========
+ // 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
+ 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/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/react-native-webview/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 000000000..1f0955d45
--- /dev/null
+++ b/packages/react-native-webview/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.pbxproj b/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..d385370e6
--- /dev/null
+++ b/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.pbxproj
@@ -0,0 +1,272 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 58B511D91A9E6C8500147676 /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "include/$(PRODUCT_NAME)";
+ dstSubfolderSpec = 16;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 134814201AA4EA6300B7C361 /* libDatadogSDKReactNativeWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libDatadogSDKReactNativeWebView.a; sourceTree = BUILT_PRODUCTS_DIR; };
+ F625545A26A82D430033052D /* Sources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Sources; sourceTree = ""; };
+ F625545B26A82D430033052D /* Tests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Tests; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 58B511D81A9E6C8500147676 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 134814211AA4EA7D00B7C361 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 134814201AA4EA6300B7C361 /* libDatadogSDKReactNativeWebView.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 58B511D21A9E6C8500147676 = {
+ isa = PBXGroup;
+ children = (
+ F625545A26A82D430033052D /* Sources */,
+ F625545B26A82D430033052D /* Tests */,
+ 134814211AA4EA7D00B7C361 /* Products */,
+ );
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 58B511DA1A9E6C8500147676 /* DatadogSDKReactNativeWebView */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "DatadogSDKReactNativeWebView" */;
+ buildPhases = (
+ 58B511D71A9E6C8500147676 /* Sources */,
+ 58B511D81A9E6C8500147676 /* Frameworks */,
+ 58B511D91A9E6C8500147676 /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = DatadogSDKReactNativeWebView;
+ productName = RCTDataManager;
+ productReference = 134814201AA4EA6300B7C361 /* libDatadogSDKReactNativeWebView.a */;
+ productType = "com.apple.product-type.library.static";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 58B511D31A9E6C8500147676 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0920;
+ ORGANIZATIONNAME = Facebook;
+ TargetAttributes = {
+ 58B511DA1A9E6C8500147676 = {
+ CreatedOnToolsVersion = 6.1.1;
+ };
+ };
+ };
+ buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "DatadogSDKReactNativeWebView" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ English,
+ en,
+ );
+ mainGroup = 58B511D21A9E6C8500147676;
+ productRefGroup = 58B511D21A9E6C8500147676;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 58B511DA1A9E6C8500147676 /* DatadogSDKReactNativeWebView */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 58B511D71A9E6C8500147676 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 58B511ED1A9E6C8500147676 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ 58B511EE1A9E6C8500147676 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 58B511F01A9E6C8500147676 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../../../React/**",
+ "$(SRCROOT)/../../react-native/React/**",
+ );
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = DatadogSDKReactNativeWebView;
+ SKIP_INSTALL = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "DatadogSDKReactNativeWebView-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 58B511F11A9E6C8500147676 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
+ "$(SRCROOT)/../../../React/**",
+ "$(SRCROOT)/../../react-native/React/**",
+ );
+ LIBRARY_SEARCH_PATHS = "$(inherited)";
+ OTHER_LDFLAGS = "-ObjC";
+ PRODUCT_NAME = DatadogSDKReactNativeWebView;
+ SKIP_INSTALL = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "DatadogSDKReactNativeWebView-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "DatadogSDKReactNativeWebView" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 58B511ED1A9E6C8500147676 /* Debug */,
+ 58B511EE1A9E6C8500147676 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "DatadogSDKReactNativeWebView" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 58B511F01A9E6C8500147676 /* Debug */,
+ 58B511F11A9E6C8500147676 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 58B511D31A9E6C8500147676 /* Project object */;
+}
diff --git a/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..94b2795e2
--- /dev/null
+++ b/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,4 @@
+
+
+
diff --git a/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000..18d981003
--- /dev/null
+++ b/packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/packages/react-native-webview/ios/Sources/DatadogSDKReactNativeWebView.h b/packages/react-native-webview/ios/Sources/DatadogSDKReactNativeWebView.h
new file mode 100644
index 000000000..5bc58b537
--- /dev/null
+++ b/packages/react-native-webview/ios/Sources/DatadogSDKReactNativeWebView.h
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+// This file is imported in the auto-generated DatadogSDKReactNative-Swift.h header file.
+// Deleting it could result in iOS builds failing.
+
+#import "RCTDatadogWebView.h"
+#import "RCTDatadogWebViewManager.h"
+#import
diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebView.h b/packages/react-native-webview/ios/Sources/RCTDatadogWebView.h
new file mode 100644
index 000000000..c1883ccc5
--- /dev/null
+++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebView.h
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+#import
+
+@class RCTDatadogWebView;
+
+@protocol RCTDatadogWebViewDelegate
+- (void)didCreateWebView:(RCTDatadogWebView *)webView;
+@end
+
+@interface RCTDatadogWebView : RNCWebViewImpl
+
+@property (nonatomic, weak) id ddWebViewDelegate;
+@property (nonatomic, assign) BOOL isTrackingEnabled;
+
+- (WKWebView*) getWKWebView;
+@end
diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebView.mm b/packages/react-native-webview/ios/Sources/RCTDatadogWebView.mm
new file mode 100644
index 000000000..622fb5848
--- /dev/null
+++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebView.mm
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+#import "RCTDatadogWebView.h"
+#import
+
+@interface RCTDatadogWebView ()
+@end
+
+@implementation RCTDatadogWebView { }
+
+- (instancetype)init
+{
+ self = [super init];
+ if (self) {
+ _isTrackingEnabled = false;
+ }
+ return self;
+}
+
+- (WKWebView *)getWKWebView {
+ return [self findWKWebViewInView: self];
+}
+
+- (WKWebView *)findWKWebViewInView:(UIView *)view {
+ // Check if the current view is a WKWebView
+ if ([view isKindOfClass:[WKWebView class]]) {
+ return (WKWebView *)view;
+ }
+
+ // Iterate through the subviews recursively
+ for (UIView *subview in view.subviews) {
+ WKWebView *webView = [self findWKWebViewInView:subview];
+ if (webView) {
+ return webView; // Return the first WKWebView found
+ }
+ }
+
+ return nil;
+}
+
+- (void)didMoveToWindow {
+ [super didMoveToWindow];
+
+ if (self.ddWebViewDelegate != nil && [self.ddWebViewDelegate respondsToSelector:@selector(didCreateWebView:)]) {
+ [self.ddWebViewDelegate didCreateWebView:self];
+ }
+}
+
+@end
diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.h b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.h
new file mode 100644
index 000000000..dec77979b
--- /dev/null
+++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.h
@@ -0,0 +1,10 @@
+/*
+ * 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.
+ */
+
+#import
+
+@interface RCTDatadogWebViewManager : RNCWebViewManager
+@end
diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.mm b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.mm
new file mode 100644
index 000000000..b32305a9b
--- /dev/null
+++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.mm
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+#import
+#import
+#import "RCTDatadogWebViewManager.h"
+#import "RCTDatadogWebView.h"
+#import "DatadogSDKReactNativeWebView-Swift.h"
+
+@interface RCTDatadogWebViewManager ()
+ @property (nonatomic, strong) NSMutableSet *allowedHosts;
+@property (nonatomic, strong) RCTDatadogWebViewTracking* webViewTracking;
+@end
+
+@implementation RCTDatadogWebViewManager { }
+
+// The module is exported to React Native with the name defined here.
+RCT_EXPORT_MODULE(DdReactNativeWebView)
+
+// Allowed Hosts (REQUIRED)
+RCT_CUSTOM_VIEW_PROPERTY(allowedHosts, NSArray, RCTDatadogWebView)
+{
+ NSArray* allowedHosts = [RCTConvert NSArray:json];
+ [self setupDatadogWebView:allowedHosts view:view];
+}
+
+// MARK: - Initialization
+- (instancetype)init
+{
+ self = [super init];
+ if (self) {
+ self.allowedHosts = [[NSMutableSet alloc] init];
+ self.webViewTracking = [[RCTDatadogWebViewTracking alloc] init];
+ }
+ return self;
+}
+
+// MARK: - View Manager
+- (UIView *)view
+{
+ RCTDatadogWebView *rctWebView = [RCTDatadogWebView new];
+ rctWebView.delegate = self;
+ rctWebView.ddWebViewDelegate = self;
+ rctWebView.javaScriptEnabled = true;
+ return rctWebView;
+}
+
+// MARK: - Datadog Setup
+
+/**
+ * Setups the Datadog WebView by setting the allowed hosts and enabling tracking.
+ *
+ * @param allowedHosts The list of allowed hosts
+ * @param view The RCTDatadogWebView as returned by the ViewManager
+ */
+- (void)setupDatadogWebView:(NSArray *)allowedHosts view:(RCTDatadogWebView*)view {
+ [self.allowedHosts removeAllObjects];
+ for (NSObject* obj in allowedHosts) {
+ if (![obj isKindOfClass:[NSString class]]) {
+ continue;
+ }
+ [self.allowedHosts addObject:obj];
+ }
+
+ [self.webViewTracking enableWithWebView:view allowedHosts:self.allowedHosts];
+}
+
+// MARK: - RCTDatadogWebViewDelegate
+- (void)didCreateWebView:(RCTDatadogWebView *)webView {
+ if (self.allowedHosts.count == 0) {
+ return;
+ }
+ [self.webViewTracking enableWithWebView:webView allowedHosts:self.allowedHosts];
+}
+
+// MARK: - WKWebViewDelegate
+- (BOOL)webView:(nonnull RNCWebViewImpl *)webView shouldStartLoadForRequest:(nonnull NSMutableDictionary *)request withCallback:(nonnull RCTDirectEventBlock)callback {
+ return true;
+}
+
+@end
diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift
new file mode 100644
index 000000000..97fd835b5
--- /dev/null
+++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+import WebKit
+import DatadogWebViewTracking
+import DatadogSDKReactNative
+import DatadogCore
+
+@objc public class RCTDatadogWebViewTracking: NSObject {
+ var webView: RCTDatadogWebView? = nil
+ var allowedHosts: Set = Set()
+ var coreListener: OnCoreInitializedListener?
+
+ public override init() {
+ super.init()
+ self.coreListener = { [weak self] (core: DatadogCoreProtocol) in
+ guard let strongSelf = self, let webView = strongSelf.webView else {
+ return
+ }
+ strongSelf.enableWebViewTracking(
+ webView: webView,
+ allowedHosts: strongSelf.allowedHosts,
+ core: core
+ )
+ }
+ }
+
+ /**
+ Enables tracking on the given WebView.
+
+ - Parameter webView: The WebView to enable tracking on.
+ - Parameter allowedHosts: The allowed hosts.
+ - Note: If the SDK core is not available immediately, this method will register a listener and
+ enable tracking only when the core will be initialized.
+ */
+ @objc public func enable(webView: RCTDatadogWebView, allowedHosts: Set) {
+ self.webView = webView
+ self.allowedHosts = allowedHosts
+
+ guard !webView.isTrackingEnabled else { return }
+
+ if let core = DatadogSDKWrapper.shared.getCoreInstance() {
+ enableWebViewTracking(webView: webView, allowedHosts: allowedHosts, core: core)
+ } else if let coreListener = self.coreListener {
+ DatadogSDKWrapper.shared.addOnCoreInitializedListener(listener: coreListener)
+ } else {
+ // TODO: Report initialization problem
+ }
+ }
+
+ private func enableWebViewTracking(
+ webView: RCTDatadogWebView,
+ allowedHosts: Set,
+ core: DatadogCoreProtocol
+ ) {
+ guard let wkWebView = webView.getWKWebView() else {
+ return
+ }
+ DispatchQueue.main.async {
+ WebViewTracking.enable(webView: wkWebView, hosts: allowedHosts, in: core)
+ self.webView?.isTrackingEnabled = true;
+ }
+ }
+}
diff --git a/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift b/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift
new file mode 100644
index 000000000..8ab987fb6
--- /dev/null
+++ b/packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift
@@ -0,0 +1,275 @@
+/*
+ * 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 2019-2020 Datadog, Inc.
+ */
+
+import XCTest
+@testable import DatadogSDKReactNativeWebView
+@testable import DatadogSDKReactNative
+@testable import DatadogWebViewTracking
+import DatadogInternal
+import React
+import DatadogLogs
+import DatadogCore
+
+internal class DatadogSDKReactNativeWebViewTests: XCTestCase {
+ override func setUp() {
+ super.setUp()
+ let mockDatadogCore = MockDatadogCore()
+ DatadogSDKWrapper.shared.setCoreInstance(core: mockDatadogCore)
+ }
+
+ func testDatadogWebViewManagerReturnsDatadogWebView() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+ // When
+ let view = viewManager.view()
+ // Then
+ XCTAssertTrue(view is RCTDatadogWebView, "ViewManager returned view is of type RCTDatadogWebView")
+ }
+
+ func testDatadogWebViewTrackingIsDisabledOnInit() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+ // When
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail()
+ return
+ }
+ // Then
+ XCTAssertFalse(view.isTrackingEnabled)
+ }
+
+ func testDatadogWebViewTrackingIsDisabledIfCoreIsNotReady() {
+ // Given
+ DatadogSDKWrapper.shared.setCoreInstance(core: nil)
+ let viewManager = RCTDatadogWebViewManager()
+ let allowedHosts = NSArray(objects: "example1.com", "example2.com")
+
+ // When
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail()
+ return
+ }
+ // Given
+ let selector = NSSelectorFromString("setupDatadogWebView:view:")
+ XCTAssertTrue(viewManager.responds(to: selector))
+
+ // When
+ viewManager.perform(selector, with: allowedHosts, with: view)
+
+ // Then
+ XCTAssertEqual(allowedHosts.count, 2)
+ XCTAssertTrue(allowedHosts.contains("example1.com"))
+ XCTAssertTrue(allowedHosts.contains("example2.com"))
+
+
+ // Then
+ XCTAssertFalse(view.isTrackingEnabled)
+ }
+
+ func testDatadogWebViewTrackingIsEnabledLateWhenCoreIsNotReady() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+ let allowedHosts = NSArray(objects: "example1.com", "example2.com")
+
+ // When
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail()
+ return
+ }
+
+ view.addSubview(WKWebView())
+
+ DatadogSDKWrapper.shared.setCoreInstance(core: nil)
+
+ // Given
+ let selector = NSSelectorFromString("setupDatadogWebView:view:")
+ XCTAssertTrue(viewManager.responds(to: selector))
+ viewManager.perform(selector, with: allowedHosts, with: view)
+
+ XCTAssertFalse(view.isTrackingEnabled)
+
+ // When
+ DatadogSDKWrapper.shared.setCoreInstance(core: MockDatadogCore())
+ DatadogSDKWrapper.shared.callInitialize()
+
+ let expectation = self.expectation(description: "WebView tracking is enabled through the listener.")
+ DispatchQueue.main.async {
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 6)
+ XCTAssertTrue(view.isTrackingEnabled)
+ }
+
+ func testDatadogWebViewTrackingIsEnabledWhenCoreIsReady() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+ let allowedHosts = NSArray(objects: "example1.com", "example2.com")
+
+ // When
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail()
+ return
+ }
+
+ view.addSubview(WKWebView())
+
+ XCTAssertFalse(view.isTrackingEnabled)
+
+ // Given
+ let selector = NSSelectorFromString("setupDatadogWebView:view:")
+ XCTAssertTrue(viewManager.responds(to: selector))
+ viewManager.perform(selector, with: allowedHosts, with: view)
+
+ let expectation = self.expectation(description: "WebView tracking is enabled in the main thread")
+ DispatchQueue.main.async {
+ expectation.fulfill()
+ }
+
+ // Then
+ wait(for: [expectation], timeout: 6)
+ // When
+ XCTAssertTrue(view.isTrackingEnabled)
+ }
+
+ func testDatadogWebViewJavascriptEnabled() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+ // When
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail()
+ return
+ }
+ // Then
+ XCTAssertTrue(view.javaScriptEnabled)
+ }
+
+ func testDatadogWebViewAllowedHostsAreEmptyOnInit() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+ // Then
+ guard let allowedHosts = viewManager.value(forKey: "allowedHosts") as? NSMutableSet else {
+ XCTFail("DatadogWebViewManager must have 'allowedHosts' property.")
+ return
+ }
+ XCTAssertEqual(allowedHosts.count, 0)
+ }
+
+ func testDatadogWebViewDelegatesAreSetOnInit() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+ // When
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail()
+ return
+ }
+ // Then
+ XCTAssert(view.ddWebViewDelegate.isEqual(viewManager))
+ XCTAssertNotNil(view.delegate)
+ XCTAssert(view.delegate!.isEqual(viewManager))
+ }
+
+ func testDatadogWebViewAllowedHostsAreSet() {
+ // Given
+ let allowedHosts = NSArray(objects: "example1.com", "example2.com")
+ let viewManager = RCTDatadogWebViewManager()
+
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail("ViewManager view is not of type RCTDatadogWebView")
+ return
+ }
+
+ let selector = NSSelectorFromString("setupDatadogWebView:view:")
+ XCTAssertTrue(viewManager.responds(to: selector))
+
+ // When
+ viewManager.perform(selector, with: allowedHosts, with: view)
+
+ // Then
+ XCTAssertEqual(allowedHosts.count, 2)
+ XCTAssertTrue(allowedHosts.contains("example1.com"))
+ XCTAssertTrue(allowedHosts.contains("example2.com"))
+ }
+
+ func testDatadogWebViewDelegateIsCalledWhenViewMovedToWindow() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail("ViewManager view is not of type RCTDatadogWebView")
+ return
+ }
+
+ let delegate = MockDatadogWebViewDelegate()
+ view.ddWebViewDelegate = delegate
+
+ XCTAssertFalse(delegate.wasCalled)
+
+ // When
+ view.didMoveToWindow()
+
+ // Then
+ XCTAssertTrue(delegate.wasCalled)
+ }
+
+ func testDatadogWebViewCanFindNestedWKWebView() {
+ // Given
+ let viewManager = RCTDatadogWebViewManager()
+
+ guard let view = viewManager.view() as? RCTDatadogWebView else {
+ XCTFail("ViewManager view is not of type RCTDatadogWebView")
+ return
+ }
+
+ let container = UIView()
+ container.addSubview(WKWebView())
+ view.addSubview(container)
+
+ // When
+ let selector = NSSelectorFromString("findWKWebViewInView:")
+ XCTAssertTrue(view.responds(to: selector))
+ let wkWebView = view.perform(selector, with: view)
+
+ // Then
+ XCTAssertNotNil(wkWebView)
+ XCTAssertTrue(wkWebView?.takeUnretainedValue() is WKWebView)
+ }
+}
+
+extension DatadogSDKWrapper {
+ func callInitialize() {
+ self.initialize(
+ coreConfiguration: Datadog.Configuration(clientToken: "mock-client-token", env: "mock-env"),
+ loggerConfiguration: DatadogLogs.Logger.Configuration(),
+ trackingConsent: TrackingConsent.granted)
+ }
+}
+
+private class MockDatadogWebViewDelegate: NSObject, RCTDatadogWebViewDelegate {
+ var wasCalled = false
+ func didCreateWebView(_ webView: RCTDatadogWebView!) {
+ self.wasCalled = true
+ }
+}
+
+private class MockDatadogCore: DatadogCoreProtocol {
+ func mostRecentModifiedFileAt(before: Date) throws -> Date? {
+ return nil
+ }
+
+ func scope(for featureType: T.Type) -> any DatadogInternal.FeatureScope where T : DatadogInternal.DatadogFeature {
+ return NOPFeatureScope()
+ }
+
+ func feature(named name: String, type: T.Type) -> T? {
+ return nil
+ }
+
+ func register(feature: T) throws where T : DatadogInternal.DatadogFeature {}
+ func send(message: DatadogInternal.FeatureMessage, else fallback: @escaping () -> Void) {}
+ func set(baggage: @escaping () -> DatadogInternal.FeatureBaggage?, forKey key: String) {}
+}
diff --git a/packages/react-native-webview/package.json b/packages/react-native-webview/package.json
index 8cf674def..0ac987f3b 100644
--- a/packages/react-native-webview/package.json
+++ b/packages/react-native-webview/package.json
@@ -23,7 +23,16 @@
"main": "lib/commonjs/index",
"files": [
"src/**",
- "lib/**"
+ "lib/**",
+ "android/build.gradle",
+ "android/detekt.yml",
+ "android/gradle.properties",
+ "android/src/**",
+ "ios/Sources/**",
+ "ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/xcsharedata",
+ "ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/*.xcworkspacedata",
+ "ios/DatadogSDKReactNativeWebView.xcodeproj/*.pbxproj",
+ "DatadogSDKReactNativeWebView.podspec"
],
"types": "lib/typescript/index.d.ts",
"react-native": "src/index",
@@ -39,7 +48,8 @@
},
"devDependencies": {
"@testing-library/react-native": "7.0.2",
- "react-native-builder-bob": "0.26.0"
+ "react-native-builder-bob": "0.26.0",
+ "react-native-webview": "^13.12.2"
},
"peerDependencies": {
"@datadog/mobile-react-native": "^2.0.1",
@@ -75,5 +85,13 @@
}
]
]
+ },
+ "codegenConfig": {
+ "name": "DdSdkReactNativeWebView",
+ "type": "all",
+ "jsSrcsDir": "./src/specs",
+ "android": {
+ "javaPackageName": "com.datadog.reactnative.webview"
+ }
}
}
diff --git a/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx b/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx
index 6385712c9..073a4317b 100644
--- a/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx
+++ b/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx
@@ -3,62 +3,58 @@
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
-import { fireEvent, render } from '@testing-library/react-native';
-import type { WebView as RNWebView } from 'react-native-webview';
-import { NativeModules } from 'react-native';
+
+import { render } from '@testing-library/react-native';
+import { WebView as RNWebView } from 'react-native-webview';
import React from 'react';
-import { DATADOG_MESSAGE_PREFIX } from '../__utils__/getInjectedJavaScriptBeforeContentLoaded';
import { WebView } from '../index';
+import { NativeDdWebView } from '../specs/NativeDdWebView';
+
+jest.mock('react-native-webview', () => {
+ return {
+ WebView: jest.fn()
+ };
+});
+
+jest.mock('../specs/NativeDdWebView', () => {
+ return {
+ NativeDdWebView: jest.fn()
+ };
+});
describe('WebView', () => {
beforeEach(() => {
jest.clearAllMocks();
});
- const DdMessage = 'custom datadog event';
- const datadogEvent = {
- nativeEvent: {
- data: `${DATADOG_MESSAGE_PREFIX} ${DdMessage}`
- }
- };
- const userDefinedEvent = {
- nativeEvent: {
- data: 'custom user-defined message'
- }
- };
- it('calls provided onMessage props', async () => {
- const onMessage = jest.fn();
- const { findByTestId } = render(
-
- );
- const webView = await findByTestId('webView');
+ it('WebView is rendered with native component', () => {
+ // Given
+ const allowedHosts = ['example.com', 'localhost'];
- fireEvent(webView, 'message', userDefinedEvent);
- expect(onMessage).toHaveBeenCalledWith(userDefinedEvent);
+ // When
+ render();
- fireEvent(webView, 'message', datadogEvent);
- expect(onMessage).toHaveBeenCalledTimes(1);
- expect(onMessage).not.toHaveBeenCalledWith(datadogEvent);
- });
- it('calls consumeWebviewEvent with Datadog logs', async () => {
- const { findByTestId } = render(
-
- );
- const webView = await findByTestId('webView');
- fireEvent(webView, 'message', datadogEvent);
+ // Then
+ const mockedWebView = jest.mocked(RNWebView);
+ const component =
+ mockedWebView.mock.calls[0][0].nativeConfig?.component;
+ const mockedNativeWebView = jest.mocked(NativeDdWebView);
- expect(NativeModules.DdSdk.consumeWebviewEvent).toHaveBeenCalledWith(
- DdMessage
- );
+ expect(component).toBe(mockedNativeWebView);
});
- it('forwards ref to the actual RN Webview component', async () => {
- const ref = React.createRef();
- const { findByTestId } = render(
-
- );
- await findByTestId('webView');
- expect(ref.current?.injectJavaScript).toBeDefined();
+ it('Datadog WebView allowedHosts are forwarded to WebView nativeConfig props', () => {
+ // Given
+ const allowedHosts = ['example.com', 'localhost'];
+
+ // When
+ render();
+
+ // Then
+ const mockedWebView = jest.mocked(RNWebView);
+ const props = mockedWebView.mock.calls[0][0].nativeConfig?.props as any;
+
+ expect(props?.allowedHosts).toBe(allowedHosts);
});
});
diff --git a/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx b/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx
index 3f95d63fc..5c238ec6f 100644
--- a/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx
+++ b/packages/react-native-webview/src/__tests__/WebviewDatadogInjectedJS.test.tsx
@@ -3,26 +3,45 @@
* 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 { dedent } from './__utils__/string-utils';
+
jest.mock('react-native-webview', () => {
return {
WebView: jest.fn(props => {
// eslint-disable-next-line no-eval
eval(props.injectedJavaScriptBeforeContentLoaded);
- return null;
+ // eslint-disable-next-line no-eval
+ eval(props.injectedJavaScript);
+ return undefined;
})
};
});
+
const callfunction = jest.fn();
+const postMessageMock = jest.fn();
+
+window['ReactNativeWebView'] = {
+ postMessage: postMessageMock
+};
describe('Webview', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('should pass injectedJavaScriptBeforeContentLoaded prop to WebView component', () => {
+ // Given
const injectedJavaScriptBeforeContentLoaded = 'callfunction()';
const allowedHosts = ['example.com', 'localhost'];
+
+ // When
render(
{
}
/>
);
+
+ // Then
+ expect(callfunction).toHaveBeenCalled();
+ });
+
+ it('should pass injectedJavaScript prop to WebView component', () => {
+ // Given
+ const injectedJavaScript = 'callfunction()';
+ const allowedHosts = ['example.com', 'localhost'];
+
+ // When
+ render(
+
+ );
+
+ // Then
+ expect(callfunction).toHaveBeenCalled();
+ });
+
+ it('should pass injectedJavaScriptBeforeContentLoaded prop to WebView component W { injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true }', () => {
+ // Given
+ const injectedJavaScript = 'callfunction()';
+ const allowedHosts = ['example.com', 'localhost'];
+
+ // When
+ render(
+
+ );
+
+ // Then
+ expect(callfunction).toHaveBeenCalled();
+ });
+
+ it('should pass injectedJavaScript prop to WebView component W { injectedJavaScriptForMainFrameOnly = true }', () => {
+ // Given
+ const injectedJavaScript = 'callfunction()';
+ const allowedHosts = ['example.com', 'localhost'];
+
+ // When
+ render(
+
+ );
+
+ // Then
expect(callfunction).toHaveBeenCalled();
});
+
+ it('should wrap injectedJavaScript in try & catch block', () => {
+ // Given
+ const onMessage = jest.fn();
+ const allowedHosts = ['localhost', 'example.com'];
+ const injectedJavaScript = 'testInjectedJavaScript()';
+
+ // When
+ render(
+
+ );
+
+ // Then
+ const mockedWebView = jest.mocked(RNWebView);
+ 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;
+ }`);
+
+ expect(realInjectedJs).toBe(expected);
+ });
+
+ it('should wrap injectedJavaScriptBeforeContentLoaded in try & catch block', () => {
+ // Given
+ const onMessage = jest.fn();
+ const allowedHosts = ['localhost', 'example.com'];
+ const injectedJavaScript = 'testInjectedJavaScript()';
+
+ // When
+ render(
+
+ );
+
+ // Then
+ const mockedWebView = jest.mocked(RNWebView);
+ const realInjectedJs = dedent(
+ mockedWebView.mock.calls[0][0]
+ .injectedJavaScriptBeforeContentLoaded ?? ''
+ );
+ 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);
+ });
+
+ it('should call postMessage with datadog error event W injectedJavaScript has errors', () => {
+ // Given
+ const onMessage = jest.fn();
+ const allowedHosts = ['localhost', 'example.com'];
+ const injectedJavaScript = 'testInjectedJavaScript()';
+
+ // When
+ render(
+
+ );
+
+ // Then
+ expect(postMessageMock).toHaveBeenCalledWith(
+ JSON.stringify({
+ source: 'DATADOG',
+ type: 'ERROR',
+ message: 'testInjectedJavaScript is not defined'
+ })
+ );
+ });
+
+ it('should call postMessage with datadog error event W injectedJavaScriptBeforeContentLoaded has errors', () => {
+ // Given
+ const onMessage = jest.fn();
+ const allowedHosts = ['localhost', 'example.com'];
+ const injectedJavaScript = 'testInjectedJavaScript()';
+
+ // When
+ render(
+
+ );
+
+ // Then
+ expect(postMessageMock).toHaveBeenCalledWith(
+ JSON.stringify({
+ source: 'DATADOG',
+ type: 'ERROR',
+ message: 'testInjectedJavaScript is not defined'
+ })
+ );
+ });
});
diff --git a/packages/react-native-webview/src/__tests__/WebviewDatadogPerformance.test.tsx b/packages/react-native-webview/src/__tests__/WebviewDatadogPerformance.test.tsx
index 01b3e8c18..769c93450 100644
--- a/packages/react-native-webview/src/__tests__/WebviewDatadogPerformance.test.tsx
+++ b/packages/react-native-webview/src/__tests__/WebviewDatadogPerformance.test.tsx
@@ -3,6 +3,7 @@
* 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';
@@ -20,8 +21,15 @@ describe('WebView performance', () => {
jest.clearAllMocks();
});
it('should update the onMessage prop of the RNWebView component', () => {
+ /**
+ * GIVEN
+ */
const onMessage = jest.fn();
const allowedHosts = ['localhost', 'example.com'];
+
+ /**
+ * WHEN
+ */
const { rerender } = render(
);
@@ -34,6 +42,10 @@ describe('WebView performance', () => {
rerender(
);
+
+ /**
+ * THEN
+ */
const mockedWebView = jest.mocked(RNWebView);
// Verify that the onMessage prop of the RNWebView component has changed
expect(mockedWebView.mock.calls[0][0].onMessage).toBe(
diff --git a/packages/react-native-webview/src/__tests__/__utils__/string-utils.ts b/packages/react-native-webview/src/__tests__/__utils__/string-utils.ts
new file mode 100644
index 000000000..eb8a2a0bc
--- /dev/null
+++ b/packages/react-native-webview/src/__tests__/__utils__/string-utils.ts
@@ -0,0 +1,13 @@
+/*
+ * 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.
+ */
+
+// Utility function to remove indentation from a given string
+export function dedent(str: string) {
+ const match = str.match(/^[ \t]*(?=\S)/gm);
+ const indent = match ? Math.min(...match.map(el => el.length)) : 0;
+ const regex = new RegExp(`^[ \\t]{${indent}}`, 'gm');
+ return indent > 0 ? str.replace(regex, '') : str;
+}
diff --git a/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts b/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts
deleted file mode 100644
index b0b07e01b..000000000
--- a/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts
+++ /dev/null
@@ -1,40 +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.
- */
-/* eslint-disable @typescript-eslint/ban-ts-comment */
-import { NativeModules } from 'react-native';
-
-import { formatAllowedHosts } from '../__utils__/formatAllowedHosts';
-
-describe('Format allowed hosts', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
- it('returns the host in expected format', () => {
- const allowedHosts = ['host1.com', 'example.fr', 'api.com'];
- expect(formatAllowedHosts(allowedHosts)).toBe(
- '\'["host1.com","example.fr","api.com"]\''
- );
- });
- it('returns an empty arrary when the host is a BigInt', () => {
- const allowedHosts = BigInt(1240);
- // @ts-ignore
- const result = formatAllowedHosts(allowedHosts);
- expect(NativeModules.DdSdk.telemetryError).toHaveBeenCalled();
- expect(result).toBe("'[]'");
- });
- it('returns an empty array when the host is a circular reference', () => {
- type circularReference = {
- host: string;
- name?: circularReference | string;
- };
- const allowedHosts: circularReference = { host: 'value', name: '' };
- allowedHosts.name = allowedHosts;
- // @ts-ignore
- const result = formatAllowedHosts(allowedHosts);
- expect(NativeModules.DdSdk.telemetryError).toHaveBeenCalled();
- expect(result).toBe("'[]'");
- });
-});
diff --git a/packages/react-native-webview/src/__tests__/getInjectedJavaScriptBeforeContentLoaded.test.ts b/packages/react-native-webview/src/__tests__/getInjectedJavaScriptBeforeContentLoaded.test.ts
deleted file mode 100644
index 74a0db7b5..000000000
--- a/packages/react-native-webview/src/__tests__/getInjectedJavaScriptBeforeContentLoaded.test.ts
+++ /dev/null
@@ -1,86 +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.
- */
-/* eslint-disable no-eval */
-import {
- getInjectedJavaScriptBeforeContentLoaded,
- DATADOG_MESSAGE_PREFIX
-} from '../__utils__/getInjectedJavaScriptBeforeContentLoaded';
-
-describe('getInjectedJavaScriptBeforeContentLoaded', () => {
- const DdMessage = 'custom datadog event';
- let allowedHosts: string[];
- let script: string;
- beforeEach(() => {
- jest.clearAllMocks();
- Object.defineProperty(window, 'ReactNativeWebView', {
- value: {
- postMessage: jest.fn()
- },
- writable: true
- });
- delete (window as any).DatadogEventBridge;
- });
- it('posts the message and returns allowed webview hosts', () => {
- allowedHosts = ['example.com', 'localhost'];
- script = getInjectedJavaScriptBeforeContentLoaded(allowedHosts);
- eval(script);
-
- // Posting the message
- (window as any).DatadogEventBridge.send(DdMessage);
- expect((window as any).ReactNativeWebView.postMessage).toBeCalledWith(
- `${DATADOG_MESSAGE_PREFIX} ${DdMessage}`
- );
-
- // Getting the allowed hosts
- expect(
- (window as any).DatadogEventBridge.getAllowedWebViewHosts()
- ).toBe('["example.com","localhost"]');
- });
- it('should return an empty array for getAllowedWebViewHosts if no hosts are given', () => {
- allowedHosts = [];
- script = getInjectedJavaScriptBeforeContentLoaded(allowedHosts);
- eval(script);
-
- expect(
- (window as any).DatadogEventBridge.getAllowedWebViewHosts()
- ).toBe('[]');
- });
- it('uses our injected javascript and the user provided implementation', () => {
- allowedHosts = ['example.com', 'localhost'];
- const callfunction = jest.fn();
- const injectedJavaScriptBeforeContentLoaded = 'callfunction()';
- script = getInjectedJavaScriptBeforeContentLoaded(
- allowedHosts,
- injectedJavaScriptBeforeContentLoaded
- );
- eval(script);
- expect(callfunction).toHaveBeenCalled();
- // Posting the message
- (window as any).DatadogEventBridge.send(DdMessage);
- expect((window as any).ReactNativeWebView.postMessage).toBeCalledWith(
- `${DATADOG_MESSAGE_PREFIX} ${DdMessage}`
- );
- });
- it('executes our injected javascript when the user provided implementation throws an error', () => {
- allowedHosts = ['example.com', 'localhost'];
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const callfunction = jest.fn().mockImplementation(() => {
- throw new Error('The user functions throws an error');
- });
- const injectedJavaScriptBeforeContentLoaded = 'callfunction()';
- script = getInjectedJavaScriptBeforeContentLoaded(
- allowedHosts,
- injectedJavaScriptBeforeContentLoaded
- );
- eval(script);
- expect(() => eval(injectedJavaScriptBeforeContentLoaded)).toThrow();
- // Posting the message
- (window as any).DatadogEventBridge.send(DdMessage);
- expect((window as any).ReactNativeWebView.postMessage).toBeCalledWith(
- `${DATADOG_MESSAGE_PREFIX} ${DdMessage}`
- );
- });
-});
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
new file mode 100644
index 000000000..b09ea8dbc
--- /dev/null
+++ b/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+import { wrapJsCodeInTryAndCatch } from '../utils/webview-js-utils';
+
+import { dedent } from './__utils__/string-utils';
+
+describe('WebView JS Utils', () => {
+ beforeEach(() => {
+ 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', () => {
+ // 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 { jsCode = undefined }', () => {
+ // Given
+ const jsCode = undefined;
+ // When
+ const wrappedCode = wrapJsCodeInTryAndCatch(jsCode);
+ // Then
+ expect(wrappedCode).toBe(undefined);
+ });
+ });
+});
diff --git a/packages/react-native-webview/src/__utils__/formatAllowedHosts.ts b/packages/react-native-webview/src/__utils__/formatAllowedHosts.ts
deleted file mode 100644
index 25142ec89..000000000
--- a/packages/react-native-webview/src/__utils__/formatAllowedHosts.ts
+++ /dev/null
@@ -1,70 +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.
- */
-import { NativeDdSdk } from '../NativeDdSdk';
-
-export function formatAllowedHosts(allowedHosts?: string[]): string {
- try {
- return `'${JSON.stringify(allowedHosts)}'`;
- } catch (e: any) {
- if (NativeDdSdk) {
- NativeDdSdk.telemetryError(
- getErrorMessage(e),
- getErrorStackTrace(e),
- 'AllowedHostsError'
- );
- }
- return "'[]'";
- }
-}
-
-/**
- * The next section is copied from packages/core/src/errorUtils
- */
-
-const EMPTY_MESSAGE = 'Unknown Error';
-const EMPTY_STACK_TRACE = '';
-
-const getErrorMessage = (error: any | undefined): string => {
- let message = EMPTY_MESSAGE;
- if (error === undefined || error === null) {
- message = EMPTY_MESSAGE;
- } else if (typeof error === 'object' && 'message' in error) {
- message = String(error.message);
- } else {
- message = String(error);
- }
-
- return message;
-};
-
-const getErrorStackTrace = (error: any | undefined): string => {
- let stack = EMPTY_STACK_TRACE;
-
- try {
- if (error === undefined || error === null) {
- stack = EMPTY_STACK_TRACE;
- } else if (typeof error === 'string') {
- stack = EMPTY_STACK_TRACE;
- } else if (typeof error === 'object') {
- if ('stacktrace' in error) {
- stack = String(error.stacktrace);
- } else if ('stack' in error) {
- stack = String(error.stack);
- } else if ('componentStack' in error) {
- stack = String(error.componentStack);
- } else if (
- 'sourceURL' in error &&
- 'line' in error &&
- 'column' in error
- ) {
- stack = `at ${error.sourceURL}:${error.line}:${error.column}`;
- }
- }
- } catch (e) {
- // Do nothing
- }
- return stack;
-};
diff --git a/packages/react-native-webview/src/__utils__/getInjectedJavaScriptBeforeContentLoaded.ts b/packages/react-native-webview/src/__utils__/getInjectedJavaScriptBeforeContentLoaded.ts
deleted file mode 100644
index bac0eea2e..000000000
--- a/packages/react-native-webview/src/__utils__/getInjectedJavaScriptBeforeContentLoaded.ts
+++ /dev/null
@@ -1,29 +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.
- */
-import { formatAllowedHosts } from './formatAllowedHosts';
-
-export const DATADOG_MESSAGE_PREFIX = '[DATADOG]';
-
-export const getInjectedJavaScriptBeforeContentLoaded = (
- allowedHosts?: string[],
- injectedJavaScriptBeforeContentLoaded?: string
-): string =>
- `
- window.DatadogEventBridge = {
- send(msg) {
- window.ReactNativeWebView.postMessage("${DATADOG_MESSAGE_PREFIX} " + msg)
- },
- getAllowedWebViewHosts() {
- return ${formatAllowedHosts(allowedHosts)}
- }
- };
- try{
- ${injectedJavaScriptBeforeContentLoaded}
- }
- catch (error) {
- // The user defined code has crashed
- }
- `;
diff --git a/packages/react-native-webview/src/ext-specs/NativeDdLogs.ts b/packages/react-native-webview/src/ext-specs/NativeDdLogs.ts
new file mode 100644
index 000000000..fc3291506
--- /dev/null
+++ b/packages/react-native-webview/src/ext-specs/NativeDdLogs.ts
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable @typescript-eslint/ban-types */
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+/**
+ * Do not import this Spec directly, use DdNativeLogsType instead.
+ */
+export interface Spec extends TurboModule {
+ readonly getConstants: () => {};
+ /**
+ * Send a log with ERROR level.
+ * @param message: The message to send.
+ * @param context: The additional context to send.
+ */
+ readonly error: (message: string, context: Object) => Promise;
+}
+
+// eslint-disable-next-line import/no-default-export
+export default TurboModuleRegistry.get('DdLogs');
diff --git a/packages/react-native-webview/src/NativeDdSdk.ts b/packages/react-native-webview/src/ext-specs/NativeDdSdk.ts
similarity index 99%
rename from packages/react-native-webview/src/NativeDdSdk.ts
rename to packages/react-native-webview/src/ext-specs/NativeDdSdk.ts
index 7ebc0e3b9..e31acf242 100644
--- a/packages/react-native-webview/src/NativeDdSdk.ts
+++ b/packages/react-native-webview/src/ext-specs/NativeDdSdk.ts
@@ -15,6 +15,7 @@ export interface PartialNativeDdSdkSpec extends TurboModule {
consumeWebviewEvent(message: string): Promise;
telemetryError(message: string, stack: string, kind: string): Promise;
}
+
export const NativeDdSdk = TurboModuleRegistry.get(
'DdSdk'
);
diff --git a/packages/react-native-webview/src/index.tsx b/packages/react-native-webview/src/index.tsx
index b99750ec0..8b0b67b97 100644
--- a/packages/react-native-webview/src/index.tsx
+++ b/packages/react-native-webview/src/index.tsx
@@ -7,40 +7,97 @@ import type { WebViewMessageEvent, WebViewProps } from 'react-native-webview';
import { WebView as RNWebView } from 'react-native-webview';
import React, { forwardRef, useCallback } from 'react';
-import { NativeDdSdk } from './NativeDdSdk';
+import NativeDdLogs from './ext-specs/NativeDdLogs';
+import { NativeDdSdk } from './ext-specs/NativeDdSdk';
import {
- DATADOG_MESSAGE_PREFIX,
- getInjectedJavaScriptBeforeContentLoaded
-} from './__utils__/getInjectedJavaScriptBeforeContentLoaded';
+ getWebViewEventBridgingJS,
+ wrapJsCodeInTryAndCatch
+} from './utils/webview-js-utils';
+import type { DatadogMessageFormat } from './utils/webview-js-utils';
+import { isNewArchitecture } from './utils/env-utils';
+import { NativeDdWebView } from './specs/NativeDdWebView';
type Props = WebViewProps & {
+ /**
+ * The list of allowed hosts for Datadog WebView tracking.
+ */
allowedHosts?: string[];
+ /**
+ * Whether injected User JS Code errors should be logged to Datadog (default: false).
+ */
+ logUserCodeErrors?: boolean;
+ /**
+ * Custom JS Code to inject before the WebView content is loaded.
+ */
injectedJavaScriptBeforeContentLoaded?: string;
};
const WebViewComponent = (props: Props, ref: React.Ref>) => {
const userDefinedOnMessage = props.onMessage;
+
const onMessage = useCallback(
(event: WebViewMessageEvent) => {
+ const handleDatadogMessage = (ddMessage: DatadogMessageFormat) => {
+ if (
+ ddMessage.type === 'ERROR' &&
+ ddMessage.message != null &&
+ (props.logUserCodeErrors ?? false)
+ ) {
+ NativeDdLogs?.error(ddMessage.message, {});
+ } else if (
+ ddMessage.type === 'NATIVE_EVENT' &&
+ ddMessage.message != null
+ ) {
+ NativeDdSdk?.consumeWebviewEvent(ddMessage.message);
+ }
+ };
+
const message = event.nativeEvent.data;
- if (message.startsWith(DATADOG_MESSAGE_PREFIX)) {
- NativeDdSdk?.consumeWebviewEvent(
- message.substring(DATADOG_MESSAGE_PREFIX.length + 1)
- );
- } else {
+ if (message == null) {
+ return;
+ }
+
+ try {
+ const jsonMsg = JSON.parse(message);
+ if (jsonMsg && jsonMsg.source === 'DATADOG') {
+ handleDatadogMessage(jsonMsg);
+ } else {
+ userDefinedOnMessage?.(event);
+ }
+ } catch (err) {
userDefinedOnMessage?.(event);
}
},
- [userDefinedOnMessage]
+ [userDefinedOnMessage, props.logUserCodeErrors]
);
+
+ const getInjectedJavascriptBeforeContentLoaded = (): string | undefined => {
+ if (isNewArchitecture()) {
+ return getWebViewEventBridgingJS(
+ props.allowedHosts,
+ props.injectedJavaScriptBeforeContentLoaded
+ );
+ } else {
+ return wrapJsCodeInTryAndCatch(
+ props.injectedJavaScriptBeforeContentLoaded
+ );
+ }
+ };
+
return (
);
diff --git a/packages/react-native-webview/src/specs/NativeDdWebView.ts b/packages/react-native-webview/src/specs/NativeDdWebView.ts
new file mode 100644
index 000000000..ae380a511
--- /dev/null
+++ b/packages/react-native-webview/src/specs/NativeDdWebView.ts
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+import type { CommonNativeWebViewProps } from 'react-native-webview/lib/WebViewTypes';
+import { requireNativeComponent } from 'react-native';
+import { isNewArchitecture } from '@datadog/mobile-react-native-webview/src/utils/env-utils';
+
+
+const NativeDdWebView = !isNewArchitecture() ? requireNativeComponent(
+ 'DdReactNativeWebView'
+) : undefined;
+
+export { NativeDdWebView };
diff --git a/packages/react-native-webview/src/utils/env-utils.ts b/packages/react-native-webview/src/utils/env-utils.ts
new file mode 100644
index 000000000..d30542bc6
--- /dev/null
+++ b/packages/react-native-webview/src/utils/env-utils.ts
@@ -0,0 +1,9 @@
+/*
+ * 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
new file mode 100644
index 000000000..101ea1873
--- /dev/null
+++ b/packages/react-native-webview/src/utils/webview-js-utils.ts
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+import { NativeDdSdk } from '../ext-specs/NativeDdSdk';
+
+export const DATADOG_MESSAGE_PREFIX = '[DATADOG]';
+
+/**
+ * Internal Datadog Message Type
+ */
+export type DatadogMessageType =
+ /**
+ * Signals errors that occured during the execution of JavaScript code in the WebView.
+ */
+ | 'ERROR'
+ /**
+ * Signals events that should be forwarded and consumed by the native SDK.
+ */
+ | 'NATIVE_EVENT';
+
+/**
+ * Internal Datadog Message Format.
+ */
+export type DatadogMessageFormat = {
+ type: DatadogMessageType;
+ message: string;
+};
+
+/**
+ * Wraps the given JS Code in a try and catch block.
+ * @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}
+ }
+ catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ window.ReactNativeWebView.postMessage(JSON.stringify({
+ source: 'DATADOG',
+ type: 'ERROR',
+ message: errorMsg
+ }));
+ true;
+ }`
+ : 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;
+ }
+ `;
+
+function formatAllowedHosts(allowedHosts?: string[]): string {
+ try {
+ return `'${JSON.stringify(allowedHosts)}'`;
+ } catch (e: any) {
+ if (NativeDdSdk) {
+ NativeDdSdk.telemetryError(
+ getErrorMessage(e),
+ getErrorStackTrace(e),
+ 'AllowedHostsError'
+ );
+ }
+ return "'[]'";
+ }
+}
+
+const getErrorMessage = (error: any | undefined): string => {
+ const EMPTY_MESSAGE = 'Unknown Error';
+ let message = EMPTY_MESSAGE;
+ if (error === undefined || error === null) {
+ message = EMPTY_MESSAGE;
+ } else if (typeof error === 'object' && 'message' in error) {
+ message = String(error.message);
+ } else {
+ message = String(error);
+ }
+
+ return message;
+};
+
+const getErrorStackTrace = (error: any | undefined): string => {
+ const EMPTY_STACK_TRACE = '';
+ let stack = EMPTY_STACK_TRACE;
+
+ try {
+ if (error === undefined || error === null) {
+ stack = EMPTY_STACK_TRACE;
+ } else if (typeof error === 'string') {
+ stack = EMPTY_STACK_TRACE;
+ } else if (typeof error === 'object') {
+ if ('stacktrace' in error) {
+ stack = String(error.stacktrace);
+ } else if ('stack' in error) {
+ stack = String(error.stack);
+ } else if ('componentStack' in error) {
+ stack = String(error.componentStack);
+ } else if (
+ 'sourceURL' in error &&
+ 'line' in error &&
+ 'column' in error
+ ) {
+ stack = `at ${error.sourceURL}:${error.line}:${error.column}`;
+ }
+ }
+ } catch (e) {
+ // Do nothing
+ }
+ return stack;
+};
diff --git a/yarn.lock b/yarn.lock
index 4070813de..1df83e457 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2632,6 +2632,7 @@ __metadata:
dependencies:
"@testing-library/react-native": 7.0.2
react-native-builder-bob: 0.26.0
+ react-native-webview: ^13.12.2
peerDependencies:
"@datadog/mobile-react-native": ^2.0.1
react: ">=16.13.1"
@@ -8494,6 +8495,7 @@ __metadata:
react-native-navigation: 7.40.1
react-native-safe-area-context: 4.10.8
react-native-screens: 3.29.0
+ react-native-webview: ^13.12.2
languageName: unknown
linkType: soft
@@ -8531,7 +8533,7 @@ __metadata:
react-native: 0.73.9
react-native-builder-bob: 0.26.0
react-native-gradle-plugin: ^0.71.19
- react-native-webview: 11.26.1
+ react-native-webview: 13.12.2
react-test-renderer: 18.1.0
typescript: 5.0.4
languageName: unknown
@@ -9255,13 +9257,6 @@ __metadata:
languageName: node
linkType: hard
-"escape-string-regexp@npm:2.0.0, escape-string-regexp@npm:^2.0.0":
- version: 2.0.0
- resolution: "escape-string-regexp@npm:2.0.0"
- checksum: 9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395
- languageName: node
- linkType: hard
-
"escape-string-regexp@npm:^1.0.5":
version: 1.0.5
resolution: "escape-string-regexp@npm:1.0.5"
@@ -9269,6 +9264,13 @@ __metadata:
languageName: node
linkType: hard
+"escape-string-regexp@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "escape-string-regexp@npm:2.0.0"
+ checksum: 9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395
+ languageName: node
+ linkType: hard
+
"escape-string-regexp@npm:^4.0.0":
version: 4.0.0
resolution: "escape-string-regexp@npm:4.0.0"
@@ -15649,16 +15651,16 @@ __metadata:
languageName: node
linkType: hard
-"react-native-webview@npm:11.26.1":
- version: 11.26.1
- resolution: "react-native-webview@npm:11.26.1"
+"react-native-webview@npm:13.12.2, react-native-webview@npm:^13.12.2":
+ version: 13.12.2
+ resolution: "react-native-webview@npm:13.12.2"
dependencies:
- escape-string-regexp: 2.0.0
+ escape-string-regexp: ^4.0.0
invariant: 2.2.4
peerDependencies:
react: "*"
react-native: "*"
- checksum: d2f95a89e944a2f1e8cf402e4e274f3568edae42e7ef190915e9fba8004a01d699c962459bdc9688c159060538e90aea3017cab24e6f4112021cbbc10ef57104
+ checksum: d84b2e2e75dc484eb40f8e285607b9686693f2d31e95802e2b358dc0820bcc294b438a77174eba157c31d95675c962c61c94885629af999416ad1355e9618fe0
languageName: node
linkType: hard