@datadog/mobile-react-native-session-replay 2.14.0 → 2.14.2
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/README.md
CHANGED
|
@@ -127,6 +127,21 @@ yarn android
|
|
|
127
127
|
|
|
128
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
129
|
|
|
130
|
+
#### CLI Options
|
|
131
|
+
|
|
132
|
+
The `datadog-generate-sr-assets` command supports the following options:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
npx datadog-generate-sr-assets [options]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Option | Alias | Description | Example |
|
|
139
|
+
|--------|-------|-------------|---------|
|
|
140
|
+
| `--ignore <pattern>` | `-i` | Additional glob patterns to ignore during scanning. Can be specified multiple times. | `npx datadog-generate-sr-assets --ignore "**/legacy/**" --ignore "**/vendor/**"` |
|
|
141
|
+
| `--verbose` | `-v` | Enable verbose output for debugging. | `npx datadog-generate-sr-assets --verbose` |
|
|
142
|
+
| `--path <path>` | `-p` | Path to the root directory to scan. Defaults to the current working directory. | `npx datadog-generate-sr-assets --path ./src` |
|
|
143
|
+
| `--followSymlinks` | | Follow symbolic links during directory traversal. Default: false (symlinks are ignored). | `npx datadog-generate-sr-assets --followSymlinks` |
|
|
144
|
+
|
|
130
145
|
**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
146
|
|
|
132
147
|
### Development Workflow
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
|
|
3
|
+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
4
|
+
* Copyright 2016-Present Datadog, Inc.
|
|
5
|
+
*/
|
|
6
|
+
#import <Foundation/Foundation.h>
|
|
7
|
+
#import "RCTTextPropertiesWrapper.h"
|
|
8
|
+
|
|
9
|
+
@class RCTUIManager;
|
|
10
|
+
|
|
11
|
+
@interface RCTTextExtractor : NSObject
|
|
12
|
+
|
|
13
|
+
- (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView* _Nonnull)view
|
|
14
|
+
withUIManager:(RCTUIManager* _Nonnull)uiManager;
|
|
15
|
+
|
|
16
|
+
- (BOOL)isRCTTextView:(UIView* _Nonnull)view;
|
|
17
|
+
|
|
18
|
+
@end
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
#import "RCTTextExtractor.h"
|
|
8
|
+
|
|
9
|
+
#if !RCT_NEW_ARCH_ENABLED
|
|
10
|
+
#import <React/RCTBridge.h>
|
|
11
|
+
#import <React/RCTUIManager.h>
|
|
12
|
+
#import <React/RCTTextView.h>
|
|
13
|
+
#import <React/RCTTextShadowView.h>
|
|
14
|
+
#import <React/RCTRawTextShadowView.h>
|
|
15
|
+
#import <React/RCTVirtualTextShadowView.h>
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
@implementation RCTTextExtractor
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the text properties from the given UIView when using the old Paper architecture.
|
|
22
|
+
* Returns nil when using new architecture or if the view is not a RCTTextView.
|
|
23
|
+
*/
|
|
24
|
+
- (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView *)view
|
|
25
|
+
withUIManager:(RCTUIManager *)uiManager {
|
|
26
|
+
#if !RCT_NEW_ARCH_ENABLED
|
|
27
|
+
if (![view isKindOfClass:[RCTTextView class]]) {
|
|
28
|
+
return nil;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
RCTTextView* textView = (RCTTextView*)view;
|
|
32
|
+
NSNumber* tag = textView.reactTag;
|
|
33
|
+
|
|
34
|
+
__block RCTTextShadowView* shadowView = nil;
|
|
35
|
+
NSTimeInterval timeout = 0.2;
|
|
36
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
37
|
+
|
|
38
|
+
// We need to access the shadow view from the UIManager queue, but we're currently on the main thread.
|
|
39
|
+
// Calling `.sync` from the main thread to the UIManager queue is unsafe, because the UIManager queue
|
|
40
|
+
// may already be executing a layout operation that in turn requires the main thread (e.g. measuring a native view).
|
|
41
|
+
// That would create a circular dependency and deadlock the app.
|
|
42
|
+
// To avoid this, we dispatch the work asynchronously to the UIManager queue and wait with a timeout.
|
|
43
|
+
// This ensures we block only if absolutely necessary, and can fail gracefully if the queue is busy.
|
|
44
|
+
dispatch_async(uiManager.methodQueue, ^{
|
|
45
|
+
shadowView = (RCTTextShadowView*)[uiManager shadowViewForReactTag:tag];
|
|
46
|
+
dispatch_semaphore_signal(semaphore);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
dispatch_time_t waitTimeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
|
|
50
|
+
long waitResult = dispatch_semaphore_wait(semaphore, waitTimeout);
|
|
51
|
+
|
|
52
|
+
if (waitResult != 0) { // timeout
|
|
53
|
+
return nil;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (shadowView == nil || ![shadowView isKindOfClass:[RCTTextShadowView class]]) {
|
|
57
|
+
return nil;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
RCTTextPropertiesWrapper* textProperties = [[RCTTextPropertiesWrapper alloc] init];
|
|
61
|
+
|
|
62
|
+
// Extract text from subviews
|
|
63
|
+
NSString* text = [self tryToExtractTextFromSubViews:shadowView.reactSubviews];
|
|
64
|
+
if (text != nil) {
|
|
65
|
+
textProperties.text = text;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Extract text attributes
|
|
69
|
+
if (shadowView.textAttributes.foregroundColor != nil) {
|
|
70
|
+
textProperties.foregroundColor = shadowView.textAttributes.foregroundColor;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
textProperties.alignment = shadowView.textAttributes.alignment;
|
|
74
|
+
textProperties.fontSize = shadowView.textAttributes.fontSize;
|
|
75
|
+
textProperties.contentRect = shadowView.layoutMetrics.contentFrame;
|
|
76
|
+
|
|
77
|
+
return textProperties;
|
|
78
|
+
#else
|
|
79
|
+
return nil;
|
|
80
|
+
#endif
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#if !RCT_NEW_ARCH_ENABLED
|
|
84
|
+
- (nullable NSString*)tryToExtractTextFromSubViews:(NSArray<RCTShadowView*>*)subviews {
|
|
85
|
+
if (subviews == nil) {
|
|
86
|
+
return nil;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
NSMutableArray<NSString*>* textParts = [NSMutableArray array];
|
|
90
|
+
|
|
91
|
+
for (RCTShadowView* subview in subviews) {
|
|
92
|
+
if ([subview isKindOfClass:[RCTRawTextShadowView class]]) {
|
|
93
|
+
RCTRawTextShadowView* rawTextView = (RCTRawTextShadowView*)subview;
|
|
94
|
+
if (rawTextView.text != nil) {
|
|
95
|
+
[textParts addObject:rawTextView.text];
|
|
96
|
+
}
|
|
97
|
+
} else if ([subview isKindOfClass:[RCTVirtualTextShadowView class]]) {
|
|
98
|
+
// We recursively get all subviews for nested Text components
|
|
99
|
+
RCTVirtualTextShadowView* virtualTextView = (RCTVirtualTextShadowView*)subview;
|
|
100
|
+
NSString* nestedText = [self tryToExtractTextFromSubViews:virtualTextView.reactSubviews];
|
|
101
|
+
if (nestedText != nil) {
|
|
102
|
+
[textParts addObject:nestedText];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (textParts.count == 0) {
|
|
108
|
+
return nil;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [textParts componentsJoinedByString:@""];
|
|
112
|
+
}
|
|
113
|
+
#endif
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Checks if the given view is an RCTTextView.
|
|
117
|
+
* Returns NO when using new architecture or if the view is not a RCTTextView.
|
|
118
|
+
*/
|
|
119
|
+
- (BOOL)isRCTTextView:(UIView *)view {
|
|
120
|
+
#if !RCT_NEW_ARCH_ENABLED
|
|
121
|
+
return [view isKindOfClass:[RCTTextView class]];
|
|
122
|
+
#else
|
|
123
|
+
return NO;
|
|
124
|
+
#endif
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@end
|
|
@@ -18,10 +18,12 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder {
|
|
|
18
18
|
|
|
19
19
|
internal let uiManager: RCTUIManager
|
|
20
20
|
internal let fabricWrapper: RCTFabricWrapper
|
|
21
|
+
private let textExtractor: RCTTextExtractor
|
|
21
22
|
|
|
22
23
|
internal init(uiManager: RCTUIManager, fabricWrapper: RCTFabricWrapper) {
|
|
23
24
|
self.uiManager = uiManager
|
|
24
25
|
self.fabricWrapper = fabricWrapper
|
|
26
|
+
self.textExtractor = RCTTextExtractor()
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
public func semantics(
|
|
@@ -30,9 +32,12 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder {
|
|
|
30
32
|
in context: SessionReplayViewTreeRecordingContext
|
|
31
33
|
) -> SessionReplayNodeSemantics? {
|
|
32
34
|
guard
|
|
33
|
-
let textProperties = fabricWrapper.tryToExtractTextProperties(from: view)
|
|
35
|
+
let textProperties = fabricWrapper.tryToExtractTextProperties(from: view)
|
|
36
|
+
?? textExtractor.tryToExtractTextProperties(from: view, with: uiManager)
|
|
34
37
|
else {
|
|
35
|
-
|
|
38
|
+
// Check if this is an RCTTextView that we couldn't extract text from
|
|
39
|
+
// This check is done in Objective-C to avoid compile-time dependency on RCTTextView
|
|
40
|
+
return textExtractor.isRCTTextView(view) ? SessionReplayInvisibleElement.constant : nil
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
let builder = RCTTextViewWireframesBuilder(
|
|
@@ -51,73 +56,6 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder {
|
|
|
51
56
|
])
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
internal func tryToExtractTextFromSubViews(
|
|
55
|
-
subviews: [RCTShadowView]?
|
|
56
|
-
) -> String? {
|
|
57
|
-
guard let subviews = subviews else {
|
|
58
|
-
return nil
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return subviews.compactMap { subview in
|
|
62
|
-
if let sub = subview as? RCTRawTextShadowView {
|
|
63
|
-
return sub.text
|
|
64
|
-
}
|
|
65
|
-
if let sub = subview as? RCTVirtualTextShadowView {
|
|
66
|
-
// We recursively get all subviews for nested Text components
|
|
67
|
-
return tryToExtractTextFromSubViews(subviews: sub.reactSubviews())
|
|
68
|
-
}
|
|
69
|
-
return nil
|
|
70
|
-
}.joined()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private func tryToExtractTextProperties(view: UIView) -> RCTTextPropertiesWrapper? {
|
|
74
|
-
guard let textView = view as? RCTTextView else {
|
|
75
|
-
return nil
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
var shadowView: RCTTextShadowView? = nil
|
|
79
|
-
let tag = textView.reactTag
|
|
80
|
-
|
|
81
|
-
let timeout: TimeInterval = 0.2
|
|
82
|
-
let semaphore = DispatchSemaphore(value: 0)
|
|
83
|
-
|
|
84
|
-
// We need to access the shadow view from the UIManager queue, but we're currently on the main thread.
|
|
85
|
-
// Calling `.sync` from the main thread to the UIManager queue is unsafe, because the UIManager queue
|
|
86
|
-
// may already be executing a layout operation that in turn requires the main thread (e.g. measuring a native view).
|
|
87
|
-
// That would create a circular dependency and deadlock the app.
|
|
88
|
-
// To avoid this, we dispatch the work asynchronously to the UIManager queue and wait with a timeout.
|
|
89
|
-
// This ensures we block only if absolutely necessary, and can fail gracefully if the queue is busy.
|
|
90
|
-
RCTGetUIManagerQueue().async {
|
|
91
|
-
shadowView = self.uiManager.shadowView(forReactTag: tag) as? RCTTextShadowView
|
|
92
|
-
semaphore.signal()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let waitResult = semaphore.wait(timeout: .now() + timeout)
|
|
96
|
-
if waitResult == .timedOut {
|
|
97
|
-
return nil
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
guard let shadow = shadowView else {
|
|
101
|
-
return nil
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let textProperties = RCTTextPropertiesWrapper()
|
|
105
|
-
|
|
106
|
-
// TODO: RUM-2173 check performance is ok
|
|
107
|
-
if let text = tryToExtractTextFromSubViews(subviews: shadow.reactSubviews()) {
|
|
108
|
-
textProperties.text = text
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if let foregroundColor = shadow.textAttributes.foregroundColor {
|
|
112
|
-
textProperties.foregroundColor = foregroundColor
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
textProperties.alignment = shadow.textAttributes.alignment
|
|
116
|
-
textProperties.fontSize = shadow.textAttributes.fontSize
|
|
117
|
-
textProperties.contentRect = shadow.contentFrame
|
|
118
|
-
|
|
119
|
-
return textProperties
|
|
120
|
-
}
|
|
121
59
|
}
|
|
122
60
|
|
|
123
61
|
internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datadog/mobile-react-native-session-replay",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.2",
|
|
4
4
|
"description": "A client-side React Native module to enable session replay with Datadog",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datadog",
|
|
@@ -96,5 +96,5 @@
|
|
|
96
96
|
"dependencies": {
|
|
97
97
|
"chokidar": "^4.0.3"
|
|
98
98
|
},
|
|
99
|
-
"gitHead": "
|
|
99
|
+
"gitHead": "1f85e56ea633c9cdee4c970a46356f5f8a1dfc67"
|
|
100
100
|
}
|