@coralogix/react-native-plugin 0.7.0 → 0.7.2

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
@@ -1,3 +1,16 @@
1
+ ## 0.7.2 (2026-07-20)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - drain every beforeSend delivery during the fatal flush
6
+ - flush crash events before fatal-error handover
7
+
8
+ ## 0.7.1 (2026-07-09)
9
+
10
+ ### 🩹 Fixes
11
+
12
+ - mask session replay views on the New Architecture (Fabric)
13
+
1
14
  ## 0.7.0 (2026-07-06)
2
15
 
3
16
  ### 🚀 Features
package/CxSdk.podspec CHANGED
@@ -16,9 +16,9 @@ Pod::Spec.new do |s|
16
16
 
17
17
  s.source_files = "ios/**/*.{h,m,mm,swift}"
18
18
 
19
- s.dependency 'Coralogix','2.10.0'
20
- s.dependency 'CoralogixInternal','2.10.0'
21
- s.dependency 'SessionReplay','2.10.0'
19
+ s.dependency 'Coralogix','2.11.1'
20
+ s.dependency 'CoralogixInternal','2.11.1'
21
+ s.dependency 'SessionReplay','2.11.1'
22
22
 
23
23
  # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
24
24
  # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
package/README.md CHANGED
@@ -467,7 +467,7 @@ import { SessionReplay } from '@coralogix/react-native-plugin';
467
467
  </View>
