@datadog/mobile-react-native-session-replay 2.0.2-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 +41 -0
- package/README.md +3 -0
- package/android/build.gradle +239 -0
- package/android/detekt.yml +572 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DatadogSDKReactNativeSessionReplayPackage.kt +46 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +57 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/NoopTextPropertiesResolver.kt +22 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +77 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt +196 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplaySDKWrapper.kt +24 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplayWrapper.kt +22 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ShadowNodeWrapper.kt +70 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/TextPropertiesResolver.kt +20 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/extensions/LongExt.kt +15 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactMaskInputTextMapper.kt +54 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactMaskTextMapper.kt +55 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactTextMapper.kt +54 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt +58 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ColorUtils.kt +22 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtils.kt +35 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +66 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/ReflectionUtils.kt +30 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtils.kt +40 -0
- package/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt +33 -0
- package/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplay.kt +34 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt +105 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +127 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolverTest.kt +271 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapperTest.kt +131 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/ColorUtilsTest.kt +42 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/DrawableUtilsTest.kt +101 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtilsTest.kt +109 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/GenericAssert.kt +69 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/MapExt.kt +29 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/ReflectUtils.kt +266 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/forge/BaseConfigurator.kt +24 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/forge/ForgeConfigurator.kt +24 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/forge/TextWireframeForgeryFactory.kt +64 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/forge/Throwable.kt +31 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/forge/ThrowableForgeryFactory.kt +21 -0
- package/android/src/test/kotlin/com/datadog/reactnative/tools/unit/forge/WireframeClipForgeryFactory.kt +25 -0
- package/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +1 -0
- package/ios/DatadogSDKReactNativeSessionReplay.xcodeproj/project.pbxproj +272 -0
- package/ios/DatadogSDKReactNativeSessionReplay.xcodeproj/project.xcworkspace/contents.xcworkspacedata +4 -0
- package/ios/Sources/DatadogSDKReactNativeSessionReplay.h +8 -0
- package/ios/Sources/DdSessionReplay.h +24 -0
- package/ios/Sources/DdSessionReplay.mm +53 -0
- package/ios/Sources/DdSessionReplayImplementation.swift +70 -0
- package/ios/Sources/RCTTextViewRecorder.swift +157 -0
- package/lib/commonjs/SessionReplay.js +66 -0
- package/lib/commonjs/SessionReplay.js.map +1 -0
- package/lib/commonjs/index.js +26 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/nativeModulesTypes.js +2 -0
- package/lib/commonjs/nativeModulesTypes.js.map +1 -0
- package/lib/commonjs/specs/NativeDdSessionReplay.js +20 -0
- package/lib/commonjs/specs/NativeDdSessionReplay.js.map +1 -0
- package/lib/module/SessionReplay.js +53 -0
- package/lib/module/SessionReplay.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/nativeModulesTypes.js +2 -0
- package/lib/module/nativeModulesTypes.js.map +1 -0
- package/lib/module/specs/NativeDdSessionReplay.js +14 -0
- package/lib/module/specs/NativeDdSessionReplay.js.map +1 -0
- package/lib/typescript/SessionReplay.d.ts +34 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/nativeModulesTypes.d.ts +18 -0
- package/lib/typescript/specs/NativeDdSessionReplay.d.ts +15 -0
- package/package.json +90 -0
- package/src/SessionReplay.ts +84 -0
- package/src/__tests__/SessionReplay.test.ts +49 -0
- package/src/index.ts +13 -0
- package/src/nativeModulesTypes.ts +29 -0
- package/src/specs/NativeDdSessionReplay.ts +28 -0
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/NoopTextPropertiesResolver.kt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
*
|
|
3
|
+
* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
|
|
4
|
+
* * This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
5
|
+
* * Copyright 2016-Present Datadog, Inc.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
package com.datadog.reactnative.sessionreplay
|
|
10
|
+
|
|
11
|
+
import android.widget.TextView
|
|
12
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
13
|
+
|
|
14
|
+
internal class NoopTextPropertiesResolver: TextPropertiesResolver {
|
|
15
|
+
override fun addReactNativeProperties(
|
|
16
|
+
originalWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
17
|
+
view: TextView,
|
|
18
|
+
pixelDensity: Float
|
|
19
|
+
): MobileSegment.Wireframe.TextWireframe {
|
|
20
|
+
return originalWireframe
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay
|
|
8
|
+
|
|
9
|
+
import android.view.View
|
|
10
|
+
import androidx.annotation.VisibleForTesting
|
|
11
|
+
import com.datadog.android.api.InternalLogger
|
|
12
|
+
import com.datadog.android.sessionreplay.ExtensionSupport
|
|
13
|
+
import com.datadog.android.sessionreplay.SessionReplayPrivacy
|
|
14
|
+
import com.datadog.android.sessionreplay.internal.recorder.OptionSelectorDetector
|
|
15
|
+
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
|
|
16
|
+
import com.datadog.reactnative.sessionreplay.mappers.ReactMaskInputTextMapper
|
|
17
|
+
import com.datadog.reactnative.sessionreplay.mappers.ReactMaskTextMapper
|
|
18
|
+
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
|
|
19
|
+
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
|
|
20
|
+
import com.facebook.react.bridge.ReactContext
|
|
21
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
22
|
+
import com.facebook.react.views.text.ReactTextView
|
|
23
|
+
import com.facebook.react.views.textinput.ReactEditText
|
|
24
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
25
|
+
|
|
26
|
+
internal class ReactNativeSessionReplayExtensionSupport(
|
|
27
|
+
private val reactContext: ReactContext,
|
|
28
|
+
private val logger: InternalLogger
|
|
29
|
+
) : ExtensionSupport {
|
|
30
|
+
|
|
31
|
+
override fun getCustomViewMappers(): Map<SessionReplayPrivacy, Map<Class<*>, WireframeMapper<View, *>>> {
|
|
32
|
+
val uiManagerModule = getUiManagerModule()
|
|
33
|
+
|
|
34
|
+
return mapOf(
|
|
35
|
+
SessionReplayPrivacy.ALLOW to mapOf(
|
|
36
|
+
ReactViewGroup::class.java to ReactViewGroupMapper(),
|
|
37
|
+
ReactTextView::class.java to ReactTextMapper(reactContext, uiManagerModule),
|
|
38
|
+
ReactEditText::class.java to ReactTextMapper(reactContext, uiManagerModule)
|
|
39
|
+
),
|
|
40
|
+
SessionReplayPrivacy.MASK to mapOf(
|
|
41
|
+
ReactViewGroup::class.java to ReactViewGroupMapper(),
|
|
42
|
+
ReactTextView::class.java to ReactMaskTextMapper(reactContext, uiManagerModule),
|
|
43
|
+
ReactEditText::class.java to ReactMaskTextMapper(reactContext, uiManagerModule)
|
|
44
|
+
),
|
|
45
|
+
SessionReplayPrivacy.MASK_USER_INPUT to mapOf(
|
|
46
|
+
ReactViewGroup::class.java to ReactViewGroupMapper(),
|
|
47
|
+
ReactTextView::class.java to ReactMaskInputTextMapper(reactContext, uiManagerModule),
|
|
48
|
+
ReactEditText::class.java to ReactMaskInputTextMapper(reactContext, uiManagerModule)
|
|
49
|
+
)
|
|
50
|
+
).mapValues {
|
|
51
|
+
it.value as Map<Class<*>, WireframeMapper<View, *>>
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
|
|
56
|
+
return listOf()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@VisibleForTesting
|
|
60
|
+
internal fun getUiManagerModule(): UIManagerModule? {
|
|
61
|
+
return try {
|
|
62
|
+
reactContext.getNativeModule(UIManagerModule::class.java)
|
|
63
|
+
} catch (e: IllegalStateException) {
|
|
64
|
+
logger.log(
|
|
65
|
+
level = InternalLogger.Level.WARN,
|
|
66
|
+
targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
|
|
67
|
+
messageBuilder = { RESOLVE_UIMANAGERMODULE_ERROR },
|
|
68
|
+
throwable = e
|
|
69
|
+
)
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
internal companion object {
|
|
75
|
+
internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactTextPropertiesResolver.kt
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay
|
|
8
|
+
|
|
9
|
+
import android.view.Gravity
|
|
10
|
+
import android.widget.TextView
|
|
11
|
+
import androidx.annotation.VisibleForTesting
|
|
12
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
13
|
+
import com.datadog.reactnative.sessionreplay.extensions.convertToDensityNormalized
|
|
14
|
+
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
15
|
+
import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils
|
|
16
|
+
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
|
|
17
|
+
import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
|
|
18
|
+
import com.facebook.react.bridge.ReactContext
|
|
19
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
20
|
+
import com.facebook.react.views.text.TextAttributes
|
|
21
|
+
import com.facebook.react.views.view.ReactViewBackgroundDrawable
|
|
22
|
+
import java.util.Locale
|
|
23
|
+
|
|
24
|
+
internal class ReactTextPropertiesResolver(
|
|
25
|
+
private val reactContext: ReactContext,
|
|
26
|
+
private val uiManagerModule: UIManagerModule,
|
|
27
|
+
private val reflectionUtils: ReflectionUtils = ReflectionUtils(),
|
|
28
|
+
private val reactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils =
|
|
29
|
+
ReactViewBackgroundDrawableUtils(),
|
|
30
|
+
private val drawableUtils: DrawableUtils = DrawableUtils()
|
|
31
|
+
): TextPropertiesResolver {
|
|
32
|
+
override fun addReactNativeProperties(
|
|
33
|
+
originalWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
34
|
+
view: TextView,
|
|
35
|
+
pixelDensity: Float,
|
|
36
|
+
): MobileSegment.Wireframe.TextWireframe {
|
|
37
|
+
val (shapeStyle, border) = resolveShapeStyleAndBorder(view, pixelDensity)
|
|
38
|
+
?: (originalWireframe.shapeStyle to originalWireframe.border)
|
|
39
|
+
|
|
40
|
+
val (textStyle, textPosition) = resolveTextStyleAndPosition(
|
|
41
|
+
originalWireframe,
|
|
42
|
+
view,
|
|
43
|
+
pixelDensity
|
|
44
|
+
) ?: (originalWireframe.textStyle to originalWireframe.textPosition)
|
|
45
|
+
|
|
46
|
+
// nothing changed, return the original wireframe
|
|
47
|
+
@Suppress("ComplexCondition")
|
|
48
|
+
if (shapeStyle == originalWireframe.shapeStyle
|
|
49
|
+
&& border == originalWireframe.border
|
|
50
|
+
&& textStyle == originalWireframe.textStyle
|
|
51
|
+
&& textPosition == originalWireframe.textPosition
|
|
52
|
+
) {
|
|
53
|
+
return originalWireframe
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return originalWireframe.copy(
|
|
57
|
+
shapeStyle = shapeStyle,
|
|
58
|
+
border = border,
|
|
59
|
+
textStyle = textStyle,
|
|
60
|
+
textPosition = textPosition
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private fun resolveTextStyleAndPosition(
|
|
65
|
+
originalWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
66
|
+
view: TextView,
|
|
67
|
+
pixelDensity: Float,
|
|
68
|
+
):
|
|
69
|
+
Pair<MobileSegment.TextStyle, MobileSegment.TextPosition>? {
|
|
70
|
+
val shadowNodeWrapper: ShadowNodeWrapper =
|
|
71
|
+
ShadowNodeWrapper.getShadowNodeWrapper(
|
|
72
|
+
reactContext = reactContext,
|
|
73
|
+
uiManagerModule = uiManagerModule,
|
|
74
|
+
reflectionUtils = reflectionUtils,
|
|
75
|
+
viewId = view.id) ?: return null
|
|
76
|
+
|
|
77
|
+
val textStyle = resolveTextStyle(originalWireframe, pixelDensity, shadowNodeWrapper)
|
|
78
|
+
val alignment = resolveTextAlignment(view, originalWireframe)
|
|
79
|
+
|
|
80
|
+
val textPosition = MobileSegment.TextPosition(
|
|
81
|
+
alignment = alignment,
|
|
82
|
+
padding = originalWireframe.textPosition?.padding
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return textStyle to textPosition
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun resolveShapeStyleAndBorder(
|
|
89
|
+
view: TextView,
|
|
90
|
+
pixelDensity: Float,
|
|
91
|
+
): Pair<MobileSegment.ShapeStyle?, MobileSegment.ShapeBorder?>? {
|
|
92
|
+
val backgroundDrawable: ReactViewBackgroundDrawable =
|
|
93
|
+
drawableUtils.getReactBackgroundFromDrawable(view.background) ?: return null
|
|
94
|
+
|
|
95
|
+
// view.alpha is the value of the opacity prop on the js side
|
|
96
|
+
val opacity = view.alpha
|
|
97
|
+
|
|
98
|
+
val (shapeStyle, border) =
|
|
99
|
+
reactViewBackgroundDrawableUtils
|
|
100
|
+
.resolveShapeAndBorder(backgroundDrawable, opacity, pixelDensity)
|
|
101
|
+
|
|
102
|
+
return shapeStyle to border
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun resolveTextAlignment(
|
|
106
|
+
view: TextView,
|
|
107
|
+
textWireframe: MobileSegment.Wireframe.TextWireframe
|
|
108
|
+
): MobileSegment.Alignment {
|
|
109
|
+
val gravity = view.gravity
|
|
110
|
+
val horizontal = textWireframe.textPosition?.alignment?.horizontal
|
|
111
|
+
val vertical =
|
|
112
|
+
when (gravity.and(Gravity.VERTICAL_GRAVITY_MASK)) {
|
|
113
|
+
Gravity.TOP -> MobileSegment.Vertical.TOP
|
|
114
|
+
Gravity.CENTER_VERTICAL,
|
|
115
|
+
Gravity.CENTER -> MobileSegment.Vertical.CENTER
|
|
116
|
+
Gravity.BOTTOM -> MobileSegment.Vertical.BOTTOM
|
|
117
|
+
else -> MobileSegment.Vertical.TOP
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return MobileSegment.Alignment(
|
|
121
|
+
horizontal = horizontal,
|
|
122
|
+
vertical = vertical
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private fun resolveTextStyle(
|
|
127
|
+
textWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
128
|
+
pixelsDensity: Float,
|
|
129
|
+
shadowNodeWrapper: ShadowNodeWrapper
|
|
130
|
+
): MobileSegment.TextStyle {
|
|
131
|
+
val fontFamily = getFontFamily(shadowNodeWrapper)
|
|
132
|
+
?: textWireframe.textStyle.family
|
|
133
|
+
val fontSize = getFontSize(shadowNodeWrapper)
|
|
134
|
+
?.convertToDensityNormalized(pixelsDensity)
|
|
135
|
+
?: textWireframe.textStyle.size
|
|
136
|
+
val fontColor = getTextColor(shadowNodeWrapper)
|
|
137
|
+
?: textWireframe.textStyle.color
|
|
138
|
+
|
|
139
|
+
return MobileSegment.TextStyle(
|
|
140
|
+
family = fontFamily,
|
|
141
|
+
size = fontSize,
|
|
142
|
+
color = fontColor
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fun getTextColor(shadowNodeWrapper: ShadowNodeWrapper): String? {
|
|
147
|
+
val resolvedColor = shadowNodeWrapper
|
|
148
|
+
.getDeclaredShadowNodeField(COLOR_FIELD_NAME) as Int?
|
|
149
|
+
if (resolvedColor != null) {
|
|
150
|
+
return formatAsRgba(resolvedColor)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun getFontSize(shadowNodeWrapper: ShadowNodeWrapper): Long? {
|
|
157
|
+
val textAttributes = shadowNodeWrapper
|
|
158
|
+
.getDeclaredShadowNodeField(TEXT_ATTRIBUTES_FIELD_NAME) as? TextAttributes?
|
|
159
|
+
if (textAttributes != null) {
|
|
160
|
+
return textAttributes.effectiveFontSize.toLong()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private fun getFontFamily(shadowNodeWrapper: ShadowNodeWrapper): String? {
|
|
167
|
+
val fontFamily = shadowNodeWrapper
|
|
168
|
+
.getDeclaredShadowNodeField(FONT_FAMILY_FIELD_NAME) as? String
|
|
169
|
+
|
|
170
|
+
if (fontFamily != null) {
|
|
171
|
+
return resolveFontFamily(fontFamily.lowercase(Locale.US))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun resolveFontFamily(typefaceName: String): String =
|
|
178
|
+
when (typefaceName) {
|
|
179
|
+
ROBOTO_TYPEFACE_NAME -> SANS_SERIF_FAMILY_NAME
|
|
180
|
+
MONOSPACE_FAMILY_NAME -> MONOSPACE_FAMILY_NAME
|
|
181
|
+
SERIF_FAMILY_NAME -> SERIF_FAMILY_NAME
|
|
182
|
+
else -> SANS_SERIF_FAMILY_NAME
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@VisibleForTesting
|
|
186
|
+
internal companion object {
|
|
187
|
+
internal const val TEXT_ATTRIBUTES_FIELD_NAME = "mTextAttributes"
|
|
188
|
+
internal const val FONT_FAMILY_FIELD_NAME = "mFontFamily"
|
|
189
|
+
internal const val COLOR_FIELD_NAME = "mColor"
|
|
190
|
+
|
|
191
|
+
private const val ROBOTO_TYPEFACE_NAME = "roboto"
|
|
192
|
+
private const val SERIF_FAMILY_NAME = "serif"
|
|
193
|
+
private const val SANS_SERIF_FAMILY_NAME = "roboto, sans-serif"
|
|
194
|
+
internal const val MONOSPACE_FAMILY_NAME = "monospace"
|
|
195
|
+
}
|
|
196
|
+
}
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/SessionReplaySDKWrapper.kt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay
|
|
8
|
+
|
|
9
|
+
import com.datadog.android.sessionreplay.SessionReplay
|
|
10
|
+
import com.datadog.android.sessionreplay.SessionReplayConfiguration
|
|
11
|
+
|
|
12
|
+
internal class SessionReplaySDKWrapper : SessionReplayWrapper {
|
|
13
|
+
/**
|
|
14
|
+
* Enables a SessionReplay feature based on the configuration provided.
|
|
15
|
+
* @param sessionReplayConfiguration Configuration to use for the feature.
|
|
16
|
+
*/
|
|
17
|
+
override fun enable(
|
|
18
|
+
sessionReplayConfiguration: SessionReplayConfiguration,
|
|
19
|
+
) {
|
|
20
|
+
SessionReplay.enable(
|
|
21
|
+
sessionReplayConfiguration,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay
|
|
8
|
+
|
|
9
|
+
import com.datadog.android.sessionreplay.SessionReplayConfiguration
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wrapper around [SessionReplay].
|
|
13
|
+
*/
|
|
14
|
+
interface SessionReplayWrapper {
|
|
15
|
+
/**
|
|
16
|
+
* Enables a SessionReplay feature based on the configuration provided.
|
|
17
|
+
* @param sessionReplayConfiguration Configuration to use for the feature.
|
|
18
|
+
*/
|
|
19
|
+
fun enable(
|
|
20
|
+
sessionReplayConfiguration: SessionReplayConfiguration,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
*
|
|
3
|
+
* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
|
|
4
|
+
* * This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
5
|
+
* * Copyright 2016-Present Datadog, Inc.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
package com.datadog.reactnative.sessionreplay
|
|
10
|
+
|
|
11
|
+
import androidx.annotation.VisibleForTesting
|
|
12
|
+
import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
|
|
13
|
+
import com.facebook.react.bridge.ReactContext
|
|
14
|
+
import com.facebook.react.uimanager.ReactShadowNode
|
|
15
|
+
import com.facebook.react.uimanager.UIImplementation
|
|
16
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
17
|
+
import java.util.concurrent.CountDownLatch
|
|
18
|
+
import java.util.concurrent.TimeUnit
|
|
19
|
+
|
|
20
|
+
internal class ShadowNodeWrapper(
|
|
21
|
+
private val shadowNode: ReactShadowNode<out ReactShadowNode<*>>?,
|
|
22
|
+
private val reflectionUtils: ReflectionUtils = ReflectionUtils()
|
|
23
|
+
) {
|
|
24
|
+
internal fun getDeclaredShadowNodeField(fieldName: String): Any? {
|
|
25
|
+
return shadowNode?.let {
|
|
26
|
+
reflectionUtils.getDeclaredField(
|
|
27
|
+
shadowNode,
|
|
28
|
+
fieldName
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
internal companion object {
|
|
34
|
+
internal fun getShadowNodeWrapper(
|
|
35
|
+
reactContext: ReactContext,
|
|
36
|
+
uiManagerModule: UIManagerModule,
|
|
37
|
+
reflectionUtils: ReflectionUtils,
|
|
38
|
+
viewId: Int
|
|
39
|
+
): ShadowNodeWrapper? {
|
|
40
|
+
val countDownLatch = CountDownLatch(1)
|
|
41
|
+
var target: ReactShadowNode<out ReactShadowNode<*>>? = null
|
|
42
|
+
|
|
43
|
+
val shadowNodeRunnable = Runnable {
|
|
44
|
+
val node = resolveShadowNode(reflectionUtils, uiManagerModule, viewId)
|
|
45
|
+
if (node != null) {
|
|
46
|
+
target = node
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
countDownLatch.countDown()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
reactContext.runOnNativeModulesQueueThread(shadowNodeRunnable)
|
|
53
|
+
countDownLatch.await(5, TimeUnit.SECONDS)
|
|
54
|
+
|
|
55
|
+
if (target == null) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return ShadowNodeWrapper(reflectionUtils = reflectionUtils, shadowNode = target)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private fun resolveShadowNode(reflectionUtils: ReflectionUtils, uiManagerModule: UIManagerModule, tag: Int): ReactShadowNode<out ReactShadowNode<*>>? {
|
|
63
|
+
val uiManagerImplementation = reflectionUtils.getDeclaredField(uiManagerModule, UI_IMPLEMENTATION_FIELD_NAME) as UIImplementation?
|
|
64
|
+
return uiManagerImplementation?.resolveShadowNode(tag)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@VisibleForTesting
|
|
68
|
+
internal const val UI_IMPLEMENTATION_FIELD_NAME = "mUIImplementation"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/TextPropertiesResolver.kt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
*
|
|
3
|
+
* * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
|
|
4
|
+
* * This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
5
|
+
* * Copyright 2016-Present Datadog, Inc.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
package com.datadog.reactnative.sessionreplay
|
|
10
|
+
|
|
11
|
+
import android.widget.TextView
|
|
12
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
13
|
+
|
|
14
|
+
internal interface TextPropertiesResolver {
|
|
15
|
+
fun addReactNativeProperties(
|
|
16
|
+
originalWireframe: MobileSegment.Wireframe.TextWireframe,
|
|
17
|
+
view: TextView,
|
|
18
|
+
pixelDensity: Float,
|
|
19
|
+
): MobileSegment.Wireframe.TextWireframe
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay.extensions
|
|
8
|
+
|
|
9
|
+
internal fun Long.convertToDensityNormalized(density: Float): Long {
|
|
10
|
+
return if (density == 0f) {
|
|
11
|
+
this
|
|
12
|
+
} else {
|
|
13
|
+
(this / density).toLong()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay.mappers
|
|
8
|
+
|
|
9
|
+
import android.widget.TextView
|
|
10
|
+
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
|
|
11
|
+
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
|
|
12
|
+
import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskInputTextViewMapper
|
|
13
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
14
|
+
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
|
|
15
|
+
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
|
|
16
|
+
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
|
|
17
|
+
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
|
|
18
|
+
import com.facebook.react.bridge.ReactContext
|
|
19
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
20
|
+
|
|
21
|
+
internal class ReactMaskInputTextMapper(
|
|
22
|
+
private val reactTextPropertiesResolver: TextPropertiesResolver,
|
|
23
|
+
private val textViewUtils: TextViewUtils = TextViewUtils()
|
|
24
|
+
): MaskInputTextViewMapper() {
|
|
25
|
+
|
|
26
|
+
internal constructor(
|
|
27
|
+
reactContext: ReactContext,
|
|
28
|
+
uiManagerModule: UIManagerModule?
|
|
29
|
+
): this(
|
|
30
|
+
reactTextPropertiesResolver = if (uiManagerModule == null) {
|
|
31
|
+
NoopTextPropertiesResolver()
|
|
32
|
+
} else {
|
|
33
|
+
ReactTextPropertiesResolver(
|
|
34
|
+
reactContext = reactContext,
|
|
35
|
+
uiManagerModule = uiManagerModule
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
override fun map(
|
|
41
|
+
view: TextView,
|
|
42
|
+
mappingContext: MappingContext,
|
|
43
|
+
asyncJobStatusCallback: AsyncJobStatusCallback
|
|
44
|
+
): List<MobileSegment.Wireframe> {
|
|
45
|
+
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback)
|
|
46
|
+
return textViewUtils.mapTextViewToWireframes(
|
|
47
|
+
wireframes = wireframes,
|
|
48
|
+
view = view,
|
|
49
|
+
mappingContext = mappingContext,
|
|
50
|
+
reactTextPropertiesResolver = reactTextPropertiesResolver
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactMaskTextMapper.kt
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay.mappers
|
|
8
|
+
|
|
9
|
+
import android.widget.TextView
|
|
10
|
+
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
|
|
11
|
+
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
|
|
12
|
+
import com.datadog.android.sessionreplay.internal.recorder.mapper.MaskTextViewMapper
|
|
13
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
14
|
+
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
|
|
15
|
+
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
|
|
16
|
+
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
|
|
17
|
+
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
|
|
18
|
+
import com.facebook.react.bridge.ReactContext
|
|
19
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
20
|
+
|
|
21
|
+
internal class ReactMaskTextMapper(
|
|
22
|
+
private val reactTextPropertiesResolver: TextPropertiesResolver =
|
|
23
|
+
NoopTextPropertiesResolver(),
|
|
24
|
+
private val textViewUtils: TextViewUtils = TextViewUtils()
|
|
25
|
+
): MaskTextViewMapper() {
|
|
26
|
+
|
|
27
|
+
internal constructor(
|
|
28
|
+
reactContext: ReactContext,
|
|
29
|
+
uiManagerModule: UIManagerModule?
|
|
30
|
+
): this(
|
|
31
|
+
reactTextPropertiesResolver = if (uiManagerModule == null) {
|
|
32
|
+
NoopTextPropertiesResolver()
|
|
33
|
+
} else {
|
|
34
|
+
ReactTextPropertiesResolver(
|
|
35
|
+
reactContext = reactContext,
|
|
36
|
+
uiManagerModule = uiManagerModule
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
override fun map(
|
|
42
|
+
view: TextView,
|
|
43
|
+
mappingContext: MappingContext,
|
|
44
|
+
asyncJobStatusCallback: AsyncJobStatusCallback
|
|
45
|
+
): List<MobileSegment.Wireframe> {
|
|
46
|
+
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback)
|
|
47
|
+
return textViewUtils.mapTextViewToWireframes(
|
|
48
|
+
wireframes = wireframes,
|
|
49
|
+
view = view,
|
|
50
|
+
mappingContext = mappingContext,
|
|
51
|
+
reactTextPropertiesResolver = reactTextPropertiesResolver
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactTextMapper.kt
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay.mappers
|
|
8
|
+
|
|
9
|
+
import android.widget.TextView
|
|
10
|
+
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
|
|
11
|
+
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
|
|
12
|
+
import com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper
|
|
13
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
14
|
+
import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
|
|
15
|
+
import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
|
|
16
|
+
import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
|
|
17
|
+
import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
|
|
18
|
+
import com.facebook.react.bridge.ReactContext
|
|
19
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
20
|
+
|
|
21
|
+
internal class ReactTextMapper(
|
|
22
|
+
private val reactTextPropertiesResolver: TextPropertiesResolver =
|
|
23
|
+
NoopTextPropertiesResolver(),
|
|
24
|
+
private val textViewUtils: TextViewUtils = TextViewUtils()
|
|
25
|
+
): TextViewMapper() {
|
|
26
|
+
|
|
27
|
+
internal constructor(
|
|
28
|
+
reactContext: ReactContext,
|
|
29
|
+
uiManagerModule: UIManagerModule?
|
|
30
|
+
): this(
|
|
31
|
+
reactTextPropertiesResolver = if (uiManagerModule == null) {
|
|
32
|
+
NoopTextPropertiesResolver()
|
|
33
|
+
} else {
|
|
34
|
+
ReactTextPropertiesResolver(
|
|
35
|
+
reactContext = reactContext,
|
|
36
|
+
uiManagerModule = uiManagerModule
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
override fun map(
|
|
42
|
+
view: TextView,
|
|
43
|
+
mappingContext: MappingContext,
|
|
44
|
+
asyncJobStatusCallback: AsyncJobStatusCallback
|
|
45
|
+
): List<MobileSegment.Wireframe> {
|
|
46
|
+
val wireframes = super.map(view, mappingContext, asyncJobStatusCallback)
|
|
47
|
+
return textViewUtils.mapTextViewToWireframes(
|
|
48
|
+
wireframes = wireframes,
|
|
49
|
+
view = view,
|
|
50
|
+
mappingContext = mappingContext,
|
|
51
|
+
reactTextPropertiesResolver = reactTextPropertiesResolver
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
|
|
7
|
+
package com.datadog.reactnative.sessionreplay.mappers
|
|
8
|
+
|
|
9
|
+
import com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback
|
|
10
|
+
import com.datadog.android.sessionreplay.internal.recorder.MappingContext
|
|
11
|
+
import com.datadog.android.sessionreplay.internal.recorder.mapper.BaseWireframeMapper
|
|
12
|
+
import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper
|
|
13
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
14
|
+
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
15
|
+
import com.datadog.reactnative.sessionreplay.utils.ReactViewBackgroundDrawableUtils
|
|
16
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
17
|
+
|
|
18
|
+
internal class ReactViewGroupMapper(
|
|
19
|
+
private val reactViewBackgroundDrawableUtils: ReactViewBackgroundDrawableUtils =
|
|
20
|
+
ReactViewBackgroundDrawableUtils(),
|
|
21
|
+
private val drawableUtils: DrawableUtils = DrawableUtils()
|
|
22
|
+
) :
|
|
23
|
+
BaseWireframeMapper<ReactViewGroup, MobileSegment.Wireframe>(),
|
|
24
|
+
TraverseAllChildrenMapper<ReactViewGroup, MobileSegment.Wireframe> {
|
|
25
|
+
|
|
26
|
+
override fun map(
|
|
27
|
+
view: ReactViewGroup,
|
|
28
|
+
mappingContext: MappingContext,
|
|
29
|
+
asyncJobStatusCallback: AsyncJobStatusCallback
|
|
30
|
+
): List<MobileSegment.Wireframe> {
|
|
31
|
+
val pixelDensity = mappingContext.systemInformation.screenDensity
|
|
32
|
+
val viewGlobalBounds = resolveViewGlobalBounds(view, pixelDensity)
|
|
33
|
+
val backgroundDrawable = drawableUtils.getReactBackgroundFromDrawable(view.background)
|
|
34
|
+
|
|
35
|
+
// view.alpha is the value of the opacity prop on the js side
|
|
36
|
+
val opacity = view.alpha
|
|
37
|
+
|
|
38
|
+
val (shapeStyle, border) =
|
|
39
|
+
if (backgroundDrawable != null) {
|
|
40
|
+
reactViewBackgroundDrawableUtils
|
|
41
|
+
.resolveShapeAndBorder(backgroundDrawable, opacity, pixelDensity)
|
|
42
|
+
} else {
|
|
43
|
+
null to null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return listOf(
|
|
47
|
+
MobileSegment.Wireframe.ShapeWireframe(
|
|
48
|
+
resolveViewId(view),
|
|
49
|
+
viewGlobalBounds.x,
|
|
50
|
+
viewGlobalBounds.y,
|
|
51
|
+
viewGlobalBounds.width,
|
|
52
|
+
viewGlobalBounds.height,
|
|
53
|
+
shapeStyle = shapeStyle,
|
|
54
|
+
border = border
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|