@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.
Files changed (56) hide show
  1. package/DatadogSDKReactNativeSessionReplay.podspec +1 -1
  2. package/android/build.gradle +16 -6
  3. package/android/consumer-proguard-rules.pro +3 -0
  4. package/android/gradle.properties +1 -1
  5. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +34 -14
  6. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +15 -4
  7. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt +8 -10
  8. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayPrivacySettings.kt +90 -0
  9. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplaySDKWrapper.kt +14 -0
  10. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayWrapper.kt +10 -0
  11. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/ReactDrawablesExt.kt +190 -0
  12. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactEditTextMapper.kt +84 -10
  13. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactNativeImageViewMapper.kt +106 -0
  14. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt +4 -5
  15. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/resources/ReactDrawableCopier.kt +34 -0
  16. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtils.kt +10 -23
  17. package/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt +28 -4
  18. package/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt +35 -3
  19. package/android/src/{main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt → rn75/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt} +6 -8
  20. package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +101 -0
  21. package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +13 -0
  22. package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +101 -0
  23. package/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +88 -0
  24. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt +67 -9
  25. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +7 -3
  26. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolverTest.kt +2 -7
  27. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapperTest.kt +2 -7
  28. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtilsTest.kt +2 -1
  29. package/ios/Sources/DdSessionReplay.mm +46 -4
  30. package/ios/Sources/DdSessionReplayImplementation.swift +83 -11
  31. package/ios/Sources/RCTTextViewRecorder.swift +1 -1
  32. package/lib/commonjs/SessionReplay.js +78 -10
  33. package/lib/commonjs/SessionReplay.js.map +1 -1
  34. package/lib/commonjs/index.js +18 -0
  35. package/lib/commonjs/index.js.map +1 -1
  36. package/lib/commonjs/specs/NativeDdSessionReplay.js.map +1 -1
  37. package/lib/module/SessionReplay.js +77 -9
  38. package/lib/module/SessionReplay.js.map +1 -1
  39. package/lib/module/index.js +2 -2
  40. package/lib/module/index.js.map +1 -1
  41. package/lib/module/specs/NativeDdSessionReplay.js.map +1 -1
  42. package/lib/typescript/SessionReplay.d.ts +73 -4
  43. package/lib/typescript/SessionReplay.d.ts.map +1 -1
  44. package/lib/typescript/index.d.ts +2 -2
  45. package/lib/typescript/index.d.ts.map +1 -1
  46. package/lib/typescript/nativeModulesTypes.d.ts +18 -3
  47. package/lib/typescript/nativeModulesTypes.d.ts.map +1 -1
  48. package/lib/typescript/specs/NativeDdSessionReplay.d.ts +15 -2
  49. package/lib/typescript/specs/NativeDdSessionReplay.d.ts.map +1 -1
  50. package/package.json +2 -1
  51. package/src/SessionReplay.ts +170 -23
  52. package/src/__tests__/SessionReplay.test.ts +94 -8
  53. package/src/index.ts +14 -2
  54. package/src/nativeModulesTypes.ts +27 -4
  55. package/src/specs/NativeDdSessionReplay.ts +21 -3
  56. 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 enable()`(
90
+ fun `M enable session replay W random privacy settings`(
71
91
  @DoubleForgery(min = 0.0, max = 100.0) replaySampleRate: Double,
72
- @Forgery privacy: SessionReplayPrivacy,
73
- @StringForgery(regex = ".+") customEndpoint: String
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
- // Not ALLOW nor MASK_USER_INPUT
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(replaySampleRate, privacy, "", mockPromise)
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(3)
66
+ assertThat(customViewMappers).hasSize(4)
66
67
 
67
68
  assertThat(customViewMappers[0].getUnsafeMapper())
68
- .isInstanceOf(ReactViewGroupMapper::class.java)
69
+ .isInstanceOf(ReactNativeImageViewMapper::class.java)
69
70
 
70
71
  assertThat(customViewMappers[1].getUnsafeMapper())
71
- .isInstanceOf(ReactTextMapper::class.java)
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 mockReactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils
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
- mockReactViewBackgroundDrawableUtils.resolveShapeAndBorder(
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 mockReactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils
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
- mockReactViewBackgroundDrawableUtils.resolveShapeAndBorder(
113
+ mockDrawableUtils.resolveShapeAndBorder(
119
114
  drawable = eq(mockReactViewBackgroundDrawable),
120
115
  pixelDensity = eq(0f),
121
116
  opacity = eq(0f)
@@ -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 = DrawableUtils()
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 defaultPrivacyLevel:defaultPrivacyLevel customEndpoint:customEndpoint resolve:resolve reject:reject];
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 defaultPrivacyLevel:(NSString *)defaultPrivacyLevel customEndpoint:(NSString*)customEndpoint resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
51
- [self.ddSessionReplayImplementation enableWithReplaySampleRate:replaySampleRate defaultPrivacyLevel:defaultPrivacyLevel customEndpoint:customEndpoint resolve:resolve reject:reject];
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(replaySampleRate: Double, defaultPrivacyLevel: String, customEndpoint: String, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void {
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
- defaultPrivacyLevel: buildPrivacyLevel(privacyLevel: defaultPrivacyLevel as NSString),
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
- func buildPrivacyLevel(privacyLevel: NSString) -> SessionReplayPrivacyLevel {
59
- switch privacyLevel.lowercased {
60
- case "mask":
61
- return .mask
62
- case "mask_user_input":
63
- return .maskUserInput
64
- case "allow":
65
- return .allow
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
- return .mask
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.privacy.staticTextObfuscator
14
+ return context.recorder.textAndInputPrivacy.staticTextObfuscator
15
15
  }
16
16
 
17
17
  internal var identifier = UUID()