@truewatchtech/react-native-mobile 0.4.2 → 0.4.3

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 (153) hide show
  1. package/FTMobileReactNativeSDK.podspec +7 -5
  2. package/android/build.gradle +10 -3
  3. package/android/src/main/java/com/ft/sdk/reactnative/FTLogImpl.java +10 -2
  4. package/android/src/main/java/com/ft/sdk/reactnative/FTMobileImpl.java +45 -10
  5. package/android/src/main/java/com/ft/sdk/reactnative/FTRUMImpl.java +131 -6
  6. package/android/src/main/java/com/ft/sdk/reactnative/FTTraceImpl.java +10 -3
  7. package/android/src/main/java/com/ft/sdk/reactnative/JsLongTaskMonitor.java +281 -0
  8. package/android/src/main/java/com/ft/sdk/reactnative/ViewTiming.java +17 -0
  9. package/android/src/main/java/com/ft/sdk/reactnative/VisitorIdConfigMapper.java +50 -0
  10. package/android/src/main/java/com/ft/sdk/reactnative/utils/ReactNativeUtils.java +5 -12
  11. package/android/src/newarch/java/com/ft/sdk/reactnative/FTMobileModule.java +14 -5
  12. package/android/src/newarch/java/com/ft/sdk/reactnative/FTRUMModule.java +44 -1
  13. package/android/src/newarch/java/com/ft/sdk/reactnative/FTTraceModule.java +14 -0
  14. package/android/src/oldarch/java/com/ft/sdk/reactnative/FTMobileModule.java +7 -2
  15. package/android/src/oldarch/java/com/ft/sdk/reactnative/FTRUMModule.java +39 -1
  16. package/android/src/oldarch/java/com/ft/sdk/reactnative/FTTraceModule.java +12 -0
  17. package/android/src/rnpost70/java/com/ft/sdk/reactnative/ReactJsThreadExecutor.java +21 -0
  18. package/android/src/rnpre70/java/com/ft/sdk/reactnative/ReactJsThreadExecutor.java +37 -0
  19. package/android/src/test/java/com/ft/sdk/reactnative/FTMobileImplTest.java +53 -0
  20. package/android/src/test/java/com/ft/sdk/reactnative/FTRUMModuleTest.java +117 -0
  21. package/android/src/test/java/com/ft/sdk/reactnative/JsLongTaskMonitorTest.java +446 -0
  22. package/android/src/test/java/com/ft/sdk/reactnative/utils/ReactNativeUtilsTest.java +33 -0
  23. package/ios/FTJSLongTaskMonitor.h +51 -0
  24. package/ios/FTJSLongTaskMonitor.m +250 -0
  25. package/ios/FTMobileReactNative.mm +54 -18
  26. package/ios/FTReactNativeLog.mm +9 -5
  27. package/ios/FTReactNativeRUM.mm +196 -31
  28. package/ios/FTReactNativeResource.h +4 -0
  29. package/ios/FTReactNativeResource.m +64 -0
  30. package/ios/FTReactNativeTrace.mm +59 -14
  31. package/ios/FTReactNativeUtils.m +10 -15
  32. package/ios/FTReactNativeViewTiming.h +18 -0
  33. package/ios/FTReactNativeWebSocket.h +10 -0
  34. package/ios/FTReactNativeWebSocket.mm +201 -0
  35. package/ios/FTWebSocketMetadataStore.h +25 -0
  36. package/ios/FTWebSocketMetadataStore.m +622 -0
  37. package/ios/FTWebSocketResourceData.h +20 -0
  38. package/ios/FTWebSocketResourceData.m +106 -0
  39. package/lib/commonjs/ft_logger.js +3 -1
  40. package/lib/commonjs/ft_logger.js.map +1 -1
  41. package/lib/commonjs/ft_mobile_agent.js +47 -4
  42. package/lib/commonjs/ft_mobile_agent.js.map +1 -1
  43. package/lib/commonjs/ft_rum.js +65 -5
  44. package/lib/commonjs/ft_rum.js.map +1 -1
  45. package/lib/commonjs/ft_tracing.js +14 -4
  46. package/lib/commonjs/ft_tracing.js.map +1 -1
  47. package/lib/commonjs/index.js +21 -0
  48. package/lib/commonjs/index.js.map +1 -1
  49. package/lib/commonjs/rum/FTBabelInteractionTracking.js +72 -0
  50. package/lib/commonjs/rum/FTBabelInteractionTracking.js.map +1 -0
  51. package/lib/commonjs/rum/FTBabelUtils.js +87 -0
  52. package/lib/commonjs/rum/FTBabelUtils.js.map +1 -0
  53. package/lib/commonjs/rum/FTRumActionTracking.js +10 -39
  54. package/lib/commonjs/rum/FTRumActionTracking.js.map +1 -1
  55. package/lib/commonjs/rum/FTRumActionTrackingContext.js +50 -0
  56. package/lib/commonjs/rum/FTRumActionTrackingContext.js.map +1 -0
  57. package/lib/commonjs/rum/FTRumViewTracking.js +147 -0
  58. package/lib/commonjs/rum/FTRumViewTracking.js.map +1 -0
  59. package/lib/commonjs/rum/FTRumWebSocketNative.js +73 -0
  60. package/lib/commonjs/rum/FTRumWebSocketNative.js.map +1 -0
  61. package/lib/commonjs/rum/FTRumWebSocketTiming.js +73 -0
  62. package/lib/commonjs/rum/FTRumWebSocketTiming.js.map +1 -0
  63. package/lib/commonjs/rum/FTRumWebSocketTracking.js +506 -0
  64. package/lib/commonjs/rum/FTRumWebSocketTracking.js.map +1 -0
  65. package/lib/commonjs/rum/longTasksUtils.js +25 -0
  66. package/lib/commonjs/rum/longTasksUtils.js.map +1 -0
  67. package/lib/commonjs/specs/NativeFTMobileReactNative.js.map +1 -1
  68. package/lib/commonjs/specs/NativeFTReactNativeRUM.js.map +1 -1
  69. package/lib/commonjs/specs/NativeFTReactNativeTrace.js.map +1 -1
  70. package/lib/commonjs/specs/NativeFTReactNativeWebSocket.js +11 -0
  71. package/lib/commonjs/specs/NativeFTReactNativeWebSocket.js.map +1 -0
  72. package/lib/commonjs/version.js +1 -1
  73. package/lib/commonjs/version.js.map +1 -1
  74. package/lib/commonjs/visitorIdConfig.js +37 -0
  75. package/lib/commonjs/visitorIdConfig.js.map +1 -0
  76. package/lib/module/ft_logger.js +3 -1
  77. package/lib/module/ft_logger.js.map +1 -1
  78. package/lib/module/ft_mobile_agent.js +47 -4
  79. package/lib/module/ft_mobile_agent.js.map +1 -1
  80. package/lib/module/ft_rum.js +65 -5
  81. package/lib/module/ft_rum.js.map +1 -1
  82. package/lib/module/ft_tracing.js +14 -2
  83. package/lib/module/ft_tracing.js.map +1 -1
  84. package/lib/module/index.js +4 -1
  85. package/lib/module/index.js.map +1 -1
  86. package/lib/module/rum/FTBabelInteractionTracking.js +66 -0
  87. package/lib/module/rum/FTBabelInteractionTracking.js.map +1 -0
  88. package/lib/module/rum/FTBabelUtils.js +80 -0
  89. package/lib/module/rum/FTBabelUtils.js.map +1 -0
  90. package/lib/module/rum/FTRumActionTracking.js +10 -39
  91. package/lib/module/rum/FTRumActionTracking.js.map +1 -1
  92. package/lib/module/rum/FTRumActionTrackingContext.js +43 -0
  93. package/lib/module/rum/FTRumActionTrackingContext.js.map +1 -0
  94. package/lib/module/rum/FTRumViewTracking.js +140 -0
  95. package/lib/module/rum/FTRumViewTracking.js.map +1 -0
  96. package/lib/module/rum/FTRumWebSocketNative.js +62 -0
  97. package/lib/module/rum/FTRumWebSocketNative.js.map +1 -0
  98. package/lib/module/rum/FTRumWebSocketTiming.js +67 -0
  99. package/lib/module/rum/FTRumWebSocketTiming.js.map +1 -0
  100. package/lib/module/rum/FTRumWebSocketTracking.js +499 -0
  101. package/lib/module/rum/FTRumWebSocketTracking.js.map +1 -0
  102. package/lib/module/rum/longTasksUtils.js +18 -0
  103. package/lib/module/rum/longTasksUtils.js.map +1 -0
  104. package/lib/module/specs/NativeFTMobileReactNative.js.map +1 -1
  105. package/lib/module/specs/NativeFTReactNativeRUM.js.map +1 -1
  106. package/lib/module/specs/NativeFTReactNativeTrace.js.map +1 -1
  107. package/lib/module/specs/NativeFTReactNativeWebSocket.js +8 -0
  108. package/lib/module/specs/NativeFTReactNativeWebSocket.js.map +1 -0
  109. package/lib/module/version.js +1 -1
  110. package/lib/module/version.js.map +1 -1
  111. package/lib/module/visitorIdConfig.js +31 -0
  112. package/lib/module/visitorIdConfig.js.map +1 -0
  113. package/lib/typescript/ft_logger.d.ts +11 -1
  114. package/lib/typescript/ft_mobile_agent.d.ts +28 -0
  115. package/lib/typescript/ft_rum.d.ts +42 -5
  116. package/lib/typescript/ft_tracing.d.ts +9 -1
  117. package/lib/typescript/index.d.ts +5 -1
  118. package/lib/typescript/rum/FTBabelInteractionTracking.d.ts +30 -0
  119. package/lib/typescript/rum/FTBabelUtils.d.ts +1 -0
  120. package/lib/typescript/rum/FTRumActionTracking.d.ts +0 -2
  121. package/lib/typescript/rum/FTRumActionTrackingContext.d.ts +11 -0
  122. package/lib/typescript/rum/FTRumViewTracking.d.ts +32 -0
  123. package/lib/typescript/rum/FTRumWebSocketNative.d.ts +22 -0
  124. package/lib/typescript/rum/FTRumWebSocketTiming.d.ts +7 -0
  125. package/lib/typescript/rum/FTRumWebSocketTracking.d.ts +54 -0
  126. package/lib/typescript/rum/longTasksUtils.d.ts +8 -0
  127. package/lib/typescript/specs/NativeFTMobileReactNative.d.ts +15 -0
  128. package/lib/typescript/specs/NativeFTReactNativeRUM.d.ts +11 -1
  129. package/lib/typescript/specs/NativeFTReactNativeTrace.d.ts +12 -0
  130. package/lib/typescript/specs/NativeFTReactNativeWebSocket.d.ts +13 -0
  131. package/lib/typescript/version.d.ts +1 -1
  132. package/lib/typescript/visitorIdConfig.d.ts +11 -0
  133. package/package.json +6 -4
  134. package/src/ft_logger.tsx +11 -1
  135. package/src/ft_mobile_agent.tsx +79 -7
  136. package/src/ft_rum.tsx +111 -7
  137. package/src/ft_tracing.tsx +24 -2
  138. package/src/index.tsx +8 -0
  139. package/src/rum/FTBabelInteractionTracking.ts +124 -0
  140. package/src/rum/FTBabelUtils.tsx +99 -0
  141. package/src/rum/FTRumActionTracking.tsx +14 -43
  142. package/src/rum/FTRumActionTrackingContext.ts +68 -0
  143. package/src/rum/FTRumViewTracking.ts +179 -0
  144. package/src/rum/FTRumWebSocketNative.ts +107 -0
  145. package/src/rum/FTRumWebSocketTiming.ts +82 -0
  146. package/src/rum/FTRumWebSocketTracking.ts +722 -0
  147. package/src/rum/longTasksUtils.ts +17 -0
  148. package/src/specs/NativeFTMobileReactNative.ts +18 -0
  149. package/src/specs/NativeFTReactNativeRUM.ts +19 -1
  150. package/src/specs/NativeFTReactNativeTrace.ts +12 -0
  151. package/src/specs/NativeFTReactNativeWebSocket.ts +33 -0
  152. package/src/version.ts +1 -1
  153. package/src/visitorIdConfig.ts +123 -0
