@coralogix/react-native-plugin 0.7.1 → 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,32 @@
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
+
23
+ ## 0.7.2 (2026-07-20)
24
+
25
+ ### 🩹 Fixes
26
+
27
+ - drain every beforeSend delivery during the fatal flush
28
+ - flush crash events before fatal-error handover
29
+
1
30
  ## 0.7.1 (2026-07-09)
2
31
 
3
32
  ### 🩹 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.10.0'
20
- s.dependency 'CoralogixInternal','2.10.0'
21
- s.dependency 'SessionReplay','2.10.0'
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.17.1"
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,17 +182,33 @@ 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
  }
195
202
 
203
+ @ReactMethod
204
+ override fun flushSpans(promise: Promise) {
205
+ // Force-flushes the batch processor and resolves once the export/upload
206
+ // attempt finishes — the same contract as iOS. Called by the JS fatal-error
207
+ // handler before it hands over to the framework's default handler, which
208
+ // terminates the process.
209
+ CoralogixRum.flush { promise.resolve("flushSpans success") }
210
+ }
211
+
196
212
  @ReactMethod
197
213
  override fun sendCxSpanData(results: ReadableArray) {
198
214
  val data = convertStrictReadableArrayToListOfMaps(results)
@@ -19,6 +19,7 @@ interface RUMClient {
19
19
  fun startTimeMeasure(name: String, labels: ReadableMap?)
20
20
  fun endTimeMeasure(name: String)
21
21
  fun reportError(details: ReadableMap)
22
+ fun flushSpans(promise: Promise)
22
23
  fun sendCxSpanData(results: ReadableArray)
23
24
  fun reportMobileVitalsMeasurement(type: String, value: Double, units: String)
24
25
  fun reportMobileVitalsMeasurementSet(type: String, metrics: ReadableArray)
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
@@ -130,10 +163,17 @@ class CoralogixErrorInstrumentation extends instrumentation.InstrumentationBase
130
163
  const defaultHandler = ErrorUtils.getGlobalHandler();
131
164
  ErrorUtils.setGlobalHandler((error, isFatal) => {
132
165
  this.report(error, isFatal);
133
- if (defaultHandler) {
134
- setTimeout(() => {
135
- defaultHandler(error, isFatal);
136
- }, 2500);
166
+ if (!defaultHandler) {
167
+ return;
168
+ }
169
+ if (isFatal) {
170
+ // In release builds the default handler terminates the app (RCTFatal),
171
+ // killing any crash event still queued in the native batch processor —
172
+ // flush it out first. waitForFatalErrorFlush never rejects and is
173
+ // time-capped, so the handover is guaranteed.
174
+ waitForFatalErrorFlush().catch(() => undefined).finally(() => defaultHandler(error, isFatal));
175
+ } else {
176
+ defaultHandler(error, isFatal);
137
177
  }
138
178
  });
139
179
  this.listenToConsoleError();
@@ -537,7 +577,7 @@ function stopJsRefreshRateSampler() {
537
577
  appStateSub = null;
538
578
  }
539
579
 
540
- var version = "0.7.1";
580
+ var version = "0.9.0";
541
581
  var pkg = {
542
582
  version: version};
543
583
 
@@ -1210,7 +1250,7 @@ const CoralogixRum = {
1210
1250
  }
1211
1251
  CxSdk.endTimeMeasure(name);
1212
1252
  },
1213
- reportError: (error, isCrash, data) => {
1253
+ reportError: (error, isCrash, data, labels) => {
1214
1254
  if (!isInited) {
1215
1255
  logger.debug('CoralogixRum must be initiated before error reporting');
1216
1256
  return;
@@ -1224,6 +1264,9 @@ const CoralogixRum = {
1224
1264
  if (data && Object.keys(data).length > 0) {
1225
1265
  errorDetails.data = data;
1226
1266
  }
1267
+ if (labels && Object.keys(labels).length > 0) {
1268
+ errorDetails.labels = labels;
1269
+ }
1227
1270
  CxSdk.reportError(errorDetails);
1228
1271
  }
1229
1272
  };
@@ -1389,10 +1432,115 @@ const parseErrorStack = error => {
1389
1432
  return null;
1390
1433
  }).filter(frame => frame !== null);
1391
1434
  };
1435
+ const FATAL_FLUSH_MAX_WAIT_MS = 2500;
1436
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
1437
+
1438
+ /**
1439
+ * Best-effort delivery of a crash before a fatal error is handed back to RN's
1440
+ * default handler, which terminates the app in release builds. `flushSpans`
1441
+ * force-flushes the native batch processor; the native SDK uploads crash events
1442
+ * directly (bypassing the beforeSend round trip) and resolves once the upload
1443
+ * attempt finishes, so awaiting it is the full crash-delivery guarantee.
1444
+ * Never rejects; resolves within FATAL_FLUSH_MAX_WAIT_MS.
1445
+ *
1446
+ * @internal Used by the error instrumentation — not part of the public API.
1447
+ */
1448
+ const waitForFatalErrorFlush = async () => {
1449
+ const flush = Promise.resolve(CxSdk.flushSpans()).catch(e => logger.debug('flushSpans failed:', e));
1450
+ await Promise.race([flush, delay(FATAL_FLUSH_MAX_WAIT_MS)]);
1451
+ };
1392
1452
 
1393
1453
  // Create event emitter
1394
1454
  const eventEmitter = new reactNative.NativeEventEmitter(CxSdk);
1395
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
+
1396
1544
  // Subscribe to beforeSend events
1397
1545
  const subscription = eventEmitter.addListener('onBeforeSend', events => {
1398
1546
  // logger.log("📨 onBeforeSend triggered with events: ", events)
@@ -1411,14 +1559,45 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1411
1559
  const results = [];
1412
1560
  for (const fullEvent of events) {
1413
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);
1414
1578
  const res = beforeSendCallback(event);
1415
1579
  if (!res) {
1416
1580
  continue;
1417
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
+
1418
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);
1419
1598
  const mergeEvent = _extends({}, fullEvent, {
1420
1599
  text: _extends({}, fullEvent.text, {
1421
- cx_rum: _extends({}, fullEvent.text.cx_rum, res)
1600
+ cx_rum: mergedCxRum
1422
1601
  })
1423
1602
  });
1424
1603
  results.push(mergeEvent);
@@ -1438,6 +1617,7 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1438
1617
  exports.CoralogixCustomSpan = CoralogixCustomSpan;
1439
1618
  exports.CoralogixCustomTracer = CoralogixCustomTracer;
1440
1619
  exports.CoralogixDomain = CoralogixDomain;
1620
+ exports.CoralogixEventType = CoralogixEventType;
1441
1621
  exports.CoralogixGlobalSpan = CoralogixGlobalSpan;
1442
1622
  exports.CoralogixLogSeverity = CoralogixLogSeverity;
1443
1623
  exports.CoralogixRum = CoralogixRum;
@@ -1445,3 +1625,4 @@ exports.CxSdk = CxSdk;
1445
1625
  exports.SessionReplay = SessionReplay;
1446
1626
  exports.attachReactNavigationObserver = attachReactNavigationObserver;
1447
1627
  exports.parseErrorStack = parseErrorStack;
1628
+ exports.waitForFatalErrorFlush = waitForFatalErrorFlush;
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
@@ -128,10 +161,17 @@ class CoralogixErrorInstrumentation extends InstrumentationBase {
128
161
  const defaultHandler = ErrorUtils.getGlobalHandler();
129
162
  ErrorUtils.setGlobalHandler((error, isFatal) => {
130
163
  this.report(error, isFatal);
131
- if (defaultHandler) {
132
- setTimeout(() => {
133
- defaultHandler(error, isFatal);
134
- }, 2500);
164
+ if (!defaultHandler) {
165
+ return;
166
+ }
167
+ if (isFatal) {
168
+ // In release builds the default handler terminates the app (RCTFatal),
169
+ // killing any crash event still queued in the native batch processor —
170
+ // flush it out first. waitForFatalErrorFlush never rejects and is
171
+ // time-capped, so the handover is guaranteed.
172
+ waitForFatalErrorFlush().catch(() => undefined).finally(() => defaultHandler(error, isFatal));
173
+ } else {
174
+ defaultHandler(error, isFatal);
135
175
  }
136
176
  });
137
177
  this.listenToConsoleError();
@@ -535,7 +575,7 @@ function stopJsRefreshRateSampler() {
535
575
  appStateSub = null;
536
576
  }
537
577
 
538
- var version = "0.7.1";
578
+ var version = "0.9.0";
539
579
  var pkg = {
540
580
  version: version};
541
581
 
@@ -1208,7 +1248,7 @@ const CoralogixRum = {
1208
1248
  }
1209
1249
  CxSdk.endTimeMeasure(name);
1210
1250
  },
1211
- reportError: (error, isCrash, data) => {
1251
+ reportError: (error, isCrash, data, labels) => {
1212
1252
  if (!isInited) {
1213
1253
  logger.debug('CoralogixRum must be initiated before error reporting');
1214
1254
  return;
@@ -1222,6 +1262,9 @@ const CoralogixRum = {
1222
1262
  if (data && Object.keys(data).length > 0) {
1223
1263
  errorDetails.data = data;
1224
1264
  }
1265
+ if (labels && Object.keys(labels).length > 0) {
1266
+ errorDetails.labels = labels;
1267
+ }
1225
1268
  CxSdk.reportError(errorDetails);
1226
1269
  }
1227
1270
  };
@@ -1387,10 +1430,115 @@ const parseErrorStack = error => {
1387
1430
  return null;
1388
1431
  }).filter(frame => frame !== null);
1389
1432
  };
1433
+ const FATAL_FLUSH_MAX_WAIT_MS = 2500;
1434
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
1435
+
1436
+ /**
1437
+ * Best-effort delivery of a crash before a fatal error is handed back to RN's
1438
+ * default handler, which terminates the app in release builds. `flushSpans`
1439
+ * force-flushes the native batch processor; the native SDK uploads crash events
1440
+ * directly (bypassing the beforeSend round trip) and resolves once the upload
1441
+ * attempt finishes, so awaiting it is the full crash-delivery guarantee.
1442
+ * Never rejects; resolves within FATAL_FLUSH_MAX_WAIT_MS.
1443
+ *
1444
+ * @internal Used by the error instrumentation — not part of the public API.
1445
+ */
1446
+ const waitForFatalErrorFlush = async () => {
1447
+ const flush = Promise.resolve(CxSdk.flushSpans()).catch(e => logger.debug('flushSpans failed:', e));
1448
+ await Promise.race([flush, delay(FATAL_FLUSH_MAX_WAIT_MS)]);
1449
+ };
1390
1450
 
1391
1451
  // Create event emitter
1392
1452
  const eventEmitter = new NativeEventEmitter(CxSdk);
1393
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
+
1394
1542
  // Subscribe to beforeSend events
1395
1543
  const subscription = eventEmitter.addListener('onBeforeSend', events => {
1396
1544
  // logger.log("📨 onBeforeSend triggered with events: ", events)
@@ -1409,14 +1557,45 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1409
1557
  const results = [];
1410
1558
  for (const fullEvent of events) {
1411
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);
1412
1576
  const res = beforeSendCallback(event);
1413
1577
  if (!res) {
1414
1578
  continue;
1415
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
+
1416
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);
1417
1596
  const mergeEvent = _extends({}, fullEvent, {
1418
1597
  text: _extends({}, fullEvent.text, {
1419
- cx_rum: _extends({}, fullEvent.text.cx_rum, res)
1598
+ cx_rum: mergedCxRum
1420
1599
  })
1421
1600
  });
1422
1601
  results.push(mergeEvent);
@@ -1433,4 +1612,4 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1433
1612
  }
1434
1613
  });
1435
1614
 
1436
- export { CoralogixCustomSpan, CoralogixCustomTracer, CoralogixDomain, CoralogixGlobalSpan, CoralogixLogSeverity, CoralogixRum, CxSdk, SessionReplay, attachReactNavigationObserver, parseErrorStack };
1615
+ export { CoralogixCustomSpan, CoralogixCustomTracer, CoralogixDomain, CoralogixEventType, CoralogixGlobalSpan, CoralogixLogSeverity, CoralogixRum, CxSdk, SessionReplay, attachReactNavigationObserver, parseErrorStack, waitForFatalErrorFlush };
package/ios/CxSdk.mm CHANGED
@@ -64,6 +64,9 @@ RCT_EXTERN_METHOD(reportNetworkRequest:(NSDictionary *)networkRequestDictionary
64
64
  withResolver:(RCTPromiseResolveBlock)resolve
65
65
  withRejecter:(RCTPromiseRejectBlock)reject)
66
66
 
67
+ RCT_EXTERN_METHOD(flushSpans:(RCTPromiseResolveBlock)resolve
68
+ withRejecter:(RCTPromiseRejectBlock)reject)
69
+
67
70
  RCT_EXTERN_METHOD(sendCxSpanData:(NSArray *)beforeSendResults
68
71
  withResolver:(RCTPromiseResolveBlock)resolve
69
72
  withRejecter:(RCTPromiseRejectBlock)reject)
package/ios/CxSdk.swift CHANGED
@@ -237,18 +237,38 @@ 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
 
254
+ @objc(flushSpans:withRejecter:)
255
+ func flushSpans(resolve: @escaping RCTPromiseResolveBlock,
256
+ reject: @escaping RCTPromiseRejectBlock) -> Void {
257
+ // Force-flushes the batch processor and resolves once the export/upload
258
+ // attempt finishes — the same public SDK API the Android bridge calls.
259
+ // Invoked by the JS fatal-error handler before it hands over to RN's default
260
+ // handler, which terminates the process in release builds. `flush` handles
261
+ // its own threading and always invokes completion (immediately when the SDK
262
+ // is not initialized), so callers never hang.
263
+ guard let coralogixRum else {
264
+ resolve("flushSpans success")
265
+ return
266
+ }
267
+ coralogixRum.flush {
268
+ resolve("flushSpans success")
269
+ }
270
+ }
271
+
252
272
  @objc(sendCxSpanData:withResolver:withRejecter:)
253
273
  func sendCxSpanData(beforeSendResults: NSArray,
254
274
  resolve:RCTPromiseResolveBlock,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.7.1",
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
@@ -25,6 +25,14 @@ interface CxSdkClient {
25
25
  endTimeMeasure(name: string): void;
26
26
  reportError(details: Record<string, unknown>): void;
27
27
  reportNetworkRequest(details: NetworkRequestDetails): void;
28
+ /**
29
+ * Forces immediate export of natively queued telemetry. iOS drains the batch
30
+ * processor and resolves after the upload attempt; Android resolves
31
+ * immediately WITHOUT flushing (the native SDK exposes no flush API yet), so
32
+ * a resolved promise must not be read as delivery there — see
33
+ * waitForFatalErrorFlush for the Android fallback.
34
+ */
35
+ flushSpans(): Promise<string>;
28
36
  shutdown(): Promise<boolean>;
29
37
  createNewSession(): Promise<boolean>;
30
38
  sendCxSpanData(results: CxSpan[]): void;
@@ -56,11 +64,22 @@ export declare const CxSdk: CxSdkClient;
56
64
  export declare const CoralogixRum: CoralogixOtelWebType;
57
65
  export declare const SessionReplay: SessionReplayType;
58
66
  export declare const parseErrorStack: (error: Error) => CoralogixStackFrame[];
67
+ /**
68
+ * Best-effort delivery of a crash before a fatal error is handed back to RN's
69
+ * default handler, which terminates the app in release builds. `flushSpans`
70
+ * force-flushes the native batch processor; the native SDK uploads crash events
71
+ * directly (bypassing the beforeSend round trip) and resolves once the upload
72
+ * attempt finishes, so awaiting it is the full crash-delivery guarantee.
73
+ * Never rejects; resolves within FATAL_FLUSH_MAX_WAIT_MS.
74
+ *
75
+ * @internal Used by the error instrumentation — not part of the public API.
76
+ */
77
+ export declare const waitForFatalErrorFlush: () => Promise<void>;
59
78
  export { type ApplicationContextConfig } from './model/ApplicationContextConfig';
60
79
  export { type ViewContextConfig } from './model/ViewContextConfig';
61
80
  export { CoralogixDomain } from './model/CoralogixDomain';
62
81
  export { type UserContextConfig } from './model/UserContextConfig';
63
- 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';
64
83
  export { type CoralogixOtelWebOptionsInstrumentations } from './model/CoralogixOtelWebOptionsInstrumentations';
65
84
  export { type CustomMeasurement } from './model/CustomMeasurement';
66
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 {