@luciq/react-native 19.2.1 → 19.3.0-40271-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 (52) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +87 -0
  3. package/RNLuciq.podspec +1 -1
  4. package/android/native.gradle +1 -1
  5. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +211 -117
  6. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +29 -7
  7. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +51 -9
  8. package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +7 -0
  9. package/dist/constants/Strings.d.ts +9 -0
  10. package/dist/constants/Strings.js +12 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +2 -1
  13. package/dist/models/CustomSpan.d.ts +47 -0
  14. package/dist/models/CustomSpan.js +82 -0
  15. package/dist/modules/APM.d.ts +58 -0
  16. package/dist/modules/APM.js +62 -0
  17. package/dist/modules/Luciq.js +2 -1
  18. package/dist/modules/NetworkLogger.d.ts +0 -5
  19. package/dist/modules/NetworkLogger.js +9 -1
  20. package/dist/native/NativeAPM.d.ts +3 -0
  21. package/dist/native/NativeLuciq.d.ts +1 -0
  22. package/dist/utils/CustomSpansManager.d.ts +38 -0
  23. package/dist/utils/CustomSpansManager.js +173 -0
  24. package/dist/utils/FeatureFlags.d.ts +6 -0
  25. package/dist/utils/FeatureFlags.js +35 -0
  26. package/dist/utils/LuciqUtils.js +6 -0
  27. package/dist/utils/XhrNetworkInterceptor.js +85 -53
  28. package/ios/RNLuciq/LuciqAPMBridge.h +13 -0
  29. package/ios/RNLuciq/LuciqAPMBridge.m +55 -0
  30. package/ios/RNLuciq/LuciqReactBridge.m +12 -0
  31. package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +1 -0
  32. package/ios/native.rb +1 -1
  33. package/package.json +1 -2
  34. package/plugin/build/index.js +9 -2
  35. package/plugin/src/withLuciqIOS.ts +9 -2
  36. package/scripts/releases/changelog_to_slack_formatter.sh +9 -0
  37. package/scripts/releases/get_job_approver.sh +60 -0
  38. package/scripts/releases/get_release_notes.sh +22 -0
  39. package/scripts/releases/get_sdk_version.sh +5 -0
  40. package/scripts/releases/get_slack_id_from_username.sh +24 -0
  41. package/src/constants/Strings.ts +24 -0
  42. package/src/index.ts +2 -0
  43. package/src/models/CustomSpan.ts +102 -0
  44. package/src/modules/APM.ts +72 -0
  45. package/src/modules/Luciq.ts +3 -1
  46. package/src/modules/NetworkLogger.ts +26 -1
  47. package/src/native/NativeAPM.ts +7 -0
  48. package/src/native/NativeLuciq.ts +1 -0
  49. package/src/utils/CustomSpansManager.ts +202 -0
  50. package/src/utils/FeatureFlags.ts +44 -0
  51. package/src/utils/LuciqUtils.ts +15 -0
  52. 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
  }
@@ -222,6 +225,26 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
222
225
  });
223
226
  }
224
227
 
228
+ /**
229
+ * Checks if the SDK is built or not.
230
+ *
231
+ * @param promise Promise that resolves with boolean indicating if enabled
232
+ */
233
+ @ReactMethod
234
+ public void isBuilt(final Promise promise) {
235
+ MainThreadHandler.runOnMainThread(new Runnable() {
236
+ @Override
237
+ public void run() {
238
+ try {
239
+ promise.resolve(Luciq.isBuilt());
240
+ } catch (Exception e) {
241
+ e.printStackTrace();
242
+ promise.resolve(false);
243
+ }
244
+ }
245
+ });
246
+ }
247
+
225
248
  @ReactMethod
