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

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 (69) 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 +193 -10
  25. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +29 -7
  26. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +36 -12
  27. package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +7 -0
  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 +179 -12
  36. package/dist/modules/NetworkLogger.d.ts +0 -5
  37. package/dist/modules/NetworkLogger.js +9 -1
  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 +6 -0
  43. package/dist/utils/FeatureFlags.js +35 -0
  44. package/dist/utils/LuciqUtils.d.ts +25 -0
  45. package/dist/utils/LuciqUtils.js +50 -0
  46. package/dist/utils/RouteMatcher.d.ts +30 -0
  47. package/dist/utils/RouteMatcher.js +67 -0
  48. package/dist/utils/XhrNetworkInterceptor.js +85 -53
  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 +4 -2
  56. package/scripts/get-github-app-token.sh +70 -0
  57. package/scripts/notify-github.sh +17 -8
  58. package/src/components/LuciqCaptureScreenLoading.tsx +210 -0
  59. package/src/index.ts +4 -0
  60. package/src/modules/APM.ts +42 -0
  61. package/src/modules/Luciq.ts +210 -12
  62. package/src/modules/NetworkLogger.ts +26 -1
  63. package/src/modules/apm/ScreenLoadingManager.ts +364 -0
  64. package/src/native/NativeAPM.ts +22 -0
  65. package/src/native/NativeLuciq.ts +1 -1
  66. package/src/utils/FeatureFlags.ts +44 -0
  67. package/src/utils/LuciqUtils.ts +64 -0
  68. package/src/utils/RouteMatcher.ts +83 -0
  69. package/src/utils/XhrNetworkInterceptor.ts +128 -55
@@ -32,6 +32,8 @@ 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
+
35
37
  public final ConcurrentHashMap<String, OnCompleteCallback<NetworkLogSnapshot>> callbackMap = new ConcurrentHashMap<String, OnCompleteCallback<NetworkLogSnapshot>>();
36
38
 
37
39
  public RNLuciqNetworkLoggerModule(ReactApplicationContext reactContext) {
@@ -57,7 +59,9 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
57
59
  }
58
60
 
59
61
  private boolean getFlagValue(String key) {
60
- return InternalAPM._isFeatureEnabledCP(key, "");
62
+ boolean value = InternalAPM._isFeatureEnabledCP(key, "");
63
+ Log.d(NET_TAG, "[getFlagValue] key=" + key + ", value=" + value);
64
+ return value;
61
65
  }
62
66
 
