@datadog/mobile-react-native-session-replay 2.12.4 → 2.13.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 +6 -2
- package/README.md +90 -1
- package/android/build.gradle +3 -2
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +2 -2
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeInternalCallback.kt +121 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +5 -1
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt +1 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/SvgViewMapper.kt +145 -0
- package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/views/DdPrivacyView.kt +10 -0
- package/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/views/DdPrivacyViewManager.kt +13 -0
- package/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/views/DdPrivacyViewManager.kt +13 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt +8 -0
- package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +21 -5
- package/assets/assets.bin +0 -0
- package/assets/assets.json +1 -0
- package/ios/Sources/DdPrivacyViewFabric.h +28 -0
- package/ios/Sources/DdPrivacyViewFabric.mm +22 -4
- package/ios/Sources/DdPrivacyViewPaper.m +24 -0
- package/ios/Sources/DdSessionReplayImplementation.swift +34 -6
- package/ios/Sources/SvgViewRecorder.swift +233 -0
- package/ios/Sources/Utils/Bundle+SessionReplay.swift +21 -0
- package/lib/commonjs/components/SessionReplayView/PrivacyView.js +2 -0
- package/lib/commonjs/components/SessionReplayView/PrivacyView.js.map +1 -1
- package/lib/commonjs/metro/index.js +75 -0
- package/lib/commonjs/metro/index.js.map +1 -0
- package/lib/commonjs/metro/processing.js +97 -0
- package/lib/commonjs/metro/processing.js.map +1 -0
- package/lib/commonjs/metro/utils.js +20 -0
- package/lib/commonjs/metro/utils.js.map +1 -0
- package/lib/commonjs/specs/DdPrivacyViewNativeComponent.js.map +1 -1
- package/lib/module/components/SessionReplayView/PrivacyView.js +2 -0
- package/lib/module/components/SessionReplayView/PrivacyView.js.map +1 -1
- package/lib/module/metro/index.js +68 -0
- package/lib/module/metro/index.js.map +1 -0
- package/lib/module/metro/processing.js +90 -0
- package/lib/module/metro/processing.js.map +1 -0
- package/lib/module/metro/utils.js +14 -0
- package/lib/module/metro/utils.js.map +1 -0
- package/lib/module/specs/DdPrivacyViewNativeComponent.js.map +1 -1
- package/lib/typescript/components/SessionReplayView/PrivacyView.d.ts +10 -1
- package/lib/typescript/components/SessionReplayView/PrivacyView.d.ts.map +1 -1
- package/lib/typescript/metro/index.d.ts +2 -0
- package/lib/typescript/metro/index.d.ts.map +1 -0
- package/lib/typescript/metro/processing.d.ts +9 -0
- package/lib/typescript/metro/processing.d.ts.map +1 -0
- package/lib/typescript/metro/utils.d.ts +2 -0
- package/lib/typescript/metro/utils.d.ts.map +1 -0
- package/lib/typescript/specs/DdPrivacyViewNativeComponent.d.ts +8 -0
- package/lib/typescript/specs/DdPrivacyViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/types/DdPrivacyView.d.ts +8 -0
- package/lib/typescript/types/DdPrivacyView.d.ts.map +1 -1
- package/metro.js +7 -0
- package/package.json +9 -3
- package/scripts/build-assets.js +31 -0
- package/src/components/SessionReplayView/PrivacyView.tsx +11 -0
- package/src/metro/index.ts +82 -0
- package/src/metro/processing.ts +119 -0
- package/src/metro/utils.ts +17 -0
- package/src/specs/DdPrivacyViewNativeComponent.ts +9 -0
- package/src/types/DdPrivacyView.ts +9 -0
|
@@ -14,12 +14,16 @@ Pod::Spec.new do |s|
|
|
|
14
14
|
s.platforms = { :ios => "12.0", :tvos => "12.0" }
|
|
15
15
|
s.source = { :git => "https://github.com/DataDog/dd-sdk-reactnative.git", :tag => "#{s.version}" }
|
|
16
16
|
|
|
17
|
-
s.source_files = "ios/Sources
|
|
17
|
+
s.source_files = "ios/Sources/**/*.{h,m,mm,swift}"
|
|
18
|
+
|
|
19
|
+
s.resource_bundles = {
|
|
20
|
+
'DDSessionReplay' => ['assets/assets.json', 'assets/assets.bin']
|
|
21
|
+
}
|
|
18
22
|
|
|
19
23
|
s.dependency "React-Core"
|
|
20
24
|
|
|
21
25
|
# /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec
|
|
22
|
-
s.dependency 'DatadogSessionReplay', '2.30.
|
|
26
|
+
s.dependency 'DatadogSessionReplay', '2.30.2'
|
|
23
27
|
s.dependency 'DatadogSDKReactNative'
|
|
24
28
|
|
|
25
29
|
s.test_spec 'Tests' do |test_spec|
|
package/README.md
CHANGED
|
@@ -51,4 +51,93 @@ SessionReplay.startRecording();
|
|
|
51
51
|
SessionReplay.stopRecording();
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
## SVG Support
|
|
55
|
+
|
|
56
|
+
Session Replay provides enhanced support for capturing SVG images in your React Native application. To enable SVG tracking, you need to set up the Datadog Babel plugin and Metro plugin.
|
|
57
|
+
|
|
58
|
+
### Prerequisites
|
|
59
|
+
|
|
60
|
+
Install the Datadog Babel plugin:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm install @datadog/mobile-react-native-babel-plugin
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
or with Yarn:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
yarn add @datadog/mobile-react-native-babel-plugin
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Setup Babel Plugin
|
|
73
|
+
|
|
74
|
+
Configure the Babel plugin in your `babel.config.js` to enable SVG tracking:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
module.exports = {
|
|
78
|
+
presets: ['module:@react-native/babel-preset'],
|
|
79
|
+
plugins: [
|
|
80
|
+
[
|
|
81
|
+
'@datadog/mobile-react-native-babel-plugin',
|
|
82
|
+
{
|
|
83
|
+
sessionReplay: {
|
|
84
|
+
// SVG tracking is disabled by default
|
|
85
|
+
// Set to true to enable SVG asset extraction
|
|
86
|
+
svgTracking: true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Setup Metro Plugin
|
|
95
|
+
|
|
96
|
+
Configure the Metro plugin in your `metro.config.js` to enable automatic SVG asset bundling:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const { withSessionReplayAssetBundler } = require('@datadog/mobile-react-native-session-replay/metro');
|
|
100
|
+
|
|
101
|
+
module.exports = withSessionReplayAssetBundler({
|
|
102
|
+
/* your existing Metro config */
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The Metro plugin automatically monitors and bundles SVG assets during development and production builds.
|
|
107
|
+
|
|
108
|
+
### Installation Workflow
|
|
109
|
+
|
|
110
|
+
When setting up your project or after installing new dependencies, follow this workflow to ensure SVG assets are properly generated for native builds:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
# 1. Install dependencies
|
|
114
|
+
yarn install
|
|
115
|
+
|
|
116
|
+
# 2. Generate Session Replay SVG assets
|
|
117
|
+
npx datadog-generate-sr-assets
|
|
118
|
+
|
|
119
|
+
# 3. Install iOS pods (if building for iOS)
|
|
120
|
+
cd ios && pod install && cd ..
|
|
121
|
+
|
|
122
|
+
# 4. Run your app
|
|
123
|
+
yarn ios
|
|
124
|
+
# or
|
|
125
|
+
yarn android
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The `datadog-generate-sr-assets` CLI utility scans your codebase for SVG elements and pre-generates optimized assets that will be included in your native builds.
|
|
129
|
+
|
|
130
|
+
**Note for CI/CD**: If you use continuous integration for your builds, make sure to include these steps in your CI pipeline. The workflow should be: `yarn install` → `npx datadog-generate-sr-assets` → `pod install` (for iOS) → build your app. This ensures SVG assets are properly generated before the native build process.
|
|
131
|
+
|
|
132
|
+
### Development Workflow
|
|
133
|
+
|
|
134
|
+
During development, the Metro plugin automatically handles SVG assets created by the Babel plugin:
|
|
135
|
+
|
|
136
|
+
1. Write your components with SVG elements from `react-native-svg`
|
|
137
|
+
2. The Babel plugin extracts and transforms SVG nodes during the build process
|
|
138
|
+
3. The Metro plugin detects new SVG assets and automatically bundles them
|
|
139
|
+
4. SVG images are seamlessly captured in Session Replay recordings
|
|
140
|
+
|
|
141
|
+
No manual asset management is required during development.
|
|
142
|
+
|
|
143
|
+
[1]: https://www.npmjs.com/package/@datadog/mobile-react-native
|
package/android/build.gradle
CHANGED
|
@@ -158,6 +158,7 @@ android {
|
|
|
158
158
|
java.srcDirs += ['src/rnpre74/kotlin']
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
assets.srcDirs += ['../assets']
|
|
161
162
|
}
|
|
162
163
|
test {
|
|
163
164
|
java.srcDir("src/test/kotlin")
|
|
@@ -213,8 +214,8 @@ dependencies {
|
|
|
213
214
|
api "com.facebook.react:react-android:$reactNativeVersion"
|
|
214
215
|
}
|
|
215
216
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
216
|
-
implementation "com.datadoghq:dd-sdk-android-session-replay:2.
|
|
217
|
-
implementation "com.datadoghq:dd-sdk-android-internal:2.
|
|
217
|
+
implementation "com.datadoghq:dd-sdk-android-session-replay:2.26.2"
|
|
218
|
+
implementation "com.datadoghq:dd-sdk-android-internal:2.26.2"
|
|
218
219
|
implementation project(path: ':datadog_mobile-react-native')
|
|
219
220
|
|
|
220
221
|
testImplementation "org.junit.platform:junit-platform-launcher:1.6.2"
|
|
@@ -49,17 +49,17 @@ class DdSessionReplayImplementation(
|
|
|
49
49
|
.setImagePrivacy(privacySettings.imagePrivacyLevel)
|
|
50
50
|
.setTouchPrivacy(privacySettings.touchPrivacyLevel)
|
|
51
51
|
.setTextAndInputPrivacy(privacySettings.textAndInputPrivacyLevel)
|
|
52
|
-
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(textViewUtils))
|
|
52
|
+
.addExtensionSupport(ReactNativeSessionReplayExtensionSupport(textViewUtils, internalCallback))
|
|
53
53
|
.let {
|
|
54
54
|
_SessionReplayInternalProxy(it).setInternalCallback(internalCallback)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
57
|
if (customEndpoint != "") {
|
|
59
58
|
configuration.useCustomEndpoint(customEndpoint)
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
sessionReplayProvider().enable(configuration.build(), sdkCore)
|
|
62
|
+
|
|
63
63
|
promise.resolve(null)
|
|
64
64
|
}
|
|
65
65
|
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeInternalCallback.kt
CHANGED
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
package com.datadog.reactnative.sessionreplay
|
|
8
8
|
|
|
9
9
|
import android.app.Activity
|
|
10
|
+
import android.util.Log
|
|
10
11
|
import com.datadog.android.sessionreplay.SessionReplayInternalCallback
|
|
12
|
+
import com.datadog.android.sessionreplay.SessionReplayInternalResourceQueue
|
|
11
13
|
import com.facebook.react.bridge.ReactContext
|
|
14
|
+
import org.json.JSONException
|
|
15
|
+
import org.json.JSONObject
|
|
16
|
+
import java.io.IOException
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Responsible for defining the internal callback implementation for react-native that will allow
|
|
@@ -17,7 +22,123 @@ import com.facebook.react.bridge.ReactContext
|
|
|
17
22
|
class ReactNativeInternalCallback(
|
|
18
23
|
private val reactContext: ReactContext,
|
|
19
24
|
) : SessionReplayInternalCallback {
|
|
25
|
+
private var resourceQueue: SessionReplayInternalResourceQueue? = null
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Companion object containing constants used by the internal callback implementation.
|
|
29
|
+
*/
|
|
30
|
+
companion object {
|
|
31
|
+
private const val ASSETS_JSON = "assets.json"
|
|
32
|
+
private const val ASSETS_BIN = "assets.bin"
|
|
33
|
+
private const val TAG = "SessionReplayInternalCallback"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private val jsonObject: JSONObject? = loadAssetsJson()
|
|
37
|
+
|
|
38
|
+
private fun loadAssetsJson(): JSONObject? {
|
|
39
|
+
return try {
|
|
40
|
+
val jsonText = reactContext.assets.open(ASSETS_JSON)
|
|
41
|
+
.bufferedReader()
|
|
42
|
+
.use { it.readText() }
|
|
43
|
+
JSONObject(jsonText)
|
|
44
|
+
} catch (e: IOException) {
|
|
45
|
+
Log.w(TAG, "Failed to read $ASSETS_JSON from assets: ${e.message}")
|
|
46
|
+
null
|
|
47
|
+
} catch (e: JSONException) {
|
|
48
|
+
Log.w(TAG, "Invalid JSON in $ASSETS_JSON: ${e.message}")
|
|
49
|
+
null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the JSON entry for a given asset key, if it exists in the index.
|
|
55
|
+
*
|
|
56
|
+
* @param key The unique asset key to look up (as defined in `assets.json`).
|
|
57
|
+
* @return A [JSONObject] containing the metadata for the asset, or `null` if not found.
|
|
58
|
+
*/
|
|
59
|
+
fun getJSONEntry(key: String): JSONObject? {
|
|
60
|
+
val json = jsonObject ?: return null
|
|
61
|
+
return if (json.has(key)) json.getJSONObject(key) else null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reads the raw byte data from `assets.bin` corresponding to an entry in `assets.json`.
|
|
66
|
+
* Uses the `offset` and `length` fields in the JSON entry to extract the exact byte
|
|
67
|
+
* range for the requested asset.
|
|
68
|
+
*
|
|
69
|
+
* @param key The unique asset key defined in `assets.json`.
|
|
70
|
+
* @return A [ByteArray] containing the binary data for the asset, or `null` if the key
|
|
71
|
+
* is missing or the read operation fails.
|
|
72
|
+
*/
|
|
73
|
+
fun getEntryData(key: String): ByteArray? {
|
|
74
|
+
val entry = getJSONEntry(key) ?: return null
|
|
75
|
+
|
|
76
|
+
val offset = entry.optInt("offset", -1)
|
|
77
|
+
val length = entry.optInt("length", -1)
|
|
78
|
+
|
|
79
|
+
if (offset < 0 || length <= 0) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return try {
|
|
84
|
+
readAssetBytes(reactContext, offset, length)
|
|
85
|
+
} catch (e: IOException) {
|
|
86
|
+
Log.w(TAG, "Failed to read entry from binary: ${e.message}")
|
|
87
|
+
null
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
private fun readAssetBytes(context: ReactContext, offset: Int, length: Int): ByteArray {
|
|
93
|
+
context.assets.open(ASSETS_BIN).use { input ->
|
|
94
|
+
var skipped = 0L
|
|
95
|
+
while (skipped < offset) {
|
|
96
|
+
val result = input.skip((offset - skipped).toLong())
|
|
97
|
+
if (result <= 0) throw IOException("Unable to skip to offset $offset in $ASSETS_BIN")
|
|
98
|
+
skipped += result
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read `length` bytes
|
|
102
|
+
val buffer = ByteArray(length)
|
|
103
|
+
var bytesRead = 0
|
|
104
|
+
while (bytesRead < length) {
|
|
105
|
+
val read = input.read(buffer, bytesRead, length - bytesRead)
|
|
106
|
+
if (read == -1) break
|
|
107
|
+
bytesRead += read
|
|
108
|
+
}
|
|
109
|
+
return buffer
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Adds a resource item to the current [SessionReplayInternalResourceQueue].
|
|
115
|
+
*
|
|
116
|
+
* @param identifier A unique identifier for the resource.
|
|
117
|
+
* @param resourceData The binary content of the resource to enqueue.
|
|
118
|
+
* @param mimeType Optional MIME type describing the resource (e.g., `"image/png"` or `"image/svg+xml"`).
|
|
119
|
+
*/
|
|
120
|
+
override fun addResourceItem(identifier: String, resourceData: ByteArray, mimeType: String?) {
|
|
121
|
+
this.resourceQueue?.addResourceItem(identifier, resourceData, mimeType)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Retrieves the current activity from the React context.
|
|
126
|
+
* Used by the Session Replay system to access the active UI context when
|
|
127
|
+
* registering lifecycle listeners.
|
|
128
|
+
*
|
|
129
|
+
* @return The currently active [Activity], or `null` if none is available.
|
|
130
|
+
*/
|
|
20
131
|
override fun getCurrentActivity(): Activity? {
|
|
21
132
|
return reactContext.currentActivity
|
|
22
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Sets the resource queue to be used by this callback.
|
|
137
|
+
*
|
|
138
|
+
* @param resourceQueue The [SessionReplayInternalResourceQueue] responsible
|
|
139
|
+
* for handling resources.
|
|
140
|
+
*/
|
|
141
|
+
override fun setResourceQueue(resourceQueue: SessionReplayInternalResourceQueue) {
|
|
142
|
+
this.resourceQueue = resourceQueue
|
|
143
|
+
}
|
|
23
144
|
}
|
|
@@ -15,7 +15,9 @@ import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper
|
|
|
15
15
|
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
|
|
16
16
|
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
|
|
17
17
|
import com.datadog.reactnative.sessionreplay.mappers.ReactViewModalMapper
|
|
18
|
+
import com.datadog.reactnative.sessionreplay.mappers.SvgViewMapper
|
|
18
19
|
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
|
|
20
|
+
import com.datadog.reactnative.sessionreplay.views.DdPrivacyView
|
|
19
21
|
import com.facebook.react.views.image.ReactImageView
|
|
20
22
|
import com.facebook.react.views.modal.ReactModalHostView
|
|
21
23
|
import com.facebook.react.views.text.ReactTextView
|
|
@@ -24,7 +26,8 @@ import com.facebook.react.views.view.ReactViewGroup
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
internal class ReactNativeSessionReplayExtensionSupport(
|
|
27
|
-
private val textViewUtils: TextViewUtils
|
|
29
|
+
private val textViewUtils: TextViewUtils,
|
|
30
|
+
private val internalCallback: ReactNativeInternalCallback
|
|
28
31
|
) : ExtensionSupport {
|
|
29
32
|
override fun name(): String {
|
|
30
33
|
return ReactNativeSessionReplayExtensionSupport::class.java.simpleName
|
|
@@ -33,6 +36,7 @@ internal class ReactNativeSessionReplayExtensionSupport(
|
|
|
33
36
|
override fun getCustomViewMappers(): List<MapperTypeWrapper<*>> {
|
|
34
37
|
return listOf(
|
|
35
38
|
MapperTypeWrapper(ReactImageView::class.java, ReactNativeImageViewMapper()),
|
|
39
|
+
MapperTypeWrapper(DdPrivacyView::class.java, SvgViewMapper(internalCallback)),
|
|
36
40
|
MapperTypeWrapper(ReactViewGroup::class.java, ReactViewGroupMapper()),
|
|
37
41
|
MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(textViewUtils)),
|
|
38
42
|
MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(textViewUtils)),
|
|
@@ -8,6 +8,7 @@ package com.datadog.reactnative.sessionreplay.mappers
|
|
|
8
8
|
|
|
9
9
|
import ReactViewBackgroundDrawableUtils
|
|
10
10
|
import com.datadog.android.sessionreplay.recorder.mapper.TraverseAllChildrenMapper
|
|
11
|
+
import com.datadog.reactnative.sessionreplay.ReactNativeInternalCallback
|
|
11
12
|
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
12
13
|
import com.facebook.react.views.view.ReactViewGroup
|
|
13
14
|
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/SvgViewMapper.kt
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package com.datadog.reactnative.sessionreplay.mappers
|
|
2
|
+
|
|
3
|
+
import ReactViewBackgroundDrawableUtils
|
|
4
|
+
import android.view.View
|
|
5
|
+
import com.datadog.android.api.InternalLogger
|
|
6
|
+
import com.datadog.android.sessionreplay.model.MobileSegment
|
|
7
|
+
import com.datadog.android.sessionreplay.recorder.MappingContext
|
|
8
|
+
import com.datadog.android.sessionreplay.recorder.mapper.BaseWireframeMapper
|
|
9
|
+
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
|
|
10
|
+
import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter
|
|
11
|
+
import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
|
|
12
|
+
import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver.resolveViewGlobalBounds
|
|
13
|
+
import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
|
|
14
|
+
import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
|
|
15
|
+
import com.datadog.reactnative.sessionreplay.ReactNativeInternalCallback
|
|
16
|
+
import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
|
|
17
|
+
import com.datadog.reactnative.sessionreplay.views.DdPrivacyView
|
|
18
|
+
import java.util.Collections
|
|
19
|
+
|
|
20
|
+
internal open class SvgViewMapper<T: View>(
|
|
21
|
+
private val internalCallback: ReactNativeInternalCallback,
|
|
22
|
+
private val drawableUtils: DrawableUtils =
|
|
23
|
+
ReactViewBackgroundDrawableUtils()
|
|
24
|
+
): BaseWireframeMapper<T>(
|
|
25
|
+
viewIdentifierResolver = DefaultViewIdentifierResolver,
|
|
26
|
+
colorStringFormatter = DefaultColorStringFormatter,
|
|
27
|
+
viewBoundsResolver = DefaultViewBoundsResolver,
|
|
28
|
+
drawableToColorMapper = DrawableToColorMapper.getDefault()
|
|
29
|
+
) {
|
|
30
|
+
private val queuedResourceIds = Collections.synchronizedSet(HashSet<String>())
|
|
31
|
+
|
|
32
|
+
@Suppress("LongMethod", "ComplexMethod")
|
|
33
|
+
override fun map(
|
|
34
|
+
view: T,
|
|
35
|
+
mappingContext: MappingContext,
|
|
36
|
+
asyncJobStatusCallback: AsyncJobStatusCallback,
|
|
37
|
+
internalLogger: InternalLogger
|
|
38
|
+
): List<MobileSegment.Wireframe> {
|
|
39
|
+
val pixelDensity = mappingContext.systemInformation.screenDensity
|
|
40
|
+
val viewGlobalBounds = resolveViewGlobalBounds(view, pixelDensity)
|
|
41
|
+
val backgroundDrawable = drawableUtils.getReactBackgroundFromDrawable(view.background)
|
|
42
|
+
|
|
43
|
+
val opacity = view.alpha
|
|
44
|
+
|
|
45
|
+
val (shapeStyle, border) =
|
|
46
|
+
if (backgroundDrawable != null) {
|
|
47
|
+
drawableUtils
|
|
48
|
+
.resolveShapeAndBorder(backgroundDrawable, opacity, pixelDensity)
|
|
49
|
+
} else {
|
|
50
|
+
null to null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
val wireframes = mutableListOf<MobileSegment.Wireframe>()
|
|
54
|
+
|
|
55
|
+
if (view is DdPrivacyView) {
|
|
56
|
+
val hash = view.attributes?.get("hash") ?: return wireframes
|
|
57
|
+
val width = view.attributes?.get("width")
|
|
58
|
+
val height = view.attributes?.get("height")
|
|
59
|
+
|
|
60
|
+
var entryData = internalCallback.getEntryData(hash)
|
|
61
|
+
?: return wireframes
|
|
62
|
+
|
|
63
|
+
// This is always guaranteed to be true due to how the babel plugin transformed the data
|
|
64
|
+
val subView = view.getChildAt(0) ?: return wireframes
|
|
65
|
+
val imageBounds = resolveViewGlobalBounds(subView, pixelDensity)
|
|
66
|
+
var entryStr: String? = null
|
|
67
|
+
|
|
68
|
+
if (width == null) {
|
|
69
|
+
entryStr = injectSvgDimensions(entryData.toString(Charsets.UTF_8), imageBounds.width.toInt(), null)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (height == null) {
|
|
73
|
+
entryStr = injectSvgDimensions(entryStr ?: entryData.toString(Charsets.UTF_8), null, imageBounds.height.toInt())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (entryStr != null) {
|
|
77
|
+
// Here we update the svg content but keep the original hash without these values
|
|
78
|
+
// The goal is to save some time, as it won't matter since the hash is used as an identifier
|
|
79
|
+
entryData = entryStr.toByteArray(Charsets.UTF_8);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
wireframes.add(MobileSegment.Wireframe.ShapeWireframe(
|
|
83
|
+
resolveViewId(view),
|
|
84
|
+
viewGlobalBounds.x,
|
|
85
|
+
viewGlobalBounds.y,
|
|
86
|
+
viewGlobalBounds.width,
|
|
87
|
+
viewGlobalBounds.height,
|
|
88
|
+
shapeStyle = shapeStyle,
|
|
89
|
+
border = border
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
val imgWireframe = MobileSegment.Wireframe.ImageWireframe(
|
|
93
|
+
resolveViewId(subView),
|
|
94
|
+
imageBounds.x,
|
|
95
|
+
imageBounds.y,
|
|
96
|
+
imageBounds.width,
|
|
97
|
+
imageBounds.height,
|
|
98
|
+
null,
|
|
99
|
+
null,
|
|
100
|
+
null,
|
|
101
|
+
null,
|
|
102
|
+
hash,
|
|
103
|
+
"svg+xml",
|
|
104
|
+
false
|
|
105
|
+
)
|
|
106
|
+
wireframes.add(imgWireframe)
|
|
107
|
+
|
|
108
|
+
if (!queuedResourceIds.contains(hash)) {
|
|
109
|
+
queuedResourceIds.add(hash)
|
|
110
|
+
internalCallback.addResourceItem(
|
|
111
|
+
hash,
|
|
112
|
+
entryData,
|
|
113
|
+
"image/svg+xml"
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return wireframes
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fun injectSvgDimensions(
|
|
122
|
+
svgData: String,
|
|
123
|
+
width: Int? = null,
|
|
124
|
+
height: Int? = null
|
|
125
|
+
): String? {
|
|
126
|
+
val attrs = mutableListOf<String>()
|
|
127
|
+
width?.let { attrs.add("""width="$it"""") }
|
|
128
|
+
height?.let { attrs.add("""height="$it"""") }
|
|
129
|
+
|
|
130
|
+
if (attrs.isEmpty()) return svgData
|
|
131
|
+
|
|
132
|
+
val insertIndex = svgData.indexOf("<svg")
|
|
133
|
+
if (insertIndex == -1) return svgData
|
|
134
|
+
|
|
135
|
+
val tagEndIndex = svgData.indexOf(">", startIndex = insertIndex)
|
|
136
|
+
if (tagEndIndex == -1) return svgData
|
|
137
|
+
|
|
138
|
+
val dimensions = " " + attrs.joinToString(" ")
|
|
139
|
+
|
|
140
|
+
val builder = StringBuilder(svgData)
|
|
141
|
+
builder.insert(tagEndIndex, dimensions)
|
|
142
|
+
|
|
143
|
+
return builder.toString()
|
|
144
|
+
}
|
|
145
|
+
}
|
package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/views/DdPrivacyView.kt
CHANGED
|
@@ -50,6 +50,16 @@ class DdPrivacyView(context: Context) : ReactViewGroup(context) {
|
|
|
50
50
|
this.setTag(R.id.datadog_hidden, value)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Defines an ID value from JS side to uniquely identify a view on both sides.
|
|
55
|
+
*/
|
|
56
|
+
var nativeID: String? = null
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Defines a set of attributes used for transformations.
|
|
60
|
+
*/
|
|
61
|
+
var attributes: Map<String, String>? = null
|
|
62
|
+
|
|
53
63
|
init {
|
|
54
64
|
this.setTag(R.id.datadog_hidden, this.hide)
|
|
55
65
|
this.setTag(R.id.datadog_image_privacy, this.imagePrivacy)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
package com.datadog.reactnative.sessionreplay.views
|
|
8
8
|
|
|
9
9
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
10
|
+
import com.facebook.react.bridge.ReadableMap
|
|
10
11
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
11
12
|
import com.facebook.react.uimanager.ViewGroupManager
|
|
12
13
|
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
@@ -47,4 +48,16 @@ class DdPrivacyViewManager(context: ReactApplicationContext) : ViewGroupManager<
|
|
|
47
48
|
override fun setTouchPrivacy(view: DdPrivacyView?, value: String?) {
|
|
48
49
|
view?.let { view.touchPrivacy = value }
|
|
49
50
|
}
|
|
51
|
+
|
|
52
|
+
@ReactProp(name = "nativeID")
|
|
53
|
+
override fun setNativeID(view: DdPrivacyView?, value: String?) {
|
|
54
|
+
view?.nativeID = value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@ReactProp(name = "attributes")
|
|
58
|
+
override fun setAttributes(view: DdPrivacyView?, map: ReadableMap?) {
|
|
59
|
+
view?.attributes = map?.toHashMap()?.mapValues {
|
|
60
|
+
it.value.toString() ?: ""
|
|
61
|
+
}
|
|
62
|
+
}
|
|
50
63
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
package com.datadog.reactnative.sessionreplay.views
|
|
7
7
|
|
|
8
8
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
import com.facebook.react.bridge.ReadableMap
|
|
9
10
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
10
11
|
import com.facebook.react.uimanager.ViewGroupManager
|
|
11
12
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
@@ -40,4 +41,16 @@ class DdPrivacyViewManager(context: ReactApplicationContext) : ViewGroupManager<
|
|
|
40
41
|
fun setTouchPrivacy(view: DdPrivacyView?, value: String?) {
|
|
41
42
|
view?.let { view.touchPrivacy = value }
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
@ReactProp(name = "nativeID")
|
|
46
|
+
fun setNativeID(view: DdPrivacyView?, value: String?) {
|
|
47
|
+
view?.nativeID = value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ReactProp(name = "attributes")
|
|
51
|
+
fun setAttributes(view: DdPrivacyView?, map: ReadableMap?) {
|
|
52
|
+
view?.attributes = map?.toHashMap()?.mapValues {
|
|
53
|
+
it.value.toString() ?: ""
|
|
54
|
+
}
|
|
55
|
+
}
|
|
43
56
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
package com.datadog.reactnative.sessionreplay
|
|
8
8
|
|
|
9
|
+
import android.content.res.AssetManager
|
|
9
10
|
import com.datadog.android.sessionreplay.ImagePrivacy
|
|
10
11
|
import com.datadog.android.sessionreplay.SessionReplayConfiguration
|
|
11
12
|
import com.datadog.android.sessionreplay.SessionReplayPrivacy
|
|
@@ -20,6 +21,7 @@ import fr.xgouchet.elmyr.annotation.BoolForgery
|
|
|
20
21
|
import fr.xgouchet.elmyr.annotation.DoubleForgery
|
|
21
22
|
import fr.xgouchet.elmyr.annotation.StringForgery
|
|
22
23
|
import fr.xgouchet.elmyr.junit5.ForgeExtension
|
|
24
|
+
import java.io.IOException
|
|
23
25
|
import org.junit.jupiter.api.AfterEach
|
|
24
26
|
import org.junit.jupiter.api.BeforeEach
|
|
25
27
|
import org.junit.jupiter.api.Test
|
|
@@ -31,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoSettings
|
|
|
31
33
|
import org.mockito.kotlin.any
|
|
32
34
|
import org.mockito.kotlin.argumentCaptor
|
|
33
35
|
import org.mockito.kotlin.doReturn
|
|
36
|
+
import org.mockito.kotlin.doThrow
|
|
34
37
|
import org.mockito.kotlin.verify
|
|
35
38
|
import org.mockito.kotlin.whenever
|
|
36
39
|
import org.mockito.quality.Strictness
|
|
@@ -56,6 +59,9 @@ internal class DdSessionReplayImplementationTest {
|
|
|
56
59
|
@Mock
|
|
57
60
|
lateinit var mockUiManagerModule: UIManagerModule
|
|
58
61
|
|
|
62
|
+
@Mock
|
|
63
|
+
lateinit var mockAssetManager: AssetManager
|
|
64
|
+
|
|
59
65
|
private val imagePrivacyMap = mapOf(
|
|
60
66
|
"MASK_ALL" to ImagePrivacy.MASK_ALL,
|
|
61
67
|
"MASK_NON_BUNDLED_ONLY" to ImagePrivacy.MASK_LARGE_ONLY,
|
|
@@ -77,6 +83,8 @@ internal class DdSessionReplayImplementationTest {
|
|
|
77
83
|
fun `set up`() {
|
|
78
84
|
whenever(mockReactContext.getNativeModule(any<Class<NativeModule>>()))
|
|
79
85
|
.doReturn(mockUiManagerModule)
|
|
86
|
+
whenever(mockReactContext.assets).doReturn(mockAssetManager)
|
|
87
|
+
whenever(mockAssetManager.open(any())).doThrow(IOException("No assets in test"))
|
|
80
88
|
|
|
81
89
|
testedSessionReplay =
|
|
82
90
|
DdSessionReplayImplementation(mockReactContext) { mockSessionReplay }
|
|
@@ -6,17 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
package com.datadog.reactnative.sessionreplay
|
|
8
8
|
|
|
9
|
+
import android.content.res.AssetManager
|
|
9
10
|
import com.datadog.android.api.InternalLogger
|
|
10
11
|
import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper
|
|
11
12
|
import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper
|
|
12
13
|
import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
|
|
13
14
|
import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
|
|
14
15
|
import com.datadog.reactnative.sessionreplay.mappers.ReactViewModalMapper
|
|
16
|
+
import com.datadog.reactnative.sessionreplay.mappers.SvgViewMapper
|
|
15
17
|
import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
|
|
16
18
|
import com.facebook.react.bridge.NativeModule
|
|
17
19
|
import com.facebook.react.bridge.ReactContext
|
|
18
20
|
import com.facebook.react.uimanager.UIManagerModule
|
|
19
21
|
import fr.xgouchet.elmyr.junit5.ForgeExtension
|
|
22
|
+
import java.io.IOException
|
|
20
23
|
import org.assertj.core.api.Assertions.assertThat
|
|
21
24
|
import org.junit.jupiter.api.BeforeEach
|
|
22
25
|
import org.junit.jupiter.api.Test
|
|
@@ -27,6 +30,7 @@ import org.mockito.junit.jupiter.MockitoExtension
|
|
|
27
30
|
import org.mockito.junit.jupiter.MockitoSettings
|
|
28
31
|
import org.mockito.kotlin.any
|
|
29
32
|
import org.mockito.kotlin.doReturn
|
|
33
|
+
import org.mockito.kotlin.doThrow
|
|
30
34
|
import org.mockito.kotlin.whenever
|
|
31
35
|
import org.mockito.quality.Strictness
|
|
32
36
|
|
|
@@ -46,15 +50,24 @@ internal class ReactNativeSessionReplayExtensionSupportTest {
|
|
|
46
50
|
@Mock
|
|
47
51
|
private lateinit var mockLogger: InternalLogger
|
|
48
52
|
|
|
53
|
+
@Mock
|
|
54
|
+
private lateinit var mockAssetManager: AssetManager
|
|
55
|
+
|
|
49
56
|
private lateinit var testedExtensionSupport: ReactNativeSessionReplayExtensionSupport
|
|
50
57
|
|
|
51
58
|
@BeforeEach
|
|
52
59
|
fun `set up`() {
|
|
53
60
|
whenever(mockReactContext.getNativeModule(any<Class<NativeModule>>()))
|
|
54
61
|
.doReturn(mockUiManagerModule)
|
|
62
|
+
whenever(mockReactContext.assets).doReturn(mockAssetManager)
|
|
63
|
+
whenever(mockAssetManager.open(any())).doThrow(IOException("No assets in test"))
|
|
55
64
|
|
|
65
|
+
val internalCallback = ReactNativeInternalCallback(mockReactContext)
|
|
56
66
|
val textViewUtils = TextViewUtils.create(mockReactContext, mockLogger)
|
|
57
|
-
testedExtensionSupport = ReactNativeSessionReplayExtensionSupport(
|
|
67
|
+
testedExtensionSupport = ReactNativeSessionReplayExtensionSupport(
|
|
68
|
+
textViewUtils,
|
|
69
|
+
internalCallback
|
|
70
|
+
)
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
@Test
|
|
@@ -63,21 +76,24 @@ internal class ReactNativeSessionReplayExtensionSupportTest {
|
|
|
63
76
|
val customViewMappers = testedExtensionSupport.getCustomViewMappers()
|
|
64
77
|
|
|
65
78
|
// Then
|
|
66
|
-
assertThat(customViewMappers).hasSize(
|
|
79
|
+
assertThat(customViewMappers).hasSize(6)
|
|
67
80
|
|
|
68
81
|
assertThat(customViewMappers[0].getUnsafeMapper())
|
|
69
82
|
.isInstanceOf(ReactNativeImageViewMapper::class.java)
|
|
70
83
|
|
|
71
84
|
assertThat(customViewMappers[1].getUnsafeMapper())
|
|
72
|
-
.isInstanceOf(
|
|
85
|
+
.isInstanceOf(SvgViewMapper::class.java)
|
|
73
86
|
|
|
74
87
|
assertThat(customViewMappers[2].getUnsafeMapper())
|
|
75
|
-
.isInstanceOf(
|
|
88
|
+
.isInstanceOf(ReactViewGroupMapper::class.java)
|
|
76
89
|
|
|
77
90
|
assertThat(customViewMappers[3].getUnsafeMapper())
|
|
78
|
-
.isInstanceOf(
|
|
91
|
+
.isInstanceOf(ReactTextMapper::class.java)
|
|
79
92
|
|
|
80
93
|
assertThat(customViewMappers[4].getUnsafeMapper())
|
|
94
|
+
.isInstanceOf(ReactEditTextMapper::class.java)
|
|
95
|
+
|
|
96
|
+
assertThat(customViewMappers[5].getUnsafeMapper())
|
|
81
97
|
.isInstanceOf(ReactViewModalMapper::class.java)
|
|
82
98
|
}
|
|
83
99
|
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|