noibu-react-native 0.2.19 → 0.2.20

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/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes of the noibu-react-native SDK release series are documented in this file using
4
4
  the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
5
5
 
6
+ ## 0.2.20
7
+
8
+ ### Added
9
+ - ** ANDROID: WebView replay support (mobile transformer)**
10
+ - Integrates a WebView-specific transformer to convert rrweb full/incremental snapshots from WebViews into web events.
11
+ - Snapshots generated during transformation are now included at the top level of the transformed output, emitted immediately after the event that produced them.
12
+ - **Live subscription emits multiple events when applicable**
13
+ - The native subscription path now routes events through `transformToWeb`, allowing multiple transformed events (e.g., WebView snapshots) to be emitted per native event.
14
+
15
+ ### Changed
16
+ - **`transformToWeb`** now drains a small internal buffer of WebView snapshots produced during transformation and appends them to the resulting array in order.
17
+ - **`nativeSessionRecorderSubscription`** (Android and iOS) now uses `transformToWeb([event])` and forwards each resulting event via the provided callback.
18
+
19
+
6
20
  ## 0.2.13
7
21
 
8
22
  ### Alpha support for iOS Session Replay in React Native
@@ -67,12 +67,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
67
67
  dependencies {
68
68
  implementation "com.facebook.react:react-native:+"
69
69
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
70
- implementation "com.noibu:sessionreplay-recorder:1.0.0"
71
- // Remove or comment out the old dependency
72
- // implementation project(":new_sessionreplay")
73
-
74
- // Add the dependency on the local KMP shared module
75
- // implementation project(":sessionreplay:shared")
70
+ implementation "com.noibu:sessionreplay-recorder:1.0.1"
76
71
  }
77
72
 
78
73
  if (isNewArchitectureEnabled()) {
@@ -41,16 +41,6 @@ class NoibuSessionReplayModule(reactContext: ReactApplicationContext) :
41
41
  promise.resolve(true)
42
42
  }
43
43
 
44
- private fun readableArrayToList(arr: ReadableArray): List<String> {
45
- val ret = mutableListOf<String>()
46
-
47
- for (i in 0 until arr.size()) {
48
- ret.add(arr.getString(i))
49
- }
50
-
51
- return ret
52
- }
53
-
54
44
  @ReactMethod
55
45
  fun addListener(eventName: String) {
56
46
  // Set up any upstream listeners or background tasks as necessary
package/dist/constants.js CHANGED
@@ -24,7 +24,7 @@ const CONTENT_TYPE = 'content-type';
24
24
  * Gets the script id from the cookie object, returns default if cannot be found
25
25
  */
26
26
  function GET_SCRIPT_ID() {
27
- return "1.0.104-rn-sdk-0.2.19" ;
27
+ return "1.0.104-rn-sdk-0.2.20" ;
28
28
  }
29
29
  /**
30
30
  * Gets the max metro recon number
@@ -1,7 +1,7 @@
1
1
  import Ajv from 'ajv';
2
2
  import mobileSchema from './schema/mobile/rr-mobile-schema.json.js';
3
3
  import webSchema from './schema/web/rr-web-schema.json.js';
4
- import { makeCustomEvent, makeMetaEvent, makeIncrementalEvent, makeFullEvent } from './transformer/transformers.js';
4
+ import { _drainWebViewSnapshots, makeCustomEvent, makeMetaEvent, makeIncrementalEvent, makeFullEvent } from './transformer/transformers.js';
5
5
 
6
6
  //import { captureException, captureMessage } from '@sentry/react'
7
7
  //@ts-ignore
@@ -41,6 +41,20 @@ function transformEventToWeb(event, validateTransformation) {
41
41
  }
42
42
  return event;
43
43
  }
44
+ function transformToWeb(mobileData) {
45
+ return mobileData.reduce((acc, event) => {
46
+ const transformed = transformEventToWeb(event);
47
+ const finalEvent = transformed ? transformed : event;
48
+ acc.push(finalEvent);
49
+ // Append any snapshots generated during this event's transformation
50
+ const snapshots = _drainWebViewSnapshots();
51
+ if (snapshots.length > 0) {
52
+ // Ensure snapshots have required fields; push as-is to preserve timestamps/order
53
+ snapshots.forEach((s) => acc.push(s));
54
+ }
55
+ return acc;
56
+ }, []);
57
+ }
44
58
  function validateAgainstWebSchema(data) {
45
59
  const validationResult = webSchemaValidator(data);
46
60
  if (!validationResult) {
@@ -54,4 +68,4 @@ function validateAgainstWebSchema(data) {
54
68
  return validationResult;
55
69
  }
56
70
 
57
- export { transformEventToWeb, validateAgainstWebSchema };
71
+ export { transformEventToWeb, transformToWeb, validateAgainstWebSchema };
@@ -216,6 +216,13 @@ export type wireframeRectangle = wireframeBase & {
216
216
  export type wireframeWebView = wireframeBase & {
217
217
  type: 'web_view';
218
218
  url?: string;
219
+ /**
220
+ * @description Contains the rrweb meta and full snapshot events for the webview content, as a JSON string.
221
+ * The string is an array of two events: [metaEvent, fullSnapshotEvent].
222
+ */
223
+ rrwebFullSnapshot: {
224
+ node: any[];
225
+ };
219
226
  };
