@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
@@ -0,0 +1,296 @@
1
+ import { NativeAPM } from '../../native/NativeAPM';
2
+ import { Logger } from '../../utils/logger';
3
+ import { fromEpochMicros, nowMicros, toEpochMicros } from '../../utils/LuciqUtils';
4
+ /**
5
+ * Automatic Screen Loading Measurement
6
+ *
7
+ * Start point: `__unsafe_action__` navigation event (below).
8
+ * - Fires when a navigation action is dispatched, before the target screen mounts.
9
+ * - `ScreenLoadingManager.createSpan()` records `nowMicros()` as the span start.
10
+ *
11
+ * End point: `_onNavigationStateChange()` (called from `onStateChange`).
12
+ * - Fires after navigation state has settled and the new screen is mounted.
13
+ * - `ScreenLoadingManager.endSpan()` fetches the native frame timestamp from
14
+ * CADisplayLink (iOS) / Choreographer (Android) to mark actual render completion.
15
+ * - The TTID is: native frame timestamp − span start.
16
+ *
17
+ *
18
+ * Manual Screen Loading Measurement
19
+ *
20
+ * Start point: Component instantiation (lazy init block before first render).
21
+ * - `nowMicros()` is captured as `constructorTimestampRef` and passed to
22
+ * `ScreenLoadingManager.createSpan()` as the span's start timestamp.
23
+ *
24
+ * End point: `useLayoutEffect` (fires synchronously after React commits DOM
25
+ * mutations, before the browser paints).
26
+ * - `ScreenLoadingManager.endSpan()` fetches the native frame timestamp
27
+ * from CADisplayLink (iOS) / Choreographer (Android) to mark the actual
28
+ * render completion. The TTID is: native frame timestamp − span start.
29
+ *
30
+ * Both approaches share the same `endSpan()` path so TTID values are comparable.
31
+ */
32
+ class ScreenLoadingManagerClass {
33
+ activeSpans = new Map();
34
+ isInitialized = false;
35
+ isEnabled = false;
36
+ isEndScreenLoadingEnabled = false;
37
+ isFrameTrackingInitialized = false;
38
+ activeSpanId = null;
39
+ maxConcurrentSpans = 50;
40
+ excludedRoutes = new Set();
41
+ async initialize() {
42
+ if (this.isInitialized) {
43
+ return;
44
+ }
45
+ try {
46
+ // Check feature flags
47
+ this.isEnabled = await NativeAPM.isScreenLoadingEnabled();
48
+ this.isEndScreenLoadingEnabled = await NativeAPM.isEndScreenLoadingEnabled();
49
+ if (this.isEnabled) {
50
+ await NativeAPM.initScreenFrameTracking();
51
+ this.isFrameTrackingInitialized = true;
52
+ Logger.log('[ScreenLoading] Manager initialized, feature enabled');
53
+ }
54
+ else {
55
+ Logger.log('[ScreenLoading] Feature disabled by flag');
56
+ }
57
+ this.isInitialized = true;
58
+ }
59
+ catch (error) {
60
+ Logger.error('[ScreenLoading] Failed to initialize:', error);
61
+ this.isEnabled = false;
62
+ }
63
+ }
64
+ async refreshFlags() {
65
+ try {
66
+ this.isEnabled = await NativeAPM.isScreenLoadingEnabled();
67
+ this.isEndScreenLoadingEnabled = await NativeAPM.isEndScreenLoadingEnabled();
68
+ if (this.isEnabled && !this.isFrameTrackingInitialized) {
69
+ await NativeAPM.initScreenFrameTracking();
70
+ this.isFrameTrackingInitialized = true;
71
+ Logger.log('[ScreenLoading] Frame tracking initialized after flag refresh');
72
+ }
73
+ Logger.log(`[ScreenLoading] Flags refreshed - enabled: ${this.isEnabled}, endScreenLoading: ${this.isEndScreenLoadingEnabled}`);
74
+ }
75
+ catch (error) {
76
+ Logger.error('[ScreenLoading] Failed to refresh flags:', error);
77
+ }
78
+ }
79
+ /**
80
+ * Exclude specific routes from automatic screen loading measurement
81
+ * @param routes Array of route names to exclude
82
+ */
83
+ excludeRoutes(routes) {
84
+ routes.forEach((route) => this.excludedRoutes.add(route));
85
+ Logger.log('[ScreenLoading] Excluded routes:', Array.from(this.excludedRoutes));
86
+ }
87
+ /**
88
+ * Include previously excluded routes back into screen loading measurement
89
+ * @param routes Array of route names to include (or empty to clear all exclusions)
90
+ */
91
+ includeRoutes(routes) {
92
+ if (!routes || routes.length === 0) {
93
+ this.excludedRoutes.clear();
94
+ Logger.log('[ScreenLoading] Cleared all route exclusions');
95
+ }
96
+ else {
97
+ routes.forEach((route) => this.excludedRoutes.delete(route));
98
+ Logger.log('[ScreenLoading] Removed exclusions for:', routes);
99
+ }
100
+ }
101
+ /**
102
+ * Check if a route is excluded from measurement
103
+ */
104
+ isRouteExcluded(routeName) {
105
+ return this.excludedRoutes.has(routeName);
106
+ }
107
+ /**
108
+ * Create a new screen loading span
109
+ * @param screenName Name of the screen
110
+ * @param isManual Whether the span is manual (not automatically created)
111
+ * @param startTimestampParam Optional start timestamp in microseconds (defaults to nowMicros())
112
+ * @returns The created span or null if the feature is not enabled
113
+ */
114
+ createSpan(screenName, isManual = false, startTimestampParam) {
115
+ if (!this.isEnabled) {
116
+ return null;
117
+ }
118
+ // Check if route is excluded (only for automatic tracking)
119
+ if (!isManual && this.isRouteExcluded(screenName)) {
120
+ Logger.log(`[ScreenLoading] Route "${screenName}" is excluded from automatic measurement`);
121
+ return null;
122
+ }
123
+ // Cleanup if exceeding capacity
124
+ if (this.activeSpans.size >= this.maxConcurrentSpans) {
125
+ this.cleanupOldestSpans();
126
+ }
127
+ const spanId = Date.now().toString();
128
+ const startTimestamp = startTimestampParam ?? nowMicros();
129
+ const span = {
130
+ spanId,
131
+ screenName,
132
+ startTimestamp,
133
+ status: 'pending',
134
+ isManual,
135
+ attributes: new Map(),
136
+ };
137
+ this.activeSpans.set(spanId, span);
138
+ // Register with native for frame tracking
139
+ try {
140
+ NativeAPM.setActiveScreenSpanId(spanId);
141
+ }
142
+ catch (error) {
143
+ Logger.error(`[ScreenLoading] Failed to set active span ID ${spanId}:`, error);
144
+ }
145
+ if (!isManual) {
146
+ this.activeSpanId = spanId;
147
+ }
148
+ span.status = 'measuring';
149
+ Logger.log(`[ScreenLoading] Created span ${spanId} for screen "${screenName}" (${isManual ? 'manual' : 'automatic'})`);
150
+ return span;
151
+ }
152
+ /**
153
+ * End a screen loading span
154
+ * @param spanId The ID of the span to end
155
+ */
156
+ async endSpan(spanId) {
157
+ if (!this.isEnabled) {
158
+ return;
159
+ }
160
+ const span = this.activeSpans.get(spanId);
161
+ if (!span || span.status === 'completed') {
162
+ return;
163
+ }
164
+ try {
165
+ // Get frame timestamp from native with retry logic
166
+ // The native frame callback (CADisplayLink/Choreographer) may not have executed yet
167
+ // if endSpan is called very quickly after createSpan. Retry up to 3 times with
168
+ // a delay of ~20ms (slightly more than one frame at 60fps) between attempts.
169
+ const maxRetries = 3;
170
+ const retryDelayMs = 20;
171
+ let frameTimestamp = null;
172
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
173
+ frameTimestamp = await NativeAPM.getScreenTimeToDisplay(spanId);
174
+ if (frameTimestamp) {
175
+ break;
176
+ }
177
+ // Wait for next frame before retrying (only if not last attempt)
178
+ if (attempt < maxRetries - 1) {
179
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
180
+ }
181
+ }
182
+ if (frameTimestamp) {
183
+ // Native returns epoch microseconds; convert to monotonic for consistent internal math
184
+ span.endTimestamp = fromEpochMicros(frameTimestamp);
185
+ span.ttid = span.endTimestamp - span.startTimestamp;
186
+ span.status = 'completed';
187
+ // Log the measurement
188
+ this.logScreenLoading(span);
189
+ }
190
+ else {
191
+ span.status = 'error';
192
+ Logger.warn(`[ScreenLoading] No frame timestamp available for span ${spanId}`);
193
+ }
194
+ }
195
+ catch (error) {
196
+ span.status = 'error';
197
+ Logger.error(`[ScreenLoading] Failed to get timestamp for span ${spanId}:`, error);
198
+ }
199
+ // Cleanup after a delay
200
+ setTimeout(() => {
201
+ this.activeSpans.delete(spanId);
202
+ }, 5000);
203
+ }
204
+ /**
205
+ * Log a screen loading span
206
+ * @param span The span to log
207
+ */
208
+ logScreenLoading(span) {
209
+ // Convert Map to plain object for JSON serialization (JSON.stringify cannot serialize Maps)
210
+ const attributesObject = Object.fromEntries(span.attributes);
211
+ const startEpochUs = Math.round(toEpochMicros(span.startTimestamp));
212
+ const endEpochUs = span.endTimestamp ? Math.round(toEpochMicros(span.endTimestamp)) : undefined;
213
+ const logData = {
214
+ span_id: span.spanId,
215
+ screen_name: span.screenName,
216
+ start_timestamp_us: startEpochUs,
217
+ end_timestamp_us: endEpochUs,
218
+ ttid_us: span.ttid ? Math.round(span.ttid) : undefined,
219
+ ttid_ms: span.ttid ? Math.round(span.ttid) / 1000 : undefined,
220
+ is_manual: span.isManual,
221
+ attributes: attributesObject,
222
+ };
223
+ Logger.log('[ScreenLoading] Measurement:', JSON.stringify(logData, null, 2));
224
+ // Sync screen loading data to native layer (also pass converted object)
225
+ try {
226
+ if (span.isManual) {
227
+ NativeAPM.syncManualScreenLoading(span.screenName, startEpochUs, Math.round(span.ttid), attributesObject);
228
+ }
229
+ else {
230
+ NativeAPM.syncScreenLoading(Number(span.spanId), span.screenName, startEpochUs, Math.round(span.ttid), attributesObject);
231
+ }
232
+ }
233
+ catch (error) {
234
+ Logger.error(`[ScreenLoading] Failed to sync screen loading for span ${span.spanId}:`, error);
235
+ }
236
+ }
237
+ /**
238
+ * End a screen loading span using the current timestamp and active span ID
239
+ */
240
+ endScreenLoading() {
241
+ if (!this.isEndScreenLoadingFeatureEnabled()) {
242
+ Logger.error('[ScreenLoading] End screen loading feature is not enabled');
243
+ return;
244
+ }
245
+ if (!this.activeSpanId) {
246
+ Logger.warn('[ScreenLoading] No active span to end screen loading');
247
+ return;
248
+ }
249
+ try {
250
+ const timeStampMicro = Math.round(toEpochMicros(nowMicros()));
251
+ const uiTraceId = Number(this.activeSpanId);
252
+ NativeAPM.endScreenLoading(timeStampMicro, uiTraceId);
253
+ Logger.log(`[ScreenLoading] endScreenLoading() was called at ${timeStampMicro} for ui trace id "${uiTraceId}"`);
254
+ }
255
+ catch (error) {
256
+ Logger.error('[ScreenLoading] Failed to end screen loading:', error);
257
+ }
258
+ }
259
+ /**
260
+ * Discard a span without logging or syncing it to native.
261
+ * Used when a span should be silently dropped (e.g., excluded route resolved after creation).
262
+ */
263
+ discardSpan(spanId) {
264
+ const span = this.activeSpans.get(spanId);
265
+ if (span) {
266
+ this.activeSpans.delete(spanId);
267
+ Logger.log(`[ScreenLoading] Discarded span ${spanId} for screen "${span.screenName}"`);
268
+ }
269
+ }
270
+ getActiveSpan(spanId) {
271
+ return this.activeSpans.get(spanId);
272
+ }
273
+ getAllActiveSpans() {
274
+ return Array.from(this.activeSpans.values());
275
+ }
276
+ addSpanAttribute(spanId, key, value) {
277
+ const span = this.activeSpans.get(spanId);
278
+ if (span) {
279
+ span.attributes.set(key, value);
280
+ }
281
+ }
282
+ cleanupOldestSpans() {
283
+ const sortedSpans = Array.from(this.activeSpans.entries()).sort((a, b) => a[1].startTimestamp - b[1].startTimestamp);
284
+ const toRemove = Math.max(0, sortedSpans.length - 30);
285
+ for (let i = 0; i < toRemove; i++) {
286
+ this.activeSpans.delete(sortedSpans[i][0]);
287
+ }
288
+ }
289
+ isFeatureEnabled() {
290
+ return this.isEnabled;
291
+ }
292
+ isEndScreenLoadingFeatureEnabled() {
293
+ return this.isEnabled && this.isEndScreenLoadingEnabled;
294
+ }
295
+ }
296
+ export const ScreenLoadingManager = new ScreenLoadingManagerClass();
@@ -17,6 +17,15 @@ export interface ApmNativeModule extends NativeModule {
17
17
  syncCustomSpan(name: string, startTimestamp: number, endTimestamp: number): Promise<void>;
18
18
  isCustomSpanEnabled(): Promise<boolean>;
19
19
  isAPMEnabled(): Promise<boolean>;
20
+ initScreenFrameTracking(): Promise<void>;
21
+ setActiveScreenSpanId(spanId: string): void;
22
+ getScreenTimeToDisplay(spanId: string): Promise<number | null>;
23
+ isScreenLoadingEnabled(): Promise<boolean>;
24
+ isEndScreenLoadingEnabled(): Promise<boolean>;
25
+ endScreenLoading(timeStampMicro: number, uiTraceId: number): void;
26
+ setScreenLoadingEnabled(isEnabled: boolean): void;
27
+ syncScreenLoading(spanId: number, screenName: string, startTimestamp: number, durationUS: number, attributes: Record<string, any>): void;
28
+ syncManualScreenLoading(screenName: string, startTimestamp: number, durationUS: number, attributes: Record<string, any>): void;
20
29
  }
