@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,622 @@
1
+ #import "FTWebSocketMetadataStore.h"
2
+ #import <CFNetwork/CFNetwork.h>
3
+ #import <objc/runtime.h>
4
+ #include <math.h>
5
+ #include <stdatomic.h>
6
+
7
+ static const NSUInteger FTMaximumHandshakes = 256;
8
+ static const NSUInteger FTMaximumResponseHeaderBytes = 16 * 1024;
9
+ static const NSTimeInterval FTCandidateLifetime = 5;
10
+ static const NSTimeInterval FTResourceLifetime = 60;
11
+
12
+ // Association accessors are inspected on main. Response data is copied only
13
+ // at response completion, synchronously on that method's caller queue.
14
+ @protocol FTWebSocketMetadataSource <NSObject>
15
+ @property (nonatomic, readonly) id delegate;
16
+ @property (nonatomic, readonly) NSURL *url;
17
+ @end
18
+
19
+ // SR 0.7.0 has a parameterless completion method. This public accessor is used
20
+ // only synchronously at that method's entry, never by a terminal bridge call.
21
+ @protocol FTWebSocketLegacyResponseSource <NSObject>
22
+ @property (nonatomic, readonly) CFHTTPMessageRef receivedHTTPHeaders;
23
+ @end
24
+
25
+ @interface FTWebSocketCandidate : NSObject
26
+ @property (nonatomic, strong) id<FTWebSocketMetadataSource> socket;
27
+ @property (nonatomic, weak) FTWebSocketMetadataStore *store;
28
+ @property (nonatomic, copy) NSString *session;
29
+ @property (nonatomic, copy) NSNumber *socketID;
30
+ @property (nonatomic, copy) NSString *URL;
31
+ @property (nonatomic, copy) NSString *resourceKey;
32
+ @property (nonatomic) NSUInteger revision;
33
+ @property (nonatomic) NSTimeInterval expires;
34
+ @property (nonatomic) BOOL associated;
35
+ @property (nonatomic) BOOL snapshotStarted;
36
+ @property (nonatomic, copy) NSDictionary *snapshot;
37
+ @end
38
+ @implementation FTWebSocketCandidate
39
+ @end
40
+
41
+ @interface FTWebSocketBinding : NSObject
42
+ @property (nonatomic, copy) NSNumber *socketID;
43
+ @property (nonatomic, copy) NSString *URL;
44
+ @property (nonatomic) NSTimeInterval expires;
45
+ @property (nonatomic, weak) FTWebSocketCandidate *candidate;
46
+ @end
47
+ @implementation FTWebSocketBinding
48
+ @end
49
+
50
+ @interface FTWebSocketTerminal : NSObject
51
+ @property (nonatomic, weak) FTWebSocketMetadataStore *store;
52
+ @property (nonatomic, copy) NSString *key;
53
+ @property (nonatomic, copy) NSDictionary *metadata;
54
+ @property (nonatomic, copy) NSString *event;
55
+ @property (nonatomic) BOOL completed;
56
+ @property (nonatomic) NSTimeInterval expires;
57
+ @end
58
+ @implementation FTWebSocketTerminal
59
+ @end
60
+
61
+ @interface FTWebSocketMetadataStore ()
62
+ @property (nonatomic, weak) id owner;
63
+ @property (nonatomic, copy) NSString *session;
64
+ @property (nonatomic) NSUInteger revision;
65
+ @property (nonatomic) BOOL enabled;
66
+ // stop disables new capture; clear/shutdown also invalidate in-flight reporting.
67
+ @property (nonatomic) BOOL valid;
68
+ @property (nonatomic) BOOL metadataAvailable;
69
+ @property (nonatomic) double lastBindingSequence;
70
+ @property (nonatomic) double lastResourceSequence;
71
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, FTWebSocketBinding *> *bindings;
72
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, FTWebSocketTerminal *> *terminals;
73
+ + (NSTimeInterval)monotonicTime;
74
+ + (void)recordSocket:(id<FTWebSocketMetadataSource>)socket;
75
+ + (void)associateCandidates;
76
+ + (void)pruneAtTime:(NSTimeInterval)now;
77
+ + (void)captureResponse:(CFHTTPMessageRef)response forSocket:(id)socket readFromSocket:(BOOL)readFromSocket;
78
+ + (NSDictionary *)responseSnapshot:(CFHTTPMessageRef)response;
79
+ @end
80
+
81
+ // One recursive lock protects all registry state, including native shutdown on
82
+ // a different module queue. RN association accessors are inspected only on main.
83
+ static NSObject *FTRegistryLock(void) {
84
+ static NSObject *lock;
85
+ static dispatch_once_t once;
86
+ dispatch_once(&once, ^{ lock = [NSObject new]; });
87
+ return lock;
88
+ }
89
+ static NSMutableArray<FTWebSocketCandidate *> *FTCandidates(void) {
90
+ static NSMutableArray *candidates;
91
+ static dispatch_once_t once;
92
+ dispatch_once(&once, ^{ candidates = [NSMutableArray new]; });
93
+ return candidates;
94
+ }
95
+ // Pointer identity avoids invoking socket hash/isEqual implementations. Values
96
+ // are owned by the bounded registry; weak keys add no socket ownership.
97
+ static NSMapTable<id, FTWebSocketCandidate *> *FTCandidateIndex(void) {
98
+ static NSMapTable *index;
99
+ static dispatch_once_t once;
100
+ dispatch_once(&once, ^{
101
+ index = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality
102
+ valueOptions:NSPointerFunctionsStrongMemory];
103
+ });
104
+ return index;
105
+ }
106
+ static atomic_bool FTRegisterSockets;
107
+ static atomic_bool FTHaveCandidates;
108
+ static void FTDiscardCandidate(FTWebSocketCandidate *candidate) {
109
+ if (!candidate) return;
110
+ // Weak-property loads can temporarily autorelease an entry. Clearing its
111
+ // payload makes reference release independent of the caller's pool lifetime.
112
+ @autoreleasepool {
113
+ if (candidate.socket && [FTCandidateIndex() objectForKey:candidate.socket] == candidate)
114
+ [FTCandidateIndex() removeObjectForKey:candidate.socket];
115
+ candidate.socket = nil;
116
+ candidate.snapshot = nil;
117
+ [FTCandidates() removeObject:candidate];
118
+ atomic_store(&FTHaveCandidates, FTCandidates().count != 0);
119
+ }
120
+ }
121
+ static NSHashTable<FTWebSocketMetadataStore *> *FTStores(void) {
122
+ static NSHashTable *stores;
123
+ static dispatch_once_t once;
124
+ dispatch_once(&once, ^{ stores = [NSHashTable weakObjectsHashTable]; });
125
+ return stores;
126
+ }
127
+ static NSMutableArray<FTWebSocketTerminal *> *FTTerminals(void) {
128
+ static NSMutableArray *terminals;
129
+ static dispatch_once_t once;
130
+ dispatch_once(&once, ^{ terminals = [NSMutableArray new]; });
131
+ return terminals;
132
+ }
133
+ static void FTDiscardTerminal(FTWebSocketTerminal *terminal) {
134
+ terminal.metadata = nil;
135
+ [terminal.store.terminals removeObjectForKey:terminal.key];
136
+ [FTTerminals() removeObject:terminal];
137
+ }
138
+ static NSUInteger FTRevision;
139
+ static BOOL FTSDKStopped;
140
+ static BOOL FTAssociationScheduled;
141
+ static dispatch_source_t FTSweepTimer;
142
+ static Class FTInstrumentedSocketClass;
143
+
144
+ static NSTimeInterval FTNow(void) { return [FTWebSocketMetadataStore monotonicTime]; }
145
+
146
+ static BOOL FTMethodMatches(Class cls, SEL selector, const char *result, unsigned int arguments, const char *argumentType) {
147
+ Method method = class_getInstanceMethod(cls, selector);
148
+ if (!method || method_getNumberOfArguments(method) != arguments + 2) return NO;
149
+ char *type = method_copyReturnType(method);
150
+ BOOL matches = strcmp(type, result) == 0;
151
+ free(type);
152
+ for (unsigned int index = 0; matches && index < arguments; index++) {
153
+ type = method_copyArgumentType(method, index + 2);
154
+ matches = strcmp(type, argumentType) == 0;
155
+ free(type);
156
+ }
157
+ return matches;
158
+ }
159
+
160
+ // Calling an init IMP must consume the allocated receiver and return +1.
161
+ // An ordinary block IMP has +0 conventions and would leak/over-release here.
162
+ typedef id (*FTSocketInitializer)(id __attribute__((ns_consumed)), SEL, id, id) __attribute__((ns_returns_retained));
163
+ static FTSocketInitializer FTOriginalInitializer;
164
+ static id FTInitializeSocket(id socket __attribute__((ns_consumed)), SEL selector, id request, id protocols) __attribute__((ns_returns_retained));
165
+ static id __attribute__((ns_returns_retained)) FTInitializeSocket(id socket __attribute__((ns_consumed)), SEL selector, id request, id protocols) {
166
+ id result = FTOriginalInitializer(socket, selector, request, protocols);
167
+ // RN 0.74/0.76 connects on main. Other call paths remain transparent.
168
+ if (result && atomic_load(&FTRegisterSockets) && NSThread.isMainThread) {
169
+ @try { [FTWebSocketMetadataStore recordSocket:result]; }
170
+ @catch (__unused NSException *exception) {}
171
+ }
172
+ return result;
173
+ }
174
+
175
+ typedef void (*FTSocketResponseCompletion)(id, SEL, CFHTTPMessageRef);
176
+ static FTSocketResponseCompletion FTOriginalResponseCompletion;
177
+ static void FTResponseCompleted(id socket, SEL selector, CFHTTPMessageRef response) {
178
+ if (atomic_load(&FTHaveCandidates)) {
179
+ @autoreleasepool {
180
+ @try { [FTWebSocketMetadataStore captureResponse:response forSocket:socket readFromSocket:NO]; }
181
+ @catch (__unused NSException *exception) {}
182
+ }
183
+ }
184
+ // Never call business code under a registry lock or swallow its exception.
185
+ FTOriginalResponseCompletion(socket, selector, response);
186
+ }
187
+
188
+ typedef void (*FTSocketLegacyResponseCompletion)(id, SEL);
189
+ static FTSocketLegacyResponseCompletion FTOriginalLegacyResponseCompletion;
190
+ static void FTLegacyResponseCompleted(id socket, SEL selector) {
191
+ if (atomic_load(&FTHaveCandidates)) {
192
+ @autoreleasepool {
193
+ @try { [FTWebSocketMetadataStore captureResponse:NULL forSocket:socket readFromSocket:YES]; }
194
+ @catch (__unused NSException *exception) {}
195
+ }
196
+ }
197
+ FTOriginalLegacyResponseCompletion(socket, selector);
198
+ }
199
+
200
+ static BOOL FTInstallHooks(void) {
201
+ static BOOL installed;
202
+ if (installed) return YES;
203
+ Class cls = NSClassFromString(@"SRWebSocket");
204
+ SEL selector = NSSelectorFromString(@"initWithURLRequest:protocols:");
205
+ SEL responseSelector = NSSelectorFromString(@"_HTTPHeadersDidFinish:");
206
+ BOOL legacy = cls && !class_getInstanceMethod(cls, responseSelector);
207
+ if (legacy) responseSelector = NSSelectorFromString(@"_HTTPHeadersDidFinish");
208
+ // Validate the entire capability before installing either hook. Matching an
209
+ // ABI cannot establish unchanged semantics in a future SocketRocket version.
210
+ if (!cls || !FTMethodMatches(cls, selector, @encode(id), 2, @encode(id)) ||
211
+ !FTMethodMatches(cls, @selector(delegate), @encode(id), 0, NULL) ||
212
+ !FTMethodMatches(cls, @selector(url), @encode(id), 0, NULL) ||
213
+ !FTMethodMatches(cls, responseSelector, @encode(void), legacy ? 0 : 1, @encode(CFHTTPMessageRef)) ||
214
+ (legacy && !FTMethodMatches(cls, @selector(receivedHTTPHeaders), @encode(CFHTTPMessageRef), 0, NULL))) return NO;
215
+ Method method = class_getInstanceMethod(cls, selector);
216
+ Method responseMethod = class_getInstanceMethod(cls, responseSelector);
217
+ FTInstrumentedSocketClass = cls;
218
+ FTOriginalInitializer = (FTSocketInitializer)method_getImplementation(method);
219
+ if (legacy) FTOriginalLegacyResponseCompletion = (FTSocketLegacyResponseCompletion)method_getImplementation(responseMethod);
220
+ else FTOriginalResponseCompletion = (FTSocketResponseCompletion)method_getImplementation(responseMethod);
221
+ class_replaceMethod(cls, responseSelector, legacy ? (IMP)FTLegacyResponseCompleted : (IMP)FTResponseCompleted, method_getTypeEncoding(responseMethod));
222
+ class_replaceMethod(cls, selector, (IMP)FTInitializeSocket, method_getTypeEncoding(method));
223
+ installed = YES;
224
+ return YES;
225
+ }
226
+
227
+ static void FTEnsureTimer(void) {
228
+ BOOL registrationEnabled = NO;
229
+ for (FTWebSocketMetadataStore *store in FTStores())
230
+ registrationEnabled |= store.enabled && store.metadataAvailable && store.owner != nil;
231
+ atomic_store(&FTRegisterSockets, registrationEnabled);
232
+ NSTimeInterval earliest = INFINITY;
233
+ for (FTWebSocketCandidate *candidate in FTCandidates()) earliest = MIN(earliest, candidate.expires);
234
+ for (FTWebSocketTerminal *terminal in FTTerminals()) earliest = MIN(earliest, terminal.expires);
235
+ for (FTWebSocketMetadataStore *store in FTStores()) {
236
+ for (FTWebSocketBinding *binding in store.bindings.allValues) earliest = MIN(earliest, binding.expires);
237
+ }
238
+ if (!isfinite(earliest)) {
239
+ if (FTSweepTimer) { dispatch_source_cancel(FTSweepTimer); FTSweepTimer = nil; }
240
+ return;
241
+ }
242
+ if (!FTSweepTimer) {
243
+ FTSweepTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
244
+ dispatch_get_global_queue(QOS_CLASS_UTILITY, 0));
245
+ dispatch_source_set_event_handler(FTSweepTimer, ^{
246
+ [FTWebSocketMetadataStore pruneAtTime:FTNow()];
247
+ });
248
+ dispatch_resume(FTSweepTimer);
249
+ }
250
+ // One timer for the nearest deadline. It stops when the registry is empty;
251
+ // no per-socket polling, and no extra one-second retention after expiry.
252
+ int64_t delay = (int64_t)(MAX(0, earliest - FTNow()) * NSEC_PER_SEC);
253
+ dispatch_source_set_timer(FTSweepTimer, dispatch_time(DISPATCH_TIME_NOW, delay), DISPATCH_TIME_FOREVER, 0);
254
+ }
255
+
256
+ static NSString *FTNormalizedURL(NSString *URL) {
257
+ return [URL isKindOfClass:NSString.class] ? [NSURL URLWithString:URL].absoluteString : nil;
258
+ }
259
+
260
+ @implementation FTWebSocketMetadataStore
261
+ + (NSTimeInterval)monotonicTime { return NSProcessInfo.processInfo.systemUptime; }
262
+ - (instancetype)init {
263
+ if ((self = [super init])) {
264
+ _bindings = [NSMutableDictionary new];
265
+ _terminals = [NSMutableDictionary new];
266
+ @synchronized (FTRegistryLock()) { [FTStores() addObject:self]; }
267
+ }
268
+ return self;
269
+ }
270
+
271
+ - (BOOL)startForOwner:(id)owner session:(NSString *)session {
272
+ if (!NSThread.isMainThread || session.length == 0) return NO;
273
+ @synchronized (FTRegistryLock()) {
274
+ if (FTSDKStopped) return NO;
275
+ if ([self.session isEqualToString:session]) return self.enabled && self.owner == owner;
276
+ [self clear];
277
+ self.owner = owner;
278
+ self.session = session;
279
+ self.revision = ++FTRevision;
280
+ self.enabled = YES;
281
+ self.valid = YES;
282
+ // Reporting and its URL/privacy policies remain available even when native
283
+ // socket enrichment is unsupported. No JS route switch is needed.
284
+ self.metadataAvailable = NO;
285
+ @try {
286
+ self.metadataAvailable = owner && FTInstallHooks();
287
+ } @catch (__unused NSException *exception) {
288
+ // Association is optional. Keep the native Resource policy and reporting.
289
+ }
290
+ self.lastBindingSequence = 0;
291
+ self.lastResourceSequence = 0;
292
+ FTEnsureTimer();
293
+ return YES;
294
+ }
295
+ }
296
+
297
+ - (void)stopForSession:(NSString *)session {
298
+ @synchronized (FTRegistryLock()) {
299
+ if (![self.session isEqualToString:session]) return;
300
+ if (NSThread.isMainThread) [[self class] associateCandidates];
301
+ self.enabled = NO;
302
+ self.revision = ++FTRevision;
303
+ // Keep bound handshakes for their JS terminal, discard unmatched candidates.
304
+ for (FTWebSocketCandidate *candidate in FTCandidates().copy) {
305
+ if (candidate.store == self && !candidate.resourceKey) FTDiscardCandidate(candidate);
306
+ }
307
+ FTEnsureTimer();
308
+ }
309
+ }
310
+
311
+ - (void)clear {
312
+ @synchronized (FTRegistryLock()) {
313
+ if (NSThread.isMainThread) [[self class] associateCandidates];
314
+ self.enabled = NO;
315
+ self.valid = NO;
316
+ self.revision = ++FTRevision;
317
+ [self.bindings removeAllObjects];
318
+ for (FTWebSocketTerminal *terminal in self.terminals.allValues) FTDiscardTerminal(terminal);
319
+ for (FTWebSocketCandidate *candidate in FTCandidates().copy) {
320
+ if (candidate.store == self) FTDiscardCandidate(candidate);
321
+ }
322
+ self.owner = nil;
323
+ FTEnsureTimer();
324
+ // Remember the closed session so a repeated start cannot resurrect it.
325
+ }
326
+ }
327
+
328
+ + (void)sdkDidStart {
329
+ @synchronized (FTRegistryLock()) { FTSDKStopped = NO; }
330
+ }
331
+
332
+ + (void)shutDown {
333
+ @synchronized (FTRegistryLock()) {
334
+ FTSDKStopped = YES;
335
+ for (FTWebSocketMetadataStore *store in FTStores()) [store clear];
336
+ for (FTWebSocketCandidate *candidate in FTCandidates().copy) FTDiscardCandidate(candidate);
337
+ [self pruneAtTime:FTNow()];
338
+ }
339
+ }
340
+
341
+ + (void)pruneAtTime:(NSTimeInterval)now {
342
+ @synchronized (FTRegistryLock()) {
343
+ for (FTWebSocketCandidate *candidate in FTCandidates().copy) {
344
+ if (candidate.expires <= now || (candidate.associated && !candidate.store.owner)) {
345
+ [candidate.store.bindings removeObjectForKey:candidate.resourceKey ?: @""];
346
+ FTDiscardCandidate(candidate);
347
+ }
348
+ }
349
+ for (FTWebSocketMetadataStore *store in FTStores()) {
350
+ for (NSString *key in store.bindings.allKeys) {
351
+ if (store.bindings[key].expires <= now || !store.owner) [store.bindings removeObjectForKey:key];
352
+ }
353
+ }
354
+ for (FTWebSocketTerminal *terminal in FTTerminals().copy) {
355
+ if (terminal.expires <= now || !terminal.store.valid) FTDiscardTerminal(terminal);
356
+ }
357
+ FTEnsureTimer();
358
+ }
359
+ }
360
+
361
+ + (void)recordSocket:(id<FTWebSocketMetadataSource>)socket {
362
+ // Base-class ABI checks do not validate overridden subclass accessors or
363
+ // response lifetime. Standard RN allocates this exact class. Other objects
364
+ // retain the native Resource route without socket enrichment or ownership.
365
+ if (object_getClass(socket) != FTInstrumentedSocketClass) return;
366
+ @synchronized (FTRegistryLock()) {
367
+ if (FTSDKStopped) return;
368
+ BOOL enabled = NO;
369
+ for (FTWebSocketMetadataStore *store in FTStores()) enabled |= store.enabled && store.metadataAvailable && store.owner != nil;
370
+ if (!enabled) return;
371
+ [self pruneAtTime:FTNow()];
372
+ if ([FTCandidateIndex() objectForKey:socket]) return;
373
+ if (FTCandidates().count >= FTMaximumHandshakes) {
374
+ FTWebSocketCandidate *oldest = FTCandidates().firstObject;
375
+ [oldest.store.bindings removeObjectForKey:oldest.resourceKey ?: @""];
376
+ FTDiscardCandidate(oldest);
377
+ }
378
+ FTWebSocketCandidate *candidate = [FTWebSocketCandidate new];
379
+ candidate.socket = socket;
380
+ candidate.expires = FTNow() + FTCandidateLifetime;
381
+ candidate.revision = FTRevision;
382
+ [FTCandidates() addObject:candidate];
383
+ [FTCandidateIndex() setObject:candidate forKey:socket];
384
+ atomic_store(&FTHaveCandidates, true);
385
+ FTEnsureTimer();
386
+ // One deferred pass per main-queue batch; the block retains no sockets.
387
+ if (!FTAssociationScheduled) {
388
+ FTAssociationScheduled = YES;
389
+ dispatch_async(dispatch_get_main_queue(), ^{
390
+ @synchronized (FTRegistryLock()) {
391
+ FTAssociationScheduled = NO;
392
+ [self associateCandidates];
393
+ }
394
+ });
395
+ }
396
+ }
397
+ }
398
+
399
+ + (NSDictionary *)responseSnapshot:(CFHTTPMessageRef)response {
400
+ if (!response || !CFHTTPMessageIsHeaderComplete(response)) return nil;
401
+ NSMutableDictionary *result = [NSMutableDictionary new];
402
+ result[@"httpStatus"] = @(CFHTTPMessageGetResponseStatusCode(response));
403
+ NSString *version = CFBridgingRelease(CFHTTPMessageCopyVersion(response));
404
+ if (version.length) result[@"httpProtocol"] = [version copy];
405
+ NSDictionary *fields = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(response));
406
+ NSMutableDictionary *headers = [NSMutableDictionary new];
407
+ NSUInteger bytes = 0;
408
+ for (NSString *name in fields) {
409
+ NSString *value = fields[name];
410
+ if (![name isKindOfClass:NSString.class] || ![value isKindOfClass:NSString.class]) return [result copy];
411
+ NSUInteger nameBytes = [name lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
412
+ NSUInteger valueBytes = [value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
413
+ if (nameBytes > FTMaximumResponseHeaderBytes || valueBytes > FTMaximumResponseHeaderBytes ||
414
+ nameBytes + valueBytes + 4 > FTMaximumResponseHeaderBytes - bytes) return [result copy];
415
+ bytes += nameBytes + valueBytes + 4;
416
+ headers[[name copy]] = [value copy];
417
+ }
418
+ if (fields) result[@"responseHeader"] = [headers copy];
419
+ return [result copy];
420
+ }
421
+
422
+ + (void)captureResponse:(CFHTTPMessageRef)response forSocket:(id)socket readFromSocket:(BOOL)readFromSocket {
423
+ FTWebSocketCandidate *candidate;
424
+ @synchronized (FTRegistryLock()) {
425
+ // Check the runtime class, not -class/isKindOfClass:, which can be overridden.
426
+ // A later isa change must not expose an unvalidated legacy response getter.
427
+ if (FTSDKStopped || object_getClass(socket) != FTInstrumentedSocketClass) return;
428
+ candidate = [FTCandidateIndex() objectForKey:socket];
429
+ if (!candidate || candidate.snapshotStarted || candidate.expires <= FTNow()) return;
430
+ candidate.snapshotStarted = YES;
431
+ }
432
+ // Keep the SR 0.7.0 receiver alive through extraction of its borrowed response.
433
+ // Both known completion methods run synchronously after header parsing. No
434
+ // queue handoff, CF pointer retention or registry lock spans this extraction.
435
+ __attribute__((objc_precise_lifetime)) id<FTWebSocketLegacyResponseSource> source = socket;
436
+ if (readFromSocket) response = source.receivedHTTPHeaders;
437
+ NSDictionary *snapshot = [self responseSnapshot:response];
438
+ @synchronized (FTRegistryLock()) {
439
+ // Cleanup/stopResource may have won while copying. Never recreate a record,
440
+ // attach to a reused ID, or commit into a later SDK capture lifecycle.
441
+ if (FTSDKStopped || object_getClass(socket) != FTInstrumentedSocketClass ||
442
+ [FTCandidateIndex() objectForKey:socket] != candidate ||
443
+ candidate.expires <= FTNow()) return;
444
+ candidate.snapshot = snapshot;
445
+ }
446
+ }
447
+
448
+ + (void)associateCandidates {
449
+ if (!NSThread.isMainThread) return;
450
+ [self pruneAtTime:FTNow()];
451
+ for (FTWebSocketCandidate *candidate in FTCandidates().copy) {
452
+ if (candidate.associated) continue;
453
+ @try {
454
+ id owner = candidate.socket.delegate;
455
+ NSNumber *socketID = objc_getAssociatedObject(candidate.socket, NSSelectorFromString(@"reactTag"));
456
+ if (![socketID isKindOfClass:NSNumber.class] || !isfinite(socketID.doubleValue) ||
457
+ socketID.doubleValue < 0 || socketID.doubleValue > 9007199254740991.0 ||
458
+ socketID.doubleValue != floor(socketID.doubleValue)) {
459
+ FTDiscardCandidate(candidate);
460
+ continue;
461
+ }
462
+ FTWebSocketMetadataStore *matched;
463
+ for (FTWebSocketMetadataStore *store in FTStores()) {
464
+ if (store.enabled && store.metadataAvailable && store.owner == owner && store.revision <= candidate.revision) { matched = store; break; }
465
+ }
466
+ if (!matched) { FTDiscardCandidate(candidate); continue; }
467
+ candidate.store = matched;
468
+ candidate.session = matched.session;
469
+ candidate.socketID = socketID;
470
+ candidate.URL = candidate.socket.url.absoluteString;
471
+ candidate.associated = YES;
472
+ for (NSString *key in matched.bindings.allKeys) {
473
+ FTWebSocketBinding *binding = matched.bindings[key];
474
+ if ([binding.socketID isEqual:socketID] && [binding.URL isEqual:candidate.URL]) {
475
+ if (binding.candidate) { FTDiscardCandidate(candidate); break; }
476
+ candidate.resourceKey = key;
477
+ candidate.expires = binding.expires;
478
+ binding.candidate = candidate;
479
+ break;
480
+ }
481
+ }
482
+ } @catch (__unused NSException *exception) { FTDiscardCandidate(candidate); }
483
+ }
484
+ FTEnsureTimer();
485
+ }
486
+
487
+ - (void)bindResource:(NSString *)key socketID:(NSNumber *)socketID URL:(NSString *)URL session:(NSString *)session sequence:(double)sequence {
488
+ if (!isfinite(sequence) || sequence <= 0 || sequence > 9007199254740991.0 || sequence != floor(sequence)) return;
489
+ if (!NSThread.isMainThread || key.length == 0 || key.length > 128 ||
490
+ !isfinite(socketID.doubleValue) || socketID.doubleValue < 0 ||
491
+ socketID.doubleValue > 9007199254740991.0 || socketID.doubleValue != floor(socketID.doubleValue)) return;
492
+ NSString *normalizedURL = FTNormalizedURL(URL);
493
+ if (!normalizedURL.length) return;
494
+ @synchronized (FTRegistryLock()) {
495
+ if (!self.enabled || !self.metadataAvailable || !self.owner || ![self.session isEqualToString:session]) return;
496
+ // This bridge uses one serial main queue; JS assigns sequence at bind
497
+ // invocation. The watermark rejects late duplicates without accumulating
498
+ // a tombstone per completed handshake.
499
+ if (sequence <= self.lastBindingSequence) return;
500
+ self.lastBindingSequence = sequence;
501
+ [[self class] associateCandidates];
502
+ if (self.bindings[key]) return;
503
+ // Bound pending registrations as well as retained socket objects.
504
+ if (self.bindings.count >= FTMaximumHandshakes) return;
505
+ FTWebSocketBinding *binding = [FTWebSocketBinding new];
506
+ binding.socketID = socketID;
507
+ binding.URL = normalizedURL;
508
+ binding.expires = FTNow() + FTResourceLifetime;
509
+ self.bindings[key] = binding;
510
+ for (FTWebSocketCandidate *candidate in FTCandidates()) {
511
+ if (candidate.store == self && [candidate.session isEqual:session] && !candidate.resourceKey &&
512
+ [candidate.socketID isEqual:socketID] && [candidate.URL isEqual:normalizedURL]) {
513
+ candidate.resourceKey = key;
514
+ candidate.expires = binding.expires;
515
+ binding.candidate = candidate;
516
+ break;
517
+ }
518
+ }
519
+ FTEnsureTimer();
520
+ }
521
+ }
522
+
523
+ - (void)releaseResource:(NSString *)key session:(NSString *)session {
524
+ @synchronized (FTRegistryLock()) {
525
+ if (![self.session isEqualToString:session]) return;
526
+ FTWebSocketBinding *binding = self.bindings[key];
527
+ FTDiscardCandidate(binding.candidate);
528
+ [self.bindings removeObjectForKey:key];
529
+ FTEnsureTimer();
530
+ }
531
+ }
532
+
533
+ - (NSDictionary *)takeResource:(NSString *)key session:(NSString *)session {
534
+ if (!NSThread.isMainThread) return nil;
535
+ @synchronized (FTRegistryLock()) {
536
+ if (![self.session isEqualToString:session]) return nil;
537
+ [[self class] associateCandidates];
538
+ FTWebSocketBinding *binding = self.bindings[key];
539
+ if (!binding) return nil;
540
+ NSDictionary *snapshot = binding.candidate.snapshot;
541
+ [self releaseResource:key session:session];
542
+ return snapshot;
543
+ }
544
+ }
545
+
546
+ - (BOOL)canReportKey:(NSString *)key session:(NSString *)session {
547
+ return NSThread.isMainThread && !FTSDKStopped && self.valid && key.length > 0 && key.length <= 128 &&
548
+ [self.session isEqualToString:session];
549
+ }
550
+
551
+ - (FTWebSocketTerminal *)terminalForKey:(NSString *)key event:(NSString *)event {
552
+ FTWebSocketTerminal *terminal = self.terminals[key];
553
+ if (terminal) return terminal;
554
+ if (FTTerminals().count >= FTMaximumHandshakes) FTDiscardTerminal(FTTerminals().firstObject);
555
+ terminal = [FTWebSocketTerminal new];
556
+ terminal.store = self;
557
+ terminal.key = key;
558
+ terminal.event = [event isEqualToString:@"open"] ? @"open" : @"error";
559
+ terminal.expires = FTNow() + FTResourceLifetime;
560
+ self.terminals[key] = terminal;
561
+ [FTTerminals() addObject:terminal];
562
+ FTEnsureTimer();
563
+ return terminal;
564
+ }
565
+
566
+ - (void)startResource:(NSString *)key socketID:(NSNumber *)socketID URL:(NSString *)URL session:(NSString *)session sequence:(double)sequence report:(void (^)(void))report {
567
+ @synchronized (FTRegistryLock()) {
568
+ if (![self canReportKey:key session:session] || !self.enabled ||
569
+ !isfinite(sequence) || sequence <= self.lastResourceSequence || sequence > 9007199254740991.0 || sequence != floor(sequence)) return;
570
+ self.lastResourceSequence = sequence;
571
+ if (self.terminals[key] || self.bindings[key]) return;
572
+ @try {
573
+ // Missing/unsupported IDs simply skip association. Reporting still uses
574
+ // the basic JS data through this same route, with no second bridge.
575
+ [self bindResource:key socketID:socketID URL:URL session:session sequence:sequence];
576
+ report();
577
+ } @catch (NSException *exception) {
578
+ [self cancelResource:key session:session];
579
+ @throw exception;
580
+ }
581
+ }
582
+ }
583
+
584
+ - (void)stopResource:(NSString *)key event:(NSString *)event session:(NSString *)session report:(void (^)(void))report {
585
+ @synchronized (FTRegistryLock()) {
586
+ if (![self canReportKey:key session:session]) return;
587
+ [[self class] pruneAtTime:FTNow()];
588
+ if (self.terminals[key]) return; // First JS terminal wins, including open then close.
589
+ NSDictionary *metadata = [self takeResource:key session:session];
590
+ FTWebSocketTerminal *terminal = [self terminalForKey:key event:event];
591
+ terminal.metadata = metadata;
592
+ report(); // The socket was released before the RUM call or promise resolves.
593
+ }
594
+ }
595
+
596
+ - (void)addResource:(NSString *)key event:(NSString *)event session:(NSString *)session report:(void (^)(NSDictionary *, NSString *))report {
597
+ @synchronized (FTRegistryLock()) {
598
+ if (![self canReportKey:key session:session]) return;
599
+ [[self class] pruneAtTime:FTNow()];
600
+ // A missing/expired snapshot or failed stop bridge uses basic data. This
601
+ // never creates a socket binding or reads a socket after its release.
602
+ FTWebSocketTerminal *terminal = [self terminalForKey:key event:event];
603
+ if (terminal.completed) return;
604
+ terminal.completed = YES;
605
+ NSDictionary *metadata = terminal.metadata;
606
+ terminal.metadata = nil;
607
+ [self releaseResource:key session:session];
608
+ report(metadata, terminal.event);
609
+ }
610
+ }
611
+
612
+ - (BOOL)cancelResource:(NSString *)key session:(NSString *)session {
613
+ @synchronized (FTRegistryLock()) {
614
+ [self releaseResource:key session:session];
615
+ if (![self canReportKey:key session:session]) return NO;
616
+ FTWebSocketTerminal *terminal = [self terminalForKey:key event:@"error"];
617
+ terminal.completed = YES;
618
+ terminal.metadata = nil;
619
+ return YES;
620
+ }
621
+ }
622
+ @end
@@ -0,0 +1,20 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ // Capture-side normalization. Neither dictionary includes invented response
4
+ // content, sizes, network phase timings, or native NSError identifiers.
5
+ FOUNDATION_EXPORT NSDictionary *FTWebSocketResourceData(NSDictionary *content, NSDictionary *metrics,
6
+ NSDictionary *metadata, NSString *event);
7
+
8
+ // Exact capture interval from decimal int64 nanosecond strings. Missing or
9
+ // invalid endpoints reject the whole pair; processing time is never substituted.
10
+ FOUNDATION_EXPORT BOOL FTWebSocketResourceInterval(NSDictionary *metrics, int64_t *start, int64_t *end);
11
+
12
+ // Same precedence and polarity as the native Resource URL policy: the block
13
+ // list wins when both callbacks exist. This decides collection, not business IO.
14
+ FOUNDATION_EXPORT BOOL FTWebSocketShouldCollectResource(NSURL *url,
15
+ BOOL (^resourceUrlHandler)(NSURL *),
16
+ BOOL (^intakeUrlHandler)(NSURL *));
17
+
18
+ // The SDK's keyed Trace generation caches correlation until addResource. A
19
+ // filtered/cancelled observation has no addResource, so discard that key only.
20
+ FOUNDATION_EXPORT BOOL FTWebSocketDiscardTraceResource(id interceptor, NSString *key);