@coralogix/react-native-plugin 0.10.1 → 0.11.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 +35 -0
- package/README.md +69 -15
- 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 +1135 -262
- package/index.esm.js +1136 -264
- 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 +120 -1
- package/src/nativeModule.d.ts +2 -0
- package/src/utils.d.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
## 0.11.0 (2026-08-31)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- New `Masked` component masks its subtree in session replay and in the interaction metadata of every control inside it
|
|
6
|
+
- Masking now applies to user-interaction events: a tap anywhere in a masked control reports its text as `***`, with both `Masked` and `SessionReplay.maskView`
|
|
7
|
+
- Masked interactions report `interaction_context.is_masked_element`, readable in `beforeSend`
|
|
8
|
+
- Scroll and swipe inside a masked container now report as masked
|
|
9
|
+
- Taps on a `Text` with an `onPress` handler are now reported as user interactions
|
|
10
|
+
|
|
11
|
+
### 🩹 Fixes
|
|
12
|
+
|
|
13
|
+
- Controls already on screen when the SDK finishes initializing now report their interactions
|
|
14
|
+
- Masking now warns on the console when a mask cannot be registered, instead of failing silently
|
|
15
|
+
- Interaction reporting can no longer break a host app's own `onPress` handler
|
|
16
|
+
- An interaction that carries no coordinates now omits them, instead of reporting them as `NaN`
|
|
17
|
+
- A `<Text>` with no `onPress` renders untouched, keeping the instrumentation off the most numerous component in a typical app
|
|
18
|
+
- 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
|
|
19
|
+
- **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
|
|
20
|
+
|
|
21
|
+
## 0.10.3 (2026-08-30)
|
|
22
|
+
|
|
23
|
+
### 🩹 Fixes
|
|
24
|
+
|
|
25
|
+
- Initialization is now safe to run more than once — no duplicate sessions, duplicate events, or crashes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## 0.10.2 (2026-08-25)
|
|
29
|
+
|
|
30
|
+
### 🩹 Fixes
|
|
31
|
+
|
|
32
|
+
- `sessionSampleRate` is now applied once instead of twice — the configured percentage was being rolled on both the JavaScript and native sides, so a rate of 1% collected roughly 0.01% of sessions
|
|
33
|
+
- A fractional `sessionSampleRate` such as `0.5` is now rounded to a whole percentage instead of being truncated to `0` on iOS, which silently sampled out every session
|
|
34
|
+
- `SessionReplay.init()` now resolves `false` instead of rejecting when the session was sampled out, and resolves a real boolean on both platforms
|
|
35
|
+
|
|
1
36
|
## 0.10.1 (2026-08-11)
|
|
2
37
|
|
|
3
38
|
### 🩹 Fixes
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ This package replaces the old `@coralogix/react-native-sdk` package.
|
|
|
12
12
|
|
|
13
13
|
To use Coralogix SDK, call `CoralogixRum.init(options)` at the soonest available moment after the app loads.
|
|
14
14
|
This will initialize the SDK based on the options you provided.
|
|
15
|
-
|
|
15
|
+
`init` is an `async` function.
|
|
16
16
|
|
|
17
17
|
```javascript
|
|
18
18
|
import { CoralogixRum } from '@coralogix/react-native-plugin'
|
|
@@ -32,6 +32,11 @@ await CoralogixRum.init({
|
|
|
32
32
|
});
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
`sessionSampleRate` is a whole percentage in `0`-`100` and is applied **once**. A
|
|
36
|
+
fractional value is rounded to an integer before it is forwarded (and a non-zero
|
|
37
|
+
rate is never rounded down to `0`), because the native bridges read the value as
|
|
38
|
+
an integer.
|
|
39
|
+
|
|
35
40
|
To provide contextual information or transmit manual logs, utilize the exported functions of `CoralogixRum`.
|
|
36
41
|
Keep in mind that these functions will remain inactive until you've invoked `CoralogixRum.init()`.
|
|
37
42
|
|
|
@@ -67,6 +72,7 @@ CoralogixRum.log(CoralogixLogSeverity.Error, 'this is a log', { key: 'value' });
|
|
|
67
72
|
CoralogixRum.error('this is a log with error severity', { key: 'value' });
|
|
68
73
|
```
|
|
69
74
|
|
|
75
|
+
<!-- split title="Logs, errors, and views" path="features/logs-errors-and-views.md" -->
|
|
70
76
|
### Custom Logs
|
|
71
77
|
|
|
72
78
|
Send structured logs with optional data and labels.
|
|
@@ -80,9 +86,10 @@ CoralogixRum.debug('this is a debug log', {key: 'value', pi: 3.14});
|
|
|
80
86
|
CoralogixRum.error('this is an error log', {error: 'yes', is_bad: 'no'});
|
|
81
87
|
```
|
|
82
88
|
|
|
83
|
-
>
|
|
84
|
-
>
|
|
85
|
-
>
|
|
89
|
+
> [!NOTE]
|
|
90
|
+
> Due to React Native limitations, not all value types are supported for the log data values.
|
|
91
|
+
> Our testing shows that React Native can't pass some types to the coralogix native layer; the ones we have identified as problematic are `Map`, `Set`, `Date` and `Function`.
|
|
92
|
+
> There could be more. For example:
|
|
86
93
|
> ```javascript
|
|
87
94
|
> // this would pass as an empty object to the coralogix native layer, same for Set and Date:
|
|
88
95
|
> CoralogixRum.debug('this will be a problem', {key: new Map().set(1, 2)});
|
|
@@ -90,7 +97,7 @@ CoralogixRum.error('this is an error log', {error: 'yes', is_bad: 'no'});
|
|
|
90
97
|
> // this would pass as null to the coralogix native layer
|
|
91
98
|
> CoralogixRum.debug('this will be a problem too', {key: () => {}});
|
|
92
99
|
> ```
|
|
93
|
-
>
|
|
100
|
+
> If you need to pass any of these types, stringify them before you pass them to the `log` method.
|
|
94
101
|
|
|
95
102
|
### Error Reporting
|
|
96
103
|
|
|
@@ -193,6 +200,8 @@ subsequent events belong to a fresh session with a new session id.
|
|
|
193
200
|
CoralogixRum.createNewSession();
|
|
194
201
|
```
|
|
195
202
|
|
|
203
|
+
<!-- /split -->
|
|
204
|
+
<!-- split title="Configuration options" path="configuration/options.md" -->
|
|
196
205
|
### Instrumentation's
|
|
197
206
|
|
|
198
207
|
Turn on/off specific instrumentation; defaults to true for all.
|
|
@@ -213,6 +222,10 @@ await CoralogixRum.init({
|
|
|
213
222
|
});
|
|
214
223
|
```
|
|
215
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
|
+
|
|
216
229
|
### Mobile Vital Detectors
|
|
217
230
|
|
|
218
231
|
Disable specific mobile vitals detection and collection
|
|
@@ -282,6 +295,13 @@ await CoralogixRum.init({
|
|
|
282
295
|
});
|
|
283
296
|
```
|
|
284
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
|
+
|
|
303
|
+
<!-- /split -->
|
|
304
|
+
<!-- split title="Custom instrumentation" path="features/custom-instrumentation.md" -->
|
|
285
305
|
### Custom Spans
|
|
286
306
|
|
|
287
307
|
Create manual RUM spans to instrument custom flows in your application. Requires `traceParentInHeader.enabled: true`.
|
|
@@ -331,7 +351,8 @@ Pass instrument names to prevent network requests, errors, or interactions fired
|
|
|
331
351
|
const tracer = CoralogixRum.getCustomTracer(['networkRequests', 'userInteractions', 'errors']);
|
|
332
352
|
```
|
|
333
353
|
|
|
334
|
-
>
|
|
354
|
+
> [!NOTE]
|
|
355
|
+
> Only one global span may be active at a time. `startGlobalSpan` returns `null` if a global span is already open.
|
|
335
356
|
|
|
336
357
|
### Time Measurement
|
|
337
358
|
|
|
@@ -357,6 +378,8 @@ CoralogixRum.endTimeMeasure('checkout');
|
|
|
357
378
|
- Unmatched `endTimeMeasure` calls are dropped silently by the native SDK.
|
|
358
379
|
- Calling `startTimeMeasure` again with an open `name` overwrites the previous start.
|
|
359
380
|
|
|
381
|
+
<!-- /split -->
|
|
382
|
+
<!-- split title="Exporting and networking" path="configuration/exporting-and-networking.md" -->
|
|
360
383
|
### Traces Exporter
|
|
361
384
|
|
|
362
385
|
Receive OTLP-formatted trace batches from the native SDK in your JavaScript code. Use this to forward spans to an OTLP-compatible backend (e.g. Jaeger, custom collector) alongside Coralogix.
|
|
@@ -400,7 +423,8 @@ The callback receives a `TraceExporterData` object following the OTLP JSON forma
|
|
|
400
423
|
}
|
|
401
424
|
```
|
|
402
425
|
|
|
403
|
-
>
|
|
426
|
+
> [!NOTE]
|
|
427
|
+
> The callback fires once per native export batch, which typically contains several spans rather than one per span.
|
|
404
428
|
|
|
405
429
|
### beforeSend
|
|
406
430
|
|
|
@@ -454,7 +478,8 @@ await CoralogixRum.init({
|
|
|
454
478
|
});
|
|
455
479
|
```
|
|
456
480
|
|
|
457
|
-
>
|
|
481
|
+
> [!NOTE]
|
|
482
|
+
> Always read it as `isSessionSampledIn ?? true`. The field is absent on
|
|
458
483
|
> native SDKs that predate it, and the SDK never invents a value it was not given.
|
|
459
484
|
|
|
460
485
|
`isSessionSampledIn` is read-only: it reports the SDK's sampling decision for the
|
|
@@ -515,6 +540,8 @@ await CoralogixRum.init({
|
|
|
515
540
|
});
|
|
516
541
|
```
|
|
517
542
|
|
|
543
|
+
<!-- /split -->
|
|
544
|
+
<!-- split title="Session Replay" path="features/session-replay.md" -->
|
|
518
545
|
### Session Replay
|
|
519
546
|
|
|
520
547
|
Session Replay allows you to record and replay user sessions to understand user behavior and debug issues.
|
|
@@ -600,22 +627,50 @@ Shutdown Session Replay to clean up resources:
|
|
|
600
627
|
await SessionReplay.shutdown();
|
|
601
628
|
```
|
|
602
629
|
|
|
603
|
-
|
|
630
|
+
<!-- /split -->
|
|
631
|
+
<!-- split title="Masking sensitive content" path="features/masking.md" -->
|
|
632
|
+
### Masking Sensitive Content
|
|
604
633
|
|
|
605
|
-
|
|
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.
|
|
606
635
|
|
|
607
|
-
|
|
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';
|
|
608
659
|
import { SessionReplay } from '@coralogix/react-native-plugin';
|
|
609
660
|
|
|
610
661
|
<View onLayout={SessionReplay.maskView}>
|
|
611
|
-
<Text>
|
|
612
|
-
<TextInput placeholder="Password" />
|
|
662
|
+
<Text>{cardNumber}</Text>
|
|
613
663
|
</View>
|
|
614
664
|
```
|
|
615
665
|
|
|
616
|
-
|
|
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
|
|
617
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.
|
|
618
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.
|
|
673
|
+
<!-- /split -->
|
|
619
674
|
### Optional - Coralogix Gradle Plugin (Android)
|
|
620
675
|
|
|
621
676
|
The Coralogix Gradle Plugin automatically instruments all OkHttp clients in your app (including third-party SDKs) at build time.
|
|
@@ -669,4 +724,3 @@ However, the plugin is the only way to capture network activity from third-party
|
|
|
669
724
|
1. `npm install react-native-url-polyfill`
|
|
670
725
|
2. At the top of your entry-point file (index.js) add:
|
|
671
726
|
`import "react-native-url-polyfill/auto"`
|
|
672
|
-
---
|
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>? =
|