@coralogix/react-native-plugin 0.7.2 → 0.9.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,25 @@
1
+ ## 0.9.0 (2026-07-29)
2
+
3
+ ### 🚀 Features
4
+
5
+ - add data & labels to React Native reportError
6
+
7
+ ### ⬆️ Dependency Updates
8
+
9
+ - Native iOS SDK bumped to **2.13.2**
10
+ - Native Android SDK bumped to **2.19.2**
11
+
12
+ ## 0.8.0 (2026-07-29)
13
+
14
+ ### 🚀 Features
15
+
16
+ - expose isSessionSampledIn to beforeSend + make SDK-owned event fields read-only
17
+
18
+ ### ⬆️ Dependency Updates
19
+
20
+ - Native iOS SDK bumped to **2.13.1**
21
+ - Native Android SDK bumped to **2.19.1**
22
+
1
23
  ## 0.7.2 (2026-07-20)
2
24
 
3
25
  ### 🩹 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.11.1'
20
- s.dependency 'CoralogixInternal','2.11.1'
21
- s.dependency 'SessionReplay','2.11.1'
19
+ s.dependency 'Coralogix','2.13.2'
20
+ s.dependency 'CoralogixInternal','2.13.2'
21
+ s.dependency 'SessionReplay','2.13.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
@@ -92,6 +92,72 @@ CoralogixRum.error('this is an error log', {error: 'yes', is_bad: 'no'});
92
92
  > ```
93
93
  > in the event that you need to use any of these types please stringify them before you pass it to the `log` method
94
94
 
95
+ ### Error Reporting
96
+
97
+ Report handled errors with optional structured data and labels. **Choose one of the variants below per error event** — each call generates a single error report.
98
+
99
+ **Parameters:**
100
+ - `error` (Error): The error object to report.
101
+ - `isCrash` (boolean): Whether this error represents a crash (`true`) or a handled exception (`false`).
102
+ - `data` (optional): Structured data object attached to the error event. Empty objects are omitted.
103
+ - `labels` (optional): Key-value pairs for categorizing the error event. Empty objects are omitted.
104
+
105
+ **Variant 1: Basic error reporting**
106
+
107
+ ```javascript
108
+ import { CoralogixRum } from '@coralogix/react-native-plugin';
109
+
110
+ try {
111
+ await processPayment(order);
112
+ } catch (error) {
113
+ CoralogixRum.reportError(error, false);
114
+ }
115
+ ```
116
+
117
+ **Variant 2: With structured data**
118
+
119
+ ```javascript
120
+ try {
121
+ await processPayment(order);
122
+ } catch (error) {
123
+ CoralogixRum.reportError(error, false, {
124
+ orderId: '12345',
125
+ paymentMethod: 'visa',
126
+ amount: 99.99,
127
+ });
128
+ }
129
+ ```
130
+
131
+ **Variant 3: With data and labels**
132
+
133
+ ```javascript
134
+ try {
135
+ await processPayment(order);
136
+ } catch (error) {
137
+ CoralogixRum.reportError(
138
+ error,
139
+ false,
140
+ { orderId: '12345', amount: 99.99 },
141
+ { team: 'payments', severity: 'high' }
142
+ );
143
+ }
144
+ ```
145
+
146
+ **Reporting crashes:**
147
+
148
+ ```javascript
149
+ try {
150
+ await criticalOperation();
151
+ } catch (error) {
152
+ CoralogixRum.reportError(
153
+ error,
154
+ true, // isCrash = true
155
+ { operation: 'criticalTask', stage: 'initialization' },
156
+ { environment: 'production' }
157
+ );
158
+ }
159
+ ```
160
+
95
161
  ### View Tracking
96
162
 
97
163
  To track views, set the view context whenever a view changes.
@@ -184,6 +250,10 @@ await CoralogixRum.init({
184
250
  Allowed values: `'errors'`, `'logs'`, `'network'`, `'userInteractions'`,
185
251
  `'mobileVitals'`, `'customSpan'`, `'customMeasurement'`.
186
252
 
253
+ Inside a [`beforeSend`](#beforesend) callback, `session_context.isSessionSampledIn`
254
+ tells you whether an event came from a sampled-in session or reached you only via
255
+ `excludeFromSampling`, so excluded categories can be filtered further.
256
+
187
257
  ### Ignore Errors
188
258
 
189
259
  The ignoreErrors option allows you to exclude errors that meet specific criteria.
@@ -353,6 +423,82 @@ await CoralogixRum.init({
353
423
  });
354
424
  ```
355
425
 
