@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 +18 -0
- package/CxSdk.podspec +3 -3
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/cxsdk/CxSdkModule.kt +44 -2
- package/index.cjs.js +119 -3
- package/index.esm.js +119 -3
- package/ios/CxSdk.swift +44 -1
- package/package.json +1 -1
- package/src/index.d.ts +1 -1
- package/src/instrumentations/network/networkCaptureUtils.d.ts +22 -0
- package/src/model/NetworkRequestDetails.d.ts +4 -0
- package/src/model/Types.d.ts +17 -1
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.
|
|
20
|
-
s.dependency 'CoralogixInternal','2.
|
|
21
|
-
s.dependency 'SessionReplay','2.
|
|
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.
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
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
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
|
};
|
package/src/model/Types.d.ts
CHANGED
|
@@ -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
|
*/
|