@coralogix/react-native-plugin 0.1.9 → 0.2.0

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,33 @@
1
+ ## 0.2.0 (2025-11-11)
2
+
3
+ ### 🚀 Features
4
+
5
+ - add session replay Documents
6
+ - added manual masking functionality
7
+ - added missing session replay options
8
+ - added basic session replay bridge implementation for react native
9
+
10
+ ### 🩹 Fixes
11
+
12
+ - pr changes
13
+ - update readme.md
14
+ - changed implementation for masking specific elements to a hook on onLayout to not influence the app's layout with an extra layout node
15
+ - iOS masking view + version bump 1.4.0
16
+ - mask all texts default value
17
+ - moved android impl for shutdown to a handler to run on main
18
+
19
+ ## 0.1.10 (2025-11-11)
20
+
21
+ ### 🩹 Fixes
22
+
23
+ Fix iOS not sending custom measurement
24
+
25
+ ## 0.1.9 (2025-11-10)
26
+
27
+ ### 🩹 Patch
28
+
29
+ - Bump iOS native version to 1.4.0
30
+
1
31
  ## 0.1.8 (2025-11-04)
2
32
 
3
33
  ### 🩹 Fixes
@@ -8,7 +38,7 @@
8
38
 
9
39
  ### 🚀 Features
10
40
 
11
- - added automatic navigation detection using the @react-navigation/native package
41
+ - added automatic navigation detection using the react-navigation/native package
12
42
 
13
43
  ### Patch
14
44
  - Bump android native version to 2.5.6
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.5.7"
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
@@ -197,7 +202,70 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
197
202
  fun removeListeners(count: Int) {
198
203
  Log.d("CxSdkModule", "removeListeners: $count")
199
204
  }
205
+ // endregion
200
206
 
207
+ // region - Session Replay methods
208
+ @ReactMethod
209
+ override fun initializeSessionReplay(options: ReadableMap, promise: Promise) {
210
+ val sessionReplayOptions = options.toSessionReplayOptions()
211
+
212
+ Handler(Looper.getMainLooper()).post {
213
+ SessionReplay.initialize(reactApplicationContext, sessionReplayOptions)
214
+ promise.resolve(true)
215
+ }
216
+ }
217
+
218
+ @ReactMethod
219
+ override fun shutdownSessionReplay(promise: Promise) {
220
+ Handler(Looper.getMainLooper()).post {
221
+ SessionReplay.shutdown()
222
+ promise.resolve(true)
223
+ }
224
+ }
225
+
226
+ @ReactMethod
227
+ override fun isSessionReplayInitialized(promise: Promise) {
228
+ val isInitialized = SessionReplay.isInitialized()
229
+ promise.resolve(isInitialized)
230
+ }
231
+
232
+ @ReactMethod
233
+ override fun isRecording(promise: Promise) {
234
+ val isRecording = SessionReplay.isRecording()
235
+ promise.resolve(isRecording)
236
+ }
237
+
238
+ @ReactMethod
239
+ override fun startSessionRecording() {
240
+ SessionReplay.startSessionRecording()
241
+ }
242
+
243
+ @ReactMethod
244
+ override fun stopSessionRecording() {
245
+ SessionReplay.stopSessionRecording()
246
+ }
247
+
248
+ @ReactMethod
249
+ override fun captureScreenshot() {
250
+ SessionReplay.captureScreenshot()
251
+ }
252
+
253
+ @ReactMethod
254
+ override fun maskViewByTag(viewTag: Int) {
255
+ val uiManager = reactApplicationContext.getNativeModule(UIManagerModule::class.java)
256
+
257
+ uiManager?.addUIBlock { nativeViewHierarchyManager ->
258
+ try {
259
+ val view = nativeViewHierarchyManager.resolveView(viewTag)
260
+ view?.maskView()
261
+ } catch (t: Throwable) {
262
+ t.printStackTrace()
263
+ }
264
+ }
265
+ }
266
+ // endregion
267
+
268
+ // region - utils
201
269
  override fun invalidate() {
202
270
  super.invalidate()
203
271
 
@@ -207,6 +275,29 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
207
275
  }
