@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,722 @@
1
+ /* global WebSocket */
2
+ import { Platform } from 'react-native';
3
+ import NativeFTReactNativeTrace from '../specs/NativeFTReactNativeTrace';
4
+ import { startWebSocketTiming } from './FTRumWebSocketTiming';
5
+ import type { WebSocketTimingMetrics } from './FTRumWebSocketTiming';
6
+ import {
7
+ clearNativeWebSocketSessions,
8
+ startNativeWebSocketSession,
9
+ webSocketNativeID,
10
+ } from './FTRumWebSocketNative';
11
+ import type {
12
+ NativeWebSocketSession,
13
+ NativeWebSocketResource,
14
+ } from './FTRumWebSocketNative';
15
+
16
+ type WebSocketConstructor = typeof WebSocket;
17
+ type WebSocketHandshakeState = 'success' | 'rejected' | 'failed';
18
+
19
+ interface ReactNativeWebSocketOptions {
20
+ headers?: Record<string, unknown>;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ interface FTRUMWebSocketResource {
25
+ url: string;
26
+ httpMethod: string;
27
+ requestHeader: Record<string, unknown>;
28
+ resourceStatus: number;
29
+ responseHeader?: Record<string, string>;
30
+ resourceType: 'websocket';
31
+ webSocketHandshake: true;
32
+ webSocketHandshakeState: WebSocketHandshakeState;
33
+ errorMessage?: string;
34
+ }
35
+
36
+ interface WebSocketHandshakeError {
37
+ message?: string;
38
+ }
39
+
40
+ interface FTRumWebSocketResourceReporter {
41
+ startResource(key: string): Promise<void>;
42
+ stopResource(key: string): Promise<void>;
43
+ addResource(
44
+ key: string,
45
+ resource: FTRUMWebSocketResource,
46
+ metrics?: WebSocketTimingMetrics
47
+ ): Promise<void>;
48
+ }
49
+
50
+ type RuntimeGlobal = typeof globalThis & {
51
+ WebSocket: WebSocketConstructor;
52
+ __DEV__?: boolean;
53
+ };
54
+
55
+ const REACT_NATIVE_DEV_WEBSOCKET =
56
+ /^wss?:\/\/(?:(?:10|172|192)\.[0-9]+\.[0-9]+\.[0-9]+|localhost|127\.0\.0\.1|\[::1\]):808[0-9]\/(?:hot|symbolicate|message|inspector|status|assets|logs|debugger-proxy)(?:[/?#].*)?$/i;
57
+
58
+ // SocketRocket embeds the actual response status in this description, which
59
+ // RCTWebSocketModule forwards to JS. Do not match proxy errors or bare numbers.
60
+ const SOCKET_ROCKET_HTTP_ERROR =
61
+ /^Received\s+bad\s+response\s+code\s+from\s+server\s*:\s*([1-9]\d{2})\s*\.?$/i;
62
+ const MAX_HTTP_ERROR_PARSE_LENGTH = 512;
63
+
64
+ export class FTRumWebSocketTracking {
65
+ private static enabled = false;
66
+ private static installed = false;
67
+ private static originalWebSocket: WebSocketConstructor | null = null;
68
+ private static instrumentedWebSocket: WebSocketConstructor | null = null;
69
+ private static resourceReporter: FTRumWebSocketResourceReporter | null = null;
70
+ private static nativeAutoTraceEnabled = false;
71
+ private static warnedFailures = new Set<string>();
72
+ private static lifecycleVersion = 0;
73
+ private static nativeSession: NativeWebSocketSession | undefined;
74
+ private static pendingHandshakes = new Set<() => void>();
75
+
76
+ /** Identifies configuration and reporting work started before SDK shutdown. */
77
+ static getLifecycleVersion(): number {
78
+ return FTRumWebSocketTracking.lifecycleVersion;
79
+ }
80
+
81
+ static shutDown(): void {
82
+ FTRumWebSocketTracking.lifecycleVersion += 1;
83
+ clearNativeWebSocketSessions();
84
+ FTRumWebSocketTracking.setNativeAutoTraceEnabled(false);
85
+ FTRumWebSocketTracking.stopTracking();
86
+ FTRumWebSocketTracking.pendingHandshakes.forEach((removeListeners) =>
87
+ removeListeners()
88
+ );
89
+ FTRumWebSocketTracking.pendingHandshakes.clear();
90
+ FTRumWebSocketTracking.warnedFailures.clear();
91
+ }
92
+
93
+ static setNativeAutoTraceEnabled(enabled: boolean): void {
94
+ FTRumWebSocketTracking.nativeAutoTraceEnabled = enabled;
95
+ }
96
+
97
+ private static stopNativeSession(): void {
98
+ FTRumWebSocketTracking.nativeSession?.stop();
99
+ FTRumWebSocketTracking.nativeSession = undefined;
100
+ }
101
+
102
+ static startTracking(resourceReporter: FTRumWebSocketResourceReporter): void {
103
+ if (Platform.OS !== 'ios') {
104
+ return;
105
+ }
106
+
107
+ FTRumWebSocketTracking.enabled = true;
108
+ FTRumWebSocketTracking.resourceReporter = resourceReporter;
109
+ const runtimeGlobal = globalThis as RuntimeGlobal;
110
+ const originalWebSocket = runtimeGlobal.WebSocket;
111
+ if (originalWebSocket === FTRumWebSocketTracking.instrumentedWebSocket) {
112
+ return;
113
+ }
114
+ if (typeof originalWebSocket !== 'function') {
115
+ FTRumWebSocketTracking.stopNativeSession();
116
+ return;
117
+ }
118
+
119
+ if (!FTRumWebSocketTracking.nativeSession) {
120
+ FTRumWebSocketTracking.nativeSession = startNativeWebSocketSession(() =>
121
+ FTRumWebSocketTracking.createResourceKey()
122
+ );
123
+ }
124
+
125
+ const InstrumentedWebSocket = function (
126
+ this: WebSocket,
127
+ ...args: unknown[]
128
+ ): WebSocket {
129
+ if (!new.target) {
130
+ return Reflect.apply(originalWebSocket, this, args);
131
+ }
132
+ // A constructor retained before stop/reinitialization must stay passive.
133
+ if (
134
+ FTRumWebSocketTracking.instrumentedWebSocket !== InstrumentedWebSocket
135
+ ) {
136
+ return Reflect.construct(originalWebSocket, args, new.target);
137
+ }
138
+ return FTRumWebSocketTracking.createWebSocket(
139
+ originalWebSocket,
140
+ args,
141
+ new.target as unknown as WebSocketConstructor
142
+ );
143
+ } as unknown as WebSocketConstructor;
144
+
145
+ try {
146
+ InstrumentedWebSocket.prototype = originalWebSocket.prototype;
147
+ Object.setPrototypeOf(InstrumentedWebSocket, originalWebSocket);
148
+ runtimeGlobal.WebSocket = InstrumentedWebSocket;
149
+ if (runtimeGlobal.WebSocket !== InstrumentedWebSocket) {
150
+ FTRumWebSocketTracking.warnOnce(
151
+ 'Unable to install WebSocket tracking.'
152
+ );
153
+ FTRumWebSocketTracking.stopNativeSession();
154
+ return;
155
+ }
156
+ FTRumWebSocketTracking.originalWebSocket = originalWebSocket;
157
+ FTRumWebSocketTracking.instrumentedWebSocket = InstrumentedWebSocket;
158
+ FTRumWebSocketTracking.installed = true;
159
+ } catch {
160
+ FTRumWebSocketTracking.stopNativeSession();
161
+ FTRumWebSocketTracking.warnOnce('Unable to install WebSocket tracking.');
162
+ }
163
+ }
164
+
165
+ static stopTracking(): void {
166
+ FTRumWebSocketTracking.stopNativeSession();
167
+ FTRumWebSocketTracking.enabled = false;
168
+ FTRumWebSocketTracking.resourceReporter = null;
169
+ if (!FTRumWebSocketTracking.installed) {
170
+ return;
171
+ }
172
+
173
+ const runtimeGlobal = globalThis as RuntimeGlobal;
174
+ if (
175
+ runtimeGlobal.WebSocket ===
176
+ FTRumWebSocketTracking.instrumentedWebSocket &&
177
+ FTRumWebSocketTracking.originalWebSocket
178
+ ) {
179
+ try {
180
+ runtimeGlobal.WebSocket = FTRumWebSocketTracking.originalWebSocket;
181
+ if (
182
+ runtimeGlobal.WebSocket !== FTRumWebSocketTracking.originalWebSocket
183
+ ) {
184
+ FTRumWebSocketTracking.warnOnce(
185
+ 'Unable to restore WebSocket constructor.'
186
+ );
187
+ return;
188
+ }
189
+ FTRumWebSocketTracking.installed = false;
190
+ FTRumWebSocketTracking.originalWebSocket = null;
191
+ FTRumWebSocketTracking.instrumentedWebSocket = null;
192
+ } catch {
193
+ // The disabled wrapper remains a passthrough if another library froze it.
194
+ FTRumWebSocketTracking.warnOnce(
195
+ 'Unable to restore WebSocket constructor.'
196
+ );
197
+ }
198
+ }
199
+ }
200
+
201
+ private static createWebSocket(
202
+ OriginalWebSocket: WebSocketConstructor,
203
+ originalArguments: unknown[],
204
+ newTarget: WebSocketConstructor
205
+ ): WebSocket {
206
+ if (!FTRumWebSocketTracking.enabled) {
207
+ return Reflect.construct(OriginalWebSocket, originalArguments, newTarget);
208
+ }
209
+
210
+ const resourceReporter = FTRumWebSocketTracking.resourceReporter;
211
+ if (!resourceReporter) {
212
+ return Reflect.construct(OriginalWebSocket, originalArguments, newTarget);
213
+ }
214
+
215
+ // Leave coercion and validation of nonstandard arguments to the constructor.
216
+ const url = originalArguments[0];
217
+ if (typeof url !== 'string') {
218
+ return Reflect.construct(OriginalWebSocket, originalArguments, newTarget);
219
+ }
220
+ const runtimeGlobal = globalThis as RuntimeGlobal;
221
+ if (
222
+ runtimeGlobal.__DEV__ === true &&
223
+ REACT_NATIVE_DEV_WEBSOCKET.test(url)
224
+ ) {
225
+ return Reflect.construct(OriginalWebSocket, originalArguments, newTarget);
226
+ }
227
+
228
+ const options = originalArguments[2];
229
+ if (
230
+ options !== undefined &&
231
+ (!options || typeof options !== 'object' || Array.isArray(options))
232
+ ) {
233
+ return Reflect.construct(OriginalWebSocket, originalArguments, newTarget);
234
+ }
235
+ const lifecycleVersion = FTRumWebSocketTracking.lifecycleVersion;
236
+ let resourceKey: string;
237
+ let traceKey: string | undefined;
238
+ let argumentsWithHeaders: unknown[];
239
+ let requestHeaders: Record<string, unknown>;
240
+ try {
241
+ resourceKey = FTRumWebSocketTracking.createResourceKey();
242
+ // Generation may cache correlation before the bridge returns or throws.
243
+ if (FTRumWebSocketTracking.nativeAutoTraceEnabled) traceKey = resourceKey;
244
+ const traceHeaders = traceKey
245
+ ? FTRumWebSocketTracking.getTraceHeaders(url, traceKey)
246
+ : {};
247
+ ({ argumentsWithHeaders, requestHeaders } =
248
+ FTRumWebSocketTracking.mergeTraceHeaders(
249
+ originalArguments,
250
+ traceHeaders
251
+ ));
252
+ } catch {
253
+ FTRumWebSocketTracking.cancelTrace(traceKey);
254
+ FTRumWebSocketTracking.warnOnce('Unable to prepare WebSocket tracking.');
255
+ return Reflect.construct(OriginalWebSocket, originalArguments, newTarget);
256
+ }
257
+
258
+ const stopTiming = startWebSocketTiming();
259
+ let socket: WebSocket;
260
+ let completed = false;
261
+ let cancelled = false;
262
+ let pendingCloseError: WebSocketHandshakeError | undefined;
263
+ let startPromise: Promise<boolean> | undefined;
264
+ // Decide once before reporting. Never switch routes after an ambiguous
265
+ // native result: start/add may already have reached the SDK.
266
+ const nativeSession = FTRumWebSocketTracking.nativeSession;
267
+ let nativeResourceStarted = false;
268
+ const removeListeners = (keepFailureClose = false) => {
269
+ completed = true;
270
+ if (!keepFailureClose) {
271
+ pendingCloseError = undefined;
272
+ FTRumWebSocketTracking.pendingHandshakes.delete(cancelHandshake);
273
+ }
274
+ // Cleanup failures must neither interrupt business dispatch nor skip the
275
+ // other listeners. Completed guards also protect callbacks already queued.
276
+ const removers = keepFailureClose
277
+ ? listenerRemovers.slice(0, 2)
278
+ : listenerRemovers;
279
+ for (const remove of removers) {
280
+ try {
281
+ remove();
282
+ } catch {
283
+ FTRumWebSocketTracking.warnOnce(
284
+ 'Unable to remove WebSocket listener.'
285
+ );
286
+ }
287
+ }
288
+ };
289
+ const releaseNativeResource = () => {
290
+ if (nativeResourceStarted) {
291
+ nativeResourceStarted = false;
292
+ nativeSession?.release(resourceKey);
293
+ }
294
+ };
295
+ const cancelHandshake = () => {
296
+ cancelled = true;
297
+ removeListeners();
298
+ releaseNativeResource();
299
+ };
300
+ const beginResource = () => {
301
+ if (!startPromise) {
302
+ nativeResourceStarted = nativeSession !== undefined;
303
+ const start = () =>
304
+ nativeSession
305
+ ? nativeSession.startResource(
306
+ resourceKey,
307
+ webSocketNativeID(socket),
308
+ url
309
+ )
310
+ : resourceReporter.startResource(resourceKey);
311
+ startPromise = FTRumWebSocketTracking.startResource(start).catch(() => {
312
+ cancelHandshake();
313
+ if (lifecycleVersion === FTRumWebSocketTracking.lifecycleVersion) {
314
+ FTRumWebSocketTracking.warnOnce(
315
+ 'Unable to start WebSocket resource.'
316
+ );
317
+ }
318
+ return false;
319
+ });
320
+ }
321
+ return startPromise;
322
+ };
323
+ const complete = (
324
+ resourceStatus: number,
325
+ state: WebSocketHandshakeState,
326
+ getError?: () => WebSocketHandshakeError,
327
+ event: 'open' | 'error' | 'close' = 'error',
328
+ supplementFailureClose = false
329
+ ) => {
330
+ if (completed) {
331
+ return;
332
+ }
333
+ completed = true;
334
+ const timing = stopTiming();
335
+ // Freeze the observed boundary before inspecting or parsing error data.
336
+ const error = getError?.();
337
+ if (
338
+ cancelled ||
339
+ lifecycleVersion !== FTRumWebSocketTracking.lifecycleVersion
340
+ ) {
341
+ removeListeners();
342
+ return;
343
+ }
344
+ let errorReady: Promise<void> | undefined;
345
+ if (supplementFailureClose && error && !error.message) {
346
+ // RN's built-in EventTarget emits error without message, immediately
347
+ // followed by close(1006, nativeErrorMessage) in the same native-event
348
+ // callback. This also covers RN 0.79's opt-in implementation without
349
+ // guessing behavior from a version number. Freeze time and stop now;
350
+ // allow only that synchronous close to supplement the error text.
351
+ pendingCloseError = error;
352
+ removeListeners(true);
353
+ errorReady = Promise.resolve().then(() => removeListeners());
354
+ } else {
355
+ removeListeners();
356
+ }
357
+ const started = beginResource();
358
+ if (nativeSession) {
359
+ // Invoke stop at the event boundary, without waiting for the start
360
+ // promise or business callbacks. The native module processes in order.
361
+ void FTRumWebSocketTracking.completeNativeResource(
362
+ nativeSession,
363
+ lifecycleVersion,
364
+ resourceKey,
365
+ started,
366
+ {
367
+ url,
368
+ httpMethod: 'GET',
369
+ requestHeader: requestHeaders,
370
+ webSocketEvent: event,
371
+ },
372
+ timing,
373
+ error,
374
+ errorReady
375
+ );
376
+ return;
377
+ }
378
+ void FTRumWebSocketTracking.completeResource(
379
+ resourceReporter,
380
+ lifecycleVersion,
381
+ resourceKey,
382
+ started,
383
+ url,
384
+ requestHeaders,
385
+ resourceStatus,
386
+ state,
387
+ timing,
388
+ error,
389
+ errorReady
390
+ );
391
+ };
392
+ const onOpen = () => complete(0, 'success', undefined, 'open');
393
+ const onError = (event: Event) => {
394
+ complete(
395
+ 0,
396
+ 'failed',
397
+ () => FTRumWebSocketTracking.handshakeErrorFromEvent(event),
398
+ 'error',
399
+ true
400
+ );
401
+ };
402
+ const onClose = (event: Event) => {
403
+ if (pendingCloseError) {
404
+ const error = pendingCloseError;
405
+ const message = FTRumWebSocketTracking.failureMessageFromClose(event);
406
+ // Accessors can reenter shutdown. Do not restore a cancelled error.
407
+ if (pendingCloseError === error) error.message = message;
408
+ removeListeners();
409
+ return;
410
+ }
411
+ // A standalone close is only a terminal signal. Never interpret its
412
+ // business code/reason as a network error or as an HTTP response.
413
+ complete(0, 'failed', undefined, 'close');
414
+ };
415
+ const listenerRemovers = [
416
+ () => socket?.removeEventListener('open', onOpen),
417
+ () => socket?.removeEventListener('error', onError),
418
+ () => socket?.removeEventListener('close', onClose),
419
+ ];
420
+
421
+ try {
422
+ socket = Reflect.construct(
423
+ OriginalWebSocket,
424
+ argumentsWithHeaders,
425
+ newTarget
426
+ );
427
+ } catch (error) {
428
+ complete(0, 'failed', () =>
429
+ FTRumWebSocketTracking.toHandshakeError(error)
430
+ );
431
+ throw error;
432
+ }
433
+
434
+ try {
435
+ socket.addEventListener('open', onOpen);
436
+ socket.addEventListener('error', onError);
437
+ socket.addEventListener('close', onClose);
438
+ if (!completed) {
439
+ FTRumWebSocketTracking.pendingHandshakes.add(cancelHandshake);
440
+ // Do not leave a native Resource behind if listener installation fails.
441
+ beginResource();
442
+ }
443
+ } catch {
444
+ cancelHandshake();
445
+ // A reentrant terminal may already have started normal Resource reporting.
446
+ if (!startPromise) FTRumWebSocketTracking.cancelTrace(traceKey);
447
+ FTRumWebSocketTracking.warnOnce('Unable to observe WebSocket events.');
448
+ }
449
+ return socket;
450
+ }
451
+
452
+ private static createResourceKey(): string {
453
+ return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, (token) => {
454
+ const randomNibble = Math.floor(Math.random() * 16);
455
+ const uuidNibble = token === 'x' ? randomNibble : (randomNibble % 4) + 8;
456
+ return uuidNibble.toString(16);
457
+ });
458
+ }
459
+
460
+ private static cancelTrace(key: string | undefined): void {
461
+ if (!key) return;
462
+ try {
463
+ // Older native binaries do not expose this optional cancellation bridge.
464
+ if (
465
+ typeof NativeFTReactNativeTrace?.cancelWebSocketTrace === 'function'
466
+ ) {
467
+ void Promise.resolve(
468
+ NativeFTReactNativeTrace.cancelWebSocketTrace(key)
469
+ ).catch(() => {});
470
+ }
471
+ } catch {
472
+ // Cleanup must never interrupt the business constructor or its exception.
473
+ }
474
+ }
475
+
476
+ private static getTraceHeaders(
477
+ url: string,
478
+ resourceKey: string
479
+ ): Record<string, unknown> {
480
+ try {
481
+ const headers = NativeFTReactNativeTrace?.getTraceHeaderFieldsSync(
482
+ url,
483
+ resourceKey
484
+ );
485
+ return FTRumWebSocketTracking.toHeaderRecord(headers);
486
+ } catch {
487
+ FTRumWebSocketTracking.warnOnce(
488
+ 'Unable to inject WebSocket trace headers.'
489
+ );
490
+ return {};
491
+ }
492
+ }
493
+
494
+ private static mergeTraceHeaders(
495
+ originalArguments: unknown[],
496
+ traceHeaders: Record<string, unknown>
497
+ ): {
498
+ argumentsWithHeaders: unknown[];
499
+ requestHeaders: Record<string, unknown>;
500
+ } {
501
+ const options = FTRumWebSocketTracking.toOptions(originalArguments[2]);
502
+ const requestHeaders = {
503
+ ...FTRumWebSocketTracking.toHeaderRecord(options.headers),
504
+ };
505
+ const traceHeaderEntries = Object.entries(traceHeaders);
506
+
507
+ if (traceHeaderEntries.length === 0) {
508
+ return {
509
+ argumentsWithHeaders: originalArguments,
510
+ requestHeaders,
511
+ };
512
+ }
513
+
514
+ const traceHeaderNames = new Set(
515
+ traceHeaderEntries.map(([key]) => key.toLowerCase())
516
+ );
517
+ Object.keys(requestHeaders).forEach((key) => {
518
+ if (traceHeaderNames.has(key.toLowerCase())) {
519
+ delete requestHeaders[key];
520
+ }
521
+ });
522
+ traceHeaderEntries.forEach(([key, value]) => {
523
+ requestHeaders[key] = value;
524
+ });
525
+
526
+ const argumentsWithHeaders = [...originalArguments];
527
+ // Read a headers accessor only once. Spreading options here would read it
528
+ // again before replacing it with the already captured header values.
529
+ const optionDescriptors = Object.getOwnPropertyDescriptors(options);
530
+ optionDescriptors.headers = {
531
+ configurable: true,
532
+ enumerable: true,
533
+ writable: true,
534
+ value: requestHeaders,
535
+ };
536
+ argumentsWithHeaders[2] = Object.defineProperties({}, optionDescriptors);
537
+ return { argumentsWithHeaders, requestHeaders };
538
+ }
539
+
540
+ private static toOptions(value: unknown): ReactNativeWebSocketOptions {
541
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
542
+ return value as ReactNativeWebSocketOptions;
543
+ }
544
+ return {};
545
+ }
546
+
547
+ private static toHeaderRecord(value: unknown): Record<string, unknown> {
548
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
549
+ return value as Record<string, unknown>;
550
+ }
551
+ return {};
552
+ }
553
+
554
+ private static eventString(
555
+ event: unknown,
556
+ property: string
557
+ ): string | undefined {
558
+ if (!event || typeof event !== 'object') {
559
+ return undefined;
560
+ }
561
+ try {
562
+ const value = (event as Record<string, unknown>)[property];
563
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
564
+ } catch {
565
+ return undefined;
566
+ }
567
+ }
568
+
569
+ private static handshakeErrorFromEvent(
570
+ event: Event
571
+ ): WebSocketHandshakeError {
572
+ return { message: FTRumWebSocketTracking.eventString(event, 'message') };
573
+ }
574
+
575
+ private static failureMessageFromClose(event: Event): string | undefined {
576
+ try {
577
+ if ((event as Event & { code?: unknown })?.code === 1006) {
578
+ return FTRumWebSocketTracking.eventString(event, 'reason');
579
+ }
580
+ } catch {
581
+ // Optional event details must never interrupt business event dispatch.
582
+ }
583
+ return undefined;
584
+ }
585
+
586
+ private static statusFromError(
587
+ message: string | undefined
588
+ ): number | undefined {
589
+ const match =
590
+ message && message.length <= MAX_HTTP_ERROR_PARSE_LENGTH
591
+ ? SOCKET_ROCKET_HTTP_ERROR.exec(message.trim())
592
+ : null;
593
+ return match ? Number(match[1]) : undefined;
594
+ }
595
+
596
+ private static toHandshakeError(error: unknown): WebSocketHandshakeError {
597
+ try {
598
+ return {
599
+ message:
600
+ error instanceof Error ? error.message || error.name : String(error),
601
+ };
602
+ } catch {
603
+ return {};
604
+ }
605
+ }
606
+
607
+ private static startResource(start: () => Promise<void>): Promise<boolean> {
608
+ try {
609
+ return Promise.resolve(start()).then(() => true);
610
+ } catch (error) {
611
+ return Promise.reject(error);
612
+ }
613
+ }
614
+
615
+ private static async completeResource(
616
+ resourceReporter: FTRumWebSocketResourceReporter,
617
+ lifecycleVersion: number,
618
+ resourceKey: string,
619
+ startPromise: Promise<boolean>,
620
+ url: string,
621
+ requestHeader: Record<string, unknown>,
622
+ resourceStatus: number,
623
+ webSocketHandshakeState: WebSocketHandshakeState,
624
+ timing: WebSocketTimingMetrics | undefined,
625
+ error?: WebSocketHandshakeError,
626
+ errorReady?: Promise<void>
627
+ ): Promise<void> {
628
+ const isCurrentLifecycle = () =>
629
+ lifecycleVersion === FTRumWebSocketTracking.lifecycleVersion;
630
+ try {
631
+ if (!(await startPromise) || !isCurrentLifecycle()) {
632
+ return;
633
+ }
634
+ try {
635
+ await resourceReporter.stopResource(resourceKey);
636
+ } catch {
637
+ // addResource completes the native Resource. Attempt it even if stop
638
+ // failed, but never retry addResource and risk duplicate completion.
639
+ if (isCurrentLifecycle()) {
640
+ FTRumWebSocketTracking.warnOnce('Unable to stop WebSocket resource.');
641
+ }
642
+ }
643
+ if (errorReady) await errorReady;
644
+ // Only old native binaries classify in JS. Resolve supplemental text
645
+ // before normalization; duration was frozen at the first terminal event.
646
+ const httpStatus = FTRumWebSocketTracking.statusFromError(error?.message);
647
+ if (httpStatus !== undefined && webSocketHandshakeState !== 'success') {
648
+ resourceStatus = httpStatus;
649
+ webSocketHandshakeState = httpStatus !== 101 ? 'rejected' : 'failed';
650
+ }
651
+ const resource: FTRUMWebSocketResource = {
652
+ url,
653
+ httpMethod: 'GET',
654
+ requestHeader,
655
+ resourceStatus,
656
+ resourceType: 'websocket',
657
+ webSocketHandshake: true,
658
+ webSocketHandshakeState,
659
+ };
660
+ if (error?.message) resource.errorMessage = error.message;
661
+ if (isCurrentLifecycle()) {
662
+ await resourceReporter.addResource(resourceKey, resource, timing);
663
+ }
664
+ } catch {
665
+ if (isCurrentLifecycle()) {
666
+ FTRumWebSocketTracking.warnOnce('Unable to report WebSocket resource.');
667
+ }
668
+ }
669
+ }
670
+
671
+ private static async completeNativeResource(
672
+ session: NativeWebSocketSession,
673
+ lifecycleVersion: number,
674
+ key: string,
675
+ started: Promise<boolean>,
676
+ content: NativeWebSocketResource,
677
+ timing: WebSocketTimingMetrics | undefined,
678
+ error?: WebSocketHandshakeError,
679
+ errorReady?: Promise<void>
680
+ ): Promise<void> {
681
+ // Attach rejection handling immediately, including when start never settles.
682
+ let stopped: Promise<void>;
683
+ try {
684
+ stopped = Promise.resolve(
685
+ session.stopResource(key, content.webSocketEvent)
686
+ ).catch(() => {
687
+ if (lifecycleVersion === FTRumWebSocketTracking.lifecycleVersion)
688
+ FTRumWebSocketTracking.warnOnce('Unable to stop WebSocket resource.');
689
+ });
690
+ } catch {
691
+ stopped = Promise.resolve();
692
+ }
693
+ try {
694
+ if (!(await started)) return;
695
+ await stopped;
696
+ if (errorReady) await errorReady;
697
+ if (lifecycleVersion !== FTRumWebSocketTracking.lifecycleVersion) return;
698
+ await session.addResource(
699
+ key,
700
+ error?.message ? { ...content, errorMessage: error.message } : content,
701
+ timing ?? {}
702
+ );
703
+ } catch {
704
+ session.release(key);
705
+ if (lifecycleVersion === FTRumWebSocketTracking.lifecycleVersion)
706
+ FTRumWebSocketTracking.warnOnce('Unable to report WebSocket resource.');
707
+ }
708
+ }
709
+
710
+ private static warnOnce(message: string): void {
711
+ if (FTRumWebSocketTracking.warnedFailures.has(message)) {
712
+ return;
713
+ }
714
+ FTRumWebSocketTracking.warnedFailures.add(message);
715
+ try {
716
+ console.warn(`[FT-SDK] ${message}`);
717
+ } catch {
718
+ // A host-provided logger must not turn instrumentation failure into one
719
+ // on the application's constructor, event dispatch or promise chain.
720
+ }
721
+ }
722
+ }