noibu-react-native 0.2.27 → 0.2.29
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/android/src/legacy/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayModule.kt +11 -2
- package/android/src/newarch/java/com/noibu/sessionreplay/reactnative/NoibuSessionRecorderModule.kt +24 -5
- package/dist/constants.js +1 -1
- package/dist/native/NativeNoibuSessionRecorder.d.ts +3 -2
- package/dist/native/NativeNoibuSessionRecorder.js +21 -2
- package/dist/sessionRecorder/nativeSessionRecorderSubscription.js +58 -12
- package/noibu-react-native.podspec +1 -1
- package/package.json +1 -1
- package/src/native/NativeNoibuSessionRecorder.ts +37 -5
package/android/src/legacy/java/com/noibu/sessionreplay/reactnative/NoibuSessionReplayModule.kt
CHANGED
|
@@ -8,6 +8,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
|
8
8
|
import com.facebook.react.bridge.ReactMethod
|
|
9
9
|
|
|
10
10
|
// Update the import statements to point to the KMP shared module's package
|
|
11
|
+
// Import from published artifact when building outside monorepo; otherwise same package
|
|
11
12
|
import com.noibu.mobile.android.sessionreplay.Noibu
|
|
12
13
|
import com.noibu.mobile.android.sessionreplay.NoibuConfig
|
|
13
14
|
|
|
@@ -23,12 +24,20 @@ class NoibuSessionReplayModule(reactContext: ReactApplicationContext) :
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
private fun sendEvent(eventName: String, params: WritableMap?) {
|
|
26
|
-
|
|
27
|
+
try {
|
|
28
|
+
reactContext?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(eventName, params)
|
|
29
|
+
} catch (t: Throwable) {
|
|
30
|
+
Log.e(NAME, "Failed to emit $eventName", t)
|
|
31
|
+
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
@ReactMethod
|
|
30
35
|
fun initialize( promise: Promise ) {
|
|
31
|
-
val context = currentActivity?.applicationContext ?: reactContext?.applicationContext
|
|
36
|
+
val context = currentActivity?.applicationContext ?: reactContext?.applicationContext
|
|
37
|
+
if (context == null) {
|
|
38
|
+
promise.reject("E_INIT", IllegalStateException("No application context"))
|
|
39
|
+
return
|
|
40
|
+
}
|
|
32
41
|
val config = NoibuConfig(
|
|
33
42
|
sessionReplayEnabled = true,
|
|
34
43
|
maskAllTextInputs = false,
|
package/android/src/newarch/java/com/noibu/sessionreplay/reactnative/NoibuSessionRecorderModule.kt
CHANGED
|
@@ -7,15 +7,18 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|
|
7
7
|
import com.facebook.react.bridge.ReactMethod
|
|
8
8
|
import com.facebook.react.turbomodule.core.interfaces.TurboModule
|
|
9
9
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
10
|
-
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
11
10
|
import com.noibu.mobile.android.sessionreplay.Noibu
|
|
12
11
|
import com.noibu.mobile.android.sessionreplay.NoibuConfig
|
|
12
|
+
import java.util.concurrent.ConcurrentLinkedQueue
|
|
13
13
|
|
|
14
14
|
class NoibuSessionRecorderModule(val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), TurboModule {
|
|
15
15
|
companion object {
|
|
16
16
|
private const val TAG = "NoibuSessionRecorder"
|
|
17
|
+
private const val MAX_BATCH = 100
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
private val eventQueue: ConcurrentLinkedQueue<String> = ConcurrentLinkedQueue()
|
|
21
|
+
|
|
19
22
|
init {
|
|
20
23
|
Log.i(TAG, "[new-arch] Module constructed")
|
|
21
24
|
}
|
|
@@ -32,10 +35,8 @@ class NoibuSessionRecorderModule(val reactContext: ReactApplicationContext) : Re
|
|
|
32
35
|
maskAllTextInputs = false,
|
|
33
36
|
)
|
|
34
37
|
Noibu.setup(context, config) { param ->
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
38
|
-
.emit("noibuRecordingEvent", map)
|
|
38
|
+
// Enqueue raw mobile event JSON for JS to consume via TurboModule polling
|
|
39
|
+
eventQueue.add(param)
|
|
39
40
|
true
|
|
40
41
|
}
|
|
41
42
|
promise.resolve(true)
|
|
@@ -44,4 +45,22 @@ class NoibuSessionRecorderModule(val reactContext: ReactApplicationContext) : Re
|
|
|
44
45
|
promise.reject("E_INIT", t)
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
|
|
49
|
+
@ReactMethod
|
|
50
|
+
fun consumeEvents(promise: Promise) {
|
|
51
|
+
try {
|
|
52
|
+
val array = Arguments.createArray()
|
|
53
|
+
var count = 0
|
|
54
|
+
var item = eventQueue.poll()
|
|
55
|
+
while (item != null && count < MAX_BATCH) {
|
|
56
|
+
array.pushString(item)
|
|
57
|
+
count += 1
|
|
58
|
+
item = eventQueue.poll()
|
|
59
|
+
}
|
|
60
|
+
promise.resolve(array)
|
|
61
|
+
} catch (t: Throwable) {
|
|
62
|
+
Log.e(TAG, "consumeEvents() failed", t)
|
|
63
|
+
promise.reject("E_CONSUME", t)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
47
66
|
}
|
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.29" ;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Gets the max metro recon number
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TurboModule } from 'react-native';
|
|
2
2
|
export interface Spec extends TurboModule {
|
|
3
3
|
initialize(): Promise<boolean>;
|
|
4
|
+
consumeEvents?(): Promise<string[]>;
|
|
4
5
|
}
|
|
5
|
-
declare
|
|
6
|
-
export default
|
|
6
|
+
declare let moduleRef: Spec | null;
|
|
7
|
+
export default moduleRef;
|
|
@@ -1,6 +1,25 @@
|
|
|
1
|
+
// Ensure the Codegen parser sees Spec usage with TurboModuleRegistry.get<Spec>(..)
|
|
2
|
+
// This block is never executed at runtime but allows codegen to infer the module name.
|
|
3
|
+
// eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unused-expressions
|
|
1
4
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
2
5
|
const { TurboModuleRegistry: TurboModuleRegistryUntyped } = require('react-native');
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
let moduleRef = null;
|
|
7
|
+
try {
|
|
8
|
+
// Note: We've already destructured the TurboModuleRegistry object into TurboModuleRegistryUntyped
|
|
9
|
+
const tmr = TurboModuleRegistryUntyped;
|
|
10
|
+
// Prefer non-throwing get() if available
|
|
11
|
+
const maybe = (tmr === null || tmr === void 0 ? void 0 : tmr.get) ? tmr.get('NoibuSessionRecorder') : undefined;
|
|
12
|
+
if (maybe) {
|
|
13
|
+
moduleRef = maybe;
|
|
14
|
+
}
|
|
15
|
+
else if (tmr === null || tmr === void 0 ? void 0 : tmr.getEnforcing) {
|
|
16
|
+
// getEnforcing may throw on legacy; catch and fall back to null
|
|
17
|
+
moduleRef = tmr.getEnforcing('NoibuSessionRecorder');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (_e) {
|
|
21
|
+
moduleRef = null;
|
|
22
|
+
}
|
|
23
|
+
var TurboNativeSessionRecorder = moduleRef;
|
|
5
24
|
|
|
6
25
|
export { TurboNativeSessionRecorder as default };
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { __rest } from 'tslib';
|
|
1
|
+
import { __awaiter, __rest } from 'tslib';
|
|
2
2
|
import { NativeModules, Platform, NativeEventEmitter } from 'react-native';
|
|
3
3
|
import TurboNativeSessionRecorder from '../native/NativeNoibuSessionRecorder.js';
|
|
4
4
|
import { noibuLog } from '../utils/log.js';
|
|
5
5
|
import { transformToWeb } from '../mobileTransformer/mobile-replay/index.js';
|
|
6
6
|
|
|
7
|
+
var _a;
|
|
7
8
|
const LINKING_ERROR = `The package 'noibu-session-replay' doesn't seem to be linked. Make sure: \n\n` +
|
|
8
9
|
'- You rebuilt the app after installing the package\n' +
|
|
9
10
|
'- You are not using Expo Go\n';
|
|
10
|
-
const
|
|
11
|
-
const NativeSessionRecorder = TurboNativeSessionRecorder !== null && TurboNativeSessionRecorder !== void 0 ? TurboNativeSessionRecorder :
|
|
11
|
+
const RNModule = (_a = NativeModules.NoibuSessionRecorder) !== null && _a !== void 0 ? _a : NativeModules.NativeSessionRecorder;
|
|
12
|
+
const NativeSessionRecorder = TurboNativeSessionRecorder !== null && TurboNativeSessionRecorder !== void 0 ? TurboNativeSessionRecorder : RNModule;
|
|
13
|
+
// Consider new-arch if the resolved module exposes the polling method
|
|
14
|
+
const isNewArchAndroid = Platform.OS === 'android' && typeof (NativeSessionRecorder === null || NativeSessionRecorder === void 0 ? void 0 : NativeSessionRecorder.consumeEvents) === 'function';
|
|
12
15
|
let nativeModuleEmitter;
|
|
13
16
|
const SupportedPlatforms = ['android', 'ios'];
|
|
14
17
|
/** The level of logging to show in the device logcat stream. */
|
|
@@ -26,20 +29,28 @@ var LogLevel;
|
|
|
26
29
|
* Initializes the Noibu - Session recording SDK if the API level is supported.
|
|
27
30
|
*/
|
|
28
31
|
function initialize() {
|
|
29
|
-
var _a;
|
|
30
32
|
if (Platform.OS === 'ios') {
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
33
|
-
nativeModuleEmitter = new NativeEventEmitter((_a = NativeModules.NativeSessionRecorder) !== null && _a !== void 0 ? _a : NativeSessionRecorder);
|
|
34
35
|
if (!SupportedPlatforms.includes(Platform.OS)) {
|
|
35
36
|
noibuLog(`Noibu - Session recording supports ${SupportedPlatforms.join(', ')} only for now.`);
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
|
-
if (NativeSessionRecorder
|
|
39
|
+
if (!NativeSessionRecorder) {
|
|
39
40
|
noibuLog('Noibu - Session recording did not initialize properly.', LINKING_ERROR);
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
43
|
+
// For legacy Android, set up the event emitter; for new-arch Android, we poll via TurboModule
|
|
44
|
+
if (!isNewArchAndroid) {
|
|
45
|
+
const moduleForEmitter = RNModule !== null && RNModule !== void 0 ? RNModule : NativeSessionRecorder;
|
|
46
|
+
if (!moduleForEmitter) {
|
|
47
|
+
noibuLog('NativeSessionRecorder module missing for legacy Android.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
nativeModuleEmitter = new NativeEventEmitter(moduleForEmitter);
|
|
51
|
+
}
|
|
42
52
|
NativeSessionRecorder.initialize();
|
|
53
|
+
noibuLog("Session recorder initialized.");
|
|
43
54
|
}
|
|
44
55
|
let isIOSInitialized = false;
|
|
45
56
|
/**
|
|
@@ -58,26 +69,61 @@ let isIOSInitialized = false;
|
|
|
58
69
|
* @throws {Error} If the Noibu Session Recorder is not initialized before calling this function.
|
|
59
70
|
*/
|
|
60
71
|
function subscribeToNativeEvent(callback) {
|
|
72
|
+
var _a;
|
|
73
|
+
noibuLog("will subscribe to NativeEvent");
|
|
61
74
|
if (Platform.OS === 'android') {
|
|
75
|
+
if (isNewArchAndroid) {
|
|
76
|
+
noibuLog("is new architecture");
|
|
77
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
78
|
+
(function pump() {
|
|
79
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
while (true) {
|
|
81
|
+
try {
|
|
82
|
+
const batch = yield NativeSessionRecorder.consumeEvents();
|
|
83
|
+
noibuLog("this is the recording array: ", batch);
|
|
84
|
+
if (Array.isArray(batch) && batch.length > 0) {
|
|
85
|
+
for (const message of batch) {
|
|
86
|
+
try {
|
|
87
|
+
const _a = JSON.parse(message), { data } = _a, rest = __rest(_a, ["data"]);
|
|
88
|
+
const transformedEvents = transformToWeb([Object.assign(Object.assign({}, rest), { data })]);
|
|
89
|
+
for (const e of transformedEvents) {
|
|
90
|
+
callback({ message: JSON.stringify(e) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
// Skip malformed items
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// Swallow errors and continue polling
|
|
101
|
+
}
|
|
102
|
+
yield sleep(50);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
})();
|
|
106
|
+
// Keep polling indefinitely; do not expose a canceller in new-arch path
|
|
107
|
+
return () => { };
|
|
108
|
+
}
|
|
109
|
+
noibuLog("is NOT new architecture");
|
|
62
110
|
if (!nativeModuleEmitter) {
|
|
63
111
|
throw new Error('You have to initialize Noibu Session Recorder first');
|
|
64
112
|
}
|
|
65
|
-
//
|
|
66
|
-
// const subscription = nativeModuleEmitter.addListener('noibuRecordingEvent', callback);
|
|
113
|
+
// Legacy bridge path with DeviceEventManagerModule
|
|
67
114
|
nativeModuleEmitter.addListener('noibuRecordingEvent', event => {
|
|
68
115
|
const message = event.message;
|
|
69
116
|
const _a = JSON.parse(message), { data } = _a, rest = __rest(_a, ["data"]);
|
|
70
117
|
noibuLog('New noibu recording event', rest);
|
|
71
118
|
const transformedEvents = transformToWeb([Object.assign(Object.assign({}, rest), { data })]);
|
|
72
|
-
noibuLog('after transformation: ', transformedEvents);
|
|
73
119
|
// Emit pre-serialized JSON strings to minimize JS heap during batching
|
|
74
|
-
transformedEvents.forEach(
|
|
120
|
+
transformedEvents.forEach(e => callback({ message: JSON.stringify(e) }));
|
|
75
121
|
});
|
|
76
122
|
// return () => subscription.remove();
|
|
77
123
|
}
|
|
78
124
|
if (Platform.OS === 'ios') {
|
|
79
125
|
if (!nativeModuleEmitter) {
|
|
80
|
-
nativeModuleEmitter = new NativeEventEmitter(
|
|
126
|
+
nativeModuleEmitter = new NativeEventEmitter(RNModule);
|
|
81
127
|
noibuLog('nativeModuleEmitter', nativeModuleEmitter);
|
|
82
128
|
}
|
|
83
129
|
nativeModuleEmitter.addListener('iOSEvent', (event) => {
|
|
@@ -95,7 +141,7 @@ function subscribeToNativeEvent(callback) {
|
|
|
95
141
|
}
|
|
96
142
|
});
|
|
97
143
|
if (!isIOSInitialized) {
|
|
98
|
-
|
|
144
|
+
(_a = RNModule === null || RNModule === void 0 ? void 0 : RNModule.startIOS) === null || _a === void 0 ? void 0 : _a.call(RNModule);
|
|
99
145
|
isIOSInitialized = true;
|
|
100
146
|
}
|
|
101
147
|
// return () => subscription.remove();
|
|
@@ -26,7 +26,7 @@ Pod::Spec.new do |s|
|
|
|
26
26
|
#s.vendored_frameworks = "ios/SessionRecorder.xcframework"
|
|
27
27
|
#s.vendored_frameworks = "ios/Noibu.xcframework"
|
|
28
28
|
|
|
29
|
-
s.dependency 'NoibuSDK', '0.0.
|
|
29
|
+
s.dependency 'NoibuSDK', '0.0.8'
|
|
30
30
|
s.pod_target_xcconfig = {
|
|
31
31
|
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/noibu-react-native/ios/**\""
|
|
32
32
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
// @ts-ignore - Using type from newer React Native versions when building locally
|
|
2
2
|
import type { TurboModule } from 'react-native';
|
|
3
|
+
// Provide a type-only ambient declaration so TS doesn't require a real export
|
|
4
|
+
// This is used only for RN Codegen parsing inside a dead code branch below
|
|
5
|
+
declare const TurboModuleRegistry: {
|
|
6
|
+
get?<T>(name: string): T | null | undefined;
|
|
7
|
+
getEnforcing?<T>(name: string): T;
|
|
8
|
+
};
|
|
3
9
|
|
|
4
10
|
export interface Spec extends TurboModule {
|
|
5
11
|
initialize(): Promise<boolean>;
|
|
12
|
+
// Returns an array of raw mobile event JSON strings and clears native queue
|
|
13
|
+
consumeEvents?(): Promise<string[]>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Ensure the Codegen parser sees Spec usage with TurboModuleRegistry.get<Spec>(..)
|
|
17
|
+
// This block is never executed at runtime but allows codegen to infer the module name.
|
|
18
|
+
// eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unused-expressions
|
|
19
|
+
if (false) {
|
|
20
|
+
// The string must match the native module name
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
TurboModuleRegistry.get<Spec>('NoibuSessionRecorder');
|
|
6
23
|
}
|
|
7
24
|
|
|
8
|
-
//
|
|
25
|
+
// Safely access TurboModuleRegistry without throwing on legacy arch
|
|
9
26
|
type TurboModuleRegistryType = {
|
|
10
|
-
|
|
27
|
+
get?<T>(name: string): T | null | undefined;
|
|
28
|
+
getEnforcing?<T>(name: string): T;
|
|
11
29
|
};
|
|
12
30
|
|
|
13
31
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
14
32
|
const { TurboModuleRegistry: TurboModuleRegistryUntyped } = require('react-native') as {
|
|
15
|
-
TurboModuleRegistry
|
|
33
|
+
TurboModuleRegistry?: TurboModuleRegistryType;
|
|
16
34
|
};
|
|
17
35
|
|
|
18
|
-
|
|
36
|
+
let moduleRef: Spec | null = null;
|
|
37
|
+
try {
|
|
38
|
+
// Note: We've already destructured the TurboModuleRegistry object into TurboModuleRegistryUntyped
|
|
39
|
+
const tmr = TurboModuleRegistryUntyped as TurboModuleRegistryType | undefined;
|
|
40
|
+
// Prefer non-throwing get() if available
|
|
41
|
+
const maybe = tmr?.get ? tmr.get<Spec>('NoibuSessionRecorder') : undefined;
|
|
42
|
+
if (maybe) {
|
|
43
|
+
moduleRef = maybe;
|
|
44
|
+
} else if (tmr?.getEnforcing) {
|
|
45
|
+
// getEnforcing may throw on legacy; catch and fall back to null
|
|
46
|
+
moduleRef = tmr.getEnforcing<Spec>('NoibuSessionRecorder');
|
|
47
|
+
}
|
|
48
|
+
} catch (_e) {
|
|
49
|
+
moduleRef = null;
|
|
50
|
+
}
|
|
19
51
|
|
|
20
|
-
export default
|
|
52
|
+
export default moduleRef;
|