468
468
  ```
469
469
 
470
- The `SessionReplay.maskView` function accepts a `LayoutChangeEvent` and will mask the view in session replay recordings.
470
+ The `SessionReplay.maskView` function accepts a `LayoutChangeEvent` and will mask the view in session replay recordings. It works on both the legacy and the New Architecture (Fabric).
471
471
 
472
472
 
473
473
  ### Optional - Coralogix Gradle Plugin (Android)
@@ -84,7 +84,7 @@ dependencies {
84
84
  implementation "com.facebook.react:react-android"
85
85
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
86
86
 
87
- implementation "com.coralogix:android-sdk:2.17.1"
87
+ implementation "com.coralogix:android-sdk:2.18.0"
88
88
 
89
89
  testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
90
90
  testImplementation "org.mockito:mockito-core:5.12.0"
@@ -43,7 +43,8 @@ import com.facebook.react.bridge.WritableMap
43
43
  import com.facebook.react.modules.core.DeviceEventManagerModule
44
44
  import org.json.JSONArray
45
45
  import org.json.JSONObject
46
- import com.facebook.react.uimanager.UIManagerModule
46
+ import com.facebook.react.bridge.UiThreadUtil
47
+ import com.facebook.react.uimanager.UIManagerHelper
47
48
  import java.util.concurrent.ConcurrentHashMap
48
49
 
49
50
  class CxSdkModule(reactContext: ReactApplicationContext) :
@@ -192,6 +193,15 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
192
193
  )
193
194
  }
194
195
 
196
+ @ReactMethod
197
+ override fun flushSpans(promise: Promise) {
198
+ // Force-flushes the batch processor and resolves once the export/upload
199
+ // attempt finishes — the same contract as iOS. Called by the JS fatal-error
200
+ // handler before it hands over to the framework's default handler, which
201
+ // terminates the process.
202
+ CoralogixRum.flush { promise.resolve("flushSpans success") }
203
+ }
204
+
195
205
  @ReactMethod
196
206
  override fun sendCxSpanData(results: ReadableArray) {
197
207
  val data = convertStrictReadableArrayToListOfMaps(results)
@@ -311,11 +321,18 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
311
321
 
312
322
  @ReactMethod
313
323
  override fun maskViewByTag(viewTag: Int) {
314
- val uiManager = reactApplicationContext.getNativeModule(UIManagerModule::class.java)
315
-
316
- uiManager?.addUIBlock { nativeViewHierarchyManager ->
324
+ // Resolve the view through the architecture-agnostic UIManagerHelper so this
325
+ // works on both the legacy (Paper) and New (Fabric) architectures.
326
+ // getNativeModule(UIManagerModule) only exists on Paper; under Fabric it
327
+ // returns null and masking silently no-ops. UIManagerHelper routes by tag:
328
+ // legacy tags -> UIManagerModule, Fabric tags -> FabricUIManager, both
329
+ // exposing UIManager.resolveView(tag).
330
+ val context = reactApplicationContext
331
+ // FabricUIManager.resolveView must be called on the UI thread.
332
+ UiThreadUtil.runOnUiThread {
317
333
  try {
318
- val view = nativeViewHierarchyManager.resolveView(viewTag)
334
+ val uiManager = UIManagerHelper.getUIManagerForReactTag(context, viewTag)
335
+ val view = uiManager?.resolveView(viewTag)
319
336
  view?.maskView()
320
337
  } catch (t: Throwable) {
321
338
  t.printStackTrace()
@@ -19,6 +19,7 @@ interface RUMClient {
19
19
  fun startTimeMeasure(name: String, labels: ReadableMap?)
20
20
  fun endTimeMeasure(name: String)
21
21
  fun reportError(details: ReadableMap)
22
+ fun flushSpans(promise: Promise)
22
23
  fun sendCxSpanData(results: ReadableArray)
23
24
  fun reportMobileVitalsMeasurement(type: String, value: Double, units: String)
24
25
  fun reportMobileVitalsMeasurementSet(type: String, metrics: ReadableArray)
package/index.cjs.js CHANGED
@@ -130,10 +130,17 @@ class CoralogixErrorInstrumentation extends instrumentation.InstrumentationBase
130
130
  const defaultHandler = ErrorUtils.getGlobalHandler();
131
131
  ErrorUtils.setGlobalHandler((error, isFatal) => {
132
132
  this.report(error, isFatal);
133
- if (defaultHandler) {
134
- setTimeout(() => {
135
- defaultHandler(error, isFatal);
136
- }, 2500);
133
+ if (!defaultHandler) {
134
+ return;
135
+ }
136
+ if (isFatal) {
137
+ // In release builds the default handler terminates the app (RCTFatal),
138
+ // killing any crash event still queued in the native batch processor —
139
+ // flush it out first. waitForFatalErrorFlush never rejects and is
140
+ // time-capped, so the handover is guaranteed.
141
+ waitForFatalErrorFlush().catch(() => undefined).finally(() => defaultHandler(error, isFatal));
142
+ } else {
143
+ defaultHandler(error, isFatal);
137
144
  }
138
145
  });
139
146
  this.listenToConsoleError();
@@ -537,7 +544,7 @@ function stopJsRefreshRateSampler() {
537
544
  appStateSub = null;
538
545
  }
539
546
 
540
- var version = "0.7.0";
547
+ var version = "0.7.2";
541
548
  var pkg = {
542
549
  version: version};
543
550
 
@@ -1389,6 +1396,23 @@ const parseErrorStack = error => {
1389
1396
  return null;
1390
1397
  }).filter(frame => frame !== null);
1391
1398
  };
1399
+ const FATAL_FLUSH_MAX_WAIT_MS = 2500;
1400
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
1401
+
1402
+ /**
1403
+ * Best-effort delivery of a crash before a fatal error is handed back to RN's
1404
+ * default handler, which terminates the app in release builds. `flushSpans`
1405
+ * force-flushes the native batch processor; the native SDK uploads crash events
1406
+ * directly (bypassing the beforeSend round trip) and resolves once the upload
1407
+ * attempt finishes, so awaiting it is the full crash-delivery guarantee.
1408
+ * Never rejects; resolves within FATAL_FLUSH_MAX_WAIT_MS.
1409
+ *
1410
+ * @internal Used by the error instrumentation — not part of the public API.
1411
+ */
1412
+ const waitForFatalErrorFlush = async () => {
1413
+ const flush = Promise.resolve(CxSdk.flushSpans()).catch(e => logger.debug('flushSpans failed:', e));
1414
+ await Promise.race([flush, delay(FATAL_FLUSH_MAX_WAIT_MS)]);
1415
+ };
1392
1416
 
1393
1417
  // Create event emitter
1394
1418
  const eventEmitter = new reactNative.NativeEventEmitter(CxSdk);
@@ -1445,3 +1469,4 @@ exports.CxSdk = CxSdk;
1445
1469
  exports.SessionReplay = SessionReplay;
1446
1470
  exports.attachReactNavigationObserver = attachReactNavigationObserver;
1447
1471
  exports.parseErrorStack = parseErrorStack;
1472
+ exports.waitForFatalErrorFlush = waitForFatalErrorFlush;
package/index.esm.js CHANGED
@@ -128,10 +128,17 @@ class CoralogixErrorInstrumentation extends InstrumentationBase {
128
128
  const defaultHandler = ErrorUtils.getGlobalHandler();
129
129
  ErrorUtils.setGlobalHandler((error, isFatal) => {
130
130
  this.report(error, isFatal);
131
- if (defaultHandler) {
132
- setTimeout(() => {
133
- defaultHandler(error, isFatal);
134
- }, 2500);
131
+ if (!defaultHandler) {
132
+ return;
133
+ }
134
+ if (isFatal) {
135
+ // In release builds the default handler terminates the app (RCTFatal),
136
+ // killing any crash event still queued in the native batch processor —
137
+ // flush it out first. waitForFatalErrorFlush never rejects and is
138
+ // time-capped, so the handover is guaranteed.
139
+ waitForFatalErrorFlush().catch(() => undefined).finally(() => defaultHandler(error, isFatal));
140
+ } else {
141
+ defaultHandler(error, isFatal);
135
142
  }
136
143
  });
137
144
  this.listenToConsoleError();
@@ -535,7 +542,7 @@ function stopJsRefreshRateSampler() {
535
542
  appStateSub = null;
536
543
  }
537
544
 
538
- var version = "0.7.0";
545
+ var version = "0.7.2";
539
546
  var pkg = {
540
547
  version: version};
541
548
 
@@ -1387,6 +1394,23 @@ const parseErrorStack = error => {
1387
1394
  return null;
1388
1395
  }).filter(frame => frame !== null);
1389
1396
  };
1397
+ const FATAL_FLUSH_MAX_WAIT_MS = 2500;
1398
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
1399
+
1400
+ /**
1401
+ * Best-effort delivery of a crash before a fatal error is handed back to RN's
1402
+ * default handler, which terminates the app in release builds. `flushSpans`
1403
+ * force-flushes the native batch processor; the native SDK uploads crash events
1404
+ * directly (bypassing the beforeSend round trip) and resolves once the upload
1405
+ * attempt finishes, so awaiting it is the full crash-delivery guarantee.
1406
+ * Never rejects; resolves within FATAL_FLUSH_MAX_WAIT_MS.
1407
+ *
1408
+ * @internal Used by the error instrumentation — not part of the public API.
1409
+ */
1410
+ const waitForFatalErrorFlush = async () => {
1411
+ const flush = Promise.resolve(CxSdk.flushSpans()).catch(e => logger.debug('flushSpans failed:', e));
1412
+ await Promise.race([flush, delay(FATAL_FLUSH_MAX_WAIT_MS)]);
1413
+ };
1390
1414
 
1391
1415
  // Create event emitter
1392
1416
  const eventEmitter = new NativeEventEmitter(CxSdk);
@@ -1433,4 +1457,4 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1433
1457
  }
1434
1458
  });
1435
1459
 
1436
- export { CoralogixCustomSpan, CoralogixCustomTracer, CoralogixDomain, CoralogixGlobalSpan, CoralogixLogSeverity, CoralogixRum, CxSdk, SessionReplay, attachReactNavigationObserver, parseErrorStack };
1460
+ export { CoralogixCustomSpan, CoralogixCustomTracer, CoralogixDomain, CoralogixGlobalSpan, CoralogixLogSeverity, CoralogixRum, CxSdk, SessionReplay, attachReactNavigationObserver, parseErrorStack, waitForFatalErrorFlush };
package/ios/CxSdk.mm CHANGED
@@ -64,6 +64,9 @@ RCT_EXTERN_METHOD(reportNetworkRequest:(NSDictionary *)networkRequestDictionary
64
64
  withResolver:(RCTPromiseResolveBlock)resolve
65
65
  withRejecter:(RCTPromiseRejectBlock)reject)
66
66
 
67
+ RCT_EXTERN_METHOD(flushSpans:(RCTPromiseResolveBlock)resolve
68
+ withRejecter:(RCTPromiseRejectBlock)reject)
69
+
67
70
  RCT_EXTERN_METHOD(sendCxSpanData:(NSArray *)beforeSendResults
68
71
  withResolver:(RCTPromiseResolveBlock)resolve
69
72
  withRejecter:(RCTPromiseRejectBlock)reject)
package/ios/CxSdk.swift CHANGED
@@ -249,6 +249,24 @@ class CxSdk: RCTEventEmitter {
249
249
  resolve("reportError success")
250
250
  }
251
251
 
252
+ @objc(flushSpans:withRejecter:)
253
+ func flushSpans(resolve: @escaping RCTPromiseResolveBlock,
254
+ reject: @escaping RCTPromiseRejectBlock) -> Void {
255
+ // Force-flushes the batch processor and resolves once the export/upload
256
+ // attempt finishes — the same public SDK API the Android bridge calls.
257
+ // Invoked by the JS fatal-error handler before it hands over to RN's default
258
+ // handler, which terminates the process in release builds. `flush` handles
259
+ // its own threading and always invokes completion (immediately when the SDK
260
+ // is not initialized), so callers never hang.
261
+ guard let coralogixRum else {
262
+ resolve("flushSpans success")
263
+ return
264
+ }
265
+ coralogixRum.flush {
266
+ resolve("flushSpans success")
267
+ }
268
+ }
269
+
252
270
  @objc(sendCxSpanData:withResolver:withRejecter:)
253
271
  func sendCxSpanData(beforeSendResults: NSArray,
254
272
  resolve:RCTPromiseResolveBlock,
@@ -351,25 +369,34 @@ class CxSdk: RCTEventEmitter {
351
369
  func maskViewByTag(viewTag: NSNumber,
352
370
  resolve:@escaping RCTPromiseResolveBlock,
353
371
  reject:@escaping RCTPromiseRejectBlock) -> Void {
354
- guard
355
- let bridge = RCTBridge.current(),
356
- let uiManager = bridge.uiManager
357
- else {
358
- reject("MASK_VIEW_ERROR", "no ui manager found, aborting maskViewByTag", nil)
372
+ guard let bridge = RCTBridge.current() else {
373
+ reject("MASK_VIEW_ERROR", "no bridge found, aborting maskViewByTag", nil)
359
374
  return
360
375
  }
361
376
 
362
- RCTExecuteOnUIManagerQueue {
363
- uiManager.addUIBlock { (_, viewRegistry) in
364
- if let view = viewRegistry?[viewTag] as? UIView {
365
- DispatchQueue.main.async {
366
- view.cxMask = true
367
- resolve("view with tag \(viewTag) marked for masking")
368
- }
369
- } else {
370
- reject("MASK_VIEW_ERROR", "view with tag \(viewTag) not found, aborting maskViewByTag", nil)
371
- }
377
+ // View resolution and the cxMask mutation must happen on the main thread.
378
+ DispatchQueue.main.async {
379
+ // New Architecture (Fabric): mounted views live in the component-view
380
+ // registry, not the legacy RCTUIManager registry. Resolve them via the
381
+ // surface presenter; without this, masking silently no-ops on Fabric.
382
+ // The presenter is typed non-null but is absent on the legacy
383
+ // architecture, so bind it through an optional before use.
384
+ let surfacePresenter: (any RCTSurfacePresenterStub)? = bridge.surfacePresenter()
385
+ if let surfacePresenter = surfacePresenter,
386
+ let view = surfacePresenter.findComponentView(withTag_DO_NOT_USE_DEPRECATED: viewTag.intValue) {
387
+ view.cxMask = true
388
+ resolve("view with tag \(viewTag) marked for masking")
389
+ return
372
390
  }
391
+
392
+ // Legacy (Paper) architecture.
393
+ if let view = bridge.uiManager?.view(forReactTag: viewTag) {
394
+ view.cxMask = true
395
+ resolve("view with tag \(viewTag) marked for masking")
396
+ return
397
+ }
398
+
399
+ reject("MASK_VIEW_ERROR", "view with tag \(viewTag) not found, aborting maskViewByTag", nil)
373
400
  }
374
401
  }
375
402
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Official Coralogix React Native plugin",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Coralogix",
package/src/index.d.ts CHANGED
@@ -25,6 +25,14 @@ interface CxSdkClient {
25
25
  endTimeMeasure(name: string): void;
26
26
  reportError(details: Record<string, unknown>): void;
27
27
  reportNetworkRequest(details: NetworkRequestDetails): void;
28
+ /**
29
+ * Forces immediate export of natively queued telemetry. iOS drains the batch
30
+ * processor and resolves after the upload attempt; Android resolves
31
+ * immediately WITHOUT flushing (the native SDK exposes no flush API yet), so
32
+ * a resolved promise must not be read as delivery there — see
33
+ * waitForFatalErrorFlush for the Android fallback.
34
+ */
35
+ flushSpans(): Promise<string>;
28
36
  shutdown(): Promise<boolean>;
29
37
  createNewSession(): Promise<boolean>;
30
38
  sendCxSpanData(results: CxSpan[]): void;
@@ -56,6 +64,17 @@ export declare const CxSdk: CxSdkClient;
56
64
  export declare const CoralogixRum: CoralogixOtelWebType;
57
65
  export declare const SessionReplay: SessionReplayType;
58
66
  export declare const parseErrorStack: (error: Error) => CoralogixStackFrame[];
67
+ /**
68
+ * Best-effort delivery of a crash before a fatal error is handed back to RN's
69
+ * default handler, which terminates the app in release builds. `flushSpans`
70
+ * force-flushes the native batch processor; the native SDK uploads crash events
71
+ * directly (bypassing the beforeSend round trip) and resolves once the upload
72
+ * attempt finishes, so awaiting it is the full crash-delivery guarantee.
73
+ * Never rejects; resolves within FATAL_FLUSH_MAX_WAIT_MS.
74
+ *
75
+ * @internal Used by the error instrumentation — not part of the public API.
76
+ */
77
+ export declare const waitForFatalErrorFlush: () => Promise<void>;
59
78
  export { type ApplicationContextConfig } from './model/ApplicationContextConfig';
60
79
  export { type ViewContextConfig } from './model/ViewContextConfig';
61
80
  export { CoralogixDomain } from './model/CoralogixDomain';