@luciq/react-native 19.4.0 → 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 (58) 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/src/main/java/ai/luciq/reactlibrary/LuciqScreenLoadingFrameTracker.java +88 -0
  23. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +184 -10
  24. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +5 -3
  25. package/dist/components/LuciqCaptureScreenLoading.d.ts +8 -0
  26. package/dist/components/LuciqCaptureScreenLoading.js +154 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +2 -0
  29. package/dist/modules/APM.d.ts +19 -0
  30. package/dist/modules/APM.js +38 -0
  31. package/dist/modules/Luciq.d.ts +1 -1
  32. package/dist/modules/Luciq.js +169 -11
  33. package/dist/modules/apm/ScreenLoadingManager.d.ts +99 -0
  34. package/dist/modules/apm/ScreenLoadingManager.js +296 -0
  35. package/dist/native/NativeAPM.d.ts +9 -0
  36. package/dist/native/NativeLuciq.d.ts +1 -1
  37. package/dist/utils/LuciqUtils.d.ts +25 -0
  38. package/dist/utils/LuciqUtils.js +44 -0
  39. package/dist/utils/RouteMatcher.d.ts +30 -0
  40. package/dist/utils/RouteMatcher.js +67 -0
  41. package/ios/RNLuciq/LuciqAPMBridge.m +82 -0
  42. package/ios/RNLuciq/LuciqReactBridge.m +1 -1
  43. package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.h +11 -0
  44. package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.m +121 -0
  45. package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +14 -0
  46. package/ios/native.rb +1 -1
  47. package/package.json +4 -1
  48. package/scripts/get-github-app-token.sh +70 -0
  49. package/scripts/notify-github.sh +17 -8
  50. package/src/components/LuciqCaptureScreenLoading.tsx +210 -0
  51. package/src/index.ts +4 -0
  52. package/src/modules/APM.ts +42 -0
  53. package/src/modules/Luciq.ts +197 -11
  54. package/src/modules/apm/ScreenLoadingManager.ts +364 -0
  55. package/src/native/NativeAPM.ts +22 -0
  56. package/src/native/NativeLuciq.ts +1 -1
  57. package/src/utils/LuciqUtils.ts +49 -0
  58. package/src/utils/RouteMatcher.ts +83 -0
