@coralogix/react-native-plugin 0.10.2 → 0.11.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 +33 -0
- package/CxSdk.podspec +3 -3
- package/README.md +42 -7
- package/android/build.gradle +4 -1
- package/android/src/main/java/com/cxsdk/CxSdkModule.kt +133 -4
- package/android/src/main/java/com/cxsdk/SessionReplayClient.kt +1 -1
- package/index.cjs.js +1081 -275
- package/index.esm.js +1082 -277
- package/ios/CxSdk.swift +98 -23
- package/package.json +1 -1
- package/src/index.d.ts +11 -4
- package/src/instrumentations/user-interaction/UserInteractionInstrumentation.d.ts +10 -0
- package/src/instrumentations/user-interaction/consts.d.ts +1 -0
- package/src/instrumentations/user-interaction/gestureUtils.d.ts +7 -1
- package/src/instrumentations/user-interaction/index.d.ts +1 -1
- package/src/logger.d.ts +8 -0
- package/src/masking/Masked.d.ts +39 -0
- package/src/masking/maskView.d.ts +9 -0
- package/src/masking/maskedContext.d.ts +90 -0
- package/src/model/Types.d.ts +109 -0
- package/src/nativeModule.d.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## 0.11.1 (2026-09-14)
|
|
2
|
+
|
|
3
|
+
### ⬆️ Dependency Updates
|
|
4
|
+
|
|
5
|
+
- Native iOS SDK bumped to **2.18.2** — crashes are reported with the crash instead of on the next launch, session replay captures one tap frame at finger-down, and interaction masking now reflects `Masked` / `SessionReplay.maskView` only, matching Android
|
|
6
|
+
|
|
7
|
+
## 0.11.0 (2026-08-31)
|
|
8
|
+
|
|
9
|
+
### 🚀 Features
|
|
10
|
+
|
|
11
|
+
- New `Masked` component masks its subtree in session replay and in the interaction metadata of every control inside it
|
|
12
|
+
- Masking now applies to user-interaction events: a tap anywhere in a masked control reports its text as `***`, with both `Masked` and `SessionReplay.maskView`
|
|
13
|
+
- Masked interactions report `interaction_context.is_masked_element`, readable in `beforeSend`
|
|
14
|
+
- Scroll and swipe inside a masked container now report as masked
|
|
15
|
+
- Taps on a `Text` with an `onPress` handler are now reported as user interactions
|
|
16
|
+
|
|
17
|
+
### 🩹 Fixes
|
|
18
|
+
|
|
19
|
+
- Controls already on screen when the SDK finishes initializing now report their interactions
|
|
20
|
+
- Masking now warns on the console when a mask cannot be registered, instead of failing silently
|
|
21
|
+
- Interaction reporting can no longer break a host app's own `onPress` handler
|
|
22
|
+
- An interaction that carries no coordinates now omits them, instead of reporting them as `NaN`
|
|
23
|
+
- A `<Text>` with no `onPress` renders untouched, keeping the instrumentation off the most numerous component in a typical app
|
|
24
|
+
- Masking registers each view once instead of on every re-measure, so a long list of masked rows no longer repeats the native call per row per layout pass
|
|
25
|
+
- **Behaviour change:** a `beforeSend` callback that deletes `event.interaction_context`, or replaces it with anything that is not an object, no longer gets the SDK's original put back in its place — only the read-only `is_masked_element` flag is kept, on an otherwise empty object. Previously the coordinates and the reported text were restored, which was the opposite of what the callback asked for. If your callback replaced the container and relied on the SDK restoring it, edit the object in place instead — blank the fields you do not want (`interaction_context.target_element_inner_text = '***'`) or delete just those keys, and everything you leave alone is uploaded as it was
|
|
26
|
+
|
|
27
|
+
## 0.10.3 (2026-08-30)
|
|
28
|
+
|
|
29
|
+
### 🩹 Fixes
|
|
30
|
+
|
|
31
|
+
- Initialization is now safe to run more than once — no duplicate sessions, duplicate events, or crashes
|
|
32
|
+
|
|
33
|
+
|
|
1
34
|
## 0.10.2 (2026-08-25)
|
|
2
35
|
|
|
3
36
|
### 🩹 Fixes
|
package/CxSdk.podspec
CHANGED
|
@@ -16,9 +16,9 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
|
|
17
17
|
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
18
18
|
|
|
19
|
-
s.dependency 'Coralogix','2.
|
|
20
|
-
s.dependency 'CoralogixInternal','2.
|
|
21
|
-
s.dependency 'SessionReplay','2.
|
|
19
|
+
s.dependency 'Coralogix','2.18.2'
|
|
20
|
+
s.dependency 'CoralogixInternal','2.18.2'
|
|
21
|
+
s.dependency 'SessionReplay','2.18.2'
|
|
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
|
@@ -222,6 +222,10 @@ await CoralogixRum.init({
|
|
|
222
222
|
});
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
+
> [!NOTE]
|
|
226
|
+
> The network instrumentation also injects the W3C `traceparent` header. Setting `network` to `false`
|
|
227
|
+
> stops trace context propagation as well, even when `traceParentInHeader` is enabled.
|
|
228
|
+
|
|
225
229
|
### Mobile Vital Detectors
|
|
226
230
|
|
|
227
231
|
Disable specific mobile vitals detection and collection
|
|
@@ -291,6 +295,11 @@ await CoralogixRum.init({
|
|
|
291
295
|
});
|
|
292
296
|
```
|
|
293
297
|
|
|
298
|
+
> [!NOTE]
|
|
299
|
+
> The `traceparent` header is added by the network instrumentation, so this feature depends on it. If
|
|
300
|
+
> you set `instrumentations.network` to `false`, no trace headers are sent even with
|
|
301
|
+
> `enabled: true`.
|
|
302
|
+
|
|
294
303
|
<!-- /split -->
|
|
295
304
|
<!-- split title="Custom instrumentation" path="features/custom-instrumentation.md" -->
|
|
296
305
|
### Custom Spans
|
|
@@ -618,22 +627,49 @@ Shutdown Session Replay to clean up resources:
|
|
|
618
627
|
await SessionReplay.shutdown();
|
|
619
628
|
```
|
|
620
629
|
|
|
621
|
-
|
|
630
|
+
<!-- /split -->
|
|
631
|
+
<!-- split title="Masking sensitive content" path="features/masking.md" -->
|
|
632
|
+
### Masking Sensitive Content
|
|
622
633
|
|
|
623
|
-
|
|
634
|
+
Masking is not gated on Session Replay. It redacts sensitive content from user-interaction events whether or not you record replays, and when Session Replay is enabled the same mask also hides that content in the recording.
|
|
624
635
|
|
|
625
|
-
|
|
636
|
+
There are two ways to mask, and **`Masked` is recommended**. Mask the whole logical element — the field, the row, the button — rather than an inner fragment of it.
|
|
637
|
+
|
|
638
|
+
#### `Masked` (recommended)
|
|
639
|
+
|
|
640
|
+
`Masked` is exported from the plugin itself and needs no Session Replay setup. Wrap anything sensitive in it — it renders a `View` and accepts the `View` props, so it can wrap a whole control or just the sensitive part of one:
|
|
641
|
+
|
|
642
|
+
```jsx
|
|
643
|
+
import { Text } from 'react-native';
|
|
644
|
+
import { Masked } from '@coralogix/react-native-plugin';
|
|
645
|
+
|
|
646
|
+
<Masked>
|
|
647
|
+
<Text>Card: {cardNumber}</Text>
|
|
648
|
+
</Masked>
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Do not put `Masked` inside a `<Text>` — wrap the outermost `<Text>` instead.
|
|
652
|
+
|
|
653
|
+
#### `SessionReplay.maskView`
|
|
654
|
+
|
|
655
|
+
Alternatively, pass `SessionReplay.maskView` to the `onLayout` prop of any view. It accepts a `LayoutChangeEvent` and works on both the legacy and the New Architecture (Fabric):
|
|
656
|
+
|
|
657
|
+
```jsx
|
|
658
|
+
import { Text, View } from 'react-native';
|
|
626
659
|
import { SessionReplay } from '@coralogix/react-native-plugin';
|
|
627
660
|
|
|
628
661
|
<View onLayout={SessionReplay.maskView}>
|
|
629
|
-
<Text>
|
|
630
|
-
<TextInput placeholder="Password" />
|
|
662
|
+
<Text>{cardNumber}</Text>
|
|
631
663
|
</View>
|
|
632
664
|
```
|
|
633
665
|
|
|
634
|
-
|
|
666
|
+
Attach it to a host component — a custom component that accepts `onLayout` but never forwards it to a real view is silently not masked. Use `Masked` for a `Switch`; `maskView` cannot flag its interactions.
|
|
667
|
+
|
|
668
|
+
#### Masked interactions
|
|
635
669
|
|
|
670
|
+
A tap on masked content is still reported, with `interaction_context.target_element_inner_text` redacted to `***` and `interaction_context.is_masked_element: true`. Identity fields and tap coordinates are reported as-is, and a replay recording shows no tap marker. `is_masked_element` is read-only, so use it in `beforeSend` to drop or further redact masked interactions.
|
|
636
671
|
|
|
672
|
+
Note that `target_element` falls back to `accessibilityLabel`, which is not redacted — give masked keypad keys a shared label (`accessibilityLabel="pin-key"`) rather than the digit they enter.
|
|
637
673
|
<!-- /split -->
|
|
638
674
|
### Optional - Coralogix Gradle Plugin (Android)
|
|
639
675
|
|
|
@@ -688,4 +724,3 @@ However, the plugin is the only way to capture network activity from third-party
|
|
|
688
724
|
1. `npm install react-native-url-polyfill`
|
|
689
725
|
2. At the top of your entry-point file (index.js) add:
|
|
690
726
|
`import "react-native-url-polyfill/auto"`
|
|
691
|
-
---
|
package/android/build.gradle
CHANGED
|
@@ -84,7 +84,10 @@ dependencies {
|
|
|
84
84
|
implementation "com.facebook.react:react-android"
|
|
85
85
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
// 2.22.0 is the first release exposing View.isCoralogixMasked() and
|
|
88
|
+
// View.isInsideCoralogixMaskedSubtree(), which the masked-interaction walk in
|
|
89
|
+
// reportUserInteraction reads instead of reimplementing the SDK's mask rule here.
|
|
90
|
+
implementation "com.coralogix:android-sdk:2.22.0"
|
|
88
91
|
|
|
89
92
|
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
|
90
93
|
testImplementation "org.mockito:mockito-core:5.12.0"
|
|
@@ -4,6 +4,8 @@ import android.app.Application
|
|
|
4
4
|
import android.os.Handler
|
|
5
5
|
import android.os.Looper
|
|
6
6
|
import android.util.Log
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.view.ViewGroup
|
|
7
9
|
import com.coralogix.android.sdk.CoralogixRum
|
|
8
10
|
import com.coralogix.android.sdk.customspans.CoralogixCustomSpan
|
|
9
11
|
import com.coralogix.android.sdk.customspans.CoralogixGlobalSpan
|
|
@@ -29,6 +31,8 @@ import com.coralogix.android.sdk.model.UserContext
|
|
|
29
31
|
import com.coralogix.android.sdk.model.ViewContext
|
|
30
32
|
import com.coralogix.android.sdk.session_replay.SessionReplay
|
|
31
33
|
import com.coralogix.android.sdk.session_replay.SessionReplay.maskView
|
|
34
|
+
import com.coralogix.android.sdk.session_replay.isCoralogixMasked
|
|
35
|
+
import com.coralogix.android.sdk.session_replay.isInsideCoralogixMaskedSubtree
|
|
32
36
|
import com.coralogix.android.sdk.session_replay.model.SessionReplayOptions
|
|
33
37
|
import com.facebook.react.bridge.Arguments
|
|
34
38
|
import com.facebook.react.bridge.Promise
|
|
@@ -265,8 +269,114 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
|
|
|
265
269
|
innerText = if (interaction.hasKey("inner_text") && !interaction.isNull("inner_text")) interaction.getString("inner_text") else null,
|
|
266
270
|
x = if (attributesMap?.hasKey("x") == true && attributesMap.isNull("x") == false) attributesMap.getDouble("x") else null,
|
|
267
271
|
y = if (attributesMap?.hasKey("y") == true && attributesMap.isNull("y") == false) attributesMap.getDouble("y") else null,
|
|
272
|
+
// Present only when JS resolved masking from the component tree. The SDK treats a
|
|
273
|
+
// non-null value as authoritative and otherwise resolves the coordinates itself,
|
|
274
|
+
// which is why JS omits the key rather than sending false.
|
|
275
|
+
isMasked = if (interaction.hasKey("is_masked") && !interaction.isNull("is_masked")) interaction.getBoolean("is_masked") else null,
|
|
268
276
|
)
|
|
269
|
-
|
|
277
|
+
|
|
278
|
+
// The tapped control's view tag, sent only when JS could not answer for
|
|
279
|
+
// itself. It is what lets the walk below see an imperative maskView: that
|
|
280
|
+
// marks a native view and leaves nothing in the component tree, so JS is
|
|
281
|
+
// blind to it, while the SDK's own coordinate test cannot tell where the
|
|
282
|
+
// control begins and ends.
|
|
283
|
+
val viewTag = if (interaction.hasKey("view_tag") && !interaction.isNull("view_tag"))
|
|
284
|
+
interaction.getInt("view_tag") else null
|
|
285
|
+
|
|
286
|
+
// JS sends one or the other, never both — a verdict it already has needs no
|
|
287
|
+
// native walk. Checked rather than assumed, because the walk is the expensive
|
|
288
|
+
// half and an authoritative isMasked would make it wasted work.
|
|
289
|
+
if (viewTag == null || details.isMasked != null) {
|
|
290
|
+
CoralogixRum.reportUserInteraction(details)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Resolving a view and reading its mask tags both require the UI thread, so
|
|
295
|
+
// reporting becomes asynchronous on this path. This method otherwise runs on
|
|
296
|
+
// the native-modules thread and reports inline, which means an interaction
|
|
297
|
+
// carrying a tag can be reported after one that arrived later without a tag.
|
|
298
|
+
// Interaction spans carry their own timestamps, so ordering is presentational
|
|
299
|
+
// rather than semantic.
|
|
300
|
+
UiThreadUtil.runOnUiThread {
|
|
301
|
+
// Set only when the walk finds a mask; never set to false. A non-null
|
|
302
|
+
// value is authoritative in the SDK, so a false here would suppress its
|
|
303
|
+
// coordinate resolution and lose masks the walk cannot see — a masked
|
|
304
|
+
// overlay that is neither ancestor nor descendant of the control. The SDK
|
|
305
|
+
// redacts the reported inner text off this flag, so nothing else is needed.
|
|
306
|
+
//
|
|
307
|
+
// Descendants count for a click only. A click reports the control's whole
|
|
308
|
+
// concatenated text, so a masked descendant has to mask the click to keep
|
|
309
|
+
// the verdict and the text consistent. A scroll reports no text at all, so
|
|
310
|
+
// the descendant half would buy nothing there and cost plenty: one masked
|
|
311
|
+
// row anywhere in a list would report every scroll of that list as masked.
|
|
312
|
+
val masked = isControlMasked(viewTag, includeDescendants = details.type == "click")
|
|
313
|
+
val resolved = if (masked) details.copy(isMasked = true) else details
|
|
314
|
+
|
|
315
|
+
// Straight back off the UI thread. Only the walk above needs it, while
|
|
316
|
+
// building and handing over the span does not — and every unmasked click
|
|
317
|
+
// and every scroll carries a tag, so leaving the reporting here would move
|
|
318
|
+
// the whole interaction path onto the main thread for the majority case
|
|
319
|
+
// that has nothing to resolve.
|
|
320
|
+
reactApplicationContext.runOnNativeModulesQueueThread {
|
|
321
|
+
CoralogixRum.reportUserInteraction(resolved)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Whether anything in the tapped control is masked: the view itself, an
|
|
328
|
+
* ancestor, or — when [includeDescendants] is set — anything below it.
|
|
329
|
+
*
|
|
330
|
+
* Ancestors always count, because masking is inherited: a control inside a
|
|
331
|
+
* masked panel is masked. Descendants count for a click, because React Native
|
|
332
|
+
* renders one logical control as several native views, so
|
|
333
|
+
* `<TouchableOpacity><Text/></TouchableOpacity>` puts the mask on a child and
|
|
334
|
+
* leaves the tapped container itself unmarked.
|
|
335
|
+
*
|
|
336
|
+
* An unresolvable tag answers false, which omits the flag and leaves the SDK's
|
|
337
|
+
* coordinate resolution as the backstop — the right outcome for the race this
|
|
338
|
+
* is most exposed to, a control unmounted between the press and this walk.
|
|
339
|
+
*/
|
|
340
|
+
private fun isControlMasked(viewTag: Int, includeDescendants: Boolean): Boolean = try {
|
|
341
|
+
val view = UIManagerHelper.getUIManagerForReactTag(reactApplicationContext, viewTag)
|
|
342
|
+
?.resolveView(viewTag)
|
|
343
|
+
// Self and ancestors through the SDK's own accessor, so the definition of
|
|
344
|
+
// masked lives in one place.
|
|
345
|
+
view != null &&
|
|
346
|
+
(view.isInsideCoralogixMaskedSubtree() ||
|
|
347
|
+
(includeDescendants && containsMaskedDescendant(view)))
|
|
348
|
+
} catch (t: Throwable) {
|
|
349
|
+
Log.d("CxSdkModule", "reportUserInteraction: could not resolve view $viewTag for masking", t)
|
|
350
|
+
false
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Breadth-first descendant search for a mask mark, iterative because recursion
|
|
355
|
+
* on an app-controlled depth is a stack overflow waiting to happen.
|
|
356
|
+
*
|
|
357
|
+
* Bounded by [MASK_WALK_NODE_BUDGET]: this runs on the UI thread for every
|
|
358
|
+
* interaction, so a deep subtree — a long list row, a heavily nested
|
|
359
|
+
* design-system component — must not be able to cause jank. Exhausting the
|
|
360
|
+
* budget answers false, leaving the SDK's coordinate resolution as the backstop
|
|
361
|
+
* rather than claiming an answer the walk never reached.
|
|
362
|
+
*/
|
|
363
|
+
private fun containsMaskedDescendant(root: View): Boolean {
|
|
364
|
+
val queue = ArrayDeque<View>()
|
|
365
|
+
root.enqueueChildren(queue)
|
|
366
|
+
|
|
367
|
+
var visited = 0
|
|
368
|
+
while (queue.isNotEmpty()) {
|
|
369
|
+
if (visited++ >= MASK_WALK_NODE_BUDGET) return false
|
|
370
|
+
val current = queue.removeFirst()
|
|
371
|
+
if (current.isCoralogixMasked()) return true
|
|
372
|
+
current.enqueueChildren(queue)
|
|
373
|
+
}
|
|
374
|
+
return false
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private fun View.enqueueChildren(queue: ArrayDeque<View>) {
|
|
378
|
+
val group = this as? ViewGroup ?: return
|
|
379
|
+
for (i in 0 until group.childCount) queue.addLast(group.getChildAt(i))
|
|
270
380
|
}
|
|
271
381
|
|
|
272
382
|
@ReactMethod
|
|
@@ -327,7 +437,7 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
|
|
|
327
437
|
}
|
|
328
438
|
|
|
329
439
|
@ReactMethod
|
|
330
|
-
override fun maskViewByTag(viewTag: Int) {
|
|
440
|
+
override fun maskViewByTag(viewTag: Int, promise: Promise) {
|
|
331
441
|
// Resolve the view through the architecture-agnostic UIManagerHelper so this
|
|
332
442
|
// works on both the legacy (Paper) and New (Fabric) architectures.
|
|
333
443
|
// getNativeModule(UIManagerModule) only exists on Paper; under Fabric it
|
|
@@ -337,12 +447,23 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
|
|
|
337
447
|
val context = reactApplicationContext
|
|
338
448
|
// FabricUIManager.resolveView must be called on the UI thread.
|
|
339
449
|
UiThreadUtil.runOnUiThread {
|
|
450
|
+
// Rejections are the only signal the JS side has: masking that does not
|
|
451
|
+
// apply is invisible in the app, so an unresolvable tag has to travel back
|
|
452
|
+
// rather than be swallowed here.
|
|
340
453
|
try {
|
|
341
454
|
val uiManager = UIManagerHelper.getUIManagerForReactTag(context, viewTag)
|
|
342
455
|
val view = uiManager?.resolveView(viewTag)
|
|
343
|
-
view
|
|
456
|
+
if (view == null) {
|
|
457
|
+
promise.reject(
|
|
458
|
+
MASK_VIEW_ERROR,
|
|
459
|
+
"view with tag $viewTag could not be resolved, aborting maskViewByTag"
|
|
460
|
+
)
|
|
461
|
+
return@runOnUiThread
|
|
462
|
+
}
|
|
463
|
+
view.maskView()
|
|
464
|
+
promise.resolve("view with tag $viewTag marked for masking")
|
|
344
465
|
} catch (t: Throwable) {
|
|
345
|
-
|
|
466
|
+
promise.reject(MASK_VIEW_ERROR, t)
|
|
346
467
|
}
|
|
347
468
|
}
|
|
348
469
|
}
|
|
@@ -950,6 +1071,14 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
|
|
|
950
1071
|
companion object {
|
|
951
1072
|
const val NAME = "CxSdk"
|
|
952
1073
|
|
|
1074
|
+
// Matches the iOS bridge's rejection code for the same failure.
|
|
1075
|
+
private const val MASK_VIEW_ERROR = "MASK_VIEW_ERROR"
|
|
1076
|
+
|
|
1077
|
+
// Node budget for the descendant half of the masked-control walk. Kept in
|
|
1078
|
+
// step with the iOS bridge's maskWalkNodeBudget so the two platforms give up
|
|
1079
|
+
// at the same tree size.
|
|
1080
|
+
private const val MASK_WALK_NODE_BUDGET = 512
|
|
1081
|
+
|
|
953
1082
|
// TODO: replace reflection with a public accessor once the Coralogix Android SDK exposes one —
|
|
954
1083
|
// 'getSpan$library_release' is a Kotlin-internal name that may be renamed in a future SDK version.
|
|
955
1084
|
internal fun extractOtelIds(child: CoralogixCustomSpan): Pair<String, String>? =
|