21
30
  export declare const NativeAPM: ApmNativeModule;
22
31
  export declare const emitter: NativeEventEmitter;
@@ -28,7 +28,7 @@ export interface LuciqNativeModule extends NativeModule {
28
28
  setNetworkLogBodyEnabled(isEnabled: boolean): void;
29
29
  setReproStepsConfig(bugMode: ReproStepsMode, crashMode: ReproStepsMode, sessionReplay: ReproStepsMode): void;
30
30
  setTrackUserSteps(isEnabled: boolean): void;
31
- reportScreenChange(firstScreen: string): void;
31
+ reportScreenChange(screenName: string, spanId: string | null): void;
32
32
  reportCurrentViewChange(screenName: string): void;
33
33
  addPrivateView(nativeTag: number | null): void;
34
34
  removePrivateView(nativeTag: number | null): void;
@@ -1,3 +1,9 @@
1
+ export declare function initFeatureFlagsCache(): Promise<void>;
2
+ export declare function getCachedW3cFlags(): {
3
+ isW3cExternalTraceIDEnabled: boolean;
4
+ isW3cExternalGeneratedHeaderEnabled: boolean;
5
+ isW3cCaughtHeaderEnabled: boolean;
6
+ };
1
7
  export declare const FeatureFlags: {
2
8
  isW3ExternalTraceID: () => Promise<boolean>;
3
9
  isW3ExternalGeneratedHeader: () => Promise<boolean>;
@@ -1,5 +1,34 @@
1
1
  import { NativeLuciq } from '../native/NativeLuciq';
2
2
  import { _registerFeatureFlagsChangeListener } from '../modules/Luciq';
3
+ import { Logger } from './logger';
4
+ const TAG = 'LCQ-RN-NET:';
5
+ let cachedW3cFlags = {
6
+ isW3cExternalTraceIDEnabled: false,
7
+ isW3cExternalGeneratedHeaderEnabled: false,
8
+ isW3cCaughtHeaderEnabled: false,
9
+ };
10
+ export async function initFeatureFlagsCache() {
11
+ Logger.debug(TAG, '[FeatureFlags] Initializing W3C feature flags cache from native bridge...');
12
+ try {
13
+ const [traceID, generatedHeader, caughtHeader] = await Promise.all([
14
+ NativeLuciq.isW3ExternalTraceIDEnabled(),
15
+ NativeLuciq.isW3ExternalGeneratedHeaderEnabled(),
16
+ NativeLuciq.isW3CaughtHeaderEnabled(),
17
+ ]);
18
+ cachedW3cFlags = {
19
+ isW3cExternalTraceIDEnabled: traceID,
20
+ isW3cExternalGeneratedHeaderEnabled: generatedHeader,
21
+ isW3cCaughtHeaderEnabled: caughtHeader,
22
+ };
23
+ Logger.debug(TAG, `[FeatureFlags] Cache initialized: traceID=${traceID}, generatedHeader=${generatedHeader}, caughtHeader=${caughtHeader}`);
24
+ }
25
+ catch (e) {
26
+ Logger.debug(TAG, '[FeatureFlags] Failed to initialize cache, using defaults (all false):', e);
27
+ }
28
+ }
29
+ export function getCachedW3cFlags() {
30
+ return cachedW3cFlags;
31
+ }
3
32
  export const FeatureFlags = {
4
33
  isW3ExternalTraceID: () => NativeLuciq.isW3ExternalTraceIDEnabled(),
5
34
  isW3ExternalGeneratedHeader: () => NativeLuciq.isW3ExternalGeneratedHeaderEnabled(),
@@ -8,6 +37,12 @@ export const FeatureFlags = {
8
37
  };
9
38
  export const registerFeatureFlagsListener = () => {
10
39
  _registerFeatureFlagsChangeListener((res) => {
40
+ Logger.debug(TAG, `[FeatureFlags] Flags updated from native listener: traceID=${res.isW3ExternalTraceIDEnabled}, generatedHeader=${res.isW3ExternalGeneratedHeaderEnabled}, caughtHeader=${res.isW3CaughtHeaderEnabled}, bodyLimit=${res.networkBodyLimit}`);
41
+ cachedW3cFlags = {
42
+ isW3cExternalTraceIDEnabled: res.isW3ExternalTraceIDEnabled,
43
+ isW3cExternalGeneratedHeaderEnabled: res.isW3ExternalGeneratedHeaderEnabled,
44
+ isW3cCaughtHeaderEnabled: res.isW3CaughtHeaderEnabled,
45
+ };
11
46
  FeatureFlags.isW3ExternalTraceID = async () => {
12
47
  return res.isW3ExternalTraceIDEnabled;
13
48
  };
@@ -75,6 +75,30 @@ export declare function resetNativeObfuscationListener(): void;
75
75
  * This method is for internal use only.
76
76
  */
77
77
  export declare function updateNetworkLogSnapshot(networkSnapshot: NetworkData): void;
78
+ /**
79
+ * @internal
80
+ * This method is for internal use only.
81
+ *
82
+ * Parses a string value to an integer, returning null if the value is null or cannot be parsed.
83
+ * @param value The string value to parse
84
+ * @returns The parsed integer or null
85
+ */
86
+ export declare function getIntValue(value: string | null): number | null;
87
+ /**
88
+ * Returns a high-resolution monotonic timestamp in microseconds.
89
+ * Use this for all internal duration measurements.
90
+ */
91
+ export declare function nowMicros(): number;
92
+ /**
93
+ * Converts an internal monotonic microsecond timestamp to epoch microseconds.
94
+ * Use this only when reporting to the native layer or external systems.
95
+ */
96
+ export declare function toEpochMicros(monotonicUs: number): number;
97
+ /**
98
+ * Converts an epoch microsecond timestamp to internal monotonic microseconds.
99
+ * Use this when receiving timestamps from the native layer that are epoch-based.
100
+ */
101
+ export declare function fromEpochMicros(epochUs: number): number;
78
102
  declare const _default: {
79
103
  parseErrorStack: (error: ExtendedError) => StackFrame[];
80
104
  captureJsErrors: () => void;
@@ -82,6 +106,7 @@ declare const _default: {
82
106
  getFullRoute: typeof getFullRoute;
83
107
  getStackTrace: (e: ExtendedError) => StackFrame[];
84
108
  stringifyIfNotString: (input: unknown) => string;
109
+ getIntValue: typeof getIntValue;
85
110
  sendCrashReport: typeof sendCrashReport;
86
111
  reportNetworkLog: (network: NetworkData) => void;
87
112
  generateTracePartialId: () => {
@@ -3,6 +3,7 @@ import parseErrorStackLib from 'react-native/Libraries/Core/Devtools/parseErrorS
3
3
  import { NativeCrashReporting } from '../native/NativeCrashReporting';
4
4
  import { NativeLuciq } from '../native/NativeLuciq';
5
5
  import { NativeAPM } from '../native/NativeAPM';
6
+ import { Logger } from './logger';
6
7
  import * as NetworkLogger from '../modules/NetworkLogger';
7
8
  import { NativeNetworkLogger, NativeNetworkLoggerEvent, NetworkListenerType, NetworkLoggerEmitter, } from '../native/NativeNetworkLogger';
8
9
  let apmFlags = {
@@ -181,10 +182,12 @@ export const reportNetworkLog = (network) => {
181
182
  if (Platform.OS === 'android') {
182
183
  const requestHeaders = JSON.stringify(network.requestHeaders);
183
184
  const responseHeaders = JSON.stringify(network.responseHeaders);
185
+ Logger.debug('LCQ-RN-NET:', `[reportNetworkLog] Sending to NativeLuciq.networkLogAndroid: ${network.method} ${network.url}, status=${network.responseCode}, duration=${network.duration}ms, error=${network.errorDomain || 'none'}`);
184
186
  NativeLuciq.networkLogAndroid(network.url, network.requestBody, network.responseBody, network.method, network.responseCode, requestHeaders, responseHeaders, network.duration);
185
187
  if (!apmFlags.isNativeInterceptionFeatureEnabled ||
186
188
  !apmFlags.hasAPMNetworkPlugin ||
187
189
  !apmFlags.shouldEnableNativeInterception) {
190
+ Logger.debug('LCQ-RN-NET:', `[reportNetworkLog] Also sending to NativeAPM.networkLogAndroid (native interception disabled): ${network.method} ${network.url}`);
188
191
  NativeAPM.networkLogAndroid(network.startTime, network.duration, requestHeaders, network.requestBody, network.requestBodySize, network.method, network.url, network.requestContentType, responseHeaders, network.responseBody, network.responseBodySize, network.responseCode, network.contentType, network.errorDomain, {
189
192
  isW3cHeaderFound: network.isW3cHeaderFound,
190
193
  partialId: network.partialId,
@@ -193,6 +196,9 @@ export const reportNetworkLog = (network) => {
193
196
  w3cCaughtHeader: network.w3cCaughtHeader,
194
197
  }, network.gqlQueryName, network.serverErrorMessage);
195
198
  }
199
+ else {
200
+ Logger.debug('LCQ-RN-NET:', `[reportNetworkLog] Skipping NativeAPM.networkLogAndroid (native interception enabled): nativeFeature=${apmFlags.isNativeInterceptionFeatureEnabled}, hasPlugin=${apmFlags.hasAPMNetworkPlugin}, shouldEnable=${apmFlags.shouldEnableNativeInterception}`);
201
+ }
196
202
  }
197
203
  else {
198
204
  NativeLuciq.networkLogIOS(network.url, network.method, network.requestBody, network.requestBodySize, network.responseBody, network.responseBodySize, network.responseCode, network.requestHeaders, network.responseHeaders, network.contentType, network.errorDomain, network.errorCode, network.startTime, network.duration, network.gqlQueryName, network.serverErrorMessage, {
@@ -304,6 +310,49 @@ export function resetNativeObfuscationListener() {
304
310
  export function updateNetworkLogSnapshot(networkSnapshot) {
305
311
  NativeNetworkLogger.updateNetworkLogSnapshot(networkSnapshot.url, networkSnapshot.id, networkSnapshot.requestBody, networkSnapshot.responseBody, networkSnapshot.responseCode ?? 200, networkSnapshot.requestHeaders, networkSnapshot.responseHeaders);
306
312
  }
313
+ /**
314
+ * @internal
315
+ * This method is for internal use only.
316
+ *
317
+ * Parses a string value to an integer, returning null if the value is null or cannot be parsed.
318
+ * @param value The string value to parse
319
+ * @returns The parsed integer or null
320
+ */
321
+ export function getIntValue(value) {
322
+ if (value === null) {
323
+ return null;
324
+ }
325
+ const parsed = parseInt(value, 10);
326
+ return isNaN(parsed) ? null : parsed;
327
+ }
328
+ // One-time anchor captured at module load to convert performance.now() to epoch time.
329
+ // performance.now() gives high-resolution monotonic time (sub-ms precision) but relative
330
+ // to app start. We pair it with Date.now() once, then derive epoch from the offset.
331
+ const perfAnchorMs = performance.now();
332
+ const epochAnchorUs = Date.now() * 1000;
333
+ /**
334
+ * Returns a high-resolution monotonic timestamp in microseconds.
335
+ * Use this for all internal duration measurements.
336
+ */
337
+ export function nowMicros() {
338
+ return performance.now() * 1000;
339
+ }
340
+ /**
341
+ * Converts an internal monotonic microsecond timestamp to epoch microseconds.
342
+ * Use this only when reporting to the native layer or external systems.
343
+ */
344
+ export function toEpochMicros(monotonicUs) {
345
+ const offsetUs = monotonicUs - perfAnchorMs * 1000;
346
+ return epochAnchorUs + offsetUs;
347
+ }
348
+ /**
349
+ * Converts an epoch microsecond timestamp to internal monotonic microseconds.
350
+ * Use this when receiving timestamps from the native layer that are epoch-based.
351
+ */
352
+ export function fromEpochMicros(epochUs) {
353
+ const offsetUs = epochUs - epochAnchorUs;
354
+ return perfAnchorMs * 1000 + offsetUs;
355
+ }
307
356
  export default {
308
357
  parseErrorStack,
309
358
  captureJsErrors,
@@ -311,6 +360,7 @@ export default {
311
360
  getFullRoute,
312
361
  getStackTrace,
313
362
  stringifyIfNotString,
363
+ getIntValue,
314
364
  sendCrashReport,
315
365
  reportNetworkLog,
316
366
  generateTracePartialId,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Matches route path definitions (potentially containing parameters and wildcards)
3
+ * against actual navigation paths.
4
+ *
5
+ * Supports `:param` segments for named parameters and `**` for wildcard matching.
6
+ */
7
+ declare class RouteMatcher {
8
+ private static _instance;
9
+ static get instance(): RouteMatcher;
10
+ /** @internal visible for testing */
11
+ static setInstance(instance: RouteMatcher): void;
12
+ /**
13
+ * Checks whether the given `routePath` definition matches the given `actualPath`.
14
+ *
15
+ * The `routePath` definition can contain parameters in the form of `:param`,
16
+ * or `**` for a wildcard parameter.
17
+ *
18
+ * Returns `true` if the `actualPath` matches the `routePath`, otherwise `false`.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * RouteMatcher.instance.match('/users', '/users'); // true
23
+ * RouteMatcher.instance.match('/user/:id', '/user/123'); // true
24
+ * RouteMatcher.instance.match('/user/**', '/user/123/profile'); // true
25
+ * ```
26
+ */
27
+ match(routePath: string | null, actualPath: string | null): boolean;
28
+ private segmentPath;
29
+ }
30
+ export default RouteMatcher;
@@ -0,0 +1,67 @@
1
+ // TODO: This class is currently unused but will be used later for route matching.
2
+ /**
3
+ * Matches route path definitions (potentially containing parameters and wildcards)
4
+ * against actual navigation paths.
5
+ *
6
+ * Supports `:param` segments for named parameters and `**` for wildcard matching.
7
+ */
8
+ class RouteMatcher {
9
+ static _instance = new RouteMatcher();
10
+ static get instance() {
11
+ return RouteMatcher._instance;
12
+ }
13
+ /** @internal visible for testing */
14
+ static setInstance(instance) {
15
+ RouteMatcher._instance = instance;
16
+ }
17
+ /**
18
+ * Checks whether the given `routePath` definition matches the given `actualPath`.
19
+ *
20
+ * The `routePath` definition can contain parameters in the form of `:param`,
21
+ * or `**` for a wildcard parameter.
22
+ *
23
+ * Returns `true` if the `actualPath` matches the `routePath`, otherwise `false`.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * RouteMatcher.instance.match('/users', '/users'); // true
28
+ * RouteMatcher.instance.match('/user/:id', '/user/123'); // true
29
+ * RouteMatcher.instance.match('/user/**', '/user/123/profile'); // true
30
+ * ```
31
+ */
32
+ match(routePath, actualPath) {
33
+ if (routePath == null || actualPath == null) {
34
+ return routePath === actualPath;
35
+ }
36
+ const routePathSegments = this.segmentPath(routePath);
37
+ const actualPathSegments = this.segmentPath(actualPath);
38
+ const hasWildcard = routePathSegments.includes('**');
39
+ if (routePathSegments.length !== actualPathSegments.length && !hasWildcard) {
40
+ return false;
41
+ }
42
+ for (let i = 0; i < routePathSegments.length; i++) {
43
+ const routeSegment = routePathSegments[i];
44
+ const isWildcard = routeSegment === '**';
45
+ const isParameter = routeSegment.startsWith(':');
46
+ const noMoreActualSegments = i >= actualPathSegments.length;
47
+ if (noMoreActualSegments) {
48
+ return isWildcard;
49
+ }
50
+ if (isParameter) {
51
+ continue;
52
+ }
53
+ if (isWildcard) {
54
+ return true;
55
+ }
56
+ if (routeSegment !== actualPathSegments[i]) {
57
+ return false;
58
+ }
59
+ }
60
+ return true;
61
+ }
62
+ segmentPath(path) {
63
+ const pathWithoutQuery = path.split('?')[0];
64
+ return pathWithoutQuery.split('/').filter((segment) => segment.length > 0);
65
+ }
66
+ }
67
+ export default RouteMatcher;