208
276
  }
209
277
 
278
+ private fun ReadableMap.toSessionReplayOptions(): SessionReplayOptions {
279
+ val scale = if (hasKey("captureScale")) getDouble("captureScale") else 0.5
280
+ val quality = if (hasKey("captureCompressQuality")) getDouble("captureCompressQuality") else 1
281
+ val sampleRate = if (hasKey("sessionRecordingSampleRate")) getInt("sessionRecordingSampleRate") else 100
282
+ val autoStart = if (hasKey("autoStartSessionRecording")) getBoolean("autoStartSessionRecording") else true
283
+ val maskAllTexts = if (hasKey("maskAllTexts")) getBoolean("maskAllTexts") else true
284
+ val maskAllImages = if (hasKey("maskAllImages")) getBoolean("maskAllImages") else false
285
+
286
+ val textsToMask = if (hasKey("textsToMask") && !isNull("textsToMask")) {
287
+ getArray("textsToMask")?.handleStringOrRegexList() ?: emptyList()
288
+ } else emptyList()
289
+
290
+ return SessionReplayOptions(
291
+ captureScale = scale.toFloat(),
292
+ captureCompressQuality = quality.toFloat(),
293
+ sessionRecordingSampleRate = sampleRate,
294
+ autoStartSessionRecording = autoStart,
295
+ maskAllTexts = maskAllTexts,
296
+ textsToMask = textsToMask,
297
+ maskAllImages = maskAllImages
298
+ )
299
+ }
300
+
210
301
  private fun ReadableMap.toCoralogixOptions(): CoralogixOptions {
211
302
  val applicationName = getString("application")
212
303
  ?: throw IllegalArgumentException("Missing required parameter: application")
@@ -550,6 +641,7 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
550
641
  }
551
642
  return out
552
643
  }
644
+ // endregion
553
645
 
554
646
  companion object {
555
647
  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.9";
222
+ var version = "0.2.0";
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.9";
220
+ var version = "0.2.0";
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
@@ -74,6 +74,36 @@ RCT_EXTERN_METHOD(reportMobileVitalsMeasurement:(NSString *)type
74
74
  withResolver:(RCTPromiseResolveBlock)resolve
75
75
  withRejecter:(RCTPromiseRejectBlock)reject)
76
76
 
77
+ RCT_EXTERN_METHOD(sendCustomMeasurement:(NSDictionary *)measurement
78
+ withResolver:(RCTPromiseResolveBlock)resolve
79
+ withRejecter:(RCTPromiseRejectBlock)reject)
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
+
77
107
  + (BOOL)requiresMainQueueSetup
78
108
  {
79
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,113 @@ 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
+
315
+ @objc(sendCustomMeasurement:withResolver:withRejecter:)
316
+ func sendCustomMeasurement(measurement: NSDictionary,
317
+ resolve:RCTPromiseResolveBlock,
318
+ reject:RCTPromiseRejectBlock) -> Void {
319
+ let name = measurement["name"] as? String ?? ""
320
+ let value = measurement["value"] as? Double ?? 0.0
321
+ coralogixRum?.sendCustomMeasurement(name: name, value: value)
322
+ resolve("sendCustomMeasurement success")
323
+ }
324
+
223
325
  private func toHybridMetricList(metrics: NSArray) -> [HybridMetric] {
224
326
  let array = metrics as? [[String: Any]] ?? []
225
327
 
@@ -306,6 +408,42 @@ class CxSdk: RCTEventEmitter {
306
408
  return options
307
409
  }
308
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
+
309
447
  private func convertToJSCompatibleEvent(event: [String: Any]) -> [String: Any] {
310
448
  var jsEvent: [String: Any] = [:]
311
449
  for (key, value) in event {
@@ -352,7 +490,7 @@ class CxSdk: RCTEventEmitter {
352
490
  return nil
353
491
  }
354
492
  }
355
-
493
+
356
494
  private func instrumentationType(from string: String) -> CoralogixExporterOptions.InstrumentationType? {
357
495
  switch string {
358
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.9",
3
+ "version": "0.2.0",
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;