@truewatchtech/react-native-mobile 0.4.1 → 0.4.3-alpha.2

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.
Files changed (130) hide show
  1. package/FTMobileReactNativeSDK.podspec +7 -5
  2. package/README.md +34 -0
  3. package/android/build.gradle +9 -2
  4. package/android/src/main/java/com/ft/sdk/reactnative/FTMobileImpl.java +45 -14
  5. package/android/src/main/java/com/ft/sdk/reactnative/FTRUMImpl.java +85 -2
  6. package/android/src/main/java/com/ft/sdk/reactnative/FTTraceImpl.java +5 -1
  7. package/android/src/main/java/com/ft/sdk/reactnative/JsLongTaskMonitor.java +281 -0
  8. package/android/src/main/java/com/ft/sdk/reactnative/utils/ReactNativeUtils.java +5 -12
  9. package/android/src/newarch/java/com/ft/sdk/reactnative/FTMobileModule.java +9 -5
  10. package/android/src/newarch/java/com/ft/sdk/reactnative/FTRUMModule.java +26 -1
  11. package/android/src/newarch/java/com/ft/sdk/reactnative/FTTraceModule.java +14 -0
  12. package/android/src/oldarch/java/com/ft/sdk/reactnative/FTMobileModule.java +2 -2
  13. package/android/src/oldarch/java/com/ft/sdk/reactnative/FTRUMModule.java +29 -1
  14. package/android/src/oldarch/java/com/ft/sdk/reactnative/FTTraceModule.java +12 -0
  15. package/android/src/rnpost70/java/com/ft/sdk/reactnative/ReactJsThreadExecutor.java +21 -0
  16. package/android/src/rnpre70/java/com/ft/sdk/reactnative/ReactJsThreadExecutor.java +37 -0
  17. package/android/src/test/java/com/ft/sdk/reactnative/FTRUMModuleTest.java +117 -0
  18. package/android/src/test/java/com/ft/sdk/reactnative/JsLongTaskMonitorTest.java +446 -0
  19. package/android/src/test/java/com/ft/sdk/reactnative/utils/ReactNativeUtilsTest.java +33 -0
  20. package/ios/FTJSLongTaskMonitor.h +51 -0
  21. package/ios/FTJSLongTaskMonitor.m +250 -0
  22. package/ios/FTMobileReactNative.mm +23 -16
  23. package/ios/FTReactNativeLog.mm +3 -4
  24. package/ios/FTReactNativeRUM.mm +103 -28
  25. package/ios/FTReactNativeResource.h +4 -0
  26. package/ios/FTReactNativeResource.m +58 -0
  27. package/ios/FTReactNativeTrace.mm +56 -13
  28. package/ios/FTReactNativeUtils.m +10 -15
  29. package/ios/FTReactNativeWebSocket.h +10 -0
  30. package/ios/FTReactNativeWebSocket.mm +201 -0
  31. package/ios/FTWebSocketMetadataStore.h +25 -0
  32. package/ios/FTWebSocketMetadataStore.m +622 -0
  33. package/ios/FTWebSocketResourceData.h +16 -0
  34. package/ios/FTWebSocketResourceData.m +75 -0
  35. package/lib/commonjs/ft_mobile_agent.js +36 -3
  36. package/lib/commonjs/ft_mobile_agent.js.map +1 -1
  37. package/lib/commonjs/ft_rum.js +46 -2
  38. package/lib/commonjs/ft_rum.js.map +1 -1
  39. package/lib/commonjs/ft_tracing.js +12 -3
  40. package/lib/commonjs/ft_tracing.js.map +1 -1
  41. package/lib/commonjs/index.js +14 -0
  42. package/lib/commonjs/index.js.map +1 -1
  43. package/lib/commonjs/rum/FTBabelInteractionTracking.js +72 -0
  44. package/lib/commonjs/rum/FTBabelInteractionTracking.js.map +1 -0
  45. package/lib/commonjs/rum/FTBabelUtils.js +87 -0
  46. package/lib/commonjs/rum/FTBabelUtils.js.map +1 -0
  47. package/lib/commonjs/rum/FTRumActionTracking.js +10 -39
  48. package/lib/commonjs/rum/FTRumActionTracking.js.map +1 -1
  49. package/lib/commonjs/rum/FTRumActionTrackingContext.js +50 -0
  50. package/lib/commonjs/rum/FTRumActionTrackingContext.js.map +1 -0
  51. package/lib/commonjs/rum/FTRumWebSocketNative.js +73 -0
  52. package/lib/commonjs/rum/FTRumWebSocketNative.js.map +1 -0
  53. package/lib/commonjs/rum/FTRumWebSocketTiming.js +55 -0
  54. package/lib/commonjs/rum/FTRumWebSocketTiming.js.map +1 -0
  55. package/lib/commonjs/rum/FTRumWebSocketTracking.js +510 -0
  56. package/lib/commonjs/rum/FTRumWebSocketTracking.js.map +1 -0
  57. package/lib/commonjs/rum/longTasksUtils.js +25 -0
  58. package/lib/commonjs/rum/longTasksUtils.js.map +1 -0
  59. package/lib/commonjs/specs/NativeFTMobileReactNative.js.map +1 -1
  60. package/lib/commonjs/specs/NativeFTReactNativeRUM.js.map +1 -1
  61. package/lib/commonjs/specs/NativeFTReactNativeTrace.js.map +1 -1
  62. package/lib/commonjs/specs/NativeFTReactNativeWebSocket.js +11 -0
  63. package/lib/commonjs/specs/NativeFTReactNativeWebSocket.js.map +1 -0
  64. package/lib/commonjs/version.js +1 -1
  65. package/lib/commonjs/version.js.map +1 -1
  66. package/lib/module/ft_mobile_agent.js +36 -3
  67. package/lib/module/ft_mobile_agent.js.map +1 -1
  68. package/lib/module/ft_rum.js +46 -2
  69. package/lib/module/ft_rum.js.map +1 -1
  70. package/lib/module/ft_tracing.js +12 -1
  71. package/lib/module/ft_tracing.js.map +1 -1
  72. package/lib/module/index.js +3 -1
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/rum/FTBabelInteractionTracking.js +66 -0
  75. package/lib/module/rum/FTBabelInteractionTracking.js.map +1 -0
  76. package/lib/module/rum/FTBabelUtils.js +80 -0
  77. package/lib/module/rum/FTBabelUtils.js.map +1 -0
  78. package/lib/module/rum/FTRumActionTracking.js +10 -39
  79. package/lib/module/rum/FTRumActionTracking.js.map +1 -1
  80. package/lib/module/rum/FTRumActionTrackingContext.js +43 -0
  81. package/lib/module/rum/FTRumActionTrackingContext.js.map +1 -0
  82. package/lib/module/rum/FTRumWebSocketNative.js +62 -0
  83. package/lib/module/rum/FTRumWebSocketNative.js.map +1 -0
  84. package/lib/module/rum/FTRumWebSocketTiming.js +49 -0
  85. package/lib/module/rum/FTRumWebSocketTiming.js.map +1 -0
  86. package/lib/module/rum/FTRumWebSocketTracking.js +503 -0
  87. package/lib/module/rum/FTRumWebSocketTracking.js.map +1 -0
  88. package/lib/module/rum/longTasksUtils.js +18 -0
  89. package/lib/module/rum/longTasksUtils.js.map +1 -0
  90. package/lib/module/specs/NativeFTMobileReactNative.js.map +1 -1
  91. package/lib/module/specs/NativeFTReactNativeRUM.js.map +1 -1
  92. package/lib/module/specs/NativeFTReactNativeTrace.js.map +1 -1
  93. package/lib/module/specs/NativeFTReactNativeWebSocket.js +8 -0
  94. package/lib/module/specs/NativeFTReactNativeWebSocket.js.map +1 -0
  95. package/lib/module/version.js +1 -1
  96. package/lib/module/version.js.map +1 -1
  97. package/lib/typescript/ft_mobile_agent.d.ts +7 -0
  98. package/lib/typescript/ft_rum.d.ts +9 -0
  99. package/lib/typescript/index.d.ts +3 -1
  100. package/lib/typescript/rum/FTBabelInteractionTracking.d.ts +30 -0
  101. package/lib/typescript/rum/FTBabelUtils.d.ts +1 -0
  102. package/lib/typescript/rum/FTRumActionTracking.d.ts +0 -2
  103. package/lib/typescript/rum/FTRumActionTrackingContext.d.ts +11 -0
  104. package/lib/typescript/rum/FTRumWebSocketNative.d.ts +23 -0
  105. package/lib/typescript/rum/FTRumWebSocketTiming.d.ts +2 -0
  106. package/lib/typescript/rum/FTRumWebSocketTracking.d.ts +56 -0
  107. package/lib/typescript/rum/longTasksUtils.d.ts +8 -0
  108. package/lib/typescript/specs/NativeFTMobileReactNative.d.ts +14 -0
  109. package/lib/typescript/specs/NativeFTReactNativeRUM.d.ts +4 -0
  110. package/lib/typescript/specs/NativeFTReactNativeTrace.d.ts +12 -0
  111. package/lib/typescript/specs/NativeFTReactNativeWebSocket.d.ts +13 -0
  112. package/lib/typescript/version.d.ts +1 -1
  113. package/package.json +7 -6
  114. package/src/ft_mobile_agent.tsx +49 -6
  115. package/src/ft_rum.tsx +61 -2
  116. package/src/ft_tracing.tsx +15 -1
  117. package/src/index.tsx +4 -0
  118. package/src/rum/FTBabelInteractionTracking.ts +124 -0
  119. package/src/rum/FTBabelUtils.tsx +99 -0
  120. package/src/rum/FTRumActionTracking.tsx +14 -43
  121. package/src/rum/FTRumActionTrackingContext.ts +68 -0
  122. package/src/rum/FTRumWebSocketNative.ts +106 -0
  123. package/src/rum/FTRumWebSocketTiming.ts +50 -0
  124. package/src/rum/FTRumWebSocketTracking.ts +727 -0
  125. package/src/rum/longTasksUtils.ts +17 -0
  126. package/src/specs/NativeFTMobileReactNative.ts +17 -0
  127. package/src/specs/NativeFTReactNativeRUM.ts +4 -0
  128. package/src/specs/NativeFTReactNativeTrace.ts +12 -0
  129. package/src/specs/NativeFTReactNativeWebSocket.ts +33 -0
  130. package/src/version.ts +1 -1