426
+ #### Telling excluded events apart in `beforeSend`
427
+
428
+ Every event carries `session_context.isSessionSampledIn`. `false` means the event
429
+ reached you only because its category is listed in
430
+ [`excludeFromSampling`](#exclude-from-sampling) — the session itself was sampled
431
+ out. Use it to apply your own filtering on top of the exclude list, for example to
432
+ keep only error-severity events from sampled-out sessions:
433
+
434
+ ```javascript
435
+ await CoralogixRum.init({
436
+ // ...
437
+ sessionSampleRate: 10,
438
+ excludeFromSampling: ['errors', 'logs'],
439
+ beforeSend: (event) => {
440
+ // Default to `true`: a native SDK older than the one this plugin pins does not
441
+ // stamp the flag, and `!undefined` would drop everything but errors.
442
+ const sampledIn = event.session_context.isSessionSampledIn ?? true;
443
+
444
+ // Session sampled out: forward only error-severity events, drop the rest.
445
+ if (
446
+ !sampledIn &&
447
+ event.event_context?.severity !== CoralogixLogSeverity.Error
448
+ ) {
449
+ return null;
450
+ }
451
+
452
+ return event;
453
+ },
454
+ });
455
+ ```
456
+
457
+ > **Note:** always read it as `isSessionSampledIn ?? true`. The field is absent on
458
+ > native SDKs that predate it, and the SDK never invents a value it was not given.
459
+
460
+ `isSessionSampledIn` is read-only: it reports the SDK's sampling decision for the
461
+ session, so assigning to it has no effect on the sent event. The decision is
462
+ recorded when the event is created, so events buffered across a session rotation
463
+ keep the decision of the session they belong to.
464
+
465
+ #### Read-only fields
466
+
467
+ Some fields exist for the callback to *read*, not to change. The SDK restores its
468
+ own values after `beforeSend` returns, so assigning to any of them — or injecting
469
+ one into a returned object — has no effect on the sent event.
470
+
471
+ Inside `session_context`, **only** the user fields are editable:
472
+
473
+ | Editable | Read-only |
474
+ | --- | --- |
475
+ | `user_id`, `user_name`, `user_email`, `user_metadata` | `session_id`, `session_creation_date`, `isSessionSampledIn`, `hasRecording` |
476
+
477
+ Any other key in `session_context` — including one your callback invents — is
478
+ discarded, so session identity cannot be forged.
479
+
480
+ Deleting an editable field is honored, since redaction-by-deletion is a valid use.
481
+ That means returning a *hand-built partial* `session_context` drops the user fields
482
+ you left out; edit the event you were given (or spread it) if you want to change
483
+ one field and keep the rest.
484
+
485
+ These top-level fields are read-only too, because rewriting them would corrupt
486
+ trace correlation, event dedup or product analytics rather than redact anything:
487
+
488
+ `spanId`, `traceId`, `fingerPrint`, `timestamp`, `platform`, `mobile_sdk`,
489
+ `snapshot_context`, `isSnapshotEvent`, `prev_session`, `view_number`,
490
+ `isNavigationEvent`
491
+
492
+ `view_number` and `isNavigationEvent` record what the SDK observed when it built
493
+ the event. They are deliberately not recomputed from `event_context.type`, so if
494
+ you relabel an event the two can differ — filter on `isNavigationEvent` when you
495
+ need "was this actually a navigation".
496
+
497
+ Everything else — `event_context` (including `severity`), `labels`,
498
+ `error_context`, `log_context`, `network_request_context`, `view_context`,
499
+ `environment`, `device_context`, `device_state`, `version_metadata` — is editable,
500
+ and returning `null` still drops the event entirely.
501
+
356
502
  ### Proxy URL
357
503
 
358
504
  Proxy configuration to route requests.
@@ -84,7 +84,7 @@ dependencies {
84
84
  implementation "com.facebook.react:react-android"
85
85
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
86
86
 
87
- implementation "com.coralogix:android-sdk:2.18.0"
87
+ implementation "com.coralogix:android-sdk:2.19.2"
88
88
 
89
89
  testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
90
90
  testImplementation "org.mockito:mockito-core:5.12.0"
@@ -182,13 +182,20 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
182
182
  ?.mapValues { it.value as Any }
183
183
  ?.takeIf { it.isNotEmpty() }
184
184
 
185
+ val eventLabels: Map<String, Any>? = details.getMap("labels")
186
+ ?.toStringAnyMap()
187
+ ?.filterValues { it != null }
188
+ ?.mapValues { it.value as Any }
189
+ ?.takeIf { it.isNotEmpty() }
190
+
185
191
  CoralogixRum.reportError(
186
192
  CoralogixErrorDecorator(
187
193
  errorType = errorType,
188
194
  errorMessage = message,
189
195
  errorStack = CoralogixStackTrace(parsedStackFrames),
190
196
  isCrash = isCrash,
191
- customAttributes = customAttributes
197
+ customAttributes = customAttributes,
198
+ labels = eventLabels
192
199
  )
193
200
  )
194
201
  }
package/index.cjs.js CHANGED
@@ -17,6 +17,18 @@ function _extends() {
17
17
  }, _extends.apply(null, arguments);
18
18
  }
19
19
 
