@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 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.0.0'
20
- s.dependency 'CoralogixInternal','2.0.0'
21
- s.dependency 'SessionReplay','2.0.0'
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.
@@ -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.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 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.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 { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
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.10";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Official Coralogix React Native plugin",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Coralogix",
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,4 @@
1
+ export declare const USER_INTERACTION_INSTRUMENTATION_NAME = "user-interaction";
2
+ export declare const USER_INTERACTION_INSTRUMENTATION_VERSION = "1";
3
+ export declare const SWIPE_MOMENTUM_TIMEOUT_MS = 80;
4
+ export declare const SWIPE_VELOCITY_THRESHOLD = 0.5;
@@ -0,0 +1,3 @@
1
+ export type Direction = 'up' | 'down' | 'left' | 'right';
2
+ export declare function inferDirectionFromDelta(dx: number, dy: number): Direction;
3
+ export declare function extractInnerText(children: any): string | undefined;
@@ -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
  */
@@ -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 {};