@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.
- 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/proguard-rules.txt +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 -19
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +7 -29
- package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +14 -34
- package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +0 -7
- 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 +170 -13
- package/dist/modules/NetworkLogger.d.ts +5 -0
- package/dist/modules/NetworkLogger.js +1 -9
- 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/FeatureFlags.d.ts +0 -6
- package/dist/utils/FeatureFlags.js +0 -35
- package/dist/utils/LuciqUtils.d.ts +25 -0
- package/dist/utils/LuciqUtils.js +44 -6
- package/dist/utils/RouteMatcher.d.ts +30 -0
- package/dist/utils/RouteMatcher.js +67 -0
- package/dist/utils/XhrNetworkInterceptor.js +53 -85
- 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 +5 -1
- 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 +198 -14
- package/src/modules/NetworkLogger.ts +1 -26
- 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/FeatureFlags.ts +0 -44
- package/src/utils/LuciqUtils.ts +49 -15
- package/src/utils/RouteMatcher.ts +83 -0
- package/src/utils/XhrNetworkInterceptor.ts +55 -128
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import LuciqConstants from './LuciqConstants';
|
|
2
2
|
import { stringifyIfNotString, generateW3CHeader } from './LuciqUtils';
|
|
3
|
-
import {
|
|
4
|
-
import { Logger } from './logger';
|
|
5
|
-
const TAG = 'LCQ-RN-NET:';
|
|
3
|
+
import { FeatureFlags } from '../utils/FeatureFlags';
|
|
6
4
|
const XMLHttpRequest = global.XMLHttpRequest;
|
|
7
5
|
let originalXHROpen = XMLHttpRequest.prototype.open;
|
|
8
6
|
let originalXHRSend = XMLHttpRequest.prototype.send;
|
|
@@ -10,34 +8,40 @@ let originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
|
10
8
|
let onProgressCallback;
|
|
11
9
|
let onDoneCallback;
|
|
12
10
|
let isInterceptorEnabled = false;
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
11
|
+
let network;
|
|
12
|
+
const _reset = () => {
|
|
13
|
+
network = {
|
|
14
|
+
id: '',
|
|
15
|
+
url: '',
|
|
16
|
+
method: '',
|
|
17
|
+
requestBody: '',
|
|
18
|
+
requestBodySize: 0,
|
|
19
|
+
responseBody: '',
|
|
20
|
+
responseBodySize: 0,
|
|
21
|
+
responseCode: 0,
|
|
22
|
+
requestHeaders: {},
|
|
23
|
+
responseHeaders: {},
|
|
24
|
+
contentType: '',
|
|
25
|
+
errorDomain: '',
|
|
26
|
+
errorCode: 0,
|
|
27
|
+
startTime: 0,
|
|
28
|
+
duration: 0,
|
|
29
|
+
gqlQueryName: '',
|
|
30
|
+
serverErrorMessage: '',
|
|
31
|
+
requestContentType: '',
|
|
32
|
+
isW3cHeaderFound: null,
|
|
33
|
+
partialId: null,
|
|
34
|
+
networkStartTimeInSeconds: null,
|
|
35
|
+
w3cGeneratedHeader: null,
|
|
36
|
+
w3cCaughtHeader: null,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const getTraceparentHeader = async (networkData) => {
|
|
40
|
+
const [isW3cExternalTraceIDEnabled, isW3cExternalGeneratedHeaderEnabled, isW3cCaughtHeaderEnabled,] = await Promise.all([
|
|
41
|
+
FeatureFlags.isW3ExternalTraceID(),
|
|
42
|
+
FeatureFlags.isW3ExternalGeneratedHeader(),
|
|
43
|
+
FeatureFlags.isW3CaughtHeader(),
|
|
44
|
+
]);
|
|
41
45
|
return injectHeaders(networkData, {
|
|
42
46
|
isW3cExternalTraceIDEnabled,
|
|
43
47
|
isW3cExternalGeneratedHeaderEnabled,
|
|
@@ -81,57 +85,39 @@ export default {
|
|
|
81
85
|
onProgressCallback = callback;
|
|
82
86
|
},
|
|
83
87
|
enableInterception() {
|
|
88
|
+
// Prevents infinite calls to XMLHttpRequest.open when enabling interception multiple times
|
|
84
89
|
if (isInterceptorEnabled) {
|
|
85
|
-
Logger.debug(TAG, 'enableInterception called but already enabled, skipping');
|
|
86
90
|
return;
|
|
87
91
|
}
|
|
88
|
-
Logger.debug(TAG, 'Enabling XHR network interception');
|
|
89
92
|
originalXHROpen = XMLHttpRequest.prototype.open;
|
|
90
93
|
originalXHRSend = XMLHttpRequest.prototype.send;
|
|
91
94
|
originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
92
95
|
// An error code that signifies an issue with the RN client.
|
|
93
96
|
const clientErrorCode = 9876;
|
|
94
97
|
XMLHttpRequest.prototype.open = function (method, url, ...args) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
networkMap.set(this, networkData);
|
|
99
|
-
Logger.debug(TAG, `[open] ${method} ${url}`);
|
|
98
|
+
_reset();
|
|
99
|
+
network.url = url;
|
|
100
|
+
network.method = method;
|
|
100
101
|
originalXHROpen.apply(this, [method, url, ...args]);
|
|
101
102
|
};
|
|
102
103
|
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
|
|
104
|
+
// According to the HTTP RFC, headers are case-insensitive, so we convert
|
|
105
|
+
// them to lower-case to make accessing headers predictable.
|
|
106
|
+
// This avoid issues like failing to get the Content-Type header for a request
|
|
107
|
+
// because the header is set as 'Content-Type' instead of 'content-type'.
|
|
103
108
|
const key = header.toLowerCase();
|
|
104
|
-
|
|
105
|
-
if (networkData) {
|
|
106
|
-
networkData.requestHeaders[key] = stringifyIfNotString(value);
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
Logger.debug(TAG, `[setRequestHeader] No networkData found in WeakMap for header "${key}" — request may have been GC'd or open() was not called`);
|
|
110
|
-
}
|
|
109
|
+
network.requestHeaders[key] = stringifyIfNotString(value);
|
|
111
110
|
originalXHRSetRequestHeader.apply(this, [header, value]);
|
|
112
111
|
};
|
|
113
|
-
XMLHttpRequest.prototype.send = function (data) {
|
|
114
|
-
const
|
|
115
|
-
if (!networkData) {
|
|
116
|
-
Logger.debug(TAG, '[send] No networkData found in WeakMap — falling back to original send (open() was not intercepted)');
|
|
117
|
-
originalXHRSend.apply(this, [data]);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
Logger.debug(TAG, `[send] ${networkData.method} ${networkData.url}`);
|
|
121
|
-
const cloneNetwork = JSON.parse(JSON.stringify(networkData));
|
|
112
|
+
XMLHttpRequest.prototype.send = async function (data) {
|
|
113
|
+
const cloneNetwork = JSON.parse(JSON.stringify(network));
|
|
122
114
|
cloneNetwork.requestBody = data ? data : '';
|
|
123
115
|
if (typeof cloneNetwork.requestBody !== 'string') {
|
|
124
116
|
cloneNetwork.requestBody = JSON.stringify(cloneNetwork.requestBody);
|
|
125
117
|
}
|
|
126
|
-
let isReported = false;
|
|
127
118
|
if (this.addEventListener) {
|
|
128
119
|
this.addEventListener('readystatechange', async () => {
|
|
129
120
|
if (!isInterceptorEnabled) {
|
|
130
|
-
Logger.debug(TAG, `[readystatechange] Interceptor disabled, ignoring state=${this.readyState} for ${cloneNetwork.url}`);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (isReported) {
|
|
134
|
-
Logger.debug(TAG, `[readystatechange] Already reported, ignoring state=${this.readyState} for ${cloneNetwork.url}`);
|
|
135
121
|
return;
|
|
136
122
|
}
|
|
137
123
|
if (this.readyState === this.HEADERS_RECEIVED) {
|
|
@@ -160,7 +146,6 @@ export default {
|
|
|
160
146
|
cloneNetwork.requestContentType =
|
|
161
147
|
cloneNetwork.requestHeaders['content-type'].split(';')[0];
|
|
162
148
|
}
|
|
163
|
-
Logger.debug(TAG, `[readystatechange] HEADERS_RECEIVED for ${cloneNetwork.url}, contentType=${cloneNetwork.contentType}`);
|
|
164
149
|
}
|
|
165
150
|
if (this.readyState === this.DONE) {
|
|
166
151
|
cloneNetwork.duration = Date.now() - cloneNetwork.startTime;
|
|
@@ -181,11 +166,11 @@ export default {
|
|
|
181
166
|
cloneNetwork.requestBody =
|
|
182
167
|
typeof _response === 'string' ? _response : JSON.stringify(_response);
|
|
183
168
|
cloneNetwork.responseBody = '';
|
|
169
|
+
// Detect a more descriptive error message.
|
|
184
170
|
if (typeof _response === 'string' && _response.length > 0) {
|
|
185
171
|
cloneNetwork.errorDomain = _response;
|
|
186
172
|
}
|
|
187
173
|
cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
|
|
188
|
-
Logger.debug(TAG, `[readystatechange] DONE with client error for ${cloneNetwork.url}, errorDomain=${cloneNetwork.errorDomain}`);
|
|
189
174
|
// @ts-ignore
|
|
190
175
|
}
|
|
191
176
|
else if (this._timedOut) {
|
|
@@ -194,7 +179,6 @@ export default {
|
|
|
194
179
|
cloneNetwork.responseCode = 0;
|
|
195
180
|
cloneNetwork.contentType = 'text/plain';
|
|
196
181
|
cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
|
|
197
|
-
Logger.debug(TAG, `[readystatechange] DONE with timeout for ${cloneNetwork.url}`);
|
|
198
182
|
}
|
|
199
183
|
// Only set response body if not already set by error handlers
|
|
200
184
|
if (!cloneNetwork.errorDomain) {
|
|
@@ -241,20 +225,16 @@ export default {
|
|
|
241
225
|
else {
|
|
242
226
|
delete cloneNetwork.gqlQueryName;
|
|
243
227
|
}
|
|
244
|
-
isReported = true;
|
|
245
|
-
Logger.debug(TAG, `[readystatechange] DONE for ${cloneNetwork.method} ${cloneNetwork.url} — status=${cloneNetwork.responseCode}, duration=${cloneNetwork.duration}ms, hasCallback=${!!onDoneCallback}`);
|
|
246
228
|
if (onDoneCallback) {
|
|
247
229
|
onDoneCallback(cloneNetwork);
|
|
248
230
|
}
|
|
249
|
-
else {
|
|
250
|
-
Logger.debug(TAG, `[readystatechange] WARNING: onDoneCallback is null, network log for ${cloneNetwork.url} will be LOST`);
|
|
251
|
-
}
|
|
252
231
|
}
|
|
253
232
|
});
|
|
254
233
|
const downloadUploadProgressCallback = (event) => {
|
|
255
234
|
if (!isInterceptorEnabled) {
|
|
256
235
|
return;
|
|
257
236
|
}
|
|
237
|
+
// check if will be able to compute progress
|
|
258
238
|
if (event.lengthComputable && onProgressCallback) {
|
|
259
239
|
const totalBytesSent = event.loaded;
|
|
260
240
|
const totalBytesExpectedToSend = event.total - event.loaded;
|
|
@@ -263,43 +243,31 @@ export default {
|
|
|
263
243
|
};
|
|
264
244
|
this.addEventListener('progress', downloadUploadProgressCallback);
|
|
265
245
|
this.upload.addEventListener('progress', downloadUploadProgressCallback);
|
|
246
|
+
// Handler for abort events (works with fetch, Axios, and any XHR-based requests)
|
|
266
247
|
this.addEventListener('abort', () => {
|
|
267
248
|
if (!isInterceptorEnabled) {
|
|
268
|
-
Logger.debug(TAG, `[abort] Interceptor disabled, ignoring abort for ${cloneNetwork.url}`);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
if (isReported) {
|
|
272
|
-
Logger.debug(TAG, `[abort] Already reported via readystatechange DONE, ignoring duplicate abort for ${cloneNetwork.url}`);
|
|
273
249
|
return;
|
|
274
250
|
}
|
|
275
|
-
isReported = true;
|
|
276
251
|
cloneNetwork.duration = Date.now() - cloneNetwork.startTime;
|
|
277
252
|
cloneNetwork.responseCode = 0;
|
|
278
253
|
cloneNetwork.errorCode = clientErrorCode;
|
|
279
254
|
cloneNetwork.errorDomain = 'cancelled';
|
|
280
255
|
cloneNetwork.responseBody = `ERROR: ${cloneNetwork.errorDomain}`;
|
|
281
|
-
Logger.debug(TAG, `[abort] Request cancelled: ${cloneNetwork.method} ${cloneNetwork.url}, duration=${cloneNetwork.duration}ms, hasCallback=${!!onDoneCallback}`);
|
|
282
|
-
if (onDoneCallback) {
|
|
283
|
-
onDoneCallback(cloneNetwork);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
Logger.debug(TAG, `[abort] WARNING: onDoneCallback is null, cancelled log for ${cloneNetwork.url} will be LOST`);
|
|
287
|
-
}
|
|
288
256
|
});
|
|
289
257
|
}
|
|
290
258
|
cloneNetwork.startTime = Date.now();
|
|
291
|
-
const traceparent = getTraceparentHeader(cloneNetwork);
|
|
259
|
+
const traceparent = await getTraceparentHeader(cloneNetwork);
|
|
292
260
|
if (traceparent) {
|
|
293
261
|
this.setRequestHeader('Traceparent', traceparent);
|
|
294
|
-
|
|
262
|
+
}
|
|
263
|
+
if (this.readyState === this.UNSENT) {
|
|
264
|
+
return; // Prevent sending the request if not opened
|
|
295
265
|
}
|
|
296
266
|
originalXHRSend.apply(this, [data]);
|
|
297
267
|
};
|
|
298
268
|
isInterceptorEnabled = true;
|
|
299
|
-
Logger.debug(TAG, 'XHR network interception enabled');
|
|
300
269
|
},
|
|
301
270
|
disableInterception() {
|
|
302
|
-
Logger.debug(TAG, 'Disabling XHR network interception');
|
|
303
271
|
isInterceptorEnabled = false;
|
|
304
272
|
XMLHttpRequest.prototype.send = originalXHRSend;
|
|
305
273
|
XMLHttpRequest.prototype.open = originalXHROpen;
|
|
@@ -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
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#import "LuciqScreenLoadingFrameTracker.h"
|
|
2
|
+
#import <QuartzCore/CADisplayLink.h>
|
|
3
|
+
|
|
4
|
+
@interface LuciqScreenLoadingFrameTracker ()
|
|
5
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *spanIdToTimestamp;
|
|
6
|
+
@property (nonatomic, strong) NSMutableSet<NSString *> *activeSpanIds;
|
|
7
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *spanIdToTrackingStart;
|
|
8
|
+
@property (nonatomic, assign) NSInteger maxStorageCapacity;
|
|
9
|
+
@property (nonatomic, assign) BOOL isTracking;
|
|
10
|
+
@property (nonatomic, strong) CADisplayLink *displayLink;
|
|
11
|
+
@end
|
|
12
|
+
|
|
13
|
+
@implementation LuciqScreenLoadingFrameTracker
|
|
14
|
+
|
|
15
|
+
+ (instancetype)sharedInstance {
|
|
16
|
+
static LuciqScreenLoadingFrameTracker *instance = nil;
|
|
17
|
+
static dispatch_once_t onceToken;
|
|
18
|
+
dispatch_once(&onceToken, ^{
|
|
19
|
+
instance = [[self alloc] init];
|
|
20
|
+
});
|
|
21
|
+
return instance;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
- (instancetype)init {
|
|
25
|
+
if (self = [super init]) {
|
|
26
|
+
self.spanIdToTimestamp = [NSMutableDictionary dictionary];
|
|
27
|
+
self.activeSpanIds = [NSMutableSet set];
|
|
28
|
+
self.spanIdToTrackingStart = [NSMutableDictionary dictionary];
|
|
29
|
+
self.maxStorageCapacity = 50;
|
|
30
|
+
self.isTracking = NO;
|
|
31
|
+
}
|
|
32
|
+
return self;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
- (void)initializeFrameTracking {
|
|
36
|
+
if (self.isTracking) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
|
|
41
|
+
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
42
|
+
self.isTracking = YES;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (void)handleDisplayLink:(CADisplayLink *)displayLink {
|
|
46
|
+
[self frameRenderedWithTimestamp:displayLink.timestamp];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (void)frameRenderedWithTimestamp:(NSTimeInterval)timestamp {
|
|
50
|
+
if (self.activeSpanIds.count > 0) {
|
|
51
|
+
// timestamp is monotonic (seconds since boot, from CADisplayLink / mach_absolute_time)
|
|
52
|
+
// Convert to epoch microseconds using the same approach as Android:
|
|
53
|
+
// figure out how long ago the frame was rendered, then subtract from current epoch
|
|
54
|
+
NSTimeInterval currentUptime = [[NSProcessInfo processInfo] systemUptime];
|
|
55
|
+
NSTimeInterval currentEpoch = [[NSDate date] timeIntervalSince1970];
|
|
56
|
+
NSTimeInterval timeSinceFrame = currentUptime - timestamp;
|
|
57
|
+
NSTimeInterval frameEpochSeconds = currentEpoch - timeSinceFrame;
|
|
58
|
+
NSTimeInterval epochTimestampMicroseconds = frameEpochSeconds * 1000000;
|
|
59
|
+
NSNumber *timestampNumber = @(epochTimestampMicroseconds);
|
|
60
|
+
|
|
61
|
+
NSMutableSet<NSString *> *resolvedSpanIds = [NSMutableSet set];
|
|
62
|
+
for (NSString *spanId in self.activeSpanIds) {
|
|
63
|
+
NSNumber *trackingStart = self.spanIdToTrackingStart[spanId];
|
|
64
|
+
if (trackingStart && timestamp < trackingStart.doubleValue) {
|
|
65
|
+
NSLog(@"[ScreenLoading] Skipping frame for span %@ (VSync %.6fs < tracking start %.6fs)", spanId, timestamp, trackingStart.doubleValue);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
self.spanIdToTimestamp[spanId] = timestampNumber;
|
|
69
|
+
[resolvedSpanIds addObject:spanId];
|
|
70
|
+
[self.spanIdToTrackingStart removeObjectForKey:spanId];
|
|
71
|
+
NSLog(@"[ScreenLoading] Frame rendered for span %@ at %.0fμs", spanId, epochTimestampMicroseconds);
|
|
72
|
+
}
|
|
73
|
+
[self.activeSpanIds minusSet:resolvedSpanIds];
|
|
74
|
+
|
|
75
|
+
// Cleanup if exceeding capacity
|
|
76
|
+
if (self.spanIdToTimestamp.count > self.maxStorageCapacity) {
|
|
77
|
+
[self cleanupStorage];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
- (void)startTrackingForSpanId:(NSString *)spanId {
|
|
83
|
+
[self.activeSpanIds addObject:spanId];
|
|
84
|
+
self.spanIdToTrackingStart[spanId] = @([[NSProcessInfo processInfo] systemUptime]);
|
|
85
|
+
NSLog(@"[ScreenLoading] Started tracking for span %@", spanId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
- (NSNumber *)getFrameTimestampForSpanId:(NSString *)spanId {
|
|
89
|
+
NSNumber *timestamp = self.spanIdToTimestamp[spanId];
|
|
90
|
+
if (timestamp) {
|
|
91
|
+
[self.spanIdToTimestamp removeObjectForKey:spanId];
|
|
92
|
+
NSLog(@"[ScreenLoading] Retrieved timestamp %@μs for span %@", timestamp, spanId);
|
|
93
|
+
}
|
|
94
|
+
return timestamp;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
- (void)cleanup {
|
|
98
|
+
if (self.isTracking) {
|
|
99
|
+
[self.displayLink invalidate];
|
|
100
|
+
self.displayLink = nil;
|
|
101
|
+
self.isTracking = NO;
|
|
102
|
+
}
|
|
103
|
+
[self.spanIdToTimestamp removeAllObjects];
|
|
104
|
+
[self.activeSpanIds removeAllObjects];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
- (void)cleanupStorage {
|
|
108
|
+
NSArray *sortedKeys = [self.spanIdToTimestamp keysSortedByValueUsingComparator:^NSComparisonResult(NSNumber *obj1, NSNumber *obj2) {
|
|
109
|
+
return [obj1 compare:obj2];
|
|
110
|
+
}];
|
|
111
|
+
|
|
112
|
+
NSInteger itemsToRemove = self.spanIdToTimestamp.count - 30;
|
|
113
|
+
if (itemsToRemove > 0) {
|
|
114
|
+
for (NSInteger i = 0; i < itemsToRemove; i++) {
|
|
115
|
+
[self.spanIdToTimestamp removeObjectForKey:sortedKeys[i]];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@end
|
|
121
|
+
|
|
@@ -10,7 +10,21 @@
|
|
|
10
10
|
@interface LCQAPM (PrivateAPIs)
|
|
11
11
|
|
|
12
12
|
@property (class, atomic, assign) BOOL networkEnabled;
|
|
13
|
+
@property (class, atomic, readonly) BOOL endScreenLoadingEnabled;
|
|
14
|
+
|
|
15
|
+
typedef void (^LCQDisplayLinkObservationCallback)(NSTimeInterval currentTimestamp, NSTimeInterval targetTimestamp);
|
|
13
16
|
|
|
14
17
|
+ (BOOL)customSpansEnabled;
|
|
15
18
|
|
|
19
|
+
+ (void)endScreenLoadingCPWithEndTimestampMUS:(double)endTimestampMUS;
|
|
20
|
+
+ (void)reportScreenLoadingCPWithStartTimestampMUS:(double)startTimestampMUS
|
|
21
|
+
durationMUS:(double)durationMUS
|
|
22
|
+
stages:(nullable NSDictionary<NSString *, NSNumber *> *)stages;;
|
|
23
|
+
|
|
24
|
+
+ (void)startObservingDisplayLinkWithCallback:(LCQDisplayLinkObservationCallback _Nonnull)callback;
|
|
25
|
+
+ (void)stopObservingDisplayLink;
|
|
26
|
+
+ (void)reportScreenLoadingCPUITraceWithName:(NSString *_Nonnull)name
|
|
27
|
+
screenLoadingStartMUS:(double)screenLoadingStartMUS
|
|
28
|
+
screenLoadingDurationMUS:(double)screenLoadingDurationMUS
|
|
29
|
+
stages:(nullable NSDictionary<NSString *, NSNumber *> *)stages;
|
|
16
30
|
@end
|
package/ios/native.rb
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luciq/react-native",
|
|
3
3
|
"description": "Luciq is the Agentic Observability Platform built for Mobile.",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.6.0",
|
|
5
5
|
"author": "Luciq (https://luciq.ai)",
|
|
6
6
|
"repository": "github:luciqai/luciq-reactnative-sdk",
|
|
7
7
|
"homepage": "https://www.luciq.ai/platforms/react-native",
|
|
@@ -43,17 +43,20 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@apollo/client": "^3.7.0",
|
|
46
|
+
"@instabug/danger-plugin-coverage": "Instabug/danger-plugin-coverage",
|
|
46
47
|
"@react-native-community/eslint-config": "^3.1.0",
|
|
47
48
|
"@react-navigation/native": "^6.1.7",
|
|
48
49
|
"@rollup/plugin-commonjs": "^25.0.3",
|
|
49
50
|
"@rollup/plugin-json": "^6.0.0",
|
|
50
51
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
|
51
52
|
"@rollup/plugin-typescript": "^11.0.0",
|
|
53
|
+
"@testing-library/react-native": "^13.3.3",
|
|
52
54
|
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
|
53
55
|
"@types/jest": "^29.5.3",
|
|
54
56
|
"@types/minimatch": "3.0.4",
|
|
55
57
|
"@types/node": "^20.4.8",
|
|
56
58
|
"@types/react-native": "^0.72.2",
|
|
59
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
57
60
|
"axios": "1.11.0",
|
|
58
61
|
"babel-core": "7.0.0-bridge.0",
|
|
59
62
|
"babel-jest": "^29.6.2",
|
|
@@ -74,6 +77,7 @@
|
|
|
74
77
|
"react-native": "^0.72.3",
|
|
75
78
|
"react-native-navigation": "7.36.0",
|
|
76
79
|
"react-navigation": "^4.4.4",
|
|
80
|
+
"react-test-renderer": "18.3.1",
|
|
77
81
|
"rollup": "^3.27.2",
|
|
78
82
|
"rollup-plugin-cleanup": "^3.2.1",
|
|
79
83
|
"rollup-plugin-copy": "^3.5.0",
|