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

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