226
249
  public void setOverAirVersion(@Nullable final ReadableMap overAirVersion) {
227
250
  MainThreadHandler.runOnMainThread(new Runnable() {
@@ -949,6 +972,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
949
972
  final String requestHeaders,
950
973
  final String responseHeaders,
951
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));
952
976
  try {
953
977
  final String date = String.valueOf(System.currentTimeMillis());
954
978
 
@@ -965,11 +989,14 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
965
989
  networkLog.setRequestHeaders(requestHeaders);
966
990
  networkLog.setResponseHeaders(responseHeaders);
967
991
  } catch (OutOfMemoryError | Exception exception) {
992
+ Log.e(NET_TAG, "[networkLogAndroid-Core] OOM/Error setting log contents: " + exception.getMessage() + " for " + method + " " + url);
968
993
  Log.d(TAG, "Error: " + exception.getMessage() + "while trying to set network log contents (request body, response body, request headers, and response headers).");
969
994
  }
970
995
 
971
996
  networkLog.insert();
997
+ Log.d(NET_TAG, "[networkLogAndroid-Core] Successfully inserted NetworkLog: " + method + " " + url);
972
998
  } catch (OutOfMemoryError | Exception exception) {
999
+ Log.e(NET_TAG, "[networkLogAndroid-Core] OOM/Error inserting network log: " + exception.getMessage() + " for " + method + " " + url);
973
1000
  Log.d(TAG, "Error: " + exception.getMessage() + "while trying to insert a network log");
974
1001
  }
975
1002
  }
@@ -1150,7 +1177,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1150
1177
  */
1151
1178
  @ReactMethod
1152
1179
  public void registerFeatureFlagsChangeListener() {
1153
-
1180
+ Log.d(NET_TAG, "[registerFeatureFlagsChangeListener] Registering native feature flags listener");
1154
1181
  MainThreadHandler.runOnMainThread(new Runnable() {
1155
1182
  @Override
1156
1183
  public void run() {
@@ -1158,6 +1185,7 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1158
1185
  InternalCore.INSTANCE._setFeaturesStateListener(new FeaturesStateListener() {
1159
1186
  @Override
1160
1187
  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());
1161
1189
  WritableMap params = Arguments.createMap();
1162
1190
  params.putBoolean("isW3ExternalTraceIDEnabled", featuresState.isW3CExternalTraceIdEnabled());
1163
1191
  params.putBoolean("isW3ExternalGeneratedHeaderEnabled", featuresState.isAttachingGeneratedHeaderEnabled());
@@ -1165,9 +1193,11 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1165
1193
  params.putInt("networkBodyLimit",featuresState.getNetworkLogCharLimit());
1166
1194
 
1167
1195
  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);
1168
1197
  }
1169
1198
  });
1170
1199
  } catch (Exception e) {
1200
+ Log.e(NET_TAG, "[registerFeatureFlagsChangeListener] Failed to register listener", e);
1171
1201
  e.printStackTrace();
1172
1202
  }
1173
1203
 
@@ -1182,13 +1212,16 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1182
1212
  */
1183
1213
  @ReactMethod
