@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
@@ -12,15 +12,17 @@ Pod::Spec.new do |s|
12
12
  s.authors = package["author"]
13
13
 
14
14
  s.platforms = { :ios => "10.0" }
15
- s.source = { :git => "https://github.com/GuanceCloud/datakit-react-native.git", :tag => "#{s.version}" }
15
+ s.source = { :git => "https://github.com/TrueWatchTech/datakit-react-native.git", :tag => "agent_#{s.version}" }
16
16
 
17
17
 
18
18
  s.source_files = "ios/**/*.{h,m,mm,swift}"
19
+ s.private_header_files = "ios/FTJSLongTaskMonitor.h", "ios/FTWebSocketMetadataStore.h", "ios/FTReactNativeWebSocket.h", "ios/FTReactNativeResource.h", "ios/FTWebSocketResourceData.h"
20
+ s.frameworks = "CFNetwork"
19
21
 
20
22
 
21
23
  s.dependency "React-Core"
22
- s.dependency 'FTMobileSDK', '1.6.2'
23
-
24
+ s.dependency 'TrueWatchSDK', '1.6.7'
25
+
24
26
  xcconfig = {
25
27
  "HEADER_SEARCH_PATHS" => "$(inherited) " +
26
28
  "$(PODS_ROOT)/React-RCTFabric/** " +
@@ -36,7 +38,7 @@ Pod::Spec.new do |s|
36
38
 
37
39
  if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
38
40
  s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
39
-
41
+
40
42
  xcconfig.merge!({
41
43
  "DEFINES_MODULE" => "YES",
42
44
  "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
@@ -48,5 +50,5 @@ Pod::Spec.new do |s|
48
50
  if respond_to?(:install_modules_dependencies, true)
49
51
  install_modules_dependencies(s)
50
52
  end
51
-
53
+
52
54
  end
package/README.md CHANGED
@@ -28,6 +28,40 @@ import {
28
28
 
29
29
  ```
30
30
 
31
+ ## WebSocket handshake tracking
32
+
33
+ To collect iOS WebSocket handshakes while native Resource auto-collection is disabled:
34
+
35
+ ```ts
36
+ await FTReactNativeRUM.setConfig({
37
+ androidAppId: 'YOUR_ANDROID_APP_ID',
38
+ iOSAppId: 'YOUR_IOS_APP_ID',
39
+ enableNativeUserResource: false,
40
+ enableIOSWebSocketResource: true,
41
+ });
42
+ ```
43
+
44
+ `enableIOSWebSocketResource` defaults to `false` and is independent of
45
+ `enableNativeUserResource`. It only affects iOS; Android WebSocket collection
46
+ continues through the native SDK under `enableNativeUserResource`.
47
+ When disabled, new iOS connections are not instrumented or injected with
48
+ WebSocket Trace headers. Handshakes already started can finish normally.
49
+ When enabled, Trace injection additionally requires `enableNativeAutoTrace`.
50
+ Only connections created after tracking starts are observed.
51
+
52
+ iOS starts JS capture and native setup together, without waiting for a native
53
+ readiness reply. JS measures the handshake and injects Trace headers; the native
54
+ bridge handles Resource reporting and optionally adds a response snapshot.
55
+ Missing socket association keeps the native URL filtering, sampling and privacy
56
+ path. Only older native packages without this bridge use the legacy JS route.
57
+ Connections created before the hooks are installed may have no native snapshot.
58
+
59
+ iOS handshake tracking reads `error.message` first. When it is absent, the SDK
60
+ can use `close.reason` from the immediately following abnormal close (`1006`),
61
+ before the next microtask. The first error still ends handshake timing; normal
62
+ close reasons are not collected. Native HTTP response snapshots take precedence
63
+ over error-text inference.
64
+
31
65
  ## More
32
66
 
33
67
  [View Doc Here](https://docs.truewatch.com/real-user-monitoring/react-native/app-access/)
@@ -94,6 +94,12 @@ android {
94
94
  }
95
95
 
96
96
  if (reactNativeMajorVersion == 0) {
97
+ if (reactNativeMinorVersion >= 70) {
98
+ java.srcDirs += ['src/rnpost70/java']
99
+ } else {
100
+ java.srcDirs += ['src/rnpre70/java']
101
+ }
102
+
97
103
  if (reactNativeMinorVersion >= 74) {
98
104
  java.srcDirs += ['src/rnpost74/java']
99
105
  } else {
@@ -179,11 +185,12 @@ repositories {
179
185
  dependencies {
180
186
  // noinspection GradleDynamicVersion
181
187
  implementation 'com.facebook.react:react-native:+'
182
- implementation 'com.truewatch.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.0'
188
+ implementation 'com.truewatch.ft.mobile.sdk.tracker.agent:ft-sdk:1.7.5'
183
189
  implementation 'com.truewatch.ft.mobile.sdk.tracker.agent:ft-native:1.1.2'
184
-
185
190
  // implementation files('../example/android/app/libs/ft-sdk-debug.aar')
186
191
  // implementation files('../example/android/app/libs/ft-session-replay-debug.aar')
187
192
 
188
193
  implementation 'com.google.code.gson:gson:2.8.5'
194
+ testImplementation 'junit:junit:4.13.2'
195
+ testImplementation 'org.mockito:mockito-core:4.11.0'
189
196
  }
@@ -271,7 +271,8 @@ public class FTMobileImpl {
271
271
  return false;
272
272
  }
273
273
 
274
- if (actual instanceof String actualString) {
274
+ if (actual instanceof String) {
275
+ String actualString = (String) actual;
275
276
  Object normalizedActual = parseJsonStringIfNeeded(actualString);
276
277
  if (normalizedActual != actual) {
277
278
  return isEqualValue(normalizedActual, expected);
@@ -298,18 +299,23 @@ public class FTMobileImpl {
298
299
  }
299
300
 
300
301
  private boolean matchesCustomKey(Object actual, Object expected) {
301
- if (expected instanceof Map<?, ?> expectedMap && expectedMap.containsKey("contains")) {
302
- return containsValue(actual, expectedMap.get("contains"));
302
+ if (expected instanceof Map) {
303
+ Map<?, ?> expectedMap = (Map<?, ?>) expected;
304
+ if (expectedMap.containsKey("contains")) {
305
+ return containsValue(actual, expectedMap.get("contains"));
306
+ }
303
307
  }
304
308
  return isEqualValue(actual, expected);
305
309
  }
306
310
 
307
311
  private boolean containsValue(Object actual, Object expectedValue) {
308
- Object normalizedActual = actual instanceof String actualString
309
- ? parseJsonStringIfNeeded(actualString)
310
- : actual;
312
+ Object normalizedActual = actual;
313
+ if (actual instanceof String) {
314
+ normalizedActual = parseJsonStringIfNeeded((String) actual);
315
+ }
311
316
 
312
- if (normalizedActual instanceof JSONArray actualArray) {
317
+ if (normalizedActual instanceof JSONArray) {
318
+ JSONArray actualArray = (JSONArray) normalizedActual;
313
319
  try {
314
320
  for (int i = 0; i < actualArray.length(); i++) {
315
321
  if (isEqualValue(actualArray.get(i), expectedValue)) {
@@ -338,6 +344,29 @@ public class FTMobileImpl {
338
344
  return value;
339
345
  }
340
346
 
347
+ @Nullable
348
+ private HashMap<String, String[]> convertDataFilters(@Nullable Map<String, Object> filters) {
349
+ if (filters == null) {
350
+ return null;
351
+ }
352
+ HashMap<String, String[]> convertedFilters = new HashMap<>();
353
+ for (Map.Entry<String, Object> entry : filters.entrySet()) {
354
+ Object value = entry.getValue();
355
+ if (!(value instanceof List<?>)) {
356
+ continue;
357
+ }
358
+ List<?> filterList = (List<?>) value;
359
+ List<String> filterValues = new ArrayList<>();
360
+ for (Object filter : filterList) {
361
+ if (filter != null) {
362
+ filterValues.add(filter.toString());
363
+ }
364
+ }
365
+ convertedFilters.put(entry.getKey(), filterValues.toArray(new String[0]));
366
+ }
367
+ return convertedFilters;
368
+ }
369
+
341
370
  public void sdkConfig(ReadableMap context, Promise promise) {
342
371
  Map<String, Object> map = context.toHashMap();
343
372
  String datakitUrl = (String) map.get("datakitUrl");
@@ -349,6 +378,8 @@ public class FTMobileImpl {
349
378
  Integer syncSleepTime = ReactNativeUtils.convertToNativeInt(map.get("syncSleepTime"));
350
379
  Boolean enableDataIntegerCompatible = (Boolean) map.get("enableDataIntegerCompatible");
351
380
  Boolean compressIntakeRequests = (Boolean) map.get("compressIntakeRequests");
381
+ Boolean enableDataFilter = (Boolean) map.get("enableDataFilter");
382
+ Map<String, Object> dataFilters = (Map<String, Object>) map.get("dataFilters");
352
383
  Integer env = ReactNativeUtils.convertToNativeInt(map.get("envType"));
353
384
  String serviceName = (String) map.get("service");
354
385
  Map<String, Object> globalContext = (Map<String, Object>) map.get("globalContext");
@@ -404,6 +435,13 @@ public class FTMobileImpl {
404
435
  if (compressIntakeRequests != null && compressIntakeRequests) {
405
436
  sdkConfig.setCompressIntakeRequests(compressIntakeRequests);
406
437
  }
438
+ if (enableDataFilter != null) {
439
+ sdkConfig.setEnableDataFilter(enableDataFilter);
440
+ }
441
+ HashMap<String, String[]> convertedDataFilters = convertDataFilters(dataFilters);
442
+ if (convertedDataFilters != null) {
443
+ sdkConfig.setDataFilters(convertedDataFilters);
444
+ }
407
445
  if (globalContext != null) {
408
446
  for (Map.Entry<String, Object> entry : globalContext.entrySet()) {
409
447
  sdkConfig.addGlobalContext(entry.getKey(), entry.getValue().toString());
@@ -617,11 +655,4 @@ public class FTMobileImpl {
617
655
  });
618
656
  }
619
657
 
620
- public void addListener(String eventName) {
621
- // Required for NativeEventEmitter. No-op because native does not need listener bookkeeping.
622
- }
623
-
624
- public void removeListeners(double count) {
625
- // Required for NativeEventEmitter. No-op because native does not need listener bookkeeping.
626
- }
627
658
  }
@@ -2,8 +2,11 @@ package com.ft.sdk.reactnative;
2
2
 
3
3
  import com.ft.sdk.reactnative.BuildConfig;
4
4
  import com.facebook.react.bridge.Promise;
5
+ import com.facebook.react.bridge.LifecycleEventListener;
6
+ import com.facebook.react.bridge.ReactApplicationContext;
5
7
  import com.facebook.react.bridge.ReactMethod;
6
8
  import com.facebook.react.bridge.ReadableMap;
9
+ import com.facebook.react.common.LifecycleState;
7
10
  import com.ft.sdk.DetectFrequency;
8
11
  import com.ft.sdk.FTInTakeUrlHandler;
9
12
  import com.ft.sdk.FTRUMConfig;
@@ -18,11 +21,28 @@ import com.ft.sdk.reactnative.utils.ReactNativeUtils;
18
21
  import java.util.ArrayList;
19
22
  import java.util.HashMap;
20
23
  import java.util.Map;
24
+ import java.util.concurrent.atomic.AtomicBoolean;
21
25
 
22
- public class FTRUMImpl {
26
+ public class FTRUMImpl implements LifecycleEventListener {
23
27
 
24
28
  private static final String DEFAULT_ERROR_TYPE = "reactnative_crash";
25
29
  public static final String NAME = "FTReactNativeRUM";
30
+ private final ReactApplicationContext reactContext;
31
+ private final JsLongTaskMonitor jsLongTaskMonitor;
32
+ private final AtomicBoolean destroyed = new AtomicBoolean();
33
+
34
+ public FTRUMImpl(ReactApplicationContext reactContext) {
35
+ this(reactContext, JsLongTaskMonitor.create(reactContext));
36
+ }
37
+
38
+ FTRUMImpl(
39
+ ReactApplicationContext reactContext,
40
+ JsLongTaskMonitor jsLongTaskMonitor
41
+ ) {
42
+ this.reactContext = reactContext;
43
+ this.jsLongTaskMonitor = jsLongTaskMonitor;
44
+ reactContext.addLifecycleEventListener(this);
45
+ }
26
46
 
27
47
  @ReactMethod
28
48
  public void setConfig(ReadableMap context, Promise promise) {
@@ -39,6 +59,8 @@ public class FTRUMImpl {
39
59
  Boolean enableTrackNativeAppANR = (Boolean) map.get("enableTrackNativeAppANR");
40
60
  Boolean enableTrackNativeFreeze = (Boolean) map.get("enableTrackNativeFreeze");
41
61
  Double nativeFreezeDurationMs = (Double) map.get("nativeFreezeDurationMs");
62
+ Boolean enableLongTask = (Boolean) map.get("enableLongTask");
63
+ Double longTaskThresholdMs = (Double) map.get("longTaskThresholdMs");
42
64
  Integer monitorType = ReactNativeUtils.convertToNativeInt(map.get("errorMonitorType"));
43
65
  Integer deviceMonitorType = ReactNativeUtils.convertToNativeInt(map.get("deviceMonitorType"));
44
66
  Integer detectFrequency = ReactNativeUtils.convertToNativeInt(map.get("detectFrequency"));
@@ -131,14 +153,75 @@ public class FTRUMImpl {
131
153
  if (allowWebViewHost != null) {
132
154
  String[] allowWebViewHostArr = new String[allowWebViewHost.size()];
133
155
  allowWebViewHost.toArray(allowWebViewHostArr);
134
- rumConfig.setAllowWebViewHost(allowWebViewHostArr);
156
+ FTSdk ftSdk = FTSdk.get();
157
+ if (ftSdk != null) {
158
+ ftSdk.getBaseConfig().setAllowWebViewHost(allowWebViewHostArr);
159
+ }
135
160
  }
136
161
 
137
162
  FTSdk.initRUMWithConfig(rumConfig);
163
+ double configuredThresholdMilliseconds = longTaskThresholdMs != null
164
+ ? longTaskThresholdMs
165
+ : 100;
166
+ double thresholdMilliseconds = Boolean.TRUE.equals(enableLongTask)
167
+ ? configuredThresholdMilliseconds
168
+ : 0;
169
+ jsLongTaskMonitor.setThresholdMilliseconds(thresholdMilliseconds);
170
+ if (
171
+ thresholdMilliseconds > 0 &&
172
+ reactContext.getLifecycleState() == LifecycleState.RESUMED
173
+ ) {
174
+ jsLongTaskMonitor.start();
175
+ } else {
176
+ jsLongTaskMonitor.stop();
177
+ }
138
178
  //LogUtils.d("configCheck","rumConfig:"+new Gson().toJson(rumConfig));
139
179
  promise.resolve(null);
140
180
  }
141
181
 
182
+ @ReactMethod
183
+ public void stopLongTaskTracking(final Promise promise) {
184
+ jsLongTaskMonitor.disable(new Runnable() {
185
+ @Override
186
+ public void run() {
187
+ promise.resolve(null);
188
+ }
189
+ });
190
+ }
191
+
192
+ public boolean setLongTaskContext(ReadableMap context) {
193
+ if (destroyed.get()) {
194
+ return false;
195
+ }
196
+ jsLongTaskMonitor.setBridgeContext(context == null ? null : context.toHashMap());
197
+ return true;
198
+ }
199
+
200
+ public void destroy() {
201
+ if (!destroyed.compareAndSet(false, true)) {
202
+ return;
203
+ }
204
+ reactContext.removeLifecycleEventListener(this);
205
+ jsLongTaskMonitor.disable(null);
206
+ }
207
+
208
+ @Override
209
+ public void onHostResume() {
210
+ if (!destroyed.get()) {
211
+ jsLongTaskMonitor.start();
212
+ }
213
+ }
214
+
215
+ @Override
216
+ public void onHostPause() {
217
+ jsLongTaskMonitor.stop();
218
+ }
219
+
220
+ @Override
221
+ public void onHostDestroy() {
222
+ jsLongTaskMonitor.stop();
223
+ }
224
+
142
225
  @ReactMethod
143
226
  public void startAction(String actionName, String actionType, ReadableMap map, Promise promise) {
144
227
  if (map != null) {
@@ -71,6 +71,10 @@ public class FTTraceImpl {
71
71
 
72
72
  @ReactMethod
73
73
  public void getTraceHeaderFields(String url, String key, Promise promise) {
74
+ promise.resolve(getTraceHeaderFieldsSync(url, key));
75
+ }
76
+
77
+ public WritableNativeMap getTraceHeaderFieldsSync(String url, String key) {
74
78
  HashMap<String, String> hashMap = null;
75
79
  if (key == null) {
76
80
  hashMap = FTTraceManager.get().getTraceHeader(url);
@@ -84,6 +88,6 @@ public class FTTraceImpl {
84
88
  map.putString(entry.getKey(), entry.getValue());
85
89
  }
86
90
  }
87
- promise.resolve(map);
91
+ return map;
88
92
  }
89
93
  }
@@ -0,0 +1,281 @@
1
+ package com.ft.sdk.reactnative;
2
+
3
+ import android.view.Choreographer;
4
+
5
+ import com.facebook.react.bridge.ReactApplicationContext;
6
+ import com.ft.sdk.FTRUMGlobalManager;
7
+
8
+ import java.util.HashMap;
9
+ import java.util.Map;
10
+ import java.util.Collections;
11
+ import java.util.concurrent.atomic.AtomicBoolean;
12
+
13
+ final class JsLongTaskMonitor {
14
+
15
+ private static final double NANOS_PER_MILLISECOND = 1_000_000d;
16
+
17
+ interface Executor {
18
+ boolean runOnJsThread(Runnable runnable);
19
+ }
20
+
21
+ interface FrameCallback {
22
+ void doFrame(long frameTimeNanos);
23
+ }
24
+
25
+ interface FrameScheduler {
26
+ void postFrameCallback();
27
+
28
+ void removeFrameCallback();
29
+ }
30
+
31
+ interface FrameSchedulerFactory {
32
+ FrameScheduler create(FrameCallback callback);
33
+ }
34
+
35
+ interface Reporter {
36
+ void reportLongTask(long durationNanos, Map<String, Object> property);
37
+ }
38
+
39
+ private final Executor executor;
40
+ private final FrameSchedulerFactory schedulerFactory;
41
+ private final Reporter reporter;
42
+ private final Object stateLock = new Object();
43
+ private final FrameCallback frameCallback;
44
+
45
+ private volatile long thresholdNanos;
46
+ private volatile int generation;
47
+ private volatile boolean requestedRunning;
48
+ private volatile Map<String, Object> bridgeContext = Collections.emptyMap();
49
+
50
+ // Accessed only on the React Native JavaScript thread.
51
+ private FrameScheduler scheduler;
52
+ private int activeGeneration;
53
+ private long lastFrameTimeNanos;
54
+
55
+ static JsLongTaskMonitor create(ReactApplicationContext reactContext) {
56
+ return new JsLongTaskMonitor(
57
+ new ReactJsThreadExecutor(reactContext),
58
+ new ChoreographerSchedulerFactory(),
59
+ new NativeLongTaskReporter()
60
+ );
61
+ }
62
+
63
+ JsLongTaskMonitor(
64
+ Executor executor,
65
+ FrameSchedulerFactory schedulerFactory,
66
+ Reporter reporter
67
+ ) {
68
+ this.executor = executor;
69
+ this.schedulerFactory = schedulerFactory;
70
+ this.reporter = reporter;
71
+ this.frameCallback = new FrameCallback() {
72
+ @Override
73
+ public void doFrame(long frameTimeNanos) {
74
+ handleFrame(frameTimeNanos);
75
+ }
76
+ };
77
+ }
78
+
79
+ void setThresholdMilliseconds(double thresholdMilliseconds) {
80
+ thresholdNanos = thresholdMilliseconds <= 0
81
+ ? 0
82
+ : (long) (thresholdMilliseconds * NANOS_PER_MILLISECOND);
83
+ if (thresholdNanos == 0) {
84
+ stop();
85
+ }
86
+ }
87
+
88
+ void setBridgeContext(Map<String, Object> context) {
89
+ bridgeContext = context == null
90
+ ? Collections.<String, Object>emptyMap()
91
+ : new HashMap<>(context);
92
+ }
93
+
94
+ void start() {
95
+ final int startGeneration;
96
+ synchronized (stateLock) {
97
+ if (thresholdNanos == 0) {
98
+ return;
99
+ }
100
+ requestedRunning = true;
101
+ startGeneration = ++generation;
102
+ }
103
+ try {
104
+ boolean accepted = executor.runOnJsThread(new Runnable() {
105
+ @Override
106
+ public void run() {
107
+ if (!isCurrentRequest(startGeneration, true)) {
108
+ return;
109
+ }
110
+ try {
111
+ if (scheduler != null) {
112
+ scheduler.removeFrameCallback();
113
+ }
114
+ scheduler = schedulerFactory.create(frameCallback);
115
+ activeGeneration = startGeneration;
116
+ lastFrameTimeNanos = 0;
117
+ scheduler.postFrameCallback();
118
+ } catch (IllegalStateException ignored) {
119
+ disableRequestOnJsThread(startGeneration);
120
+ }
121
+ }
122
+ });
123
+ if (!accepted) {
124
+ markRequestDisabled(startGeneration);
125
+ }
126
+ } catch (IllegalStateException ignored) {
127
+ markRequestDisabled(startGeneration);
128
+ }
129
+ }
130
+
131
+ void stop() {
132
+ stop(null);
133
+ }
134
+
135
+ // Unlike a lifecycle pause, SDK shutdown must prevent automatic restarts.
136
+ void disable(final Runnable completion) {
137
+ thresholdNanos = 0;
138
+ stop(completion);
139
+ }
140
+
141
+ void stop(final Runnable completion) {
142
+ final Runnable completeOnce = once(completion);
143
+ final int stopGeneration;
144
+ synchronized (stateLock) {
145
+ requestedRunning = false;
146
+ stopGeneration = ++generation;
147
+ }
148
+ try {
149
+ boolean accepted = executor.runOnJsThread(new Runnable() {
150
+ @Override
151
+ public void run() {
152
+ if (isCurrentRequest(stopGeneration, false)) {
153
+ try {
154
+ if (scheduler != null) {
155
+ scheduler.removeFrameCallback();
156
+ }
157
+ } catch (IllegalStateException ignored) {
158
+ // The request is already disabled, so only local state needs clearing.
159
+ }
160
+ scheduler = null;
161
+ activeGeneration = 0;
162
+ lastFrameTimeNanos = 0;
163
+ }
164
+ completeOnce.run();
165
+ }
166
+ });
167
+ if (!accepted) {
168
+ completeOnce.run();
169
+ }
170
+ } catch (IllegalStateException ignored) {
171
+ // The JavaScript queue may already be unavailable during teardown.
172
+ completeOnce.run();
173
+ }
174
+ }
175
+
176
+ private boolean isCurrentRequest(int requestGeneration, boolean running) {
177
+ synchronized (stateLock) {
178
+ return generation == requestGeneration && requestedRunning == running;
179
+ }
180
+ }
181
+
182
+ private void markRequestDisabled(int requestGeneration) {
183
+ synchronized (stateLock) {
184
+ if (generation == requestGeneration) {
185
+ requestedRunning = false;
186
+ }
187
+ }
188
+ }
189
+
190
+ private void disableRequestOnJsThread(int requestGeneration) {
191
+ markRequestDisabled(requestGeneration);
192
+ scheduler = null;
193
+ activeGeneration = 0;
194
+ lastFrameTimeNanos = 0;
195
+ }
196
+
197
+ private static Runnable once(final Runnable completion) {
198
+ final AtomicBoolean completed = new AtomicBoolean();
199
+ return new Runnable() {
200
+ @Override
201
+ public void run() {
202
+ if (completed.compareAndSet(false, true) && completion != null) {
203
+ completion.run();
204
+ }
205
+ }
206
+ };
207
+ }
208
+
209
+ private void handleFrame(long frameTimeNanos) {
210
+ if (!isCurrentRequest(activeGeneration, true) || scheduler == null) {
211
+ return;
212
+ }
213
+
214
+ if (lastFrameTimeNanos != 0) {
215
+ long durationNanos = frameTimeNanos - lastFrameTimeNanos;
216
+ if (durationNanos > thresholdNanos) {
217
+ try {
218
+ // Take a detection-time snapshot before handing the event to the SDK.
219
+ reporter.reportLongTask(durationNanos, new HashMap<>(bridgeContext));
220
+ } catch (RuntimeException ignored) {
221
+ // Reporting must not interrupt the JavaScript frame callback loop.
222
+ }
223
+ }
224
+ }
225
+ lastFrameTimeNanos = frameTimeNanos;
226
+
227
+ if (isCurrentRequest(activeGeneration, true) && scheduler != null) {
228
+ try {
229
+ scheduler.postFrameCallback();
230
+ } catch (IllegalStateException ignored) {
231
+ disableRequestOnJsThread(activeGeneration);
232
+ }
233
+ }
234
+ }
235
+
236
+ private static final class ChoreographerSchedulerFactory implements FrameSchedulerFactory {
237
+ @Override
238
+ public FrameScheduler create(FrameCallback callback) {
239
+ return new ChoreographerScheduler(Choreographer.getInstance(), callback);
240
+ }
241
+ }
242
+
243
+ private static final class ChoreographerScheduler implements FrameScheduler {
244
+ private final Choreographer choreographer;
245
+ private final Choreographer.FrameCallback callback;
246
+
247
+ private ChoreographerScheduler(
248
+ Choreographer choreographer,
249
+ final FrameCallback callback
250
+ ) {
251
+ this.choreographer = choreographer;
252
+ this.callback = new Choreographer.FrameCallback() {
253
+ @Override
254
+ public void doFrame(long frameTimeNanos) {
255
+ callback.doFrame(frameTimeNanos);
256
+ }
257
+ };
258
+ }
259
+
260
+ @Override
261
+ public void postFrameCallback() {
262
+ choreographer.postFrameCallback(callback);
263
+ }
264
+
265
+ @Override
266
+ public void removeFrameCallback() {
267
+ choreographer.removeFrameCallback(callback);
268
+ }
269
+ }
270
+
271
+ private static final class NativeLongTaskReporter implements Reporter {
272
+ @Override
273
+ public void reportLongTask(long durationNanos, Map<String, Object> property) {
274
+ FTRUMGlobalManager.get().addLongTask(
275
+ "",
276
+ durationNanos,
277
+ new HashMap<>(property)
278
+ );
279
+ }
280
+ }
281
+ }
@@ -1,14 +1,13 @@
1
1
  package com.ft.sdk.reactnative.utils;
2
2
 
3
3
  import java.util.regex.Pattern;
4
- import java.util.regex.Matcher;
5
4
 
6
5
  public class ReactNativeUtils {
7
6
 
8
- private static final Pattern[] RN_DEV_INNER_URL_REGEX = {
9
- Pattern.compile("^http://((10|172|192).[0-9]+.[0-9]+.[0-9]+|localhost|127.0.0.1):808[0-9]/logs$"), // expo
10
- Pattern.compile("^http://localhost:808[0-9]/(hot|symbolicate|message|inspector).*$") // rn
11
- };
7
+ private static final Pattern RN_DEV_INNER_URL_PATTERN = Pattern.compile(
8
+ "^(?:https?|wss?)://(?:(?:10|172|192)(?:\\.[0-9]+){3}|localhost|127\\.0\\.0\\.1|\\[::1\\]):808[0-9]/(?:hot|symbolicate|message|inspector|status|assets|logs|debugger-proxy)(?:[/?#].*)?$",
9
+ Pattern.CASE_INSENSITIVE
10
+ );
12
11
 
13
12
  private ReactNativeUtils() {
14
13
  // Private constructor to prevent instantiation
@@ -48,12 +47,6 @@ public class ReactNativeUtils {
48
47
  * @return
49
48
  */
50
49
  public static boolean isReactNativeDevUrl(String url) {
51
- for (Pattern pattern : RN_DEV_INNER_URL_REGEX) {
52
- Matcher matcher = pattern.matcher(url);
53
- if (matcher.matches()) {
54
- return true;
55
- }
56
- }
57
- return false;
50
+ return url != null && RN_DEV_INNER_URL_PATTERN.matcher(url).matches();
58
51
  }
59
52
  }