@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.
- package/FTMobileReactNativeSDK.podspec +7 -5
- package/android/build.gradle +10 -3
- package/android/src/main/java/com/ft/sdk/reactnative/FTLogImpl.java +10 -2
- package/android/src/main/java/com/ft/sdk/reactnative/FTMobileImpl.java +45 -10
- package/android/src/main/java/com/ft/sdk/reactnative/FTRUMImpl.java +131 -6
- package/android/src/main/java/com/ft/sdk/reactnative/FTTraceImpl.java +10 -3
- package/android/src/main/java/com/ft/sdk/reactnative/JsLongTaskMonitor.java +281 -0
- package/android/src/main/java/com/ft/sdk/reactnative/ViewTiming.java +17 -0
- package/android/src/main/java/com/ft/sdk/reactnative/VisitorIdConfigMapper.java +50 -0
- package/android/src/main/java/com/ft/sdk/reactnative/utils/ReactNativeUtils.java +5 -12
- package/android/src/newarch/java/com/ft/sdk/reactnative/FTMobileModule.java +14 -5
- package/android/src/newarch/java/com/ft/sdk/reactnative/FTRUMModule.java +44 -1
- package/android/src/newarch/java/com/ft/sdk/reactnative/FTTraceModule.java +14 -0
- package/android/src/oldarch/java/com/ft/sdk/reactnative/FTMobileModule.java +7 -2
- package/android/src/oldarch/java/com/ft/sdk/reactnative/FTRUMModule.java +39 -1
- package/android/src/oldarch/java/com/ft/sdk/reactnative/FTTraceModule.java +12 -0
- package/android/src/rnpost70/java/com/ft/sdk/reactnative/ReactJsThreadExecutor.java +21 -0
- package/android/src/rnpre70/java/com/ft/sdk/reactnative/ReactJsThreadExecutor.java +37 -0
- package/android/src/test/java/com/ft/sdk/reactnative/FTMobileImplTest.java +53 -0
- package/android/src/test/java/com/ft/sdk/reactnative/FTRUMModuleTest.java +117 -0
- package/android/src/test/java/com/ft/sdk/reactnative/JsLongTaskMonitorTest.java +446 -0
- package/android/src/test/java/com/ft/sdk/reactnative/utils/ReactNativeUtilsTest.java +33 -0
- package/ios/FTJSLongTaskMonitor.h +51 -0
- package/ios/FTJSLongTaskMonitor.m +250 -0
- package/ios/FTMobileReactNative.mm +54 -18
- package/ios/FTReactNativeLog.mm +9 -5
- package/ios/FTReactNativeRUM.mm +196 -31
- package/ios/FTReactNativeResource.h +4 -0
- package/ios/FTReactNativeResource.m +64 -0
- package/ios/FTReactNativeTrace.mm +59 -14
- package/ios/FTReactNativeUtils.m +10 -15
- package/ios/FTReactNativeViewTiming.h +18 -0
- package/ios/FTReactNativeWebSocket.h +10 -0
- package/ios/FTReactNativeWebSocket.mm +201 -0
- package/ios/FTWebSocketMetadataStore.h +25 -0
- package/ios/FTWebSocketMetadataStore.m +622 -0
- package/ios/FTWebSocketResourceData.h +20 -0
- package/ios/FTWebSocketResourceData.m +106 -0
- package/lib/commonjs/ft_logger.js +3 -1
- package/lib/commonjs/ft_logger.js.map +1 -1
- package/lib/commonjs/ft_mobile_agent.js +47 -4
- package/lib/commonjs/ft_mobile_agent.js.map +1 -1
- package/lib/commonjs/ft_rum.js +65 -5
- package/lib/commonjs/ft_rum.js.map +1 -1
- package/lib/commonjs/ft_tracing.js +14 -4
- package/lib/commonjs/ft_tracing.js.map +1 -1
- package/lib/commonjs/index.js +21 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/rum/FTBabelInteractionTracking.js +72 -0
- package/lib/commonjs/rum/FTBabelInteractionTracking.js.map +1 -0
- package/lib/commonjs/rum/FTBabelUtils.js +87 -0
- package/lib/commonjs/rum/FTBabelUtils.js.map +1 -0
- package/lib/commonjs/rum/FTRumActionTracking.js +10 -39
- package/lib/commonjs/rum/FTRumActionTracking.js.map +1 -1
- package/lib/commonjs/rum/FTRumActionTrackingContext.js +50 -0
- package/lib/commonjs/rum/FTRumActionTrackingContext.js.map +1 -0
- package/lib/commonjs/rum/FTRumViewTracking.js +147 -0
- package/lib/commonjs/rum/FTRumViewTracking.js.map +1 -0
- package/lib/commonjs/rum/FTRumWebSocketNative.js +73 -0
- package/lib/commonjs/rum/FTRumWebSocketNative.js.map +1 -0
- package/lib/commonjs/rum/FTRumWebSocketTiming.js +73 -0
- package/lib/commonjs/rum/FTRumWebSocketTiming.js.map +1 -0
- package/lib/commonjs/rum/FTRumWebSocketTracking.js +506 -0
- package/lib/commonjs/rum/FTRumWebSocketTracking.js.map +1 -0
- package/lib/commonjs/rum/longTasksUtils.js +25 -0
- package/lib/commonjs/rum/longTasksUtils.js.map +1 -0
- package/lib/commonjs/specs/NativeFTMobileReactNative.js.map +1 -1
- package/lib/commonjs/specs/NativeFTReactNativeRUM.js.map +1 -1
- package/lib/commonjs/specs/NativeFTReactNativeTrace.js.map +1 -1
- package/lib/commonjs/specs/NativeFTReactNativeWebSocket.js +11 -0
- package/lib/commonjs/specs/NativeFTReactNativeWebSocket.js.map +1 -0
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/commonjs/visitorIdConfig.js +37 -0
- package/lib/commonjs/visitorIdConfig.js.map +1 -0
- package/lib/module/ft_logger.js +3 -1
- package/lib/module/ft_logger.js.map +1 -1
- package/lib/module/ft_mobile_agent.js +47 -4
- package/lib/module/ft_mobile_agent.js.map +1 -1
- package/lib/module/ft_rum.js +65 -5
- package/lib/module/ft_rum.js.map +1 -1
- package/lib/module/ft_tracing.js +14 -2
- package/lib/module/ft_tracing.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/rum/FTBabelInteractionTracking.js +66 -0
- package/lib/module/rum/FTBabelInteractionTracking.js.map +1 -0
- package/lib/module/rum/FTBabelUtils.js +80 -0
- package/lib/module/rum/FTBabelUtils.js.map +1 -0
- package/lib/module/rum/FTRumActionTracking.js +10 -39
- package/lib/module/rum/FTRumActionTracking.js.map +1 -1
- package/lib/module/rum/FTRumActionTrackingContext.js +43 -0
- package/lib/module/rum/FTRumActionTrackingContext.js.map +1 -0
- package/lib/module/rum/FTRumViewTracking.js +140 -0
- package/lib/module/rum/FTRumViewTracking.js.map +1 -0
- package/lib/module/rum/FTRumWebSocketNative.js +62 -0
- package/lib/module/rum/FTRumWebSocketNative.js.map +1 -0
- package/lib/module/rum/FTRumWebSocketTiming.js +67 -0
- package/lib/module/rum/FTRumWebSocketTiming.js.map +1 -0
- package/lib/module/rum/FTRumWebSocketTracking.js +499 -0
- package/lib/module/rum/FTRumWebSocketTracking.js.map +1 -0
- package/lib/module/rum/longTasksUtils.js +18 -0
- package/lib/module/rum/longTasksUtils.js.map +1 -0
- package/lib/module/specs/NativeFTMobileReactNative.js.map +1 -1
- package/lib/module/specs/NativeFTReactNativeRUM.js.map +1 -1
- package/lib/module/specs/NativeFTReactNativeTrace.js.map +1 -1
- package/lib/module/specs/NativeFTReactNativeWebSocket.js +8 -0
- package/lib/module/specs/NativeFTReactNativeWebSocket.js.map +1 -0
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/module/visitorIdConfig.js +31 -0
- package/lib/module/visitorIdConfig.js.map +1 -0
- package/lib/typescript/ft_logger.d.ts +11 -1
- package/lib/typescript/ft_mobile_agent.d.ts +28 -0
- package/lib/typescript/ft_rum.d.ts +42 -5
- package/lib/typescript/ft_tracing.d.ts +9 -1
- package/lib/typescript/index.d.ts +5 -1
- package/lib/typescript/rum/FTBabelInteractionTracking.d.ts +30 -0
- package/lib/typescript/rum/FTBabelUtils.d.ts +1 -0
- package/lib/typescript/rum/FTRumActionTracking.d.ts +0 -2
- package/lib/typescript/rum/FTRumActionTrackingContext.d.ts +11 -0
- package/lib/typescript/rum/FTRumViewTracking.d.ts +32 -0
- package/lib/typescript/rum/FTRumWebSocketNative.d.ts +22 -0
- package/lib/typescript/rum/FTRumWebSocketTiming.d.ts +7 -0
- package/lib/typescript/rum/FTRumWebSocketTracking.d.ts +54 -0
- package/lib/typescript/rum/longTasksUtils.d.ts +8 -0
- package/lib/typescript/specs/NativeFTMobileReactNative.d.ts +15 -0
- package/lib/typescript/specs/NativeFTReactNativeRUM.d.ts +11 -1
- package/lib/typescript/specs/NativeFTReactNativeTrace.d.ts +12 -0
- package/lib/typescript/specs/NativeFTReactNativeWebSocket.d.ts +13 -0
- package/lib/typescript/version.d.ts +1 -1
- package/lib/typescript/visitorIdConfig.d.ts +11 -0
- package/package.json +6 -4
- package/src/ft_logger.tsx +11 -1
- package/src/ft_mobile_agent.tsx +79 -7
- package/src/ft_rum.tsx +111 -7
- package/src/ft_tracing.tsx +24 -2
- package/src/index.tsx +8 -0
- package/src/rum/FTBabelInteractionTracking.ts +124 -0
- package/src/rum/FTBabelUtils.tsx +99 -0
- package/src/rum/FTRumActionTracking.tsx +14 -43
- package/src/rum/FTRumActionTrackingContext.ts +68 -0
- package/src/rum/FTRumViewTracking.ts +179 -0
- package/src/rum/FTRumWebSocketNative.ts +107 -0
- package/src/rum/FTRumWebSocketTiming.ts +82 -0
- package/src/rum/FTRumWebSocketTracking.ts +722 -0
- package/src/rum/longTasksUtils.ts +17 -0
- package/src/specs/NativeFTMobileReactNative.ts +18 -0
- package/src/specs/NativeFTReactNativeRUM.ts +19 -1
- package/src/specs/NativeFTReactNativeTrace.ts +12 -0
- package/src/specs/NativeFTReactNativeWebSocket.ts +33 -0
- package/src/version.ts +1 -1
- 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
|
-
|
|
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
|
|
116
|
-
if (
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|