1184
1214
  public void isW3ExternalTraceIDEnabled(Promise promise) {
1185
-
1215
+ Log.d(NET_TAG, "[isW3ExternalTraceIDEnabled] Querying native flag");
1186
1216
  MainThreadHandler.runOnMainThread(new Runnable() {
1187
1217
  @Override
1188
1218
  public void run() {
1189
1219
  try {
1190
- promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID));
1220
+ boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_EXTERNAL_TRACE_ID);
1221
+ Log.d(NET_TAG, "[isW3ExternalTraceIDEnabled] Result=" + enabled);
1222
+ promise.resolve(enabled);
1191
1223
  } catch (Exception e) {
1224
+ Log.e(NET_TAG, "[isW3ExternalTraceIDEnabled] Error querying flag", e);
1192
1225
  e.printStackTrace();
1193
1226
  promise.resolve(false);
1194
1227
  }
@@ -1204,13 +1237,16 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1204
1237
  */
1205
1238
  @ReactMethod
1206
1239
  public void isW3ExternalGeneratedHeaderEnabled(Promise promise) {
1207
-
1240
+ Log.d(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Querying native flag");
1208
1241
  MainThreadHandler.runOnMainThread(new Runnable() {
1209
1242
  @Override
1210
1243
  public void run() {
1211
1244
  try {
1212
- promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER));
1245
+ boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_GENERATED_HEADER);
1246
+ Log.d(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Result=" + enabled);
1247
+ promise.resolve(enabled);
1213
1248
  } catch (Exception e) {
1249
+ Log.e(NET_TAG, "[isW3ExternalGeneratedHeaderEnabled] Error querying flag", e);
1214
1250
  e.printStackTrace();
1215
1251
  promise.resolve(false);
1216
1252
  }
@@ -1225,13 +1261,16 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1225
1261
  */
1226
1262
  @ReactMethod
1227
1263
  public void isW3CaughtHeaderEnabled(Promise promise) {
1228
-
1264
+ Log.d(NET_TAG, "[isW3CaughtHeaderEnabled] Querying native flag");
1229
1265
  MainThreadHandler.runOnMainThread(new Runnable() {
1230
1266
  @Override
1231
1267
  public void run() {
1232
1268
  try {
1233
- promise.resolve(InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER));
1269
+ boolean enabled = InternalCore.INSTANCE._isFeatureEnabled(CoreFeature.W3C_ATTACHING_CAPTURED_HEADER);
1270
+ Log.d(NET_TAG, "[isW3CaughtHeaderEnabled] Result=" + enabled);
1271
+ promise.resolve(enabled);
1234
1272
  } catch (Exception e) {
1273
+ Log.e(NET_TAG, "[isW3CaughtHeaderEnabled] Error querying flag", e);
1235
1274
  e.printStackTrace();
1236
1275
  promise.resolve(false);
1237
1276
  }
@@ -1325,13 +1364,16 @@ public class RNLuciqReactnativeModule extends EventEmitterModule {
1325
1364
  */
1326
1365
  @ReactMethod
1327
1366
  public void getNetworkBodyMaxSize(Promise promise) {
1328
-
1367
+ Log.d(NET_TAG, "[getNetworkBodyMaxSize] Querying network body size limit");
1329
1368
  MainThreadHandler.runOnMainThread(new Runnable() {
1330
1369
  @Override
1331
1370
  public void run() {
1332
1371
  try {
1333
- promise.resolve(InternalCore.INSTANCE.get_networkLogCharLimit());
1372
+ Object limit = InternalCore.INSTANCE.get_networkLogCharLimit();
1373
+ Log.d(NET_TAG, "[getNetworkBodyMaxSize] Result=" + limit);
1374
+ promise.resolve(limit);
1334
1375
  } catch (Exception e) {
1376
+ Log.e(NET_TAG, "[getNetworkBodyMaxSize] Error querying limit", e);
1335
1377
  e.printStackTrace();
1336
1378
  promise.resolve(false);
1337
1379
  }
@@ -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,9 @@
1
+ export declare class LuciqStrings {
2
+ static readonly customSpanAPMDisabledMessage: string;
3
+ static readonly customSpanDisabled: string;
4
+ static readonly customSpanSDKNotInitializedMessage: string;
5
+ static readonly customSpanNameEmpty: string;
6
+ static readonly customSpanEndTimeBeforeStartTime: string;
7
+ static readonly customSpanNameTruncated: string;
8
+ static readonly customSpanLimitReached: string;
9
+ }
@@ -0,0 +1,12 @@
1
+ export class LuciqStrings {
2
+ static customSpanAPMDisabledMessage = 'APM is disabled, custom span not created. Please enable APM by following the instructions at this link:\n' +
3
+ 'https://docs.luciq.ai/product-guides-and-integrations/product-guides/application-performance-monitoring';
4
+ static customSpanDisabled = 'Custom span is disabled, custom span not created. Please enable Custom Span by following the instructions at this link:\n' +
5
+ 'https://docs.luciq.ai/product-guides-and-integrations/product-guides/application-performance-monitoring';
6
+ static customSpanSDKNotInitializedMessage = 'Luciq API was called before the SDK is built. To build it, first by following the instructions at this link:\n' +
7
+ 'https://docs.luciq.ai/product-guides-and-integrations/product-guides/application-performance-monitoring';
8
+ static customSpanNameEmpty = 'Custom span name cannot be empty. Please provide a valid name for the custom span.';
9
+ static customSpanEndTimeBeforeStartTime = 'Custom span end time must be after start time. Please provide a valid start and end time for the custom span.';
10
+ static customSpanNameTruncated = 'Custom span name truncated to 150 characters';
11
+ static customSpanLimitReached = 'Maximum number of concurrent custom spans (100) reached. Please end some spans before starting new ones.';
12
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { LuciqConfig } from './models/LuciqConfig';
2
2
  import Report from './models/Report';
3
3
  import type { ThemeConfig } from './models/ThemeConfig';
4
+ import { CustomSpan } from './models/CustomSpan';
4
5
  import * as APM from './modules/APM';
5
6
  import * as BugReporting from './modules/BugReporting';
6
7
  import * as CrashReporting from './modules/CrashReporting';
@@ -15,6 +16,6 @@ import * as Surveys from './modules/Surveys';
15
16
  import * as SessionReplay from './modules/SessionReplay';
16
17
  import type { SessionMetadata } from './models/SessionMetadata';
17
18
  export * from './utils/Enums';
18
- export { Report, APM, BugReporting, CrashReporting, FeatureRequests, NetworkLogger, SessionReplay, Replies, Surveys, ProactiveReportingConfigOptions, createProactiveReportingConfig, };
19
+ export { Report, CustomSpan, APM, BugReporting, CrashReporting, FeatureRequests, NetworkLogger, SessionReplay, Replies, Surveys, ProactiveReportingConfigOptions, createProactiveReportingConfig, };
19
20
  export type { LuciqConfig, Survey, NetworkData, NetworkDataObfuscationHandler, SessionMetadata, ThemeConfig, };
20
21
  export default Luciq;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import Report from './models/Report';
2
+ import { CustomSpan } from './models/CustomSpan';
2
3
  // Modules
3
4
  import * as APM from './modules/APM';
4
5
  import * as BugReporting from './modules/BugReporting';
@@ -11,5 +12,5 @@ import * as Replies from './modules/Replies';
11
12
  import * as Surveys from './modules/Surveys';
12
13
  import * as SessionReplay from './modules/SessionReplay';
13
14
  export * from './utils/Enums';
14
- export { Report, APM, BugReporting, CrashReporting, FeatureRequests, NetworkLogger, SessionReplay, Replies, Surveys, createProactiveReportingConfig, };
15
+ export { Report, CustomSpan, APM, BugReporting, CrashReporting, FeatureRequests, NetworkLogger, SessionReplay, Replies, Surveys, createProactiveReportingConfig, };
15
16
  export default Luciq;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Callback to unregister a span from tracking
3
+ */
4
+ type UnregisterCallback = (span: CustomSpan) => void;
5
+ /**
6
+ * Callback to sync span data to native SDK
7
+ */
8
+ type SyncCallback = (name: string, startTimestamp: number, endTimestamp: number) => Promise<void>;
9
+ /**
10
+ * Represents a custom span for performance tracking.
11
+ * A span measures the duration of an operation and reports it to the native SDK.
12
+ */
13
+ export declare class CustomSpan {
14
+ private name;
15
+ private startTime;
16
+ private startMonotonic;
17
+ private endTime?;
18
+ private duration?;
19
+ private hasEnded;
20
+ private endPromise?;
21
+ private unregisterCallback;
22
+ private syncCallback;
23
+ /**
24
+ * Creates a new custom span. The span starts immediately upon creation.
25
+ * @internal - Use APM.startCustomSpan() instead
26
+ */
27
+ constructor(name: string, unregisterCallback: UnregisterCallback, syncCallback: SyncCallback);
28
+ /**
29
+ * Ends this custom span and reports it to the native SDK.
30
+ * This method is idempotent - calling it multiple times is safe.
31
+ * Subsequent calls will wait for the first call to complete.
32
+ */
33
+ end(): Promise<void>;
34
+ /**
35
+ * Get the span name
36
+ */
37
+ getName(): string;
38
+ /**
39
+ * Check if the span has ended
40
+ */
41
+ isEnded(): boolean;
42
+ /**
43
+ * Get the span duration in milliseconds (only available after end())
44
+ */
45
+ getDuration(): number | undefined;
46
+ }
47
+ export {};
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Represents a custom span for performance tracking.
3
+ * A span measures the duration of an operation and reports it to the native SDK.
4
+ */
5
+ export class CustomSpan {
6
+ name;
7
+ startTime; // Date.now() in milliseconds
8
+ startMonotonic; // performance.now() in milliseconds
9
+ endTime;
10
+ duration;
11
+ hasEnded = false;
12
+ endPromise;
13
+ unregisterCallback;
14
+ syncCallback;
15
+ /**
16
+ * Creates a new custom span. The span starts immediately upon creation.
17
+ * @internal - Use APM.startCustomSpan() instead
18
+ */
19
+ constructor(name, unregisterCallback, syncCallback) {
20
+ this.name = name;
21
+ this.startTime = Date.now();
22
+ this.startMonotonic = performance.now();
23
+ this.unregisterCallback = unregisterCallback;
24
+ this.syncCallback = syncCallback;
25
+ }
26
+ /**
27
+ * Ends this custom span and reports it to the native SDK.
28
+ * This method is idempotent - calling it multiple times is safe.
29
+ * Subsequent calls will wait for the first call to complete.
30
+ */
31
+ async end() {
32
+ // Thread-safe check using Promise-based locking
33
+ if (this.hasEnded) {
34
+ if (this.endPromise) {
35
+ await this.endPromise;
36
+ }
37
+ return;
38
+ }
39
+ // Create lock and mark as ended
40
+ let resolveEnd;
41
+ this.endPromise = new Promise((resolve) => {
42
+ resolveEnd = resolve;
43
+ });
44
+ this.hasEnded = true;
45
+ try {
46
+ // Unregister from active spans
47
+ this.unregisterCallback(this);
48
+ // Calculate duration using monotonic clock
49
+ const endMonotonic = performance.now();
50
+ this.duration = endMonotonic - this.startMonotonic;
51
+ // Calculate end time using wall clock
52
+ this.endTime = this.startTime + this.duration;
53
+ // Convert to microseconds for native SDK
54
+ const startMicros = this.startTime * 1000;
55
+ const endMicros = this.endTime * 1000;
56
+ // Send to native SDK
57
+ await this.syncCallback(this.name, startMicros, endMicros);
58
+ }
59
+ finally {
60
+ // Release lock
61
+ resolveEnd();
62
+ }
63
+ }
64
+ /**
65
+ * Get the span name
66
+ */
67
+ getName() {
68
+ return this.name;
69
+ }
70
+ /**
71
+ * Check if the span has ended
72
+ */
73
+ isEnded() {
74
+ return this.hasEnded;
75
+ }
76
+ /**
77
+ * Get the span duration in milliseconds (only available after end())
78
+ */
79
+ getDuration() {
80
+ return this.duration;
81
+ }
82
+ }
@@ -1,3 +1,4 @@
1
+ import type { CustomSpan } from '../models/CustomSpan';
1
2
  /**
2
3
  * Enables or disables APM
3
4
  * @param isEnabled
@@ -80,3 +81,60 @@ export declare const _lcqSleep: () => void;
80
81
  * @param isEnabled
81
82
  */
82
83
  export declare const setScreenRenderingEnabled: (isEnabled: boolean) => void;
84
+ /**
85
+ * Starts a custom span for performance tracking.
86
+ *
87
+ * A custom span measures the duration of an arbitrary operation that is not
88
+ * automatically tracked by the SDK. The span must be manually ended by calling
89
+ * the `end()` method on the returned span object.
90
+ *
91
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
92
+ * Leading and trailing whitespace will be trimmed.
93
+ *
94
+ * @returns Promise<CustomSpan | null> - The span object to end later, or null if:
95
+ * - Name is empty after trimming
96
+ * - SDK is not initialized
97
+ * - APM is disabled
98
+ * - Custom spans feature is disabled
99
+ * - Maximum concurrent spans limit (100) reached
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const span = await APM.startCustomSpan('Load User Profile');
104
+ * if (span) {
105
+ * try {
106
+ * // ... perform operation ...
107
+ * } finally {
108
+ * await span.end();
109
+ * }
110
+ * }
111
+ * ```
112
+ */
113
+ export declare const startCustomSpan: (name: string) => Promise<CustomSpan | null>;
114
+ /**
115
+ * Records a completed custom span with pre-recorded timestamps.
116
+ *
117
+ * Use this method when you have already recorded the start and end times
118
+ * of an operation and want to report it retroactively.
119
+ *
120
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
121
+ * Leading and trailing whitespace will be trimmed.
122
+ * @param startDate - The start time of the operation
123
+ * @param endDate - The end time of the operation (must be after startDate)
124
+ *
125
+ * @returns Promise<void> - Resolves when the span has been recorded, or logs error if:
126
+ * - Name is empty after trimming
127
+ * - End date is not after start date
128
+ * - SDK is not initialized
129
+ * - APM is disabled
130
+ * - Custom spans feature is disabled
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const start = new Date();
135
+ * // ... operation already completed ...
136
+ * const end = new Date();
137
+ * await APM.addCompletedCustomSpan('Cache Lookup', start, end);
138
+ * ```
139
+ */
140
+ export declare const addCompletedCustomSpan: (name: string, startDate: Date, endDate: Date) => Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { Platform } from 'react-native';
2
2
  import { NativeAPM } from '../native/NativeAPM';
3
3
  import { NativeLuciq } from '../native/NativeLuciq';
4
+ import { startCustomSpan as startCustomSpanInternal, addCompletedCustomSpan as addCompletedCustomSpanInternal, } from '../utils/CustomSpansManager';
4
5
  /**
5
6
  * Enables or disables APM
6
7
  * @param isEnabled
@@ -109,3 +110,64 @@ export const _lcqSleep = () => {
109
110
  export const setScreenRenderingEnabled = (isEnabled) => {
110
111
  NativeAPM.setScreenRenderingEnabled(isEnabled);
111
112
  };
113
+ /**
114
+ * Starts a custom span for performance tracking.
115
+ *
116
+ * A custom span measures the duration of an arbitrary operation that is not
117
+ * automatically tracked by the SDK. The span must be manually ended by calling
118
+ * the `end()` method on the returned span object.
119
+ *
120
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
121
+ * Leading and trailing whitespace will be trimmed.
122
+ *
123
+ * @returns Promise<CustomSpan | null> - The span object to end later, or null if:
124
+ * - Name is empty after trimming
125
+ * - SDK is not initialized
126
+ * - APM is disabled
127
+ * - Custom spans feature is disabled
128
+ * - Maximum concurrent spans limit (100) reached
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const span = await APM.startCustomSpan('Load User Profile');
133
+ * if (span) {
134
+ * try {
135
+ * // ... perform operation ...
136
+ * } finally {
137
+ * await span.end();
138
+ * }
139
+ * }
140
+ * ```
141
+ */
142
+ export const startCustomSpan = async (name) => {
143
+ return startCustomSpanInternal(name);
144
+ };
145
+ /**
146
+ * Records a completed custom span with pre-recorded timestamps.
147
+ *
148
+ * Use this method when you have already recorded the start and end times
149
+ * of an operation and want to report it retroactively.
150
+ *
151
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
152
+ * Leading and trailing whitespace will be trimmed.
153
+ * @param startDate - The start time of the operation
154
+ * @param endDate - The end time of the operation (must be after startDate)
155
+ *
156
+ * @returns Promise<void> - Resolves when the span has been recorded, or logs error if:
157
+ * - Name is empty after trimming
158
+ * - End date is not after start date
159
+ * - SDK is not initialized
160
+ * - APM is disabled
161
+ * - Custom spans feature is disabled
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const start = new Date();
166
+ * // ... operation already completed ...
167
+ * const end = new Date();
168
+ * await APM.addCompletedCustomSpan('Cache Lookup', start, end);
169
+ * ```
170
+ */
171
+ export const addCompletedCustomSpan = async (name, startDate, endDate) => {
172
+ return addCompletedCustomSpanInternal(name, startDate, endDate);
173
+ };