@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,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",
|
|
@@ -50,11 +50,13 @@
|
|
|
50
50
|
"@rollup/plugin-json": "^6.0.0",
|
|
51
51
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
|
52
52
|
"@rollup/plugin-typescript": "^11.0.0",
|
|
53
|
+
"@testing-library/react-native": "^13.3.3",
|
|
53
54
|
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
|
54
55
|
"@types/jest": "^29.5.3",
|
|
55
56
|
"@types/minimatch": "3.0.4",
|
|
56
57
|
"@types/node": "^20.4.8",
|
|
57
58
|
"@types/react-native": "^0.72.2",
|
|
59
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
58
60
|
"axios": "1.11.0",
|
|
59
61
|
"babel-core": "7.0.0-bridge.0",
|
|
60
62
|
"babel-jest": "^29.6.2",
|
|
@@ -75,6 +77,7 @@
|
|
|
75
77
|
"react-native": "^0.72.3",
|
|
76
78
|
"react-native-navigation": "7.36.0",
|
|
77
79
|
"react-navigation": "^4.4.4",
|
|
80
|
+
"react-test-renderer": "18.3.1",
|
|
78
81
|
"rollup": "^3.27.2",
|
|
79
82
|
"rollup-plugin-cleanup": "^3.2.1",
|
|
80
83
|
"rollup-plugin-copy": "^3.5.0",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Generates a GitHub App installation token using openssl + curl.
|
|
4
|
+
# No external dependencies required.
|
|
5
|
+
#
|
|
6
|
+
# Usage: bash get-github-app-token.sh <APP_ID_ENV> <PRIVATE_KEY_ENV> <INSTALLATION_ID_ENV>
|
|
7
|
+
# Example: bash get-github-app-token.sh AND_LUCIQ_APP_ID AND_LUCIQ_PRIVATE_KEY AND_LUCIQ_INSTALLATION_ID
|
|
8
|
+
# Example: bash get-github-app-token.sh AND_INSTABUG_APP_ID AND_INSTABUG_PRIVATE_KEY AND_INSTABUG_INSTALLATION_ID
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
APP_ID_ENV="${1:?Usage: $0 <APP_ID_ENV> <PRIVATE_KEY_ENV> <INSTALLATION_ID_ENV>}"
|
|
13
|
+
PRIVATE_KEY_ENV="${2:?Usage: $0 <APP_ID_ENV> <PRIVATE_KEY_ENV> <INSTALLATION_ID_ENV>}"
|
|
14
|
+
INSTALL_ID_ENV="${3:?Usage: $0 <APP_ID_ENV> <PRIVATE_KEY_ENV> <INSTALLATION_ID_ENV>}"
|
|
15
|
+
|
|
16
|
+
APP_ID="${!APP_ID_ENV:?Error: $APP_ID_ENV is not set}"
|
|
17
|
+
PRIVATE_KEY="${!PRIVATE_KEY_ENV:?Error: $PRIVATE_KEY_ENV is not set}"
|
|
18
|
+
INSTALL_ID="${!INSTALL_ID_ENV:?Error: $INSTALL_ID_ENV is not set}"
|
|
19
|
+
|
|
20
|
+
# Reconstruct PEM file from flattened env var
|
|
21
|
+
# CircleCI flattens multiline env vars into a single line,
|
|
22
|
+
# so we extract header/footer and re-wrap the base64 body at 64 chars
|
|
23
|
+
PEM_FILE=$(mktemp)
|
|
24
|
+
chmod 600 "$PEM_FILE"
|
|
25
|
+
trap 'rm -f "$PEM_FILE"' EXIT
|
|
26
|
+
|
|
27
|
+
BODY=$(printf '%s' "$PRIVATE_KEY" | sed 's/-----BEGIN RSA PRIVATE KEY-----//;s/-----END RSA PRIVATE KEY-----//;s/ //g')
|
|
28
|
+
{
|
|
29
|
+
echo "-----BEGIN RSA PRIVATE KEY-----"
|
|
30
|
+
echo "$BODY" | fold -w 64
|
|
31
|
+
echo "-----END RSA PRIVATE KEY-----"
|
|
32
|
+
} > "$PEM_FILE"
|
|
33
|
+
|
|
34
|
+
# Base64url encode (RFC 4648): replace +/ with -_, strip =
|
|
35
|
+
b64url() {
|
|
36
|
+
openssl base64 -A | tr '+/' '-_' | tr -d '='
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
NOW=$(date +%s)
|
|
40
|
+
IAT=$((NOW - 60)) # 60s clock skew allowance per GitHub docs
|
|
41
|
+
EXP=$((NOW + 600)) # 10min max JWT lifetime per GitHub docs
|
|
42
|
+
|
|
43
|
+
# Create JWT header and payload
|
|
44
|
+
HEADER=$(printf '{"alg":"RS256","typ":"JWT"}' | b64url)
|
|
45
|
+
PAYLOAD=$(printf '{"iat":%d,"exp":%d,"iss":"%s"}' "$IAT" "$EXP" "$APP_ID" | b64url)
|
|
46
|
+
|
|
47
|
+
# Sign with RSA-SHA256
|
|
48
|
+
SIGNATURE=$(printf '%s.%s' "$HEADER" "$PAYLOAD" | openssl dgst -sha256 -sign "$PEM_FILE" -binary | b64url)
|
|
49
|
+
|
|
50
|
+
JWT_TOKEN="${HEADER}.${PAYLOAD}.${SIGNATURE}"
|
|
51
|
+
|
|
52
|
+
# Exchange JWT for installation token
|
|
53
|
+
RESPONSE=$(curl -sf -X POST \
|
|
54
|
+
-H "Authorization: Bearer $JWT_TOKEN" \
|
|
55
|
+
-H "Accept: application/vnd.github+json" \
|
|
56
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
57
|
+
"https://api.github.com/app/installations/${INSTALL_ID}/access_tokens") || {
|
|
58
|
+
echo "Error: GitHub API request failed (HTTP error)" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty')
|
|
63
|
+
|
|
64
|
+
if [ -z "$TOKEN" ]; then
|
|
65
|
+
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message // "unknown error"')
|
|
66
|
+
echo "Error: Failed to get installation token: $ERROR_MSG" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "$TOKEN"
|
package/scripts/notify-github.sh
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
3
5
|
pr_url="https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls?head=$CIRCLE_PROJECT_USERNAME:$CIRCLE_BRANCH&state=open"
|
|
4
6
|
pr_response=$(curl --location --request GET "$pr_url" --header "Authorization: Bearer $RELEASE_GITHUB_TOKEN")
|
|
5
7
|
|
|
6
|
-
if
|
|
7
|
-
echo "
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
if ! echo "$pr_response" | jq -e 'type == "array"' >/dev/null; then
|
|
9
|
+
echo "Unexpected GitHub API response (not an array):"
|
|
10
|
+
echo "$pr_response"
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
--data-raw "$1"
|
|
14
|
+
if [ "$(echo "$pr_response" | jq length)" -eq 0 ]; then
|
|
15
|
+
echo "No PR found to update"
|
|
16
|
+
exit 0
|
|
15
17
|
fi
|
|
18
|
+
|
|
19
|
+
pr_comment_url=$(echo "$pr_response" | jq -r ".[]._links.comments.href")
|
|
20
|
+
|
|
21
|
+
curl --location --request POST "$pr_comment_url" \
|
|
22
|
+
--header 'Content-Type: application/json' \
|
|
23
|
+
--header "Authorization: Bearer $RELEASE_GITHUB_TOKEN" \
|
|
24
|
+
--data-raw "$1"
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useLayoutEffect, useContext } from 'react';
|
|
2
|
+
import { View, ViewProps } from 'react-native';
|
|
3
|
+
import { ScreenLoadingManager } from '../modules/apm/ScreenLoadingManager';
|
|
4
|
+
import { Logger } from '../utils/logger';
|
|
5
|
+
import { nowMicros, toEpochMicros } from '../utils/LuciqUtils';
|
|
6
|
+
|
|
7
|
+
// Context to handle nested components
|
|
8
|
+
const ScreenLoadingContext = React.createContext<boolean>(false);
|
|
9
|
+
|
|
10
|
+
export interface LuciqScreenLoadingProps extends ViewProps {
|
|
11
|
+
screenName: string;
|
|
12
|
+
record?: boolean;
|
|
13
|
+
onMeasured?: (ttid: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function LuciqCaptureScreenLoading(props: LuciqScreenLoadingProps) {
|
|
17
|
+
const { screenName, record, onMeasured, onLayout, children, ...viewProps } = props;
|
|
18
|
+
|
|
19
|
+
const isNested = useContext(ScreenLoadingContext);
|
|
20
|
+
|
|
21
|
+
// Refs for timestamps (these don't need to trigger re-renders)
|
|
22
|
+
const constructorTimestampRef = useRef<number>(nowMicros()); // microseconds
|
|
23
|
+
const renderStartTimestampRef = useRef<number | undefined>(undefined);
|
|
24
|
+
const renderEndTimestampRef = useRef<number | undefined>(undefined);
|
|
25
|
+
const mountTimestampRef = useRef<number | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
// Guards to ensure single execution
|
|
28
|
+
const initializedRef = useRef(false);
|
|
29
|
+
const hasFirstRenderCompletedRef = useRef(false);
|
|
30
|
+
const attributesRecordedRef = useRef(false);
|
|
31
|
+
const initialSpanIdRef = useRef<string | null>(null);
|
|
32
|
+
|
|
33
|
+
// Capture render start timestamp ONLY on first render
|
|
34
|
+
if (!hasFirstRenderCompletedRef.current) {
|
|
35
|
+
renderStartTimestampRef.current = nowMicros();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Initialize span - runs once like constructor (lazy initialization)
|
|
39
|
+
if (!initializedRef.current) {
|
|
40
|
+
initializedRef.current = true;
|
|
41
|
+
// Initialize span if conditions are met
|
|
42
|
+
try {
|
|
43
|
+
if (record !== false && ScreenLoadingManager.isFeatureEnabled()) {
|
|
44
|
+
const span = ScreenLoadingManager.createSpan(
|
|
45
|
+
screenName,
|
|
46
|
+
true,
|
|
47
|
+
constructorTimestampRef.current,
|
|
48
|
+
);
|
|
49
|
+
if (span) {
|
|
50
|
+
initialSpanIdRef.current = span.spanId;
|
|
51
|
+
Logger.log(`[LuciqScreenLoading] Span ${span.spanId} created in constructor`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
Logger.error('[LuciqScreenLoading] Failed to create span:', error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [spanId, setSpanId] = useState<string | null>(initialSpanIdRef.current);
|
|
60
|
+
const [isMeasured, setIsMeasured] = useState(false);
|
|
61
|
+
|
|
62
|
+
// Ref to avoid stale closure in useLayoutEffect
|
|
63
|
+
const onMeasuredRef = useRef(onMeasured);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
onMeasuredRef.current = onMeasured;
|
|
66
|
+
}, [onMeasured]);
|
|
67
|
+
|
|
68
|
+
// Refs to track latest values for cleanup (componentWillUnmount)
|
|
69
|
+
const spanIdRef = useRef<string | null>(spanId);
|
|
70
|
+
const isMeasuredRef = useRef(isMeasured);
|
|
71
|
+
|
|
72
|
+
// Keep refs in sync with state
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
spanIdRef.current = spanId;
|
|
75
|
+
}, [spanId]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
isMeasuredRef.current = isMeasured;
|
|
79
|
+
}, [isMeasured]);
|
|
80
|
+
|
|
81
|
+
// Handle nested component detection
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
// Check if we're nested and should ignore this component
|
|
84
|
+
if (isNested && initialSpanIdRef.current) {
|
|
85
|
+
Logger.log(
|
|
86
|
+
`[LuciqScreenLoading] Nested component detected, ignoring span ${initialSpanIdRef.current}`,
|
|
87
|
+
);
|
|
88
|
+
// Cancel the span
|
|
89
|
+
setSpanId(null);
|
|
90
|
+
}
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
+
}, []); // Empty deps = componentDidMount
|
|
93
|
+
|
|
94
|
+
// Record lifecycle timestamps after first render completes (synchronous)
|
|
95
|
+
// useLayoutEffect fires synchronously after DOM mutations but before browser paint
|
|
96
|
+
useLayoutEffect(() => {
|
|
97
|
+
// Skip if no span, already recorded, or nested
|
|
98
|
+
if (!spanId || attributesRecordedRef.current || isNested) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// endSpan is async (native frame timestamp fetch), fire-and-forget from useLayoutEffect
|
|
103
|
+
ScreenLoadingManager.endSpan(spanId)
|
|
104
|
+
.then(() => {
|
|
105
|
+
const completedSpan = ScreenLoadingManager.getActiveSpan(spanId);
|
|
106
|
+
if (completedSpan?.ttid && onMeasuredRef.current) {
|
|
107
|
+
onMeasuredRef.current(completedSpan.ttid / 1000);
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.catch((error) => {
|
|
111
|
+
Logger.warn('[LuciqScreenLoading] Failed to end span:', error);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
attributesRecordedRef.current = true;
|
|
115
|
+
mountTimestampRef.current = nowMicros();
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Record all timestamps
|
|
119
|
+
ScreenLoadingManager.addSpanAttribute(
|
|
120
|
+
spanId,
|
|
121
|
+
'cnst_mus_st',
|
|
122
|
+
toEpochMicros(constructorTimestampRef.current),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (renderStartTimestampRef.current) {
|
|
126
|
+
ScreenLoadingManager.addSpanAttribute(
|
|
127
|
+
spanId,
|
|
128
|
+
'rnd_mus_st',
|
|
129
|
+
toEpochMicros(renderStartTimestampRef.current),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ScreenLoadingManager.addSpanAttribute(
|
|
134
|
+
spanId,
|
|
135
|
+
'mnt_mus_st',
|
|
136
|
+
toEpochMicros(mountTimestampRef.current),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Record all durations
|
|
140
|
+
if (renderStartTimestampRef.current) {
|
|
141
|
+
// Constructor duration: time from component init to first render start
|
|
142
|
+
const constructorDuration =
|
|
143
|
+
renderStartTimestampRef.current - constructorTimestampRef.current;
|
|
144
|
+
ScreenLoadingManager.addSpanAttribute(spanId, 'cnst_mus', constructorDuration);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (renderEndTimestampRef.current && renderStartTimestampRef.current) {
|
|
148
|
+
// Render duration: time spent creating JSX
|
|
149
|
+
const renderDuration = renderEndTimestampRef.current - renderStartTimestampRef.current;
|
|
150
|
+
ScreenLoadingManager.addSpanAttribute(spanId, 'rnd_mus', renderDuration);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (mountTimestampRef.current && renderEndTimestampRef.current) {
|
|
154
|
+
// Mount duration: time from render complete to effect execution
|
|
155
|
+
const mountDuration = mountTimestampRef.current - renderEndTimestampRef.current;
|
|
156
|
+
ScreenLoadingManager.addSpanAttribute(spanId, 'mnt_mus', mountDuration);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
Logger.log(`[LuciqScreenLoading] Lifecycle measurements for span ${spanId}:`, {
|
|
160
|
+
constructor_us: renderStartTimestampRef.current
|
|
161
|
+
? renderStartTimestampRef.current - constructorTimestampRef.current
|
|
162
|
+
: undefined,
|
|
163
|
+
render_us:
|
|
164
|
+
renderEndTimestampRef.current && renderStartTimestampRef.current
|
|
165
|
+
? renderEndTimestampRef.current - renderStartTimestampRef.current
|
|
166
|
+
: undefined,
|
|
167
|
+
mount_us:
|
|
168
|
+
mountTimestampRef.current && renderEndTimestampRef.current
|
|
169
|
+
? mountTimestampRef.current - renderEndTimestampRef.current
|
|
170
|
+
: undefined,
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
Logger.error(`[LuciqScreenLoading] Failed to record attributes for span ${spanId}:`, error);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// End the span — mark as measured synchronously to guard against unmount race
|
|
177
|
+
setIsMeasured(true);
|
|
178
|
+
|
|
179
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
180
|
+
}, [spanId]); // Run when spanId is set
|
|
181
|
+
|
|
182
|
+
// componentWillUnmount equivalent
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
return () => {
|
|
185
|
+
// Cleanup on unmount if not measured
|
|
186
|
+
if (spanIdRef.current && !isMeasuredRef.current) {
|
|
187
|
+
ScreenLoadingManager.endSpan(spanIdRef.current).catch((error) => {
|
|
188
|
+
Logger.warn('[LuciqScreenLoading] Failed to end span on unmount:', error);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}, []); // Empty deps = only runs cleanup on unmount
|
|
193
|
+
|
|
194
|
+
// Create the JSX result
|
|
195
|
+
const result = (
|
|
196
|
+
<ScreenLoadingContext.Provider value={spanId !== null}>
|
|
197
|
+
<View {...viewProps} onLayout={onLayout}>
|
|
198
|
+
{children}
|
|
199
|
+
</View>
|
|
200
|
+
</ScreenLoadingContext.Provider>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Capture render end timestamp ONLY on first render (after JSX creation)
|
|
204
|
+
if (!hasFirstRenderCompletedRef.current) {
|
|
205
|
+
renderEndTimestampRef.current = nowMicros();
|
|
206
|
+
hasFirstRenderCompletedRef.current = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -45,4 +45,8 @@ export type {
|
|
|
45
45
|
ThemeConfig,
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
// Screen Loading Component
|
|
49
|
+
export { LuciqCaptureScreenLoading } from './components/LuciqCaptureScreenLoading';
|
|
50
|
+
export type { LuciqScreenLoadingProps } from './components/LuciqCaptureScreenLoading';
|
|
51
|
+
|
|
48
52
|
export default Luciq;
|
package/src/modules/APM.ts
CHANGED
|
@@ -7,6 +7,13 @@ import {
|
|
|
7
7
|
addCompletedCustomSpan as addCompletedCustomSpanInternal,
|
|
8
8
|
} from '../utils/CustomSpansManager';
|
|
9
9
|
import type { CustomSpan } from '../models/CustomSpan';
|
|
10
|
+
import { ScreenLoadingManager } from './apm/ScreenLoadingManager';
|
|
11
|
+
import { Logger } from '../utils/logger';
|
|
12
|
+
|
|
13
|
+
// Initialize Screen Loading on module load
|
|
14
|
+
ScreenLoadingManager.initialize().catch((error) => {
|
|
15
|
+
Logger.error('[APM] Failed to initialize Screen Loading:', error);
|
|
16
|
+
});
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
19
|
* Enables or disables APM
|
|
@@ -195,3 +202,38 @@ export const addCompletedCustomSpan = async (
|
|
|
195
202
|
): Promise<void> => {
|
|
196
203
|
return addCompletedCustomSpanInternal(name, startDate, endDate);
|
|
197
204
|
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Enables or disables Screen Loading feature
|
|
208
|
+
* @param isEnabled
|
|
209
|
+
*/
|
|
210
|
+
export const setScreenLoadingEnabled = (isEnabled: boolean) => {
|
|
211
|
+
try {
|
|
212
|
+
NativeAPM.setScreenLoadingEnabled(isEnabled);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
Logger.error('[APM] Failed to set screen loading enabled:', error);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extends the currently running screen loading trace with a new end timestamp.
|
|
220
|
+
*/
|
|
221
|
+
export const endScreenLoading = () => {
|
|
222
|
+
ScreenLoadingManager.endScreenLoading();
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Exclude specific routes from automatic screen loading measurement
|
|
227
|
+
* @param routes Array of route names to exclude
|
|
228
|
+
*/
|
|
229
|
+
export function excludeScreenLoadingRoutes(routes: string[]): void {
|
|
230
|
+
ScreenLoadingManager.excludeRoutes(routes);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Include previously excluded routes back into screen loading measurement
|
|
235
|
+
* @param routes Array of route names to include (or empty to clear all exclusions)
|
|
236
|
+
*/
|
|
237
|
+
export function includeScreenLoadingRoutes(routes?: string[]): void {
|
|
238
|
+
ScreenLoadingManager.includeRoutes(routes);
|
|
239
|
+
}
|