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 +14 -0
- package/android/build.gradle +1 -6
- package/android/src/main/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayModule.kt +0 -10
- package/dist/constants.js +1 -1
- package/dist/mobileTransformer/mobile-replay/index.js +16 -2
- package/dist/mobileTransformer/mobile-replay/mobile.types.d.ts +7 -0
- package/dist/mobileTransformer/mobile-replay/schema/mobile/rr-mobile-schema.json.js +12 -0
- package/dist/mobileTransformer/mobile-replay/transformer/transformers.d.ts +1 -0
- package/dist/mobileTransformer/mobile-replay/transformer/transformers.js +15 -5
- package/dist/mobileTransformer/mobile-replay/transformer/webview-transformer.d.ts +9 -0
- package/dist/mobileTransformer/mobile-replay/transformer/webview-transformer.js +124 -0
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +9 -9
- package/package.json +1 -3
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
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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()) {
|
package/android/src/main/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayModule.kt
CHANGED
|
@@ -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.
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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,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 {
|
|
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
|
-
|
|
66
|
+
const message = event.message;
|
|
67
67
|
const _a = JSON.parse(message), { data } = _a, rest = __rest(_a, ["data"]);
|
|
68
|
-
noibuLog(
|
|
69
|
-
const
|
|
70
|
-
noibuLog(
|
|
71
|
-
callback({ message:
|
|
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
|
|
83
|
-
callback({ message:
|
|
82
|
+
const transformedEvents = transformToWeb([event.message]);
|
|
83
|
+
transformedEvents.forEach((e) => callback({ message: e }));
|
|
84
84
|
}
|
|
85
85
|
catch (e) {
|
|
86
|
-
noibuLog(`[Error]
|
|
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.
|
|
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",
|