@coralogix/react-native-plugin 0.2.10 β†’ 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,30 @@
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
+
19
+ ## 0.2.11 (2026-03-10)
20
+
21
+ ### πŸš€ Features
22
+
23
+ - Add automatic user interaction instrumentation (clicks, scrolls, swipes) for React Native components ([CX-33494](https://coralogix.atlassian.net/browse/CX-33494))
24
+ - Add native bridge method `reportUserInteraction` for Android and iOS
25
+ - Bump Android native SDK to 2.9.0
26
+ - Bump iOS native SDK (Coralogix/CoralogixInternal/SessionReplay) to 2.2.0
27
+
1
28
  ## 0.2.10 (2026-02-22)
2
29
 
3
30
  ### 🩹 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.0.0'
20
- s.dependency 'CoralogixInternal','2.0.0'
21
- s.dependency 'SessionReplay','2.0.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.7.2"
78
+ implementation "com.coralogix:android-sdk:2.9.3"
79
79
  }
80
80
 
81
81
  react {
@@ -14,8 +14,10 @@ import com.coralogix.android.sdk.model.CoralogixLogSeverity
14
14
  import com.coralogix.android.sdk.model.CoralogixOptions
15
15
  import com.coralogix.android.sdk.model.Framework
16
16
  import com.coralogix.android.sdk.model.HybridMetric
17
+ import com.coralogix.android.sdk.model.UserInteractionDetails
17
18
  import com.coralogix.android.sdk.model.Instrumentation
18
19
  import com.coralogix.android.sdk.model.MobileVitalType
20
+ import com.coralogix.android.sdk.model.NetworkCaptureRule
19
21
  import com.coralogix.android.sdk.model.TraceParentInHeaderConfig
20
22
  import com.coralogix.android.sdk.model.TraceParentInHeaderConfigOptions
21
23
  import com.coralogix.android.sdk.model.UserContext
@@ -195,6 +197,27 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
195
197
  }
196
198
  }
197
199
 
200
+ @ReactMethod
201
+ fun reportUserInteraction(interaction: ReadableMap) {
202
+ val attributesMap = if (interaction.hasKey("attributes") && !interaction.isNull("attributes"))
203
+ interaction.getMap("attributes") else null
204
+
205
+ val details = UserInteractionDetails(
206
+ type = interaction.getString("type") ?: run {
207
+ Log.w("CxSdkModule", "reportUserInteraction: missing required field 'type', dropping interaction")
208
+ return
209
+ },
210
+ direction = if (interaction.hasKey("direction") && !interaction.isNull("direction")) interaction.getString("direction") else null,
211
+ targetElement = if (interaction.hasKey("target_element") && !interaction.isNull("target_element")) interaction.getString("target_element") else null,
212
+ elementClasses = if (interaction.hasKey("element_classes") && !interaction.isNull("element_classes")) interaction.getString("element_classes") else null,
213
+ targetId = if (interaction.hasKey("target_id") && !interaction.isNull("target_id")) interaction.getString("target_id") else null,
214
+ innerText = if (interaction.hasKey("inner_text") && !interaction.isNull("inner_text")) interaction.getString("inner_text") else null,
215
+ x = if (attributesMap?.hasKey("x") == true && attributesMap.isNull("x") == false) attributesMap.getDouble("x") else null,
216
+ y = if (attributesMap?.hasKey("y") == true && attributesMap.isNull("y") == false) attributesMap.getDouble("y") else null,
217
+ )
218
+ CoralogixRum.reportUserInteraction(details)
219
+ }
220
+
198
221
  @ReactMethod
199
222
  fun addListener(eventName: String) {
200
223
  Log.d("CxSdkModule", "addListener called with eventName: $eventName")
@@ -327,6 +350,10 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
327
350
 
328
351
  val collectIpData = if (hasKey("collectIPData")) getBoolean("collectIPData") else true
329
352
 
353
+ val networkExtraConfig = if (hasKey("networkExtraConfig") && !isNull("networkExtraConfig"))
354
+ getArray("networkExtraConfig")?.toNetworkCaptureRuleList() ?: emptyList()
355
+ else emptyList()
356
+
330
357
  return CoralogixOptions(
331
358
  applicationName = applicationName,
332
359
  coralogixDomain = coralogixDomain,
@@ -345,7 +372,8 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
345
372
  debug = if (hasKey("debug")) getBoolean("debug") else false,
346
373
  proxyUrl = getString("proxyUrl"),
347
374
  beforeSendCallback = ::beforeSendCallback,
348
- collectIPData = collectIpData
375
+ collectIPData = collectIpData,
376
+ networkCaptureConfig = networkExtraConfig
349
377
  )
350
378
  }
