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 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. Shopping Cart
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 'MetaStreamIO';
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
- export { MetaStreamIO, MetaStreamProvider };
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
- <ViewShot ref={this.viewShotRef} style={{ flex: 1 }} options={{ format: "jpg", quality: 0.3 }}>
31
- <View
32
- style={[styles.container, style]}
33
- onTouchStart={(e) => this.handleTouch(e, 'touch_start')}
34
- onTouchMove={(e) => this.handleTouch(e, 'touch_move')}
35
- onTouchEnd={(e) => this.handleTouch(e, 'touch_end')}
36
- >
37
- {children}
38
- </View>
39
- </ViewShot>
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: 350,
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
- console.log("DBG:: Uploading frame");
133
- this.uploadFrame(base64, isDuplicate);
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.8",
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",