iidrak-analytics-react 1.2.8 → 1.2.9
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 +20 -2
- package/index.d.ts +15 -1
- package/index.js +3 -1
- package/metastreamio/components/Redact.js +66 -0
- package/metastreamio/metastreamio.provider.js +16 -12
- package/metastreamio/metastreamio.recorder.js +121 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ This package relies on several peer dependencies for device info, storage, and s
|
|
|
19
19
|
npm install iidrak-analytics-react
|
|
20
20
|
|
|
21
21
|
# Install required peer dependencies
|
|
22
|
-
npm install @react-native-async-storage/async-storage @react-native-community/netinfo react-native-device-info react-native-view-shot
|
|
22
|
+
npm install @react-native-async-storage/async-storage @react-native-community/netinfo react-native-device-info react-native-view-shot react-native-safe-area-context
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
**iOS Users**: after installing, remember to run:
|
|
@@ -137,7 +137,25 @@ Though `MetaStreamProvider` captures the video, you can manually log screen view
|
|
|
137
137
|
tracker.screen("HomeScreen", {});
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
### 4.
|
|
140
|
+
### 4. Privacy & Redaction
|
|
141
|
+
|
|
142
|
+
To protect sensitive user data (PII) like passwords or credit card numbers during session replay, wrap your sensitive components with the `<Redact>` component.
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
import { Redact } from 'iidrak-analytics-react';
|
|
146
|
+
|
|
147
|
+
<Redact>
|
|
148
|
+
<TextInput
|
|
149
|
+
placeholder="Enter Password"
|
|
150
|
+
secureTextEntry={true}
|
|
151
|
+
onChangeText={setPassword}
|
|
152
|
+
/>
|
|
153
|
+
</Redact>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This ensures the wrapped area is covered by a privacy mask in the recorded video, while remaining fully functional for the user.
|
|
157
|
+
|
|
158
|
+
### 5. Shopping Cart
|
|
141
159
|
|
|
142
160
|
Manage a persistent shopping cart state:
|
|
143
161
|
|
package/index.d.ts
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
|
-
declare module '
|
|
1
|
+
declare module 'iidrak-analytics-react' {
|
|
2
|
+
import { Component } from 'react';
|
|
3
|
+
import { ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
export class MetaStreamIO {
|
|
6
|
+
constructor(config: any);
|
|
7
|
+
start_session(options?: any): any;
|
|
8
|
+
trackEvent(options: any): void;
|
|
9
|
+
// Add other methods as needed
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class MetaStreamProvider extends Component<{ tracker: MetaStreamIO, children?: any, style?: ViewStyle }> { }
|
|
13
|
+
|
|
14
|
+
export class Redact extends Component<{ children: any, style?: ViewStyle }> { }
|
|
15
|
+
}
|
package/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import MetaStreamIO from "./metastreamio/metastreamio.interface.js";
|
|
2
2
|
import { MetaStreamProvider } from "./metastreamio/metastreamio.provider.js";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
import Redact from "./metastreamio/components/Redact.js";
|
|
5
|
+
|
|
6
|
+
export { MetaStreamIO, MetaStreamProvider, Redact };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { Component } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { MetaStreamContext } from '../metastreamio.provider';
|
|
4
|
+
|
|
5
|
+
const generateId = () => {
|
|
6
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Redact Component
|
|
11
|
+
*
|
|
12
|
+
* Wraps sensitive UI elements and registers their coordinates with the MetaStreamIORecorder.
|
|
13
|
+
* The recorder will then send these coordinates to the server (or mask them locally) so that
|
|
14
|
+
* sensitive data is not stored/viewable in session replays.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* <Redact>
|
|
18
|
+
* <Text>Sensitive Data</Text>
|
|
19
|
+
* </Redact>
|
|
20
|
+
*/
|
|
21
|
+
class Redact extends Component {
|
|
22
|
+
static contextType = MetaStreamContext;
|
|
23
|
+
|
|
24
|
+
constructor(props) {
|
|
25
|
+
super(props);
|
|
26
|
+
this.id = generateId();
|
|
27
|
+
this.viewRef = React.createRef();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
componentDidMount() {
|
|
31
|
+
// Registration is handled by onLayout
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
componentWillUnmount() {
|
|
35
|
+
this.unregister();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
register() {
|
|
39
|
+
const { tracker } = this.context || {};
|
|
40
|
+
if (tracker && tracker.recorder) {
|
|
41
|
+
// We pass the ref so the recorder can measure it on demand
|
|
42
|
+
tracker.recorder.addPrivacyZone(this.id, this.viewRef.current);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
unregister() {
|
|
47
|
+
const { tracker } = this.context || {};
|
|
48
|
+
if (tracker && tracker.recorder) {
|
|
49
|
+
tracker.recorder.removePrivacyZone(this.id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
render() {
|
|
54
|
+
const { children, style } = this.props;
|
|
55
|
+
// Check if context is available. If not, just render children without redaction logic to avoid crash.
|
|
56
|
+
// But for development we might want to warn.
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View ref={this.viewRef} testID={this.id} style={[{ opacity: 1 }, style]} collapsable={false} onLayout={() => this.register()}>
|
|
60
|
+
{children}
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default Redact;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import React, { Component } from 'react';
|
|
1
|
+
import React, { Component, createContext } from 'react';
|
|
2
2
|
import { View, StyleSheet, Dimensions } from 'react-native';
|
|
3
3
|
import ViewShot from 'react-native-view-shot';
|
|
4
4
|
|
|
5
|
+
const MetaStreamContext = createContext(null);
|
|
6
|
+
|
|
5
7
|
class MetaStreamProvider extends Component {
|
|
6
8
|
constructor(props) {
|
|
7
9
|
super(props);
|
|
@@ -27,16 +29,18 @@ class MetaStreamProvider extends Component {
|
|
|
27
29
|
const { children, style } = this.props;
|
|
28
30
|
|
|
29
31
|
return (
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
<MetaStreamContext.Provider value={{ tracker: this.props.tracker }}>
|
|
33
|
+
<ViewShot ref={this.viewShotRef} style={{ flex: 1 }} options={{ format: "jpg", quality: 0.3 }}>
|
|
34
|
+
<View
|
|
35
|
+
style={[styles.container, style]}
|
|
36
|
+
onTouchStart={(e) => this.handleTouch(e, 'touch_start')}
|
|
37
|
+
onTouchMove={(e) => this.handleTouch(e, 'touch_move')}
|
|
38
|
+
onTouchEnd={(e) => this.handleTouch(e, 'touch_end')}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</View>
|
|
42
|
+
</ViewShot>
|
|
43
|
+
</MetaStreamContext.Provider>
|
|
40
44
|
);
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -47,4 +51,4 @@ const styles = StyleSheet.create({
|
|
|
47
51
|
}
|
|
48
52
|
});
|
|
49
53
|
|
|
50
|
-
export { MetaStreamProvider };
|
|
54
|
+
export { MetaStreamProvider, MetaStreamContext };
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { strf } from "./string.format.js";
|
|
4
4
|
import { captureRef } from "react-native-view-shot";
|
|
5
|
-
import { Platform, Dimensions } from "react-native";
|
|
5
|
+
import { Platform, Dimensions, Keyboard, StatusBar } from "react-native";
|
|
6
|
+
import { initialWindowMetrics } from "react-native-safe-area-context";
|
|
6
7
|
|
|
7
8
|
strf();
|
|
8
9
|
|
|
@@ -29,12 +30,102 @@ class MetaStreamIORecorder {
|
|
|
29
30
|
// Priority: config.appName > user.user_id > config.app_id > "default"
|
|
30
31
|
this.app_id = config.appName ? config.appName : ((user && user.user_id) ? user.user_id : (config.app_id || "default"));
|
|
31
32
|
this.lastBase64 = null;
|
|
33
|
+
this.privacyZones = new Map();
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
setSessionId(id) {
|
|
35
37
|
this.sessionId = id;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
addPrivacyZone(id, ref) {
|
|
41
|
+
if (id && ref) {
|
|
42
|
+
this.privacyZones.set(id, ref);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
removePrivacyZone(id) {
|
|
47
|
+
if (id) {
|
|
48
|
+
this.privacyZones.delete(id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async measurePrivacyZones(rootViewRef) {
|
|
53
|
+
if (!rootViewRef || !rootViewRef.current) return [];
|
|
54
|
+
|
|
55
|
+
const zones = [];
|
|
56
|
+
|
|
57
|
+
// Measure root view to get its page offset (position on screen)
|
|
58
|
+
// using measureInWindow first as it handles viewport clipping/resize better on Android
|
|
59
|
+
const rootMetrics = await new Promise(resolve => {
|
|
60
|
+
try {
|
|
61
|
+
if (rootViewRef.current.measureInWindow) {
|
|
62
|
+
rootViewRef.current.measureInWindow((x, y, width, height) => {
|
|
63
|
+
resolve({ width, height, pageX: x, pageY: y });
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
rootViewRef.current.measure((x, y, width, height, pageX, pageY) => {
|
|
67
|
+
resolve({ width, height, pageX, pageY });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
resolve(null);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
|
76
|
+
const { height: fullScreenHeight } = Dimensions.get('screen');
|
|
77
|
+
|
|
78
|
+
// Calculate visible height. If keyboard is open, the visible area is reduced.
|
|
79
|
+
// We subtract Insets (Top/Status + Bottom/Nav) and KeyboardHeight.
|
|
80
|
+
// This ensures baseH represents the actual "Visible View Content Height", matching the capture.
|
|
81
|
+
// This fixes both Macro shifts and Micro gaps (e.g. Nav Bar gap).
|
|
82
|
+
const insets = initialWindowMetrics ? initialWindowMetrics.insets : { top: 0, bottom: 0 };
|
|
83
|
+
const effectiveHeight = (this.keyboardHeight && this.keyboardHeight > 0)
|
|
84
|
+
? Math.min(screenHeight, fullScreenHeight - this.keyboardHeight - insets.top - insets.bottom)
|
|
85
|
+
: screenHeight;
|
|
86
|
+
|
|
87
|
+
// Fallback or use root metrics, clamped to effective visible height
|
|
88
|
+
const baseW = (rootMetrics && rootMetrics.width > 0) ? Math.min(rootMetrics.width, screenWidth) : screenWidth;
|
|
89
|
+
const baseH = (rootMetrics && rootMetrics.height > 0) ? Math.min(rootMetrics.height, effectiveHeight) : effectiveHeight;
|
|
90
|
+
// Force 0 offset to avoid shifting redaction up if measure reports a top offset (e.g. status bar)
|
|
91
|
+
const baseX = 0;
|
|
92
|
+
const baseY = 0;
|
|
93
|
+
|
|
94
|
+
const promises = Array.from(this.privacyZones.entries()).map(([id, ref]) => {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
if (!ref || !ref.measure) {
|
|
97
|
+
resolve(null);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
ref.measure((x, y, width, height, pageX, pageY) => {
|
|
102
|
+
if (baseW <= 0 || baseH <= 0) { resolve(null); return; }
|
|
103
|
+
|
|
104
|
+
// Relative calculation: (ItemPageY - RootPageY) / RootHeight
|
|
105
|
+
const relX = (pageX - baseX) / baseW;
|
|
106
|
+
const relY = (pageY - baseY) / baseH;
|
|
107
|
+
const relW = width / baseW;
|
|
108
|
+
const relH = height / baseH;
|
|
109
|
+
|
|
110
|
+
resolve({
|
|
111
|
+
id,
|
|
112
|
+
x: relX,
|
|
113
|
+
y: relY,
|
|
114
|
+
width: relW,
|
|
115
|
+
height: relH
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
} catch (e) {
|
|
119
|
+
resolve(null);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const results = await Promise.all(promises);
|
|
125
|
+
return results.filter(r => r !== null);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
38
129
|
async startRecording(viewRef) {
|
|
39
130
|
if (this.isRecording || !this.sessionId || !this.endpoint) {
|
|
40
131
|
if (!this.endpoint) this.logger.warn("recorder", "No recording endpoint configured");
|
|
@@ -86,6 +177,9 @@ class MetaStreamIORecorder {
|
|
|
86
177
|
clearInterval(this.intervalId);
|
|
87
178
|
this.intervalId = null;
|
|
88
179
|
}
|
|
180
|
+
if (this.keyboardShowListener && this.keyboardShowListener.remove) this.keyboardShowListener.remove();
|
|
181
|
+
if (this.keyboardHideListener && this.keyboardHideListener.remove) this.keyboardHideListener.remove();
|
|
182
|
+
|
|
89
183
|
this.isRecording = false;
|
|
90
184
|
this.logger.log("recorder", "Stopped recording");
|
|
91
185
|
}
|
|
@@ -93,6 +187,16 @@ class MetaStreamIORecorder {
|
|
|
93
187
|
startLoop(viewRef) {
|
|
94
188
|
const intervalMs = 1000 / this.fps;
|
|
95
189
|
this.lastUploadTime = 0;
|
|
190
|
+
|
|
191
|
+
// Track keyboard height to fix redaction alignment issues when 'window' dimensions are stale
|
|
192
|
+
this.keyboardHeight = 0;
|
|
193
|
+
this.keyboardShowListener = Keyboard.addListener('keyboardDidShow', (e) => {
|
|
194
|
+
this.keyboardHeight = e.endCoordinates.height;
|
|
195
|
+
});
|
|
196
|
+
this.keyboardHideListener = Keyboard.addListener('keyboardDidHide', () => {
|
|
197
|
+
this.keyboardHeight = 0;
|
|
198
|
+
});
|
|
199
|
+
|
|
96
200
|
console.log("Starting loop");
|
|
97
201
|
this.intervalId = setInterval(async () => {
|
|
98
202
|
if (!viewRef.current) return;
|
|
@@ -101,10 +205,15 @@ class MetaStreamIORecorder {
|
|
|
101
205
|
// Capture as base64 for comparison and direct upload
|
|
102
206
|
// Resize to width 400px (height maintained automatically) to reduce size
|
|
103
207
|
// Use PNG to ensure deterministic output for diffing
|
|
208
|
+
// Use screen dimensions to prevent zooming when keyboard opens
|
|
209
|
+
// Use screen dimensions to prevent zooming when keyboard opens
|
|
210
|
+
// width: 360, height implicitly calculated to preserve aspect ratio
|
|
211
|
+
const captureWidth = 360;
|
|
212
|
+
|
|
104
213
|
const base64 = await captureRef(viewRef, {
|
|
105
214
|
format: "jpg",
|
|
106
215
|
quality: this.quality,
|
|
107
|
-
width:
|
|
216
|
+
width: captureWidth,
|
|
108
217
|
result: "base64"
|
|
109
218
|
});
|
|
110
219
|
|
|
@@ -129,8 +238,11 @@ class MetaStreamIORecorder {
|
|
|
129
238
|
const timeSinceLastUpload = (now - this.lastUploadTime) / 1000;
|
|
130
239
|
|
|
131
240
|
if (!isDuplicate || timeSinceLastUpload > 5.0) {
|
|
132
|
-
|
|
133
|
-
this.
|
|
241
|
+
// Measure privacy zones
|
|
242
|
+
const zones = await this.measurePrivacyZones(viewRef);
|
|
243
|
+
|
|
244
|
+
console.log("DBG:: Uploading frame with " + zones.length + " privacy zones");
|
|
245
|
+
this.uploadFrame(base64, isDuplicate, zones);
|
|
134
246
|
this.lastUploadTime = now;
|
|
135
247
|
this.lastBase64 = base64;
|
|
136
248
|
}
|
|
@@ -140,7 +252,7 @@ class MetaStreamIORecorder {
|
|
|
140
252
|
}, intervalMs);
|
|
141
253
|
}
|
|
142
254
|
|
|
143
|
-
async uploadFrame(data, isDuplicate) {
|
|
255
|
+
async uploadFrame(data, isDuplicate, privacyZones = []) {
|
|
144
256
|
if (!this.recorderSessionId) return;
|
|
145
257
|
|
|
146
258
|
const formData = new FormData();
|
|
@@ -150,6 +262,10 @@ class MetaStreamIORecorder {
|
|
|
150
262
|
formData.append('timestamp', String(timestamp));
|
|
151
263
|
formData.append('duration', String(duration));
|
|
152
264
|
|
|
265
|
+
if (privacyZones && privacyZones.length > 0) {
|
|
266
|
+
formData.append('privacy_zones', JSON.stringify(privacyZones));
|
|
267
|
+
}
|
|
268
|
+
|
|
153
269
|
if (isDuplicate) {
|
|
154
270
|
formData.append('is_duplicate', 'true');
|
|
155
271
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iidrak-analytics-react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "react native client for metastreamio",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@react-native-async-storage/async-storage": ">=2.2.0",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"react": ">=18.0.0",
|
|
9
9
|
"react-native": ">=0.70.0",
|
|
10
10
|
"react-native-device-info": ">=10.0.0",
|
|
11
|
-
"react-native-view-shot": ">=3.0.0"
|
|
11
|
+
"react-native-view-shot": ">=3.0.0",
|
|
12
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"string-format": "^0.5.0"
|
|
15
|
-
"uuid": "^8.3.2"
|
|
15
|
+
"string-format": "^0.5.0"
|
|
16
16
|
},
|
|
17
17
|
"main": "index.js",
|
|
18
18
|
"repository": "https://github.com/analyticsdept/metastreamio-reactjs.git",
|