@coralogix/react-native-plugin 0.2.11 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## 0.3.1 (2026-03-19)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - Pass `hasBeforeSend` flag to native on init so `beforeSendCallback` is set to null when not configured, avoiding an unnecessary JS round-trip for every span ([CX-35463](https://coralogix.atlassian.net/browse/CX-35463))
6
+
7
+ ## 0.3.0 (2026-03-10)
8
+
9
+ ### 🚀 Features
10
+
11
+ - Add network payload and header capture support ([CX-32504](https://coralogix.atlassian.net/browse/CX-32504)): configure `networkExtraConfig` rules to capture request/response headers and payloads per URL pattern
12
+ - Add `NetworkCaptureRule` type to public API for typed configuration
13
+ - Android: pass `networkExtraConfig` rules through native bridge to Android SDK
14
+ - JS: async fetch instrumentation with response payload capture via `Response.clone()`
15
+ - Android version bumped to 2.9.3
16
+ - iOS version bumped to 2.3.0
17
+
18
+ ### 🩹 Fixes
19
+
20
+ - add explicit types to Headers.forEach callback
21
+ - use Headers.forEach() instead of entries() for es2022 lib compat
22
+ - cast serializeNetworkCaptureRules return type to any[] for native bridge
23
+ - update CoralogixRum.init return type to Promise<void>
24
+
1
25
  ## 0.2.11 (2026-03-10)
2
26
 
3
27
  ### 🚀 Features
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.2.0'
20
- s.dependency 'CoralogixInternal','2.2.0'
21
- s.dependency 'SessionReplay','2.2.0'
19
+ s.dependency 'Coralogix','2.3.0'
20
+ s.dependency 'CoralogixInternal','2.3.0'
21
+ s.dependency 'SessionReplay','2.3.0'
22
22
 
23
23
  # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
24
24
  # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
@@ -75,7 +75,7 @@ dependencies {
75
75
  implementation "com.facebook.react:react-android"
76
76
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
77
 
78
- implementation "com.coralogix:android-sdk:2.9.0"
78
+ implementation "com.coralogix:android-sdk:2.9.3"
79
79
  }
80
80
 
81
81
  react {
@@ -17,6 +17,7 @@ import com.coralogix.android.sdk.model.HybridMetric
17
17
  import com.coralogix.android.sdk.model.UserInteractionDetails
18
18
  import com.coralogix.android.sdk.model.Instrumentation
19
19
  import com.coralogix.android.sdk.model.MobileVitalType
20
+ import com.coralogix.android.sdk.model.NetworkCaptureRule
20
21
  import com.coralogix.android.sdk.model.TraceParentInHeaderConfig
21
22
  import com.coralogix.android.sdk.model.TraceParentInHeaderConfigOptions
22
23
  import com.coralogix.android.sdk.model.UserContext
@@ -349,6 +350,10 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
349
350
 
350
351
  val collectIpData = if (hasKey("collectIPData")) getBoolean("collectIPData") else true
351
352
 
353
+ val networkExtraConfig = if (hasKey("networkExtraConfig") && !isNull("networkExtraConfig"))
354
+ getArray("networkExtraConfig")?.toNetworkCaptureRuleList() ?: emptyList()
355
+ else emptyList()
356
+
352
357
  return CoralogixOptions(
353
358
  applicationName = applicationName,
354
359
  coralogixDomain = coralogixDomain,
@@ -366,8 +371,9 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
366
371
  traceParentInHeader = traceParentInHeaderConfig,
367
372
  debug = if (hasKey("debug")) getBoolean("debug") else false,
368
373
  proxyUrl = getString("proxyUrl"),
369
- beforeSendCallback = ::beforeSendCallback,
370
- collectIPData = collectIpData
374
+ beforeSendCallback = if (hasKey("hasBeforeSend") && getBoolean("hasBeforeSend")) ::beforeSendCallback else null,
375
+ collectIPData = collectIpData,
376
+ networkCaptureConfig = networkExtraConfig
371
377
  )
372
378
  }
373
379
 
@@ -713,9 +719,45 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
713
719
  getDouble("responseContentLength").toLong() else 0,
714
720
  errorMessage = getString("errorMessage"),
715
721
  traceId = getString("customTraceId"),
716
- spanId = getString("customSpanId")
722
+ spanId = getString("customSpanId"),
723
+ requestHeaders = if (hasKey("request_headers") && !isNull("request_headers"))
724
+ getMap("request_headers")?.toStringMap() else null,
725
+ responseHeaders = if (hasKey("response_headers") && !isNull("response_headers"))
726
+ getMap("response_headers")?.toStringMap() else null,
727
+ requestPayload = if (hasKey("request_payload") && !isNull("request_payload"))
728
+ getString("request_payload") else null,
729
+ responsePayload = if (hasKey("response_payload") && !isNull("response_payload"))
730
+ getString("response_payload") else null
717
731
  ).also { Log.d("CxSdkModule", "toNetworkRequestDetails: $it") }
718
732
 
733
+ private fun ReadableArray.toNetworkCaptureRuleList(): List<NetworkCaptureRule> {
734
+ val result = mutableListOf<NetworkCaptureRule>()
735
+ for (i in 0 until size()) {
736
+ val map = getMap(i) ?: continue
737
+ result.add(NetworkCaptureRule(
738
+ url = if (map.hasKey("url") && !map.isNull("url")) map.getString("url") else null,
739
+ urlPattern = if (map.hasKey("urlPattern") && !map.isNull("urlPattern")) {
740
+ val source = map.getString("urlPattern") ?: ""
741
+ val flags = if (map.hasKey("urlPatternFlags") && !map.isNull("urlPatternFlags"))
742
+ map.getString("urlPatternFlags") ?: "" else ""
743
+ val options = mutableSetOf<RegexOption>()
744
+ if (flags.contains('i')) options.add(RegexOption.IGNORE_CASE)
745
+ if (flags.contains('m')) options.add(RegexOption.MULTILINE)
746
+ source.toRegex(options)
747
+ } else null,
748
+ reqHeaders = if (map.hasKey("reqHeaders") && !map.isNull("reqHeaders"))
749
+ map.getArray("reqHeaders")?.toStringList() ?: emptyList()
750
+ else emptyList(),
751
+ resHeaders = if (map.hasKey("resHeaders") && !map.isNull("resHeaders"))
752
+ map.getArray("resHeaders")?.toStringList() ?: emptyList()
753
+ else emptyList(),
754
+ collectReqPayload = if (map.hasKey("collectReqPayload")) map.getBoolean("collectReqPayload") else false,
755
+ collectResPayload = if (map.hasKey("collectResPayload")) map.getBoolean("collectResPayload") else false,
756
+ ))
757
+ }
758
+ return result
759
+ }
760
+
719
761
  private fun ReadableArray.toHybridMetricList(): List<HybridMetric> {
720
762
  val out = ArrayList<HybridMetric>(size())
721
763
  for (i in 0 until size()) {
package/index.cjs.js CHANGED
@@ -523,7 +523,7 @@ function stopJsRefreshRateSampler() {
523
523
  appStateSub = null;
524
524
  }
525
525
 
526
- var version = "0.2.11";
526
+ var version = "0.3.1";
527
527
  var pkg = {
528
528
  version: version};
529
529
 
@@ -622,6 +622,52 @@ let OtelNetworkAttrs = /*#__PURE__*/function (OtelNetworkAttrs) {
622
622
  return OtelNetworkAttrs;
623
623
  }({});
624
624
 
625
+ /**
626
+ * Returns the first rule whose `urlPattern` regex or exact `url` matches the
627
+ * given URL. Returns undefined if no rule matches or the list is empty.
628
+ */
629
+ function resolveNetworkCaptureRule(url, rules) {
630
+ return rules.find(rule => rule.urlPattern ? rule.urlPattern.test(url) : rule.url === url);
631
+ }
632
+
633
+ /**
634
+ * Filters a headers map to only the keys present in `allowlist`.
635
+ * Matching is case-insensitive; output keys use the casing from `allowlist`.
636
+ */
637
+ function filterHeaders(headers, allowlist) {
638
+ const result = {};
639
+ for (const [key, value] of Object.entries(headers)) {
640
+ const configKey = allowlist.find(k => k.toLowerCase() === key.toLowerCase());
641
+ if (configKey !== undefined) result[configKey] = value;
642
+ }
643
+ return result;
644
+ }
645
+
646
+ /**
647
+ * Returns `body` if it is within `maxChars`, otherwise returns `undefined`.
648
+ * Bodies over the limit are dropped entirely — never truncated.
649
+ */
650
+ function applyPayloadLimit(body, maxChars = 1024) {
651
+ return body.length <= maxChars ? body : undefined;
652
+ }
653
+
654
+ /**
655
+ * Converts NetworkCaptureRule[] to a form that can cross the JS→native bridge.
656
+ * RegExp cannot be serialized directly, so `urlPattern` is split into its
657
+ * `source` and `flags` strings; the native side reconstructs the regex.
658
+ */
659
+ function serializeNetworkCaptureRules(rules) {
660
+ return rules.map(rule => {
661
+ var _rule$urlPattern, _rule$urlPattern2;
662
+ return _extends({}, rule, {
663
+ urlPattern: (_rule$urlPattern = rule.urlPattern) == null ? void 0 : _rule$urlPattern.source,
664
+ // Strip 'g' and 'y' flags — stateful flags cause RegExp.lastIndex side-effects
665
+ // when the native side calls .test() repeatedly on a shared instance.
666
+ urlPatternFlags: (_rule$urlPattern2 = rule.urlPattern) == null ? void 0 : _rule$urlPattern2.flags.replace(/[gy]/g, '')
667
+ });
668
+ });
669
+ }
670
+
625
671
  class CoralogixFetchInstrumentation extends instrumentationFetch.FetchInstrumentation {
626
672
  constructor(config) {
627
673
  var _traceParentInHeader$;
@@ -635,7 +681,9 @@ class CoralogixFetchInstrumentation extends instrumentationFetch.FetchInstrument
635
681
  // If enabled and user didn't provide urls -> match all
636
682
  propagateTraceHeaderCorsUrls: enabled ? urls != null ? urls : /.*/ : allowNone()
637
683
  };
638
- fetchConfig.applyCustomAttributesOnSpan = (span, request, result) => {
684
+ fetchConfig.applyCustomAttributesOnSpan = async function (span, request, result) {
685
+ // span.end() must remain synchronous and first — this completes the OTel span
686
+ // regardless of any async work that follows for capture enrichment.
639
687
  span.end();
640
688
  const readableSpan = span;
641
689
  const attrs = readableSpan.attributes;
@@ -667,6 +715,66 @@ class CoralogixFetchInstrumentation extends instrumentationFetch.FetchInstrument
667
715
  customTraceId,
668
716
  customSpanId
669
717
  };
718
+ const networkExtraConfig = config.networkExtraConfig;
719
+ if (networkExtraConfig != null && networkExtraConfig.length) {
720
+ const rule = resolveNetworkCaptureRule(url, networkExtraConfig);
721
+ if (rule) {
722
+ var _rule$reqHeaders, _rule$resHeaders;
723
+ // Request headers
724
+ if ((_rule$reqHeaders = rule.reqHeaders) != null && _rule$reqHeaders.length) {
725
+ let rawHeaders;
726
+ if (request instanceof Request) {
727
+ rawHeaders = headersToRecord(request.headers);
728
+ } else {
729
+ const h = request.headers;
730
+ if (h instanceof Headers) {
731
+ rawHeaders = headersToRecord(h);
732
+ } else if (Array.isArray(h)) {
733
+ rawHeaders = Object.fromEntries(h);
734
+ } else {
735
+ var _ref;
736
+ rawHeaders = (_ref = h) != null ? _ref : {};
737
+ }
738
+ }
739
+ const filtered = filterHeaders(rawHeaders, rule.reqHeaders);
740
+ if (Object.keys(filtered).length > 0) details.request_headers = filtered;
741
+ }
742
+
743
+ // Response headers
744
+ if ((_rule$resHeaders = rule.resHeaders) != null && _rule$resHeaders.length && result instanceof Response) {
745
+ const filtered = filterHeaders(headersToRecord(result.headers), rule.resHeaders);
746
+ if (Object.keys(filtered).length > 0) details.response_headers = filtered;
747
+ }
748
+
749
+ // Request payload — string bodies only (FormData/Blob/ReadableStream skipped)
750
+ if (rule.collectReqPayload) {
751
+ let reqBody;
752
+ if (request instanceof Request) {
753
+ try {
754
+ reqBody = await request.clone().text();
755
+ } catch (_unused) {/* consumed or unavailable */}
756
+ } else {
757
+ const body = request == null ? void 0 : request.body;
758
+ if (typeof body === 'string') reqBody = body;
759
+ }
760
+ if (reqBody !== undefined) {
761
+ const limited = applyPayloadLimit(reqBody);
762
+ if (limited !== undefined) details.request_payload = limited;
763
+ }
764
+ }
765
+
766
+ // Response payload — clone before body is consumed by app code (best-effort)
767
+ if (rule.collectResPayload && result instanceof Response && !result.bodyUsed) {
768
+ try {
769
+ const text = await result.clone().text();
770
+ const limited = applyPayloadLimit(text);
771
+ if (limited !== undefined) details.response_payload = limited;
772
+ } catch (_unused2) {/* consumed or unavailable */}
773
+ }
774
+ }
775
+ }
776
+
777
+ // reportNetworkRequest must always be last, after all awaits
670
778
  CoralogixRum.reportNetworkRequest(details);
671
779
  };
672
780
  super(fetchConfig);
@@ -679,6 +787,13 @@ function allowNone() {
679
787
  // matches nothing
680
788
  return /^$/;
681
789
  }
790
+ function headersToRecord(headers) {
791
+ const result = {};
792
+ headers.forEach((value, key) => {
793
+ result[key] = value;
794
+ });
795
+ return result;
796
+ }
682
797
 
683
798
  function isSessionReplayOptionsValid(options) {
684
799
  const scaleValid = options.captureScale > 0 && options.captureScale <= 1;
@@ -772,7 +887,9 @@ const CoralogixRum = {
772
887
  finalOptions = resolvedOptions;
773
888
  }
774
889
  await CxSdk.initialize(_extends({}, finalOptions, {
775
- frameworkVersion: pkg.version
890
+ frameworkVersion: pkg.version,
891
+ networkExtraConfig: finalOptions.networkExtraConfig ? serializeNetworkCaptureRules(finalOptions.networkExtraConfig) : undefined,
892
+ hasBeforeSend: !!finalOptions.beforeSend
776
893
  }));
777
894
  isInited = true;
778
895
  },
@@ -1071,10 +1188,11 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1071
1188
  logger.debug('⚠️ Skipping event processing - SDK not initialized or events invalid');
1072
1189
  return;
1073
1190
  }
1074
- logger.debug('🔄 Processing events with beforeSendCallback');
1191
+
1075
1192
  // Process event with beforeSendCallback if available
1076
1193
  if (beforeSendCallback) {
1077
1194
  try {
1195
+ logger.debug('🔄 Processing events with beforeSendCallback');
1078
1196
  // Process each event in the array
1079
1197
  const results = [];
1080
1198
  for (const fullEvent of events) {
package/index.esm.js CHANGED
@@ -521,7 +521,7 @@ function stopJsRefreshRateSampler() {
521
521
  appStateSub = null;
522
522
  }
523
523
 
524
- var version = "0.2.11";
524
+ var version = "0.3.1";
525
525
  var pkg = {
526
526
  version: version};
527
527
 
@@ -620,6 +620,52 @@ let OtelNetworkAttrs = /*#__PURE__*/function (OtelNetworkAttrs) {
620
620
  return OtelNetworkAttrs;
621
621
  }({});
622
622
 
623
+ /**
624
+ * Returns the first rule whose `urlPattern` regex or exact `url` matches the
625
+ * given URL. Returns undefined if no rule matches or the list is empty.
626
+ */
627
+ function resolveNetworkCaptureRule(url, rules) {
628
+ return rules.find(rule => rule.urlPattern ? rule.urlPattern.test(url) : rule.url === url);
629
+ }
630
+
631
+ /**
632
+ * Filters a headers map to only the keys present in `allowlist`.
633
+ * Matching is case-insensitive; output keys use the casing from `allowlist`.
634
+ */
635
+ function filterHeaders(headers, allowlist) {
636
+ const result = {};
637
+ for (const [key, value] of Object.entries(headers)) {
638
+ const configKey = allowlist.find(k => k.toLowerCase() === key.toLowerCase());
639
+ if (configKey !== undefined) result[configKey] = value;
640
+ }
641
+ return result;
642
+ }
643
+
644
+ /**
645
+ * Returns `body` if it is within `maxChars`, otherwise returns `undefined`.
646
+ * Bodies over the limit are dropped entirely — never truncated.
647
+ */
648
+ function applyPayloadLimit(body, maxChars = 1024) {
649
+ return body.length <= maxChars ? body : undefined;
650
+ }
651
+
652
+ /**
653
+ * Converts NetworkCaptureRule[] to a form that can cross the JS→native bridge.
654
+ * RegExp cannot be serialized directly, so `urlPattern` is split into its
655
+ * `source` and `flags` strings; the native side reconstructs the regex.
656
+ */
657
+ function serializeNetworkCaptureRules(rules) {
658
+ return rules.map(rule => {
659
+ var _rule$urlPattern, _rule$urlPattern2;
660
+ return _extends({}, rule, {
661
+ urlPattern: (_rule$urlPattern = rule.urlPattern) == null ? void 0 : _rule$urlPattern.source,
662
+ // Strip 'g' and 'y' flags — stateful flags cause RegExp.lastIndex side-effects
663
+ // when the native side calls .test() repeatedly on a shared instance.
664
+ urlPatternFlags: (_rule$urlPattern2 = rule.urlPattern) == null ? void 0 : _rule$urlPattern2.flags.replace(/[gy]/g, '')
665
+ });
666
+ });
667
+ }
668
+
623
669
  class CoralogixFetchInstrumentation extends FetchInstrumentation {
624
670
  constructor(config) {
625
671
  var _traceParentInHeader$;
@@ -633,7 +679,9 @@ class CoralogixFetchInstrumentation extends FetchInstrumentation {
633
679
  // If enabled and user didn't provide urls -> match all
634
680
  propagateTraceHeaderCorsUrls: enabled ? urls != null ? urls : /.*/ : allowNone()
635
681
  };
636
- fetchConfig.applyCustomAttributesOnSpan = (span, request, result) => {
682
+ fetchConfig.applyCustomAttributesOnSpan = async function (span, request, result) {
683
+ // span.end() must remain synchronous and first — this completes the OTel span
684
+ // regardless of any async work that follows for capture enrichment.
637
685
  span.end();
638
686
  const readableSpan = span;
639
687
  const attrs = readableSpan.attributes;
@@ -665,6 +713,66 @@ class CoralogixFetchInstrumentation extends FetchInstrumentation {
665
713
  customTraceId,
666
714
  customSpanId
667
715
  };
716
+ const networkExtraConfig = config.networkExtraConfig;
717
+ if (networkExtraConfig != null && networkExtraConfig.length) {
718
+ const rule = resolveNetworkCaptureRule(url, networkExtraConfig);
719
+ if (rule) {
720
+ var _rule$reqHeaders, _rule$resHeaders;
721
+ // Request headers
722
+ if ((_rule$reqHeaders = rule.reqHeaders) != null && _rule$reqHeaders.length) {
723
+ let rawHeaders;
724
+ if (request instanceof Request) {
725
+ rawHeaders = headersToRecord(request.headers);
726
+ } else {
727
+ const h = request.headers;
728
+ if (h instanceof Headers) {
729
+ rawHeaders = headersToRecord(h);
730
+ } else if (Array.isArray(h)) {
731
+ rawHeaders = Object.fromEntries(h);
732
+ } else {
733
+ var _ref;
734
+ rawHeaders = (_ref = h) != null ? _ref : {};
735
+ }
736
+ }
737
+ const filtered = filterHeaders(rawHeaders, rule.reqHeaders);
738
+ if (Object.keys(filtered).length > 0) details.request_headers = filtered;
739
+ }
740
+
741
+ // Response headers
742
+ if ((_rule$resHeaders = rule.resHeaders) != null && _rule$resHeaders.length && result instanceof Response) {
743
+ const filtered = filterHeaders(headersToRecord(result.headers), rule.resHeaders);
744
+ if (Object.keys(filtered).length > 0) details.response_headers = filtered;
745
+ }
746
+
747
+ // Request payload — string bodies only (FormData/Blob/ReadableStream skipped)
748
+ if (rule.collectReqPayload) {
749
+ let reqBody;
750
+ if (request instanceof Request) {
751
+ try {
752
+ reqBody = await request.clone().text();
753
+ } catch (_unused) {/* consumed or unavailable */}
754
+ } else {
755
+ const body = request == null ? void 0 : request.body;
756
+ if (typeof body === 'string') reqBody = body;
757
+ }
758
+ if (reqBody !== undefined) {
759
+ const limited = applyPayloadLimit(reqBody);
760
+ if (limited !== undefined) details.request_payload = limited;
761
+ }
762
+ }
763
+
764
+ // Response payload — clone before body is consumed by app code (best-effort)
765
+ if (rule.collectResPayload && result instanceof Response && !result.bodyUsed) {
766
+ try {
767
+ const text = await result.clone().text();
768
+ const limited = applyPayloadLimit(text);
769
+ if (limited !== undefined) details.response_payload = limited;
770
+ } catch (_unused2) {/* consumed or unavailable */}
771
+ }
772
+ }
773
+ }
774
+
775
+ // reportNetworkRequest must always be last, after all awaits
668
776
  CoralogixRum.reportNetworkRequest(details);
669
777
  };
670
778
  super(fetchConfig);
@@ -677,6 +785,13 @@ function allowNone() {
677
785
  // matches nothing
678
786
  return /^$/;
679
787
  }
788
+ function headersToRecord(headers) {
789
+ const result = {};
790
+ headers.forEach((value, key) => {
791
+ result[key] = value;
792
+ });
793
+ return result;
794
+ }
680
795
 
681
796
  function isSessionReplayOptionsValid(options) {
682
797
  const scaleValid = options.captureScale > 0 && options.captureScale <= 1;
@@ -770,7 +885,9 @@ const CoralogixRum = {
770
885
  finalOptions = resolvedOptions;
771
886
  }
772
887
  await CxSdk.initialize(_extends({}, finalOptions, {
773
- frameworkVersion: pkg.version
888
+ frameworkVersion: pkg.version,
889
+ networkExtraConfig: finalOptions.networkExtraConfig ? serializeNetworkCaptureRules(finalOptions.networkExtraConfig) : undefined,
890
+ hasBeforeSend: !!finalOptions.beforeSend
774
891
  }));
775
892
  isInited = true;
776
893
  },
@@ -1069,10 +1186,11 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1069
1186
  logger.debug('⚠️ Skipping event processing - SDK not initialized or events invalid');
1070
1187
  return;
1071
1188
  }
1072
- logger.debug('🔄 Processing events with beforeSendCallback');
1189
+
1073
1190
  // Process event with beforeSendCallback if available
1074
1191
  if (beforeSendCallback) {
1075
1192
  try {
1193
+ logger.debug('🔄 Processing events with beforeSendCallback');
1076
1194
  // Process each event in the array
1077
1195
  const results = [];
1078
1196
  for (const fullEvent of events) {
package/ios/CxSdk.swift CHANGED
@@ -29,16 +29,13 @@ class CxSdk: RCTEventEmitter {
29
29
  resolve:RCTPromiseResolveBlock,
30
30
  reject:RCTPromiseRejectBlock) -> Void {
31
31
  do {
32
- // Create beforeSendCallback only if parameter["beforeSend"] is not null
33
- let beforeSendCallBack: (([[String: Any]]) -> Void)? = { [weak self] (event: [[String: Any]]) -> Void in
34
- // Convert the event dictionary to a format that
35
- // can be safely passed to JavaScript
32
+ let hasBeforeSend = parameter["hasBeforeSend"] as? Bool ?? false
33
+ let beforeSendCallBack: (([[String: Any]]) -> Void)? = hasBeforeSend ? { [weak self] (event: [[String: Any]]) -> Void in
36
34
  let jsEvent = event.map { (element) in
37
35
  return self?.convertToJSCompatibleEvent(event: element)
38
36
  }
39
-
40
37
  self?.sendEvent(withName: "onBeforeSend", body: jsEvent)
41
- }
38
+ } : nil
42
39
 
43
40
  var options = try self.toCoralogixOptions(parameter: parameter)
44
41
  options.beforeSendCallBack = beforeSendCallBack
@@ -163,7 +160,9 @@ class CxSdk: RCTEventEmitter {
163
160
  reject("Invalid networkRequestDictionary", "networkRequestDictionary is not a dictionary", nil)
164
161
  return
165
162
  }
163
+
166
164
  let cxNetworkDict = toCoralogixNetwork(dictionary: dictionary)
165
+ print("[CX DEBUG] cxNetworkDict response_payload: \( cxNetworkDict)")
167
166
  coralogixRum?.setNetworkRequestContext(dictionary: cxNetworkDict)
168
167
  resolve("reportNetworkRequest success")
169
168
  }
@@ -365,6 +364,38 @@ class CxSdk: RCTEventEmitter {
365
364
  }
366
365
  }
367
366
 
367
+ private func toNetworkCaptureRuleList(_ rules: [[String: Any]]?) -> [NetworkCaptureRule] {
368
+ guard let rules = rules else { return [] }
369
+ let result = rules.compactMap { dict -> NetworkCaptureRule? in
370
+ let reqHeaders = dict["reqHeaders"] as? [String]
371
+ let resHeaders = dict["resHeaders"] as? [String]
372
+ let collectReqPayload = dict["collectReqPayload"] as? Bool ?? false
373
+ let collectResPayload = dict["collectResPayload"] as? Bool ?? false
374
+ if let source = dict["urlPattern"] as? String {
375
+ let flags = dict["urlPatternFlags"] as? String ?? ""
376
+ var options: NSRegularExpression.Options = []
377
+ if flags.contains("i") { options.insert(.caseInsensitive) }
378
+ if flags.contains("m") { options.insert(.anchorsMatchLines) }
379
+ guard let regex = try? NSRegularExpression(pattern: source, options: options) else {
380
+ return nil
381
+ }
382
+ return NetworkCaptureRule(urlPattern: regex,
383
+ reqHeaders: reqHeaders,
384
+ resHeaders: resHeaders,
385
+ collectReqPayload: collectReqPayload,
386
+ collectResPayload: collectResPayload)
387
+ } else if let url = dict["url"] as? String {
388
+ return NetworkCaptureRule(url: url,
389
+ reqHeaders: reqHeaders,
390
+ resHeaders: resHeaders,
391
+ collectReqPayload: collectReqPayload,
392
+ collectResPayload: collectResPayload)
393
+ }
394
+ return nil
395
+ }
396
+ return result
397
+ }
398
+
368
399
  private func toCoralogixNetwork(dictionary: [String: Any]) -> [String: Any] {
369
400
  var result = [String: Any]()
370
401
  result["url"] = dictionary["url"] as? String ?? ""
@@ -376,6 +407,10 @@ class CxSdk: RCTEventEmitter {
376
407
  result["schema"] = dictionary["schema"] as? String ?? ""
377
408
  result["customTraceId"] = dictionary["customTraceId"] as? String ?? ""
378
409
  result["customSpanId"] = dictionary["customSpanId"] as? String ?? ""
410
+ if let v = dictionary["request_headers"] as? [String: String] { result["request_headers"] = v }
411
+ if let v = dictionary["response_headers"] as? [String: String] { result["response_headers"] = v }
412
+ if let v = dictionary["request_payload"] as? String { result["request_payload"] = v }
413
+ if let v = dictionary["response_payload"] as? String { result["response_payload"] = v }
379
414
  return result
380
415
  }
381
416
 
@@ -420,7 +455,11 @@ class CxSdk: RCTEventEmitter {
420
455
  userMetadata: userMetadata)
421
456
  let ignoreUrls = (parameter["ignoreUrls"] as? [Any])?.compactMap { $0 as? String } ?? []
422
457
  let ignoreError = (parameter["ignoreErrors"] as? [Any])?.compactMap { $0 as? String } ?? []
423
- let options = CoralogixExporterOptions(coralogixDomain: coralogixDomain,
458
+ let rawNetworkExtraConfig = parameter["networkExtraConfig"] as? [[String: Any]]
459
+ let networkExtraConfig = toNetworkCaptureRuleList(rawNetworkExtraConfig)
460
+
461
+ let options = CoralogixExporterOptions(
462
+ coralogixDomain: coralogixDomain,
424
463
  userContext: coralogixUser,
425
464
  environment: parameter["environment"] as? String ?? "",
426
465
  application: application,
@@ -434,6 +473,7 @@ class CxSdk: RCTEventEmitter {
434
473
  proxyUrl: parameter["proxyUrl"] as? String ?? nil,
435
474
  traceParentInHeader: mapTraceParentInHeader(parameter["traceParentInHeader"] as? [String: Any]),
436
475
  mobileVitals: mobileVitalsDict,
476
+ networkExtraConfig: networkExtraConfig,
437
477
  debug: parameter["debug"] as? Bool ?? true)
438
478
 
439
479
  return options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.2.11",
3
+ "version": "0.3.1",
4
4
  "description": "Official Coralogix React Native plugin",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Coralogix",
package/src/index.d.ts CHANGED
@@ -9,6 +9,7 @@ import { SessionReplayOptions, SessionReplayType } from './model/SessionReplayTy
9
9
  interface CxSdkClient {
10
10
  initialize(options: CoralogixBrowserSdkConfig & {
11
11
  frameworkVersion?: string;
12
+ hasBeforeSend?: boolean;
12
13
  }): Promise<boolean>;
13
14
  setUserContext(userContext: UserContextConfig): void;
14
15
  getUserContext(): Promise<UserContextConfig>;
@@ -46,7 +47,7 @@ export { type ApplicationContextConfig } from './model/ApplicationContextConfig'
46
47
  export { type ViewContextConfig } from './model/ViewContextConfig';
47
48
  export { CoralogixDomain } from './model/CoralogixDomain';
48
49
  export { type UserContextConfig } from './model/UserContextConfig';
49
- export { type CoralogixBrowserSdkConfig, CoralogixLogSeverity, } from './model/Types';
50
+ export { type CoralogixBrowserSdkConfig, type NetworkCaptureRule, CoralogixLogSeverity, } from './model/Types';
50
51
  export { type CoralogixOtelWebOptionsInstrumentations } from './model/CoralogixOtelWebOptionsInstrumentations';
51
52
  export { type CustomMeasurement } from './model/CustomMeasurement';
52
53
  export { type NetworkRequestDetails } from './model/NetworkRequestDetails';
@@ -0,0 +1,22 @@
1
+ import { NetworkCaptureRule } from '../../model/Types';
2
+ /**
3
+ * Returns the first rule whose `urlPattern` regex or exact `url` matches the
4
+ * given URL. Returns undefined if no rule matches or the list is empty.
5
+ */
6
+ export declare function resolveNetworkCaptureRule(url: string, rules: NetworkCaptureRule[]): NetworkCaptureRule | undefined;
7
+ /**
8
+ * Filters a headers map to only the keys present in `allowlist`.
9
+ * Matching is case-insensitive; output keys use the casing from `allowlist`.
10
+ */
11
+ export declare function filterHeaders(headers: Record<string, string>, allowlist: string[]): Record<string, string>;
12
+ /**
13
+ * Returns `body` if it is within `maxChars`, otherwise returns `undefined`.
14
+ * Bodies over the limit are dropped entirely — never truncated.
15
+ */
16
+ export declare function applyPayloadLimit(body: string, maxChars?: number): string | undefined;
17
+ /**
18
+ * Converts NetworkCaptureRule[] to a form that can cross the JS→native bridge.
19
+ * RegExp cannot be serialized directly, so `urlPattern` is split into its
20
+ * `source` and `flags` strings; the native side reconstructs the regex.
21
+ */
22
+ export declare function serializeNetworkCaptureRules(rules: NetworkCaptureRule[]): any[];
@@ -11,4 +11,8 @@ export type NetworkRequestDetails = {
11
11
  errorMessage?: string | null;
12
12
  customTraceId: string;
13
13
  customSpanId: string;
14
+ request_headers?: Record<string, string>;
15
+ response_headers?: Record<string, string>;
16
+ request_payload?: string;
17
+ response_payload?: string;
14
18
  };
@@ -133,10 +133,24 @@ export interface CxRumEvent {
133
133
  timestamp: number;
134
134
  isSnapshotEvent?: boolean;
135
135
  }
136
- export interface EditableCxRumEvent extends Omit<CxRumEvent, 'session_context' | 'timestamp'> {
136
+ export interface EditableCxRumEvent extends Omit<CxRumEvent, 'session_context' | 'timestamp' | 'snapshot_context'> {
137
137
  session_context: Pick<SessionContext, keyof UserMetadata>;
138
138
  }
139
139
  export type BeforeSendResult = EditableCxRumEvent | null;
140
+ export interface NetworkCaptureRule {
141
+ /** Exact-string URL match. One of `url` or `urlPattern` is required. */
142
+ url?: string;
143
+ /** Regex URL match. One of `url` or `urlPattern` is required. */
144
+ urlPattern?: RegExp;
145
+ /** Allowlisted request header names to capture (case-insensitive). */
146
+ reqHeaders?: string[];
147
+ /** Allowlisted response header names to capture (case-insensitive). */
148
+ resHeaders?: string[];
149
+ /** Capture request body (string bodies only, ≤1024 chars). Default: false. */
150
+ collectReqPayload?: boolean;
151
+ /** Capture response body (best-effort, ≤1024 chars). Default: false. */
152
+ collectResPayload?: boolean;
153
+ }
140
154
  export interface TraceParentInHeader {
141
155
  enabled: boolean;
142
156
  options?: {
@@ -192,6 +206,8 @@ export interface CoralogixBrowserSdkConfig {
192
206
  beforeSend?: (event: EditableCxRumEvent) => BeforeSendResult;
193
207
  /** Send requests through a proxy */
194
208
  proxyUrl?: string;
209
+ /** Rules for capturing request/response headers and payloads per URL. */
210
+ networkExtraConfig?: NetworkCaptureRule[];
195
211
  /**
196
212
  * JS refresh rate metric collection configuration.
197
213
  */