@@ -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;
@@ -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: () => {
@@ -304,6 +304,49 @@ export function resetNativeObfuscationListener() {
304
304
  export function updateNetworkLogSnapshot(networkSnapshot) {
305
305
  NativeNetworkLogger.updateNetworkLogSnapshot(networkSnapshot.url, networkSnapshot.id, networkSnapshot.requestBody, networkSnapshot.responseBody, networkSnapshot.responseCode ?? 200, networkSnapshot.requestHeaders, networkSnapshot.responseHeaders);
306
306
  }
307
+ /**
308
+ * @internal
309
+ * This method is for internal use only.
310
+ *
311
+ * Parses a string value to an integer, returning null if the value is null or cannot be parsed.
312
+ * @param value The string value to parse
313
+ * @returns The parsed integer or null
314
+ */
315
+ export function getIntValue(value) {
316
+ if (value === null) {
317
+ return null;
318
+ }
319
+ const parsed = parseInt(value, 10);
320
+ return isNaN(parsed) ? null : parsed;
321
+ }
322
+ // One-time anchor captured at module load to convert performance.now() to epoch time.
323
+ // performance.now() gives high-resolution monotonic time (sub-ms precision) but relative
324
+ // to app start. We pair it with Date.now() once, then derive epoch from the offset.
325
+ const perfAnchorMs = performance.now();
326
+ const epochAnchorUs = Date.now() * 1000;
327
+ /**
328
+ * Returns a high-resolution monotonic timestamp in microseconds.
329
+ * Use this for all internal duration measurements.
330
+ */
331
+ export function nowMicros() {
332
+ return performance.now() * 1000;
333
+ }
334
+ /**
335
+ * Converts an internal monotonic microsecond timestamp to epoch microseconds.
336
+ * Use this only when reporting to the native layer or external systems.
337
+ */
338
+ export function toEpochMicros(monotonicUs) {
339
+ const offsetUs = monotonicUs - perfAnchorMs * 1000;
340
+ return epochAnchorUs + offsetUs;
341
+ }
342
+ /**
343
+ * Converts an epoch microsecond timestamp to internal monotonic microseconds.
344
+ * Use this when receiving timestamps from the native layer that are epoch-based.
345
+ */
346
+ export function fromEpochMicros(epochUs) {
347
+ const offsetUs = epochUs - epochAnchorUs;
348
+ return perfAnchorMs * 1000 + offsetUs;
349
+ }
307
350
  export default {
308
351
  parseErrorStack,
309
352
  captureJsErrors,
@@ -311,6 +354,7 @@ export default {
311
354
  getFullRoute,
312
355
  getStackTrace,
313
356
  stringifyIfNotString,
357
+ getIntValue,
314
358
  sendCrashReport,
315
359
  reportNetworkLog,
316
360
  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;
@@ -8,6 +8,7 @@
8
8
  #import <LuciqSDK/LCQTypes.h>
9
9
  #import <React/RCTUIManager.h>
10
10
  #import "Util/LCQAPM+PrivateAPIs.h"
11
+ #import "LuciqScreenLoadingFrameTracker.h"
11
12
 
12
13
  @implementation LuciqAPMBridge
13
14
 
@@ -145,7 +146,88 @@ RCT_EXPORT_METHOD(isAPMEnabled:(RCTPromiseResolveBlock)resolve
145
146
  }
146
147
  }
147
148
 
149
+ // Screen Loading methods
150
+ RCT_EXPORT_METHOD(initScreenFrameTracking:(RCTPromiseResolveBlock)resolve
151
+ rejecter:(RCTPromiseRejectBlock)reject)
152
+ {
153
+ dispatch_async(dispatch_get_main_queue(), ^{
154
+ [[LuciqScreenLoadingFrameTracker sharedInstance] initializeFrameTracking];
155
+ resolve(nil);
156
+ });
157
+ }
158
+
159
+ RCT_EXPORT_METHOD(setActiveScreenSpanId:(NSString *)spanId)
160
+ {
161
+ dispatch_async(dispatch_get_main_queue(), ^{
162
+ [[LuciqScreenLoadingFrameTracker sharedInstance] startTrackingForSpanId:spanId];
163
+ });
164
+ }
165
+
166
+ RCT_EXPORT_METHOD(getScreenTimeToDisplay:(NSString *)spanId
167
+ resolver:(RCTPromiseResolveBlock)resolve
168
+ rejecter:(RCTPromiseRejectBlock)reject)
169
+ {
170
+ dispatch_async(dispatch_get_main_queue(), ^{
171
+ NSNumber *timestamp = [[LuciqScreenLoadingFrameTracker sharedInstance] getFrameTimestampForSpanId:spanId];
172
+ resolve(timestamp);
173
+ });
174
+ }
175
+
176
+ RCT_EXPORT_METHOD(isScreenLoadingEnabled:(RCTPromiseResolveBlock)resolve
177
+ rejecter:(RCTPromiseRejectBlock)reject){
148
178
 
179
+ BOOL isScreenLoadingEnabled = LCQAPM.screenLoadingEnabled;
180
+ resolve(@(isScreenLoadingEnabled));
181
+ }
182
+
183
+ RCT_EXPORT_METHOD(isEndScreenLoadingEnabled:(RCTPromiseResolveBlock)resolve
184
+ rejecter:(RCTPromiseRejectBlock)reject){
185
+
186
+ BOOL isEndScreenLoadingEnabled = LCQAPM.endScreenLoadingEnabled;
187
+ resolve(@(isEndScreenLoadingEnabled));
188
+ }
189
+
190
+ // uiTraceId is unused on iOS but required to keep the React Native Bridge call
191
+ // signature consistent with Android, which uses it.
192
+ RCT_EXPORT_METHOD(endScreenLoading:(double)timeStampMicro
193
+ uiTraceId:(double)uiTraceId){
194
+ [LCQAPM endScreenLoadingCPWithEndTimestampMUS:timeStampMicro];
195
+ }
196
+
197
+ RCT_EXPORT_METHOD(setScreenLoadingEnabled:(BOOL)isEnabled){
198
+ LCQAPM.screenLoadingEnabled = isEnabled;
199
+ }
200
+
201
+ - (NSMutableDictionary<NSString *, NSNumber *> *)buildStagesMapFromAttributes:(NSDictionary *)stages {
202
+ NSMutableDictionary<NSString *, NSNumber *> *stagesMap = [NSMutableDictionary dictionary];
203
+ NSArray<NSString *> *keys = @[@"cnst_mus_st" , @"cnst_mus",@"rnd_mus_st", @"rnd_mus", @"mnt_mus_st" ,@"mnt_mus", @"lyt_mus_st" , @"lyt_mus"];
204
+ for (NSString *key in keys) {
205
+ if (stages[key])
206
+ stagesMap[key] = @([stages[key] longLongValue]);
207
+ }
208
+ return stagesMap;
209
+ }
210
+
211
+ // Syncs screen loading data to native layer for reporting
212
+ RCT_EXPORT_METHOD(syncScreenLoading:(double)spanId
213
+ screenName:(NSString *)screenName
214
+ startTimestamp:(double)startTimestamp
215
+ ttid_us:(double)ttid_us
216
+ attributes:(NSDictionary *)stages){
217
+
218
+ NSMutableDictionary<NSString *, NSNumber *> *stagesMap = [self buildStagesMapFromAttributes:stages];
219
+ [LCQAPM reportScreenLoadingCPWithStartTimestampMUS:startTimestamp durationMUS:ttid_us stages:stagesMap];
220
+ }
221
+
222
+ // Syncs manual screen loading measurements to native layer for reporting (no span ID)
223
+ RCT_EXPORT_METHOD(syncManualScreenLoading:(NSString *)screenName
224
+ startTimestamp:(double)startTimestamp
225
+ ttid_mus:(double)ttid_mus
226
+ attributes:(NSDictionary *)stages){
227
+
228
+ NSMutableDictionary<NSString *, NSNumber *> *stagesMap = [self buildStagesMapFromAttributes:stages];
229
+ [LCQAPM reportScreenLoadingCPUITraceWithName:screenName screenLoadingStartMUS:startTimestamp screenLoadingDurationMUS:ttid_mus stages:stagesMap];
230
+ }
149
231
 
150
232
  @synthesize description;
151
233
 
@@ -453,7 +453,7 @@ RCT_EXPORT_METHOD(show) {
453
453
  [[NSRunLoop mainRunLoop] performSelector:@selector(show) target:[Luciq class] argument:nil order:0 modes:@[NSDefaultRunLoopMode]];
454
454
  }
455
455
 
456
- RCT_EXPORT_METHOD(reportScreenChange:(NSString *)screenName) {
456
+ RCT_EXPORT_METHOD(reportScreenChange:(NSString *)screenName spanId:(NSString * _Nullable)spanId) {
457
457
  SEL setPrivateApiSEL = NSSelectorFromString(@"logViewDidAppearEvent:");
458
458
  if ([[Luciq class] respondsToSelector:setPrivateApiSEL]) {
459
459
  NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[Luciq class] methodSignatureForSelector:setPrivateApiSEL]];
@@ -0,0 +1,11 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ @interface LuciqScreenLoadingFrameTracker : NSObject
4
+
5
+ + (instancetype)sharedInstance;
6
+ - (void)startTrackingForSpanId:(NSString *)spanId;
7
+ - (NSNumber *)getFrameTimestampForSpanId:(NSString *)spanId;
8
+ - (void)initializeFrameTracking;
9
+ - (void)cleanup;
10
+
11
+ @end