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

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