@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.
- package/FTMobileReactNativeSDK.podspec +7 -5
- package/README.md +34 -0
- package/android/build.gradle +9 -2
- package/android/src/main/java/com/ft/sdk/reactnative/FTMobileImpl.java +45 -14
- package/android/src/main/java/com/ft/sdk/reactnative/FTRUMImpl.java +85 -2
- package/android/src/main/java/com/ft/sdk/reactnative/FTTraceImpl.java +5 -1
- package/android/src/main/java/com/ft/sdk/reactnative/JsLongTaskMonitor.java +281 -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 +9 -5
- package/android/src/newarch/java/com/ft/sdk/reactnative/FTRUMModule.java +26 -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 +2 -2
- package/android/src/oldarch/java/com/ft/sdk/reactnative/FTRUMModule.java +29 -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/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 +23 -16
- package/ios/FTReactNativeLog.mm +3 -4
- package/ios/FTReactNativeRUM.mm +103 -28
- package/ios/FTReactNativeResource.h +4 -0
- package/ios/FTReactNativeResource.m +58 -0
- package/ios/FTReactNativeTrace.mm +56 -13
- package/ios/FTReactNativeUtils.m +10 -15
- 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 +16 -0
- package/ios/FTWebSocketResourceData.m +75 -0
- package/lib/commonjs/ft_mobile_agent.js +36 -3
- package/lib/commonjs/ft_mobile_agent.js.map +1 -1
- package/lib/commonjs/ft_rum.js +46 -2
- package/lib/commonjs/ft_rum.js.map +1 -1
- package/lib/commonjs/ft_tracing.js +12 -3
- package/lib/commonjs/ft_tracing.js.map +1 -1
- package/lib/commonjs/index.js +14 -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/FTRumWebSocketNative.js +73 -0
- package/lib/commonjs/rum/FTRumWebSocketNative.js.map +1 -0
- package/lib/commonjs/rum/FTRumWebSocketTiming.js +55 -0
- package/lib/commonjs/rum/FTRumWebSocketTiming.js.map +1 -0
- package/lib/commonjs/rum/FTRumWebSocketTracking.js +510 -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/module/ft_mobile_agent.js +36 -3
- package/lib/module/ft_mobile_agent.js.map +1 -1
- package/lib/module/ft_rum.js +46 -2
- package/lib/module/ft_rum.js.map +1 -1
- package/lib/module/ft_tracing.js +12 -1
- package/lib/module/ft_tracing.js.map +1 -1
- package/lib/module/index.js +3 -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/FTRumWebSocketNative.js +62 -0
- package/lib/module/rum/FTRumWebSocketNative.js.map +1 -0
- package/lib/module/rum/FTRumWebSocketTiming.js +49 -0
- package/lib/module/rum/FTRumWebSocketTiming.js.map +1 -0
- package/lib/module/rum/FTRumWebSocketTracking.js +503 -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/typescript/ft_mobile_agent.d.ts +7 -0
- package/lib/typescript/ft_rum.d.ts +9 -0
- package/lib/typescript/index.d.ts +3 -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/FTRumWebSocketNative.d.ts +23 -0
- package/lib/typescript/rum/FTRumWebSocketTiming.d.ts +2 -0
- package/lib/typescript/rum/FTRumWebSocketTracking.d.ts +56 -0
- package/lib/typescript/rum/longTasksUtils.d.ts +8 -0
- package/lib/typescript/specs/NativeFTMobileReactNative.d.ts +14 -0
- package/lib/typescript/specs/NativeFTReactNativeRUM.d.ts +4 -0
- 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/package.json +7 -6
- package/src/ft_mobile_agent.tsx +49 -6
- package/src/ft_rum.tsx +61 -2
- package/src/ft_tracing.tsx +15 -1
- package/src/index.tsx +4 -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/FTRumWebSocketNative.ts +106 -0
- package/src/rum/FTRumWebSocketTiming.ts +50 -0
- package/src/rum/FTRumWebSocketTracking.ts +727 -0
- package/src/rum/longTasksUtils.ts +17 -0
- package/src/specs/NativeFTMobileReactNative.ts +17 -0
- package/src/specs/NativeFTReactNativeRUM.ts +4 -0
- package/src/specs/NativeFTReactNativeTrace.ts +12 -0
- package/src/specs/NativeFTReactNativeWebSocket.ts +33 -0
- package/src/version.ts +1 -1
package/src/ft_tracing.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|