@@ -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,179 @@
1
+ import { AppState } from 'react-native';
2
+ import type { AppStateStatus } from 'react-native';
3
+ import { FTReactNativeRUM } from '../ft_rum';
4
+ import { bridgeContextManager } from '../ft_mobile_agent';
5
+ import type { Spec as NativeRUMSpec } from '../specs/NativeFTReactNativeRUM';
6
+
7
+ type Route = { key: string; name: string };
8
+ type Loading = { monotonicMs: number; epochMs: number };
9
+ const MAX_PENDING_LOAD_MS = 15 * 60 * 1000;
10
+
11
+ /** Shared lifecycle coordinator for navigation adapters. Use one per navigator. */
12
+ export class FTRumViewTracking {
13
+ private current?: Route;
14
+ private active?: { key: string; generation: number };
15
+ private generation = 0;
16
+ private lifecycle = 0;
17
+ private state: AppStateStatus | null = null;
18
+ private subscription?: { remove(): void };
19
+ private loading = new Map<string, Loading>();
20
+
21
+ constructor(private readonly onBackground?: () => void) {}
22
+
23
+ start(): void {
24
+ if (this.subscription) return;
25
+ this.state = AppState.currentState;
26
+ const lifecycle = ++this.lifecycle;
27
+ const listener = (state: AppStateStatus): void => {
28
+ if (lifecycle === this.lifecycle) this.onAppState(state);
29
+ };
30
+ const subscription = AppState.addEventListener('change', listener);
31
+ // RN < 0.65 uses removeEventListener instead of returning a subscription.
32
+ const legacyAppState = AppState as typeof AppState & {
33
+ removeEventListener?: (type: string, callback: typeof listener) => void;
34
+ };
35
+ this.subscription = subscription || {
36
+ remove: () => legacyAppState.removeEventListener?.('change', listener),
37
+ };
38
+ }
39
+
40
+ /** Capture both clocks at the loading boundary, before any async work. */
41
+ beginLoad(key: string): void {
42
+ this.start();
43
+ this.pruneLoading();
44
+ if (this.state === 'background' || this.loading.has(key)) return;
45
+ if (
46
+ typeof performance === 'undefined' ||
47
+ typeof performance.now !== 'function'
48
+ )
49
+ return;
50
+ // Only the latest transition can supply loading for this navigator.
51
+ this.loading.clear();
52
+ this.loading.set(key, {
53
+ monotonicMs: performance.now(),
54
+ epochMs: Date.now(),
55
+ });
56
+ }
57
+
58
+ /** Remember a transitioning route so foreground recovery uses the current page. */
59
+ setCurrent(route: Route): void {
60
+ this.start();
61
+ this.current = { key: route.key, name: route.name };
62
+ }
63
+
64
+ show(route: Route, loadingKey: string = route.key): number | undefined {
65
+ this.setCurrent(route);
66
+ this.pruneLoading();
67
+ const loading = this.loading.get(loadingKey);
68
+ this.loading.clear();
69
+ if (this.state !== 'active') return undefined;
70
+ if (this.active?.key === route.key) return this.active.generation;
71
+ this.active = { key: route.key, generation: ++this.generation };
72
+ if (loading) {
73
+ const durationNs = Math.floor(
74
+ (performance.now() - loading.monotonicMs) * 1000000
75
+ );
76
+ if (Number.isFinite(durationNs) && durationNs > 0) {
77
+ this.report(
78
+ this.nativeRUM.onCreateViewWithLoadStart(
79
+ route.name,
80
+ durationNs,
81
+ Number.isFinite(loading.epochMs) ? loading.epochMs : 0
82
+ )
83
+ );
84
+ }
85
+ }
86
+ // Enqueue these calls together. Waiting for a Promise here could start a stale
87
+ // page after background/route changes; native consumes loading before start.
88
+ this.report(
89
+ this.nativeRUM.startViewWithTrackingType(
90
+ route.name,
91
+ bridgeContextManager.mergeWithLocalPropertiesSync(),
92
+ 1 // Automatic is an internal framework bridge detail.
93
+ )
94
+ );
95
+ return this.active.generation;
96
+ }
97
+
98
+ /** Apply a completed interval only to the appearance that started it. */
99
+ updateLoadingTime(
100
+ key: string,
101
+ generation: number | undefined,
102
+ durationNs: number
103
+ ): void {
104
+ if (
105
+ this.state !== 'active' ||
106
+ !this.active ||
107
+ this.active.key !== key ||
108
+ this.active.generation !== generation ||
109
+ !Number.isSafeInteger(durationNs) ||
110
+ durationNs <= 0
111
+ )
112
+ return;
113
+ // Enqueue immediately; never let a Promise continuation update another View.
114
+ this.report(FTReactNativeRUM.updateViewLoadingTime(durationNs));
115
+ }
116
+
117
+ /** Only the appearance that owns the active generation may close it. */
118
+ hide(key: string, generation: number | undefined): void {
119
+ if (this.state !== 'active') return;
120
+ if (
121
+ !this.active ||
122
+ this.active.key !== key ||
123
+ this.active.generation !== generation
124
+ )
125
+ return;
126
+ this.active = undefined;
127
+ this.current = undefined;
128
+ this.report(FTReactNativeRUM.stopView());
129
+ }
130
+
131
+ stop(): void {
132
+ ++this.lifecycle;
133
+ this.subscription?.remove();
134
+ this.subscription = undefined;
135
+ if (this.active && this.state === 'active')
136
+ this.report(FTReactNativeRUM.stopView());
137
+ this.active = undefined;
138
+ this.current = undefined;
139
+ this.loading.clear();
140
+ // Never reuse generation values: callbacks from a previous subscription can arrive late.
141
+ }
142
+
143
+ private onAppState = (state: AppStateStatus): void => {
144
+ if (state === this.state) return;
145
+ this.state = state;
146
+ if (state === 'background') {
147
+ // Native closes Automatic Views at its own history boundary. No JS stop.
148
+ this.active = undefined;
149
+ this.loading.clear();
150
+ this.onBackground?.();
151
+ } else if (state === 'active' && this.current) {
152
+ this.show(this.current);
153
+ }
154
+ };
155
+
156
+ private pruneLoading(): void {
157
+ const now =
158
+ typeof performance !== 'undefined' &&
159
+ typeof performance.now === 'function'
160
+ ? performance.now()
161
+ : NaN;
162
+ for (const [key, load] of this.loading) {
163
+ const age = now - load.monotonicMs;
164
+ if (!Number.isFinite(age) || age < 0 || age >= MAX_PENDING_LOAD_MS)
165
+ this.loading.delete(key);
166
+ }
167
+ }
168
+
169
+ private get nativeRUM(): NativeRUMSpec {
170
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
171
+ return require('../specs/NativeFTReactNativeRUM').default;
172
+ }
173
+
174
+ private report(result: Promise<void>): void {
175
+ result.catch((error) =>
176
+ console.warn('[TrueWatch] Automatic View tracking failed', error)
177
+ );
178
+ }
179
+ }
@@ -0,0 +1,107 @@
1
+ import NativeWebSocket from '../specs/NativeFTReactNativeWebSocket';
2
+ import { bridgeContextManager } from '../ft_mobile_agent';
3
+ import type { WebSocketTimingMetrics } from './FTRumWebSocketTiming';
4
+
5
+ export interface NativeWebSocketResource {
6
+ url: string;
7
+ httpMethod: string;
8
+ requestHeader: Record<string, unknown>;
9
+ webSocketEvent: string;
10
+ errorMessage?: string;
11
+ }
12
+
13
+ function ignoreBridgeFailure(call: () => unknown): void {
14
+ try {
15
+ void Promise.resolve(call()).catch(() => {});
16
+ } catch {
17
+ /* Setup/cleanup replies must not interrupt JS capture. */
18
+ }
19
+ }
20
+
21
+ export class NativeWebSocketSession {
22
+ private stopped = false;
23
+ private sequence = 0;
24
+
25
+ constructor(private readonly session: string) {
26
+ // Enqueue setup before start/stop/add on the same native module queue.
27
+ // Its reply never gates JS capture or selects a different reporting route.
28
+ ignoreBridgeFailure(() => NativeWebSocket!.startCapture(session));
29
+ }
30
+
31
+ startResource(
32
+ key: string,
33
+ socketID: number | undefined,
34
+ url: string
35
+ ): Promise<void> {
36
+ const property = bridgeContextManager.mergeWithLocalPropertiesSync();
37
+ return NativeWebSocket!.startResource(
38
+ key,
39
+ socketID ?? -1,
40
+ url,
41
+ this.session,
42
+ ++this.sequence,
43
+ property
44
+ );
45
+ }
46
+
47
+ stopResource(key: string, event: string): Promise<void> {
48
+ const property = bridgeContextManager.mergeWithLocalPropertiesSync();
49
+ return NativeWebSocket!.stopResource(key, event, this.session, property);
50
+ }
51
+
52
+ addResource(
53
+ key: string,
54
+ content: NativeWebSocketResource,
55
+ metrics: Partial<WebSocketTimingMetrics>
56
+ ): Promise<void> {
57
+ return NativeWebSocket!.addResource(key, content, metrics, this.session);
58
+ }
59
+
60
+ release(key: string): void {
61
+ ignoreBridgeFailure(() =>
62
+ NativeWebSocket?.releaseResource(key, this.session)
63
+ );
64
+ }
65
+
66
+ stop(): void {
67
+ if (this.stopped) return;
68
+ this.stopped = true;
69
+ // Existing handshakes still finish through their originally selected route.
70
+ ignoreBridgeFailure(() => NativeWebSocket?.stopCapture(this.session));
71
+ }
72
+ }
73
+
74
+ export function startNativeWebSocketSession(
75
+ createKey: () => string
76
+ ): NativeWebSocketSession | undefined {
77
+ try {
78
+ if (
79
+ typeof NativeWebSocket?.startCapture === 'function' &&
80
+ typeof NativeWebSocket.stopCapture === 'function' &&
81
+ typeof NativeWebSocket.clear === 'function' &&
82
+ typeof NativeWebSocket.startResource === 'function' &&
83
+ typeof NativeWebSocket.stopResource === 'function' &&
84
+ typeof NativeWebSocket.addResource === 'function' &&
85
+ typeof NativeWebSocket.releaseResource === 'function'
86
+ )
87
+ return new NativeWebSocketSession(createKey());
88
+ } catch {
89
+ /* Old native binaries keep the JS fallback. */
90
+ }
91
+ return undefined;
92
+ }
93
+
94
+ export function clearNativeWebSocketSessions(): void {
95
+ ignoreBridgeFailure(() => NativeWebSocket?.clear());
96
+ }
97
+
98
+ export function webSocketNativeID(socket: unknown): number | undefined {
99
+ try {
100
+ const id = (socket as { _socketId?: unknown })?._socketId;
101
+ return typeof id === 'number' && Number.isSafeInteger(id) && id >= 0
102
+ ? id
103
+ : undefined;
104
+ } catch {
105
+ return undefined;
106
+ }
107
+ }