63
67
  private WritableMap convertFromMapToWritableMap(Map<String, Object> map) {
@@ -86,14 +90,18 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
86
90
  */
87
91
  @ReactMethod
88
92
  public void isNativeInterceptionEnabled(Promise promise) {
93
+ Log.d(NET_TAG, "[isNativeInterceptionEnabled] Querying CP_NATIVE_INTERCEPTION_ENABLED flag");
89
94
  MainThreadHandler.runOnMainThread(new Runnable() {
90
95
  @Override
91
96
  public void run() {
92
97
  try {
93
- promise.resolve(getFlagValue(CP_NATIVE_INTERCEPTION_ENABLED));
98
+ boolean enabled = getFlagValue(CP_NATIVE_INTERCEPTION_ENABLED);
99
+ Log.d(NET_TAG, "[isNativeInterceptionEnabled] Result=" + enabled);
100
+ promise.resolve(enabled);
94
101
  } catch (Exception e) {
102
+ Log.e(NET_TAG, "[isNativeInterceptionEnabled] Error — falling back to false (JS interceptor)", e);
95
103
  e.printStackTrace();
96
- promise.resolve(false); // Will rollback to JS interceptor
104
+ promise.resolve(false);
97
105
  }
98
106
 
99
107
  }
@@ -107,14 +115,18 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
107
115
  */
108
116
  @ReactMethod
109
117
  public void hasAPMNetworkPlugin(Promise promise) {
118
+ Log.d(NET_TAG, "[hasAPMNetworkPlugin] Querying APM_NETWORK_PLUGIN_INSTALLED flag");
110
119
  MainThreadHandler.runOnMainThread(new Runnable() {
111
120
  @Override
112
121
  public void run() {
113
122
  try {
114
- promise.resolve(getFlagValue(APM_NETWORK_PLUGIN_INSTALLED));
123
+ boolean hasPlugin = getFlagValue(APM_NETWORK_PLUGIN_INSTALLED);
124
+ Log.d(NET_TAG, "[hasAPMNetworkPlugin] Result=" + hasPlugin);
125
+ promise.resolve(hasPlugin);
115
126
  } catch (Exception e) {
127
+ Log.e(NET_TAG, "[hasAPMNetworkPlugin] Error — falling back to false", e);
116
128
  e.printStackTrace();
117
- promise.resolve(false); // Will rollback to JS interceptor
129
+ promise.resolve(false);
118
130
  }
119
131
 
120
132
  }
@@ -124,12 +136,14 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
124
136
 
125
137
  @ReactMethod
126
138
  public void registerNetworkLogsListener() {
139
+ Log.d(NET_TAG, "[registerNetworkLogsListener] Registering network log sanitizer");
127
140
  MainThreadHandler.runOnMainThread(new Runnable() {
128
141
  @Override
129
142
  public void run() {
130
143
  InternalAPM._registerNetworkLogSanitizer((networkLogSnapshot, onCompleteCallback) -> {
131
144
  final String id = String.valueOf(onCompleteCallback.hashCode());
132
145
  callbackMap.put(id, onCompleteCallback);
146
+ Log.d(NET_TAG, "[NetworkLogSanitizer] Received snapshot — id=" + id + ", url=" + networkLogSnapshot.getUrl() + ", responseCode=" + networkLogSnapshot.getResponseCode() + ", callbackMapSize=" + callbackMap.size());
133
147
 
134
148
  WritableMap networkSnapshotParams = Arguments.createMap();
135
149
  networkSnapshotParams.putString("id", id);
@@ -147,6 +161,7 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
147
161
  }
148
162
 
149
163
  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());
150
165
  });
151
166
  }
152
167
  });
@@ -154,10 +169,12 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
154
169
 
155
170
  @ReactMethod
156
171
  public void resetNetworkLogsListener() {
172
+ Log.d(NET_TAG, "[resetNetworkLogsListener] Clearing network log sanitizer, callbackMapSize=" + callbackMap.size());
157
173
  MainThreadHandler.runOnMainThread(new Runnable() {
158
174
  @Override
159
175
  public void run() {
160
176
  InternalAPM._registerNetworkLogSanitizer(null);
177
+ Log.d(NET_TAG, "[resetNetworkLogsListener] Sanitizer cleared");
161
178
  }
162
179
  });
163
180
  }
@@ -172,23 +189,28 @@ public class RNLuciqNetworkLoggerModule extends EventEmitterModule {
172
189
  ReadableMap requestHeaders,
173
190
  ReadableMap responseHeaders
174
191
  ) {
192
+ Log.d(NET_TAG, "[updateNetworkLogSnapshot] callbackID=" + callbackID + ", url=" + url + ", responseCode=" + responseCode + ", callbackMapSize=" + callbackMap.size());
175
193
  try {
176
- // Convert ReadableMap to a Java Map for easier handling
177
194
  Map<String, Object> requestHeadersMap = convertReadableMapToMap(requestHeaders);
178
195
  Map<String, Object> responseHeadersMap = convertReadableMapToMap(responseHeaders);
179
196
 
180
197
  NetworkLogSnapshot modifiedSnapshot = null;
181
198
  if (!url.isEmpty()) {
182
199
  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)");
183
202
  }
184
203
 
185
204
  final OnCompleteCallback<NetworkLogSnapshot> callback = callbackMap.get(callbackID);
186
205
  if (callback != null) {
187
206
  callback.onComplete(modifiedSnapshot);
188
207
  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());
189
211
  }
190
212
  } catch (Exception e) {
191
- // Reject the promise to indicate an error occurred
213
+ Log.e(NET_TAG, "[updateNetworkLogSnapshot] Exception processing snapshot: " + e.getMessage() + " for callbackID=" + callbackID, e);
192
214
  Log.e("IB-CP-Bridge", "LuciqNetworkLogger.updateNetworkLogSnapshot failed to parse the network snapshot object.");
193
215
  }
194
216
  }
@@ -86,6 +86,7 @@ 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";
89
90
 
90
91
  private LuciqCustomTextPlaceHolder placeHolders;
91
92
  private static Report currentReport;
@@ -163,6 +164,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
163
164
 
164
165
 
165
166
  ) {
167
+ Log.d(NET_TAG, "[init] Called — logLevel=" + logLevel + ", useNativeNetworkInterception=" + useNativeNetworkInterception + ", codePushVersion=" + codePushVersion + ", appVariant=" + appVariant);
166
168
  MainThreadHandler.runOnMainThread(new Runnable() {
167
169
  @Override
168
170
  public void run() {
@@ -204,6 +206,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
204
206
  }
205
207
 
206
208
  builder.build();
209
+ Log.d(NET_TAG, "[init] SDK build complete");
207
210
  }
208
211
  });
209
212
  }
@@ -969,6 +972,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
969
972
  final String requestHeaders,
970
973
  final String responseHeaders,
971
974
  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));
