@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 +31 -1
- package/CxSdk.podspec +1 -1
- package/README.md +102 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/cxsdk/CxSdkModule.kt +93 -1
- package/android/src/main/java/com/cxsdk/{ICxSdkModule.kt → RUMClient.kt} +1 -1
- package/android/src/main/java/com/cxsdk/SessionReplayClient.kt +15 -0
- package/index.cjs.js +51 -1
- package/index.esm.js +52 -3
- package/ios/CxSdk.mm +30 -0
- package/ios/CxSdk.swift +145 -7
- package/package.json +1 -1
- package/src/index.d.ts +2 -0
- package/src/model/SessionReplayType.d.ts +21 -0
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
|
|
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 request
|
|
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.
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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),
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
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;
|