From dfa2bf0fc967e7d380d46e5df416d5615c77b036 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 23 Sep 2024 16:13:57 +0200 Subject: [PATCH 01/12] Android: Native Datadog WebView component --- package.json | 2 +- .../react-native-webview/android/build.gradle | 239 ++++++++ .../react-native-webview/android/detekt.yml | 572 ++++++++++++++++++ .../android/gradle.properties | 5 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + packages/react-native-webview/android/gradlew | 185 ++++++ .../react-native-webview/android/gradlew.bat | 104 ++++ .../android/settings.gradle | 7 + .../android/src/main/AndroidManifest.xml | 11 + .../webview/DdSdkReactNativeWebViewManager.kt | 127 ++++ .../webview/DdSdkReactNativeWebViewPackage.kt | 32 + .../reactnative/tools/unit/GenericAssert.kt | 69 +++ .../reactnative/tools/unit/ReflectUtils.kt | 266 ++++++++ .../org.mockito.plugins.MockMaker | 1 + packages/react-native-webview/package.json | 17 +- .../react-native-webview/src/NativeDdSdk.ts | 2 +- .../src/__tests__/WebviewDatadog.test.tsx | 54 +- .../src/__tests__/formatAllowedHosts.test.ts | 48 +- ...ectedJavaScriptBeforeContentLoaded.test.ts | 86 --- .../src/__utils__/formatAllowedHosts.ts | 70 --- ...etInjectedJavaScriptBeforeContentLoaded.ts | 29 - packages/react-native-webview/src/index.tsx | 59 +- .../src/specs/NativeDdWebView.ts | 14 + .../src/utils/format-utils.ts | 30 + yarn.lock | 27 +- 26 files changed, 1767 insertions(+), 295 deletions(-) create mode 100644 packages/react-native-webview/android/build.gradle create mode 100644 packages/react-native-webview/android/detekt.yml create mode 100644 packages/react-native-webview/android/gradle.properties create mode 100644 packages/react-native-webview/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 packages/react-native-webview/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 packages/react-native-webview/android/gradlew create mode 100644 packages/react-native-webview/android/gradlew.bat create mode 100644 packages/react-native-webview/android/settings.gradle create mode 100644 packages/react-native-webview/android/src/main/AndroidManifest.xml create mode 100644 packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt create mode 100644 packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt create mode 100644 packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt create mode 100644 packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt create mode 100644 packages/react-native-webview/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker delete mode 100644 packages/react-native-webview/src/__tests__/getInjectedJavaScriptBeforeContentLoaded.test.ts delete mode 100644 packages/react-native-webview/src/__utils__/formatAllowedHosts.ts delete mode 100644 packages/react-native-webview/src/__utils__/getInjectedJavaScriptBeforeContentLoaded.ts create mode 100644 packages/react-native-webview/src/specs/NativeDdWebView.ts create mode 100644 packages/react-native-webview/src/utils/format-utils.ts 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/android/build.gradle b/packages/react-native-webview/android/build.gradle new file mode 100644 index 000000000..aa691fcb8 --- /dev/null +++ b/packages/react-native-webview/android/build.gradle @@ -0,0 +1,239 @@ +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 { + 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 + api "com.facebook.react:react-android:$reactNativeVersion" + } + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "com.datadoghq:dd-sdk-android-webview:2.11.0" + 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 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 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..a75f26fd3 --- /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') \ No newline at end of file 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/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt b/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt new file mode 100644 index 000000000..998da927d --- /dev/null +++ b/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt @@ -0,0 +1,127 @@ +/* +* 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() { + // Custom WebView Client + private class DatadogWebViewClient : RNCWebViewClient() + + // Custom WebView + @SuppressLint("SetJavaScriptEnabled") + private class DatadogWebView(reactContext: ThemedReactContext) : RNCWebView(reactContext) { + @Volatile + var isWebViewTrackingEnabled: Boolean = false + + init { + // JavaScript has to be enabled for auto-instrumentation. + this.settings.javaScriptEnabled = true + } + + /** + * Enables Datadog tracking on the WebView. + * @param allowedHosts the list of allowed hosts + */ + fun enableTracking( + reactContext: ReactContext, + sdkCore: SdkCore?, + allowedHosts: List + ) { + if (sdkCore != null) { + enableWebViewTracking(reactContext, sdkCore, allowedHosts) + } else { + DatadogSDKWrapperStorage.addOnInitializedListener { core -> + enableWebViewTracking(reactContext, core, allowedHosts) + } + } + } + + private fun enableWebViewTracking( + reactContext: ReactContext, + sdkCore: SdkCore, + allowedHosts: List + ) { + if (isWebViewTrackingEnabled) { + return + } + + WebViewTracking.enable( + this, + allowedHosts = allowedHosts, + sdkCore = sdkCore + ) + + isWebViewTrackingEnabled = true + } + } + + // 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 + + init { + DatadogSDKWrapperStorage.addOnInitializedListener { core -> + datadogCore = core + } + } + + // The Custom WebView exposed properties. + @ReactProp(name = "allowedHosts") + fun setAllowedHosts(view: RNCWebViewWrapper, allowedHosts: ReadableArray) { + (view.webView as DatadogWebView).enableTracking( + reactContext, + datadogCore, + toStringList(allowedHosts) + ) + } + + // Overrides the default ViewInstance by binding the CustomWebView to it. + override fun createViewInstance(context: ThemedReactContext): RNCWebViewWrapper { + return super.createViewInstance(context, DatadogWebView(context)) + } + + // Attaches our custom WebView client to the WebView. + override fun addEventEmitters( + reactContext: ThemedReactContext, + view: RNCWebViewWrapper + ) { + view.webView.webViewClient = DatadogWebViewClient() + } + + // Utility function for converting the ReadableArray to a list of strings. + private fun toStringList(props: ReadableArray): List { + return props.toArrayList().filterIsInstance() + } + + // The name used to reference this custom View from React Native. + override fun getName(): String { + return VIEW_NAME + } +} \ No newline at end of file diff --git a/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt b/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt new file mode 100644 index 000000000..f15f6eed6 --- /dev/null +++ b/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt @@ -0,0 +1,32 @@ +/* +* 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..71b045169 --- /dev/null +++ b/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt @@ -0,0 +1,69 @@ +/* + * 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.tools.unit + +import org.assertj.core.api.AbstractAssert +import org.assertj.core.api.Assertions.assertThat + +class GenericAssert(actual: Any?) : + AbstractAssert(actual, GenericAssert::class.java) { + + fun doesNotHaveField(name: String): GenericAssert { + val field: Any? = actual.getFieldValue(name) + assertThat(field) + .overridingErrorMessage( + "Expecting object to not have $name, but found it having value $field" + ) + .isNull() + return this + } + + fun getActualValue(name: String): T { + val field: Any? = actual.getFieldValue(name) + assertThat(field) + .overridingErrorMessage( + "Expecting object to have a non null field named $name, but field was null" + ) + .isNotNull() + return field!! as T + } + + fun hasField(name: String, nestedAssert: (GenericAssert) -> Unit = {}): GenericAssert { + val field: Any? = actual.getFieldValue(name) + assertThat(field) + .overridingErrorMessage( + "Expecting object to have a non null field named $name, but field was null" + ) + .isNotNull() + nestedAssert(GenericAssert(field!!)) + return this + } + + fun hasFieldEqualTo(name: String, expected: F): GenericAssert { + val field: Any? = actual.getFieldValue(name) + assertThat(field).isEqualTo(expected) + return this + } + + fun hasFieldWithClass(name: String, expectedClassName: String): GenericAssert { + val field: Any? = actual.getFieldValue(name) + assertThat(field?.javaClass?.name).isEqualTo(expectedClassName) + return this + } + + fun isInstanceOf(expectedClassName: String): GenericAssert { + val className = actual.javaClass.canonicalName!! + assertThat(className).isEqualTo(expectedClassName) + return this + } + + companion object { + fun assertThat(actual: Any?): GenericAssert { + return GenericAssert(actual) + } + } +} diff --git a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt b/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt new file mode 100644 index 000000000..49beb4465 --- /dev/null +++ b/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt @@ -0,0 +1,266 @@ +/* + * 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.tools.unit + +import java.lang.reflect.Field +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.LinkedList +import kotlin.reflect.jvm.isAccessible + +/** + * Creates an instance of the given class name. + * @param className the full name of the class to instantiate + * @param params the parameters to provide the constructor + * @return the created instance + */ +@Suppress("SpreadOperator") +fun createInstance( + className: String, + vararg params: Any? +): Any { + return Class.forName(className) + .kotlin + .constructors.first() + .apply { isAccessible = true } + .call(*params) +} + +/** + * Sets a static value on the target class. + * @param fieldName the name of the field + * @param fieldValue the value to set + */ +@Suppress("SwallowedException") +inline fun Class.setStaticValue( + fieldName: String, + fieldValue: R +) { + val field = getDeclaredField(fieldName) + + // make it accessible + field.isAccessible = true + + // Make it non final + try { + val modifiersField = Field::class.java.getDeclaredField("modifiers") + modifiersField.isAccessible = true + modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) + } catch (e: NoSuchFieldException) { + // do nothing + @Suppress("PrintStackTrace") + e.printStackTrace() + } + field.set(null, fieldValue) +} + +/** + * Gets the static value from the target class. + * @param className the full name of the class + * @param fieldName the name of the field + */ +inline fun getStaticValue( + className: String, + fieldName: String +): R { + val clazz = Class.forName(className) + val field = clazz.getDeclaredField(fieldName) + // make it accessible + field.isAccessible = true + + return field.get(null) as R +} + +/** + * Gets the static value from the target class. + * @param fieldName the name of the field + */ +inline fun Class.getStaticValue(fieldName: String): R { + val field = getDeclaredField(fieldName) + + // make it accessible + field.isAccessible = true + + return field.get(null) as R +} + +/** + * Sets the field value on the target instance. + * @param fieldName the name of the field + * @param fieldValue the value of the field + */ +@Suppress("SwallowedException") +inline fun Any.setFieldValue( + fieldName: String, + fieldValue: T +): Boolean { + var field: Field? = null + val classesToSearch = LinkedList>() + classesToSearch.add(this.javaClass) + val classesSearched = mutableSetOf>() + + while (field == null && classesToSearch.isNotEmpty()) { + val toSearchIn = classesToSearch.remove() + try { + field = toSearchIn.getDeclaredField(fieldName) + } catch (e: NoSuchFieldException) { + // do nothing + } + classesSearched.add(toSearchIn) + toSearchIn.superclass?.let { + if (!classesSearched.contains(it)) { + classesToSearch.add(it) + } + } + } + + // make it accessible + if (field != null) { + field.isAccessible = true + + // Make it non final + val modifiersField = Field::class.java.getDeclaredField("modifiers") + modifiersField.isAccessible = true + modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) + + field.set(this, fieldValue) + return true + } else { + return false + } +} + +/** + * Gets the field value from the target instance. + * @param fieldName the name of the field + */ +inline fun R.getFieldValue( + fieldName: String, + enclosingClass: Class = this.javaClass +): T { + val field = enclosingClass.getDeclaredField(fieldName) + field.isAccessible = true + return field.get(this) as T +} + +/** + * Invokes a method on the target instance. + * @param methodName the name of the method + * @param params the parameters to provide the method + * @return the result from the invoked method + */ +@Suppress("SpreadOperator", "UNCHECKED_CAST", "TooGenericExceptionCaught") +fun T.invokeMethod( + methodName: String, + vararg params: Any? +): Any? { + val declarationParams = Array?>(params.size) { + params[it]?.javaClass + } + + val method = getDeclaredMethodRecursively(methodName, true, declarationParams) + val wasAccessible = method.isAccessible + + val output: Any? + method.isAccessible = true + try { + output = if (params.isEmpty()) { + method.invoke(this) + } else { + method.invoke(this, *params) + } + } catch (e: InvocationTargetException) { + throw e.cause ?: e + } finally { + method.isAccessible = wasAccessible + } + + return output +} + +/** + * Invokes a method on the target instance, where one or more of the parameters + * are generics. + * @param methodName the name of the method + * @param params the parameters to provide the method + * @return the result from the invoked method + */ +@Suppress("SpreadOperator", "UNCHECKED_CAST") +fun T.invokeGenericMethod( + methodName: String, + vararg params: Any +): Any? { + val declarationParams = Array?>(params.size) { + params[it].javaClass + } + + val method = getDeclaredMethodRecursively(methodName, false, declarationParams) + val wasAccessible = method.isAccessible + + val output: Any? + method.isAccessible = true + try { + output = if (params.isEmpty()) { + method.invoke(this) + } else { + method.invoke(this, *params) + } + } catch (e: InvocationTargetException) { + throw e.cause ?: e + } finally { + method.isAccessible = wasAccessible + } + + return output +} + +@Suppress("TooGenericExceptionCaught", "SwallowedException", "SpreadOperator") +private fun T.getDeclaredMethodRecursively( + methodName: String, + matchingParams: Boolean, + declarationParams: Array?> +): Method { + val classesToSearch = mutableListOf>(this.javaClass) + val classesSearched = mutableListOf>() + var method: Method? + do { + val lookingInClass = classesToSearch.removeAt(0) + classesSearched.add(lookingInClass) + method = try { + if (matchingParams) { + lookingInClass.getDeclaredMethod(methodName, *declarationParams) + } else { + lookingInClass.declaredMethods.firstOrNull { + it.name == methodName && + it.parameterTypes.size == declarationParams.size + } + } + } catch (e: Throwable) { + null + } + + val superclass = lookingInClass.superclass + if (superclass != null && + !classesToSearch.contains(superclass) && + !classesSearched.contains(superclass) + ) { + classesToSearch.add(superclass) + } + lookingInClass.interfaces.forEach { + if (!classesToSearch.contains(it) && !classesSearched.contains(it)) { + classesToSearch.add(it) + } + } + } while (method == null && classesToSearch.isNotEmpty()) + + checkNotNull(method) { + "Unable to access method $methodName on ${javaClass.canonicalName}" + } + + return method +} 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/package.json b/packages/react-native-webview/package.json index 8cf674def..e75262de5 100644 --- a/packages/react-native-webview/package.json +++ b/packages/react-native-webview/package.json @@ -23,7 +23,11 @@ "main": "lib/commonjs/index", "files": [ "src/**", - "lib/**" + "lib/**", + "android/build.gradle", + "android/detekt.yml", + "android/gradle.properties", + "android/src/**" ], "types": "lib/typescript/index.d.ts", "react-native": "src/index", @@ -39,7 +43,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 +80,13 @@ } ] ] + }, + "codegenConfig": { + "name": "DdSdkReactNativeWebView", + "type": "all", + "jsSrcsDir": "./src/specs", + "android": { + "javaPackageName": "com.datadog.reactnative.webview" + } } } diff --git a/packages/react-native-webview/src/NativeDdSdk.ts b/packages/react-native-webview/src/NativeDdSdk.ts index 7ebc0e3b9..0ef4c14dd 100644 --- a/packages/react-native-webview/src/NativeDdSdk.ts +++ b/packages/react-native-webview/src/NativeDdSdk.ts @@ -12,9 +12,9 @@ import { TurboModuleRegistry } from 'react-native'; * We don't declare it in a spec file so we don't end up with a duplicate definition of the native module. */ 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/__tests__/WebviewDatadog.test.tsx b/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx index 6385712c9..eab46e21b 100644 --- a/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx +++ b/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx @@ -3,62 +3,10 @@ * 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 React from 'react'; - -import { DATADOG_MESSAGE_PREFIX } from '../__utils__/getInjectedJavaScriptBeforeContentLoaded'; -import { WebView } from '../index'; - 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'); - fireEvent(webView, 'message', userDefinedEvent); - expect(onMessage).toHaveBeenCalledWith(userDefinedEvent); - - 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); - - expect(NativeModules.DdSdk.consumeWebviewEvent).toHaveBeenCalledWith( - DdMessage - ); - }); - 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('Dummy Test', () => {}); }); diff --git a/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts b/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts index b0b07e01b..f2d4da820 100644 --- a/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts +++ b/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts @@ -4,37 +4,31 @@ * Copyright 2016-Present Datadog, Inc. */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { NativeModules } from 'react-native'; -import { formatAllowedHosts } from '../__utils__/formatAllowedHosts'; +import { wrapJsCodeInTryAndCatch } from '../utils/format-utils'; -describe('Format allowed hosts', () => { +describe('Format Utils', () => { 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("'[]'"); + + describe('Wrap JS Code in Try & Catch', () => { + it('returns the JS code wrapped in try and catch', () => { + const jsCode = "console.log('test')"; + const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); + expect(wrappedCode).toBe(` + 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; + }`); + }); }); }); 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/__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/index.tsx b/packages/react-native-webview/src/index.tsx index b99750ec0..e86984a31 100644 --- a/packages/react-native-webview/src/index.tsx +++ b/packages/react-native-webview/src/index.tsx @@ -7,38 +7,71 @@ 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 { - DATADOG_MESSAGE_PREFIX, - getInjectedJavaScriptBeforeContentLoaded -} from './__utils__/getInjectedJavaScriptBeforeContentLoaded'; +import { NativeDdWebView } from './specs/NativeDdWebView'; +import type { DatadogMessageFormat } from './utils/format-utils'; +import { wrapJsCodeInTryAndCatch } from './utils/format-utils'; 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) + ) { + // TODO: Log Error through Turbo Registry native call. + // DdLogs.error(ddMessage.message, 'USER_CODE_WEBVIEW_ERROR'); + } + }; + 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] ); + return ( ( + 'DdReactNativeWebView' +); + +export { NativeDdWebView }; diff --git a/packages/react-native-webview/src/utils/format-utils.ts b/packages/react-native-webview/src/utils/format-utils.ts new file mode 100644 index 000000000..228cd062e --- /dev/null +++ b/packages/react-native-webview/src/utils/format-utils.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +/** + * 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 type DatadogMessageFormat = { + type: 'ERROR'; + message: string; +}; + +export const wrapJsCodeInTryAndCatch = (javascriptCode?: string): string => + ` + 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; + }`; diff --git a/yarn.lock b/yarn.lock index 4070813de..00880c4a1 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" @@ -8531,7 +8532,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 +9256,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 +9263,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 +15650,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 From 26fb6c9b595c79e2fd7b5bf025ec38f2b0ec966c Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 25 Sep 2024 15:01:38 +0200 Subject: [PATCH 02/12] Android: refactoring and WebView unit tests --- .../android/settings.gradle | 2 +- .../webview/DdSdkReactNativeWebViewManager.kt | 98 +++++++------- .../webview/DdSdkReactNativeWebViewPackage.kt | 6 +- .../reactnative/webview/DatadogWebViewTest.kt | 120 ++++++++++++++++++ 4 files changed, 175 insertions(+), 51 deletions(-) create mode 100644 packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt diff --git a/packages/react-native-webview/android/settings.gradle b/packages/react-native-webview/android/settings.gradle index a75f26fd3..8868a8667 100644 --- a/packages/react-native-webview/android/settings.gradle +++ b/packages/react-native-webview/android/settings.gradle @@ -4,4 +4,4 @@ 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') \ No newline at end of file +project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-webview/android') \ No newline at end of file diff --git a/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt b/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt index 998da927d..4bd0a42e5 100644 --- a/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt +++ b/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt @@ -23,56 +23,21 @@ import com.reactnativecommunity.webview.RNCWebViewWrapper /** * The entry point to use Datadog auto-instrumented WebView feature. */ -class DdSdkReactNativeWebViewManager(private val reactContext: ReactContext) : RNCWebViewManager() { +class DdSdkReactNativeWebViewManager( + private val reactContext: ReactContext +) : RNCWebViewManager() { // Custom WebView Client private class DatadogWebViewClient : RNCWebViewClient() // Custom WebView @SuppressLint("SetJavaScriptEnabled") - private class DatadogWebView(reactContext: ThemedReactContext) : RNCWebView(reactContext) { - @Volatile - var isWebViewTrackingEnabled: Boolean = false - + private class DatadogWebView( + reactContext: ThemedReactContext + ) : RNCWebView(reactContext) { init { // JavaScript has to be enabled for auto-instrumentation. this.settings.javaScriptEnabled = true } - - /** - * Enables Datadog tracking on the WebView. - * @param allowedHosts the list of allowed hosts - */ - fun enableTracking( - reactContext: ReactContext, - sdkCore: SdkCore?, - allowedHosts: List - ) { - if (sdkCore != null) { - enableWebViewTracking(reactContext, sdkCore, allowedHosts) - } else { - DatadogSDKWrapperStorage.addOnInitializedListener { core -> - enableWebViewTracking(reactContext, core, allowedHosts) - } - } - } - - private fun enableWebViewTracking( - reactContext: ReactContext, - sdkCore: SdkCore, - allowedHosts: List - ) { - if (isWebViewTrackingEnabled) { - return - } - - WebViewTracking.enable( - this, - allowedHosts = allowedHosts, - sdkCore = sdkCore - ) - - isWebViewTrackingEnabled = true - } } // The name used to reference this custom View from React Native. @@ -83,23 +48,39 @@ class DdSdkReactNativeWebViewManager(private val reactContext: ReactContext) : R /** * The instance of Datadog SDK Core. */ - @Volatile - private var datadogCore: SdkCore? = null + @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 + _datadogCore = core } } // The Custom WebView exposed properties. @ReactProp(name = "allowedHosts") fun setAllowedHosts(view: RNCWebViewWrapper, allowedHosts: ReadableArray) { - (view.webView as DatadogWebView).enableTracking( - reactContext, - datadogCore, - toStringList(allowedHosts) - ) + // TODO: 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) + } + } + } } // Overrides the default ViewInstance by binding the CustomWebView to it. @@ -120,6 +101,25 @@ class DdSdkReactNativeWebViewManager(private val reactContext: ReactContext) : R 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/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt b/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt index f15f6eed6..0d57e8fe5 100644 --- a/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt +++ b/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt @@ -6,6 +6,8 @@ package com.datadog.reactnative.webview +import com.datadog.android.webview.WebViewTracking +import com.datadog.reactnative.DatadogSDKWrapperStorage import com.facebook.react.TurboReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext @@ -17,7 +19,9 @@ class DdSdkReactNativeWebViewPackage : TurboReactPackage() { override fun createViewManagers( reactContext: ReactApplicationContext ): MutableList> { - return mutableListOf(DdSdkReactNativeWebViewManager(reactContext)) + return mutableListOf(DdSdkReactNativeWebViewManager( + reactContext + )) } override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { 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..bc8ba3063 --- /dev/null +++ b/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/webview/DatadogWebViewTest.kt @@ -0,0 +1,120 @@ +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.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) + } +} From 9b11e84d941582fc3a31bcc423a598058e4f4be9 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 2 Oct 2024 11:57:57 +0200 Subject: [PATCH 03/12] iOS: Native Datadog WebView Component --- .../DdSdkReactNativeExample.xcscheme | 10 + example/ios/Podfile | 1 + example/ios/Podfile.lock | 44 ++- example/package.json | 3 +- .../DatadogSDKReactNativeWebView.podspec | 46 +++ .../project.pbxproj | 272 +++++++++++++++++ .../contents.xcworkspacedata | 4 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../Sources/DatadogSDKReactNativeWebView.h | 12 + .../ios/Sources/RCTDatadogWebView.h | 21 ++ .../ios/Sources/RCTDatadogWebView.m | 53 ++++ .../ios/Sources/RCTDatadogWebViewManager.h | 10 + .../ios/Sources/RCTDatadogWebViewManager.m | 84 ++++++ .../Sources/RCTDatadogWebViewTracking.swift | 67 +++++ .../DatadogSDKReactNativeWebViewTests.swift | 275 ++++++++++++++++++ packages/react-native-webview/package.json | 7 +- yarn.lock | 1 + 17 files changed, 915 insertions(+), 3 deletions(-) create mode 100644 packages/react-native-webview/DatadogSDKReactNativeWebView.podspec create mode 100644 packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.pbxproj create mode 100644 packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/react-native-webview/ios/DatadogSDKReactNativeWebView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/react-native-webview/ios/Sources/DatadogSDKReactNativeWebView.h create mode 100644 packages/react-native-webview/ios/Sources/RCTDatadogWebView.h create mode 100644 packages/react-native-webview/ios/Sources/RCTDatadogWebView.m create mode 100644 packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.h create mode 100644 packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.m create mode 100644 packages/react-native-webview/ios/Sources/RCTDatadogWebViewTracking.swift create mode 100644 packages/react-native-webview/ios/Tests/DatadogSDKReactNativeWebViewTests.swift 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/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/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.m b/packages/react-native-webview/ios/Sources/RCTDatadogWebView.m new file mode 100644 index 000000000..622fb5848 --- /dev/null +++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebView.m @@ -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.m b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.m new file mode 100644 index 000000000..b32305a9b --- /dev/null +++ b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.m @@ -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 e75262de5..0ac987f3b 100644 --- a/packages/react-native-webview/package.json +++ b/packages/react-native-webview/package.json @@ -27,7 +27,12 @@ "android/build.gradle", "android/detekt.yml", "android/gradle.properties", - "android/src/**" + "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", diff --git a/yarn.lock b/yarn.lock index 00880c4a1..1df83e457 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8495,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 From 1793a3301bb8893f4eb5d55a2cf7a945d765d3eb Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 14 Oct 2024 12:03:33 +0200 Subject: [PATCH 04/12] Log WebView User JS errors --- .../src/ext-specs/NativeDdLogs.ts | 25 +++++++++++++++++++ .../src/{ => ext-specs}/NativeDdSdk.ts | 0 packages/react-native-webview/src/index.tsx | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 packages/react-native-webview/src/ext-specs/NativeDdLogs.ts rename packages/react-native-webview/src/{ => ext-specs}/NativeDdSdk.ts (100%) 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 100% rename from packages/react-native-webview/src/NativeDdSdk.ts rename to packages/react-native-webview/src/ext-specs/NativeDdSdk.ts diff --git a/packages/react-native-webview/src/index.tsx b/packages/react-native-webview/src/index.tsx index e86984a31..b7473291c 100644 --- a/packages/react-native-webview/src/index.tsx +++ b/packages/react-native-webview/src/index.tsx @@ -7,6 +7,7 @@ import type { WebViewMessageEvent, WebViewProps } from 'react-native-webview'; import { WebView as RNWebView } from 'react-native-webview'; import React, { forwardRef, useCallback } from 'react'; +import NativeDdLogs from './ext-specs/NativeDdLogs'; import { NativeDdWebView } from './specs/NativeDdWebView'; import type { DatadogMessageFormat } from './utils/format-utils'; import { wrapJsCodeInTryAndCatch } from './utils/format-utils'; @@ -37,6 +38,7 @@ const WebViewComponent = (props: Props, ref: React.Ref>) => { ddMessage.message != null && (props.logUserCodeErrors ?? false) ) { + NativeDdLogs?.error(ddMessage.message, {}); // TODO: Log Error through Turbo Registry native call. // DdLogs.error(ddMessage.message, 'USER_CODE_WEBVIEW_ERROR'); } From be62b5b2d4c09c855db74a92bc15fb91d0312e2c Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Tue, 15 Oct 2024 13:52:51 +0200 Subject: [PATCH 05/12] Using Objective-C++ for native component --- .../ios/Sources/{RCTDatadogWebView.m => RCTDatadogWebView.mm} | 0 .../{RCTDatadogWebViewManager.m => RCTDatadogWebViewManager.mm} | 0 packages/react-native-webview/src/index.tsx | 2 -- 3 files changed, 2 deletions(-) rename packages/react-native-webview/ios/Sources/{RCTDatadogWebView.m => RCTDatadogWebView.mm} (100%) rename packages/react-native-webview/ios/Sources/{RCTDatadogWebViewManager.m => RCTDatadogWebViewManager.mm} (100%) diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebView.m b/packages/react-native-webview/ios/Sources/RCTDatadogWebView.mm similarity index 100% rename from packages/react-native-webview/ios/Sources/RCTDatadogWebView.m rename to packages/react-native-webview/ios/Sources/RCTDatadogWebView.mm diff --git a/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.m b/packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.mm similarity index 100% rename from packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.m rename to packages/react-native-webview/ios/Sources/RCTDatadogWebViewManager.mm diff --git a/packages/react-native-webview/src/index.tsx b/packages/react-native-webview/src/index.tsx index b7473291c..82950af19 100644 --- a/packages/react-native-webview/src/index.tsx +++ b/packages/react-native-webview/src/index.tsx @@ -39,8 +39,6 @@ const WebViewComponent = (props: Props, ref: React.Ref>) => { (props.logUserCodeErrors ?? false) ) { NativeDdLogs?.error(ddMessage.message, {}); - // TODO: Log Error through Turbo Registry native call. - // DdLogs.error(ddMessage.message, 'USER_CODE_WEBVIEW_ERROR'); } }; From 90e1d1773182a7348430c7686382844201a1f66a Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 16 Oct 2024 10:40:18 +0200 Subject: [PATCH 06/12] Wrap injectedJavaScript user code in try & catch --- packages/react-native-webview/src/index.tsx | 3 +++ .../react-native-webview/src/utils/format-utils.ts | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/react-native-webview/src/index.tsx b/packages/react-native-webview/src/index.tsx index 82950af19..f0ff965d2 100644 --- a/packages/react-native-webview/src/index.tsx +++ b/packages/react-native-webview/src/index.tsx @@ -71,6 +71,9 @@ const WebViewComponent = (props: Props, ref: React.Ref>) => { allowedHosts: props.allowedHosts } }} + injectedJavaScript={wrapJsCodeInTryAndCatch( + props.injectedJavaScript + )} injectedJavaScriptBeforeContentLoaded={wrapJsCodeInTryAndCatch( props.injectedJavaScriptBeforeContentLoaded )} diff --git a/packages/react-native-webview/src/utils/format-utils.ts b/packages/react-native-webview/src/utils/format-utils.ts index 228cd062e..2b7241f32 100644 --- a/packages/react-native-webview/src/utils/format-utils.ts +++ b/packages/react-native-webview/src/utils/format-utils.ts @@ -14,8 +14,11 @@ export type DatadogMessageFormat = { message: string; }; -export const wrapJsCodeInTryAndCatch = (javascriptCode?: string): string => - ` +export const wrapJsCodeInTryAndCatch = ( + javascriptCode?: string +): string | undefined => + javascriptCode + ? ` try{ ${javascriptCode} } @@ -27,4 +30,5 @@ export const wrapJsCodeInTryAndCatch = (javascriptCode?: string): string => message: errorMsg })); true; - }`; + }` + : undefined; From 62d4bb8fefe634ad813faac22b32daeae88ecb13 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 16 Oct 2024 10:40:52 +0200 Subject: [PATCH 07/12] Added WebView RN unit tests --- .../src/__tests__/WebviewDatadog.test.tsx | 50 ++++- .../WebviewDatadogInjectedJS.test.tsx | 201 +++++++++++++++++- .../WebviewDatadogPerformance.test.tsx | 12 ++ .../src/__tests__/__utils__/string-utils.ts | 13 ++ .../src/__tests__/format-utils.test.ts | 51 +++++ .../src/__tests__/formatAllowedHosts.test.ts | 34 --- 6 files changed, 325 insertions(+), 36 deletions(-) create mode 100644 packages/react-native-webview/src/__tests__/__utils__/string-utils.ts create mode 100644 packages/react-native-webview/src/__tests__/format-utils.test.ts delete mode 100644 packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts diff --git a/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx b/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx index eab46e21b..073a4317b 100644 --- a/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx +++ b/packages/react-native-webview/src/__tests__/WebviewDatadog.test.tsx @@ -3,10 +3,58 @@ * 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 { 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(); }); - it('Dummy Test', () => {}); + it('WebView is rendered with native component', () => { + // Given + const allowedHosts = ['example.com', 'localhost']; + + // When + render(); + + // Then + const mockedWebView = jest.mocked(RNWebView); + const component = + mockedWebView.mock.calls[0][0].nativeConfig?.component; + const mockedNativeWebView = jest.mocked(NativeDdWebView); + + expect(component).toBe(mockedNativeWebView); + }); + + 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__/format-utils.test.ts b/packages/react-native-webview/src/__tests__/format-utils.test.ts new file mode 100644 index 000000000..508d40b2b --- /dev/null +++ b/packages/react-native-webview/src/__tests__/format-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/format-utils'; + +import { dedent } from './__utils__/string-utils'; + +describe('Format 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/__tests__/formatAllowedHosts.test.ts b/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts deleted file mode 100644 index f2d4da820..000000000 --- a/packages/react-native-webview/src/__tests__/formatAllowedHosts.test.ts +++ /dev/null @@ -1,34 +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 { wrapJsCodeInTryAndCatch } from '../utils/format-utils'; - -describe('Format Utils', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Wrap JS Code in Try & Catch', () => { - it('returns the JS code wrapped in try and catch', () => { - const jsCode = "console.log('test')"; - const wrappedCode = wrapJsCodeInTryAndCatch(jsCode); - expect(wrappedCode).toBe(` - 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; - }`); - }); - }); -}); From d6142c2125300de8f916a4fe7c503295fa983a0b Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Thu, 14 Nov 2024 11:33:55 +0100 Subject: [PATCH 08/12] Android: Legacy WebView tracking support for new architecture --- packages/core/android/build.gradle | 2 +- .../android/build.gradle | 2 +- .../android/build.gradle | 2 +- .../react-native-webview/android/build.gradle | 12 +- .../android/settings.gradle | 2 +- .../webview/DdSdkReactNativeWebViewPackage.kt | 9 +- .../webview/DdSdkReactNativeWebViewManager.kt | 29 +--- .../webview/DdSdkReactNativeWebViewPackage.kt | 36 +++++ ...utils.test.ts => webview-js-utils.test.ts} | 4 +- .../src/ext-specs/NativeDdSdk.ts | 1 + packages/react-native-webview/src/index.tsx | 31 +++- .../src/specs/NativeDdWebView.ts | 6 +- .../src/utils/env-utils.ts | 9 ++ .../src/utils/format-utils.ts | 34 ---- .../src/utils/webview-js-utils.ts | 150 ++++++++++++++++++ 15 files changed, 253 insertions(+), 76 deletions(-) rename packages/react-native-webview/android/src/{main/kotlin => newarch}/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt (84%) rename packages/react-native-webview/android/src/{main/kotlin => oldarch}/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt (81%) create mode 100644 packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt rename packages/react-native-webview/src/__tests__/{format-utils.test.ts => webview-js-utils.test.ts} (93%) create mode 100644 packages/react-native-webview/src/utils/env-utils.ts delete mode 100644 packages/react-native-webview/src/utils/format-utils.ts create mode 100644 packages/react-native-webview/src/utils/webview-js-utils.ts diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 3883de8f5..16f6718b8 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -122,7 +122,7 @@ android { } defaultConfig { - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" diff --git a/packages/internal-testing-tools/android/build.gradle b/packages/internal-testing-tools/android/build.gradle index 22b4be9c2..6bdfbeafe 100644 --- a/packages/internal-testing-tools/android/build.gradle +++ b/packages/internal-testing-tools/android/build.gradle @@ -117,7 +117,7 @@ android { } defaultConfig { - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index 8d833e098..fa42afabb 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -120,7 +120,7 @@ android { } defaultConfig { - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index aa691fcb8..61ea0e638 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -116,7 +116,7 @@ android { } defaultConfig { - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" @@ -124,6 +124,14 @@ android { } sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += ['src/newarch'] + } else { + java.srcDirs += ['src/oldarch'] + } + } + test { java.srcDir("src/test/kotlin") } @@ -178,7 +186,7 @@ dependencies { // 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 - api "com.facebook.react:react-android:$reactNativeVersion" + implementation "com.facebook.react:react-android:$reactNativeVersion" } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "com.datadoghq:dd-sdk-android-webview:2.11.0" diff --git a/packages/react-native-webview/android/settings.gradle b/packages/react-native-webview/android/settings.gradle index 8868a8667..f23344bcd 100644 --- a/packages/react-native-webview/android/settings.gradle +++ b/packages/react-native-webview/android/settings.gradle @@ -4,4 +4,4 @@ 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') \ No newline at end of file +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/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt similarity index 84% rename from packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt rename to packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt index 0d57e8fe5..ada4cc97c 100644 --- a/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt +++ b/packages/react-native-webview/android/src/newarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewPackage.kt @@ -6,22 +6,21 @@ package com.datadog.reactnative.webview -import com.datadog.android.webview.WebViewTracking -import com.datadog.reactnative.DatadogSDKWrapperStorage 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(DdSdkReactNativeWebViewManager( - reactContext - )) + return mutableListOf( + RNCWebViewManager() + ) } override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { diff --git a/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt similarity index 81% rename from packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt rename to packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt index 4bd0a42e5..911ccb989 100644 --- a/packages/react-native-webview/android/src/main/kotlin/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt +++ b/packages/react-native-webview/android/src/oldarch/com/datadog/reactnative/webview/DdSdkReactNativeWebViewManager.kt @@ -19,27 +19,12 @@ 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() { - // Custom WebView Client - private class DatadogWebViewClient : RNCWebViewClient() - - // Custom WebView - @SuppressLint("SetJavaScriptEnabled") - private class DatadogWebView( - reactContext: ThemedReactContext - ) : RNCWebView(reactContext) { - init { - // JavaScript has to be enabled for auto-instrumentation. - this.settings.javaScriptEnabled = true - } - } - // The name used to reference this custom View from React Native. companion object { const val VIEW_NAME = "DdReactNativeWebView" @@ -68,7 +53,7 @@ class DdSdkReactNativeWebViewManager( // The Custom WebView exposed properties. @ReactProp(name = "allowedHosts") fun setAllowedHosts(view: RNCWebViewWrapper, allowedHosts: ReadableArray) { - // TODO: Log failures w Telemetry + // TODO: RUM-7218 (Log failures w Telemetry) val webView = view.webView as? RNCWebView ?: return val datadogCore = _datadogCore val hosts = toStringList(allowedHosts) @@ -83,20 +68,20 @@ class DdSdkReactNativeWebViewManager( } } - // Overrides the default ViewInstance by binding the CustomWebView to it. + @SuppressLint("SetJavaScriptEnabled") override fun createViewInstance(context: ThemedReactContext): RNCWebViewWrapper { - return super.createViewInstance(context, DatadogWebView(context)) + val webView = RNCWebView(context) + webView.settings.javaScriptEnabled = true + return super.createViewInstance(context, webView) } - // Attaches our custom WebView client to the WebView. override fun addEventEmitters( reactContext: ThemedReactContext, view: RNCWebViewWrapper ) { - view.webView.webViewClient = DatadogWebViewClient() + view.webView.webViewClient = RNCWebViewClient() } - // Utility function for converting the ReadableArray to a list of strings. private fun toStringList(props: ReadableArray): List { return props.toArrayList().filterIsInstance() } @@ -124,4 +109,4 @@ class DdSdkReactNativeWebViewManager( override fun getName(): String { return VIEW_NAME } -} \ No newline at end of file +} 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/src/__tests__/format-utils.test.ts b/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts similarity index 93% rename from packages/react-native-webview/src/__tests__/format-utils.test.ts rename to packages/react-native-webview/src/__tests__/webview-js-utils.test.ts index 508d40b2b..b09ea8dbc 100644 --- a/packages/react-native-webview/src/__tests__/format-utils.test.ts +++ b/packages/react-native-webview/src/__tests__/webview-js-utils.test.ts @@ -4,11 +4,11 @@ * Copyright 2016-Present Datadog, Inc. */ -import { wrapJsCodeInTryAndCatch } from '../utils/format-utils'; +import { wrapJsCodeInTryAndCatch } from '../utils/webview-js-utils'; import { dedent } from './__utils__/string-utils'; -describe('Format Utils', () => { +describe('WebView JS Utils', () => { beforeEach(() => { jest.clearAllMocks(); }); diff --git a/packages/react-native-webview/src/ext-specs/NativeDdSdk.ts b/packages/react-native-webview/src/ext-specs/NativeDdSdk.ts index 0ef4c14dd..e31acf242 100644 --- a/packages/react-native-webview/src/ext-specs/NativeDdSdk.ts +++ b/packages/react-native-webview/src/ext-specs/NativeDdSdk.ts @@ -12,6 +12,7 @@ import { TurboModuleRegistry } from 'react-native'; * We don't declare it in a spec file so we don't end up with a duplicate definition of the native module. */ export interface PartialNativeDdSdkSpec extends TurboModule { + consumeWebviewEvent(message: string): Promise; telemetryError(message: string, stack: string, kind: string): Promise; } diff --git a/packages/react-native-webview/src/index.tsx b/packages/react-native-webview/src/index.tsx index f0ff965d2..8b0b67b97 100644 --- a/packages/react-native-webview/src/index.tsx +++ b/packages/react-native-webview/src/index.tsx @@ -8,9 +8,14 @@ import { WebView as RNWebView } from 'react-native-webview'; import React, { forwardRef, useCallback } from 'react'; import NativeDdLogs from './ext-specs/NativeDdLogs'; +import { NativeDdSdk } from './ext-specs/NativeDdSdk'; +import { + 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'; -import type { DatadogMessageFormat } from './utils/format-utils'; -import { wrapJsCodeInTryAndCatch } from './utils/format-utils'; type Props = WebViewProps & { /** @@ -39,6 +44,11 @@ const WebViewComponent = (props: Props, ref: React.Ref>) => { (props.logUserCodeErrors ?? false) ) { NativeDdLogs?.error(ddMessage.message, {}); + } else if ( + ddMessage.type === 'NATIVE_EVENT' && + ddMessage.message != null + ) { + NativeDdSdk?.consumeWebviewEvent(ddMessage.message); } }; @@ -61,6 +71,19 @@ const WebViewComponent = (props: Props, ref: React.Ref>) => { [userDefinedOnMessage, props.logUserCodeErrors] ); + const getInjectedJavascriptBeforeContentLoaded = (): string | undefined => { + if (isNewArchitecture()) { + return getWebViewEventBridgingJS( + props.allowedHosts, + props.injectedJavaScriptBeforeContentLoaded + ); + } else { + return wrapJsCodeInTryAndCatch( + props.injectedJavaScriptBeforeContentLoaded + ); + } + }; + return ( >) => { injectedJavaScript={wrapJsCodeInTryAndCatch( props.injectedJavaScript )} - injectedJavaScriptBeforeContentLoaded={wrapJsCodeInTryAndCatch( - props.injectedJavaScriptBeforeContentLoaded - )} + injectedJavaScriptBeforeContentLoaded={getInjectedJavascriptBeforeContentLoaded()} ref={ref} /> ); diff --git a/packages/react-native-webview/src/specs/NativeDdWebView.ts b/packages/react-native-webview/src/specs/NativeDdWebView.ts index a3c3ed28a..ae380a511 100644 --- a/packages/react-native-webview/src/specs/NativeDdWebView.ts +++ b/packages/react-native-webview/src/specs/NativeDdWebView.ts @@ -6,9 +6,11 @@ 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 = requireNativeComponent( + +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/format-utils.ts b/packages/react-native-webview/src/utils/format-utils.ts deleted file mode 100644 index 2b7241f32..000000000 --- a/packages/react-native-webview/src/utils/format-utils.ts +++ /dev/null @@ -1,34 +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. - */ - -/** - * 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 type DatadogMessageFormat = { - type: 'ERROR'; - message: string; -}; - -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; 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; +}; From 1ef6848104d6a3369de762c62bff8da10c28d9c7 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 15 Nov 2024 09:32:34 +0100 Subject: [PATCH 09/12] Android: remove unused test utils --- .../reactnative/tools/unit/GenericAssert.kt | 53 +--- .../reactnative/tools/unit/ReflectUtils.kt | 266 ------------------ .../reactnative/webview/DatadogWebViewTest.kt | 2 +- 3 files changed, 2 insertions(+), 319 deletions(-) delete mode 100644 packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt 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 index 71b045169..8248b6224 100644 --- a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt +++ b/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt @@ -4,63 +4,12 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.tools.unit +package com.datadog.reactnative.tools.unit import org.assertj.core.api.AbstractAssert -import org.assertj.core.api.Assertions.assertThat class GenericAssert(actual: Any?) : AbstractAssert(actual, GenericAssert::class.java) { - - fun doesNotHaveField(name: String): GenericAssert { - val field: Any? = actual.getFieldValue(name) - assertThat(field) - .overridingErrorMessage( - "Expecting object to not have $name, but found it having value $field" - ) - .isNull() - return this - } - - fun getActualValue(name: String): T { - val field: Any? = actual.getFieldValue(name) - assertThat(field) - .overridingErrorMessage( - "Expecting object to have a non null field named $name, but field was null" - ) - .isNotNull() - return field!! as T - } - - fun hasField(name: String, nestedAssert: (GenericAssert) -> Unit = {}): GenericAssert { - val field: Any? = actual.getFieldValue(name) - assertThat(field) - .overridingErrorMessage( - "Expecting object to have a non null field named $name, but field was null" - ) - .isNotNull() - nestedAssert(GenericAssert(field!!)) - return this - } - - fun hasFieldEqualTo(name: String, expected: F): GenericAssert { - val field: Any? = actual.getFieldValue(name) - assertThat(field).isEqualTo(expected) - return this - } - - fun hasFieldWithClass(name: String, expectedClassName: String): GenericAssert { - val field: Any? = actual.getFieldValue(name) - assertThat(field?.javaClass?.name).isEqualTo(expectedClassName) - return this - } - - fun isInstanceOf(expectedClassName: String): GenericAssert { - val className = actual.javaClass.canonicalName!! - assertThat(className).isEqualTo(expectedClassName) - return this - } - companion object { fun assertThat(actual: Any?): GenericAssert { return GenericAssert(actual) diff --git a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt b/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt deleted file mode 100644 index 49beb4465..000000000 --- a/packages/react-native-webview/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt +++ /dev/null @@ -1,266 +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. - */ - -package com.datadog.tools.unit - -import java.lang.reflect.Field -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.Method -import java.lang.reflect.Modifier -import java.util.LinkedList -import kotlin.reflect.jvm.isAccessible - -/** - * Creates an instance of the given class name. - * @param className the full name of the class to instantiate - * @param params the parameters to provide the constructor - * @return the created instance - */ -@Suppress("SpreadOperator") -fun createInstance( - className: String, - vararg params: Any? -): Any { - return Class.forName(className) - .kotlin - .constructors.first() - .apply { isAccessible = true } - .call(*params) -} - -/** - * Sets a static value on the target class. - * @param fieldName the name of the field - * @param fieldValue the value to set - */ -@Suppress("SwallowedException") -inline fun Class.setStaticValue( - fieldName: String, - fieldValue: R -) { - val field = getDeclaredField(fieldName) - - // make it accessible - field.isAccessible = true - - // Make it non final - try { - val modifiersField = Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true - modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) - } catch (e: NoSuchFieldException) { - // do nothing - @Suppress("PrintStackTrace") - e.printStackTrace() - } - field.set(null, fieldValue) -} - -/** - * Gets the static value from the target class. - * @param className the full name of the class - * @param fieldName the name of the field - */ -inline fun getStaticValue( - className: String, - fieldName: String -): R { - val clazz = Class.forName(className) - val field = clazz.getDeclaredField(fieldName) - // make it accessible - field.isAccessible = true - - return field.get(null) as R -} - -/** - * Gets the static value from the target class. - * @param fieldName the name of the field - */ -inline fun Class.getStaticValue(fieldName: String): R { - val field = getDeclaredField(fieldName) - - // make it accessible - field.isAccessible = true - - return field.get(null) as R -} - -/** - * Sets the field value on the target instance. - * @param fieldName the name of the field - * @param fieldValue the value of the field - */ -@Suppress("SwallowedException") -inline fun Any.setFieldValue( - fieldName: String, - fieldValue: T -): Boolean { - var field: Field? = null - val classesToSearch = LinkedList>() - classesToSearch.add(this.javaClass) - val classesSearched = mutableSetOf>() - - while (field == null && classesToSearch.isNotEmpty()) { - val toSearchIn = classesToSearch.remove() - try { - field = toSearchIn.getDeclaredField(fieldName) - } catch (e: NoSuchFieldException) { - // do nothing - } - classesSearched.add(toSearchIn) - toSearchIn.superclass?.let { - if (!classesSearched.contains(it)) { - classesToSearch.add(it) - } - } - } - - // make it accessible - if (field != null) { - field.isAccessible = true - - // Make it non final - val modifiersField = Field::class.java.getDeclaredField("modifiers") - modifiersField.isAccessible = true - modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv()) - - field.set(this, fieldValue) - return true - } else { - return false - } -} - -/** - * Gets the field value from the target instance. - * @param fieldName the name of the field - */ -inline fun R.getFieldValue( - fieldName: String, - enclosingClass: Class = this.javaClass -): T { - val field = enclosingClass.getDeclaredField(fieldName) - field.isAccessible = true - return field.get(this) as T -} - -/** - * Invokes a method on the target instance. - * @param methodName the name of the method - * @param params the parameters to provide the method - * @return the result from the invoked method - */ -@Suppress("SpreadOperator", "UNCHECKED_CAST", "TooGenericExceptionCaught") -fun T.invokeMethod( - methodName: String, - vararg params: Any? -): Any? { - val declarationParams = Array?>(params.size) { - params[it]?.javaClass - } - - val method = getDeclaredMethodRecursively(methodName, true, declarationParams) - val wasAccessible = method.isAccessible - - val output: Any? - method.isAccessible = true - try { - output = if (params.isEmpty()) { - method.invoke(this) - } else { - method.invoke(this, *params) - } - } catch (e: InvocationTargetException) { - throw e.cause ?: e - } finally { - method.isAccessible = wasAccessible - } - - return output -} - -/** - * Invokes a method on the target instance, where one or more of the parameters - * are generics. - * @param methodName the name of the method - * @param params the parameters to provide the method - * @return the result from the invoked method - */ -@Suppress("SpreadOperator", "UNCHECKED_CAST") -fun T.invokeGenericMethod( - methodName: String, - vararg params: Any -): Any? { - val declarationParams = Array?>(params.size) { - params[it].javaClass - } - - val method = getDeclaredMethodRecursively(methodName, false, declarationParams) - val wasAccessible = method.isAccessible - - val output: Any? - method.isAccessible = true - try { - output = if (params.isEmpty()) { - method.invoke(this) - } else { - method.invoke(this, *params) - } - } catch (e: InvocationTargetException) { - throw e.cause ?: e - } finally { - method.isAccessible = wasAccessible - } - - return output -} - -@Suppress("TooGenericExceptionCaught", "SwallowedException", "SpreadOperator") -private fun T.getDeclaredMethodRecursively( - methodName: String, - matchingParams: Boolean, - declarationParams: Array?> -): Method { - val classesToSearch = mutableListOf>(this.javaClass) - val classesSearched = mutableListOf>() - var method: Method? - do { - val lookingInClass = classesToSearch.removeAt(0) - classesSearched.add(lookingInClass) - method = try { - if (matchingParams) { - lookingInClass.getDeclaredMethod(methodName, *declarationParams) - } else { - lookingInClass.declaredMethods.firstOrNull { - it.name == methodName && - it.parameterTypes.size == declarationParams.size - } - } - } catch (e: Throwable) { - null - } - - val superclass = lookingInClass.superclass - if (superclass != null && - !classesToSearch.contains(superclass) && - !classesSearched.contains(superclass) - ) { - classesToSearch.add(superclass) - } - lookingInClass.interfaces.forEach { - if (!classesToSearch.contains(it) && !classesSearched.contains(it)) { - classesToSearch.add(it) - } - } - } while (method == null && classesToSearch.isNotEmpty()) - - checkNotNull(method) { - "Unable to access method $methodName on ${javaClass.canonicalName}" - } - - return method -} 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 index bc8ba3063..201038311 100644 --- 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 @@ -4,7 +4,7 @@ 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.tools.unit.GenericAssert.Companion.assertThat +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 From 0d36cd4a34b21c2ed7f1c3fafaf67b95c725af55 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 15 Nov 2024 11:51:21 +0100 Subject: [PATCH 10/12] Bumped dd-sdk-android-webview version --- packages/react-native-webview/android/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index 61ea0e638..e385278a9 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -188,8 +188,10 @@ dependencies { // 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 "com.datadoghq:dd-sdk-android-webview:2.11.0" + implementation project(path: ':datadog_mobile-react-native') implementation project(path: ':react-native-webview') From 82cab57c7780895dd82a57f50ca0029e1f8ddec3 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Fri, 15 Nov 2024 11:52:37 +0100 Subject: [PATCH 11/12] Android: fixed minSdkVersion to 21 --- example/android/build.gradle | 2 +- packages/core/android/build.gradle | 2 +- packages/internal-testing-tools/android/build.gradle | 2 +- packages/react-native-session-replay/android/build.gradle | 2 +- packages/react-native-webview/android/build.gradle | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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/packages/core/android/build.gradle b/packages/core/android/build.gradle index 16f6718b8..3883de8f5 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -122,7 +122,7 @@ android { } defaultConfig { - minSdkVersion 24 + minSdkVersion 21 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" diff --git a/packages/internal-testing-tools/android/build.gradle b/packages/internal-testing-tools/android/build.gradle index 6bdfbeafe..22b4be9c2 100644 --- a/packages/internal-testing-tools/android/build.gradle +++ b/packages/internal-testing-tools/android/build.gradle @@ -117,7 +117,7 @@ android { } defaultConfig { - minSdkVersion 24 + minSdkVersion 21 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" diff --git a/packages/react-native-session-replay/android/build.gradle b/packages/react-native-session-replay/android/build.gradle index fa42afabb..8d833e098 100644 --- a/packages/react-native-session-replay/android/build.gradle +++ b/packages/react-native-session-replay/android/build.gradle @@ -120,7 +120,7 @@ android { } defaultConfig { - minSdkVersion 24 + minSdkVersion 21 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" diff --git a/packages/react-native-webview/android/build.gradle b/packages/react-native-webview/android/build.gradle index e385278a9..6bf908d43 100644 --- a/packages/react-native-webview/android/build.gradle +++ b/packages/react-native-webview/android/build.gradle @@ -116,7 +116,7 @@ android { } defaultConfig { - minSdkVersion 24 + minSdkVersion 21 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" From 36c3d39dcd642d0f7e62bfbf19c5982e0d1f1682 Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Mon, 18 Nov 2024 10:34:28 +0100 Subject: [PATCH 12/12] Added missing copyright header --- .../com/datadog/reactnative/webview/DatadogWebViewTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 index 201038311..e9cf9ce03 100644 --- 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 @@ -1,3 +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. + */ + package com.datadog.reactnative.webview import com.datadog.android.api.SdkCore