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.
@@ -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
- reactContext?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(eventName, params)
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 ?: return
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,
@@ -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
- val map = Arguments.createMap()
36
- map.putString("message", param)
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" ;
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 const _default: Spec;
6
- export default _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
- const TurboModuleRegistry = TurboModuleRegistryUntyped;
4
- var TurboNativeSessionRecorder = TurboModuleRegistry.getEnforcing('NoibuSessionRecorder');
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 { NativeSessionRecorder: LegacyNativeSessionRecorder, NoibuSessionRecorder } = NativeModules;
11
- const NativeSessionRecorder = TurboNativeSessionRecorder !== null && TurboNativeSessionRecorder !== void 0 ? TurboNativeSessionRecorder : LegacyNativeSessionRecorder;
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 === null) {
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
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
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((e) => callback({ message: JSON.stringify(e) }));
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(NoibuSessionRecorder);
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
- NoibuSessionRecorder.startIOS();
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.9'
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,6 +1,6 @@
1
1
  {
2
2
  "name": "noibu-react-native",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
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",
@@ -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
- // Create a typed facade for TurboModuleRegistry so we can use generics
25
+ // Safely access TurboModuleRegistry without throwing on legacy arch
9
26
  type TurboModuleRegistryType = {
10
- getEnforcing<T>(name: string): T;
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: TurboModuleRegistryType;
33
+ TurboModuleRegistry?: TurboModuleRegistryType;
16
34
  };
17
35
 
18
- const TurboModuleRegistry: TurboModuleRegistryType = TurboModuleRegistryUntyped;
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 TurboModuleRegistry.getEnforcing<Spec>('NoibuSessionRecorder');
52
+ export default moduleRef;