@@ -1,6 +1,8 @@
1
1
  // import { NativeModules } from 'react-native';
2
2
  //FTReactNativeTrace
3
3
 
4
+ import { FTRumWebSocketTracking } from './rum/FTRumWebSocketTracking';
5
+
4
6
  /**
5
7
  * Trace types for use.
6
8
  */
@@ -68,13 +70,25 @@ type FTReactNativeTraceType = {
68
70
  };
69
71
 
70
72
  class FTReactNativeTraceWrapper implements FTReactNativeTraceType {
73
+ private webSocketConfigVersion = 0;
71
74
  /* eslint-disable @typescript-eslint/no-var-requires */
72
75
  private trace: FTReactNativeTraceType =
73
76
  require('./specs/NativeFTReactNativeTrace').default;
74
77
  /* eslint-enable @typescript-eslint/no-var-requires */
75
78
 
76
79
  setConfig(config: FTTraceConfig): Promise<void> {
77
- return this.trace.setConfig(config);
80
+ const webSocketLifecycle = FTRumWebSocketTracking.getLifecycleVersion();
81
+ const webSocketConfigVersion = ++this.webSocketConfigVersion;
82
+ const traceWebSocket = config.enableNativeAutoTrace === true;
83
+ return this.trace.setConfig(config).then(() => {
84
+ if (
85
+ webSocketLifecycle !== FTRumWebSocketTracking.getLifecycleVersion() ||
86
+ webSocketConfigVersion !== this.webSocketConfigVersion
87
+ ) {
88
+ return;
89
+ }
90
+ FTRumWebSocketTracking.setNativeAutoTraceEnabled(traceWebSocket);
91
+ });
78
92
  }
79
93
  /**
80
94
  * Get trace HTTP request header data.
package/src/index.tsx CHANGED
@@ -28,6 +28,8 @@ import {
28
28
  import { FTTraceConfig, FTReactNativeTrace, TraceType } from './ft_tracing';
29
29
  import { FTRumActionTracking } from './rum/FTRumActionTracking';
30
30
  import { FTRumErrorTracking } from './rum/FTRumErrorTracking';
31
+ import { FTBabelInteractionTracking } from './rum/FTBabelInteractionTracking';
32
+ import { __ftExtractText } from './rum/FTBabelUtils';
31
33
 
32
34
  export {
33
35
  FTMobileReactNative,
@@ -56,4 +58,6 @@ export {
56
58
  TraceType,
57
59
  FTRumActionTracking,
58
60
  FTRumErrorTracking,
61
+ FTBabelInteractionTracking,
62
+ __ftExtractText,
59
63
  };
@@ -0,0 +1,124 @@
1
+ import { resolveActionTrackingContext } from './FTRumActionTrackingContext';
2
+
3
+ declare global {
4
+ // eslint-disable-next-line no-var
5
+ var __FT_RN_BABEL_PLUGIN_ENABLED__: boolean | undefined;
6
+ }
7
+
8
+ export type FTBabelActionTarget = {
9
+ 'options': {
10
+ useContent: boolean;
11
+ useNamePrefix: boolean;
12
+ };
13
+ 'handlerArgs'?: unknown[];
14
+ 'componentName': string;
15
+ 'getContent'?: () => string[];
16
+ 'ft-action-name'?: string[];
17
+ 'customActionName'?: string[];
18
+ 'accessibilityLabel'?: string[];
19
+ };
20
+
21
+ type ActionReporter = (
22
+ actionName: string,
23
+ actionType: string,
24
+ property?: object
25
+ ) => Promise<void>;
26
+
27
+ type BabelTrackingConfig = {
28
+ trackInteractions: boolean;
29
+ actionReporter?: ActionReporter;
30
+ };
31
+
32
+ const DEFAULT_ACTION_TYPE = 'click';
33
+
34
+ const normalize = (value: string): string => value.replace(/\s+/g, ' ').trim();
35
+
36
+ const normalizeActionType = (value: unknown): string =>
37
+ typeof value === 'string' && value.trim()
38
+ ? value.trim()
39
+ : DEFAULT_ACTION_TYPE;
40
+
41
+ const nonEmpty = (values: string[] | undefined): string[] | null => {
42
+ if (!values) {
43
+ return null;
44
+ }
45
+ const normalized = values.map(normalize).filter(Boolean);
46
+ return normalized.length > 0 ? normalized : null;
47
+ };
48
+
49
+ class BabelInteractionTracking {
50
+ private trackInteractions = false;
51
+ private actionReporter: ActionReporter | undefined;
52
+
53
+ getInstance(): BabelInteractionTracking {
54
+ return this;
55
+ }
56
+
57
+ configure(config: BabelTrackingConfig): void {
58
+ this.trackInteractions = config.trackInteractions;
59
+ this.actionReporter = config.actionReporter;
60
+ }
61
+
62
+ getTargetName(target: FTBabelActionTarget): string {
63
+ let selectedContent = nonEmpty(target['ft-action-name']);
64
+ selectedContent ||= nonEmpty(target.customActionName);
65
+ selectedContent ||= nonEmpty(target.accessibilityLabel);
66
+ if (!selectedContent && target.options.useContent) {
67
+ selectedContent = nonEmpty(target.getContent?.());
68
+ }
69
+
70
+ if (!selectedContent) {
71
+ return target.componentName;
72
+ }
73
+
74
+ const handlerArgs = target.handlerArgs || [];
75
+ const numericArgument = handlerArgs.find(
76
+ (value) => typeof value === 'number'
77
+ ) as number | undefined;
78
+ const index =
79
+ numericArgument !== undefined &&
80
+ Number.isInteger(numericArgument) &&
81
+ numericArgument >= 0 &&
82
+ numericArgument < selectedContent.length
83
+ ? numericArgument
84
+ : 0;
85
+ const output = selectedContent[index] || selectedContent[0];
86
+
87
+ return target.options.useNamePrefix
88
+ ? `${target.componentName} ("${output}")`
89
+ : output;
90
+ }
91
+
92
+ wrapRumAction(
93
+ handler: ((...args: any[]) => any) | null | undefined,
94
+ actionType: string,
95
+ target: FTBabelActionTarget
96
+ ): (...args: any[]) => any {
97
+ const normalizedActionType = normalizeActionType(actionType);
98
+ return (...args: any[]) => {
99
+ try {
100
+ if (this.trackInteractions && this.actionReporter) {
101
+ const context = resolveActionTrackingContext(args);
102
+ if (context.enabled) {
103
+ const actionName = this.getTargetName({
104
+ ...target,
105
+ handlerArgs: args,
106
+ });
107
+ Promise.resolve(
108
+ this.actionReporter(
109
+ actionName,
110
+ normalizedActionType,
111
+ context.property
112
+ )
113
+ ).catch(() => undefined);
114
+ }
115
+ }
116
+ } catch (_error) {
117
+ // Tracking must never block the application's original handler.
118
+ }
119
+ return handler?.(...args);
120
+ };
121
+ }
122
+ }
123
+
124
+ export const FTBabelInteractionTracking = new BabelInteractionTracking();
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+
3
+ const LABEL_PROPS = ['label', 'title', 'text'] as const;
4
+
5
+ const normalize = (value: string): string => value.replace(/\s+/g, ' ').trim();
6
+
7
+ function flattenText(values: unknown[]): string[] {
8
+ const output: string[] = [];
9
+ for (const value of values) {
10
+ for (const text of __ftExtractText(value)) {
11
+ output.push(text);
12
+ }
13
+ }
14
+ return output;
15
+ }
16
+
17
+ export function __ftExtractText(node: any, prefer?: any[]): string[] {
18
+ if (Array.isArray(prefer)) {
19
+ const preferred = flattenText(prefer);
20
+ if (preferred.length > 0) {
21
+ return preferred;
22
+ }
23
+ }
24
+
25
+ if (node == null || typeof node === 'boolean') {
26
+ return [];
27
+ }
28
+ if (typeof node === 'string' || typeof node === 'number') {
29
+ const value = normalize(String(node));
30
+ return value ? [value] : [];
31
+ }
32
+ if (Array.isArray(node)) {
33
+ return flattenText(node);
34
+ }
35
+ if (
36
+ typeof node === 'object' &&
37
+ typeof Symbol !== 'undefined' &&
38
+ Symbol.iterator in node
39
+ ) {
40
+ return flattenText(Array.from(node as Iterable<unknown>));
41
+ }
42
+ if (typeof node === 'function' && node.length === 0) {
43
+ try {
44
+ return __ftExtractText(node());
45
+ } catch (_error) {
46
+ return [];
47
+ }
48
+ }
49
+ if (!React.isValidElement(node)) {
50
+ return [];
51
+ }
52
+
53
+ const props = (node.props || {}) as Record<string, unknown>;
54
+ for (const propertyName of LABEL_PROPS) {
55
+ if (props[propertyName] != null) {
56
+ const label = __ftExtractText(props[propertyName]);
57
+ if (label.length > 0) {
58
+ return label;
59
+ }
60
+ }
61
+ }
62
+
63
+ const rawChildren = Array.isArray(props.children)
64
+ ? props.children
65
+ : [props.children];
66
+ const children = rawChildren.filter(
67
+ (child) => child != null && child !== false
68
+ );
69
+ if (children.length === 0) {
70
+ return [];
71
+ }
72
+
73
+ const perChild = children.map((child) => __ftExtractText(child));
74
+ let labeledChildCount = 0;
75
+ children.forEach((child, index) => {
76
+ const childProps = React.isValidElement(child)
77
+ ? ((child.props || {}) as Record<string, unknown>)
78
+ : null;
79
+ const hasLabelProperty =
80
+ childProps !== null &&
81
+ LABEL_PROPS.some((propertyName) => childProps[propertyName] != null);
82
+ if (hasLabelProperty || perChild[index].length > 0) {
83
+ labeledChildCount += 1;
84
+ }
85
+ });
86
+
87
+ // Each child has already been extracted and normalized. Only merge its text.
88
+ const flattened: string[] = [];
89
+ for (const values of perChild) {
90
+ for (const value of values) {
91
+ flattened.push(value);
92
+ }
93
+ }
94
+ if (labeledChildCount > 1) {
95
+ return Array.from(new Set(flattened));
96
+ }
97
+ const joined = flattened.join(' ');
98
+ return joined ? [joined] : [];
99
+ }
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
2
  import { FTReactNativeRUM } from '../ft_rum';
3
- const FT_ENABLE_TRACK = 'ft-enable-track';
4
- const FT_EXTRA_PROPERTY = 'ft-extra-property';
3
+ import { resolveActionTrackingContextFromTarget } from './FTRumActionTrackingContext';
5
4
  export class FTRumActionTracking {
6
5
  private static isTracking = false;
7
6
  private static preActionTimestamp = Number.MIN_VALUE;
@@ -34,6 +33,7 @@ export class FTRumActionTracking {
34
33
  return;
35
34
  }
36
35
  const original = React.createElement;
36
+ this.originalCreateElement = original;
37
37
  React.createElement = (
38
38
  ...args: Parameters<typeof React.createElement>
39
39
  ): any => {
@@ -53,6 +53,7 @@ export class FTRumActionTracking {
53
53
  }
54
54
 
55
55
  const originalMemo = React.memo;
56
+ this.originalMemo = originalMemo;
56
57
  React.memo = (
57
58
  component: any,
58
59
  propsAreEqual?: (prevProps: any, newProps: any) => boolean
@@ -86,6 +87,9 @@ export class FTRumActionTracking {
86
87
  FTRumActionTracking.isTracking = true;
87
88
  }
88
89
  static stopTracking() {
90
+ if (!FTRumActionTracking.isTracking) {
91
+ return;
92
+ }
89
93
  React.createElement = this.originalCreateElement;
90
94
  React.memo = this.originalMemo;
91
95
  FTRumActionTracking.isTracking = false;
@@ -112,26 +116,17 @@ export class FTRumActionTracking {
112
116
 
113
117
  private static handleTargetEvent(targetNode: any | null) {
114
118
  if (targetNode) {
115
- const enable = FTRumActionTracking.findActionEnableTrack(targetNode);
116
- if (enable) {
117
- const extraProperty =
118
- FTRumActionTracking.findActionProperty(targetNode);
119
+ const context = resolveActionTrackingContextFromTarget(targetNode);
120
+ if (context.enabled) {
119
121
  const elementTypeName =
120
122
  FTRumActionTracking.resolveActionName(targetNode);
121
123
  if (elementTypeName) {
122
- let jsonObject = null;
123
- if (extraProperty) {
124
- try {
125
- jsonObject = JSON.parse(extraProperty);
126
- } catch (error) {
127
- console.warn(
128
- `Error parsing JSON string ${extraProperty}:`,
129
- error
130
- );
131
- }
132
- }
133
- if (jsonObject) {
134
- FTReactNativeRUM.startAction(elementTypeName, 'click', jsonObject);
124
+ if (context.property) {
125
+ FTReactNativeRUM.startAction(
126
+ elementTypeName,
127
+ 'click',
128
+ context.property
129
+ );
135
130
  } else {
136
131
  FTReactNativeRUM.startAction(elementTypeName, 'click');
137
132
  }
@@ -178,30 +173,6 @@ export class FTRumActionTracking {
178
173
  }
179
174
  return null;
180
175
  }
181
- private static findActionProperty(targetNode: any): string | null {
182
- let currentNode = targetNode;
183
- while (currentNode) {
184
- const props = currentNode.memoizedProps;
185
- if (props && props[FT_EXTRA_PROPERTY]) {
186
- return props[FT_EXTRA_PROPERTY];
187
- }
188
- currentNode = currentNode.return;
189
- }
190
- return null;
191
- }
192
- private static findActionEnableTrack(targetNode: any): boolean {
193
- let currentNode = targetNode;
194
- let enable = true;
195
- while (currentNode) {
196
- const props = currentNode.memoizedProps;
197
- if (props && props[FT_ENABLE_TRACK]) {
198
- enable = props[FT_ENABLE_TRACK] === 'true';
199
- break;
200
- }
201
- currentNode = currentNode.return;
202
- }
203
- return enable;
204
- }
205
176
  }
206
177
 
207
178
  const areObjectShallowEqual = (
@@ -0,0 +1,68 @@
1
+ const FT_ENABLE_TRACK = 'ft-enable-track';
2
+ const FT_EXTRA_PROPERTY = 'ft-extra-property';
3
+
4
+ export type FTRumActionTrackingContext = {
5
+ enabled: boolean;
6
+ property?: object;
7
+ };
8
+
9
+ type FiberNode = {
10
+ memoizedProps?: Record<string, unknown>;
11
+ return?: FiberNode | null;
12
+ };
13
+
14
+ function findFiberTarget(handlerArgs: unknown[]): FiberNode | null {
15
+ for (const handlerArg of handlerArgs) {
16
+ if (
17
+ handlerArg &&
18
+ typeof handlerArg === 'object' &&
19
+ '_targetInst' in handlerArg
20
+ ) {
21
+ return (handlerArg as { _targetInst?: FiberNode })._targetInst || null;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+
27
+ function findFiberProperty(
28
+ targetNode: FiberNode | null,
29
+ propertyName: string
30
+ ): unknown {
31
+ let currentNode = targetNode;
32
+ while (currentNode) {
33
+ const props = currentNode.memoizedProps;
34
+ if (props && props[propertyName]) {
35
+ return props[propertyName];
36
+ }
37
+ currentNode = currentNode.return || null;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ export function resolveActionTrackingContextFromTarget(
43
+ targetNode: FiberNode | null
44
+ ): FTRumActionTrackingContext {
45
+ const enableTrack = findFiberProperty(targetNode, FT_ENABLE_TRACK);
46
+ const extraProperty = findFiberProperty(targetNode, FT_EXTRA_PROPERTY);
47
+ const context: FTRumActionTrackingContext = {
48
+ enabled: enableTrack === undefined || enableTrack === 'true',
49
+ };
50
+
51
+ if (extraProperty !== undefined) {
52
+ try {
53
+ const parsedProperty = JSON.parse(String(extraProperty));
54
+ if (parsedProperty && typeof parsedProperty === 'object') {
55
+ context.property = parsedProperty as object;
56
+ }
57
+ } catch (error) {
58
+ console.warn(`Error parsing JSON string ${extraProperty}:`, error);
59
+ }
60
+ }
61
+ return context;
62
+ }
63
+
64
+ export function resolveActionTrackingContext(
65
+ handlerArgs: unknown[]
66
+ ): FTRumActionTrackingContext {
67
+ return resolveActionTrackingContextFromTarget(findFiberTarget(handlerArgs));
68
+ }
@@ -0,0 +1,106 @@
1
+ import NativeWebSocket from '../specs/NativeFTReactNativeWebSocket';
2
+ import { bridgeContextManager } from '../ft_mobile_agent';
3
+
4
+ export interface NativeWebSocketResource {
5
+ url: string;
6
+ httpMethod: string;
7
+ requestHeader: Record<string, unknown>;
8
+ webSocketEvent: string;
9
+ errorMessage?: string;
10
+ }
11
+
12
+ function ignoreBridgeFailure(call: () => unknown): void {
13
+ try {
14
+ void Promise.resolve(call()).catch(() => {});
15
+ } catch {
16
+ /* Setup/cleanup replies must not interrupt JS capture. */
17
+ }
18
+ }
19
+
20
+ export class NativeWebSocketSession {
21
+ private stopped = false;
22
+ private sequence = 0;
23
+
24
+ constructor(private readonly session: string) {
25
+ // Enqueue setup before start/stop/add on the same native module queue.
26
+ // Its reply never gates JS capture or selects a different reporting route.
27
+ ignoreBridgeFailure(() => NativeWebSocket!.startCapture(session));
28
+ }
29
+
30
+ startResource(
31
+ key: string,
32
+ socketID: number | undefined,
33
+ url: string
34
+ ): Promise<void> {
35
+ const property = bridgeContextManager.mergeWithLocalPropertiesSync();
36
+ return NativeWebSocket!.startResource(
37
+ key,
38
+ socketID ?? -1,
39
+ url,
40
+ this.session,
41
+ ++this.sequence,
42
+ property
43
+ );
44
+ }
45
+
46
+ stopResource(key: string, event: string): Promise<void> {
47
+ const property = bridgeContextManager.mergeWithLocalPropertiesSync();
48
+ return NativeWebSocket!.stopResource(key, event, this.session, property);
49
+ }
50
+
51
+ addResource(
52
+ key: string,
53
+ content: NativeWebSocketResource,
54
+ metrics: { duration?: number }
55
+ ): Promise<void> {
56
+ return NativeWebSocket!.addResource(key, content, metrics, this.session);
57
+ }
58
+
59
+ release(key: string): void {
60
+ ignoreBridgeFailure(() =>
61
+ NativeWebSocket?.releaseResource(key, this.session)
62
+ );
63
+ }
64
+
65
+ stop(): void {
66
+ if (this.stopped) return;
67
+ this.stopped = true;
68
+ // Existing handshakes still finish through their originally selected route.
69
+ ignoreBridgeFailure(() => NativeWebSocket?.stopCapture(this.session));
70
+ }
71
+ }
72
+
73
+ export function startNativeWebSocketSession(
74
+ createKey: () => string
75
+ ): NativeWebSocketSession | undefined {
76
+ try {
77
+ if (
78
+ typeof NativeWebSocket?.startCapture === 'function' &&
79
+ typeof NativeWebSocket.stopCapture === 'function' &&
80
+ typeof NativeWebSocket.clear === 'function' &&
81
+ typeof NativeWebSocket.startResource === 'function' &&
82
+ typeof NativeWebSocket.stopResource === 'function' &&
83
+ typeof NativeWebSocket.addResource === 'function' &&
84
+ typeof NativeWebSocket.releaseResource === 'function'
85
+ )
86
+ return new NativeWebSocketSession(createKey());
87
+ } catch {
88
+ /* Old native binaries keep the JS fallback. */
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ export function clearNativeWebSocketSessions(): void {
94
+ ignoreBridgeFailure(() => NativeWebSocket?.clear());
95
+ }
96
+
97
+ export function webSocketNativeID(socket: unknown): number | undefined {
98
+ try {
99
+ const id = (socket as { _socketId?: unknown })?._socketId;
100
+ return typeof id === 'number' && Number.isSafeInteger(id) && id >= 0
101
+ ? id
102
+ : undefined;
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
@@ -0,0 +1,50 @@
1
+ /** Measures only the JS-observed opening handshake; bridge delays are excluded. */
2
+ export function startWebSocketTiming(): () => number | undefined {
3
+ const wallStart = readWallTime();
4
+ let monotonicNow: (() => number) | undefined;
5
+ let monotonicStart: number | undefined;
6
+ try {
7
+ const clock = globalThis.performance;
8
+ if (typeof clock?.now === 'function') {
9
+ monotonicNow = () => clock.now();
10
+ monotonicStart = monotonicNow();
11
+ }
12
+ } catch {
13
+ // Older runtimes and other instrumentation may not provide this clock.
14
+ }
15
+
16
+ return () => {
17
+ let durationMs: number | undefined;
18
+ try {
19
+ if (monotonicNow && Number.isFinite(monotonicStart)) {
20
+ const elapsed = monotonicNow() - (monotonicStart as number);
21
+ if (Number.isFinite(elapsed) && elapsed >= 0) {
22
+ durationMs = elapsed;
23
+ }
24
+ }
25
+ } catch {
26
+ // Fall back to wall time if the monotonic clock becomes unavailable.
27
+ }
28
+ if (durationMs === undefined && wallStart !== undefined) {
29
+ const wallEnd = readWallTime();
30
+ if (wallEnd !== undefined && wallEnd >= wallStart) {
31
+ durationMs = wallEnd - wallStart;
32
+ }
33
+ }
34
+ if (durationMs === undefined) {
35
+ return undefined;
36
+ }
37
+ // Resource metrics use nanoseconds. Do not send an invalid bridge number.
38
+ const durationNs = Math.round(durationMs * 1e6);
39
+ return Number.isSafeInteger(durationNs) ? durationNs : undefined;
40
+ };
41
+ }
42
+
43
+ function readWallTime(): number | undefined {
44
+ try {
45
+ const time = Date.now();
46
+ return Number.isFinite(time) ? time : undefined;
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }