From 44a906a5be4893c026bac70cc4423eef09300ebb Mon Sep 17 00:00:00 2001 From: Marius Constantin Date: Thu, 21 Dec 2023 13:32:06 +0100 Subject: [PATCH 01/15] RUM-2484 Provide the SR privacy level through the DatadogEventBridge --- .../android/webview/WebViewTracking.kt | 11 +++- .../webview/internal/DatadogEventBridge.kt | 12 +++- .../android/webview/WebViewTrackingTest.kt | 62 ++++++++++++++++++- .../internal/DatadogEventBridgeTest.kt | 29 +++++++-- 4 files changed, 106 insertions(+), 8 deletions(-) diff --git a/features/dd-sdk-android-webview/src/main/kotlin/com/datadog/android/webview/WebViewTracking.kt b/features/dd-sdk-android-webview/src/main/kotlin/com/datadog/android/webview/WebViewTracking.kt index f2216dddbb..308c104391 100644 --- a/features/dd-sdk-android-webview/src/main/kotlin/com/datadog/android/webview/WebViewTracking.kt +++ b/features/dd-sdk-android-webview/src/main/kotlin/com/datadog/android/webview/WebViewTracking.kt @@ -72,9 +72,13 @@ object WebViewTracking { { JAVA_SCRIPT_NOT_ENABLED_WARNING_MESSAGE } ) } - val webViewEventConsumer = buildWebViewEventConsumer(sdkCore as FeatureSdkCore, logsSampleRate) + val featureSdkCore = sdkCore as FeatureSdkCore + val featureContext = sdkCore.getFeatureContext(Feature.SESSION_REPLAY_FEATURE_NAME) + val privacyLevel = (featureContext[SESSION_REPLAY_PRIVACY_KEY] as? String) + ?: SESSION_REPLAY_MASK_ALL_PRIVACY + val webViewEventConsumer = buildWebViewEventConsumer(featureSdkCore, logsSampleRate) webView.addJavascriptInterface( - DatadogEventBridge(webViewEventConsumer, allowedHosts), + DatadogEventBridge(webViewEventConsumer, allowedHosts, privacyLevel), DATADOG_EVENT_BRIDGE_NAME ) } @@ -152,6 +156,9 @@ object WebViewTracking { } } + internal const val SESSION_REPLAY_PRIVACY_KEY = "session_replay_privacy" + internal const val SESSION_REPLAY_MASK_ALL_PRIVACY = "mask" + internal const val JAVA_SCRIPT_NOT_ENABLED_WARNING_MESSAGE = "You are trying to enable the WebView" + "tracking but the java script capability was not enabled for the given WebView." diff --git a/features/dd-sdk-android-webview/src/main/kotlin/com/datadog/android/webview/internal/DatadogEventBridge.kt b/features/dd-sdk-android-webview/src/main/kotlin/com/datadog/android/webview/internal/DatadogEventBridge.kt index 1d272acdce..4322c2d0a6 100644 --- a/features/dd-sdk-android-webview/src/main/kotlin/com/datadog/android/webview/internal/DatadogEventBridge.kt +++ b/features/dd-sdk-android-webview/src/main/kotlin/com/datadog/android/webview/internal/DatadogEventBridge.kt @@ -19,7 +19,8 @@ import com.google.gson.JsonArray */ internal class DatadogEventBridge( internal val webViewEventConsumer: WebViewEventConsumer, - private val allowedHosts: List + private val allowedHosts: List, + private val privacyLevel: String ) { // region Bridge @@ -51,6 +52,15 @@ internal class DatadogEventBridge( return origins.toString() } + /** + * Called from the browser-sdk to get the privacy level of the session replay feature. + * @return the privacy level as a String ("allow", "mask", "mask_user_input") + */ + @JavascriptInterface + fun getPrivacyLevel(): String { + return privacyLevel + } + // endregion companion object { diff --git a/features/dd-sdk-android-webview/src/test/kotlin/com/datadog/android/webview/WebViewTrackingTest.kt b/features/dd-sdk-android-webview/src/test/kotlin/com/datadog/android/webview/WebViewTrackingTest.kt index cd8d0d75e9..82d4885797 100644 --- a/features/dd-sdk-android-webview/src/test/kotlin/com/datadog/android/webview/WebViewTrackingTest.kt +++ b/features/dd-sdk-android-webview/src/test/kotlin/com/datadog/android/webview/WebViewTrackingTest.kt @@ -29,6 +29,7 @@ import com.datadog.android.webview.internal.storage.NoOpDataWriter import com.google.gson.JsonObject import fr.xgouchet.elmyr.Forge import fr.xgouchet.elmyr.annotation.Forgery +import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat @@ -97,7 +98,6 @@ internal class WebViewTrackingTest { whenever( mockCore.getFeature(Feature.LOGS_FEATURE_NAME) ) doReturn mockLogsFeatureScope - whenever( mockRumFeatureScope.unwrap() ) doReturn mockRumFeature @@ -135,6 +135,66 @@ internal class WebViewTrackingTest { ) } + @Test + fun `M extract and provide the SR privacy level W enable {privacy level provided}`( + @Forgery fakeUrls: List, + @StringForgery fakePrivacyLevel: String + ) { + // Given + val mockSrFeatureContext = mapOf( + WebViewTracking.SESSION_REPLAY_PRIVACY_KEY to fakePrivacyLevel + ) + whenever(mockCore.getFeatureContext(Feature.SESSION_REPLAY_FEATURE_NAME)) doReturn + mockSrFeatureContext + val fakeHosts = fakeUrls.map { it.host } + val mockSettings: WebSettings = mock { + whenever(it.javaScriptEnabled).thenReturn(true) + } + val mockWebView: WebView = mock { + whenever(it.settings).thenReturn(mockSettings) + } + val argumentCaptor = argumentCaptor() + + // When + WebViewTracking.enable(mockWebView, fakeHosts, sdkCore = mockCore) + + // Then + verify(mockWebView).addJavascriptInterface( + argumentCaptor.capture(), + eq(WebViewTracking.DATADOG_EVENT_BRIDGE_NAME) + ) + assertThat(argumentCaptor.firstValue.getPrivacyLevel()).isEqualTo(fakePrivacyLevel) + } + + @Test + fun `M used the default SR privacy level W enable {privacy level not provided}`( + @Forgery fakeUrls: List + ) { + // Given + val mockSrFeatureContext = mapOf() + whenever(mockCore.getFeatureContext(Feature.SESSION_REPLAY_FEATURE_NAME)) doReturn + mockSrFeatureContext + val fakeHosts = fakeUrls.map { it.host } + val mockSettings: WebSettings = mock { + whenever(it.javaScriptEnabled).thenReturn(true) + } + val mockWebView: WebView = mock { + whenever(it.settings).thenReturn(mockSettings) + } + val argumentCaptor = argumentCaptor() + + // When + WebViewTracking.enable(mockWebView, fakeHosts, sdkCore = mockCore) + + // Then + verify(mockWebView).addJavascriptInterface( + argumentCaptor.capture(), + eq(WebViewTracking.DATADOG_EVENT_BRIDGE_NAME) + ) + assertThat(argumentCaptor.firstValue.getPrivacyLevel()) + .isEqualTo(WebViewTracking.SESSION_REPLAY_MASK_ALL_PRIVACY) + } + @Test fun `M attach the bridge and send a warn log W enable { javascript not enabled }`( @Forgery fakeUrls: List diff --git a/features/dd-sdk-android-webview/src/test/kotlin/com/datadog/android/webview/internal/DatadogEventBridgeTest.kt b/features/dd-sdk-android-webview/src/test/kotlin/com/datadog/android/webview/internal/DatadogEventBridgeTest.kt index d891553203..6bd4cfae83 100644 --- a/features/dd-sdk-android-webview/src/test/kotlin/com/datadog/android/webview/internal/DatadogEventBridgeTest.kt +++ b/features/dd-sdk-android-webview/src/test/kotlin/com/datadog/android/webview/internal/DatadogEventBridgeTest.kt @@ -37,11 +37,15 @@ internal class DatadogEventBridgeTest { @Mock lateinit var mockWebViewEventConsumer: MixedWebViewEventConsumer + @StringForgery + lateinit var fakePrivacyLevel: String + @BeforeEach fun `set up`() { testedDatadogEventBridge = DatadogEventBridge( mockWebViewEventConsumer, - emptyList() + emptyList(), + fakePrivacyLevel ) } @@ -63,7 +67,7 @@ internal class DatadogEventBridgeTest { ) { // Given val expectedHosts = hosts.joinToString(",", prefix = "[", postfix = "]") { "\"$it\"" } - testedDatadogEventBridge = DatadogEventBridge(mock(), hosts) + testedDatadogEventBridge = DatadogEventBridge(mock(), hosts, fakePrivacyLevel) // When val allowedWebViewHosts = testedDatadogEventBridge.getAllowedWebViewHosts() @@ -81,7 +85,11 @@ internal class DatadogEventBridgeTest { ) { // Given val expectedHosts = hosts.joinToString(",", prefix = "[", postfix = "]") { "\"$it\"" } - testedDatadogEventBridge = DatadogEventBridge(mockWebViewEventConsumer, hosts) + testedDatadogEventBridge = DatadogEventBridge( + mockWebViewEventConsumer, + hosts, + fakePrivacyLevel + ) // When val allowedWebViewHosts = testedDatadogEventBridge.getAllowedWebViewHosts() @@ -99,7 +107,11 @@ internal class DatadogEventBridgeTest { // Given val expectedHosts = hosts.map { URL(it).host } .joinToString(",", prefix = "[", postfix = "]") { "\"$it\"" } - testedDatadogEventBridge = DatadogEventBridge(mockWebViewEventConsumer, hosts) + testedDatadogEventBridge = DatadogEventBridge( + mockWebViewEventConsumer, + hosts, + fakePrivacyLevel + ) // When val allowedWebViewHosts = testedDatadogEventBridge.getAllowedWebViewHosts() @@ -107,4 +119,13 @@ internal class DatadogEventBridgeTest { // Then assertThat(allowedWebViewHosts).isEqualTo(expectedHosts) } + + @Test + fun `M return the provided privacy level W getPrivacyLevel()`() { + // When + val privacyLevel = testedDatadogEventBridge.getPrivacyLevel() + + // Then + assertThat(privacyLevel).isEqualTo(fakePrivacyLevel) + } } From bf58d3ad8c9eadcb1df611afb488298732d9f03a Mon Sep 17 00:00:00 2001 From: Marius Constantin Date: Fri, 22 Dec 2023 09:59:08 +0100 Subject: [PATCH 02/15] RUM-2590 Add recorded WebView scenario into the sample app --- .../sessionreplay/SessionReplayFragment.kt | 1 + .../sessionreplay/WebViewRecordFragment.kt | 55 +++++++++++++++++++ .../res/layout/fragment_session_replay.xml | 10 ++++ .../fragment_webview_with_replay_support.xml | 29 ++++++++++ .../src/main/res/navigation/nav_graph.xml | 3 + sample/kotlin/src/main/res/values/strings.xml | 3 + 6 files changed, 101 insertions(+) create mode 100644 sample/kotlin/src/main/kotlin/com/datadog/android/sample/sessionreplay/WebViewRecordFragment.kt create mode 100644 sample/kotlin/src/main/res/layout/fragment_webview_with_replay_support.xml diff --git a/sample/kotlin/src/main/kotlin/com/datadog/android/sample/sessionreplay/SessionReplayFragment.kt b/sample/kotlin/src/main/kotlin/com/datadog/android/sample/sessionreplay/SessionReplayFragment.kt index 9a91c0063a..4f26167117 100644 --- a/sample/kotlin/src/main/kotlin/com/datadog/android/sample/sessionreplay/SessionReplayFragment.kt +++ b/sample/kotlin/src/main/kotlin/com/datadog/android/sample/sessionreplay/SessionReplayFragment.kt @@ -62,6 +62,7 @@ internal class SessionReplayFragment : R.id.navigation_unsupported_views -> R.id.fragment_unsupported_views R.id.navigation_image_components -> R.id.fragment_image_components R.id.navigation_image_scaling -> R.id.fragment_image_scaling + R.id.navigation_webview_recording -> R.id.fragment_webview_record else -> null } diff --git a/sample/kotlin/src/main/kotlin/com/datadog/android/sample/sessionreplay/WebViewRecordFragment.kt b/sample/kotlin/src/main/kotlin/com/datadog/android/sample/sessionreplay/WebViewRecordFragment.kt new file mode 100644 index 0000000000..13fc756ebb --- /dev/null +++ b/sample/kotlin/src/main/kotlin/com/datadog/android/sample/sessionreplay/WebViewRecordFragment.kt @@ -0,0 +1,55 @@ +/* + * 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.android.sample.sessionreplay + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Button +import androidx.fragment.app.Fragment +import com.datadog.android.rum.GlobalRumMonitor +import com.datadog.android.sample.R +import com.datadog.android.webview.WebViewTracking + +internal class WebViewRecordFragment : Fragment() { + + private lateinit var webView: WebView + private lateinit var startCustomRumViewButton: Button + private val webViewTrackingHosts = listOf( + "datadoghq.dev" + ) + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate( + R.layout.fragment_webview_with_replay_support, + container, + false + ) + startCustomRumViewButton = rootView.findViewById(R.id.start_custom_rum_view_button) + webView = rootView.findViewById(R.id.webview) + webView.webViewClient = WebViewClient() + webView.settings.javaScriptEnabled = true + WebViewTracking.enable(webView, webViewTrackingHosts) + startCustomRumViewButton.setOnClickListener { + GlobalRumMonitor.get().startView(this, "Custom RUM View") + } + return rootView + } + + override fun onResume() { + super.onResume() + webView.loadUrl("https://datadoghq.dev/browser-sdk-test-playground/webview-support/#click_event") + } +} diff --git a/sample/kotlin/src/main/res/layout/fragment_session_replay.xml b/sample/kotlin/src/main/res/layout/fragment_session_replay.xml index bd8e14ba03..5f3aed2da5 100644 --- a/sample/kotlin/src/main/res/layout/fragment_session_replay.xml +++ b/sample/kotlin/src/main/res/layout/fragment_session_replay.xml @@ -120,5 +120,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/navigation_image_components"/> + + diff --git a/sample/kotlin/src/main/res/layout/fragment_webview_with_replay_support.xml b/sample/kotlin/src/main/res/layout/fragment_webview_with_replay_support.xml new file mode 100644 index 0000000000..18b9c36a65 --- /dev/null +++ b/sample/kotlin/src/main/res/layout/fragment_webview_with_replay_support.xml @@ -0,0 +1,29 @@ + + + + + + +