noibu-react-native 0.2.28 → 0.2.30

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,24 @@
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.30
7
+
8
+ - **Improve documentation for SDK configuration**
9
+
10
+ ## 0.2.29
11
+
12
+ - **React Native use new architecture using a poll mechanism rather than the old bridge**
13
+
14
+
15
+ ## 0.2.28
16
+
17
+ - **Fix a broken build where SDK would only work with iOS 26+**
18
+
19
+ ## 0.2.27
20
+
21
+ - **Recycling UI/library-managed bitmaps caused Glide/Fresco to crash when returning
22
+ recycled bitmaps to pools. Let GC manage lifecycle; still compress+store PNG.**
23
+
6
24
  ## 0.2.26
7
25
  - **iOS: iOS 26 Support**
8
26
 
package/README.md CHANGED
@@ -69,12 +69,18 @@ That's it! First time the module is set up, it runs an init and starts listening
69
69
 
70
70
  - `config` which consists of
71
71
  - `@property domain {string}` - indicates which Noibu dashboard session recordings should go to ([learn more about domains](https://help.noibu.com/hc/en-us/articles/4846518088845-Domains-Overview))
72
- - `@property [blockedElements] {string[]}` - lets you specify component ids to be ignored by SDK when collecting error information
73
- - `@property [enableHttpDataCollection] {boolean}` - indicates whether SDK should collect HTTP information like headers or body from requests
74
- - `@property [listOfUrlsToCollectHttpDataFrom] {string[]}` - is an allow list of URLs to allow HTTP data collection from, works best with `enableHttpDataCollection` enabled
75
- - `@property [httpPiiBlockingPatterns] {RegExp[]}` - allows you to specify RegEx patterns for what PII information should be removed from JSON request and response data for the value in a key value pair
76
- - `@property [fuzzyFieldsToRedact] {string[]}` - allows you to specify fuzzy strings for what PII information should be removed from JSON request and response data based on the key in a key value pair
77
- - `@property [exactFieldsToRedact] {string[]}` - allows you to specify strict strings for what PII information should be removed from JSON request and response data based on the key in a key value pair
72
+ - `@property [blockedElements] {string[]}` - defaults see below; lets you specify component ids to be ignored by SDK when collecting error information
73
+ - `@property [enableHttpDataCollection] {boolean}` - default `false`; indicates whether SDK should collect HTTP information like headers or body from requests
74
+ - `@property [listOfUrlsToCollectHttpDataFrom] {string[]}` - is an allowlist of URLs to allow HTTP data collection from, works best with `enableHttpDataCollection` enabled
75
+ - `@property [httpPiiBlockingPatterns] {RegExp[]}` - defaults see below; allows you to specify RegEx patterns for what PII information should be removed from JSON request and response data for the value in a key value pair
76
+ - `@property [fuzzyFieldsToRedact] {string[]}` - defaults see below; allows you to specify fuzzy strings for what PII information should be removed from JSON request and response data based on the key in a key value pair
77
+ - `@property [exactFieldsToRedact] {string[]}` - defaults see below; allows you to specify strict strings for what PII information should be removed from JSON request and response data based on the key in a key value pair
78
+
79
+ ### Note on overriding properties
80
+
81
+ The properties `blockedElements`, `httpPiiBlockingPatterns`, `fuzzyFieldsToRedact`, and `exactFieldsToRedact` have
82
+ default values.
83
+ If you want to override them or add new ones, you must also pass the original values, as seen below.
78
84
 
79
85
  Example:
80
86
 
@@ -199,11 +205,23 @@ Adds a custom attribute to the session.
199
205
  - `@param {any} value` - It's value, should be a JSON.stringify-able type.
200
206
  - `@returns {Promise<string>}` - A success message, or validation failure cause.
201
207
 
208
+ ##### Example custom attributes we recommend
209
+
210
+ - `appVersion` The version of your application the user's session is running.
211
+ - `customerId` The ID of the customer the user's session is associated with.
212
+ - `orderId` The ID of the order associated with the user's session.
213
+ - `voC` The Voice of Customer survey ID the user's session is associated with.
214
+
215
+ ##### Limitations on attribute name lengths
216
+
217
+ If the character length of the attribute name exceeds 50 characters, the attribute will fail to be added.
218
+ See example below:
219
+
202
220
  ```js
203
221
  import { NoibuJS } from 'noibu-react-native';
204
222
 
205
- NoibuJS.addCustomAttribute('myNameIs', 'Jeff'); // Promise<SUCCESS>
206
- NoibuJS.addCustomAttribute('veryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLong', 'Jeff'); // Promise<NAME_TOO_LONG>
223
+ NoibuJS.addCustomAttribute('stripeApiKey', 'some-api-key'); // Promise<SUCCESS>
224
+ NoibuJS.addCustomAttribute('system_configuration_payment_gateway_stripe_api_key_prod', 'some-api-key'); // Promise<NAME_TOO_LONG>
207
225
  ```
208
226
 
209
227
  #### `addError(customError: Error) => string`
@@ -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.28" ;
27
+ return "1.0.104-rn-sdk-0.2.30" ;
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noibu-react-native",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
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;