@luciq/react-native 19.4.0-47504-SNAPSHOT → 19.6.0

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 (67) hide show
  1. package/.claude/agents/codebase-analyzer.md +33 -0
  2. package/.claude/agents/codebase-locator.md +42 -0
  3. package/.claude/agents/codebase-pattern-finder.md +40 -0
  4. package/.claude/commands/apply-pr-reviews.md +253 -0
  5. package/.claude/commands/create-jira-workitem.md +27 -0
  6. package/.claude/commands/create-pr.md +138 -0
  7. package/.claude/commands/create-public-release-notes.md +145 -0
  8. package/.claude/commands/create-rca.md +286 -0
  9. package/.claude/commands/debug-sdk.md +66 -0
  10. package/.claude/commands/describe-pr.md +40 -0
  11. package/.claude/commands/new-api.md +60 -0
  12. package/.claude/commands/new-feature.md +75 -0
  13. package/.claude/commands/pr-review.md +85 -0
  14. package/.claude/commands/research-codebase.md +41 -0
  15. package/.claude/commands/review.md +73 -0
  16. package/.claude/memory/MEMORY.md +1 -0
  17. package/.claude/memory/feedback_pr_title_format.md +10 -0
  18. package/.claude/rules/react-native-typescript.md +46 -0
  19. package/CHANGELOG.md +12 -0
  20. package/CLAUDE.md +125 -0
  21. package/android/native.gradle +1 -1
  22. package/android/proguard-rules.txt +1 -1
  23. package/android/src/main/java/ai/luciq/reactlibrary/LuciqScreenLoadingFrameTracker.java +88 -0
  24. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +184 -19
  25. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +7 -29
  26. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +14 -34
  27. package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +0 -7
  28. package/dist/components/LuciqCaptureScreenLoading.d.ts +8 -0
  29. package/dist/components/LuciqCaptureScreenLoading.js +154 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +2 -0
  32. package/dist/modules/APM.d.ts +19 -0
  33. package/dist/modules/APM.js +38 -0
  34. package/dist/modules/Luciq.d.ts +1 -1
  35. package/dist/modules/Luciq.js +170 -13
  36. package/dist/modules/NetworkLogger.d.ts +5 -0
  37. package/dist/modules/NetworkLogger.js +1 -9
  38. package/dist/modules/apm/ScreenLoadingManager.d.ts +99 -0
  39. package/dist/modules/apm/ScreenLoadingManager.js +296 -0
  40. package/dist/native/NativeAPM.d.ts +9 -0
  41. package/dist/native/NativeLuciq.d.ts +1 -1
  42. package/dist/utils/FeatureFlags.d.ts +0 -6
  43. package/dist/utils/FeatureFlags.js +0 -35
  44. package/dist/utils/LuciqUtils.d.ts +25 -0
  45. package/dist/utils/LuciqUtils.js +44 -6
  46. package/dist/utils/RouteMatcher.d.ts +30 -0
  47. package/dist/utils/RouteMatcher.js +67 -0
  48. package/dist/utils/XhrNetworkInterceptor.js +53 -85
  49. package/ios/RNLuciq/LuciqAPMBridge.m +82 -0
  50. package/ios/RNLuciq/LuciqReactBridge.m +1 -1
  51. package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.h +11 -0
  52. package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.m +121 -0
  53. package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +14 -0
  54. package/ios/native.rb +1 -1
  55. package/package.json +5 -1
  56. package/src/components/LuciqCaptureScreenLoading.tsx +210 -0
  57. package/src/index.ts +4 -0
  58. package/src/modules/APM.ts +42 -0
  59. package/src/modules/Luciq.ts +198 -14
  60. package/src/modules/NetworkLogger.ts +1 -26
  61. package/src/modules/apm/ScreenLoadingManager.ts +364 -0
  62. package/src/native/NativeAPM.ts +22 -0
  63. package/src/native/NativeLuciq.ts +1 -1
  64. package/src/utils/FeatureFlags.ts +0 -44
  65. package/src/utils/LuciqUtils.ts +49 -15
  66. package/src/utils/RouteMatcher.ts +83 -0
  67. package/src/utils/XhrNetworkInterceptor.ts +55 -128
@@ -32,8 +32,6 @@ import java.util.concurrent.ConcurrentHashMap;
32
32
 
33
33
  public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
34
34
 
35
- private static final String NET_TAG = "LCQ-RN-NET";
36
-
37
35
  public final ConcurrentHashMap<String, OnCompleteCallback<NetworkLogSnapshot>> callbackMap = new ConcurrentHashMap<String, OnCompleteCallback<NetworkLogSnapshot>>();
38
36
 
39
37
  public RNLuciqNetworkLoggerModule(ReactApplicationContext reactContext) {
@@ -59,9 +57,7 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
59
57
  }
