@coralogix/react-native-plugin 0.2.11 → 0.3.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,21 @@
1
+ ## 0.3.0 (2026-03-10)
2
+
3
+ ### 🚀 Features
4
+
5
+ - 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
6
+ - Add `NetworkCaptureRule` type to public API for typed configuration
7
+ - Android: pass `networkExtraConfig` rules through native bridge to Android SDK
8
+ - JS: async fetch instrumentation with response payload capture via `Response.clone()`
9
+ - Android version bumped to 2.9.3
10
+ - iOS version bumped to 2.3.0
11
+
12
+ ### 🩹 Fixes
13
+
14
+ - add explicit types to Headers.forEach callback
15
+ - use Headers.forEach() instead of entries() for es2022 lib compat
16
+ - cast serializeNetworkCaptureRules return type to any[] for native bridge
17
+ - update CoralogixRum.init return type to Promise<void>
18
+
1
19
  ## 0.2.11 (2026-03-10)
2
20
 
3
21
  ### 🚀 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,
@@ -367,7 +372,8 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
367
372
  debug = if (hasKey("debug")) getBoolean("debug") else false,
368
373
  proxyUrl = getString("proxyUrl"),
369
374
  beforeSendCallback = ::beforeSendCallback,
370
- collectIPData = collectIpData
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.0";
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,8 @@ 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
776
892
  }));
777
893
  isInited = true;
778
894
  },
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.0";
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,8 @@ 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
774
890
  }));
775
891
  isInited = true;
776
892
  },
package/ios/CxSdk.swift CHANGED
@@ -163,7 +163,9 @@ class CxSdk: RCTEventEmitter {
163
163
  reject("Invalid networkRequestDictionary", "networkRequestDictionary is not a dictionary", nil)
164
164
  return
165
165
  }
166
+
166
167
  let cxNetworkDict = toCoralogixNetwork(dictionary: dictionary)
168
+ print("[CX DEBUG] cxNetworkDict response_payload: \( cxNetworkDict)")
167
169
  coralogixRum?.setNetworkRequestContext(dictionary: cxNetworkDict)
168
170
  resolve("reportNetworkRequest success")
169
171
  }
@@ -365,6 +367,38 @@ class CxSdk: RCTEventEmitter {
365
367
  }
366
368
  }
367
369
 
370
+ private func toNetworkCaptureRuleList(_ rules: [[String: Any]]?) -> [NetworkCaptureRule] {
371
+ guard let rules = rules else { return [] }
372
+ let result = rules.compactMap { dict -> NetworkCaptureRule? in
373
+ let reqHeaders = dict["reqHeaders"] as? [String]
374
+ let resHeaders = dict["resHeaders"] as? [String]
375
+ let collectReqPayload = dict["collectReqPayload"] as? Bool ?? false
376
+ let collectResPayload = dict["collectResPayload"] as? Bool ?? false
377
+ if let source = dict["urlPattern"] as? String {
378
+ let flags = dict["urlPatternFlags"] as? String ?? ""
379
+ var options: NSRegularExpression.Options = []
380
+ if flags.contains("i") { options.insert(.caseInsensitive) }
381
+ if flags.contains("m") { options.insert(.anchorsMatchLines) }
382
+ guard let regex = try? NSRegularExpression(pattern: source, options: options) else {
383
+ return nil
384
+ }
385
+ return NetworkCaptureRule(urlPattern: regex,
386
+ reqHeaders: reqHeaders,
387
+ resHeaders: resHeaders,
388
+ collectReqPayload: collectReqPayload,
389
+ collectResPayload: collectResPayload)
390
+ } else if let url = dict["url"] as? String {
391
+ return NetworkCaptureRule(url: url,
392
+ reqHeaders: reqHeaders,
393
+ resHeaders: resHeaders,
394
+ collectReqPayload: collectReqPayload,
395
+ collectResPayload: collectResPayload)
396
+ }
397
+ return nil
398
+ }
399
+ return result
400
+ }
401
+
368
402
  private func toCoralogixNetwork(dictionary: [String: Any]) -> [String: Any] {
369
403
  var result = [String: Any]()
370
404
  result["url"] = dictionary["url"] as? String ?? ""
@@ -376,6 +410,10 @@ class CxSdk: RCTEventEmitter {
376
410
  result["schema"] = dictionary["schema"] as? String ?? ""
377
411
  result["customTraceId"] = dictionary["customTraceId"] as? String ?? ""
378
412
  result["customSpanId"] = dictionary["customSpanId"] as? String ?? ""
413
+ if let v = dictionary["request_headers"] as? [String: String] { result["request_headers"] = v }
414
+ if let v = dictionary["response_headers"] as? [String: String] { result["response_headers"] = v }
415
+ if let v = dictionary["request_payload"] as? String { result["request_payload"] = v }
416
+ if let v = dictionary["response_payload"] as? String { result["response_payload"] = v }
379
417
  return result
380
418
  }
381
419
 
@@ -420,7 +458,11 @@ class CxSdk: RCTEventEmitter {
420
458
  userMetadata: userMetadata)
421
459
  let ignoreUrls = (parameter["ignoreUrls"] as? [Any])?.compactMap { $0 as? String } ?? []
422
460
  let ignoreError = (parameter["ignoreErrors"] as? [Any])?.compactMap { $0 as? String } ?? []
423
- let options = CoralogixExporterOptions(coralogixDomain: coralogixDomain,
461
+ let rawNetworkExtraConfig = parameter["networkExtraConfig"] as? [[String: Any]]
462
+ let networkExtraConfig = toNetworkCaptureRuleList(rawNetworkExtraConfig)
463
+
464
+ let options = CoralogixExporterOptions(
465
+ coralogixDomain: coralogixDomain,
424
466
  userContext: coralogixUser,
425
467
  environment: parameter["environment"] as? String ?? "",
426
468
  application: application,
@@ -434,6 +476,7 @@ class CxSdk: RCTEventEmitter {
434
476
  proxyUrl: parameter["proxyUrl"] as? String ?? nil,
435
477
  traceParentInHeader: mapTraceParentInHeader(parameter["traceParentInHeader"] as? [String: Any]),
436
478
  mobileVitals: mobileVitalsDict,
479
+ networkExtraConfig: networkExtraConfig,
437
480
  debug: parameter["debug"] as? Bool ?? true)
438
481
 
439
482
  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.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
@@ -46,7 +46,7 @@ export { type ApplicationContextConfig } from './model/ApplicationContextConfig'
46
46
  export { type ViewContextConfig } from './model/ViewContextConfig';
47
47
  export { CoralogixDomain } from './model/CoralogixDomain';
48
48
  export { type UserContextConfig } from './model/UserContextConfig';
49
- export { type CoralogixBrowserSdkConfig, CoralogixLogSeverity, } from './model/Types';
49
+ export { type CoralogixBrowserSdkConfig, type NetworkCaptureRule, CoralogixLogSeverity, } from './model/Types';
50
50
  export { type CoralogixOtelWebOptionsInstrumentations } from './model/CoralogixOtelWebOptionsInstrumentations';
51
51
  export { type CustomMeasurement } from './model/CustomMeasurement';
52
52
  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
  */