@datadog/mobile-react-native-session-replay 2.4.3-alpha.0 → 2.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DatadogSDKReactNativeSessionReplay.podspec +1 -1
- package/android/build.gradle +16 -6
- package/android/consumer-proguard-rules.pro +3 -0
- package/android/gradle.properties +1 -1
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +34 -14
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +15 -4
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt +8 -10
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayPrivacySettings.kt +90 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplaySDKWrapper.kt +14 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayWrapper.kt +10 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt +190 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactEditTextMapper.kt +84 -10
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt +106 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt +4 -5
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt +34 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtils.kt +10 -23
- package/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt +28 -4
- package/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt +35 -3
- package/android/src/{main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt → rn75/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt} +6 -8
- package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +101 -0
- package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +13 -0
- package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +101 -0
- package/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +88 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt +67 -9
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +7 -3
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolverTest.kt +2 -7
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapperTest.kt +2 -7
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtilsTest.kt +2 -1
- package/ios/Sources/DdSessionReplay.mm +46 -4
- package/ios/Sources/DdSessionReplayImplementation.swift +83 -11
- package/ios/Sources/RCTTextViewRecorder.swift +1 -1
- package/lib/commonjs/SessionReplay.js +78 -10
- package/lib/commonjs/SessionReplay.js.map +1 -1
- package/lib/commonjs/index.js +18 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/NativeDdSessionReplay.js.map +1 -1
- package/lib/module/SessionReplay.js +77 -9
- package/lib/module/SessionReplay.js.map +1 -1
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/NativeDdSessionReplay.js.map +1 -1
- package/lib/typescript/SessionReplay.d.ts +73 -4
- package/lib/typescript/SessionReplay.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/nativeModulesTypes.d.ts +18 -3
- package/lib/typescript/nativeModulesTypes.d.ts.map +1 -1
- package/lib/typescript/specs/NativeDdSessionReplay.d.ts +15 -2
- package/lib/typescript/specs/NativeDdSessionReplay.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/SessionReplay.ts +170 -23
- package/src/__tests__/SessionReplay.test.ts +94 -8
- package/src/index.ts +14 -2
- package/src/nativeModulesTypes.ts +27 -4
- package/src/specs/NativeDdSessionReplay.ts +21 -3
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +0 -66
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
|
|
3
|
+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
4
|
+
* Copyright 2016-Present Datadog, Inc.
|
|
5
|
+
*/
|
|
6
|
+
import android.graphics.drawable.Drawable
|
|
7
|
+
import android.graphics.drawable.InsetDrawable
|
|
8
|
+
import android.graphics.drawable.LayerDrawable
|
|
9
|
+
import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized
|
|
10
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
11
|
+
import com.datadog.reactnative.sessionreplay.extensions.getRadius
|
|
12
|
+
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
13
|
+
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
|
|
14
|
+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
15
|
+
import com.facebook.react.uimanager.Spacing
|
|
16
|
+
import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable
|
|
17
|
+
|
|
18
|
+
internal class ReactViewBackgroundDrawableUtils : DrawableUtils() {
|
|
19
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
20
|
+
override fun resolveShapeAndBorder(
|
|
21
|
+
drawable: Drawable,
|
|
22
|
+
opacity: Float,
|
|
23
|
+
pixelDensity: Float
|
|
24
|
+
): Pair<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?> {
|
|
25
|
+
if (drawable !is CSSBackgroundDrawable) {
|
|
26
|
+
return null to null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
val borderProps = resolveBorder(drawable, pixelDensity)
|
|
30
|
+
val backgroundColor = getBackgroundColor(drawable)
|
|
31
|
+
val colorHexString = if (backgroundColor != null) {
|
|
32
|
+
formatAsRgba(backgroundColor)
|
|
33
|
+
} else {
|
|
34
|
+
return null to borderProps
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return MobileSegment.ShapeStyle(
|
|
38
|
+
colorHexString,
|
|
39
|
+
opacity,
|
|
40
|
+
getBorderRadius(drawable)
|
|
41
|
+
) to borderProps
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
45
|
+
override fun getReactBackgroundFromDrawable(drawable: Drawable?): Drawable? {
|
|
46
|
+
if (drawable is CSSBackgroundDrawable) {
|
|
47
|
+
return drawable
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (drawable is InsetDrawable) {
|
|
51
|
+
return getReactBackgroundFromDrawable(drawable.drawable)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (drawable is LayerDrawable) {
|
|
55
|
+
for (layerNumber in 0 until drawable.numberOfLayers) {
|
|
56
|
+
val layer = drawable.getDrawable(layerNumber)
|
|
57
|
+
if (layer is CSSBackgroundDrawable) {
|
|
58
|
+
return layer
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
67
|
+
private fun getBorderRadius(drawable: CSSBackgroundDrawable): Float {
|
|
68
|
+
val width = drawable.intrinsicWidth.toFloat()
|
|
69
|
+
val height = drawable.intrinsicHeight.toFloat()
|
|
70
|
+
return drawable.borderRadius.uniform?.getRadius(width, height) ?: 0f
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
74
|
+
private fun getBackgroundColor(
|
|
75
|
+
backgroundDrawable: CSSBackgroundDrawable
|
|
76
|
+
): Int? {
|
|
77
|
+
return reflectionUtils.getDeclaredField(
|
|
78
|
+
backgroundDrawable,
|
|
79
|
+
COLOR_FIELD_NAME
|
|
80
|
+
) as Int?
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
84
|
+
private fun resolveBorder(
|
|
85
|
+
backgroundDrawable: CSSBackgroundDrawable,
|
|
86
|
+
pixelDensity: Float
|
|
87
|
+
): MobileSegment.ShapeBorder {
|
|
88
|
+
val borderWidth =
|
|
89
|
+
backgroundDrawable.fullBorderWidth.toLong().convertToDensityNormalized(pixelDensity)
|
|
90
|
+
val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL))
|
|
91
|
+
|
|
92
|
+
return MobileSegment.ShapeBorder(
|
|
93
|
+
color = borderColor,
|
|
94
|
+
width = borderWidth
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private companion object {
|
|
99
|
+
private const val COLOR_FIELD_NAME = "mColor"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import android.graphics.drawable.Drawable
|
|
2
|
+
import android.graphics.drawable.InsetDrawable
|
|
3
|
+
import android.graphics.drawable.LayerDrawable
|
|
4
|
+
import com.datadog.android.internal.utils.densityNormalized
|
|
5
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
6
|
+
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
7
|
+
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
|
|
8
|
+
import com.facebook.react.uimanager.Spacing
|
|
9
|
+
import com.facebook.react.views.view.ReactViewBackgroundDrawable
|
|
10
|
+
|
|
11
|
+
internal class ReactViewBackgroundDrawableUtils() : DrawableUtils() {
|
|
12
|
+
override fun resolveShapeAndBorder(
|
|
13
|
+
drawable: Drawable,
|
|
14
|
+
opacity: Float,
|
|
15
|
+
pixelDensity: Float
|
|
16
|
+
): Pair<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?> {
|
|
17
|
+
if (drawable !is ReactViewBackgroundDrawable) {
|
|
18
|
+
return null to null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
val borderProps = resolveBorder(drawable, pixelDensity)
|
|
22
|
+
val cornerRadius = drawable
|
|
23
|
+
.fullBorderRadius
|
|
24
|
+
.toLong()
|
|
25
|
+
.densityNormalized(pixelDensity)
|
|
26
|
+
|
|
27
|
+
val backgroundColor = getBackgroundColor(drawable)
|
|
28
|
+
val colorHexString = if (backgroundColor != null) {
|
|
29
|
+
formatAsRgba(backgroundColor)
|
|
30
|
+
} else {
|
|
31
|
+
return null to borderProps
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return MobileSegment.ShapeStyle(
|
|
35
|
+
colorHexString,
|
|
36
|
+
opacity,
|
|
37
|
+
cornerRadius
|
|
38
|
+
) to borderProps
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun getReactBackgroundFromDrawable(drawable: Drawable?): Drawable? {
|
|
42
|
+
if (drawable is ReactViewBackgroundDrawable) {
|
|
43
|
+
return drawable
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (drawable is InsetDrawable) {
|
|
47
|
+
return getReactBackgroundFromDrawable(drawable.drawable)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (drawable is LayerDrawable) {
|
|
51
|
+
for (layerNumber in 0 until drawable.numberOfLayers) {
|
|
52
|
+
val layer = drawable.getDrawable(layerNumber)
|
|
53
|
+
if (layer is ReactViewBackgroundDrawable) {
|
|
54
|
+
return layer
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private fun resolveBorder(
|
|
63
|
+
backgroundDrawable: ReactViewBackgroundDrawable,
|
|
64
|
+
pixelDensity: Float
|
|
65
|
+
): MobileSegment.ShapeBorder {
|
|
66
|
+
val borderWidth =
|
|
67
|
+
backgroundDrawable.fullBorderWidth.toLong().densityNormalized(pixelDensity)
|
|
68
|
+
val borderColor = formatAsRgba(backgroundDrawable.getBorderColor(Spacing.ALL))
|
|
69
|
+
|
|
70
|
+
return MobileSegment.ShapeBorder(
|
|
71
|
+
color = borderColor,
|
|
72
|
+
width = borderWidth
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun getBackgroundColor(
|
|
77
|
+
backgroundDrawable: ReactViewBackgroundDrawable
|
|
78
|
+
): Int? {
|
|
79
|
+
return reflectionUtils.getDeclaredField(
|
|
80
|
+
backgroundDrawable,
|
|
81
|
+
COLOR_FIELD_NAME
|
|
82
|
+
) as Int?
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private companion object {
|
|
86
|
+
private const val COLOR_FIELD_NAME = "mColor"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
package com.datadog.reactnative.sessionreplay
|
|
8
8
|
|
|
9
|
+
import com.datadog.android.sessionreplay.ImagePrivacy
|
|
9
10
|
import com.datadog.android.sessionreplay.SessionReplayConfiguration
|
|
10
11
|
import com.datadog.android.sessionreplay.SessionReplayPrivacy
|
|
12
|
+
import com.datadog.android.sessionreplay.TextAndInputPrivacy
|
|
13
|
+
import com.datadog.android.sessionreplay.TouchPrivacy
|
|
11
14
|
import com.datadog.tools.unit.GenericAssert.Companion.assertThat
|
|
12
15
|
import com.facebook.react.bridge.NativeModule
|
|
13
16
|
import com.facebook.react.bridge.Promise
|
|
14
17
|
import com.facebook.react.bridge.ReactContext
|
|
15
18
|
import com.facebook.react.uimanager.UIManagerModule
|
|
19
|
+
import fr.xgouchet.elmyr.annotation.BoolForgery
|
|
16
20
|
import fr.xgouchet.elmyr.annotation.DoubleForgery
|
|
17
|
-
import fr.xgouchet.elmyr.annotation.Forgery
|
|
18
21
|
import fr.xgouchet.elmyr.annotation.StringForgery
|
|
19
22
|
import fr.xgouchet.elmyr.junit5.ForgeExtension
|
|
20
23
|
import org.junit.jupiter.api.AfterEach
|
|
@@ -53,6 +56,23 @@ internal class DdSessionReplayImplementationTest {
|
|
|
53
56
|
@Mock
|
|
54
57
|
lateinit var mockUiManagerModule: UIManagerModule
|
|
55
58
|
|
|
59
|
+
private val imagePrivacyMap = mapOf(
|
|
60
|
+
"MASK_ALL" to ImagePrivacy.MASK_ALL,
|
|
61
|
+
"MASK_NON_BUNDLED_ONLY" to ImagePrivacy.MASK_LARGE_ONLY,
|
|
62
|
+
"MASK_NONE" to ImagePrivacy.MASK_NONE
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
private val touchPrivacyMap = mapOf(
|
|
66
|
+
"SHOW" to TouchPrivacy.SHOW,
|
|
67
|
+
"HIDE" to TouchPrivacy.HIDE
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
private val inputPrivacyMap = mapOf(
|
|
71
|
+
"MASK_ALL" to TextAndInputPrivacy.MASK_ALL,
|
|
72
|
+
"MASK_ALL_INPUTS" to TextAndInputPrivacy.MASK_ALL_INPUTS,
|
|
73
|
+
"MASK_SENSITIVE_INPUTS" to TextAndInputPrivacy.MASK_SENSITIVE_INPUTS
|
|
74
|
+
)
|
|
75
|
+
|
|
56
76
|
@BeforeEach
|
|
57
77
|
fun `set up`() {
|
|
58
78
|
whenever(mockReactContext.getNativeModule(any<Class<NativeModule>>()))
|
|
@@ -67,10 +87,32 @@ internal class DdSessionReplayImplementationTest {
|
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
@Test
|
|
70
|
-
fun `M enable session replay W
|
|
90
|
+
fun `M enable session replay W random privacy settings`(
|
|
71
91
|
@DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double,
|
|
72
|
-
@
|
|
73
|
-
@
|
|
92
|
+
@StringForgery(regex = ".+") customEndpoint: String,
|
|
93
|
+
@BoolForgery startRecordingImmediately: Boolean
|
|
94
|
+
) {
|
|
95
|
+
val imagePrivacy = imagePrivacyMap.keys.random()
|
|
96
|
+
val touchPrivacy = touchPrivacyMap.keys.random()
|
|
97
|
+
val textAndInputPrivacy = inputPrivacyMap.keys.random()
|
|
98
|
+
|
|
99
|
+
testSessionReplayEnable(
|
|
100
|
+
replaySampleRate = replaySampleRate,
|
|
101
|
+
customEndpoint = customEndpoint,
|
|
102
|
+
imagePrivacy = imagePrivacy,
|
|
103
|
+
touchPrivacy = touchPrivacy,
|
|
104
|
+
textAndInputPrivacy = textAndInputPrivacy,
|
|
105
|
+
startRecordingImmediately = startRecordingImmediately
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private fun testSessionReplayEnable(
|
|
110
|
+
replaySampleRate: Double,
|
|
111
|
+
customEndpoint: String,
|
|
112
|
+
imagePrivacy: String,
|
|
113
|
+
touchPrivacy: String,
|
|
114
|
+
textAndInputPrivacy: String,
|
|
115
|
+
startRecordingImmediately: Boolean
|
|
74
116
|
) {
|
|
75
117
|
// Given
|
|
76
118
|
val sessionReplayConfigCaptor = argumentCaptor<SessionReplayConfiguration>()
|
|
@@ -78,8 +120,9 @@ internal class DdSessionReplayImplementationTest {
|
|
|
78
120
|
// When
|
|
79
121
|
testedSessionReplay.enable(
|
|
80
122
|
replaySampleRate,
|
|
81
|
-
privacy.toString(),
|
|
82
123
|
customEndpoint,
|
|
124
|
+
SessionReplayPrivacySettings(imagePrivacy, touchPrivacy, textAndInputPrivacy),
|
|
125
|
+
startRecordingImmediately,
|
|
83
126
|
mockPromise
|
|
84
127
|
)
|
|
85
128
|
|
|
@@ -87,27 +130,42 @@ internal class DdSessionReplayImplementationTest {
|
|
|
87
130
|
verify(mockSessionReplay).enable(sessionReplayConfigCaptor.capture(), any())
|
|
88
131
|
assertThat(sessionReplayConfigCaptor.firstValue)
|
|
89
132
|
.hasFieldEqualTo("sampleRate", replaySampleRate.toFloat())
|
|
90
|
-
.hasFieldEqualTo("privacy", privacy)
|
|
91
133
|
.hasFieldEqualTo("customEndpointUrl", customEndpoint)
|
|
134
|
+
.hasFieldEqualTo("textAndInputPrivacy", inputPrivacyMap[textAndInputPrivacy])
|
|
135
|
+
.hasFieldEqualTo("imagePrivacy", imagePrivacyMap[imagePrivacy])
|
|
136
|
+
.hasFieldEqualTo("touchPrivacy", touchPrivacyMap[touchPrivacy])
|
|
92
137
|
}
|
|
93
138
|
|
|
94
139
|
@Test
|
|
95
140
|
fun `M enable session replay without custom endpoint W empty string()`(
|
|
96
141
|
@DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double,
|
|
97
|
-
|
|
98
|
-
@StringForgery(regex = "^/(?!ALLOW|MASK_USER_INPUT)([a-z0-9]+)$/i") privacy: String
|
|
142
|
+
@BoolForgery startRecordingImmediately: Boolean
|
|
99
143
|
) {
|
|
100
144
|
// Given
|
|
145
|
+
val imagePrivacy = imagePrivacyMap.keys.random()
|
|
146
|
+
val touchPrivacy = touchPrivacyMap.keys.random()
|
|
147
|
+
val textAndInputPrivacy = inputPrivacyMap.keys.random()
|
|
101
148
|
val sessionReplayConfigCaptor = argumentCaptor<SessionReplayConfiguration>()
|
|
102
149
|
|
|
103
150
|
// When
|
|
104
|
-
testedSessionReplay.enable(
|
|
151
|
+
testedSessionReplay.enable(
|
|
152
|
+
replaySampleRate,
|
|
153
|
+
"",
|
|
154
|
+
SessionReplayPrivacySettings(
|
|
155
|
+
imagePrivacyLevel = imagePrivacy,
|
|
156
|
+
touchPrivacyLevel = touchPrivacy,
|
|
157
|
+
textAndInputPrivacyLevel = textAndInputPrivacy
|
|
158
|
+
),
|
|
159
|
+
startRecordingImmediately,
|
|
160
|
+
mockPromise
|
|
161
|
+
)
|
|
105
162
|
|
|
106
163
|
// Then
|
|
107
164
|
verify(mockSessionReplay).enable(sessionReplayConfigCaptor.capture(), any())
|
|
108
165
|
assertThat(sessionReplayConfigCaptor.firstValue)
|
|
109
166
|
.hasFieldEqualTo("sampleRate", replaySampleRate.toFloat())
|
|
110
167
|
.hasFieldEqualTo("privacy", SessionReplayPrivacy.MASK)
|
|
168
|
+
.hasFieldEqualTo("startRecordingImmediately", startRecordingImmediately)
|
|
111
169
|
.doesNotHaveField("customEndpointUrl")
|
|
112
170
|
}
|
|
113
171
|
}
|
|
@@ -8,6 +8,7 @@ package com.datadog.reactnative.sessionreplay
|
|
|
8
8
|
|
|
9
9
|
import com.datadog.android.api.InternalLogger
|
|
10
10
|
import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper
|
|
11
|
+
import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper
|
|
11
12
|
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
|
|
12
13
|
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
|
|
13
14
|
import com.facebook.react.bridge.NativeModule
|
|
@@ -62,15 +63,18 @@ internal class ReactNativeSessionReplayExtensionSupportTest {
|
|
|
62
63
|
val customViewMappers = testedExtensionSupport.getCustomViewMappers()
|
|
63
64
|
|
|
64
65
|
// Then
|
|
65
|
-
assertThat(customViewMappers).hasSize(
|
|
66
|
+
assertThat(customViewMappers).hasSize(4)
|
|
66
67
|
|
|
67
68
|
assertThat(customViewMappers[0].getUnsafeMapper())
|
|
68
|
-
.isInstanceOf(
|
|
69
|
+
.isInstanceOf(ReactNativeImageViewMapper::class.java)
|
|
69
70
|
|
|
70
71
|
assertThat(customViewMappers[1].getUnsafeMapper())
|
|
71
|
-
.isInstanceOf(
|
|
72
|
+
.isInstanceOf(ReactViewGroupMapper::class.java)
|
|
72
73
|
|
|
73
74
|
assertThat(customViewMappers[2].getUnsafeMapper())
|
|
75
|
+
.isInstanceOf(ReactTextMapper::class.java)
|
|
76
|
+
|
|
77
|
+
assertThat(customViewMappers[3].getUnsafeMapper())
|
|
74
78
|
.isInstanceOf(ReactEditTextMapper::class.java)
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -15,7 +15,6 @@ import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver.Compani
|
|
|
15
15
|
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver.Companion.TEXT_ATTRIBUTES_FIELD_NAME
|
|
16
16
|
import com.datadog.reactnative.sessionreplay.ShadowNodeWrapper.Companion.UI_IMPLEMENTATION_FIELD_NAME
|
|
17
17
|
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
18
|
-
import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils
|
|
19
18
|
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
|
|
20
19
|
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
|
|
21
20
|
import com.datadog.reactnative.tools.unit.forge.ForgeConfigurator
|
|
@@ -64,14 +63,11 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
64
63
|
@Mock
|
|
65
64
|
lateinit var mockTextView: TextView
|
|
66
65
|
|
|
67
|
-
@Mock
|
|
68
|
-
lateinit var mockDrawableUtils: DrawableUtils
|
|
69
|
-
|
|
70
66
|
@Mock
|
|
71
67
|
lateinit var mockReactViewBackgroundDrawable: ReactViewBackgroundDrawable
|
|
72
68
|
|
|
73
69
|
@Mock
|
|
74
|
-
lateinit var
|
|
70
|
+
lateinit var mockDrawableUtils: DrawableUtils
|
|
75
71
|
|
|
76
72
|
@Mock
|
|
77
73
|
lateinit var mockShadowNodeWrapper: ShadowNodeWrapper
|
|
@@ -108,7 +104,6 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
108
104
|
testedResolver = ReactTextPropertiesResolver(
|
|
109
105
|
reactContext = mockReactContext,
|
|
110
106
|
uiManagerModule = mockUiManagerModule,
|
|
111
|
-
reactViewBackgroundDrawableUtils = mockReactViewBackgroundDrawableUtils,
|
|
112
107
|
drawableUtils = mockDrawableUtils,
|
|
113
108
|
reflectionUtils = mockReflectionUtils
|
|
114
109
|
)
|
|
@@ -145,7 +140,7 @@ internal class ReactTextPropertiesResolverTest {
|
|
|
145
140
|
)
|
|
146
141
|
).thenReturn(mockReactViewBackgroundDrawable)
|
|
147
142
|
whenever(
|
|
148
|
-
|
|
143
|
+
mockDrawableUtils.resolveShapeAndBorder(
|
|
149
144
|
drawable = eq(mockReactViewBackgroundDrawable),
|
|
150
145
|
opacity = eq(0f),
|
|
151
146
|
pixelDensity = eq(0f)
|
|
@@ -15,7 +15,6 @@ import com.datadog.android.sessionreplay.recorder.MappingContext
|
|
|
15
15
|
import com.datadog.android.sessionreplay.recorder.SystemInformation
|
|
16
16
|
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
|
|
17
17
|
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
18
|
-
import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils
|
|
19
18
|
import com.facebook.react.views.view.ReactViewBackgroundDrawable
|
|
20
19
|
import com.facebook.react.views.view.ReactViewGroup
|
|
21
20
|
import fr.xgouchet.elmyr.junit5.ForgeExtension
|
|
@@ -40,7 +39,7 @@ internal class ReactViewGroupMapperTest {
|
|
|
40
39
|
private lateinit var testedMapper: ReactViewGroupMapper
|
|
41
40
|
|
|
42
41
|
@Mock
|
|
43
|
-
private lateinit var
|
|
42
|
+
private lateinit var mockDrawableUtils: DrawableUtils
|
|
44
43
|
|
|
45
44
|
@Mock
|
|
46
45
|
private lateinit var mockReactViewGroup: ReactViewGroup
|
|
@@ -57,9 +56,6 @@ internal class ReactViewGroupMapperTest {
|
|
|
57
56
|
@Mock
|
|
58
57
|
private lateinit var mockSystemInformation: SystemInformation
|
|
59
58
|
|
|
60
|
-
@Mock
|
|
61
|
-
private lateinit var mockDrawableUtils: DrawableUtils
|
|
62
|
-
|
|
63
59
|
@Mock
|
|
64
60
|
private lateinit var mockReactViewBackgroundDrawable: ReactViewBackgroundDrawable
|
|
65
61
|
|
|
@@ -75,7 +71,6 @@ internal class ReactViewGroupMapperTest {
|
|
|
75
71
|
whenever(mockSystemInformation.screenDensity).thenReturn(0f)
|
|
76
72
|
|
|
77
73
|
testedMapper = ReactViewGroupMapper(
|
|
78
|
-
reactViewBackgroundDrawableUtils = mockReactViewBackgroundDrawableUtils,
|
|
79
74
|
drawableUtils = mockDrawableUtils
|
|
80
75
|
)
|
|
81
76
|
}
|
|
@@ -115,7 +110,7 @@ internal class ReactViewGroupMapperTest {
|
|
|
115
110
|
)
|
|
116
111
|
).thenReturn(mockReactViewBackgroundDrawable)
|
|
117
112
|
whenever(
|
|
118
|
-
|
|
113
|
+
mockDrawableUtils.resolveShapeAndBorder(
|
|
119
114
|
drawable = eq(mockReactViewBackgroundDrawable),
|
|
120
115
|
pixelDensity = eq(0f),
|
|
121
116
|
opacity = eq(0f)
|
package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtilsTest.kt
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
package com.datadog.reactnative.sessionreplay.utils
|
|
8
8
|
|
|
9
|
+
import ReactViewBackgroundDrawableUtils
|
|
9
10
|
import android.graphics.drawable.ColorDrawable
|
|
10
11
|
import android.graphics.drawable.InsetDrawable
|
|
11
12
|
import android.graphics.drawable.LayerDrawable
|
|
@@ -47,7 +48,7 @@ internal class DrawableUtilsTest {
|
|
|
47
48
|
whenever(mockLayerDrawable.numberOfLayers).thenReturn(3)
|
|
48
49
|
whenever(mockLayerDrawable.getDrawable(0)).thenReturn(mockReactViewBackgroundDrawable)
|
|
49
50
|
|
|
50
|
-
testedDrawableUtils =
|
|
51
|
+
testedDrawableUtils = ReactViewBackgroundDrawableUtils()
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
@Test
|
|
@@ -18,12 +18,32 @@
|
|
|
18
18
|
RCT_EXPORT_MODULE()
|
|
19
19
|
|
|
20
20
|
RCT_REMAP_METHOD(enable, withEnableReplaySampleRate:(double)replaySampleRate
|
|
21
|
-
withDefaultPrivacyLevel:(NSString*)defaultPrivacyLevel
|
|
22
21
|
withCustomEndpoint:(NSString*)customEndpoint
|
|
22
|
+
withImagePrivacyLevel:(NSString*)imagePrivacyLevel
|
|
23
|
+
withTouchPrivacyLevel:(NSString*)touchPrivacyLevel
|
|
24
|
+
withTextAndInputPrivacyLevel:(NSString*)textAndInputPrivacyLevel
|
|
25
|
+
withStartRecordingImmediately:(BOOL)startRecordingImmediately
|
|
23
26
|
withResolver:(RCTPromiseResolveBlock)resolve
|
|
24
27
|
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
25
28
|
{
|
|
26
|
-
[self enable:replaySampleRate
|
|
29
|
+
[self enable:replaySampleRate
|
|
30
|
+
customEndpoint:customEndpoint
|
|
31
|
+
imagePrivacyLevel:imagePrivacyLevel
|
|
32
|
+
touchPrivacyLevel:touchPrivacyLevel
|
|
33
|
+
textAndInputPrivacyLevel:textAndInputPrivacyLevel
|
|
34
|
+
startRecordingImmediately:startRecordingImmediately
|
|
35
|
+
resolve:resolve
|
|
36
|
+
reject:reject];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
RCT_EXPORT_METHOD(startRecording:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
|
|
40
|
+
{
|
|
41
|
+
[self startRecordingWithResolver:resolve reject:reject];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
RCT_EXPORT_METHOD(stopRecording:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
|
|
45
|
+
{
|
|
46
|
+
[self stopRecordingWithResolver:resolve reject:reject];
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
// Thanks to this guard, we won't compile this code when we build for the old architecture.
|
|
@@ -47,8 +67,30 @@ RCT_REMAP_METHOD(enable, withEnableReplaySampleRate:(double)replaySampleRate
|
|
|
47
67
|
return NO;
|
|
48
68
|
}
|
|
49
69
|
|
|
50
|
-
- (void)enable:(double)replaySampleRate
|
|
51
|
-
|
|
70
|
+
- (void)enable:(double)replaySampleRate
|
|
71
|
+
customEndpoint:(NSString*)customEndpoint
|
|
72
|
+
imagePrivacyLevel:(NSString *)imagePrivacyLevel
|
|
73
|
+
touchPrivacyLevel:(NSString *)touchPrivacyLevel
|
|
74
|
+
textAndInputPrivacyLevel:(NSString *)textAndInputPrivacyLevel
|
|
75
|
+
startRecordingImmediately:(BOOL)startRecordingImmediately
|
|
76
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
77
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
78
|
+
[self.ddSessionReplayImplementation enableWithReplaySampleRate:replaySampleRate
|
|
79
|
+
customEndpoint:customEndpoint
|
|
80
|
+
imagePrivacyLevel:imagePrivacyLevel
|
|
81
|
+
touchPrivacyLevel:touchPrivacyLevel
|
|
82
|
+
textAndInputPrivacyLevel:textAndInputPrivacyLevel
|
|
83
|
+
startRecordingImmediately:startRecordingImmediately
|
|
84
|
+
resolve:resolve
|
|
85
|
+
reject:reject];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
- (void)startRecordingWithResolver:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
89
|
+
[self.ddSessionReplayImplementation startRecordingWithResolve:resolve reject:reject];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
- (void)stopRecordingWithResolver:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
93
|
+
[self.ddSessionReplayImplementation stopRecordingWithResolve:resolve reject:reject];
|
|
52
94
|
}
|
|
53
95
|
|
|
54
96
|
@end
|
|
@@ -30,14 +30,26 @@ public class DdSessionReplayImplementation: NSObject {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
@objc
|
|
33
|
-
public func enable(
|
|
33
|
+
public func enable(
|
|
34
|
+
replaySampleRate: Double,
|
|
35
|
+
customEndpoint: String,
|
|
36
|
+
imagePrivacyLevel: NSString,
|
|
37
|
+
touchPrivacyLevel: NSString,
|
|
38
|
+
textAndInputPrivacyLevel: NSString,
|
|
39
|
+
startRecordingImmediately: Bool,
|
|
40
|
+
resolve:RCTPromiseResolveBlock,
|
|
41
|
+
reject:RCTPromiseRejectBlock
|
|
42
|
+
) -> Void {
|
|
34
43
|
var customEndpointURL: URL? = nil
|
|
35
44
|
if (customEndpoint != "") {
|
|
36
45
|
customEndpointURL = URL(string: "\(customEndpoint)/api/v2/replay" as String)
|
|
37
46
|
}
|
|
38
47
|
var sessionReplayConfiguration = SessionReplay.Configuration(
|
|
39
48
|
replaySampleRate: Float(replaySampleRate),
|
|
40
|
-
|
|
49
|
+
textAndInputPrivacyLevel: convertTextAndInputPrivacy(textAndInputPrivacyLevel),
|
|
50
|
+
imagePrivacyLevel: convertImagePrivacy(imagePrivacyLevel),
|
|
51
|
+
touchPrivacyLevel: convertTouchPrivacy(touchPrivacyLevel),
|
|
52
|
+
startRecordingImmediately: startRecordingImmediately,
|
|
41
53
|
customEndpoint: customEndpointURL
|
|
42
54
|
)
|
|
43
55
|
|
|
@@ -55,16 +67,65 @@ public class DdSessionReplayImplementation: NSObject {
|
|
|
55
67
|
resolve(nil)
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
@objc
|
|
71
|
+
public func startRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
|
|
72
|
+
if let core = DatadogSDKWrapper.shared.getCoreInstance() {
|
|
73
|
+
sessionReplay.startRecording(in: core)
|
|
74
|
+
} else {
|
|
75
|
+
consolePrint("Core instance was not found when calling startRecording in Session Replay.", .critical)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
resolve(nil)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@objc
|
|
82
|
+
public func stopRecording(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
|
|
83
|
+
if let core = DatadogSDKWrapper.shared.getCoreInstance() {
|
|
84
|
+
sessionReplay.stopRecording(in: core)
|
|
85
|
+
} else {
|
|
86
|
+
consolePrint("Core instance was not found when calling stopRecording in Session Replay.", .critical)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
resolve(nil)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func convertImagePrivacy(_ imagePrivacy: NSString) -> ImagePrivacyLevel {
|
|
93
|
+
switch imagePrivacy {
|
|
94
|
+
case "MASK_NON_BUNDLED_ONLY":
|
|
95
|
+
return .maskNonBundledOnly
|
|
96
|
+
case "MASK_ALL":
|
|
97
|
+
return .maskAll
|
|
98
|
+
case "MASK_NONE":
|
|
99
|
+
return .maskNone
|
|
66
100
|
default:
|
|
67
|
-
|
|
101
|
+
consolePrint("Unknown Session Replay Image Privacy Level given: \(imagePrivacy), using .maskAll as default.", .warn)
|
|
102
|
+
return .maskAll
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func convertTouchPrivacy(_ touchPrivacy: NSString) -> TouchPrivacyLevel {
|
|
107
|
+
switch touchPrivacy {
|
|
108
|
+
case "SHOW":
|
|
109
|
+
return .show
|
|
110
|
+
case "HIDE":
|
|
111
|
+
return .hide
|
|
112
|
+
default:
|
|
113
|
+
consolePrint("Unknown Session Replay Touch Privacy Level given: \(touchPrivacy), using .hide as default.", .warn)
|
|
114
|
+
return .hide
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func convertTextAndInputPrivacy(_ textAndInputPrivacy: NSString) -> TextAndInputPrivacyLevel {
|
|
119
|
+
switch textAndInputPrivacy {
|
|
120
|
+
case "MASK_SENSITIVE_INPUTS":
|
|
121
|
+
return .maskSensitiveInputs
|
|
122
|
+
case "MASK_ALL_INPUTS":
|
|
123
|
+
return .maskAllInputs
|
|
124
|
+
case "MASK_ALL":
|
|
125
|
+
return .maskAll
|
|
126
|
+
default:
|
|
127
|
+
consolePrint("Unknown Session Replay Text and Input Privacy Level given: \(textAndInputPrivacy), using .maskAll as default.", .warn)
|
|
128
|
+
return .maskAll
|
|
68
129
|
}
|
|
69
130
|
}
|
|
70
131
|
}
|
|
@@ -74,10 +135,21 @@ internal protocol SessionReplayProtocol {
|
|
|
74
135
|
with configuration: SessionReplay.Configuration,
|
|
75
136
|
in core: DatadogCoreProtocol
|
|
76
137
|
)
|
|
138
|
+
|
|
139
|
+
func startRecording(in core: DatadogCoreProtocol)
|
|
140
|
+
func stopRecording(in core: DatadogCoreProtocol)
|
|
77
141
|
}
|
|
78
142
|
|
|
79
143
|
internal class NativeSessionReplay: SessionReplayProtocol {
|
|
80
144
|
func enable(with configuration: DatadogSessionReplay.SessionReplay.Configuration, in core: DatadogCoreProtocol) {
|
|
81
145
|
SessionReplay.enable(with: configuration, in: core)
|
|
82
146
|
}
|
|
147
|
+
|
|
148
|
+
func startRecording(in core: any DatadogInternal.DatadogCoreProtocol) {
|
|
149
|
+
SessionReplay.startRecording(in: core)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func stopRecording(in core: any DatadogInternal.DatadogCoreProtocol) {
|
|
153
|
+
SessionReplay.stopRecording(in: core)
|
|
154
|
+
}
|
|
83
155
|
}
|
|
@@ -11,7 +11,7 @@ import React
|
|
|
11
11
|
|
|
12
12
|
internal class RCTTextViewRecorder: SessionReplayNodeRecorder {
|
|
13
13
|
internal var textObfuscator: (SessionReplayViewTreeRecordingContext) -> SessionReplayTextObfuscating = { context in
|
|
14
|
-
return context.recorder.
|
|
14
|
+
return context.recorder.textAndInputPrivacy.staticTextObfuscator
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
internal var identifier = UUID()
|