972
976
  try {
973
977
  final String date = String.valueOf(System.currentTimeMillis());
974
978
 
@@ -985,11 +989,14 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
985
989
  networkLog.setRequestHeaders(requestHeaders);
986
990
  networkLog.setResponseHeaders(responseHeaders);
987
991
  } catch (OutOfMemoryError | Exception exception) {
992
+ Log.e(NET_TAG, "[networkLogAndroid-Core] OOM/Error setting log contents: " + exception.getMessage() + " for " + method + " " + url);
988
993
  Log.d(TAG, "Error: " + exception.getMessage() + "while trying to set network log contents (request body, response body, request headers, and response headers).");
989
994
  }
990
995
 
991
996
  networkLog.insert();
997
+ Log.d(NET_TAG, "[networkLogAndroid-Core] Successfully inserted NetworkLog: " + method + " " + url);
992
998
  } catch (OutOfMemoryError | Exception exception) {
999
+ Log.e(NET_TAG, "[networkLogAndroid-Core] OOM/Error inserting network log: " + exception.getMessage() + " for " + method + " " + url);
993
1000
  Log.d(TAG, "Error: " + exception.getMessage() + "while trying to insert a network log");
994
1001
  }
995
1002
  }
@@ -1078,16 +1085,18 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1078
1085
  * Reports that the screen has been changed (Repro Steps) the screen sent to this method will be the 'current view' on the dashboard
1079
1086
  *
1080
1087
  * @param screenName string containing the screen name
1088
+ * @param spanId the span ID for screen loading tracking (nullable)
1081
1089
  */
1082
1090
  @ReactMethod
