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