@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,37 @@
1
+ package com.ft.sdk.reactnative;
2
+
3
+ import android.os.Handler;
4
+ import android.os.Looper;
5
+
6
+ import com.facebook.react.bridge.ReactApplicationContext;
7
+ import com.facebook.react.bridge.queue.MessageQueueThread;
8
+ import com.facebook.react.bridge.queue.MessageQueueThreadImpl;
9
+ import com.facebook.react.bridge.queue.ReactQueueConfiguration;
10
+
11
+ final class ReactJsThreadExecutor implements JsLongTaskMonitor.Executor {
12
+ private final ReactApplicationContext reactContext;
13
+
14
+ ReactJsThreadExecutor(ReactApplicationContext reactContext) {
15
+ this.reactContext = reactContext;
16
+ }
17
+
18
+ @Override
19
+ public boolean runOnJsThread(Runnable runnable) {
20
+ if (!reactContext.hasActiveCatalystInstance()) {
21
+ return false;
22
+ }
23
+ ReactQueueConfiguration configuration =
24
+ reactContext.getCatalystInstance().getReactQueueConfiguration();
25
+ if (configuration == null) {
26
+ return false;
27
+ }
28
+ MessageQueueThread queue = configuration.getJSQueueThread();
29
+ if (!(queue instanceof MessageQueueThreadImpl)) {
30
+ return false;
31
+ }
32
+ Looper looper = ((MessageQueueThreadImpl) queue).getLooper();
33
+ // Older RN queue APIs return void. Post SDK work to the same JS looper so
34
+ // a stopped queue can explicitly reject it, without waiting for a callback.
35
+ return looper != null && new Handler(looper).post(runnable);
36
+ }
37
+ }
@@ -0,0 +1,53 @@
1
+ package com.ft.sdk.reactnative;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+
5
+ import com.ft.sdk.FTSDKConfig;
6
+
7
+ import org.junit.Test;
8
+
9
+ public class FTMobileImplTest {
10
+ @Test
11
+ public void buildsConfigWithoutInitialUploadAddress() {
12
+ FTSDKConfig config = FTMobileImpl.buildSDKConfig(null, null, null);
13
+
14
+ assertEquals("", config.getDatakitUrl());
15
+ assertEquals("", config.getDatawayUrl());
16
+ assertEquals("", config.getClientToken());
17
+ }
18
+
19
+ @Test
20
+ public void ignoresIncompleteDatawayConfig() {
21
+ FTSDKConfig config = FTMobileImpl.buildSDKConfig(null, "https://ingest.example.com", null);
22
+
23
+ assertEquals("", config.getDatakitUrl());
24
+ assertEquals("", config.getDatawayUrl());
25
+ assertEquals("", config.getClientToken());
26
+ }
27
+
28
+ @Test
29
+ public void keepsExistingDatakitConfigBehavior() {
30
+ FTSDKConfig config = FTMobileImpl.buildSDKConfig(
31
+ "http://10.0.0.1:9529",
32
+ "https://ingest.example.com",
33
+ "client-token"
34
+ );
35
+
36
+ assertEquals("http://10.0.0.1:9529", config.getDatakitUrl());
37
+ assertEquals("", config.getDatawayUrl());
38
+ assertEquals("", config.getClientToken());
39
+ }
40
+
41
+ @Test
42
+ public void keepsExistingDatawayConfigBehavior() {
43
+ FTSDKConfig config = FTMobileImpl.buildSDKConfig(
44
+ null,
45
+ "https://ingest.example.com",
46
+ "client-token"
47
+ );
48
+
49
+ assertEquals("", config.getDatakitUrl());
50
+ assertEquals("https://ingest.example.com", config.getDatawayUrl());
51
+ assertEquals("client-token", config.getClientToken());
52
+ }
53
+ }
@@ -0,0 +1,117 @@
1
+ package com.ft.sdk.reactnative;
2
+
3
+ import com.facebook.react.bridge.Promise;
4
+ import com.facebook.react.bridge.ReactApplicationContext;
5
+
6
+ import org.junit.Test;
7
+
8
+ import java.lang.reflect.Method;
9
+
10
+ import static org.junit.Assume.assumeTrue;
11
+ import static org.mockito.ArgumentMatchers.any;
12
+ import static org.mockito.Mockito.mock;
13
+ import static org.mockito.Mockito.times;
14
+ import static org.mockito.Mockito.verify;
15
+ import static org.mockito.Mockito.verifyNoMoreInteractions;
16
+ import static org.mockito.Mockito.when;
17
+ import static org.junit.Assert.assertFalse;
18
+ import static org.junit.Assert.assertTrue;
19
+
20
+ public class FTRUMModuleTest {
21
+ @Test
22
+ public void bridgeContextUpdatesDoNotStartOrReviveMonitor() {
23
+ ReactApplicationContext context = mock(ReactApplicationContext.class);
24
+ // Exercise the real module forwarding separately: only a snapshot is installed.
25
+ FTRUMModule module = new FTRUMModule(context);
26
+ assertTrue(module.setLongTaskContext(null));
27
+ module.invalidate();
28
+ assertFalse(module.setLongTaskContext(null));
29
+ }
30
+ @Test
31
+ public void shutdownResolvesWhenJSQueueIsUnavailable() {
32
+ ReactApplicationContext context = mock(ReactApplicationContext.class);
33
+ FTRUMModule module = new FTRUMModule(context);
34
+ Promise promise = mock(Promise.class);
35
+
36
+ module.stopLongTaskTracking(promise);
37
+
38
+ verify(promise, times(1)).resolve(null);
39
+ verifyNoMoreInteractions(promise);
40
+ }
41
+
42
+ @Test
43
+ public void shutdownResolvesWhenRNRejectsDispatch() throws Exception {
44
+ Method dispatch = ReactApplicationContext.class.getMethod("runOnJSQueueThread", Runnable.class);
45
+ // RN 0.63 has a void API. Reflection keeps this test compilable on both APIs.
46
+ assumeTrue(dispatch.getReturnType() == boolean.class);
47
+ ReactApplicationContext context = mock(ReactApplicationContext.class);
48
+ when(dispatch.invoke(context, any(Runnable.class))).thenReturn(false);
49
+ FTRUMModule module = new FTRUMModule(context);
50
+ Promise promise = mock(Promise.class);
51
+
52
+ module.stopLongTaskTracking(promise);
53
+
54
+ dispatch.invoke(verify(context, times(1)), any(Runnable.class));
55
+ verify(promise, times(1)).resolve(null);
56
+ verifyNoMoreInteractions(promise);
57
+ }
58
+
59
+ @Test
60
+ public void shutdownResolvesWhenRNQueueIsNotInitialized() throws Exception {
61
+ Method dispatch = ReactApplicationContext.class.getMethod("runOnJSQueueThread", Runnable.class);
62
+ assumeTrue(dispatch.getReturnType() == boolean.class);
63
+ ReactApplicationContext context = mock(ReactApplicationContext.class);
64
+ when(dispatch.invoke(context, any(Runnable.class))).thenThrow(new AssertionError("JS queue is null"));
65
+ FTRUMModule module = new FTRUMModule(context);
66
+ Promise promise = mock(Promise.class);
67
+
68
+ module.stopLongTaskTracking(promise);
69
+
70
+ verify(promise, times(1)).resolve(null);
71
+ verifyNoMoreInteractions(promise);
72
+ }
73
+
74
+ @Test
75
+ public void invalidateAndLegacyDestroyShareOneCleanup() {
76
+ ReactApplicationContext context = mock(ReactApplicationContext.class);
77
+ FTRUMModule module = new FTRUMModule(context);
78
+
79
+ module.invalidate();
80
+ module.invalidate();
81
+ module.onCatalystInstanceDestroy();
82
+
83
+ verify(context, times(1)).removeLifecycleEventListener(any(FTRUMImpl.class));
84
+ }
85
+
86
+ @Test
87
+ public void legacyDestroyThenInvalidateAlsoCleansUpOnce() {
88
+ ReactApplicationContext context = mock(ReactApplicationContext.class);
89
+ FTRUMModule module = new FTRUMModule(context);
90
+
91
+ module.onCatalystInstanceDestroy();
92
+ module.invalidate();
93
+
94
+ verify(context, times(1)).removeLifecycleEventListener(any(FTRUMImpl.class));
95
+ }
96
+
97
+ @Test
98
+ public void destroyedRUMDoesNotResumeMonitoring() {
99
+ ReactApplicationContext context = mock(ReactApplicationContext.class);
100
+ JsLongTaskMonitor.Executor executor = mock(JsLongTaskMonitor.Executor.class);
101
+ JsLongTaskMonitor monitor = new JsLongTaskMonitor(
102
+ executor,
103
+ mock(JsLongTaskMonitor.FrameSchedulerFactory.class),
104
+ mock(JsLongTaskMonitor.Reporter.class)
105
+ );
106
+ FTRUMImpl rum = new FTRUMImpl(context, monitor);
107
+
108
+ rum.destroy();
109
+ rum.destroy();
110
+ monitor.setThresholdMilliseconds(100);
111
+ rum.onHostResume();
112
+
113
+ verify(context, times(1)).removeLifecycleEventListener(rum);
114
+ verify(executor, times(1)).runOnJsThread(any(Runnable.class));
115
+ verifyNoMoreInteractions(executor);
116
+ }
117
+ }
@@ -0,0 +1,446 @@
1
+ package com.ft.sdk.reactnative;
2
+
3
+ import org.junit.Before;
4
+ import org.junit.Test;
5
+
6
+ import java.util.ArrayList;
7
+ import java.util.List;
8
+ import java.util.Map;
9
+ import java.util.HashMap;
10
+ import java.util.Collections;
11
+
12
+ import static org.junit.Assert.assertEquals;
13
+ import static org.junit.Assert.assertFalse;
14
+ import static org.junit.Assert.assertTrue;
15
+
16
+ public class JsLongTaskMonitorTest {
17
+
18
+ private FakeExecutor executor;
19
+ private FakeSchedulerFactory schedulerFactory;
20
+ private FakeReporter reporter;
21
+ private JsLongTaskMonitor monitor;
22
+
23
+ @Before
24
+ public void setUp() {
25
+ executor = new FakeExecutor();
26
+ schedulerFactory = new FakeSchedulerFactory();
27
+ reporter = new FakeReporter();
28
+ monitor = new JsLongTaskMonitor(executor, schedulerFactory, reporter);
29
+ }
30
+
31
+ @Test
32
+ public void createsFrameSchedulerOnlyOnJsThread() {
33
+ monitor.setThresholdMilliseconds(100);
34
+ monitor.start();
35
+
36
+ assertEquals(0, schedulerFactory.createCount);
37
+ assertEquals(1, executor.runnables.size());
38
+
39
+ executor.runAll();
40
+
41
+ assertEquals(1, schedulerFactory.createCount);
42
+ assertEquals(1, schedulerFactory.scheduler.postCount);
43
+ }
44
+
45
+ @Test
46
+ public void bridgeContextUpdatesBeforeDelayedStartAndSnapshotsEachDetection() {
47
+ Map<String, Object> context = new HashMap<>();
48
+ context.put("sdk_bridge_info", "{\"react_native\":\"test-version\"}");
49
+ context.put("wgt_id", "first");
50
+ monitor.setBridgeContext(context);
51
+ monitor.setThresholdMilliseconds(100);
52
+ monitor.start();
53
+ context.put("wgt_id", "second");
54
+ monitor.setBridgeContext(context);
55
+ executor.runAll();
56
+ schedulerFactory.scheduler.fire(1_000_000_000L);
57
+ schedulerFactory.scheduler.fire(1_250_000_000L);
58
+ Map<String, Object> firstEvent = reporter.properties.get(0);
59
+ Map<String, Object> expected = new HashMap<>(context);
60
+ context.put("wgt_id", "third");
61
+ monitor.setBridgeContext(context);
62
+ schedulerFactory.scheduler.fire(1_500_000_000L);
63
+
64
+ assertEquals(java.util.Arrays.asList(250_000_000L, 250_000_000L), reporter.durations);
65
+ assertEquals(expected, firstEvent);
66
+ assertEquals(context, reporter.properties.get(1));
67
+ monitor.disable(null);
68
+ context.put("wgt_id", "after-stop");
69
+ monitor.setBridgeContext(context);
70
+ schedulerFactory.scheduler.fire(2_000_000_000L);
71
+ assertEquals(2, reporter.durations.size());
72
+ }
73
+
74
+ @Test
75
+ public void noBridgeContextReportsEmptyProperties() {
76
+ startWithThreshold(100);
77
+ schedulerFactory.scheduler.fire(1_000_000_000L);
78
+ schedulerFactory.scheduler.fire(1_250_000_000L);
79
+ assertEquals(250_000_000L, reporter.durations.get(0).longValue());
80
+ assertEquals(Collections.emptyMap(), reporter.properties.get(0));
81
+ }
82
+
83
+ @Test
84
+ public void firstFrameIsBaselineAndThresholdComparisonIsStrict() {
85
+ startWithThreshold(100);
86
+
87
+ schedulerFactory.scheduler.fire(1_000_000_000L);
88
+ schedulerFactory.scheduler.fire(1_100_000_000L);
89
+ schedulerFactory.scheduler.fire(1_200_000_001L);
90
+
91
+ assertEquals(1, reporter.durations.size());
92
+ assertEquals(100_000_001L, reporter.durations.get(0).longValue());
93
+ }
94
+
95
+ @Test
96
+ public void reportsEveryQualifyingFullFrameInterval() {
97
+ startWithThreshold(100);
98
+
99
+ schedulerFactory.scheduler.fire(1_000_000_000L);
100
+ schedulerFactory.scheduler.fire(1_050_000_000L);
101
+ schedulerFactory.scheduler.fire(1_200_000_000L);
102
+ schedulerFactory.scheduler.fire(1_450_000_000L);
103
+
104
+ assertEquals(2, reporter.durations.size());
105
+ assertEquals(150_000_000L, reporter.durations.get(0).longValue());
106
+ assertEquals(250_000_000L, reporter.durations.get(1).longValue());
107
+ }
108
+
109
+ @Test
110
+ public void preservesFractionalMillisecondThresholds() {
111
+ startWithThreshold(200.5);
112
+
113
+ schedulerFactory.scheduler.fire(1_000_000_000L);
114
+ schedulerFactory.scheduler.fire(1_200_500_000L);
115
+ schedulerFactory.scheduler.fire(1_401_000_001L);
116
+
117
+ assertEquals(1, reporter.durations.size());
118
+ assertEquals(200_500_001L, reporter.durations.get(0).longValue());
119
+ }
120
+
121
+ @Test
122
+ public void stopRemovesCallbackAndRestartResetsBaseline() {
123
+ startWithThreshold(100);
124
+ FakeScheduler firstScheduler = schedulerFactory.scheduler;
125
+ firstScheduler.fire(1_000_000_000L);
126
+
127
+ monitor.stop();
128
+ firstScheduler.fire(1_500_000_000L);
129
+ executor.runAll();
130
+
131
+ assertEquals(1, firstScheduler.removeCount);
132
+ assertTrue(reporter.durations.isEmpty());
133
+
134
+ monitor.start();
135
+ executor.runAll();
136
+ FakeScheduler secondScheduler = schedulerFactory.scheduler;
137
+ secondScheduler.fire(2_000_000_000L);
138
+ secondScheduler.fire(2_100_000_001L);
139
+
140
+ assertEquals(1, reporter.durations.size());
141
+ assertEquals(100_000_001L, reporter.durations.get(0).longValue());
142
+ }
143
+
144
+ @Test
145
+ public void repeatedStartKeepsOnlyLatestRequest() {
146
+ monitor.setThresholdMilliseconds(100);
147
+ monitor.start();
148
+ monitor.start();
149
+
150
+ executor.runAll();
151
+
152
+ assertEquals(1, schedulerFactory.createCount);
153
+ assertEquals(1, schedulerFactory.scheduler.postCount);
154
+ }
155
+
156
+ @Test
157
+ public void zeroThresholdDisablesMonitoring() {
158
+ startWithThreshold(100);
159
+ FakeScheduler scheduler = schedulerFactory.scheduler;
160
+
161
+ monitor.setThresholdMilliseconds(0);
162
+ executor.runAll();
163
+ scheduler.fire(1_000_000_000L);
164
+ scheduler.fire(2_000_000_000L);
165
+
166
+ assertEquals(1, scheduler.removeCount);
167
+ assertTrue(reporter.durations.isEmpty());
168
+ }
169
+
170
+ @Test
171
+ public void schedulerFailureDoesNotCrashOrPostFrames() {
172
+ schedulerFactory.shouldThrow = true;
173
+ monitor.setThresholdMilliseconds(100);
174
+ monitor.start();
175
+
176
+ executor.runAll();
177
+
178
+ assertEquals(1, schedulerFactory.createCount);
179
+ assertFalse(schedulerFactory.scheduler.wasPosted);
180
+ }
181
+
182
+ @Test
183
+ public void javascriptQueueFailureDoesNotCrash() {
184
+ executor.shouldThrow = true;
185
+ monitor.setThresholdMilliseconds(100);
186
+
187
+ monitor.start();
188
+ monitor.stop();
189
+
190
+ assertEquals(0, schedulerFactory.createCount);
191
+ }
192
+
193
+ @Test
194
+ public void rejectedStartDoesNotCreateScheduler() {
195
+ executor.acceptsTasks = false;
196
+ monitor.setThresholdMilliseconds(100);
197
+
198
+ monitor.start();
199
+
200
+ assertEquals(0, schedulerFactory.createCount);
201
+ assertTrue(executor.runnables.isEmpty());
202
+ }
203
+
204
+ @Test
205
+ public void rejectedStopCompletesAndSuppressesExistingFrameCallbacks() {
206
+ startWithThreshold(100);
207
+ FakeScheduler scheduler = schedulerFactory.scheduler;
208
+ scheduler.fire(1_000_000_000L);
209
+ int posts = scheduler.postCount;
210
+ executor.acceptsTasks = false;
211
+ final int[] completions = {0};
212
+
213
+ monitor.disable(() -> completions[0]++);
214
+ scheduler.fire(1_500_000_000L);
215
+ monitor.start();
216
+
217
+ assertEquals(1, completions[0]);
218
+ assertEquals(posts, scheduler.postCount);
219
+ assertEquals(1, schedulerFactory.createCount);
220
+ assertTrue(reporter.durations.isEmpty());
221
+ }
222
+
223
+ @Test
224
+ public void throwingQueueCompletesStopExactlyOnce() {
225
+ executor.shouldThrow = true;
226
+ final int[] completions = {0};
227
+
228
+ monitor.stop(() -> completions[0]++);
229
+
230
+ assertEquals(1, completions[0]);
231
+ }
232
+
233
+ @Test
234
+ public void inlineCompletionFailureIsNotInvokedTwice() {
235
+ JsLongTaskMonitor inlineMonitor = new JsLongTaskMonitor(
236
+ runnable -> {
237
+ runnable.run();
238
+ return true;
239
+ },
240
+ schedulerFactory,
241
+ reporter
242
+ );
243
+ final int[] completions = {0};
244
+
245
+ inlineMonitor.stop(() -> {
246
+ completions[0]++;
247
+ throw new IllegalStateException("Promise is already unavailable");
248
+ });
249
+
250
+ assertEquals(1, completions[0]);
251
+ }
252
+
253
+ @Test
254
+ public void framePostFailureDisablesMonitoring() {
255
+ schedulerFactory.throwOnPost = true;
256
+ monitor.setThresholdMilliseconds(100);
257
+
258
+ monitor.start();
259
+ executor.runAll();
260
+
261
+ assertEquals(1, schedulerFactory.createCount);
262
+ assertFalse(schedulerFactory.scheduler.wasPosted);
263
+ }
264
+
265
+ @Test
266
+ public void stopCompletionRunsAfterCallbackRemoval() {
267
+ startWithThreshold(100);
268
+ final boolean[] completed = {false};
269
+
270
+ monitor.stop(new Runnable() {
271
+ @Override
272
+ public void run() {
273
+ completed[0] = true;
274
+ }
275
+ });
276
+
277
+ assertFalse(completed[0]);
278
+ assertEquals(0, schedulerFactory.scheduler.removeCount);
279
+
280
+ executor.runAll();
281
+
282
+ assertTrue(completed[0]);
283
+ assertEquals(1, schedulerFactory.scheduler.removeCount);
284
+ }
285
+
286
+ @Test
287
+ public void disablePreventsRestartUntilConfiguredAgain() {
288
+ startWithThreshold(100);
289
+ FakeScheduler firstScheduler = schedulerFactory.scheduler;
290
+ firstScheduler.fire(1_000_000_000L);
291
+ final int[] completions = {0};
292
+
293
+ monitor.disable(() -> completions[0]++);
294
+ // A foreground event can arrive before the JS queue finishes stopping.
295
+ monitor.start();
296
+ firstScheduler.fire(1_500_000_000L);
297
+ assertEquals(0, completions[0]);
298
+ executor.runAll();
299
+
300
+ assertEquals(1, completions[0]);
301
+ assertEquals(1, firstScheduler.removeCount);
302
+ assertTrue(reporter.durations.isEmpty());
303
+
304
+ // Further background/foreground transitions must not re-enable monitoring.
305
+ monitor.stop();
306
+ monitor.start();
307
+ executor.runAll();
308
+ assertEquals(1, schedulerFactory.createCount);
309
+
310
+ monitor.setThresholdMilliseconds(200);
311
+ monitor.start();
312
+ executor.runAll();
313
+ assertEquals(2, schedulerFactory.createCount);
314
+ schedulerFactory.scheduler.fire(10_000_000_000L);
315
+ schedulerFactory.scheduler.fire(10_150_000_000L);
316
+ assertTrue(reporter.durations.isEmpty());
317
+ schedulerFactory.scheduler.fire(10_400_000_000L);
318
+ assertEquals(1, reporter.durations.size());
319
+ assertEquals(250_000_000L, reporter.durations.get(0).longValue());
320
+ }
321
+
322
+ @Test
323
+ public void repeatedDisableCancelsPendingStartAndCompletesEachRequest() {
324
+ monitor.setThresholdMilliseconds(100);
325
+ monitor.start();
326
+ final int[] completions = {0};
327
+
328
+ monitor.disable(() -> completions[0]++);
329
+ monitor.disable(() -> completions[0]++);
330
+ monitor.start();
331
+ executor.runAll();
332
+
333
+ assertEquals(2, completions[0]);
334
+ assertEquals(0, schedulerFactory.createCount);
335
+ assertTrue(reporter.durations.isEmpty());
336
+ }
337
+
338
+ @Test
339
+ public void reporterFailureDoesNotStopFrameCallbacks() {
340
+ reporter.shouldThrow = true;
341
+ startWithThreshold(100);
342
+ int initialPostCount = schedulerFactory.scheduler.postCount;
343
+
344
+ schedulerFactory.scheduler.fire(1_000_000_000L);
345
+ schedulerFactory.scheduler.fire(1_200_000_000L);
346
+
347
+ assertEquals(initialPostCount + 2, schedulerFactory.scheduler.postCount);
348
+ }
349
+
350
+ private void startWithThreshold(double thresholdMilliseconds) {
351
+ monitor.setThresholdMilliseconds(thresholdMilliseconds);
352
+ monitor.start();
353
+ executor.runAll();
354
+ }
355
+
356
+ private static final class FakeExecutor implements JsLongTaskMonitor.Executor {
357
+ private final List<Runnable> runnables = new ArrayList<>();
358
+ private boolean shouldThrow;
359
+ private boolean acceptsTasks = true;
360
+
361
+ @Override
362
+ public boolean runOnJsThread(Runnable runnable) {
363
+ if (shouldThrow) {
364
+ throw new IllegalStateException("JavaScript queue unavailable");
365
+ }
366
+ if (!acceptsTasks) {
367
+ return false;
368
+ }
369
+ runnables.add(runnable);
370
+ return true;
371
+ }
372
+
373
+ private void runAll() {
374
+ while (!runnables.isEmpty()) {
375
+ runnables.remove(0).run();
376
+ }
377
+ }
378
+ }
379
+
380
+ private static final class FakeSchedulerFactory
381
+ implements JsLongTaskMonitor.FrameSchedulerFactory {
382
+ private int createCount;
383
+ private boolean shouldThrow;
384
+ private boolean throwOnPost;
385
+ private FakeScheduler scheduler = new FakeScheduler();
386
+
387
+ @Override
388
+ public JsLongTaskMonitor.FrameScheduler create(
389
+ JsLongTaskMonitor.FrameCallback callback
390
+ ) {
391
+ createCount++;
392
+ if (shouldThrow) {
393
+ throw new IllegalStateException("No looper");
394
+ }
395
+ scheduler = new FakeScheduler();
396
+ scheduler.callback = callback;
397
+ scheduler.throwOnPost = throwOnPost;
398
+ return scheduler;
399
+ }
400
+ }
401
+
402
+ private static final class FakeScheduler
403
+ implements JsLongTaskMonitor.FrameScheduler {
404
+ private JsLongTaskMonitor.FrameCallback callback;
405
+ private int postCount;
406
+ private int removeCount;
407
+ private boolean wasPosted;
408
+ private boolean throwOnPost;
409
+
410
+ @Override
411
+ public void postFrameCallback() {
412
+ if (throwOnPost) {
413
+ throw new IllegalStateException("Frame scheduler unavailable");
414
+ }
415
+ postCount++;
416
+ wasPosted = true;
417
+ }
418
+
419
+ @Override
420
+ public void removeFrameCallback() {
421
+ removeCount++;
422
+ wasPosted = false;
423
+ }
424
+
425
+ private void fire(long timestampNanos) {
426
+ if (callback != null) {
427
+ callback.doFrame(timestampNanos);
428
+ }
429
+ }
430
+ }
431
+
432
+ private static final class FakeReporter implements JsLongTaskMonitor.Reporter {
433
+ private final List<Long> durations = new ArrayList<>();
434
+ private final List<Map<String, Object>> properties = new ArrayList<>();
435
+ private boolean shouldThrow;
436
+
437
+ @Override
438
+ public void reportLongTask(long durationNanos, Map<String, Object> property) {
439
+ if (shouldThrow) {
440
+ throw new IllegalStateException("Reporter unavailable");
441
+ }
442
+ durations.add(durationNanos);
443
+ properties.add(property);
444
+ }
445
+ }
446
+ }
@@ -0,0 +1,33 @@
1
+ package com.ft.sdk.reactnative.utils;
2
+
3
+ import static org.junit.Assert.assertFalse;
4
+ import static org.junit.Assert.assertTrue;
5
+
6
+ import org.junit.Test;
7
+
8
+ public class ReactNativeUtilsTest {
9
+
10
+ @Test
11
+ public void filtersAndroidEmulatorInspectorResource() {
12
+ assertTrue(ReactNativeUtils.isReactNativeDevUrl(
13
+ "http://10.0.2.2:8081/inspector/device?name=sdk_gphone64_arm64%20-%2012%20-%20API%2032"
14
+ + "&app=com.ft.sdk.reactnative.example&device=a520746d2418fde7c33ceff1d852b112040a08c0"
15
+ ));
16
+ }
17
+
18
+ @Test
19
+ public void filtersReactNativeAndExpoDevelopmentResources() {
20
+ assertTrue(ReactNativeUtils.isReactNativeDevUrl("http://localhost:8081/hot?platform=android"));
21
+ assertTrue(ReactNativeUtils.isReactNativeDevUrl("ws://10.0.2.2:8081/message?device=emulator"));
22
+ assertTrue(ReactNativeUtils.isReactNativeDevUrl("http://192.168.1.2:8082/logs"));
23
+ assertTrue(ReactNativeUtils.isReactNativeDevUrl("http://[::1]:8081/debugger-proxy?role=client"));
24
+ }
25
+
26
+ @Test
27
+ public void doesNotFilterApplicationResources() {
28
+ assertFalse(ReactNativeUtils.isReactNativeDevUrl("https://api.example.com/inspector/device"));
29
+ assertFalse(ReactNativeUtils.isReactNativeDevUrl("http://10.0.2.2:9529/inspector/device"));
30
+ assertFalse(ReactNativeUtils.isReactNativeDevUrl("http://10.0.2.2:8081/business"));
31
+ assertFalse(ReactNativeUtils.isReactNativeDevUrl(null));
32
+ }
33
+ }