1083
- public void reportScreenChange(final String screenName) {
1091
+ public void reportScreenChange(final String screenName, @Nullable final String spanId) {
1084
1092
  MainThreadHandler.runOnMainThread(new Runnable() {
1085
1093
  @Override
1086
1094
  public void run() {
1087
1095
  try {
1088
- Method method = getMethod(Class.forName("ai.luciq.library.Luciq"), "reportScreenChange", Bitmap.class, String.class);
1096
+ Long uiTraceId = spanId != null ? Long.parseLong(spanId) : null;
1097
+ Method method = getMethod(Class.forName("ai.luciq.library.Luciq"), "reportScreenChange", Bitmap.class, String.class , Long.class);
1089
1098
  if (method != null) {
1090
- method.invoke(null, null, screenName);
1099
+ method.invoke(null, null, screenName , uiTraceId);
1091
1100
  }
1092
1101
  } catch (Exception e) {
1093
1102
  e.printStackTrace();
@@ -1170,7 +1179,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1170
1179
  */
1171
1180
  @ReactMethod
1172
1181
  public void registerFeatureFlagsChangeListener() {
1173
-
1182
+ Log.d(NET_TAG, "[registerFeatureFlagsChangeListener] Registering native feature flags listener");
1174
1183
  MainThreadHandler.runOnMainThread(new Runnable() {
1175
1184
  @Override
1176
1185
  public void run() {
@@ -1178,6 +1187,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1178
1187
  InternalCore.INSTANCE._setFeaturesStateListener(new FeaturesStateListener() {
1179
1188
  @Override
1180
1189
  public void invoke(@NonNull CoreFeaturesState featuresState) {
1190
+ Log.d(NET_TAG, "[FeatureFlagsListener] Received update — W3CTraceID=" + featuresState.isW3CExternalTraceIdEnabled() + ", generatedHeader=" + featuresState.isAttachingGeneratedHeaderEnabled() + ", caughtHeader=" + featuresState.isAttachingCapturedHeaderEnabled() + ", networkBodyLimit=" + featuresState.getNetworkLogCharLimit());
1181
1191
  WritableMap params = Arguments.createMap();
1182
1192
  params.putBoolean("isW3ExternalTraceIDEnabled", featuresState.isW3CExternalTraceIdEnabled());
1183
1193
  params.putBoolean("isW3ExternalGeneratedHeaderEnabled", featuresState.isAttachingGeneratedHeaderEnabled());
@@ -1185,9 +1195,11 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1185
1195
  params.putInt("networkBodyLimit",featuresState.getNetworkLogCharLimit());
1186
1196
 
1187
1197
  sendEvent(Constants.LCQ_ON_FEATURE_FLAGS_UPDATE_RECEIVED_CALLBACK, params);
1198
+ Log.d(NET_TAG, "[FeatureFlagsListener] Sent event to JS: " + Constants.LCQ_ON_FEATURE_FLAGS_UPDATE_RECEIVED_CALLBACK);
1188
1199
  }
1189
1200
  });
1190
1201
  } catch (Exception e) {
1202
+ Log.e(NET_TAG, "[registerFeatureFlagsChangeListener] Failed to register listener", e);
1191
1203
  e.printStackTrace();
1192
1204
  }
1193
1205
 
@@ -1202,13 +1214,16 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1202
1214
  */
1203
1215
  @ReactMethod
1204
1216
  public void isW3ExternalTraceIDEnabled(Promise promise) {
1205
-
1217
+ Log.d(NET_TAG, "[isW3ExternalTraceIDEnabled] Querying native flag");
1206
1218
  MainThreadHandler.runOnMainThread(new Runnable() {
1207
1219
  @Override
1208
1220
  public void run() {
1209
1221
  try {
1210
- promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID));
1222
+ boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID);
1223
+ Log.d(NET_TAG, "[isW3ExternalTraceIDEnabled] Result=" + enabled);
1224
+ promise.resolve(enabled);
1211
1225
  } catch (Exception e) {
1226
+ Log.e(NET_TAG, "[isW3ExternalTraceIDEnabled] Error querying flag", e);
1212
1227
  e.printStackTrace();
1213
1228
  promise.resolve(false);
1214
1229
  }
@@ -1224,13 +1239,16 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1224
1239
  */
1225
1240
  @ReactMethod
1226
1241
  public void isW3ExternalGeneratedHeaderEnabled(Promise promise) {
1227
-
1242
+ Log.d(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Querying native flag");
1228
1243
  MainThreadHandler.runOnMainThread(new Runnable() {
1229
1244
  @Override
1230
1245
  public void run() {
1231
1246
  try {
1232
- promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER));
1247
+ boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER);
1248
+ Log.d(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Result=" + enabled);
1249
+ promise.resolve(enabled);
1233
1250
  } catch (Exception e) {
1251
+ Log.e(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Error querying flag", e);
1234
1252
  e.printStackTrace();
1235
1253
  promise.resolve(false);
1236
1254
  }
@@ -1245,13 +1263,16 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1245
1263
  */
1246
1264
  @ReactMethod
1247
1265
  public void isW3CaughtHeaderEnabled(Promise promise) {
1248
-
1266
+ Log.d(NET_TAG, "[isW3CaughtHeaderEnabled] Querying native flag");
1249
1267
  MainThreadHandler.runOnMainThread(new Runnable() {
1250
1268
  @Override
1251
1269
  public void run() {
1252
1270
  try {
1253
- promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER));
1271
+ boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER);
1272
+ Log.d(NET_TAG, "[isW3CaughtHeaderEnabled] Result=" + enabled);
1273
+ promise.resolve(enabled);
1254
1274
  } catch (Exception e) {
1275
+ Log.e(NET_TAG, "[isW3CaughtHeaderEnabled] Error querying flag", e);
1255
1276
  e.printStackTrace();
1256
1277
  promise.resolve(false);
1257
1278
  }
@@ -1345,13 +1366,16 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1345
1366
  */
1346
1367
  @ReactMethod
1347
1368
  public void getNetworkBodyMaxSize(Promise promise) {
1348
-
1369
+ Log.d(NET_TAG, "[getNetworkBodyMaxSize] Querying network body size limit");
1349
1370
  MainThreadHandler.runOnMainThread(new Runnable() {
1350
1371
  @Override
1351
1372
  public void run() {
1352
1373
  try {
1353
- promise.resolve(InternalCore.INSTANCE.get_networkLogCharLimit());
1374
+ Object limit = InternalCore.INSTANCE.get_networkLogCharLimit();
1375
+ Log.d(NET_TAG, "[getNetworkBodyMaxSize] Result=" + limit);
1376
+ promise.resolve(limit);
1354
1377
  } catch (Exception e) {
1378
+ Log.e(NET_TAG, "[getNetworkBodyMaxSize] Error querying limit", e);
1355
1379
  e.printStackTrace();
1356
1380
  promise.resolve(false);
1357
1381
  }
@@ -1,5 +1,7 @@
1
1
  package ai.luciq.reactlibrary.utils;
2
2
 
3
+ import android.util.Log;
4
+
3
5
  import androidx.annotation.Nullable;
4
6
  import androidx.annotation.VisibleForTesting;
5
7
 
@@ -10,6 +12,7 @@ import com.facebook.react.bridge.WritableMap;
10
12
  import com.facebook.react.modules.core.DeviceEventManagerModule;
11
13
 
12
14
  public abstract class EventEmitterModule extends ReactContextBaseJavaModule {
15
+ private static final String NET_TAG = "LCQ-RN-NET";
13
16
  private int listenerCount = 0;
14
17
 
15
18
  public EventEmitterModule(ReactApplicationContext context) {
@@ -22,14 +25,18 @@ public abstract class EventEmitterModule extends ReactContextBaseJavaModule {
22
25
  getReactApplicationContext()
23
26
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
24
27
  .emit(event, params);
28
+ } else {
29
+ Log.w(NET_TAG, "[EventEmitter] Event DROPPED (no JS listeners): event=" + event + ", module=" + getName() + ", listenerCount=0");
25
30
  }
26
31
  }
27
32
 
28
33
  protected void addListener(String ignoredEvent) {
29
34
  listenerCount++;
35
+ Log.d(NET_TAG, "[EventEmitter] addListener — module=" + getName() + ", event=" + ignoredEvent + ", listenerCount=" + listenerCount);
30
36
  }
31
37
 
32
38
  protected void removeListeners(Integer count) {
33
39
  listenerCount -= count;
40
+ Log.d(NET_TAG, "[EventEmitter] removeListeners — module=" + getName() + ", removed=" + count + ", listenerCount=" + listenerCount);
34
41
  }
35
42
  }
@@ -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.