@datadog/mobile-react-native-session-replay 2.12.3 → 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.
Files changed (60) hide show
  1. package/DatadogSDKReactNativeSessionReplay.podspec +6 -2
  2. package/README.md +90 -1
  3. package/android/build.gradle +3 -2
  4. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +2 -2
  5. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeInternalCallback.kt +121 -0
  6. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +5 -1
  7. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactViewGroupMapper.kt +1 -0
  8. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/SvgViewMapper.kt +145 -0
  9. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/views/DdPrivacyView.kt +10 -0
  10. package/android/src/newarch/kotlin/com/datadog/reactnative/sessionreplay/views/DdPrivacyViewManager.kt +13 -0
  11. package/android/src/oldarch/kotlin/com/datadog/reactnative/sessionreplay/views/DdPrivacyViewManager.kt +13 -0
  12. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementationTest.kt +8 -0
  13. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +21 -5
  14. package/assets/assets.bin +0 -0
  15. package/assets/assets.json +1 -0
  16. package/ios/Sources/DdPrivacyViewFabric.h +28 -0
  17. package/ios/Sources/DdPrivacyViewFabric.mm +22 -4
  18. package/ios/Sources/DdPrivacyViewPaper.m +24 -0
  19. package/ios/Sources/DdSessionReplayImplementation.swift +34 -6
  20. package/ios/Sources/SvgViewRecorder.swift +233 -0
  21. package/ios/Sources/Utils/Bundle+SessionReplay.swift +21 -0
  22. package/lib/commonjs/components/SessionReplayView/PrivacyView.js +2 -0
  23. package/lib/commonjs/components/SessionReplayView/PrivacyView.js.map +1 -1
  24. package/lib/commonjs/metro/index.js +75 -0
  25. package/lib/commonjs/metro/index.js.map +1 -0
  26. package/lib/commonjs/metro/processing.js +97 -0
  27. package/lib/commonjs/metro/processing.js.map +1 -0
  28. package/lib/commonjs/metro/utils.js +20 -0
  29. package/lib/commonjs/metro/utils.js.map +1 -0
  30. package/lib/commonjs/specs/DdPrivacyViewNativeComponent.js.map +1 -1
  31. package/lib/module/components/SessionReplayView/PrivacyView.js +2 -0
  32. package/lib/module/components/SessionReplayView/PrivacyView.js.map +1 -1
  33. package/lib/module/metro/index.js +68 -0
  34. package/lib/module/metro/index.js.map +1 -0
  35. package/lib/module/metro/processing.js +90 -0
  36. package/lib/module/metro/processing.js.map +1 -0
  37. package/lib/module/metro/utils.js +14 -0
  38. package/lib/module/metro/utils.js.map +1 -0
  39. package/lib/module/specs/DdPrivacyViewNativeComponent.js.map +1 -1
  40. package/lib/typescript/components/SessionReplayView/PrivacyView.d.ts +10 -1
  41. package/lib/typescript/components/SessionReplayView/PrivacyView.d.ts.map +1 -1
  42. package/lib/typescript/metro/index.d.ts +2 -0
  43. package/lib/typescript/metro/index.d.ts.map +1 -0
  44. package/lib/typescript/metro/processing.d.ts +9 -0
  45. package/lib/typescript/metro/processing.d.ts.map +1 -0
  46. package/lib/typescript/metro/utils.d.ts +2 -0
  47. package/lib/typescript/metro/utils.d.ts.map +1 -0
  48. package/lib/typescript/specs/DdPrivacyViewNativeComponent.d.ts +8 -0
  49. package/lib/typescript/specs/DdPrivacyViewNativeComponent.d.ts.map +1 -1
  50. package/lib/typescript/types/DdPrivacyView.d.ts +8 -0
  51. package/lib/typescript/types/DdPrivacyView.d.ts.map +1 -1
  52. package/metro.js +7 -0
  53. package/package.json +9 -3
  54. package/scripts/build-assets.js +31 -0
  55. package/src/components/SessionReplayView/PrivacyView.tsx +11 -0
  56. package/src/metro/index.ts +82 -0
  57. package/src/metro/processing.ts +119 -0
  58. package/src/metro/utils.ts +17 -0
  59. package/src/specs/DdPrivacyViewNativeComponent.ts +9 -0
  60. 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/*.{h,m,mm,swift}"
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.0'
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
- [1]: https://www.npmjs.com/package/@datadog/mobile-react-native
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
@@ -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.25.0"
217
- implementation "com.datadoghq:dd-sdk-android-internal:2.25.0"
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
 
@@ -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
 
@@ -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
+ }
@@ -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(textViewUtils)
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(5)
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(ReactViewGroupMapper::class.java)
85
+ .isInstanceOf(SvgViewMapper::class.java)
73
86
 
74
87
  assertThat(customViewMappers[2].getUnsafeMapper())
75
- .isInstanceOf(ReactTextMapper::class.java)
88
+ .isInstanceOf(ReactViewGroupMapper::class.java)
76
89
 
77
90
  assertThat(customViewMappers[3].getUnsafeMapper())
78
- .isInstanceOf(ReactEditTextMapper::class.java)
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
+ {}