@coralogix/react-native-plugin 0.2.10 → 0.2.11
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 +9 -0
- package/CxSdk.podspec +3 -3
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/cxsdk/CxSdkModule.kt +22 -0
- package/index.cjs.js +323 -2
- package/index.esm.js +323 -2
- package/ios/CxSdk.mm +2 -0
- package/ios/CxSdk.swift +36 -8
- package/package.json +1 -1
- package/src/index.d.ts +2 -1
- package/src/instrumentations/user-interaction/UserInteractionInstrumentation.d.ts +10 -0
- package/src/instrumentations/user-interaction/consts.d.ts +4 -0
- package/src/instrumentations/user-interaction/gestureUtils.d.ts +3 -0
- package/src/instrumentations/user-interaction/index.d.ts +1 -0
- package/src/model/CoralogixOtelWebType.d.ts +2 -1
- package/src/model/Types.d.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.2.11 (2026-03-10)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- Add automatic user interaction instrumentation (clicks, scrolls, swipes) for React Native components ([CX-33494](https://coralogix.atlassian.net/browse/CX-33494))
|
|
6
|
+
- Add native bridge method `reportUserInteraction` for Android and iOS
|
|
7
|
+
- Bump Android native SDK to 2.9.0
|
|
8
|
+
- Bump iOS native SDK (Coralogix/CoralogixInternal/SessionReplay) to 2.2.0
|
|
9
|
+
|
|
1
10
|
## 0.2.10 (2026-02-22)
|
|
2
11
|
|
|
3
12
|
### 🩹 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.
|
|
20
|
-
s.dependency 'CoralogixInternal','2.
|
|
21
|
-
s.dependency 'SessionReplay','2.
|
|
19
|
+
s.dependency 'Coralogix','2.2.0'
|
|
20
|
+
s.dependency 'CoralogixInternal','2.2.0'
|
|
21
|
+
s.dependency 'SessionReplay','2.2.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.
|
|
78
|
+
implementation "com.coralogix:android-sdk:2.9.0"
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
react {
|
|
@@ -14,6 +14,7 @@ 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
|
|
19
20
|
import com.coralogix.android.sdk.model.TraceParentInHeaderConfig
|
|
@@ -195,6 +196,27 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
@ReactMethod
|
|
200
|
+
fun reportUserInteraction(interaction: ReadableMap) {
|
|
201
|
+
val attributesMap = if (interaction.hasKey("attributes") && !interaction.isNull("attributes"))
|
|
202
|
+
interaction.getMap("attributes") else null
|
|
203
|
+
|
|
204
|
+
val details = UserInteractionDetails(
|
|
205
|
+
type = interaction.getString("type") ?: run {
|
|
206
|
+
Log.w("CxSdkModule", "reportUserInteraction: missing required field 'type', dropping interaction")
|
|
207
|
+
return
|
|
208
|
+
},
|
|
209
|
+
direction = if (interaction.hasKey("direction") && !interaction.isNull("direction")) interaction.getString("direction") else null,
|
|
210
|
+
targetElement = if (interaction.hasKey("target_element") && !interaction.isNull("target_element")) interaction.getString("target_element") else null,
|
|
211
|
+
elementClasses = if (interaction.hasKey("element_classes") && !interaction.isNull("element_classes")) interaction.getString("element_classes") else null,
|
|
212
|
+
targetId = if (interaction.hasKey("target_id") && !interaction.isNull("target_id")) interaction.getString("target_id") else null,
|
|
213
|
+
innerText = if (interaction.hasKey("inner_text") && !interaction.isNull("inner_text")) interaction.getString("inner_text") else null,
|
|
214
|
+
x = if (attributesMap?.hasKey("x") == true && attributesMap.isNull("x") == false) attributesMap.getDouble("x") else null,
|
|
215
|
+
y = if (attributesMap?.hasKey("y") == true && attributesMap.isNull("y") == false) attributesMap.getDouble("y") else null,
|
|
216
|
+
)
|
|
217
|
+
CoralogixRum.reportUserInteraction(details)
|
|
218
|
+
}
|
|
219
|
+
|
|
198
220
|
@ReactMethod
|
|
199
221
|
fun addListener(eventName: String) {
|
|
200
222
|
Log.d("CxSdkModule", "addListener called with eventName: $eventName")
|
package/index.cjs.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var reactNative = require('react-native');
|
|
4
|
-
var
|
|
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.
|
|
526
|
+
var version = "0.2.11";
|
|
224
527
|
var pkg = {
|
|
225
528
|
version: version};
|
|
226
529
|
|
|
@@ -596,6 +899,10 @@ const CoralogixRum = {
|
|
|
596
899
|
const SessionReplay = {
|
|
597
900
|
init: async options => {
|
|
598
901
|
logger.debug("session replay: init called with options: ", options);
|
|
902
|
+
if (!CoralogixRum.isInited) {
|
|
903
|
+
logger.warn("SessionReplay.init called before CoralogixRum is initialized. Call and await CoralogixRum.init() first to avoid initialization errors.");
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
599
906
|
const optionsValid = isSessionReplayOptionsValid(options);
|
|
600
907
|
if (!optionsValid) {
|
|
601
908
|
logger.warn("invalid options in SessionReplay.init: ", options);
|
|
@@ -709,6 +1016,20 @@ async function registerCoralogixInstrumentations(options) {
|
|
|
709
1016
|
trackMobileVitals(options);
|
|
710
1017
|
}
|
|
711
1018
|
|
|
1019
|
+
// User interaction instrumentation (clicks, scrolls, swipes)
|
|
1020
|
+
const shouldInterceptUserInteractions = !instrumentationsOptions || instrumentationsOptions.user_interaction !== false;
|
|
1021
|
+
if (shouldInterceptUserInteractions) {
|
|
1022
|
+
instrumentations.push(new UserInteractionInstrumentation({
|
|
1023
|
+
onInteraction: ctx => {
|
|
1024
|
+
// isInited becomes true only after CxSdk.initialize() resolves,
|
|
1025
|
+
// so this guard drops any interactions that fire in the brief window
|
|
1026
|
+
// between instrumentation registration and native SDK init completing.
|
|
1027
|
+
if (!isInited) return;
|
|
1028
|
+
CxSdk.reportUserInteraction(ctx);
|
|
1029
|
+
}
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
712
1033
|
// Register Instrumentations
|
|
713
1034
|
_deregisterInstrumentations = instrumentation.registerInstrumentations({
|
|
714
1035
|
tracerProvider,
|
package/index.esm.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AppState, Platform, NativeModules, NativeEventEmitter, findNodeHandle } from 'react-native';
|
|
2
|
-
import
|
|
2
|
+
import React from 'react';
|
|
3
3
|
import { InstrumentationBase, registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
4
|
+
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
4
5
|
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
|
|
5
6
|
import { W3CTraceContextPropagator } from '@opentelemetry/core';
|
|
6
7
|
|
|
@@ -124,6 +125,308 @@ class CoralogixErrorInstrumentation extends InstrumentationBase {
|
|
|
124
125
|
disable() {}
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
const USER_INTERACTION_INSTRUMENTATION_NAME = 'user-interaction';
|
|
129
|
+
const USER_INTERACTION_INSTRUMENTATION_VERSION = '1';
|
|
130
|
+
|
|
131
|
+
// After onScrollEndDrag, how long to wait before classifying as scroll.
|
|
132
|
+
// If onMomentumScrollBegin fires within this window, it's a swipe instead.
|
|
133
|
+
const SWIPE_MOMENTUM_TIMEOUT_MS = 80;
|
|
134
|
+
|
|
135
|
+
// Drag speed (px/ms) below which a gesture is classified as a deliberate scroll.
|
|
136
|
+
// Computed from position delta and elapsed time in onScrollEndDrag; works on all platforms.
|
|
137
|
+
// ~0.5 px/ms ≈ 300px in 600ms (slow deliberate drag); a fling is typically >1 px/ms.
|
|
138
|
+
const SWIPE_VELOCITY_THRESHOLD = 0.5;
|
|
139
|
+
|
|
140
|
+
// Mirrors Android SDK's direction inference: compare |dx| vs |dy|, sign gives direction.
|
|
141
|
+
// contentOffset delta: dy > 0 = user scrolled DOWN, dx > 0 = user scrolled RIGHT.
|
|
142
|
+
function inferDirectionFromDelta(dx, dy) {
|
|
143
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
144
|
+
return dx > 0 ? 'right' : 'left';
|
|
145
|
+
}
|
|
146
|
+
return dy > 0 ? 'down' : 'up';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Shallow-recursive text extraction from React children.
|
|
150
|
+
// Handles: string, number, arrays, and React elements with props.children.
|
|
151
|
+
function extractInnerText(children) {
|
|
152
|
+
var _children$props;
|
|
153
|
+
if (children === null || children === undefined) return undefined;
|
|
154
|
+
if (typeof children === 'string') return children.trim() || undefined;
|
|
155
|
+
if (typeof children === 'number') return String(children);
|
|
156
|
+
if (Array.isArray(children)) {
|
|
157
|
+
const parts = children.map(extractInnerText).filter(Boolean);
|
|
158
|
+
return parts.length > 0 ? parts.join(' ') : undefined;
|
|
159
|
+
}
|
|
160
|
+
if ((children == null || (_children$props = children.props) == null ? void 0 : _children$props.children) !== undefined) {
|
|
161
|
+
return extractInnerText(children.props.children);
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let isEnabled = false;
|
|
167
|
+
// No-op default so reportInteraction never needs a null-check.
|
|
168
|
+
// Set to the real callback by the constructor and cleared on disable().
|
|
169
|
+
let onInteraction = () => {};
|
|
170
|
+
|
|
171
|
+
// Module-level scroll state shared across all ScrollView instances.
|
|
172
|
+
// Hooks cannot be used in our forwardRef wrappers because the plugin's React import
|
|
173
|
+
// and the renderer's React are different instances in a consuming app — hooks rely on
|
|
174
|
+
// ReactCurrentDispatcher which is instance-specific. Non-hook APIs (forwardRef,
|
|
175
|
+
// createElement) work fine because they use Symbol.for() which is global.
|
|
176
|
+
// Concurrent scrolling of two ScrollViews is extremely rare on mobile, so shared
|
|
177
|
+
// state is acceptable here.
|
|
178
|
+
let scrollState = {
|
|
179
|
+
startOffset: null,
|
|
180
|
+
startTime: null,
|
|
181
|
+
timeout: null
|
|
182
|
+
};
|
|
183
|
+
function reportInteraction(ctx) {
|
|
184
|
+
if (!isEnabled) return;
|
|
185
|
+
onInteraction(ctx);
|
|
186
|
+
}
|
|
187
|
+
function makeInstrumentedPressable(OrigComp, componentName) {
|
|
188
|
+
const InstrumentedComp = /*#__PURE__*/React.forwardRef(function InstrumentedPressable(props, ref) {
|
|
189
|
+
const wrappedProps = props.onPress ? _extends({}, props, {
|
|
190
|
+
onPress: e => {
|
|
191
|
+
var _props$accessibilityL, _extractInnerText;
|
|
192
|
+
const {
|
|
193
|
+
pageX,
|
|
194
|
+
pageY
|
|
195
|
+
} = e.nativeEvent;
|
|
196
|
+
reportInteraction({
|
|
197
|
+
type: 'click',
|
|
198
|
+
attributes: {
|
|
199
|
+
x: Math.round(pageX * 100) / 100,
|
|
200
|
+
y: Math.round(pageY * 100) / 100
|
|
201
|
+
},
|
|
202
|
+
element_classes: componentName,
|
|
203
|
+
target_element: (_props$accessibilityL = props.accessibilityLabel) != null ? _props$accessibilityL : componentName,
|
|
204
|
+
target_id: props.testID,
|
|
205
|
+
inner_text: (_extractInnerText = extractInnerText(props.children)) != null ? _extractInnerText : props.title
|
|
206
|
+
});
|
|
207
|
+
props.onPress(e);
|
|
208
|
+
}
|
|
209
|
+
}) : props;
|
|
210
|
+
return /*#__PURE__*/React.createElement(OrigComp, _extends({}, wrappedProps, {
|
|
211
|
+
ref
|
|
212
|
+
}));
|
|
213
|
+
});
|
|
214
|
+
InstrumentedComp.displayName = componentName;
|
|
215
|
+
return InstrumentedComp;
|
|
216
|
+
}
|
|
217
|
+
function makeInstrumentedSwitch(OrigComp) {
|
|
218
|
+
const InstrumentedSwitch = /*#__PURE__*/React.forwardRef(function InstrumentedSwitch(props, ref) {
|
|
219
|
+
const wrappedProps = props.onValueChange ? _extends({}, props, {
|
|
220
|
+
onValueChange: value => {
|
|
221
|
+
var _props$accessibilityL2;
|
|
222
|
+
reportInteraction({
|
|
223
|
+
type: 'click',
|
|
224
|
+
element_classes: 'Switch',
|
|
225
|
+
target_element: (_props$accessibilityL2 = props.accessibilityLabel) != null ? _props$accessibilityL2 : 'Switch',
|
|
226
|
+
target_id: props.testID
|
|
227
|
+
});
|
|
228
|
+
props.onValueChange(value);
|
|
229
|
+
}
|
|
230
|
+
}) : props;
|
|
231
|
+
return /*#__PURE__*/React.createElement(OrigComp, _extends({}, wrappedProps, {
|
|
232
|
+
ref
|
|
233
|
+
}));
|
|
234
|
+
});
|
|
235
|
+
InstrumentedSwitch.displayName = 'Switch';
|
|
236
|
+
return InstrumentedSwitch;
|
|
237
|
+
}
|
|
238
|
+
function makeInstrumentedScrollView(OrigComp) {
|
|
239
|
+
const InstrumentedScrollView = /*#__PURE__*/React.forwardRef(function InstrumentedScrollView(props, ref) {
|
|
240
|
+
const wrappedProps = _extends({}, props, {
|
|
241
|
+
onScrollBeginDrag: e => {
|
|
242
|
+
scrollState.startOffset = _extends({}, e.nativeEvent.contentOffset);
|
|
243
|
+
scrollState.startTime = Date.now();
|
|
244
|
+
props.onScrollBeginDrag == null || props.onScrollBeginDrag(e);
|
|
245
|
+
},
|
|
246
|
+
onScrollEndDrag: e => {
|
|
247
|
+
var _scrollState$timeout;
|
|
248
|
+
const endOffset = _extends({}, e.nativeEvent.contentOffset);
|
|
249
|
+
// Capture start state immediately — scrollState may be mutated by a
|
|
250
|
+
// concurrent ScrollView's onScrollBeginDrag before the timeout fires.
|
|
251
|
+
const {
|
|
252
|
+
startOffset,
|
|
253
|
+
startTime
|
|
254
|
+
} = scrollState;
|
|
255
|
+
clearTimeout((_scrollState$timeout = scrollState.timeout) != null ? _scrollState$timeout : undefined);
|
|
256
|
+
|
|
257
|
+
// Compute drag speed (px/ms) to distinguish a deliberate scroll from a fling.
|
|
258
|
+
// Works on both iOS and Android without relying on native velocity fields.
|
|
259
|
+
let classified = false;
|
|
260
|
+
if (startOffset !== null && startTime !== null) {
|
|
261
|
+
const elapsed = Date.now() - startTime;
|
|
262
|
+
const dx = endOffset.x - startOffset.x;
|
|
263
|
+
const dy = endOffset.y - startOffset.y;
|
|
264
|
+
const speed = elapsed > 0 ? Math.sqrt(dx * dx + dy * dy) / elapsed : 0;
|
|
265
|
+
if (speed < SWIPE_VELOCITY_THRESHOLD) {
|
|
266
|
+
// Slow deliberate drag → report scroll immediately.
|
|
267
|
+
reportInteraction({
|
|
268
|
+
type: 'scroll',
|
|
269
|
+
target_element: 'ScrollView',
|
|
270
|
+
direction: inferDirectionFromDelta(dx, dy)
|
|
271
|
+
});
|
|
272
|
+
scrollState = {
|
|
273
|
+
startOffset: null,
|
|
274
|
+
startTime: null,
|
|
275
|
+
timeout: null
|
|
276
|
+
};
|
|
277
|
+
classified = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!classified) {
|
|
281
|
+
// Fast drag → wait for onMomentumScrollBegin to classify as swipe.
|
|
282
|
+
// If momentum doesn't fire within the window, fall back to scroll.
|
|
283
|
+
// Uses captured startOffset (not scrollState.startOffset) to avoid
|
|
284
|
+
// reading stale state if another scroll begins before the timeout fires.
|
|
285
|
+
scrollState.timeout = setTimeout(() => {
|
|
286
|
+
if (startOffset) {
|
|
287
|
+
reportInteraction({
|
|
288
|
+
type: 'scroll',
|
|
289
|
+
target_element: 'ScrollView',
|
|
290
|
+
direction: inferDirectionFromDelta(endOffset.x - startOffset.x, endOffset.y - startOffset.y)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
scrollState = {
|
|
294
|
+
startOffset: null,
|
|
295
|
+
startTime: null,
|
|
296
|
+
timeout: null
|
|
297
|
+
};
|
|
298
|
+
}, SWIPE_MOMENTUM_TIMEOUT_MS);
|
|
299
|
+
}
|
|
300
|
+
props.onScrollEndDrag == null || props.onScrollEndDrag(e);
|
|
301
|
+
},
|
|
302
|
+
onMomentumScrollBegin: e => {
|
|
303
|
+
var _scrollState$timeout2;
|
|
304
|
+
clearTimeout((_scrollState$timeout2 = scrollState.timeout) != null ? _scrollState$timeout2 : undefined);
|
|
305
|
+
const currentOffset = _extends({}, e.nativeEvent.contentOffset);
|
|
306
|
+
const {
|
|
307
|
+
startOffset
|
|
308
|
+
} = scrollState;
|
|
309
|
+
if (startOffset) {
|
|
310
|
+
reportInteraction({
|
|
311
|
+
type: 'swipe',
|
|
312
|
+
target_element: 'ScrollView',
|
|
313
|
+
direction: inferDirectionFromDelta(currentOffset.x - startOffset.x, currentOffset.y - startOffset.y)
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
scrollState = {
|
|
317
|
+
startOffset: null,
|
|
318
|
+
startTime: null,
|
|
319
|
+
timeout: null
|
|
320
|
+
};
|
|
321
|
+
props.onMomentumScrollBegin == null || props.onMomentumScrollBegin(e);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
return /*#__PURE__*/React.createElement(OrigComp, _extends({}, wrappedProps, {
|
|
325
|
+
ref
|
|
326
|
+
}));
|
|
327
|
+
});
|
|
328
|
+
InstrumentedScrollView.displayName = 'ScrollView';
|
|
329
|
+
|
|
330
|
+
// Copy static properties from OrigComp (e.g. ScrollView.Context used by VirtualizedList/FlatList).
|
|
331
|
+
// React.forwardRef returns a plain object so these statics are lost without explicit copying.
|
|
332
|
+
const skipKeys = new Set(['$$typeof', 'render', 'displayName']);
|
|
333
|
+
Object.getOwnPropertyNames(OrigComp).forEach(key => {
|
|
334
|
+
if (skipKeys.has(key)) return;
|
|
335
|
+
try {
|
|
336
|
+
const descriptor = Object.getOwnPropertyDescriptor(OrigComp, key);
|
|
337
|
+
if (descriptor) Object.defineProperty(InstrumentedScrollView, key, descriptor);
|
|
338
|
+
} catch (_unused) {/* ignore non-configurable descriptors */}
|
|
339
|
+
});
|
|
340
|
+
return InstrumentedScrollView;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// react-native is in Metro's nonInlinedRequires list, so app code reads properties
|
|
344
|
+
// like _rn.Pressable from the shared module object at each render. Mutating the
|
|
345
|
+
// module object here (via Object.defineProperty) means every subsequent property
|
|
346
|
+
// read returns our wrappers.
|
|
347
|
+
//
|
|
348
|
+
// react-native/index.js defines exports as getter-only accessors with no setters,
|
|
349
|
+
// so direct assignment is silently ignored. Object.defineProperty with a data
|
|
350
|
+
// descriptor converts the accessor to a writable data property.
|
|
351
|
+
const RN = require('react-native');
|
|
352
|
+
const originals = {
|
|
353
|
+
Button: RN.Button,
|
|
354
|
+
Pressable: RN.Pressable,
|
|
355
|
+
TouchableOpacity: RN.TouchableOpacity,
|
|
356
|
+
TouchableHighlight: RN.TouchableHighlight,
|
|
357
|
+
TouchableNativeFeedback: RN.TouchableNativeFeedback,
|
|
358
|
+
TouchableWithoutFeedback: RN.TouchableWithoutFeedback,
|
|
359
|
+
Switch: RN.Switch,
|
|
360
|
+
ScrollView: RN.ScrollView
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Wrapper components are created once from the originals captured above.
|
|
364
|
+
// They are patched into / restored from the RN module in enable() / disable().
|
|
365
|
+
const instrumented = {
|
|
366
|
+
Button: makeInstrumentedPressable(originals.Button, 'Button'),
|
|
367
|
+
Pressable: makeInstrumentedPressable(originals.Pressable, 'Pressable'),
|
|
368
|
+
TouchableOpacity: makeInstrumentedPressable(originals.TouchableOpacity, 'TouchableOpacity'),
|
|
369
|
+
TouchableHighlight: makeInstrumentedPressable(originals.TouchableHighlight, 'TouchableHighlight'),
|
|
370
|
+
TouchableNativeFeedback: makeInstrumentedPressable(originals.TouchableNativeFeedback, 'TouchableNativeFeedback'),
|
|
371
|
+
TouchableWithoutFeedback: makeInstrumentedPressable(originals.TouchableWithoutFeedback, 'TouchableWithoutFeedback'),
|
|
372
|
+
Switch: makeInstrumentedSwitch(originals.Switch),
|
|
373
|
+
ScrollView: makeInstrumentedScrollView(originals.ScrollView)
|
|
374
|
+
};
|
|
375
|
+
function defineComp(key, value) {
|
|
376
|
+
Object.defineProperty(RN, key, {
|
|
377
|
+
value,
|
|
378
|
+
writable: true,
|
|
379
|
+
configurable: true,
|
|
380
|
+
enumerable: true
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function patchComponents() {
|
|
384
|
+
defineComp('Button', instrumented.Button);
|
|
385
|
+
defineComp('Pressable', instrumented.Pressable);
|
|
386
|
+
defineComp('TouchableOpacity', instrumented.TouchableOpacity);
|
|
387
|
+
defineComp('TouchableHighlight', instrumented.TouchableHighlight);
|
|
388
|
+
defineComp('TouchableNativeFeedback', instrumented.TouchableNativeFeedback);
|
|
389
|
+
defineComp('TouchableWithoutFeedback', instrumented.TouchableWithoutFeedback);
|
|
390
|
+
defineComp('Switch', instrumented.Switch);
|
|
391
|
+
defineComp('ScrollView', instrumented.ScrollView);
|
|
392
|
+
}
|
|
393
|
+
function restoreComponents() {
|
|
394
|
+
defineComp('Button', originals.Button);
|
|
395
|
+
defineComp('Pressable', originals.Pressable);
|
|
396
|
+
defineComp('TouchableOpacity', originals.TouchableOpacity);
|
|
397
|
+
defineComp('TouchableHighlight', originals.TouchableHighlight);
|
|
398
|
+
defineComp('TouchableNativeFeedback', originals.TouchableNativeFeedback);
|
|
399
|
+
defineComp('TouchableWithoutFeedback', originals.TouchableWithoutFeedback);
|
|
400
|
+
defineComp('Switch', originals.Switch);
|
|
401
|
+
defineComp('ScrollView', originals.ScrollView);
|
|
402
|
+
}
|
|
403
|
+
class UserInteractionInstrumentation extends InstrumentationBase {
|
|
404
|
+
constructor(config) {
|
|
405
|
+
super(USER_INTERACTION_INSTRUMENTATION_NAME, USER_INTERACTION_INSTRUMENTATION_VERSION, {});
|
|
406
|
+
onInteraction = config.onInteraction;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
410
|
+
init() {}
|
|
411
|
+
enable() {
|
|
412
|
+
isEnabled = true;
|
|
413
|
+
patchComponents();
|
|
414
|
+
}
|
|
415
|
+
disable() {
|
|
416
|
+
isEnabled = false;
|
|
417
|
+
onInteraction = () => {};
|
|
418
|
+
restoreComponents();
|
|
419
|
+
if (scrollState.timeout) {
|
|
420
|
+
clearTimeout(scrollState.timeout);
|
|
421
|
+
}
|
|
422
|
+
scrollState = {
|
|
423
|
+
startOffset: null,
|
|
424
|
+
startTime: null,
|
|
425
|
+
timeout: null
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
127
430
|
const DEFAULT_SAMPLE_DURATION_MS = 5000;
|
|
128
431
|
const DEFAULT_SAMPLE_INTERVAL_MS = 60000;
|
|
129
432
|
let isSampling = false;
|
|
@@ -218,7 +521,7 @@ function stopJsRefreshRateSampler() {
|
|
|
218
521
|
appStateSub = null;
|
|
219
522
|
}
|
|
220
523
|
|
|
221
|
-
var version = "0.2.
|
|
524
|
+
var version = "0.2.11";
|
|
222
525
|
var pkg = {
|
|
223
526
|
version: version};
|
|
224
527
|
|
|
@@ -594,6 +897,10 @@ const CoralogixRum = {
|
|
|
594
897
|
const SessionReplay = {
|
|
595
898
|
init: async options => {
|
|
596
899
|
logger.debug("session replay: init called with options: ", options);
|
|
900
|
+
if (!CoralogixRum.isInited) {
|
|
901
|
+
logger.warn("SessionReplay.init called before CoralogixRum is initialized. Call and await CoralogixRum.init() first to avoid initialization errors.");
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
597
904
|
const optionsValid = isSessionReplayOptionsValid(options);
|
|
598
905
|
if (!optionsValid) {
|
|
599
906
|
logger.warn("invalid options in SessionReplay.init: ", options);
|
|
@@ -707,6 +1014,20 @@ async function registerCoralogixInstrumentations(options) {
|
|
|
707
1014
|
trackMobileVitals(options);
|
|
708
1015
|
}
|
|
709
1016
|
|
|
1017
|
+
// User interaction instrumentation (clicks, scrolls, swipes)
|
|
1018
|
+
const shouldInterceptUserInteractions = !instrumentationsOptions || instrumentationsOptions.user_interaction !== false;
|
|
1019
|
+
if (shouldInterceptUserInteractions) {
|
|
1020
|
+
instrumentations.push(new UserInteractionInstrumentation({
|
|
1021
|
+
onInteraction: ctx => {
|
|
1022
|
+
// isInited becomes true only after CxSdk.initialize() resolves,
|
|
1023
|
+
// so this guard drops any interactions that fire in the brief window
|
|
1024
|
+
// between instrumentation registration and native SDK init completing.
|
|
1025
|
+
if (!isInited) return;
|
|
1026
|
+
CxSdk.reportUserInteraction(ctx);
|
|
1027
|
+
}
|
|
1028
|
+
}));
|
|
1029
|
+
}
|
|
1030
|
+
|
|
710
1031
|
// Register Instrumentations
|
|
711
1032
|
_deregisterInstrumentations = registerInstrumentations({
|
|
712
1033
|
tracerProvider,
|
package/ios/CxSdk.mm
CHANGED
|
@@ -41,6 +41,8 @@ RCT_EXTERN_METHOD(setApplicationContext:(NSDictionary *)applicationContext
|
|
|
41
41
|
RCT_EXTERN_METHOD(getSessionId:(RCTPromiseResolveBlock)resolve
|
|
42
42
|
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
43
43
|
|
|
44
|
+
RCT_EXTERN_METHOD(reportUserInteraction:(NSDictionary *)interaction)
|
|
45
|
+
|
|
44
46
|
RCT_EXTERN_METHOD(shutdown:(RCTPromiseResolveBlock)resolve
|
|
45
47
|
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
46
48
|
|
package/ios/CxSdk.swift
CHANGED
|
@@ -168,6 +168,31 @@ class CxSdk: RCTEventEmitter {
|
|
|
168
168
|
resolve("reportNetworkRequest success")
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
@objc(reportUserInteraction:)
|
|
172
|
+
func reportUserInteraction(interaction: NSDictionary) {
|
|
173
|
+
var dictionary: [String: Any] = [:]
|
|
174
|
+
|
|
175
|
+
if let type = interaction["type"] as? String { dictionary["event_name"] = type }
|
|
176
|
+
if let targetElement = interaction["target_element"] as? String { dictionary["target_element"] = targetElement }
|
|
177
|
+
if let elementClasses = interaction["element_classes"] as? String { dictionary["element_classes"] = elementClasses }
|
|
178
|
+
if let targetId = interaction["target_id"] as? String { dictionary["element_id"] = targetId }
|
|
179
|
+
if let innerText = interaction["inner_text"] as? String { dictionary["target_element_inner_text"] = innerText }
|
|
180
|
+
if let direction = interaction["direction"] as? String { dictionary["scroll_direction"] = direction }
|
|
181
|
+
if let attributes = interaction["attributes"] as? [String: Any] {
|
|
182
|
+
if let x = attributes["x"] as? Double { dictionary["x"] = x }
|
|
183
|
+
if let y = attributes["y"] as? Double { dictionary["y"] = y }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let payload = dictionary
|
|
187
|
+
DispatchQueue.main.async { [weak self] in
|
|
188
|
+
guard let rum = self?.coralogixRum else {
|
|
189
|
+
print("[CxSdk] reportUserInteraction called before CoralogixRum is initialized, dropping interaction")
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
rum.setUserInteraction(payload)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
171
196
|
@objc(shutdown:withRejecter:)
|
|
172
197
|
func shutdown(resolve:RCTPromiseResolveBlock,
|
|
173
198
|
reject:RCTPromiseRejectBlock) -> Void {
|
|
@@ -187,7 +212,7 @@ class CxSdk: RCTEventEmitter {
|
|
|
187
212
|
let message = dictionary[Keys.errorMessage.rawValue] as? String ?? ""
|
|
188
213
|
let errorType = dictionary[Keys.errorType.rawValue] as? String ?? "5"
|
|
189
214
|
let isCrash = dictionary[Keys.isCrash.rawValue] as? Bool ?? false
|
|
190
|
-
|
|
215
|
+
|
|
191
216
|
coralogixRum?.reportError(message: message,
|
|
192
217
|
stackTrace: stackTrace,
|
|
193
218
|
errorType: errorType,
|
|
@@ -229,15 +254,19 @@ class CxSdk: RCTEventEmitter {
|
|
|
229
254
|
func initializeSessionReplay(options: NSDictionary,
|
|
230
255
|
resolve:RCTPromiseResolveBlock,
|
|
231
256
|
reject:RCTPromiseRejectBlock) -> Void {
|
|
257
|
+
guard coralogixRum != nil else {
|
|
258
|
+
reject("CX_SDK_ERROR", "Coralogix RUM must be initialized before Session Replay. Call CoralogixRum.init() first and await it.", nil)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
232
261
|
do {
|
|
233
262
|
let sessionReplayOptions = try self.toSessionReplayOptions(parameter: options)
|
|
234
263
|
SessionReplay.initializeWithOptions(sessionReplayOptions:sessionReplayOptions)
|
|
264
|
+
resolve("initializeSessionReplay success")
|
|
235
265
|
} catch let error as CxSdkError {
|
|
236
266
|
reject("CX_SDK_ERROR", error.localizedDescription, error)
|
|
237
267
|
} catch {
|
|
238
268
|
reject("UNEXPECTED_ERROR", "An unexpected error occurred: \(error.localizedDescription)", error)
|
|
239
269
|
}
|
|
240
|
-
resolve("initializeSessionReplay success")
|
|
241
270
|
}
|
|
242
271
|
|
|
243
272
|
@objc(shutdownSessionReplay:withRejecter:)
|
|
@@ -288,7 +317,7 @@ class CxSdk: RCTEventEmitter {
|
|
|
288
317
|
}
|
|
289
318
|
resolve("captureScreenshot success")
|
|
290
319
|
}
|
|
291
|
-
|
|
320
|
+
|
|
292
321
|
@objc(maskViewByTag:withResolver:withRejecter:)
|
|
293
322
|
func maskViewByTag(viewTag: NSNumber,
|
|
294
323
|
resolve:@escaping RCTPromiseResolveBlock,
|
|
@@ -300,7 +329,7 @@ class CxSdk: RCTEventEmitter {
|
|
|
300
329
|
reject("MASK_VIEW_ERROR", "no ui manager found, aborting maskViewByTag", nil)
|
|
301
330
|
return
|
|
302
331
|
}
|
|
303
|
-
|
|
332
|
+
|
|
304
333
|
RCTExecuteOnUIManagerQueue {
|
|
305
334
|
uiManager.addUIBlock { (_, viewRegistry) in
|
|
306
335
|
if let view = viewRegistry?[viewTag] as? UIView {
|
|
@@ -400,7 +429,6 @@ class CxSdk: RCTEventEmitter {
|
|
|
400
429
|
ignoreUrls: ignoreUrls,
|
|
401
430
|
ignoreErrors: ignoreError,
|
|
402
431
|
labels: labels,
|
|
403
|
-
fpsSampleRate: parameter["fpsSamplingSeconds"] as? TimeInterval ?? 300,
|
|
404
432
|
instrumentations: instrumentationDict,
|
|
405
433
|
collectIPData: parameter["collectIPData"] as? Bool ?? true,
|
|
406
434
|
proxyUrl: parameter["proxyUrl"] as? String ?? nil,
|
|
@@ -449,9 +477,9 @@ class CxSdk: RCTEventEmitter {
|
|
|
449
477
|
|
|
450
478
|
private func mapTraceParentInHeader(_ rnConfig: [String: Any]?) -> [String: Any]? {
|
|
451
479
|
guard let rnConfig = rnConfig else { return nil }
|
|
452
|
-
|
|
480
|
+
|
|
453
481
|
var nativeConfig: [String: Any] = [:]
|
|
454
|
-
|
|
482
|
+
|
|
455
483
|
// Copy all keys, but rename "enabled" to "enable"
|
|
456
484
|
for (key, value) in rnConfig {
|
|
457
485
|
if key == "enabled" {
|
|
@@ -460,7 +488,7 @@ class CxSdk: RCTEventEmitter {
|
|
|
460
488
|
nativeConfig[key] = value
|
|
461
489
|
}
|
|
462
490
|
}
|
|
463
|
-
|
|
491
|
+
|
|
464
492
|
return nativeConfig.isEmpty ? nil : nativeConfig
|
|
465
493
|
}
|
|
466
494
|
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UserContextConfig } from './model/UserContextConfig';
|
|
2
2
|
import type { ApplicationContextConfig } from './model/ApplicationContextConfig';
|
|
3
3
|
import type { CoralogixOtelWebType } from './model/CoralogixOtelWebType';
|
|
4
|
-
import type { CoralogixBrowserSdkConfig, CoralogixStackFrame, CxSpan, HybridMetric } from './model/Types';
|
|
4
|
+
import type { CoralogixBrowserSdkConfig, CoralogixStackFrame, CxSpan, HybridMetric, InteractionContext } from './model/Types';
|
|
5
5
|
import { CoralogixLogSeverity } from './model/Types';
|
|
6
6
|
import type { NetworkRequestDetails } from './model/NetworkRequestDetails';
|
|
7
7
|
import type { CustomMeasurement } from './model/CustomMeasurement';
|
|
@@ -36,6 +36,7 @@ interface CxSdkClient {
|
|
|
36
36
|
stopSessionRecording(): void;
|
|
37
37
|
captureScreenshot(): void;
|
|
38
38
|
maskViewByTag(viewTag: number): void;
|
|
39
|
+
reportUserInteraction(interaction: InteractionContext): void;
|
|
39
40
|
}
|
|
40
41
|
export declare const CxSdk: CxSdkClient;
|
|
41
42
|
export declare const CoralogixRum: CoralogixOtelWebType;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { InstrumentationBase } from '@opentelemetry/instrumentation';
|
|
2
|
+
import type { InteractionContext } from '../../model/Types';
|
|
3
|
+
export declare class UserInteractionInstrumentation extends InstrumentationBase {
|
|
4
|
+
constructor(config: {
|
|
5
|
+
onInteraction: (ctx: InteractionContext) => void;
|
|
6
|
+
});
|
|
7
|
+
init(): void;
|
|
8
|
+
enable(): void;
|
|
9
|
+
disable(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UserInteractionInstrumentation } from './UserInteractionInstrumentation';
|
|
@@ -9,8 +9,9 @@ import { NetworkRequestDetails } from './NetworkRequestDetails';
|
|
|
9
9
|
export interface CoralogixOtelWebType extends SendLog {
|
|
10
10
|
/**
|
|
11
11
|
* Init CoralogixRum.
|
|
12
|
+
* Returns a Promise that resolves when initialization is complete.
|
|
12
13
|
*/
|
|
13
|
-
init: (options: CoralogixBrowserSdkConfig) => void
|
|
14
|
+
init: (options: CoralogixBrowserSdkConfig) => Promise<void>;
|
|
14
15
|
/**
|
|
15
16
|
* Turn CoralogixRum off.
|
|
16
17
|
*/
|
package/src/model/Types.d.ts
CHANGED
|
@@ -208,4 +208,17 @@ export type HybridMetric = {
|
|
|
208
208
|
value: number;
|
|
209
209
|
units: string;
|
|
210
210
|
};
|
|
211
|
+
export interface InteractionContext {
|
|
212
|
+
type: 'click' | 'scroll' | 'swipe';
|
|
213
|
+
/** Required. Element identifier: accessibilityLabel ?? componentName for clicks; 'ScrollView' for scroll/swipe. */
|
|
214
|
+
target_element: string;
|
|
215
|
+
direction?: 'up' | 'down' | 'left' | 'right';
|
|
216
|
+
element_classes?: string;
|
|
217
|
+
target_id?: string;
|
|
218
|
+
inner_text?: string;
|
|
219
|
+
attributes?: {
|
|
220
|
+
x?: number;
|
|
221
|
+
y?: number;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
211
224
|
export {};
|