@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.
- package/.claude/agents/codebase-analyzer.md +33 -0
- package/.claude/agents/codebase-locator.md +42 -0
- package/.claude/agents/codebase-pattern-finder.md +40 -0
- package/.claude/commands/apply-pr-reviews.md +253 -0
- package/.claude/commands/create-jira-workitem.md +27 -0
- package/.claude/commands/create-pr.md +138 -0
- package/.claude/commands/create-public-release-notes.md +145 -0
- package/.claude/commands/create-rca.md +286 -0
- package/.claude/commands/debug-sdk.md +66 -0
- package/.claude/commands/describe-pr.md +40 -0
- package/.claude/commands/new-api.md +60 -0
- package/.claude/commands/new-feature.md +75 -0
- package/.claude/commands/pr-review.md +85 -0
- package/.claude/commands/research-codebase.md +41 -0
- package/.claude/commands/review.md +73 -0
- package/.claude/memory/MEMORY.md +1 -0
- package/.claude/memory/feedback_pr_title_format.md +10 -0
- package/.claude/rules/react-native-typescript.md +46 -0
- package/CHANGELOG.md +12 -0
- package/CLAUDE.md +125 -0
- package/android/native.gradle +1 -1
- package/android/src/main/java/ai/luciq/reactlibrary/LuciqScreenLoadingFrameTracker.java +88 -0
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +184 -10
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +5 -3
- package/dist/components/LuciqCaptureScreenLoading.d.ts +8 -0
- package/dist/components/LuciqCaptureScreenLoading.js +154 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/modules/APM.d.ts +19 -0
- package/dist/modules/APM.js +38 -0
- package/dist/modules/Luciq.d.ts +1 -1
- package/dist/modules/Luciq.js +169 -11
- package/dist/modules/apm/ScreenLoadingManager.d.ts +99 -0
- package/dist/modules/apm/ScreenLoadingManager.js +296 -0
- package/dist/native/NativeAPM.d.ts +9 -0
- package/dist/native/NativeLuciq.d.ts +1 -1
- package/dist/utils/LuciqUtils.d.ts +25 -0
- package/dist/utils/LuciqUtils.js +44 -0
- package/dist/utils/RouteMatcher.d.ts +30 -0
- package/dist/utils/RouteMatcher.js +67 -0
- package/ios/RNLuciq/LuciqAPMBridge.m +82 -0
- package/ios/RNLuciq/LuciqReactBridge.m +1 -1
- package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.h +11 -0
- package/ios/RNLuciq/LuciqScreenLoadingFrameTracker.m +121 -0
- package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +14 -0
- package/ios/native.rb +1 -1
- package/package.json +4 -1
- package/scripts/get-github-app-token.sh +70 -0
- package/scripts/notify-github.sh +17 -8
- package/src/components/LuciqCaptureScreenLoading.tsx +210 -0
- package/src/index.ts +4 -0
- package/src/modules/APM.ts +42 -0
- package/src/modules/Luciq.ts +197 -11
- package/src/modules/apm/ScreenLoadingManager.ts +364 -0
- package/src/native/NativeAPM.ts +22 -0
- package/src/native/NativeLuciq.ts +1 -1
- package/src/utils/LuciqUtils.ts +49 -0
- 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(
|
|
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: () => {
|
package/dist/utils/LuciqUtils.js
CHANGED
|
@@ -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
|