220
227
  export type wireframePlaceholder = wireframeBase & {
221
228
  type: 'placeholder';
@@ -1539,6 +1539,18 @@ var definitions = {
1539
1539
  },
1540
1540
  y: {
1541
1541
  type: "number"
1542
+ },
1543
+ rrwebFullSnapshot: {
1544
+ description: "Contains the rrweb meta and full snapshot events for the webview content, as a JSON string. The string is an array of two events: [metaEvent, fullSnapshotEvent].",
1545
+ type: "object",
1546
+ properties: {
1547
+ node: {
1548
+ type: "string"
1549
+ }
1550
+ },
1551
+ required: [
1552
+ "node"
1553
+ ]
1542
1554
  }
1543
1555
  },
1544
1556
  required: [
@@ -2,6 +2,7 @@ import { customEvent, fullSnapshotEvent, incrementalSnapshotEvent, metaEvent } f
2
2
  import { fullSnapshotEvent as MobileFullSnapshotEvent, keyboardEvent, metaEvent as MobileMetaEvent, MobileIncrementalSnapshotEvent, serializedNodeWithId, wireframe, wireframeDiv, wireframeNavigationBar, wireframeStatusBar } from '../mobile.types';
3
3
  import { ConversionContext, ConversionResult } from './types';
4
4
  export declare const BACKGROUND = "#f3f4ef";
5
+ export declare function _drainWebViewSnapshots(): any[];
5
6
  export declare const NAVIGATION_BAR_ID = 8;
6
7
  export declare const KEYBOARD_ID = 10;
7
8
  export declare const STATUS_BAR_PARENT_ID = 11;
@@ -3,9 +3,18 @@ import { isObject } from '../../utils.js';
3
3
  import { NodeType } from '../mobile.types.js';
4
4
  import { makeOpenKeyboardPlaceholder, makeNavigationBar, makeStatusBar } from './screen-chrome.js';
5
5
  import { makeBodyStyles, makeHTMLStyles, makeStylesString, asStyleString, makeDeterminateProgressStyles, makeIndeterminateProgressStyles, makeMinimalStyles, makePositionStyles, makeColorStyles } from './wireframeStyle.js';
6
+ import { transform } from './webview-transformer.js';
6
7
 
7
8
  const BACKGROUND = '#f3f4ef';
8
9
  const FOREGROUND = '#35373e';
10
+ // Buffer to collect snapshots generated during element transformations (e.g., WebView)
11
+ // These are later drained and appended to the top-level transformed JSON.
12
+ let __pendingWebViewSnapshots = [];
13
+ function _drainWebViewSnapshots() {
14
+ const out = __pendingWebViewSnapshots;
15
+ __pendingWebViewSnapshots = [];
16
+ return out;
17
+ }
9
18
  /**
10
19
  * generates a sequence of ids
11
20
  * from 100 to 9,999,999
@@ -183,11 +192,12 @@ function makeTextElement(wireframe, children, context) {
183
192
  };
184
193
  }
185
194
  function makeWebViewElement(wireframe, children, context) {
186
- const labelledWireframe = Object.assign({}, wireframe);
187
- if ('url' in wireframe) {
188
- labelledWireframe.label = wireframe.url;
195
+ const webViewWireframe = wireframe;
196
+ const { frame, snapshots } = transform(webViewWireframe);
197
+ if (Array.isArray(snapshots) && snapshots.length > 0) {
198
+ __pendingWebViewSnapshots.push(...snapshots);
189
199
  }
190
- return makePlaceholderElement(labelledWireframe, children, context);
200
+ return { result: frame, context };
191
201
  }
192
202
  function makePlaceholderElement(wireframe, children, context) {
193
203
  var _a, _b;
@@ -1151,4 +1161,4 @@ function makeCSSReset(context) {
1151
1161
  };
1152
1162
  }
1153
1163
 
1154
- export { BACKGROUND, KEYBOARD_ID, NAVIGATION_BAR_ID, STATUS_BAR_ID, STATUS_BAR_PARENT_ID, _isPositiveInteger, dataURIOrPNG, makeCustomEvent, makeDivElement, makeFullEvent, makeIncrementalEvent, makeMetaEvent, makePlaceholderElement, stripBarsFromWireframes };
1164
+ export { BACKGROUND, KEYBOARD_ID, NAVIGATION_BAR_ID, STATUS_BAR_ID, STATUS_BAR_PARENT_ID, _drainWebViewSnapshots, _isPositiveInteger, dataURIOrPNG, makeCustomEvent, makeDivElement, makeFullEvent, makeIncrementalEvent, makeMetaEvent, makePlaceholderElement, stripBarsFromWireframes };
@@ -0,0 +1,9 @@
1
+ import { wireframeWebView as WebView } from "../mobile.types";
2
+ type Node = any;
3
+ type Snapshot = any;
4
+ type Result = {
5
+ frame: Node;
6
+ snapshots: Snapshot[];
7
+ };
8
+ export declare function transform(webView: WebView): Result;
9
+ export {};
@@ -0,0 +1,124 @@
1
+ function transform(webView) {
2
+ const frameId = 999999999;
3
+ const rootId = frameId + 1;
4
+ const result = { frame: null, snapshots: [] };
5
+ try {
6
+ const snapshots = webView.rrwebFullSnapshot.node;
7
+ const fullSnapshotIndex = snapshots.findIndex((event) => event.type === 2);
8
+ const fullSnapshot = snapshots[fullSnapshotIndex];
9
+ const incrementalSnapshots = snapshots.filter((snapshot, index) => snapshot.type === 3 && index > fullSnapshotIndex);
10
+ const syntheticSnapshot = createSyntheticSnapshot(fullSnapshot, frameId, rootId);
11
+ incrementalSnapshots.forEach((snapshot) => patchIncrementalSnapshot(snapshot, rootId));
12
+ result.frame = createFrameNode(frameId);
13
+ ;
14
+ result.snapshots.push(syntheticSnapshot, ...incrementalSnapshots);
15
+ }
16
+ catch (error) {
17
+ console.error(error);
18
+ }
19
+ return result;
20
+ }
21
+ function createSyntheticSnapshot(snapshot, frameId, rootId) {
22
+ const childNodes = snapshot.data.node.childNodes;
23
+ const htmlNodeIndex = childNodes.findIndex((node) => node.type === 2 && node.tagName === "html");
24
+ const htmlNode = childNodes[htmlNodeIndex];
25
+ patch(htmlNode);
26
+ return {
27
+ type: 3,
28
+ data: {
29
+ source: 0,
30
+ adds: [
31
+ {
32
+ parentId: frameId,
33
+ nextId: null,
34
+ node: {
35
+ type: 0,
36
+ childNodes: [htmlNode],
37
+ id: rootId,
38
+ }
39
+ }
40
+ ],
41
+ removes: [],
42
+ texts: [],
43
+ attributes: [],
44
+ isAttachIframe: true,
45
+ },
46
+ timestamp: snapshot.timestamp,
47
+ };
48
+ function patch(node) {
49
+ node.rootId = rootId;
50
+ if (node.id) {
51
+ node.id = rootId + node.id;
52
+ }
53
+ if (node.childNodes) {
54
+ node.childNodes.forEach(patch);
55
+ }
56
+ }
57
+ }
58
+ function patchIncrementalSnapshot(snapshot, rootId) {
59
+ const { data } = snapshot;
60
+ const { adds, removes, attributes, positions } = data;
61
+ if (data.id) {
62
+ data.id = rootId + data.id;
63
+ }
64
+ if (adds === null || adds === void 0 ? void 0 : adds.length) {
65
+ adds.forEach(patchAdd);
66
+ }
67
+ if (removes === null || removes === void 0 ? void 0 : removes.length) {
68
+ removes.forEach(patchRemove);
69
+ }
70
+ if (attributes === null || attributes === void 0 ? void 0 : attributes.length) {
71
+ attributes.forEach(patchAttribute);
72
+ }
73
+ if (positions === null || positions === void 0 ? void 0 : positions.length) {
74
+ positions.forEach(patchAttribute);
75
+ }
76
+ function patchNode(node) {
77
+ if (node.id) {
78
+ node.id = rootId + node.id;
79
+ }
80
+ if (node.rootId) {
81
+ node.rootId = rootId + node.rootId;
82
+ }
83
+ if (node.childNodes) {
84
+ node.childNodes.forEach(patchNode);
85
+ }
86
+ }
87
+ function patchAdd(add) {
88
+ if (add.parentId) {
89
+ add.parentId = rootId + add.parentId;
90
+ }
91
+ if (add.nextId) {
92
+ add.nextId = rootId + add.nextId;
93
+ }
94
+ if (add.node) {
95
+ patchNode(add.node);
96
+ }
97
+ }
98
+ function patchRemove(remove) {
99
+ if (remove.parentId) {
100
+ remove.parentId = rootId + remove.parentId;
101
+ }
102
+ if (remove.id) {
103
+ remove.id = rootId + remove.id;
104
+ }
105
+ }
106
+ function patchAttribute(attribute) {
107
+ if (attribute.id) {
108
+ attribute.id = rootId + attribute.id;
109
+ }
110
+ }
111
+ }
112
+ function createFrameNode(id) {
113
+ return {
114
+ type: 2,
115
+ tagName: "iframe",
116
+ attributes: {
117
+ style: "width: 100%; height: 100%;"
118
+ },
119
+ childNodes: [],
120
+ id: id,
121
+ };
122
+ }
123
+
124
+ export { transform };
@@ -1,7 +1,7 @@
1
1
  import { __rest } from 'tslib';
2
2
  import { NativeModules, Platform, NativeEventEmitter } from 'react-native';
3
3
  import { noibuLog } from '../utils/log.js';
4
- import { transformEventToWeb } from '../mobileTransformer/mobile-replay/index.js';
4
+ import { transformToWeb } from '../mobileTransformer/mobile-replay/index.js';
5
5
 
6
6
  const LINKING_ERROR = `The package 'noibu-session-replay' doesn't seem to be linked. Make sure: \n\n` +
7
7
  // Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + TODO: add back when iOS is supported.
@@ -63,12 +63,12 @@ function subscribeToNativeEvent(callback) {
63
63
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
64
64
  // const subscription = nativeModuleEmitter.addListener('noibuRecordingEvent', callback);
65
65
  nativeModuleEmitter.addListener('noibuRecordingEvent', event => {
66
- let message = event.message;
66
+ const message = event.message;
67
67
  const _a = JSON.parse(message), { data } = _a, rest = __rest(_a, ["data"]);
68
- noibuLog("New noibu recording event", rest);
69
- const transformedEvent = transformEventToWeb(Object.assign({ data }, rest));
70
- noibuLog("after transformation: ", transformedEvent);
71
- callback({ message: transformedEvent });
68
+ noibuLog('New noibu recording event', rest);
69
+ const transformedEvents = transformToWeb([Object.assign(Object.assign({}, rest), { data })]);
70
+ noibuLog('after transformation: ', transformedEvents);
71
+ transformedEvents.forEach((e) => callback({ message: e }));
72
72
  });
73
73
  // return () => subscription.remove();
74
74
  }
@@ -79,11 +79,11 @@ function subscribeToNativeEvent(callback) {
79
79
  }
80
80
  nativeModuleEmitter.addListener('iosPOCRecordingEvent', event => {
81
81
  try {
82
- const transformedEvent = transformEventToWeb(event.message);
83
- callback({ message: transformedEvent });
82
+ const transformedEvents = transformToWeb([event.message]);
83
+ transformedEvents.forEach((e) => callback({ message: e }));
84
84
  }
85
85
  catch (e) {
86
- noibuLog(`[Error] transformEventToWeb failed: ${e.message}`);
86
+ noibuLog(`[Error] transformToWeb failed: ${e.message}`);
87
87
  }
88
88
  });
89
89
  if (!isIOSInitialized) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noibu-react-native",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
4
4
  "targetNjsVersion": "1.0.104",
5
5
  "description": "React-Native SDK for NoibuJS to collect errors in React-Native applications",
6
6
  "main": "dist/entry/index.js",
@@ -42,14 +42,12 @@
42
42
  },
43
43
  "optionalDependencies": {
44
44
  "@react-native-async-storage/async-storage": "^1.19.0",
45
- "fflate": "^0.8.2",
46
45
  "react-native-navigation": ">=2.29.0",
47
46
  "react-native-url-polyfill": "^1.3.0",
48
47
  "react-native-uuid": "^2.0.1"
49
48
  },
50
49
  "peerDependencies": {
51
50
  "@react-native-async-storage/async-storage": "^1.19.0",
52
- "fflate": "^0.8.2",
53
51
  "react": ">=16.13.1 <=18",
54
52
  "react-native": ">=0.63.0",
55
53
  "react-native-navigation": ">=2.29.0",