60
58
 
61
59
  private boolean getFlagValue(String key) {
62
- boolean value = InternalAPM._isFeatureEnabledCP(key, "");
63
- Log.d(NET_TAG, "[getFlagValue] key=" + key + ", value=" + value);
64
- return value;
60
+ return InternalAPM._isFeatureEnabledCP(key, "");
65
61
  }
66
62
 
67
63
  private WritableMap convertFromMapToWritableMap(Map<String, Object> map) {
@@ -90,18 +86,14 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
90
86
  */
91
87
  @ReactMethod
92
88
  public void isNativeInterceptionEnabled(Promise promise) {
93
- Log.d(NET_TAG, "[isNativeInterceptionEnabled] Querying CP_NATIVE_INTERCEPTION_ENABLED flag");
94
89
  MainThreadHandler.runOnMainThread(new Runnable() {
95
90
  @Override
96
91
  public void run() {
97
92
  try {
98
- boolean enabled = getFlagValue(CP_NATIVE_INTERCEPTION_ENABLED);
99
- Log.d(NET_TAG, "[isNativeInterceptionEnabled] Result=" + enabled);
100
- promise.resolve(enabled);
93
+ promise.resolve(getFlagValue(CP_NATIVE_INTERCEPTION_ENABLED));
101
94
  } catch (Exception e) {
102
- Log.e(NET_TAG, "[isNativeInterceptionEnabled] Error — falling back to false (JS interceptor)", e);
103
95
  e.printStackTrace();
104
- promise.resolve(false);
96
+ promise.resolve(false); // Will rollback to JS interceptor
105
97
  }
106
98
 
107
99
  }
@@ -115,18 +107,14 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
115
107
  */
116
108
  @ReactMethod
117
109
  public void hasAPMNetworkPlugin(Promise promise) {
118
- Log.d(NET_TAG, "[hasAPMNetworkPlugin] Querying APM_NETWORK_PLUGIN_INSTALLED flag");
119
110
  MainThreadHandler.runOnMainThread(new Runnable() {
120
111
  @Override
121
112
  public void run() {
122
113
  try {
123
- boolean hasPlugin = getFlagValue(APM_NETWORK_PLUGIN_INSTALLED);
124
- Log.d(NET_TAG, "[hasAPMNetworkPlugin] Result=" + hasPlugin);
125
- promise.resolve(hasPlugin);
114
+ promise.resolve(getFlagValue(APM_NETWORK_PLUGIN_INSTALLED));
126
115
  } catch (Exception e) {
127
- Log.e(NET_TAG, "[hasAPMNetworkPlugin] Error — falling back to false", e);
128
116
  e.printStackTrace();
129
- promise.resolve(false);
117
+ promise.resolve(false); // Will rollback to JS interceptor
130
118
  }
131
119
 
132
120
  }
@@ -136,14 +124,12 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
136
124
 
137
125
  @ReactMethod
138
126
  public void registerNetworkLogsListener() {
139
- Log.d(NET_TAG, "[registerNetworkLogsListener] Registering network log sanitizer");
140
127
  MainThreadHandler.runOnMainThread(new Runnable() {
141
128
  @Override
142
129
  public void run() {
143
130
  InternalAPM._registerNetworkLogSanitizer((networkLogSnapshot, onCompleteCallback) -> {
144
131
  final String id = String.valueOf(onCompleteCallback.hashCode());
145
132
  callbackMap.put(id, onCompleteCallback);
146
- Log.d(NET_TAG, "[NetworkLogSanitizer] Received snapshot — id=" + id + ", url=" + networkLogSnapshot.getUrl() + ", responseCode=" + networkLogSnapshot.getResponseCode() + ", callbackMapSize=" + callbackMap.size());
147
133
 
148
134
  WritableMap networkSnapshotParams = Arguments.createMap();
149
135
  networkSnapshotParams.putString("id", id);
@@ -161,7 +147,6 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
161
147
  }
162
148
 
163
149
  sendEvent(Constants.LCQ_NETWORK_LOGGER_HANDLER, networkSnapshotParams);
164
- Log.d(NET_TAG, "[NetworkLogSanitizer] Sent event to JS: " + Constants.LCQ_NETWORK_LOGGER_HANDLER + " for " + networkLogSnapshot.getUrl());
165
150
  });
166
151
  }
167
152
  });
@@ -169,12 +154,10 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
169
154
 
170
155
  @ReactMethod
171
156
  public void resetNetworkLogsListener() {
172
- Log.d(NET_TAG, "[resetNetworkLogsListener] Clearing network log sanitizer, callbackMapSize=" + callbackMap.size());
173
157
  MainThreadHandler.runOnMainThread(new Runnable() {
174
158
  @Override
175
159
  public void run() {
176
160
  InternalAPM._registerNetworkLogSanitizer(null);
177
- Log.d(NET_TAG, "[resetNetworkLogsListener] Sanitizer cleared");
178
161
  }
179
162
  });
180
163
  }
@@ -189,28 +172,23 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
189
172
  ReadableMap requestHeaders,
190
173
  ReadableMap responseHeaders
191
174
  ) {
192
- Log.d(NET_TAG, "[updateNetworkLogSnapshot] callbackID=" + callbackID + ", url=" + url + ", responseCode=" + responseCode + ", callbackMapSize=" + callbackMap.size());
193
175
  try {
176
+ // Convert ReadableMap to a Java Map for easier handling
194
177
  Map<String, Object> requestHeadersMap = convertReadableMapToMap(requestHeaders);
195
178
  Map<String, Object> responseHeadersMap = convertReadableMapToMap(responseHeaders);
196
179
 
197
180
  NetworkLogSnapshot modifiedSnapshot = null;
198
181
  if (!url.isEmpty()) {
199
182
  modifiedSnapshot = new NetworkLogSnapshot(url, requestHeadersMap, requestBody, responseHeadersMap, responseBody, responseCode);
200
- } else {
201
- Log.d(NET_TAG, "[updateNetworkLogSnapshot] Empty URL — snapshot will be null (request filtered/removed)");
202
183
  }
203
184
 
204
185
  final OnCompleteCallback<NetworkLogSnapshot> callback = callbackMap.get(callbackID);
205
186
  if (callback != null) {
206
187
  callback.onComplete(modifiedSnapshot);
207
188
  callbackMap.remove(callbackID);
208
- Log.d(NET_TAG, "[updateNetworkLogSnapshot] Callback invoked and removed for " + callbackID + ", remaining=" + callbackMap.size());
209
- } else {
210
- Log.e(NET_TAG, "[updateNetworkLogSnapshot] No callback found for callbackID=" + callbackID + " — possible leak or duplicate call, mapKeys=" + callbackMap.keySet());
211
189
  }
212
190
  } catch (Exception e) {
213
- Log.e(NET_TAG, "[updateNetworkLogSnapshot] Exception processing snapshot: " + e.getMessage() + " for callbackID=" + callbackID, e);
191
+ // Reject the promise to indicate an error occurred
214
192
  Log.e("IB-CP-Bridge", "LuciqNetworkLogger.updateNetworkLogSnapshot failed to parse the network snapshot object.");
215
193
  }
216
194
  }
@@ -86,7 +86,6 @@ import javax.annotation.Nullable;
86
86
  public class RNLuciqReactnativeModule extends EventEmitterModule {
87
87
 
88
88
  private static final String TAG = "Luciq-RN-Core";
89
- private static final String NET_TAG = "LCQ-RN-NET";
90
89
 
91
90
  private LuciqCustomTextPlaceHolder placeHolders;
92
91
  private static Report currentReport;
@@ -164,7 +163,6 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
164
163
 
165
164
 
166
165
  ) {
167
- Log.d(NET_TAG, "[init] Called — logLevel=" + logLevel + ", useNativeNetworkInterception=" + useNativeNetworkInterception + ", codePushVersion=" + codePushVersion + ", appVariant=" + appVariant);
168
166
  MainThreadHandler.runOnMainThread(new Runnable() {
169
167
  @Override
170
168
  public void run() {
@@ -206,7 +204,6 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
206
204
  }
207
205
 
208
206
  builder.build();
209
- Log.d(NET_TAG, "[init] SDK build complete");
210
207
  }
211
208
  });
212
209
  }
@@ -972,7 +969,6 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
972
969
  final String requestHeaders,
973
970
  final String responseHeaders,
974
971
  final double duration) {
975
- Log.d(NET_TAG, "[networkLogAndroid-Core] Received from JS: " + method + " " + url + ", status=" + (int) responseCode + ", duration=" + (long) duration + "ms, reqBodyLen=" + (requestBody != null ? requestBody.length() : 0) + ", resBodyLen=" + (responseBody != null ? responseBody.length() : 0));
976
972
  try {
977
973
  final String date = String.valueOf(System.currentTimeMillis());
978
974
 
@@ -989,14 +985,11 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
989
985
  networkLog.setRequestHeaders(requestHeaders);
990
986
  networkLog.setResponseHeaders(responseHeaders);
991
987
  } catch (OutOfMemoryError | Exception exception) {
992
- Log.e(NET_TAG, "[networkLogAndroid-Core] OOM/Error setting log contents: " + exception.getMessage() + " for " + method + " " + url);
993
988
  Log.d(TAG, "Error: " + exception.getMessage() + "while trying to set network log contents (request body, response body, request headers, and response headers).");
994
989
  }
995
990
 
996
991
  networkLog.insert();
997
- Log.d(NET_TAG, "[networkLogAndroid-Core] Successfully inserted NetworkLog: " + method + " " + url);
998
992
  } catch (OutOfMemoryError | Exception exception) {
999
- Log.e(NET_TAG, "[networkLogAndroid-Core] OOM/Error inserting network log: " + exception.getMessage() + " for " + method + " " + url);
1000
993
  Log.d(TAG, "Error: " + exception.getMessage() + "while trying to insert a network log");
1001
994
  }
1002
995
  }
@@ -1085,16 +1078,18 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1085
1078
  * Reports that the screen has been changed (Repro Steps) the screen sent to this method will be the 'current view' on the dashboard
1086
1079
  *
1087
1080
  * @param screenName string containing the screen name
1081
+ * @param spanId the span ID for screen loading tracking (nullable)
1088
1082
  */
1089
1083
  @ReactMethod
1090
- public void reportScreenChange(final String screenName) {
1084
+ public void reportScreenChange(final String screenName, @Nullable final String spanId) {
1091
1085
  MainThreadHandler.runOnMainThread(new Runnable() {
1092
1086
  @Override
1093
1087
  public void run() {
1094
1088
  try {
1095
- Method method = getMethod(Class.forName("ai.luciq.library.Luciq"), "reportScreenChange", Bitmap.class, String.class);
1089
+ Long uiTraceId = spanId != null ? Long.parseLong(spanId) : null;
1090
+ Method method = getMethod(Class.forName("ai.luciq.library.Luciq"), "reportScreenChange", Bitmap.class, String.class , Long.class);
1096
1091
  if (method != null) {
1097
- method.invoke(null, null, screenName);
1092
+ method.invoke(null, null, screenName , uiTraceId);
1098
1093
  }
1099
1094
  } catch (Exception e) {
1100
1095
  e.printStackTrace();
@@ -1177,7 +1172,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1177
1172
  */
1178
1173
  @ReactMethod
1179
1174
  public void registerFeatureFlagsChangeListener() {
1180
- Log.d(NET_TAG, "[registerFeatureFlagsChangeListener] Registering native feature flags listener");
1175
+
1181
1176
  MainThreadHandler.runOnMainThread(new Runnable() {
1182
1177
  @Override
1183
1178
  public void run() {
@@ -1185,7 +1180,6 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1185
1180
  InternalCore.INSTANCE._setFeaturesStateListener(new FeaturesStateListener() {
1186
1181
  @Override
1187
1182
  public void invoke(@NonNull CoreFeaturesState featuresState) {
1188
- Log.d(NET_TAG, "[FeatureFlagsListener] Received update — W3CTraceID=" + featuresState.isW3CExternalTraceIdEnabled() + ", generatedHeader=" + featuresState.isAttachingGeneratedHeaderEnabled() + ", caughtHeader=" + featuresState.isAttachingCapturedHeaderEnabled() + ", networkBodyLimit=" + featuresState.getNetworkLogCharLimit());
1189
1183
  WritableMap params = Arguments.createMap();
1190
1184
  params.putBoolean("isW3ExternalTraceIDEnabled", featuresState.isW3CExternalTraceIdEnabled());
1191
1185
  params.putBoolean("isW3ExternalGeneratedHeaderEnabled", featuresState.isAttachingGeneratedHeaderEnabled());
@@ -1193,11 +1187,9 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1193
1187
  params.putInt("networkBodyLimit",featuresState.getNetworkLogCharLimit());
1194
1188
 
1195
1189
  sendEvent(Constants.LCQ_ON_FEATURE_FLAGS_UPDATE_RECEIVED_CALLBACK, params);
1196
- Log.d(NET_TAG, "[FeatureFlagsListener] Sent event to JS: " + Constants.LCQ_ON_FEATURE_FLAGS_UPDATE_RECEIVED_CALLBACK);
1197
1190
  }
1198
1191
  });
1199
1192
  } catch (Exception e) {
1200
- Log.e(NET_TAG, "[registerFeatureFlagsChangeListener] Failed to register listener", e);
1201
1193
  e.printStackTrace();
1202
1194
  }
1203
1195
 
@@ -1212,16 +1204,13 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1212
1204
  */
1213
1205
  @ReactMethod
1214
1206
  public void isW3ExternalTraceIDEnabled(Promise promise) {
1215
- Log.d(NET_TAG, "[isW3ExternalTraceIDEnabled] Querying native flag");
1207
+
1216
1208
  MainThreadHandler.runOnMainThread(new Runnable() {
1217
1209
  @Override
1218
1210
  public void run() {
1219
1211
  try {
1220
- boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID);
1221
- Log.d(NET_TAG, "[isW3ExternalTraceIDEnabled] Result=" + enabled);
1222
- promise.resolve(enabled);
1212
+ promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID));
1223
1213
  } catch (Exception e) {
1224
- Log.e(NET_TAG, "[isW3ExternalTraceIDEnabled] Error querying flag", e);
1225
1214
  e.printStackTrace();
1226
1215
  promise.resolve(false);
1227
1216
  }
@@ -1237,16 +1226,13 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1237
1226
  */
1238
1227
  @ReactMethod
1239
1228
  public void isW3ExternalGeneratedHeaderEnabled(Promise promise) {
1240
- Log.d(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Querying native flag");
1229
+
1241
1230
  MainThreadHandler.runOnMainThread(new Runnable() {
1242
1231
  @Override
1243
1232
  public void run() {
1244
1233
  try {
1245
- boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER);
1246
- Log.d(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Result=" + enabled);
1247
- promise.resolve(enabled);
1234
+ promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER));
1248
1235
  } catch (Exception e) {
1249
- Log.e(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Error querying flag", e);
1250
1236
  e.printStackTrace();
1251
1237
  promise.resolve(false);
1252
1238
  }
@@ -1261,16 +1247,13 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1261
1247
  */
1262
1248
  @ReactMethod
1263
1249
  public void isW3CaughtHeaderEnabled(Promise promise) {
1264
- Log.d(NET_TAG, "[isW3CaughtHeaderEnabled] Querying native flag");
1250
+
1265
1251
  MainThreadHandler.runOnMainThread(new Runnable() {
1266
1252
  @Override
1267
1253
  public void run() {
1268
1254
  try {
1269
- boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER);
1270
- Log.d(NET_TAG, "[isW3CaughtHeaderEnabled] Result=" + enabled);
1271
- promise.resolve(enabled);
1255
+ promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER));
1272
1256
  } catch (Exception e) {
1273
- Log.e(NET_TAG, "[isW3CaughtHeaderEnabled] Error querying flag", e);
1274
1257
  e.printStackTrace();
1275
1258
  promise.resolve(false);
1276
1259
  }
@@ -1364,16 +1347,13 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1364
1347
  */
1365
1348
  @ReactMethod
1366
1349
  public void getNetworkBodyMaxSize(Promise promise) {
1367
- Log.d(NET_TAG, "[getNetworkBodyMaxSize] Querying network body size limit");
1350
+
1368
1351
  MainThreadHandler.runOnMainThread(new Runnable() {
1369
1352
  @Override
1370
1353
  public void run() {
1371
1354
  try {
1372
- Object limit = InternalCore.INSTANCE.get_networkLogCharLimit();
1373
- Log.d(NET_TAG, "[getNetworkBodyMaxSize] Result=" + limit);
1374
- promise.resolve(limit);
1355
+ promise.resolve(InternalCore.INSTANCE.get_networkLogCharLimit());
1375
1356
  } catch (Exception e) {
1376
- Log.e(NET_TAG, "[getNetworkBodyMaxSize] Error querying limit", e);
1377
1357
  e.printStackTrace();
1378
1358
  promise.resolve(false);
1379
1359
  }
@@ -1,7 +1,5 @@
1
1
  package ai.luciq.reactlibrary.utils;
2
2
 
3
- import android.util.Log;
4
-
5
3
  import androidx.annotation.Nullable;
6
4
  import androidx.annotation.VisibleForTesting;
7
5
 
@@ -12,7 +10,6 @@ import com.facebook.react.bridge.WritableMap;
12
10
  import com.facebook.react.modules.core.DeviceEventManagerModule;
13
11
 
14
12
  public abstract class EventEmitterModule extends ReactContextBaseJavaModule {
15
- private static final String NET_TAG = "LCQ-RN-NET";
16
13
  private int listenerCount = 0;
17
14
 
18
15
  public EventEmitterModule(ReactApplicationContext context) {
@@ -25,18 +22,14 @@ public abstract class EventEmitterModule extends ReactContextBaseJavaModule {
25
22
  getReactApplicationContext()
26
23
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
27
24
  .emit(event, params);
28
- } else {
29
- Log.w(NET_TAG, "[EventEmitter] Event DROPPED (no JS listeners): event=" + event + ", module=" + getName() + ", listenerCount=0");
30
25
  }
31
26
  }
32
27
 
33
28
  protected void addListener(String ignoredEvent) {
34
29
  listenerCount++;
35
- Log.d(NET_TAG, "[EventEmitter] addListener — module=" + getName() + ", event=" + ignoredEvent + ", listenerCount=" + listenerCount);
36
30
  }
37
31
 
38
32
  protected void removeListeners(Integer count) {
39
33
  listenerCount -= count;
40
- Log.d(NET_TAG, "[EventEmitter] removeListeners — module=" + getName() + ", removed=" + count + ", listenerCount=" + listenerCount);
41
34
  }
42
35
  }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { ViewProps } from 'react-native';
3
+ export interface LuciqScreenLoadingProps extends ViewProps {
4
+ screenName: string;
5
+ record?: boolean;
6
+ onMeasured?: (ttid: number) => void;
7
+ }
8
+ export declare function LuciqCaptureScreenLoading(props: LuciqScreenLoadingProps): React.JSX.Element;
@@ -0,0 +1,154 @@
1
+ import React, { useState, useRef, useEffect, useLayoutEffect, useContext } from 'react';
2
+ import { View } from 'react-native';
3
+ import { ScreenLoadingManager } from '../modules/apm/ScreenLoadingManager';
4
+ import { Logger } from '../utils/logger';
5
+ import { nowMicros, toEpochMicros } from '../utils/LuciqUtils';
6
+ // Context to handle nested components
7
+ const ScreenLoadingContext = React.createContext(false);
8
+ export function LuciqCaptureScreenLoading(props) {
9
+ const { screenName, record, onMeasured, onLayout, children, ...viewProps } = props;
10
+ const isNested = useContext(ScreenLoadingContext);
11
+ // Refs for timestamps (these don't need to trigger re-renders)
12
+ const constructorTimestampRef = useRef(nowMicros()); // microseconds
13
+ const renderStartTimestampRef = useRef(undefined);
14
+ const renderEndTimestampRef = useRef(undefined);
15
+ const mountTimestampRef = useRef(undefined);
16
+ // Guards to ensure single execution
17
+ const initializedRef = useRef(false);
18
+ const hasFirstRenderCompletedRef = useRef(false);
19
+ const attributesRecordedRef = useRef(false);
20
+ const initialSpanIdRef = useRef(null);
21
+ // Capture render start timestamp ONLY on first render
22
+ if (!hasFirstRenderCompletedRef.current) {
23
+ renderStartTimestampRef.current = nowMicros();
24
+ }
25
+ // Initialize span - runs once like constructor (lazy initialization)
26
+ if (!initializedRef.current) {
27
+ initializedRef.current = true;
28
+ // Initialize span if conditions are met
29
+ try {
30
+ if (record !== false && ScreenLoadingManager.isFeatureEnabled()) {
31
+ const span = ScreenLoadingManager.createSpan(screenName, true, constructorTimestampRef.current);
32
+ if (span) {
33
+ initialSpanIdRef.current = span.spanId;
34
+ Logger.log(`[LuciqScreenLoading] Span ${span.spanId} created in constructor`);
35
+ }
36
+ }
37
+ }
38
+ catch (error) {
39
+ Logger.error('[LuciqScreenLoading] Failed to create span:', error);
40
+ }
41
+ }
42
+ const [spanId, setSpanId] = useState(initialSpanIdRef.current);
43
+ const [isMeasured, setIsMeasured] = useState(false);
44
+ // Ref to avoid stale closure in useLayoutEffect
45
+ const onMeasuredRef = useRef(onMeasured);
46
+ useEffect(() => {
47
+ onMeasuredRef.current = onMeasured;
48
+ }, [onMeasured]);
49
+ // Refs to track latest values for cleanup (componentWillUnmount)
50
+ const spanIdRef = useRef(spanId);
51
+ const isMeasuredRef = useRef(isMeasured);
52
+ // Keep refs in sync with state
53
+ useEffect(() => {
54
+ spanIdRef.current = spanId;
55
+ }, [spanId]);
56
+ useEffect(() => {
57
+ isMeasuredRef.current = isMeasured;
58
+ }, [isMeasured]);
59
+ // Handle nested component detection
60
+ useEffect(() => {
61
+ // Check if we're nested and should ignore this component
62
+ if (isNested && initialSpanIdRef.current) {
63
+ Logger.log(`[LuciqScreenLoading] Nested component detected, ignoring span ${initialSpanIdRef.current}`);
64
+ // Cancel the span
65
+ setSpanId(null);
66
+ }
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ }, []); // Empty deps = componentDidMount
69
+ // Record lifecycle timestamps after first render completes (synchronous)
70
+ // useLayoutEffect fires synchronously after DOM mutations but before browser paint
71
+ useLayoutEffect(() => {
72
+ // Skip if no span, already recorded, or nested
73
+ if (!spanId || attributesRecordedRef.current || isNested) {
74
+ return;
75
+ }
76
+ // endSpan is async (native frame timestamp fetch), fire-and-forget from useLayoutEffect
77
+ ScreenLoadingManager.endSpan(spanId)
78
+ .then(() => {
79
+ const completedSpan = ScreenLoadingManager.getActiveSpan(spanId);
80
+ if (completedSpan?.ttid && onMeasuredRef.current) {
81
+ onMeasuredRef.current(completedSpan.ttid / 1000);
82
+ }
83
+ })
84
+ .catch((error) => {
85
+ Logger.warn('[LuciqScreenLoading] Failed to end span:', error);
86
+ });
87
+ attributesRecordedRef.current = true;
88
+ mountTimestampRef.current = nowMicros();
89
+ try {
90
+ // Record all timestamps
91
+ ScreenLoadingManager.addSpanAttribute(spanId, 'cnst_mus_st', toEpochMicros(constructorTimestampRef.current));
92
+ if (renderStartTimestampRef.current) {
93
+ ScreenLoadingManager.addSpanAttribute(spanId, 'rnd_mus_st', toEpochMicros(renderStartTimestampRef.current));
94
+ }
95
+ ScreenLoadingManager.addSpanAttribute(spanId, 'mnt_mus_st', toEpochMicros(mountTimestampRef.current));
96
+ // Record all durations
97
+ if (renderStartTimestampRef.current) {
98
+ // Constructor duration: time from component init to first render start
99
+ const constructorDuration = renderStartTimestampRef.current - constructorTimestampRef.current;
100
+ ScreenLoadingManager.addSpanAttribute(spanId, 'cnst_mus', constructorDuration);
101
+ }
102
+ if (renderEndTimestampRef.current && renderStartTimestampRef.current) {
103
+ // Render duration: time spent creating JSX
104
+ const renderDuration = renderEndTimestampRef.current - renderStartTimestampRef.current;
105
+ ScreenLoadingManager.addSpanAttribute(spanId, 'rnd_mus', renderDuration);
106
+ }
107
+ if (mountTimestampRef.current && renderEndTimestampRef.current) {
108
+ // Mount duration: time from render complete to effect execution
109
+ const mountDuration = mountTimestampRef.current - renderEndTimestampRef.current;
110
+ ScreenLoadingManager.addSpanAttribute(spanId, 'mnt_mus', mountDuration);
111
+ }
112
+ Logger.log(`[LuciqScreenLoading] Lifecycle measurements for span ${spanId}:`, {
113
+ constructor_us: renderStartTimestampRef.current
114
+ ? renderStartTimestampRef.current - constructorTimestampRef.current
115
+ : undefined,
116
+ render_us: renderEndTimestampRef.current && renderStartTimestampRef.current
117
+ ? renderEndTimestampRef.current - renderStartTimestampRef.current
118
+ : undefined,
119
+ mount_us: mountTimestampRef.current && renderEndTimestampRef.current
120
+ ? mountTimestampRef.current - renderEndTimestampRef.current
121
+ : undefined,
122
+ });
123
+ }
124
+ catch (error) {
125
+ Logger.error(`[LuciqScreenLoading] Failed to record attributes for span ${spanId}:`, error);
126
+ }
127
+ // End the span — mark as measured synchronously to guard against unmount race
128
+ setIsMeasured(true);
129
+ // eslint-disable-next-line react-hooks/exhaustive-deps
130
+ }, [spanId]); // Run when spanId is set
131
+ // componentWillUnmount equivalent
132
+ useEffect(() => {
133
+ return () => {
134
+ // Cleanup on unmount if not measured
135
+ if (spanIdRef.current && !isMeasuredRef.current) {
136
+ ScreenLoadingManager.endSpan(spanIdRef.current).catch((error) => {
137
+ Logger.warn('[LuciqScreenLoading] Failed to end span on unmount:', error);
138
+ });
139
+ }
140
+ };
141
+ }, []); // Empty deps = only runs cleanup on unmount
142
+ // Create the JSX result
143
+ const result = (<ScreenLoadingContext.Provider value={spanId !== null}>
144
+ <View {...viewProps} onLayout={onLayout}>
145
+ {children}
146
+ </View>
147
+ </ScreenLoadingContext.Provider>);
148
+ // Capture render end timestamp ONLY on first render (after JSX creation)
149
+ if (!hasFirstRenderCompletedRef.current) {
150
+ renderEndTimestampRef.current = nowMicros();
151
+ hasFirstRenderCompletedRef.current = true;
152
+ }
153
+ return result;
154
+ }
package/dist/index.d.ts CHANGED
@@ -18,4 +18,6 @@ import type { SessionMetadata } from './models/SessionMetadata';
18
18
  export * from './utils/Enums';
19
19
  export { Report, CustomSpan, APM, BugReporting, CrashReporting, FeatureRequests, NetworkLogger, SessionReplay, Replies, Surveys, ProactiveReportingConfigOptions, createProactiveReportingConfig, };
20
20
  export type { LuciqConfig, Survey, NetworkData, NetworkDataObfuscationHandler, SessionMetadata, ThemeConfig, };
21
+ export { LuciqCaptureScreenLoading } from './components/LuciqCaptureScreenLoading';
22
+ export type { LuciqScreenLoadingProps } from './components/LuciqCaptureScreenLoading';
21
23
  export default Luciq;
package/dist/index.js CHANGED
@@ -13,4 +13,6 @@ import * as Surveys from './modules/Surveys';
13
13
  import * as SessionReplay from './modules/SessionReplay';
14
14
  export * from './utils/Enums';
15
15
  export { Report, CustomSpan, APM, BugReporting, CrashReporting, FeatureRequests, NetworkLogger, SessionReplay, Replies, Surveys, createProactiveReportingConfig, };
16
+ // Screen Loading Component
17
+ export { LuciqCaptureScreenLoading } from './components/LuciqCaptureScreenLoading';
16
18
  export default Luciq;
@@ -138,3 +138,22 @@ export declare const startCustomSpan: (name: string) => Promise<CustomSpan | nul
138
138
  * ```
139
139
  */
140
140
  export declare const addCompletedCustomSpan: (name: string, startDate: Date, endDate: Date) => Promise<void>;
141
+ /**
142
+ * Enables or disables Screen Loading feature
143
+ * @param isEnabled
144
+ */
145
+ export declare const setScreenLoadingEnabled: (isEnabled: boolean) => void;
146
+ /**
147
+ * Extends the currently running screen loading trace with a new end timestamp.
148
+ */
149
+ export declare const endScreenLoading: () => void;
150
+ /**
151
+ * Exclude specific routes from automatic screen loading measurement
152
+ * @param routes Array of route names to exclude
153
+ */
154
+ export declare function excludeScreenLoadingRoutes(routes: string[]): void;
155
+ /**
156
+ * Include previously excluded routes back into screen loading measurement
157
+ * @param routes Array of route names to include (or empty to clear all exclusions)
158
+ */
159
+ export declare function includeScreenLoadingRoutes(routes?: string[]): void;
@@ -2,6 +2,12 @@ import { Platform } from 'react-native';
2
2
  import { NativeAPM } from '../native/NativeAPM';
3
3
  import { NativeLuciq } from '../native/NativeLuciq';
4
4
  import { startCustomSpan as startCustomSpanInternal, addCompletedCustomSpan as addCompletedCustomSpanInternal, } from '../utils/CustomSpansManager';
5
+ import { ScreenLoadingManager } from './apm/ScreenLoadingManager';
6
+ import { Logger } from '../utils/logger';
7
+ // Initialize Screen Loading on module load
8
+ ScreenLoadingManager.initialize().catch((error) => {
9
+ Logger.error('[APM] Failed to initialize Screen Loading:', error);
10
+ });
5
11
  /**
6
12
  * Enables or disables APM
7
13
  * @param isEnabled
@@ -171,3 +177,35 @@ export const startCustomSpan = async (name) => {
171
177
  export const addCompletedCustomSpan = async (name, startDate, endDate) => {
172
178
  return addCompletedCustomSpanInternal(name, startDate, endDate);
173
179
  };
180
+ /**
181
+ * Enables or disables Screen Loading feature
182
+ * @param isEnabled
183
+ */
184
+ export const setScreenLoadingEnabled = (isEnabled) => {
185
+ try {
186
+ NativeAPM.setScreenLoadingEnabled(isEnabled);
187
+ }
188
+ catch (error) {
189
+ Logger.error('[APM] Failed to set screen loading enabled:', error);
190
+ }
191
+ };
192
+ /**
193
+ * Extends the currently running screen loading trace with a new end timestamp.
194
+ */
195
+ export const endScreenLoading = () => {
196
+ ScreenLoadingManager.endScreenLoading();
197
+ };
198
+ /**
199
+ * Exclude specific routes from automatic screen loading measurement
200
+ * @param routes Array of route names to exclude
201
+ */
202
+ export function excludeScreenLoadingRoutes(routes) {
203
+ ScreenLoadingManager.excludeRoutes(routes);
204
+ }
205
+ /**
206
+ * Include previously excluded routes back into screen loading measurement
207
+ * @param routes Array of route names to include (or empty to clear all exclusions)
208
+ */
209
+ export function includeScreenLoadingRoutes(routes) {
210
+ ScreenLoadingManager.includeRoutes(routes);
211
+ }
@@ -286,7 +286,7 @@ export declare const onStateChange: (state?: NavigationStateV5) => void;
286
286
  * @param navigationRef a refrence of a navigation container
287
287
  *
288
288
  */
289
- export declare const setNavigationListener: (navigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>) => () => void;
289
+ export declare const setNavigationListener: (navigationRef: NavigationContainerRefWithCurrent<ReactNavigation.RootParamList>) => void;
290
290
  export declare const reportScreenChange: (screenName: string) => void;
291
291
  /**
292
292
  * Add feature flags to the next report.