20
+ let CoralogixEventType = /*#__PURE__*/function (CoralogixEventType) {
21
+ CoralogixEventType["ERROR"] = "error";
22
+ CoralogixEventType["NETWORK_REQUEST"] = "network-request";
23
+ CoralogixEventType["LOG"] = "log";
24
+ CoralogixEventType["USER_INTERACTION"] = "user-interaction";
25
+ CoralogixEventType["WEB_VITALS"] = "web-vitals";
26
+ CoralogixEventType["LONG_TASK"] = "longtask";
27
+ CoralogixEventType["RESOURCES"] = "resources";
28
+ CoralogixEventType["INTERNAL"] = "internal";
29
+ CoralogixEventType["NAVIGATION"] = "navigation";
30
+ return CoralogixEventType;
31
+ }({});
20
32
  let CoralogixLogSeverity = /*#__PURE__*/function (CoralogixLogSeverity) {
21
33
  CoralogixLogSeverity[CoralogixLogSeverity["Debug"] = 1] = "Debug";
22
34
  CoralogixLogSeverity[CoralogixLogSeverity["Verbose"] = 2] = "Verbose";
@@ -27,6 +39,27 @@ let CoralogixLogSeverity = /*#__PURE__*/function (CoralogixLogSeverity) {
27
39
  return CoralogixLogSeverity;
28
40
  }({});
29
41
 
42
+ /** Versions of the plugin and the underlying native SDK that produced an event. */
43
+
44
+ /**
45
+ * A RUM event as it goes on the wire.
46
+ *
47
+ * Fields marked `readonly` are SDK-owned: span identity, dedup keys, SDK-owned
48
+ * counters and the SDK's own observations. `beforeSend` may read them, but the
49
+ * SDK's value is always restored after the callback returns — assigning to one
50
+ * has no effect on the sent event.
51
+ */
52
+
53
+ /**
54
+ * The view of an event that `beforeSend` receives.
55
+ *
56
+ * Only the user fields of `session_context` are editable — session identity, the
57
+ * sampling decision and the recording flag are SDK-owned and are restored from
58
+ * the SDK's own values after the callback returns. The same holds for the
59
+ * `readonly` fields inherited from {@link CxRumEvent}. Nothing a callback writes
60
+ * to those can reach the wire, so they cannot be forged.
61
+ */
62
+
30
63
  /**
31
64
  * Instrumentation names that can be excluded from a custom tracer's span context.
32
65
  * Pass one or more values to `getCustomTracer()` to prevent those event types from
@@ -544,7 +577,7 @@ function stopJsRefreshRateSampler() {
544
577
  appStateSub = null;
545
578
  }
546
579
 
547
- var version = "0.7.2";
580
+ var version = "0.9.0";
548
581
  var pkg = {
549
582
  version: version};
550
583
 
@@ -1217,7 +1250,7 @@ const CoralogixRum = {
1217
1250
  }
1218
1251
  CxSdk.endTimeMeasure(name);
1219
1252
  },
1220
- reportError: (error, isCrash, data) => {
1253
+ reportError: (error, isCrash, data, labels) => {
1221
1254
  if (!isInited) {
1222
1255
  logger.debug('CoralogixRum must be initiated before error reporting');
1223
1256
  return;
@@ -1231,6 +1264,9 @@ const CoralogixRum = {
1231
1264
  if (data && Object.keys(data).length > 0) {
1232
1265
  errorDetails.data = data;
1233
1266
  }
1267
+ if (labels && Object.keys(labels).length > 0) {
1268
+ errorDetails.labels = labels;
1269
+ }
1234
1270
  CxSdk.reportError(errorDetails);
1235
1271
  }
1236
1272
  };
@@ -1417,6 +1453,94 @@ const waitForFatalErrorFlush = async () => {
1417
1453
  // Create event emitter
1418
1454
  const eventEmitter = new reactNative.NativeEventEmitter(CxSdk);
1419
1455
 
1456
+ /**
1457
+ * The only `session_context` fields a `beforeSend` callback may change. Every
1458
+ * other field there is SDK-owned — session identity, the sampling decision, the
1459
+ * recording flag — and is restored from the SDK's own values after the callback
1460
+ * runs.
1461
+ *
1462
+ * Deliberately an allowlist rather than a list of protected fields: a session
1463
+ * field added by a future native SDK is then read-only by default instead of
1464
+ * silently becoming forgeable. Matches the Android SDK, whose `EditableCxRum`
1465
+ * only accepts a user context, so nothing else in the session can be rewritten.
1466
+ */
1467
+ const EDITABLE_SESSION_FIELDS = ['user_id', 'user_name', 'user_email', 'user_metadata'];
1468
+
1469
+ /**
1470
+ * Top-level `cx_rum` keys a callback must not change: span identity, dedup keys,
1471
+ * SDK-owned counters and the SDK's own observations. A rewrite here would corrupt
1472
+ * trace correlation or downstream funnel analysis rather than redact anything.
1473
+ *
1474
+ * Mirrors the native SDKs' read-only set. `view_number` and `isNavigationEvent`
1475
+ * are included for the same reason they are natively: they record what the SDK
1476
+ * saw at build time, not a property derived from the (possibly relabelled) event.
1477
+ */
1478
+ const READ_ONLY_CX_RUM_KEYS = ['snapshot_context', 'isSnapshotEvent', 'mobile_sdk', 'timestamp', 'traceId', 'spanId', 'fingerPrint', 'prev_session', 'platform', 'view_number', 'isNavigationEvent'];
1479
+ const isPlainObject = value => typeof value === 'object' && value !== null && !Array.isArray(value);
1480
+
1481
+ /**
1482
+ * Deep copy of a value that came off the native bridge, so a callback mutating a
1483
+ * nested object in place cannot also mutate the snapshot we restore from. Bridge
1484
+ * payloads are JSON by construction; anything exotic is returned as-is rather
1485
+ * than throwing, since a failed snapshot must not drop the event.
1486
+ */
1487
+ const cloneBridgeValue = value => {
1488
+ if (value === null || typeof value !== 'object') {
1489
+ return value;
1490
+ }
1491
+ try {
1492
+ return JSON.parse(JSON.stringify(value));
1493
+ } catch (e) {
1494
+ return value;
1495
+ }
1496
+ };
1497
+
1498
+ /**
1499
+ * Rebuilds `session_context` from the SDK's own values, copying over only the
1500
+ * fields in {@link EDITABLE_SESSION_FIELDS}. Anything the callback rewrote or
1501
+ * invented outside that set is discarded.
1502
+ *
1503
+ * @param merged `session_context` as it stands after the callback ran.
1504
+ * @param original `session_context` snapshotted *before* the callback ran.
1505
+ */
1506
+ const rebuildSessionContext = (merged, original) => {
1507
+ // The SDK always sends a session_context. If it somehow did not, a callback
1508
+ // must not be able to invent one — session identity would be forgeable through
1509
+ // exactly the gap this function exists to close.
1510
+ if (!isPlainObject(original)) {
1511
+ return original;
1512
+ }
1513
+ // A callback that replaced session_context with a non-object (or dropped it)
1514
+ // gets the SDK's version back wholesale: there are no edits to salvage, and
1515
+ // native would otherwise fail to decode and drop the whole event.
1516
+ if (!isPlainObject(merged)) {
1517
+ return original;
1518
+ }
1519
+ const rebuilt = _extends({}, original);
1520
+ for (const key of EDITABLE_SESSION_FIELDS) {
1521
+ if (key in merged) {
1522
+ rebuilt[key] = merged[key];
1523
+ } else {
1524
+ // Removing a user field is a legitimate redaction, so honor the deletion.
1525
+ delete rebuilt[key];
1526
+ }
1527
+ }
1528
+ return rebuilt;
1529
+ };
1530
+
1531
+ /** Puts the SDK's values back on the read-only top-level keys, in place. */
1532
+ const restoreReadOnlyCxRumKeys = (merged, protectedValues) => {
1533
+ for (const key of READ_ONLY_CX_RUM_KEYS) {
1534
+ if (key in protectedValues) {
1535
+ merged[key] = protectedValues[key];
1536
+ } else {
1537
+ // Native never sent this key, so a callback must not be able to fabricate
1538
+ // one that looks like it came from the SDK.
1539
+ delete merged[key];
1540
+ }
1541
+ }
1542
+ };
1543
+
1420
1544
  // Subscribe to beforeSend events
1421
1545
  const subscription = eventEmitter.addListener('onBeforeSend', events => {
1422
1546
  // logger.log("📨 onBeforeSend triggered with events: ", events)
@@ -1435,14 +1559,45 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1435
1559
  const results = [];
1436
1560
  for (const fullEvent of events) {
1437
1561
  const event = fullEvent.text.cx_rum;
1562
+
1563
+ // Snapshot everything SDK-owned *before* the callback runs. Callbacks
1564
+ // routinely mutate the event in place (the documented redaction example
1565
+ // does) and `event` is the very object nested in `fullEvent`, so reading
1566
+ // these afterwards would just hand back the callback's own edits. The
1567
+ // snapshot is deep so an in-place edit to a nested object (say
1568
+ // `event.mobile_sdk.sdk_version`) can't tamper with it either.
1569
+ const protectedValues = {};
1570
+ if (isPlainObject(event)) {
1571
+ for (const key of READ_ONLY_CX_RUM_KEYS) {
1572
+ if (key in event) {
1573
+ protectedValues[key] = cloneBridgeValue(event[key]);
1574
+ }
1575
+ }
1576
+ }
1577
+ const protectedSessionContext = cloneBridgeValue(event == null ? void 0 : event.session_context);
1438
1578
  const res = beforeSendCallback(event);
1439
1579
  if (!res) {
1440
1580
  continue;
1441
1581
  }
1582
+ // A truthy non-object return means the callback malfunctioned. Discard its
1583
+ // edits rather than trusting it: spreading a string would splatter index
1584
+ // keys across cx_rum, and forwarding the event untouched would skip the
1585
+ // restores below — letting an in-place tamper through on the one path that
1586
+ // never gets checked.
1587
+ let edits = {};
1588
+ if (isPlainObject(res)) {
1589
+ edits = res;
1590
+ } else {
1591
+ logger.debug('⚠️ beforeSend returned a non-object; discarding its edits');
1592
+ }
1593
+
1442
1594
  // Merge the result from each event
1595
+ const mergedCxRum = _extends({}, fullEvent.text.cx_rum, edits);
1596
+ mergedCxRum.session_context = rebuildSessionContext(mergedCxRum.session_context, protectedSessionContext);
1597
+ restoreReadOnlyCxRumKeys(mergedCxRum, protectedValues);
1443
1598
  const mergeEvent = _extends({}, fullEvent, {
1444
1599
  text: _extends({}, fullEvent.text, {
1445
- cx_rum: _extends({}, fullEvent.text.cx_rum, res)
1600
+ cx_rum: mergedCxRum
1446
1601
  })
1447
1602
  });
1448
1603
  results.push(mergeEvent);
@@ -1462,6 +1617,7 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1462
1617
  exports.CoralogixCustomSpan = CoralogixCustomSpan;
1463
1618
  exports.CoralogixCustomTracer = CoralogixCustomTracer;
1464
1619
  exports.CoralogixDomain = CoralogixDomain;
1620
+ exports.CoralogixEventType = CoralogixEventType;
1465
1621
  exports.CoralogixGlobalSpan = CoralogixGlobalSpan;
1466
1622
  exports.CoralogixLogSeverity = CoralogixLogSeverity;
1467
1623
  exports.CoralogixRum = CoralogixRum;
package/index.esm.js CHANGED
@@ -15,6 +15,18 @@ function _extends() {
15
15
  }, _extends.apply(null, arguments);
16
16
  }
17
17
 
18
+ let CoralogixEventType = /*#__PURE__*/function (CoralogixEventType) {
19
+ CoralogixEventType["ERROR"] = "error";
20
+ CoralogixEventType["NETWORK_REQUEST"] = "network-request";
21
+ CoralogixEventType["LOG"] = "log";
22
+ CoralogixEventType["USER_INTERACTION"] = "user-interaction";
23
+ CoralogixEventType["WEB_VITALS"] = "web-vitals";
24
+ CoralogixEventType["LONG_TASK"] = "longtask";
25
+ CoralogixEventType["RESOURCES"] = "resources";
26
+ CoralogixEventType["INTERNAL"] = "internal";
27
+ CoralogixEventType["NAVIGATION"] = "navigation";
28
+ return CoralogixEventType;
29
+ }({});
18
30
  let CoralogixLogSeverity = /*#__PURE__*/function (CoralogixLogSeverity) {
19
31
  CoralogixLogSeverity[CoralogixLogSeverity["Debug"] = 1] = "Debug";
20
32
  CoralogixLogSeverity[CoralogixLogSeverity["Verbose"] = 2] = "Verbose";
@@ -25,6 +37,27 @@ let CoralogixLogSeverity = /*#__PURE__*/function (CoralogixLogSeverity) {
25
37
  return CoralogixLogSeverity;
26
38
  }({});
27
39
 
40
+ /** Versions of the plugin and the underlying native SDK that produced an event. */
41
+
42
+ /**
43
+ * A RUM event as it goes on the wire.
44
+ *
45
+ * Fields marked `readonly` are SDK-owned: span identity, dedup keys, SDK-owned
46
+ * counters and the SDK's own observations. `beforeSend` may read them, but the
47
+ * SDK's value is always restored after the callback returns — assigning to one
48
+ * has no effect on the sent event.
49
+ */
50
+
51
+ /**
52
+ * The view of an event that `beforeSend` receives.
53
+ *
54
+ * Only the user fields of `session_context` are editable — session identity, the
55
+ * sampling decision and the recording flag are SDK-owned and are restored from
56
+ * the SDK's own values after the callback returns. The same holds for the
57
+ * `readonly` fields inherited from {@link CxRumEvent}. Nothing a callback writes
58
+ * to those can reach the wire, so they cannot be forged.
59
+ */
60
+
28
61
  /**
29
62
  * Instrumentation names that can be excluded from a custom tracer's span context.
30
63
  * Pass one or more values to `getCustomTracer()` to prevent those event types from
@@ -542,7 +575,7 @@ function stopJsRefreshRateSampler() {
542
575
  appStateSub = null;
543
576
  }
544
577
 
545
- var version = "0.7.2";
578
+ var version = "0.9.0";
546
579
  var pkg = {
547
580
  version: version};
548
581
 
@@ -1215,7 +1248,7 @@ const CoralogixRum = {
1215
1248
  }
1216
1249
  CxSdk.endTimeMeasure(name);
1217
1250
  },
1218
- reportError: (error, isCrash, data) => {
1251
+ reportError: (error, isCrash, data, labels) => {
1219
1252
  if (!isInited) {
1220
1253
  logger.debug('CoralogixRum must be initiated before error reporting');
1221
1254
  return;
@@ -1229,6 +1262,9 @@ const CoralogixRum = {
1229
1262
  if (data && Object.keys(data).length > 0) {
1230
1263
  errorDetails.data = data;
1231
1264
  }
1265
+ if (labels && Object.keys(labels).length > 0) {
1266
+ errorDetails.labels = labels;
1267
+ }
1232
1268
  CxSdk.reportError(errorDetails);
1233
1269
  }
1234
1270
  };
@@ -1415,6 +1451,94 @@ const waitForFatalErrorFlush = async () => {
1415
1451
  // Create event emitter
1416
1452
  const eventEmitter = new NativeEventEmitter(CxSdk);
1417
1453
 
1454
+ /**
1455
+ * The only `session_context` fields a `beforeSend` callback may change. Every
1456
+ * other field there is SDK-owned — session identity, the sampling decision, the
1457
+ * recording flag — and is restored from the SDK's own values after the callback
1458
+ * runs.
1459
+ *
1460
+ * Deliberately an allowlist rather than a list of protected fields: a session
1461
+ * field added by a future native SDK is then read-only by default instead of
1462
+ * silently becoming forgeable. Matches the Android SDK, whose `EditableCxRum`
1463
+ * only accepts a user context, so nothing else in the session can be rewritten.
1464
+ */
1465
+ const EDITABLE_SESSION_FIELDS = ['user_id', 'user_name', 'user_email', 'user_metadata'];
1466
+
1467
+ /**
1468
+ * Top-level `cx_rum` keys a callback must not change: span identity, dedup keys,
1469
+ * SDK-owned counters and the SDK's own observations. A rewrite here would corrupt
1470
+ * trace correlation or downstream funnel analysis rather than redact anything.
1471
+ *
1472
+ * Mirrors the native SDKs' read-only set. `view_number` and `isNavigationEvent`
1473
+ * are included for the same reason they are natively: they record what the SDK
1474
+ * saw at build time, not a property derived from the (possibly relabelled) event.
1475
+ */
1476
+ const READ_ONLY_CX_RUM_KEYS = ['snapshot_context', 'isSnapshotEvent', 'mobile_sdk', 'timestamp', 'traceId', 'spanId', 'fingerPrint', 'prev_session', 'platform', 'view_number', 'isNavigationEvent'];
1477
+ const isPlainObject = value => typeof value === 'object' && value !== null && !Array.isArray(value);
1478
+
1479
+ /**
1480
+ * Deep copy of a value that came off the native bridge, so a callback mutating a
1481
+ * nested object in place cannot also mutate the snapshot we restore from. Bridge
1482
+ * payloads are JSON by construction; anything exotic is returned as-is rather
1483
+ * than throwing, since a failed snapshot must not drop the event.
1484
+ */
1485
+ const cloneBridgeValue = value => {
1486
+ if (value === null || typeof value !== 'object') {
1487
+ return value;
1488
+ }
1489
+ try {
1490
+ return JSON.parse(JSON.stringify(value));
1491
+ } catch (e) {
1492
+ return value;
1493
+ }
1494
+ };
1495
+
1496
+ /**
1497
+ * Rebuilds `session_context` from the SDK's own values, copying over only the
1498
+ * fields in {@link EDITABLE_SESSION_FIELDS}. Anything the callback rewrote or
1499
+ * invented outside that set is discarded.
1500
+ *
1501
+ * @param merged `session_context` as it stands after the callback ran.
1502
+ * @param original `session_context` snapshotted *before* the callback ran.
1503
+ */
1504
+ const rebuildSessionContext = (merged, original) => {
1505
+ // The SDK always sends a session_context. If it somehow did not, a callback
1506
+ // must not be able to invent one — session identity would be forgeable through
1507
+ // exactly the gap this function exists to close.
1508
+ if (!isPlainObject(original)) {
1509
+ return original;
1510
+ }
1511
+ // A callback that replaced session_context with a non-object (or dropped it)
1512
+ // gets the SDK's version back wholesale: there are no edits to salvage, and
1513
+ // native would otherwise fail to decode and drop the whole event.
1514
+ if (!isPlainObject(merged)) {
1515
+ return original;
1516
+ }
1517
+ const rebuilt = _extends({}, original);
1518
+ for (const key of EDITABLE_SESSION_FIELDS) {
1519
+ if (key in merged) {
1520
+ rebuilt[key] = merged[key];
1521
+ } else {
1522
+ // Removing a user field is a legitimate redaction, so honor the deletion.
1523
+ delete rebuilt[key];
1524
+ }
1525
+ }
1526
+ return rebuilt;
1527
+ };
1528
+
1529
+ /** Puts the SDK's values back on the read-only top-level keys, in place. */
1530
+ const restoreReadOnlyCxRumKeys = (merged, protectedValues) => {
1531
+ for (const key of READ_ONLY_CX_RUM_KEYS) {
1532
+ if (key in protectedValues) {
1533
+ merged[key] = protectedValues[key];
1534
+ } else {
1535
+ // Native never sent this key, so a callback must not be able to fabricate
1536
+ // one that looks like it came from the SDK.
1537
+ delete merged[key];
1538
+ }
1539
+ }
1540
+ };
1541
+
1418
1542
  // Subscribe to beforeSend events
1419
1543
  const subscription = eventEmitter.addListener('onBeforeSend', events => {
1420
1544
  // logger.log("📨 onBeforeSend triggered with events: ", events)
@@ -1433,14 +1557,45 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1433
1557
  const results = [];
1434
1558
  for (const fullEvent of events) {
1435
1559
  const event = fullEvent.text.cx_rum;
1560
+
1561
+ // Snapshot everything SDK-owned *before* the callback runs. Callbacks
1562
+ // routinely mutate the event in place (the documented redaction example
1563
+ // does) and `event` is the very object nested in `fullEvent`, so reading
1564
+ // these afterwards would just hand back the callback's own edits. The
1565
+ // snapshot is deep so an in-place edit to a nested object (say
1566
+ // `event.mobile_sdk.sdk_version`) can't tamper with it either.
1567
+ const protectedValues = {};
1568
+ if (isPlainObject(event)) {
1569
+ for (const key of READ_ONLY_CX_RUM_KEYS) {
1570
+ if (key in event) {
1571
+ protectedValues[key] = cloneBridgeValue(event[key]);
1572
+ }
1573
+ }
1574
+ }
1575
+ const protectedSessionContext = cloneBridgeValue(event == null ? void 0 : event.session_context);
1436
1576
  const res = beforeSendCallback(event);
1437
1577
  if (!res) {
1438
1578
  continue;
1439
1579
  }
1580
+ // A truthy non-object return means the callback malfunctioned. Discard its
1581
+ // edits rather than trusting it: spreading a string would splatter index
1582
+ // keys across cx_rum, and forwarding the event untouched would skip the
1583
+ // restores below — letting an in-place tamper through on the one path that
1584
+ // never gets checked.
1585
+ let edits = {};
1586
+ if (isPlainObject(res)) {
1587
+ edits = res;
1588
+ } else {
1589
+ logger.debug('⚠️ beforeSend returned a non-object; discarding its edits');
1590
+ }
1591
+
1440
1592
  // Merge the result from each event
1593
+ const mergedCxRum = _extends({}, fullEvent.text.cx_rum, edits);
1594
+ mergedCxRum.session_context = rebuildSessionContext(mergedCxRum.session_context, protectedSessionContext);
1595
+ restoreReadOnlyCxRumKeys(mergedCxRum, protectedValues);
1441
1596
  const mergeEvent = _extends({}, fullEvent, {
1442
1597
  text: _extends({}, fullEvent.text, {
1443
- cx_rum: _extends({}, fullEvent.text.cx_rum, res)
1598
+ cx_rum: mergedCxRum
1444
1599
  })
1445
1600
  });
1446
1601
  results.push(mergeEvent);
@@ -1457,4 +1612,4 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1457
1612
  }
1458
1613
  });
1459
1614
 
1460
- export { CoralogixCustomSpan, CoralogixCustomTracer, CoralogixDomain, CoralogixGlobalSpan, CoralogixLogSeverity, CoralogixRum, CxSdk, SessionReplay, attachReactNavigationObserver, parseErrorStack, waitForFatalErrorFlush };
1615
+ export { CoralogixCustomSpan, CoralogixCustomTracer, CoralogixDomain, CoralogixEventType, CoralogixGlobalSpan, CoralogixLogSeverity, CoralogixRum, CxSdk, SessionReplay, attachReactNavigationObserver, parseErrorStack, waitForFatalErrorFlush };
package/ios/CxSdk.swift CHANGED
@@ -237,15 +237,17 @@ class CxSdk: RCTEventEmitter {
237
237
  let message = dictionary[Keys.errorMessage.rawValue] as? String ?? ""
238
238
  let errorType = dictionary[Keys.errorType.rawValue] as? String ?? "5"
239
239
  let isCrash = dictionary[Keys.isCrash.rawValue] as? Bool ?? false
240
- // Pass `data` only when non-empty — an empty dict would surface
241
- // `error_context.data = {}` on the emitted RUM event.
240
+ // Pass `data`/`labels` only when non-empty — an empty dict would surface an
241
+ // empty `error_context.error_custom_data` (or labels) on the emitted RUM event.
242
242
  let customAttributes = (dictionary["data"] as? [String: Any]).flatMap { $0.isEmpty ? nil : $0 }
243
+ let labels = (dictionary["labels"] as? [String: Any]).flatMap { $0.isEmpty ? nil : $0 }
243
244
 
244
245
  coralogixRum?.reportError(message: message,
245
246
  stackTrace: stackTrace,
246
247
  errorType: errorType,
247
248
  isCrash: isCrash,
248
- customAttributes: customAttributes)
249
+ customAttributes: customAttributes,
250
+ labels: labels)
249
251
  resolve("reportError success")
250
252
  }
251
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.7.2",
3
+ "version": "0.9.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
@@ -79,7 +79,7 @@ export { type ApplicationContextConfig } from './model/ApplicationContextConfig'
79
79
  export { type ViewContextConfig } from './model/ViewContextConfig';
80
80
  export { CoralogixDomain } from './model/CoralogixDomain';
81
81
  export { type UserContextConfig } from './model/UserContextConfig';
82
- export { type CoralogixBrowserSdkConfig, type CoralogixIgnoredInstrument, type ExcludableInstrumentation, type NetworkCaptureRule, type TraceExporterData, CoralogixLogSeverity, } from './model/Types';
82
+ export { type CoralogixBrowserSdkConfig, type CoralogixIgnoredInstrument, type ExcludableInstrumentation, type NetworkCaptureRule, type TraceExporterData, type EditableCxRumEvent, type BeforeSendResult, CoralogixEventType, CoralogixLogSeverity, } from './model/Types';
83
83
  export { type CoralogixOtelWebOptionsInstrumentations } from './model/CoralogixOtelWebOptionsInstrumentations';
84
84
  export { type CustomMeasurement } from './model/CustomMeasurement';
85
85
  export { type NetworkRequestDetails } from './model/NetworkRequestDetails';
@@ -59,7 +59,7 @@ export interface CoralogixOtelWebType extends SendLog {
59
59
  * Unknown names are dropped silently by the native SDK.
60
60
  */
61
61
  endTimeMeasure: (name: string) => void;
62
- reportError: (error: Error, isCrash: boolean, data?: Record<string, unknown>) => void;
62
+ reportError: (error: Error, isCrash: boolean, data?: Record<string, unknown>, labels?: Record<string, unknown>) => void;
63
63
  /**
64
64
  * Report a network request to the underlying Coralogix SDK
65
65
  * @param details
@@ -28,6 +28,28 @@ export interface SessionContext extends UserMetadata {
28
28
  device: string | undefined;
29
29
  os: string | undefined;
30
30
  osVersion: string | number | undefined;
31
+ /**
32
+ * Whether the session this event belongs to was sampled in. `false` means the
33
+ * event reached export only because its category is listed in
34
+ * `excludeFromSampling` while the session itself was sampled out.
35
+ *
36
+ * Read-only: it records the SDK's sampling decision for the session, so
37
+ * assigning to it inside `beforeSend` has no effect on the sent event. The
38
+ * decision is stamped on each event when it is created, so events buffered
39
+ * across a session rotation keep the decision of the session they belong to.
40
+ *
41
+ * Optional because a native SDK older than the version this plugin pins does
42
+ * not stamp it, and the SDK never fabricates a value it did not receive. Treat
43
+ * `undefined` as `true` — the native SDKs use the same default, since an
44
+ * unstamped event can only have reached export on a sampled-in session.
45
+ */
46
+ readonly isSessionSampledIn?: boolean;
47
+ /** Whether Session Replay was recording for this session. Read-only. */
48
+ readonly hasRecording?: boolean;
49
+ /** Session identity. Read-only — the SDK owns it. */
50
+ readonly session_id?: string;
51
+ /** Session start time. Read-only — the SDK owns it. */
52
+ readonly session_creation_date?: number;
31
53
  }
32
54
  export interface DeviceState {
33
55
  battery: string | undefined;
@@ -114,8 +136,25 @@ export interface SnapshotContext {
114
136
  actionCount: number;
115
137
  hasRecording: boolean;
116
138
  }
139
+ /** Versions of the plugin and the underlying native SDK that produced an event. */
140
+ export interface MobileSdkMetadata {
141
+ readonly framework?: string;
142
+ readonly os?: string;
143
+ /** Version of `@coralogix/react-native-plugin`. */
144
+ readonly sdk_version?: string;
145
+ /** Version of the native iOS/Android Coralogix SDK. */
146
+ readonly native_version?: string;
147
+ }
148
+ /**
149
+ * A RUM event as it goes on the wire.
150
+ *
151
+ * Fields marked `readonly` are SDK-owned: span identity, dedup keys, SDK-owned
152
+ * counters and the SDK's own observations. `beforeSend` may read them, but the
153
+ * SDK's value is always restored after the callback returns — assigning to one
154
+ * has no effect on the sent event.
155
+ */
117
156
  export interface CxRumEvent {
118
- platform: string;
157
+ readonly platform: string;
119
158
  version_metadata: VersionMetaData;
120
159
  session_context: SessionContext;
121
160
  device_context: DeviceContext;
@@ -125,16 +164,39 @@ export interface CxRumEvent {
125
164
  error_context?: ErrorContext;
126
165
  log_context?: LogContext;
127
166
  network_request_context?: NetworkRequestContext;
128
- snapshot_context?: SnapshotContext;
167
+ readonly snapshot_context?: SnapshotContext;
129
168
  labels: CoralogixRumLabels;
130
- spanId: string;
131
- traceId: string;
169
+ readonly spanId: string;
170
+ readonly traceId: string;
132
171
  environment: string;
133
- timestamp: number;
134
- isSnapshotEvent?: boolean;
172
+ readonly timestamp: number;
173
+ readonly isSnapshotEvent?: boolean;
174
+ /** SDK/framework versions. Read-only. */
175
+ readonly mobile_sdk?: MobileSdkMetadata;
176
+ /** Event dedup key. Read-only. */
177
+ readonly fingerPrint?: string;
178
+ /** Previous session identity, present only after a session rotation. Read-only. */
179
+ readonly prev_session?: Record<string, unknown>;
180
+ /** SDK-owned per-session navigation step counter. Read-only. */
181
+ readonly view_number?: number | null;
182
+ /**
183
+ * Whether the SDK observed a navigation when it built this event. Read-only,
184
+ * and intentionally not recomputed from `event_context.type` — it records what
185
+ * the SDK saw, so it can legitimately diverge from a relabelled event type.
186
+ */
187
+ readonly isNavigationEvent?: boolean;
135
188
  }
189
+ /**
190
+ * The view of an event that `beforeSend` receives.
191
+ *
192
+ * Only the user fields of `session_context` are editable — session identity, the
193
+ * sampling decision and the recording flag are SDK-owned and are restored from
194
+ * the SDK's own values after the callback returns. The same holds for the
195
+ * `readonly` fields inherited from {@link CxRumEvent}. Nothing a callback writes
196
+ * to those can reach the wire, so they cannot be forged.
197
+ */
136
198
  export interface EditableCxRumEvent extends Omit<CxRumEvent, 'session_context' | 'timestamp' | 'snapshot_context'> {
137
- session_context: Pick<SessionContext, keyof UserMetadata>;
199
+ session_context: Pick<SessionContext, keyof UserMetadata | 'isSessionSampledIn'>;
138
200
  }
139
201
  export type BeforeSendResult = EditableCxRumEvent | null;
140
202
  export interface NetworkCaptureRule {