351
379
 
@@ -691,9 +719,45 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
691
719
  getDouble("responseContentLength").toLong() else 0,
692
720
  errorMessage = getString("errorMessage"),
693
721
  traceId = getString("customTraceId"),
694
- 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
695
731
  ).also { Log.d("CxSdkModule", "toNetworkRequestDetails: $it") }
696
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
+
697
761
  private fun ReadableArray.toHybridMetricList(): List<HybridMetric> {
698
762
  val out = ArrayList<HybridMetric>(size())
699
763
  for (i in 0 until size()) {
package/index.cjs.js CHANGED
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  var reactNative = require('react-native');
4
- var instrumentationFetch = require('@opentelemetry/instrumentation-fetch');
4
+ var React = require('react');
5
5
  var instrumentation = require('@opentelemetry/instrumentation');
6
+ var instrumentationFetch = require('@opentelemetry/instrumentation-fetch');
6
7
  var sdkTraceWeb = require('@opentelemetry/sdk-trace-web');
7
8
  var core = require('@opentelemetry/core');
8
9
 
@@ -126,6 +127,308 @@ class CoralogixErrorInstrumentation extends instrumentation.InstrumentationBase
126
127
  disable() {}
127
128
  }
128
129
 
130
+ const USER_INTERACTION_INSTRUMENTATION_NAME = 'user-interaction';
131
+ const USER_INTERACTION_INSTRUMENTATION_VERSION = '1';
132
+
133
+ // After onScrollEndDrag, how long to wait before classifying as scroll.
134
+ // If onMomentumScrollBegin fires within this window, it's a swipe instead.
135
+ const SWIPE_MOMENTUM_TIMEOUT_MS = 80;
136
+
137
+ // Drag speed (px/ms) below which a gesture is classified as a deliberate scroll.
138
+ // Computed from position delta and elapsed time in onScrollEndDrag; works on all platforms.
139
+ // ~0.5 px/ms β‰ˆ 300px in 600ms (slow deliberate drag); a fling is typically >1 px/ms.
140
+ const SWIPE_VELOCITY_THRESHOLD = 0.5;
141
+
142
+ // Mirrors Android SDK's direction inference: compare |dx| vs |dy|, sign gives direction.
143
+ // contentOffset delta: dy > 0 = user scrolled DOWN, dx > 0 = user scrolled RIGHT.
144
+ function inferDirectionFromDelta(dx, dy) {
145
+ if (Math.abs(dx) > Math.abs(dy)) {
146
+ return dx > 0 ? 'right' : 'left';
147
+ }
148
+ return dy > 0 ? 'down' : 'up';
149
+ }
150
+
151
+ // Shallow-recursive text extraction from React children.
152
+ // Handles: string, number, arrays, and React elements with props.children.
153
+ function extractInnerText(children) {
154
+ var _children$props;
155
+ if (children === null || children === undefined) return undefined;
156
+ if (typeof children === 'string') return children.trim() || undefined;
157
+ if (typeof children === 'number') return String(children);
158
+ if (Array.isArray(children)) {
159
+ const parts = children.map(extractInnerText).filter(Boolean);
160
+ return parts.length > 0 ? parts.join(' ') : undefined;
161
+ }
162
+ if ((children == null || (_children$props = children.props) == null ? void 0 : _children$props.children) !== undefined) {
163
+ return extractInnerText(children.props.children);
164
+ }
165
+ return undefined;
166
+ }
167
+
168
+ let isEnabled = false;
169
+ // No-op default so reportInteraction never needs a null-check.
170
+ // Set to the real callback by the constructor and cleared on disable().
171
+ let onInteraction = () => {};
172
+
173
+ // Module-level scroll state shared across all ScrollView instances.
174
+ // Hooks cannot be used in our forwardRef wrappers because the plugin's React import
175
+ // and the renderer's React are different instances in a consuming app β€” hooks rely on
176
+ // ReactCurrentDispatcher which is instance-specific. Non-hook APIs (forwardRef,
177
+ // createElement) work fine because they use Symbol.for() which is global.
178
+ // Concurrent scrolling of two ScrollViews is extremely rare on mobile, so shared
179
+ // state is acceptable here.
180
+ let scrollState = {
181
+ startOffset: null,
182
+ startTime: null,
183
+ timeout: null
184
+ };
185
+ function reportInteraction(ctx) {
186
+ if (!isEnabled) return;
187
+ onInteraction(ctx);
188
+ }
189
+ function makeInstrumentedPressable(OrigComp, componentName) {
190
+ const InstrumentedComp = /*#__PURE__*/React.forwardRef(function InstrumentedPressable(props, ref) {
191
+ const wrappedProps = props.onPress ? _extends({}, props, {
192
+ onPress: e => {
193
+ var _props$accessibilityL, _extractInnerText;
194
+ const {
195
+ pageX,
196
+ pageY
197
+ } = e.nativeEvent;
198
+ reportInteraction({
199
+ type: 'click',
200
+ attributes: {
201
+ x: Math.round(pageX * 100) / 100,
202
+ y: Math.round(pageY * 100) / 100
203
+ },
204
+ element_classes: componentName,
205
+ target_element: (_props$accessibilityL = props.accessibilityLabel) != null ? _props$accessibilityL : componentName,
206
+ target_id: props.testID,
207
+ inner_text: (_extractInnerText = extractInnerText(props.children)) != null ? _extractInnerText : props.title
208
+ });
209
+ props.onPress(e);
210
+ }
211
+ }) : props;
212
+ return /*#__PURE__*/React.createElement(OrigComp, _extends({}, wrappedProps, {
213
+ ref
214
+ }));
215
+ });
216
+ InstrumentedComp.displayName = componentName;
217
+ return InstrumentedComp;
218
+ }
219
+ function makeInstrumentedSwitch(OrigComp) {
220
+ const InstrumentedSwitch = /*#__PURE__*/React.forwardRef(function InstrumentedSwitch(props, ref) {
221
+ const wrappedProps = props.onValueChange ? _extends({}, props, {
222
+ onValueChange: value => {
223
+ var _props$accessibilityL2;
224
+ reportInteraction({
225
+ type: 'click',
226
+ element_classes: 'Switch',
227
+ target_element: (_props$accessibilityL2 = props.accessibilityLabel) != null ? _props$accessibilityL2 : 'Switch',
228
+ target_id: props.testID
229
+ });
230
+ props.onValueChange(value);
231
+ }
232
+ }) : props;
233
+ return /*#__PURE__*/React.createElement(OrigComp, _extends({}, wrappedProps, {
234
+ ref
235
+ }));
236
+ });
237
+ InstrumentedSwitch.displayName = 'Switch';
238
+ return InstrumentedSwitch;
239
+ }
240
+ function makeInstrumentedScrollView(OrigComp) {
241
+ const InstrumentedScrollView = /*#__PURE__*/React.forwardRef(function InstrumentedScrollView(props, ref) {
242
+ const wrappedProps = _extends({}, props, {
243
+ onScrollBeginDrag: e => {
244
+ scrollState.startOffset = _extends({}, e.nativeEvent.contentOffset);
245
+ scrollState.startTime = Date.now();
246
+ props.onScrollBeginDrag == null || props.onScrollBeginDrag(e);
247
+ },
248
+ onScrollEndDrag: e => {
249
+ var _scrollState$timeout;
250
+ const endOffset = _extends({}, e.nativeEvent.contentOffset);
251
+ // Capture start state immediately β€” scrollState may be mutated by a
252
+ // concurrent ScrollView's onScrollBeginDrag before the timeout fires.
253
+ const {
254
+ startOffset,
255
+ startTime
256
+ } = scrollState;
257
+ clearTimeout((_scrollState$timeout = scrollState.timeout) != null ? _scrollState$timeout : undefined);
258
+
259
+ // Compute drag speed (px/ms) to distinguish a deliberate scroll from a fling.
260
+ // Works on both iOS and Android without relying on native velocity fields.
261
+ let classified = false;
262
+ if (startOffset !== null && startTime !== null) {
263
+ const elapsed = Date.now() - startTime;
264
+ const dx = endOffset.x - startOffset.x;
265
+ const dy = endOffset.y - startOffset.y;
266
+ const speed = elapsed > 0 ? Math.sqrt(dx * dx + dy * dy) / elapsed : 0;
267
+ if (speed < SWIPE_VELOCITY_THRESHOLD) {
268
+ // Slow deliberate drag β†’ report scroll immediately.
269
+ reportInteraction({
270
+ type: 'scroll',
271
+ target_element: 'ScrollView',
272
+ direction: inferDirectionFromDelta(dx, dy)
273
+ });
274
+ scrollState = {
275
+ startOffset: null,
276
+ startTime: null,
277
+ timeout: null
278
+ };
279
+ classified = true;
280
+ }
281
+ }
282
+ if (!classified) {
283
+ // Fast drag β†’ wait for onMomentumScrollBegin to classify as swipe.
284
+ // If momentum doesn't fire within the window, fall back to scroll.
285
+ // Uses captured startOffset (not scrollState.startOffset) to avoid
286
+ // reading stale state if another scroll begins before the timeout fires.
287
+ scrollState.timeout = setTimeout(() => {
288
+ if (startOffset) {
289
+ reportInteraction({
290
+ type: 'scroll',
291
+ target_element: 'ScrollView',
292
+ direction: inferDirectionFromDelta(endOffset.x - startOffset.x, endOffset.y - startOffset.y)
293
+ });
294
+ }
295
+ scrollState = {
296
+ startOffset: null,
297
+ startTime: null,
298
+ timeout: null
299
+ };
300
+ }, SWIPE_MOMENTUM_TIMEOUT_MS);
301
+ }
302
+ props.onScrollEndDrag == null || props.onScrollEndDrag(e);
303
+ },
304
+ onMomentumScrollBegin: e => {
305
+ var _scrollState$timeout2;
306
+ clearTimeout((_scrollState$timeout2 = scrollState.timeout) != null ? _scrollState$timeout2 : undefined);
307
+ const currentOffset = _extends({}, e.nativeEvent.contentOffset);
308
+ const {
309
+ startOffset
310
+ } = scrollState;
311
+ if (startOffset) {
312
+ reportInteraction({
313
+ type: 'swipe',
314
+ target_element: 'ScrollView',
315
+ direction: inferDirectionFromDelta(currentOffset.x - startOffset.x, currentOffset.y - startOffset.y)
316
+ });
317
+ }
318
+ scrollState = {
319
+ startOffset: null,
320
+ startTime: null,
321
+ timeout: null
322
+ };
323
+ props.onMomentumScrollBegin == null || props.onMomentumScrollBegin(e);
324
+ }
325
+ });
326
+ return /*#__PURE__*/React.createElement(OrigComp, _extends({}, wrappedProps, {
327
+ ref
328
+ }));
329
+ });
330
+ InstrumentedScrollView.displayName = 'ScrollView';
331
+
332
+ // Copy static properties from OrigComp (e.g. ScrollView.Context used by VirtualizedList/FlatList).
333
+ // React.forwardRef returns a plain object so these statics are lost without explicit copying.
334
+ const skipKeys = new Set(['$$typeof', 'render', 'displayName']);
335
+ Object.getOwnPropertyNames(OrigComp).forEach(key => {
336
+ if (skipKeys.has(key)) return;
337
+ try {
338
+ const descriptor = Object.getOwnPropertyDescriptor(OrigComp, key);
339
+ if (descriptor) Object.defineProperty(InstrumentedScrollView, key, descriptor);
340
+ } catch (_unused) {/* ignore non-configurable descriptors */}
341
+ });
342
+ return InstrumentedScrollView;
343
+ }
344
+
345
+ // react-native is in Metro's nonInlinedRequires list, so app code reads properties
346
+ // like _rn.Pressable from the shared module object at each render. Mutating the
347
+ // module object here (via Object.defineProperty) means every subsequent property
348
+ // read returns our wrappers.
349
+ //
350
+ // react-native/index.js defines exports as getter-only accessors with no setters,
351
+ // so direct assignment is silently ignored. Object.defineProperty with a data
352
+ // descriptor converts the accessor to a writable data property.
353
+ const RN = require('react-native');
354
+ const originals = {
355
+ Button: RN.Button,
356
+ Pressable: RN.Pressable,
357
+ TouchableOpacity: RN.TouchableOpacity,
358
+ TouchableHighlight: RN.TouchableHighlight,
359
+ TouchableNativeFeedback: RN.TouchableNativeFeedback,
360
+ TouchableWithoutFeedback: RN.TouchableWithoutFeedback,
361
+ Switch: RN.Switch,
362
+ ScrollView: RN.ScrollView
363
+ };
364
+
365
+ // Wrapper components are created once from the originals captured above.
366
+ // They are patched into / restored from the RN module in enable() / disable().
367
+ const instrumented = {
368
+ Button: makeInstrumentedPressable(originals.Button, 'Button'),
369
+ Pressable: makeInstrumentedPressable(originals.Pressable, 'Pressable'),
370
+ TouchableOpacity: makeInstrumentedPressable(originals.TouchableOpacity, 'TouchableOpacity'),
371
+ TouchableHighlight: makeInstrumentedPressable(originals.TouchableHighlight, 'TouchableHighlight'),
372
+ TouchableNativeFeedback: makeInstrumentedPressable(originals.TouchableNativeFeedback, 'TouchableNativeFeedback'),
373
+ TouchableWithoutFeedback: makeInstrumentedPressable(originals.TouchableWithoutFeedback, 'TouchableWithoutFeedback'),
374
+ Switch: makeInstrumentedSwitch(originals.Switch),
375
+ ScrollView: makeInstrumentedScrollView(originals.ScrollView)
376
+ };
377
+ function defineComp(key, value) {
378
+ Object.defineProperty(RN, key, {
379
+ value,
380
+ writable: true,
381
+ configurable: true,
382
+ enumerable: true
383
+ });
384
+ }
385
+ function patchComponents() {
386
+ defineComp('Button', instrumented.Button);
387
+ defineComp('Pressable', instrumented.Pressable);
388
+ defineComp('TouchableOpacity', instrumented.TouchableOpacity);
389
+ defineComp('TouchableHighlight', instrumented.TouchableHighlight);
390
+ defineComp('TouchableNativeFeedback', instrumented.TouchableNativeFeedback);
391
+ defineComp('TouchableWithoutFeedback', instrumented.TouchableWithoutFeedback);
392
+ defineComp('Switch', instrumented.Switch);
393
+ defineComp('ScrollView', instrumented.ScrollView);
394
+ }
395
+ function restoreComponents() {
396
+ defineComp('Button', originals.Button);
397
+ defineComp('Pressable', originals.Pressable);
398
+ defineComp('TouchableOpacity', originals.TouchableOpacity);
399
+ defineComp('TouchableHighlight', originals.TouchableHighlight);
400
+ defineComp('TouchableNativeFeedback', originals.TouchableNativeFeedback);
401
+ defineComp('TouchableWithoutFeedback', originals.TouchableWithoutFeedback);
402
+ defineComp('Switch', originals.Switch);
403
+ defineComp('ScrollView', originals.ScrollView);
404
+ }
405
+ class UserInteractionInstrumentation extends instrumentation.InstrumentationBase {
406
+ constructor(config) {
407
+ super(USER_INTERACTION_INSTRUMENTATION_NAME, USER_INTERACTION_INSTRUMENTATION_VERSION, {});
408
+ onInteraction = config.onInteraction;
409
+ }
410
+
411
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
412
+ init() {}
413
+ enable() {
414
+ isEnabled = true;
415
+ patchComponents();
416
+ }
417
+ disable() {
418
+ isEnabled = false;
419
+ onInteraction = () => {};
420
+ restoreComponents();
421
+ if (scrollState.timeout) {
422
+ clearTimeout(scrollState.timeout);
423
+ }
424
+ scrollState = {
425
+ startOffset: null,
426
+ startTime: null,
427
+ timeout: null
428
+ };
429
+ }
430
+ }
431
+
129
432
  const DEFAULT_SAMPLE_DURATION_MS = 5000;
130
433
  const DEFAULT_SAMPLE_INTERVAL_MS = 60000;
131
434
  let isSampling = false;
@@ -220,7 +523,7 @@ function stopJsRefreshRateSampler() {
220
523
  appStateSub = null;
221
524
  }
222
525
 
223
- var version = "0.2.10";
526
+ var version = "0.3.0";
224
527
  var pkg = {
225
528
  version: version};
226
529
 
@@ -319,6 +622,52 @@ let OtelNetworkAttrs = /*#__PURE__*/function (OtelNetworkAttrs) {
319
622
  return OtelNetworkAttrs;
320
623
  }({});
321
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
+
322
671
  class CoralogixFetchInstrumentation extends instrumentationFetch.FetchInstrumentation {
323
672
  constructor(config) {
324
673
  var _traceParentInHeader$;
@@ -332,7 +681,9 @@ class CoralogixFetchInstrumentation extends instrumentationFetch.FetchInstrument
332
681
  // If enabled and user didn't provide urls -> match all
333
682
  propagateTraceHeaderCorsUrls: enabled ? urls != null ? urls : /.*/ : allowNone()
334
683
  };
335
- 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.
336
687
  span.end();
337
688
  const readableSpan = span;
338
689
  const attrs = readableSpan.attributes;
@@ -364,6 +715,66 @@ class CoralogixFetchInstrumentation extends instrumentationFetch.FetchInstrument
364
715
  customTraceId,
365
716
  customSpanId
366
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
367
778
  CoralogixRum.reportNetworkRequest(details);
368
779
  };
369
780
  super(fetchConfig);
@@ -376,6 +787,13 @@ function allowNone() {
376
787
  // matches nothing
377
788
  return /^$/;
378
789
  }
790
+ function headersToRecord(headers) {
791
+ const result = {};
792
+ headers.forEach((value, key) => {
793
+ result[key] = value;
794
+ });
795
+ return result;
796
+ }
379
797
 
380
798
  function isSessionReplayOptionsValid(options) {
381
799
  const scaleValid = options.captureScale > 0 && options.captureScale <= 1;
@@ -469,7 +887,8 @@ const CoralogixRum = {
469
887
  finalOptions = resolvedOptions;
470
888
  }
471
889
  await CxSdk.initialize(_extends({}, finalOptions, {
472
- frameworkVersion: pkg.version
890
+ frameworkVersion: pkg.version,
891
+ networkExtraConfig: finalOptions.networkExtraConfig ? serializeNetworkCaptureRules(finalOptions.networkExtraConfig) : undefined
473
892
  }));
474
893
  isInited = true;
475
894
  },
@@ -596,6 +1015,10 @@ const CoralogixRum = {
596
1015
  const SessionReplay = {
597
1016
  init: async options => {
598
1017
  logger.debug("session replay: init called with options: ", options);
1018
+ if (!CoralogixRum.isInited) {
1019
+ logger.warn("SessionReplay.init called before CoralogixRum is initialized. Call and await CoralogixRum.init() first to avoid initialization errors.");
1020
+ return false;
1021
+ }
599
1022
  const optionsValid = isSessionReplayOptionsValid(options);
600
1023
  if (!optionsValid) {
601
1024
  logger.warn("invalid options in SessionReplay.init: ", options);
@@ -709,6 +1132,20 @@ async function registerCoralogixInstrumentations(options) {
709
1132
  trackMobileVitals(options);
710
1133
  }
711
1134
 
1135
+ // User interaction instrumentation (clicks, scrolls, swipes)
1136
+ const shouldInterceptUserInteractions = !instrumentationsOptions || instrumentationsOptions.user_interaction !== false;
1137
+ if (shouldInterceptUserInteractions) {
1138
+ instrumentations.push(new UserInteractionInstrumentation({
1139
+ onInteraction: ctx => {
1140
+ // isInited becomes true only after CxSdk.initialize() resolves,
1141
+ // so this guard drops any interactions that fire in the brief window
1142
+ // between instrumentation registration and native SDK init completing.
1143
+ if (!isInited) return;
1144
+ CxSdk.reportUserInteraction(ctx);
1145
+ }
1146
+ }));
1147
+ }
1148
+
712
1149
  // Register Instrumentations
713
1150
  _deregisterInstrumentations = instrumentation.registerInstrumentations({
714
1151
  tracerProvider,