@coralogix/react-native-plugin 0.1.10 → 0.2.1

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,31 @@
1
+ ## 0.2.1 (2025-11-18)
2
+
3
+ ### 🚀 Features
4
+
5
+ - update git ignore ([#72](https://github.com/coralogix/cx-react-native-plugin/pull/72))
6
+
7
+ ### 🩹 Patch
8
+
9
+ - Bump Android native version to 2.6.0, updated the code to support latest changes in the native sdk
10
+
11
+ ## 0.2.0 (2025-11-11)
12
+
13
+ ### 🚀 Features
14
+
15
+ - add session replay Documents
16
+ - added manual masking functionality
17
+ - added missing session replay options
18
+ - added basic session replay bridge implementation for react native
19
+
20
+ ### 🩹 Fixes
21
+
22
+ - pr changes
23
+ - update readme.md
24
+ - changed implementation for masking specific elements to a hook on onLayout to not influence the app's layout with an extra layout node
25
+ - iOS masking view + version bump 1.4.0
26
+ - mask all texts default value
27
+ - moved android impl for shutdown to a handler to run on main
28
+
1
29
  ## 0.1.10 (2025-11-11)
2
30
 
3
31
  ### 🩹 Fixes
package/CxSdk.podspec CHANGED
@@ -18,7 +18,7 @@ Pod::Spec.new do |s|
18
18
 
19
19
  s.dependency 'Coralogix','1.4.0'
20
20
  s.dependency 'CoralogixInternal','1.4.0'
21
-
21
+ s.dependency 'SessionReplay','1.4.0'
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
@@ -210,7 +210,7 @@ await CoralogixRum.init({
210
210
  Proxy configuration to route requests.
211
211
  By specifying a proxy URL, all RUM data will be directed to this URL via the POST method.
212
212
  However, it is necessary for this data to be subsequently relayed from the proxy to Coralogix.
213
- The Coralogix route for each request that is sent to the proxy is available in the requests cxforward parameter
213
+ The Coralogix route for each request that is sent to the proxy is available in the request's cxforward parameter
214
214
  (for example, https://www.your-proxy.com/endpoint?cxforward=https%3A%2F%2Fingress.eu1.rum-ingress-coralogix.com%2Fbrowser%2Fv1beta%2Flogs).
215
215
 
216
216
  ```javascript
@@ -221,6 +221,107 @@ await CoralogixRum.init({
221
221
  });
222
222
  ```
223
223
 
224
+ ### Session Replay
225
+
226
+ Session Replay allows you to record and replay user sessions to understand user behavior and debug issues.
227
+
228
+ #### Initialize Session Replay
229
+
230
+ To initialize Session Replay, call `SessionReplay.init(options)` with the desired configuration options.
231
+
232
+ ```javascript
233
+ import { SessionReplay } from '@coralogix/react-native-plugin';
234
+
235
+ await SessionReplay.init({
236
+ captureScale: 0.5, // Scale factor for screenshots (0.0 to 1.0)
237
+ captureCompressQuality: 0.8, // Compression quality for screenshots (0.0 to 1.0)
238
+ sessionRecordingSampleRate: 100, // Percentage of sessions to record (0 to 100)
239
+ autoStartSessionRecording: true, // Automatically start recording when initialized
240
+ maskAllTexts: true, // Mask all text content by default (optional, default: true)
241
+ textsToMask: ['password', '^card.*'], // Array of strings/regex patterns for specific text masking (optional)
242
+ maskAllImages: false, // Mask all images (optional, default: false)
243
+ });
244
+ ```
245
+
246
+ **Options:**
247
+ - `captureScale` (required): Scale factor for screenshots. Must be between 0.0 and 1.0. Lower values reduce file size but may decrease quality.
248
+ - `captureCompressQuality` (required): Compression quality for screenshots. Must be between 0.0 and 1.0. Higher values improve quality but increase file size.
249
+ - `sessionRecordingSampleRate` (required): Percentage of sessions to record. Must be between 0 and 100. Use 100 to record all sessions.
250
+ - `autoStartSessionRecording` (required): If `true`, recording starts automatically after initialization. If `false`, you must manually call `startSessionRecording()`.
251
+ - `maskAllTexts` (optional): If `true`, all text content is masked by default. Defaults to `true`.
252
+ - `textsToMask` (optional): Array of strings or regex patterns to mask specific text content. Only used when `maskAllTexts` is `false`.
253
+ - `maskAllImages` (optional): If `true`, all images are masked. Defaults to `false`.
254
+
255
+ #### Check Initialization Status
256
+
257
+ Check if Session Replay has been initialized:
258
+
259
+ ```javascript
260
+ const isInited = await SessionReplay.isInited();
261
+ console.log('Session Replay initialized:', isInited);
262
+ ```
263
+
264
+ #### Check Recording Status
265
+
266
+ Check if Session Replay is currently recording:
267
+
268
+ ```javascript
269
+ const isRecording = await SessionReplay.isRecording();
270
+ console.log('Session Replay recording:', isRecording);
271
+ ```
272
+
273
+ #### Start Recording
274
+
275
+ Manually start session recording:
276
+
277
+ ```javascript
278
+ SessionReplay.startSessionRecording();
279
+ ```
280
+
281
+ **Note:** If `autoStartSessionRecording` is set to `true` in the init options, recording starts automatically and you don't need to call this method.
282
+
283
+ #### Stop Recording
284
+
285
+ Manually stop session recording:
286
+
287
+ ```javascript
288
+ SessionReplay.stopSessionRecording();
289
+ ```
290
+
291
+ #### Capture Screenshot
292
+
293
+ Manually capture a screenshot during a session:
294
+
295
+ ```javascript
296
+ SessionReplay.captureScreenshot();
297
+ ```
298
+
299
+ This is useful for capturing specific moments in the user journey that you want to highlight.
300
+
301
+ #### Shutdown Session Replay
302
+
303
+ Shutdown Session Replay to clean up resources:
304
+
305
+ ```javascript
306
+ await SessionReplay.shutdown();
307
+ ```
308
+
309
+ #### Masking Sensitive Content
310
+
311
+ To mask sensitive content in your app, use the `onLayout` prop with `SessionReplay.maskView` on any View component that should be masked:
312
+
313
+ ```javascript
314
+ import { SessionReplay } from '@coralogix/react-native-plugin';
315
+
316
+ <View onLayout={SessionReplay.maskView}>
317
+ <Text>This text will be masked in session replay</Text>
318
+ <TextInput placeholder="Password" />
319
+ </View>
320
+ ```
321
+
322
+ The `SessionReplay.maskView` function accepts a `LayoutChangeEvent` and will mask the view in session replay recordings.
323
+
324
+
224
325
  ### Optional - Coralogix Gradle Plugin (Android)
225
326
 
226
327
  The Coralogix Gradle Plugin automatically instruments all OkHttp clients in your app (including third-party SDKs) at build time.
@@ -75,7 +75,7 @@ dependencies {
75
75
  implementation "com.facebook.react:react-android"
76
76
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
77
 
78
- implementation "com.coralogix:android-sdk:2.5.6"
78
+ implementation "com.coralogix:android-sdk:2.6.0"
79
79
  }
80
80
 
81
81
  react {
@@ -18,6 +18,9 @@ import com.coralogix.android.sdk.model.Instrumentation
18
18
  import com.coralogix.android.sdk.model.MobileVitalType
19
19
  import com.coralogix.android.sdk.model.UserContext
20
20
  import com.coralogix.android.sdk.model.ViewContext
21
+ import com.coralogix.android.sdk.session_replay.SessionReplay
22
+ import com.coralogix.android.sdk.session_replay.SessionReplay.maskView
23
+ import com.coralogix.android.sdk.session_replay.model.SessionReplayOptions
21
24
  import com.facebook.react.bridge.Arguments
22
25
  import com.facebook.react.bridge.Promise
23
26
  import com.facebook.react.bridge.ReactApplicationContext
@@ -32,14 +35,16 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
32
35
  import org.json.JSONArray
33
36
  import org.json.JSONObject
34
37
  import java.lang.Long.getLong
38
+ import com.facebook.react.uimanager.UIManagerModule
35
39
 
36
40
  class CxSdkModule(reactContext: ReactApplicationContext) :
37
- ReactContextBaseJavaModule(reactContext), ICxSdkModule {
41
+ ReactContextBaseJavaModule(reactContext), RUMClient, SessionReplayClient {
38
42
 
39
43
  override fun getName(): String {
40
44
  return NAME
41
45
  }
42
46
 
47
+ // region - SDK methods
43
48
  @ReactMethod
44
49
  override fun initialize(config: ReadableMap, promise: Promise) {
45
50
  val application = reactApplicationContext.applicationContext as Application
@@ -88,13 +93,14 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
88
93
 
89
94
  @ReactMethod
90
95
  override fun setLabels(labelsMap: ReadableMap) {
91
- val labels = labelsMap.toStringMap()
96
+ val labels = labelsMap.toStringAnyMap()
92
97
  CoralogixRum.setLabels(labels)
93
98
  }
94
99
 
95
100
  @ReactMethod
96
101
  override fun getLabels(promise: Promise) {
97
- promise.resolve(CoralogixRum.getLabels().toWritableMap())
102
+ val labelsMap = convertMapToWritableMap(CoralogixRum.getLabels())
103
+ promise.resolve(labelsMap)
98
104
  }
99
105
 
100
106
  @ReactMethod
@@ -104,7 +110,7 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
104
110
 
105
111
  @ReactMethod
106
112
  override fun log(severity: Int, message: String, data: ReadableMap, labels: ReadableMap) {
107
- CoralogixRum.log(severity.toCoralogixLogSeverity(), message, data.toStringMap(), labels.toStringMap())
113
+ CoralogixRum.log(severity.toCoralogixLogSeverity(), message, data.toStringAnyMap(), labels.toStringAnyMap())
108
114
  }
109
115
 
110
116
  @ReactMethod
@@ -197,7 +203,70 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
197
203
  fun removeListeners(count: Int) {
198
204
  Log.d("CxSdkModule", "removeListeners: $count")
199
205
  }
206
+ // endregion
200
207
 
208
+ // region - Session Replay methods
209
+ @ReactMethod
210
+ override fun initializeSessionReplay(options: ReadableMap, promise: Promise) {
211
+ val sessionReplayOptions = options.toSessionReplayOptions()
212
+
213
+ Handler(Looper.getMainLooper()).post {
214
+ SessionReplay.initialize(reactApplicationContext, sessionReplayOptions)
215
+ promise.resolve(true)
216
+ }
217
+ }
218
+
219
+ @ReactMethod
220
+ override fun shutdownSessionReplay(promise: Promise) {
221
+ Handler(Looper.getMainLooper()).post {
222
+ SessionReplay.shutdown()
223
+ promise.resolve(true)
224
+ }
225
+ }
226
+
227
+ @ReactMethod
228
+ override fun isSessionReplayInitialized(promise: Promise) {
229
+ val isInitialized = SessionReplay.isInitialized()
230
+ promise.resolve(isInitialized)
231
+ }
232
+
233
+ @ReactMethod
234
+ override fun isRecording(promise: Promise) {
235
+ val isRecording = SessionReplay.isRecording()
236
+ promise.resolve(isRecording)
237
+ }
238
+
239
+ @ReactMethod
240
+ override fun startSessionRecording() {
241
+ SessionReplay.startSessionRecording()
242
+ }
243
+
244
+ @ReactMethod
245
+ override fun stopSessionRecording() {
246
+ SessionReplay.stopSessionRecording()
247
+ }
248
+
249
+ @ReactMethod
250
+ override fun captureScreenshot() {
251
+ SessionReplay.captureScreenshot()
252
+ }
253
+
254
+ @ReactMethod
255
+ override fun maskViewByTag(viewTag: Int) {
256
+ val uiManager = reactApplicationContext.getNativeModule(UIManagerModule::class.java)
257
+
258
+ uiManager?.addUIBlock { nativeViewHierarchyManager ->
259
+ try {
260
+ val view = nativeViewHierarchyManager.resolveView(viewTag)
261
+ view?.maskView()
262
+ } catch (t: Throwable) {
263
+ t.printStackTrace()
264
+ }
265
+ }
266
+ }
267
+ // endregion
268
+
269
+ // region - utils
201
270
  override fun invalidate() {
202
271
  super.invalidate()
203
272
 
@@ -207,6 +276,29 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
207
276
  }
208
277
  }
209
278
 
279
+ private fun ReadableMap.toSessionReplayOptions(): SessionReplayOptions {
280
+ val scale = if (hasKey("captureScale")) getDouble("captureScale") else 0.5
281
+ val quality = if (hasKey("captureCompressQuality")) getDouble("captureCompressQuality") else 1
282
+ val sampleRate = if (hasKey("sessionRecordingSampleRate")) getInt("sessionRecordingSampleRate") else 100
283
+ val autoStart = if (hasKey("autoStartSessionRecording")) getBoolean("autoStartSessionRecording") else true
284
+ val maskAllTexts = if (hasKey("maskAllTexts")) getBoolean("maskAllTexts") else true
285
+ val maskAllImages = if (hasKey("maskAllImages")) getBoolean("maskAllImages") else false
286
+
287
+ val textsToMask = if (hasKey("textsToMask") && !isNull("textsToMask")) {
288
+ getArray("textsToMask")?.handleStringOrRegexList() ?: emptyList()
289
+ } else emptyList()
290
+
291
+ return SessionReplayOptions(
292
+ captureScale = scale.toFloat(),
293
+ captureCompressQuality = quality.toFloat(),
294
+ sessionRecordingSampleRate = sampleRate,
295
+ autoStartSessionRecording = autoStart,
296
+ maskAllTexts = maskAllTexts,
297
+ textsToMask = textsToMask,
298
+ maskAllImages = maskAllImages
299
+ )
300
+ }
301
+
210
302
  private fun ReadableMap.toCoralogixOptions(): CoralogixOptions {
211
303
  val applicationName = getString("application")
212
304
  ?: throw IllegalArgumentException("Missing required parameter: application")
@@ -225,7 +317,7 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
225
317
  applicationName = applicationName,
226
318
  coralogixDomain = coralogixDomain,
227
319
  publicKey = publicKey,
228
- labels = getMap("labels")?.toStringMap() ?: mapOf(),
320
+ labels = getMap("labels")?.toStringAnyMap() ?: mapOf(),
229
321
  environment = getString("environment") ?: "",
230
322
  version = getString("version") ?: "",
231
323
  userContext = getMap("user_context")?.toUserContext() ?: UserContext(),
@@ -426,6 +518,27 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
426
518
  )
427
519
  }
428
520
 
521
+ private fun ReadableMap.toStringAnyMap(): Map<String, Any?> {
522
+ val result = mutableMapOf<String, Any?>()
523
+ val iterator = keySetIterator()
524
+ while (iterator.hasNextKey()) {
525
+ val key = iterator.nextKey()
526
+ val jsonValue = when (getType(key)) {
527
+ ReadableType.Null -> JSONObject.NULL
528
+ ReadableType.Boolean -> getBoolean(key)
529
+ ReadableType.Number -> getDouble(key)
530
+ ReadableType.String -> getString(key)
531
+ ReadableType.Map -> getMap(key)?.toStringAnyMap()
532
+ ReadableType.Array -> convertReadableArrayToAnyList(getArray(key))
533
+ else -> JSONObject.NULL
534
+ }
535
+
536
+ result[key] = jsonValue
537
+ }
538
+
539
+ return result
540
+ }
541
+
429
542
  private fun ReadableMap.toStringMap(): Map<String, String> {
430
543
  val result = mutableMapOf<String, String>()
431
544
  val iterator = keySetIterator()
@@ -550,6 +663,7 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
550
663
  }
551
664
  return out
552
665
  }
666
+ // endregion
553
667
 
554
668
  companion object {
555
669
  const val NAME = "CxSdk"
@@ -4,7 +4,7 @@ import com.facebook.react.bridge.Promise
4
4
  import com.facebook.react.bridge.ReadableArray
5
5
  import com.facebook.react.bridge.ReadableMap
6
6
 
7
- interface ICxSdkModule {
7
+ interface RUMClient {
8
8
  fun initialize(config: ReadableMap, promise: Promise)
9
9
  fun setUserContext(userContextMap: ReadableMap)
10
10
  fun getUserContext(promise: Promise)
@@ -0,0 +1,15 @@
1
+ package com.cxsdk
2
+
3
+ import com.facebook.react.bridge.Promise
4
+ import com.facebook.react.bridge.ReadableMap
5
+
6
+ interface SessionReplayClient {
7
+ fun initializeSessionReplay(options: ReadableMap, promise: Promise)
8
+ fun shutdownSessionReplay(promise: Promise)
9
+ fun isSessionReplayInitialized(promise: Promise)
10
+ fun isRecording(promise: Promise)
11
+ fun startSessionRecording()
12
+ fun stopSessionRecording()
13
+ fun captureScreenshot()
14
+ fun maskViewByTag(viewTag: Int)
15
+ }
package/index.cjs.js CHANGED
@@ -219,7 +219,7 @@ function stopJsRefreshRateSampler() {
219
219
  appStateSub = null;
220
220
  }
221
221
 
222
- var version = "0.1.10";
222
+ var version = "0.2.1";
223
223
  var pkg = {
224
224
  version: version};
225
225
 
@@ -373,6 +373,13 @@ class CoralogixFetchInstrumentation extends instrumentationFetch.FetchInstrument
373
373
  }
374
374
  }
375
375
 
376
+ function isSessionReplayOptionsValid(options) {
377
+ const scaleValid = options.captureScale > 0 && options.captureScale <= 1;
378
+ const sampleRateValid = options.sessionRecordingSampleRate >= 0 && options.sessionRecordingSampleRate <= 100;
379
+ const qualityValid = options.captureCompressQuality > 0 && options.captureCompressQuality <= 1;
380
+ return scaleValid && sampleRateValid && qualityValid;
381
+ }
382
+
376
383
  let lastRouteName;
377
384
  function getLastNavigationRouteDetected() {
378
385
  return lastRouteName;
@@ -582,6 +589,48 @@ const CoralogixRum = {
582
589
  CxSdk.reportError(errorDetails);
583
590
  }
584
591
  };
592
+ const SessionReplay = {
593
+ init: async options => {
594
+ logger.debug("session replay: init called with options: ", options);
595
+ const optionsValid = isSessionReplayOptionsValid(options);
596
+ if (!optionsValid) {
597
+ logger.warn("invalid options in SessionReplay.init: ", options);
598
+ return false;
599
+ }
600
+ return await CxSdk.initializeSessionReplay(options);
601
+ },
602
+ shutdown: async () => {
603
+ logger.debug("session replay: shutdown called");
604
+ return await CxSdk.shutdownSessionReplay();
605
+ },
606
+ captureScreenshot: () => {
607
+ logger.debug("session replay: captureScreenshot called");
608
+ CxSdk.captureScreenshot();
609
+ },
610
+ isInited: async () => {
611
+ logger.debug("session replay: isInited called");
612
+ return await CxSdk.isSessionReplayInitialized();
613
+ },
614
+ isRecording: async () => {
615
+ logger.debug("session replay: isRecording called");
616
+ return await CxSdk.isRecording();
617
+ },
618
+ startSessionRecording: () => {
619
+ logger.debug("session replay: startSessionRecording called");
620
+ CxSdk.startSessionRecording();
621
+ },
622
+ stopSessionRecording: () => {
623
+ logger.debug("session replay: stopSessionRecording called");
624
+ CxSdk.stopSessionRecording();
625
+ },
626
+ maskView: event => {
627
+ logger.debug("session replay: maskViewByTag called");
628
+ const viewTag = reactNative.findNodeHandle(event.target);
629
+ if (viewTag) {
630
+ CxSdk.maskViewByTag(viewTag);
631
+ }
632
+ }
633
+ };
585
634
  function trackMobileVitals(options) {
586
635
  var _options$mobileVitals;
587
636
  const shouldEnableJsRefreshRateDetector = ((_options$mobileVitals = options.mobileVitals) == null ? void 0 : _options$mobileVitals.jsRefreshRate) !== false;
@@ -729,4 +778,5 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
729
778
  exports.CoralogixDomain = CoralogixDomain;
730
779
  exports.CoralogixLogSeverity = CoralogixLogSeverity;
731
780
  exports.CoralogixRum = CoralogixRum;
781
+ exports.SessionReplay = SessionReplay;
732
782
  exports.attachReactNavigationObserver = attachReactNavigationObserver;
package/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AppState, Platform, NativeModules, NativeEventEmitter } from 'react-native';
1
+ import { AppState, Platform, NativeModules, NativeEventEmitter, findNodeHandle } from 'react-native';
2
2
  import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
3
3
  import { InstrumentationBase, registerInstrumentations } from '@opentelemetry/instrumentation';
4
4
  import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
@@ -217,7 +217,7 @@ function stopJsRefreshRateSampler() {
217
217
  appStateSub = null;
218
218
  }
219
219
 
220
- var version = "0.1.10";
220
+ var version = "0.2.1";
221
221
  var pkg = {
222
222
  version: version};
223
223
 
@@ -371,6 +371,13 @@ class CoralogixFetchInstrumentation extends FetchInstrumentation {
371
371
  }
372
372
  }
373
373
 
374
+ function isSessionReplayOptionsValid(options) {
375
+ const scaleValid = options.captureScale > 0 && options.captureScale <= 1;
376
+ const sampleRateValid = options.sessionRecordingSampleRate >= 0 && options.sessionRecordingSampleRate <= 100;
377
+ const qualityValid = options.captureCompressQuality > 0 && options.captureCompressQuality <= 1;
378
+ return scaleValid && sampleRateValid && qualityValid;
379
+ }
380
+
374
381
  let lastRouteName;
375
382
  function getLastNavigationRouteDetected() {
376
383
  return lastRouteName;
@@ -580,6 +587,48 @@ const CoralogixRum = {
580
587
  CxSdk.reportError(errorDetails);
581
588
  }
582
589
  };
590
+ const SessionReplay = {
591
+ init: async options => {
592
+ logger.debug("session replay: init called with options: ", options);
593
+ const optionsValid = isSessionReplayOptionsValid(options);
594
+ if (!optionsValid) {
595
+ logger.warn("invalid options in SessionReplay.init: ", options);
596
+ return false;
597
+ }
598
+ return await CxSdk.initializeSessionReplay(options);
599
+ },
600
+ shutdown: async () => {
601
+ logger.debug("session replay: shutdown called");
602
+ return await CxSdk.shutdownSessionReplay();
603
+ },
604
+ captureScreenshot: () => {
605
+ logger.debug("session replay: captureScreenshot called");
606
+ CxSdk.captureScreenshot();
607
+ },
608
+ isInited: async () => {
609
+ logger.debug("session replay: isInited called");
610
+ return await CxSdk.isSessionReplayInitialized();
611
+ },
612
+ isRecording: async () => {
613
+ logger.debug("session replay: isRecording called");
614
+ return await CxSdk.isRecording();
615
+ },
616
+ startSessionRecording: () => {
617
+ logger.debug("session replay: startSessionRecording called");
618
+ CxSdk.startSessionRecording();
619
+ },
620
+ stopSessionRecording: () => {
621
+ logger.debug("session replay: stopSessionRecording called");
622
+ CxSdk.stopSessionRecording();
623
+ },
624
+ maskView: event => {
625
+ logger.debug("session replay: maskViewByTag called");
626
+ const viewTag = findNodeHandle(event.target);
627
+ if (viewTag) {
628
+ CxSdk.maskViewByTag(viewTag);
629
+ }
630
+ }
631
+ };
583
632
  function trackMobileVitals(options) {
584
633
  var _options$mobileVitals;
585
634
  const shouldEnableJsRefreshRateDetector = ((_options$mobileVitals = options.mobileVitals) == null ? void 0 : _options$mobileVitals.jsRefreshRate) !== false;
@@ -724,4 +773,4 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
724
773
  }
725
774
  });
726
775
 
727
- export { CoralogixDomain, CoralogixLogSeverity, CoralogixRum, attachReactNavigationObserver };
776
+ export { CoralogixDomain, CoralogixLogSeverity, CoralogixRum, SessionReplay, attachReactNavigationObserver };
package/ios/CxSdk.mm CHANGED
@@ -78,6 +78,32 @@ RCT_EXTERN_METHOD(sendCustomMeasurement:(NSDictionary *)measurement
78
78
  withResolver:(RCTPromiseResolveBlock)resolve
79
79
  withRejecter:(RCTPromiseRejectBlock)reject)
80
80
 
81
+ RCT_EXTERN_METHOD(initializeSessionReplay:(NSDictionary *)options
82
+ withResolver:(RCTPromiseResolveBlock)resolve
83
+ withRejecter:(RCTPromiseRejectBlock)reject)
84
+
85
+ RCT_EXTERN_METHOD(shutdownSessionReplay:(RCTPromiseResolveBlock)resolve
86
+ withRejecter:(RCTPromiseRejectBlock)reject)
87
+
88
+ RCT_EXTERN_METHOD(isSessionReplayInitialized:(RCTPromiseResolveBlock)resolve
89
+ withRejecter:(RCTPromiseRejectBlock)reject)
90
+
91
+ RCT_EXTERN_METHOD(isRecording:(RCTPromiseResolveBlock)resolve
92
+ withRejecter:(RCTPromiseRejectBlock)reject)
93
+
94
+ RCT_EXTERN_METHOD(startSessionRecording:(RCTPromiseResolveBlock)resolve
95
+ withRejecter:(RCTPromiseRejectBlock)reject)
96
+
97
+ RCT_EXTERN_METHOD(stopSessionRecording:(RCTPromiseResolveBlock)resolve
98
+ withRejecter:(RCTPromiseRejectBlock)reject)
99
+
100
+ RCT_EXTERN_METHOD(captureScreenshot:(RCTPromiseResolveBlock)resolve
101
+ withRejecter:(RCTPromiseRejectBlock)reject)
102
+
103
+ RCT_EXTERN_METHOD(maskViewByTag:(nonnull NSNumber *)viewTag
104
+ withResolver:(RCTPromiseResolveBlock)resolve
105
+ withRejecter:(RCTPromiseRejectBlock)reject)
106
+
81
107
  + (BOOL)requiresMainQueueSetup
82
108
  {
83
109
  return NO;
package/ios/CxSdk.swift CHANGED
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import Coralogix
3
+ import SessionReplay
3
4
  import React
4
5
 
5
6
  enum CxSdkError: Error {
@@ -193,8 +194,8 @@ class CxSdk: RCTEventEmitter {
193
194
 
194
195
  @objc(sendCxSpanData:withResolver:withRejecter:)
195
196
  func sendCxSpanData(beforeSendResults: NSArray,
196
- resolve:RCTPromiseResolveBlock,
197
- reject:RCTPromiseRejectBlock) -> Void {
197
+ resolve:RCTPromiseResolveBlock,
198
+ reject:RCTPromiseRejectBlock) -> Void {
198
199
  guard let beforeSendResults = beforeSendResults as? [[String: Any]] else {
199
200
  reject("Invalid sendBeforeSendData", "sendBeforeSendData is not a dictionary", nil)
200
201
  return
@@ -205,8 +206,8 @@ class CxSdk: RCTEventEmitter {
205
206
 
206
207
  @objc(reportMobileVitalsMeasurementSet:metrics:withResolver:withRejecter:)
207
208
  func reportMobileVitalsMeasurementSet(type: String, metrics: NSArray,
208
- resolve:RCTPromiseResolveBlock,
209
- reject:RCTPromiseRejectBlock) -> Void {
209
+ resolve:RCTPromiseResolveBlock,
210
+ reject:RCTPromiseRejectBlock) -> Void {
210
211
  let list = self.toHybridMetricList(metrics: metrics)
211
212
  coralogixRum?.reportMobileVitalsMeasurement(type: type, metrics: list)
212
213
  resolve("reportMobileVitalsMeasurement success")
@@ -214,12 +215,103 @@ class CxSdk: RCTEventEmitter {
214
215
 
215
216
  @objc(reportMobileVitalsMeasurement:value:units:withResolver:withRejecter:)
216
217
  func reportMobileVitalsMeasurement(type: String, value: Double, units: String,
217
- resolve:RCTPromiseResolveBlock,
218
- reject:RCTPromiseRejectBlock) -> Void {
218
+ resolve:RCTPromiseResolveBlock,
219
+ reject:RCTPromiseRejectBlock) -> Void {
219
220
  coralogixRum?.reportMobileVitalsMeasurement(type: type, value: value, units: units)
220
221
  resolve("reportMobileVitalsMeasurement success")
221
222
  }
222
223
 
224
+
225
+ @objc(initializeSessionReplay:withResolver:withRejecter:)
226
+ func initializeSessionReplay(options: NSDictionary,
227
+ resolve:RCTPromiseResolveBlock,
228
+ reject:RCTPromiseRejectBlock) -> Void {
229
+ do {
230
+ let sessionReplayOptions = try self.toSessionReplayOptions(parameter: options)
231
+ SessionReplay.initializeWithOptions(sessionReplayOptions:sessionReplayOptions)
232
+ } catch let error as CxSdkError {
233
+ reject("CX_SDK_ERROR", error.localizedDescription, error)
234
+ } catch {
235
+ reject("UNEXPECTED_ERROR", "An unexpected error occurred: \(error.localizedDescription)", error)
236
+ }
237
+ resolve("initializeSessionReplay success")
238
+ }
239
+
240
+ @objc(shutdownSessionReplay:withRejecter:)
241
+ func shutdownSessionReplay(resolve:RCTPromiseResolveBlock,
242
+ reject:RCTPromiseRejectBlock) -> Void {
243
+ SessionReplay.shared.stopRecording()
244
+ resolve("shutdownSessionReplay success")
245
+ }
246
+
247
+ @objc(isSessionReplayInitialized:withRejecter:)
248
+ func isSessionReplayInitialized(resolve:@escaping RCTPromiseResolveBlock,
249
+ reject:@escaping RCTPromiseRejectBlock) -> Void {
250
+ let isInitialized = SessionReplay.shared.isInitialized()
251
+ resolve("\(String(describing: isInitialized))")
252
+ }
253
+
254
+ @objc(isRecording:withRejecter:)
255
+ func isRecording(resolve:@escaping RCTPromiseResolveBlock,
256
+ reject:@escaping RCTPromiseRejectBlock) -> Void {
257
+ let isRecording = SessionReplay.shared.isRecording()
258
+ resolve("\(String(describing: isRecording))")
259
+ }
260
+
261
+ @objc(startSessionRecording:withRejecter:)
262
+ func startSessionRecording(resolve:RCTPromiseResolveBlock,
263
+ reject:RCTPromiseRejectBlock) -> Void {
264
+ SessionReplay.shared.startRecording()
265
+ resolve("startSessionRecording success")
266
+ }
267
+
268
+ @objc(stopSessionRecording:withRejecter:)
269
+ func stopSessionRecording(resolve:RCTPromiseResolveBlock,
270
+ reject:RCTPromiseRejectBlock) -> Void {
271
+ SessionReplay.shared.stopRecording()
272
+ resolve("stopSessionRecording success")
273
+ }
274
+
275
+ @objc(captureScreenshot:withRejecter:)
276
+ func captureScreenshot(resolve:RCTPromiseResolveBlock,
277
+ reject:RCTPromiseRejectBlock) -> Void {
278
+ let result = SessionReplay.shared.captureEvent(properties: ["event": "screenshot"])
279
+ switch result {
280
+ case .failure(let error):
281
+ print("Error capturing screenshot: \(error)")
282
+ return
283
+ case .success:
284
+ break
285
+ }
286
+ resolve("captureScreenshot success")
287
+ }
288
+
289
+ @objc(maskViewByTag:withResolver:withRejecter:)
290
+ func maskViewByTag(viewTag: NSNumber,
291
+ resolve:@escaping RCTPromiseResolveBlock,
292
+ reject:@escaping RCTPromiseRejectBlock) -> Void {
293
+ guard
294
+ let bridge = RCTBridge.current(),
295
+ let uiManager = bridge.uiManager
296
+ else {
297
+ reject("MASK_VIEW_ERROR", "no ui manager found, aborting maskViewByTag", nil)
298
+ return
299
+ }
300
+
301
+ RCTExecuteOnUIManagerQueue {
302
+ uiManager.addUIBlock { (_, viewRegistry) in
303
+ if let view = viewRegistry?[viewTag] as? UIView {
304
+ DispatchQueue.main.async {
305
+ view.cxMask = true
306
+ resolve("view with tag \(viewTag) marked for masking")
307
+ }
308
+ } else {
309
+ reject("MASK_VIEW_ERROR", "view with tag \(viewTag) not found, aborting maskViewByTag", nil)
310
+ }
311
+ }
312
+ }
313
+ }
314
+
223
315
  @objc(sendCustomMeasurement:withResolver:withRejecter:)
224
316
  func sendCustomMeasurement(measurement: NSDictionary,
225
317
  resolve:RCTPromiseResolveBlock,
@@ -316,6 +408,42 @@ class CxSdk: RCTEventEmitter {
316
408
  return options
317
409
  }
318
410
 
411
+ private func toSessionReplayOptions(parameter: NSDictionary) throws -> SessionReplayOptions {
412
+ guard let captureScale = parameter["captureScale"] as? Double else {
413
+ debugPrint("CaptureScale Key Missing")
414
+ throw CxSdkError.invalidPublicKey
415
+ }
416
+
417
+ guard let captureCompressionQuality = parameter["captureCompressQuality"] as? Double else {
418
+ debugPrint("Capture Compress Quality Key Missing")
419
+ throw CxSdkError.invalidPublicKey
420
+ }
421
+
422
+ guard let sessionRecordingSampleRate = parameter["sessionRecordingSampleRate"] as? Double else {
423
+ debugPrint("Session Recording SampleRate Key Missing")
424
+ throw CxSdkError.invalidPublicKey
425
+ }
426
+
427
+ guard let autoStartSessionRecording = parameter["autoStartSessionRecording"] as? Bool else {
428
+ debugPrint("AutoStart Session Recording Key Missing")
429
+ throw CxSdkError.invalidPublicKey
430
+ }
431
+
432
+ let maskAllTexts = parameter["maskAllTexts"] as? Bool ?? true
433
+ let textsToMask = parameter["textsToMask"] as? [String] ?? []
434
+ let maskAllImages = parameter["maskAllImages"] as? Bool ?? false
435
+
436
+ let sessionReplayOptions = SessionReplayOptions(recordingType: .image,
437
+ captureScale: captureScale, // 2.0
438
+ captureCompressionQuality: captureCompressionQuality, // 0.8
439
+ sessionRecordingSampleRate: Int(sessionRecordingSampleRate),
440
+ maskText: maskAllTexts ? [".*"] : textsToMask,
441
+ maskOnlyCreditCards: false,
442
+ maskAllImages: maskAllImages,
443
+ autoStartSessionRecording: autoStartSessionRecording)
444
+ return sessionReplayOptions
445
+ }
446
+
319
447
  private func convertToJSCompatibleEvent(event: [String: Any]) -> [String: Any] {
320
448
  var jsEvent: [String: Any] = [:]
321
449
  for (key, value) in event {
@@ -362,7 +490,7 @@ class CxSdk: RCTEventEmitter {
362
490
  return nil
363
491
  }
364
492
  }
365
-
493
+
366
494
  private func instrumentationType(from string: String) -> CoralogixExporterOptions.InstrumentationType? {
367
495
  switch string {
368
496
  case "mobile_vitals":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.1.10",
3
+ "version": "0.2.1",
4
4
  "description": "Official Coralogix React Native plugin",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Coralogix",
package/src/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { CoralogixOtelWebType } from './model/CoralogixOtelWebType';
2
+ import { SessionReplayType } from './model/SessionReplayType';
2
3
  export declare const CoralogixRum: CoralogixOtelWebType;
4
+ export declare const SessionReplay: SessionReplayType;
3
5
  export { type ApplicationContextConfig } from './model/ApplicationContextConfig';
4
6
  export { type ViewContextConfig } from './model/ViewContextConfig';
5
7
  export { CoralogixDomain } from './model/CoralogixDomain';
@@ -0,0 +1,21 @@
1
+ import { LayoutChangeEvent } from 'react-native';
2
+ export interface SessionReplayType {
3
+ init: (options: SessionReplayOptions) => Promise<boolean>;
4
+ shutdown: () => Promise<boolean>;
5
+ isInited: () => Promise<boolean>;
6
+ isRecording: () => Promise<boolean>;
7
+ startSessionRecording: () => void;
8
+ stopSessionRecording: () => void;
9
+ captureScreenshot: () => void;
10
+ maskView: (event: LayoutChangeEvent) => void;
11
+ }
12
+ export type SessionReplayOptions = {
13
+ captureScale: number;
14
+ captureCompressQuality: number;
15
+ sessionRecordingSampleRate: number;
16
+ autoStartSessionRecording: boolean;
17
+ maskAllTexts?: boolean;
18
+ textsToMask?: Array<string>;
19
+ maskAllImages?: boolean;
20
+ };
21
+ export declare function isSessionReplayOptionsValid(options: SessionReplayOptions): boolean;