@cxpinsight/react-native 0.19.1 → 0.21.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/CHANGELOG.md +42 -0
- package/android/src/main/java/com/cxpinsight/reactnative/CXPVitalsModule.java +170 -0
- package/android/src/main/java/com/cxpinsight/reactnative/CXPinsightPackage.java +2 -1
- package/ios/CXPVitals.m +143 -0
- package/lib/CXPinsight.d.ts +8 -1
- package/lib/CXPinsight.d.ts.map +1 -1
- package/lib/CXPinsight.js +26 -1
- package/lib/CXPinsight.js.map +1 -1
- package/lib/CXPinsightProvider.d.ts.map +1 -1
- package/lib/CXPinsightProvider.js +7 -1
- package/lib/CXPinsightProvider.js.map +1 -1
- package/lib/core/autoTapCapture.d.ts.map +1 -1
- package/lib/core/autoTapCapture.js +12 -2
- package/lib/core/autoTapCapture.js.map +1 -1
- package/lib/core/captureStatus.d.ts +4 -0
- package/lib/core/captureStatus.d.ts.map +1 -1
- package/lib/core/captureStatus.js +2 -0
- package/lib/core/captureStatus.js.map +1 -1
- package/lib/core/network.d.ts +3 -1
- package/lib/core/network.d.ts.map +1 -1
- package/lib/core/network.js +9 -3
- package/lib/core/network.js.map +1 -1
- package/lib/core/triggerEngine.d.ts.map +1 -1
- package/lib/core/triggerEngine.js +72 -10
- package/lib/core/triggerEngine.js.map +1 -1
- package/lib/core/vitals.d.ts +50 -0
- package/lib/core/vitals.d.ts.map +1 -0
- package/lib/core/vitals.js +343 -0
- package/lib/core/vitals.js.map +1 -0
- package/lib/instrumented/Pressable.d.ts.map +1 -1
- package/lib/instrumented/Pressable.js +5 -0
- package/lib/instrumented/Pressable.js.map +1 -1
- package/lib/instrumented/TouchableOpacity.d.ts.map +1 -1
- package/lib/instrumented/TouchableOpacity.js +5 -0
- package/lib/instrumented/TouchableOpacity.js.map +1 -1
- package/lib/survey/SurveyModal.d.ts.map +1 -1
- package/lib/survey/SurveyModal.js +8 -0
- package/lib/survey/SurveyModal.js.map +1 -1
- package/lib/types.d.ts +6 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
Requires `@cxpinsight/survey-spec@^0.11.0` (unchanged).
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **App start and frame vitals on Android.** `APP_START_COLD` (process start → the first
|
|
9
|
+
screen's first frame, once per process), `APP_START_WARM` (activity re-created while the
|
|
10
|
+
process lived on), `APP_START_HOT` (back to the foreground) and, per screen visit,
|
|
11
|
+
`SLOW_FRAMES` / `FROZEN_FRAMES` (share of drawn frames over their deadline / over 700 ms,
|
|
12
|
+
Android vitals definitions), as `web_vitals` events like the rest. Android is also told
|
|
13
|
+
when the first screen is fully drawn (`reportFullyDrawn`), so the system's own
|
|
14
|
+
startup numbers (Play Console, Macrobenchmark) see it. Measured on an API 35 emulator
|
|
15
|
+
against Android's own numbers: cold start within 64 ms of "Fully drawn", hot start within
|
|
16
|
+
a frame of `am start -W`, slow frames 8.0 % vs `gfxinfo` 8.6 %.
|
|
17
|
+
Needs a native build (not Expo Go); visits under 30 frames report no frame share.
|
|
18
|
+
- iOS ships the same native module, switched off until it has been measured on iOS; iOS
|
|
19
|
+
keeps screen load, INP and JS stall from 0.20.0.
|
|
20
|
+
|
|
21
|
+
## 0.20.0
|
|
22
|
+
|
|
23
|
+
Requires `@cxpinsight/survey-spec@^0.11.0`. Publish that first.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **Performance vitals on mobile, in the same shape as web.** Each screen visit reports
|
|
27
|
+
`SCREEN_LOAD` (the tap or route change that opened the screen → its first frame on the
|
|
28
|
+
display), `INP` (an input → the frame showing its result, the worst in the visit) and
|
|
29
|
+
`JS_STALL` (the longest time the JS thread was blocked), as `web_vitals` events with the
|
|
30
|
+
web SDK's fields, so the dashboard's Performance tab reads both platforms the same way.
|
|
31
|
+
Thresholds follow published standards: LCP's for screen load, web INP's, Apple's hangs.
|
|
32
|
+
Measured on iOS against a 60 fps screen recording: start exact, end within a frame.
|
|
33
|
+
No setup needed; screens come from the provider's navigation tracking.
|
|
34
|
+
- **An ask can send the respondent to a link after submitting** (the survey's
|
|
35
|
+
`settings.completion.redirectUrl`, resolved by survey-spec's `submitRedirect`).
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- **The offline copy of the trigger config works on Android again.** It was one
|
|
39
|
+
AsyncStorage value, which Android cannot read back over ~2 MB (passed at ~4,000+
|
|
40
|
+
interactions). It is now stored in pieces under 900 KB; a copy stored by an earlier SDK
|
|
41
|
+
is read once and replaced.
|
|
42
|
+
- **App starts revalidate the trigger config** with its ETag and use the stored copy on
|
|
43
|
+
304, instead of downloading ~300 KB each time.
|
|
44
|
+
|
|
3
45
|
## 0.19.1
|
|
4
46
|
|
|
5
47
|
Requires `@cxpinsight/survey-spec@^0.10.0` (unchanged).
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
package com.cxpinsight.reactnative;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.app.Application;
|
|
5
|
+
import android.os.Build;
|
|
6
|
+
import android.os.Bundle;
|
|
7
|
+
import android.os.Handler;
|
|
8
|
+
import android.os.HandlerThread;
|
|
9
|
+
import android.os.Process;
|
|
10
|
+
import android.os.SystemClock;
|
|
11
|
+
import android.view.FrameMetrics;
|
|
12
|
+
import android.view.Window;
|
|
13
|
+
|
|
14
|
+
import androidx.annotation.NonNull;
|
|
15
|
+
import androidx.annotation.Nullable;
|
|
16
|
+
|
|
17
|
+
import com.facebook.react.bridge.Arguments;
|
|
18
|
+
import com.facebook.react.bridge.Promise;
|
|
19
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
20
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
21
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
22
|
+
import com.facebook.react.bridge.WritableMap;
|
|
23
|
+
|
|
24
|
+
import java.lang.ref.WeakReference;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Mobile vitals that only native code can see — the Android half of ios/CXPVitals.m and the native
|
|
28
|
+
* half of src/core/vitals.ts.
|
|
29
|
+
*
|
|
30
|
+
* getLaunchInfo when this process started, when an activity was last created (a WARM start: the
|
|
31
|
+
* process lived on, the activity did not) and when the app last came to the
|
|
32
|
+
* foreground (a HOT start) — wall-clock ms, the clock JS's Date.now() reads.
|
|
33
|
+
* startFrameMonitor / getFrameStats
|
|
34
|
+
* frames DRAWN since the monitor started (FrameMetrics, the source Google's JankStats
|
|
35
|
+
* uses): total, slow (over the frame's deadline — the vsync interval) and frozen (over
|
|
36
|
+
* 700 ms), the Android vitals definitions. Cumulative; JS takes the difference per
|
|
37
|
+
* screen visit.
|
|
38
|
+
*/
|
|
39
|
+
public class CXPVitalsModule extends ReactContextBaseJavaModule {
|
|
40
|
+
static final String NAME = "CXPVitals";
|
|
41
|
+
|
|
42
|
+
private static final long FROZEN_NS = 700_000_000L;
|
|
43
|
+
|
|
44
|
+
private static volatile double activityCreatedMs = 0;
|
|
45
|
+
private static volatile double foregroundMs = 0;
|
|
46
|
+
private static volatile boolean callbacksRegistered = false;
|
|
47
|
+
// A cold start happens once per process; a JS reload in the same process is not one.
|
|
48
|
+
private static volatile boolean launchReported = false;
|
|
49
|
+
|
|
50
|
+
private long total = 0, slow = 0, frozen = 0;
|
|
51
|
+
private boolean monitoring = false;
|
|
52
|
+
private WeakReference<Window> watched = new WeakReference<>(null);
|
|
53
|
+
private Window.OnFrameMetricsAvailableListener listener;
|
|
54
|
+
private HandlerThread metricsThread;
|
|
55
|
+
private Handler metricsHandler;
|
|
56
|
+
|
|
57
|
+
CXPVitalsModule(ReactApplicationContext context) {
|
|
58
|
+
super(context);
|
|
59
|
+
registerLifecycle(context);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@NonNull
|
|
63
|
+
@Override
|
|
64
|
+
public String getName() { return NAME; }
|
|
65
|
+
|
|
66
|
+
private static double wallFromElapsed(long elapsedMs) {
|
|
67
|
+
return System.currentTimeMillis() - (SystemClock.elapsedRealtime() - elapsedMs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private void registerLifecycle(ReactApplicationContext context) {
|
|
71
|
+
if (callbacksRegistered) return;
|
|
72
|
+
if (!(context.getApplicationContext() instanceof Application)) return;
|
|
73
|
+
callbacksRegistered = true;
|
|
74
|
+
((Application) context.getApplicationContext()).registerActivityLifecycleCallbacks(
|
|
75
|
+
new Application.ActivityLifecycleCallbacks() {
|
|
76
|
+
@Override public void onActivityCreated(@NonNull Activity a, @Nullable Bundle b) {
|
|
77
|
+
activityCreatedMs = System.currentTimeMillis();
|
|
78
|
+
}
|
|
79
|
+
@Override public void onActivityStarted(@NonNull Activity a) {
|
|
80
|
+
foregroundMs = System.currentTimeMillis();
|
|
81
|
+
if (monitoring) attach(a);
|
|
82
|
+
}
|
|
83
|
+
@Override public void onActivityResumed(@NonNull Activity a) { if (monitoring) attach(a); }
|
|
84
|
+
@Override public void onActivityPaused(@NonNull Activity a) {}
|
|
85
|
+
@Override public void onActivityStopped(@NonNull Activity a) {}
|
|
86
|
+
@Override public void onActivitySaveInstanceState(@NonNull Activity a, @NonNull Bundle b) {}
|
|
87
|
+
@Override public void onActivityDestroyed(@NonNull Activity a) {}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@ReactMethod
|
|
92
|
+
public void getLaunchInfo(Promise promise) {
|
|
93
|
+
WritableMap m = Arguments.createMap();
|
|
94
|
+
boolean first = !launchReported;
|
|
95
|
+
launchReported = true;
|
|
96
|
+
m.putBoolean("firstInProcess", first);
|
|
97
|
+
double processStart = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
|
98
|
+
? wallFromElapsed(Process.getStartElapsedRealtime()) : 0;
|
|
99
|
+
m.putDouble("processStartMs", processStart);
|
|
100
|
+
m.putBoolean("prewarmed", false);
|
|
101
|
+
m.putDouble("finishedLaunchingMs", 0);
|
|
102
|
+
m.putDouble("foregroundMs", foregroundMs);
|
|
103
|
+
m.putDouble("activityCreatedMs", activityCreatedMs);
|
|
104
|
+
m.putDouble("nowMs", System.currentTimeMillis());
|
|
105
|
+
m.putString("frameSource", "frame_metrics");
|
|
106
|
+
promise.resolve(m);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@ReactMethod
|
|
110
|
+
public void startFrameMonitor() {
|
|
111
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
|
|
112
|
+
monitoring = true;
|
|
113
|
+
Activity a = getCurrentActivity();
|
|
114
|
+
if (a != null) a.runOnUiThread(() -> attach(a));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private synchronized void attach(Activity activity) {
|
|
118
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || activity == null) return;
|
|
119
|
+
Window window = activity.getWindow();
|
|
120
|
+
if (window == null || window == watched.get()) return;
|
|
121
|
+
Window previous = watched.get();
|
|
122
|
+
if (previous != null && listener != null) {
|
|
123
|
+
try { previous.removeOnFrameMetricsAvailableListener(listener); } catch (Exception ignored) {}
|
|
124
|
+
}
|
|
125
|
+
if (metricsThread == null) {
|
|
126
|
+
metricsThread = new HandlerThread("cxp-frame-metrics");
|
|
127
|
+
metricsThread.start();
|
|
128
|
+
metricsHandler = new Handler(metricsThread.getLooper());
|
|
129
|
+
}
|
|
130
|
+
if (listener == null) {
|
|
131
|
+
listener = (w, metrics, dropped) -> {
|
|
132
|
+
long duration = metrics.getMetric(FrameMetrics.TOTAL_DURATION);
|
|
133
|
+
long deadline = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
134
|
+
? metrics.getMetric(FrameMetrics.DEADLINE) : 16_666_667L;
|
|
135
|
+
synchronized (CXPVitalsModule.this) {
|
|
136
|
+
total += 1 + dropped;
|
|
137
|
+
if (duration > deadline) slow += 1;
|
|
138
|
+
if (duration > FROZEN_NS) frozen += 1;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
window.addOnFrameMetricsAvailableListener(listener, metricsHandler);
|
|
144
|
+
watched = new WeakReference<>(window);
|
|
145
|
+
} catch (Exception ignored) {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The first screen's content is on the display: tell Android, so the system logs "Fully drawn"
|
|
150
|
+
* (Time to Full Display, measured by Android from the launch) — what Play Console's startup
|
|
151
|
+
* metrics and Macrobenchmark read. Once per process; Android ignores repeats.
|
|
152
|
+
*/
|
|
153
|
+
@ReactMethod
|
|
154
|
+
public void reportFullyDrawn() {
|
|
155
|
+
Activity a = getCurrentActivity();
|
|
156
|
+
if (a == null) return;
|
|
157
|
+
a.runOnUiThread(() -> { try { a.reportFullyDrawn(); } catch (Exception ignored) {} });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@ReactMethod
|
|
161
|
+
public void getFrameStats(Promise promise) {
|
|
162
|
+
WritableMap m = Arguments.createMap();
|
|
163
|
+
synchronized (this) {
|
|
164
|
+
m.putDouble("total", total);
|
|
165
|
+
m.putDouble("slow", slow);
|
|
166
|
+
m.putDouble("frozen", frozen);
|
|
167
|
+
}
|
|
168
|
+
promise.resolve(m);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -7,6 +7,7 @@ import com.facebook.react.bridge.NativeModule;
|
|
|
7
7
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
8
8
|
import com.facebook.react.uimanager.ViewManager;
|
|
9
9
|
|
|
10
|
+
import java.util.Arrays;
|
|
10
11
|
import java.util.Collections;
|
|
11
12
|
import java.util.List;
|
|
12
13
|
|
|
@@ -14,7 +15,7 @@ public class CXPinsightPackage implements ReactPackage {
|
|
|
14
15
|
@NonNull
|
|
15
16
|
@Override
|
|
16
17
|
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
|
17
|
-
return
|
|
18
|
+
return Arrays.<NativeModule>asList(new CXPScreenshotModule(reactContext), new CXPVitalsModule(reactContext));
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
@NonNull
|
package/ios/CXPVitals.m
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Mobile vitals that only native code can see — the native half of src/core/vitals.ts.
|
|
2
|
+
//
|
|
3
|
+
// getLaunchInfo when this process started (and whether iOS prewarmed it), when the app finished
|
|
4
|
+
// launching, and when it last came to the foreground — wall-clock ms, the clock
|
|
5
|
+
// JS's Date.now() reads, so JS can measure up to the first frame it sees.
|
|
6
|
+
// startFrameMonitor / getFrameStats
|
|
7
|
+
// frames the main thread delivered since the monitor started: total, slow (later
|
|
8
|
+
// than 1.5 refresh intervals — a missed deadline) and frozen (over 700 ms), the
|
|
9
|
+
// Android vitals definitions. Counts are cumulative; JS takes the difference per
|
|
10
|
+
// screen visit. Paused while the app is in the background.
|
|
11
|
+
//
|
|
12
|
+
// Prewarm: since iOS 15 the system may start an app's process before anyone opens it, so the process
|
|
13
|
+
// start can be minutes before the launch. Apple's advice is to measure such launches from when the
|
|
14
|
+
// app finishes launching instead; `prewarmed` says which one JS should use.
|
|
15
|
+
|
|
16
|
+
#import <React/RCTBridgeModule.h>
|
|
17
|
+
#import <UIKit/UIKit.h>
|
|
18
|
+
#import <QuartzCore/QuartzCore.h>
|
|
19
|
+
#include <sys/sysctl.h>
|
|
20
|
+
#include <unistd.h>
|
|
21
|
+
|
|
22
|
+
static double CXPNowMs(void) { return [[NSDate date] timeIntervalSince1970] * 1000.0; }
|
|
23
|
+
|
|
24
|
+
static double cxpProcessStartMs = 0;
|
|
25
|
+
static double cxpFinishedLaunchingMs = 0;
|
|
26
|
+
static double cxpForegroundMs = 0;
|
|
27
|
+
static BOOL cxpPrewarmed = NO;
|
|
28
|
+
// A cold start happens once per process; a JS reload in the same process is not one.
|
|
29
|
+
static BOOL cxpLaunchReported = NO;
|
|
30
|
+
|
|
31
|
+
static double CXPProcessStartMs(void)
|
|
32
|
+
{
|
|
33
|
+
struct kinfo_proc info;
|
|
34
|
+
size_t size = sizeof(info);
|
|
35
|
+
int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid() };
|
|
36
|
+
if (sysctl(mib, 4, &info, &size, NULL, 0) != 0) return 0;
|
|
37
|
+
struct timeval t = info.kp_proc.p_starttime;
|
|
38
|
+
return t.tv_sec * 1000.0 + t.tv_usec / 1000.0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Runs before main() — so the launch notifications below are never missed. A C constructor rather
|
|
42
|
+
// than +load: RCT_EXPORT_MODULE defines +load on this class to register the module.
|
|
43
|
+
__attribute__((constructor)) static void CXPVitalsLaunchHooks(void)
|
|
44
|
+
{
|
|
45
|
+
cxpProcessStartMs = CXPProcessStartMs();
|
|
46
|
+
const char *prewarm = getenv("ActivePrewarm");
|
|
47
|
+
cxpPrewarmed = prewarm != NULL && strcmp(prewarm, "1") == 0;
|
|
48
|
+
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
49
|
+
[nc addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil
|
|
50
|
+
usingBlock:^(__unused NSNotification *n) { cxpFinishedLaunchingMs = CXPNowMs(); }];
|
|
51
|
+
[nc addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:nil
|
|
52
|
+
usingBlock:^(__unused NSNotification *n) { cxpForegroundMs = CXPNowMs(); }];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@interface CXPFrameMonitor : NSObject
|
|
56
|
+
@property (nonatomic, strong) CADisplayLink *link;
|
|
57
|
+
@property (nonatomic) CFTimeInterval last;
|
|
58
|
+
@property (nonatomic) long long total;
|
|
59
|
+
@property (nonatomic) long long slow;
|
|
60
|
+
@property (nonatomic) long long frozen;
|
|
61
|
+
@end
|
|
62
|
+
|
|
63
|
+
@implementation CXPFrameMonitor
|
|
64
|
+
|
|
65
|
+
- (void)start
|
|
66
|
+
{
|
|
67
|
+
if (self.link) return;
|
|
68
|
+
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
|
|
69
|
+
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
70
|
+
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
71
|
+
[nc addObserver:self selector:@selector(pause) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
|
72
|
+
[nc addObserver:self selector:@selector(resume) name:UIApplicationDidBecomeActiveNotification object:nil];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
- (void)pause { self.link.paused = YES; self.last = 0; }
|
|
76
|
+
- (void)resume { self.last = 0; self.link.paused = NO; }
|
|
77
|
+
|
|
78
|
+
- (void)tick:(CADisplayLink *)link
|
|
79
|
+
{
|
|
80
|
+
CFTimeInterval now = link.timestamp;
|
|
81
|
+
if (self.last > 0) {
|
|
82
|
+
CFTimeInterval expected = link.targetTimestamp - link.timestamp;
|
|
83
|
+
if (expected <= 0 || expected > 0.1) expected = 1.0 / 60.0;
|
|
84
|
+
CFTimeInterval dt = now - self.last;
|
|
85
|
+
self.total += 1;
|
|
86
|
+
if (dt > expected * 1.5) self.slow += 1;
|
|
87
|
+
if (dt > 0.7) self.frozen += 1;
|
|
88
|
+
}
|
|
89
|
+
self.last = now;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@end
|
|
93
|
+
|
|
94
|
+
@interface CXPVitals : NSObject <RCTBridgeModule>
|
|
95
|
+
@end
|
|
96
|
+
|
|
97
|
+
@implementation CXPVitals
|
|
98
|
+
|
|
99
|
+
RCT_EXPORT_MODULE(CXPVitals)
|
|
100
|
+
|
|
101
|
+
static CXPFrameMonitor *cxpMonitor = nil;
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
+ (BOOL)requiresMainQueueSetup { return NO; }
|
|
105
|
+
- (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); }
|
|
106
|
+
|
|
107
|
+
RCT_EXPORT_METHOD(getLaunchInfo:(RCTPromiseResolveBlock)resolve
|
|
108
|
+
reject:(__unused RCTPromiseRejectBlock)reject)
|
|
109
|
+
{
|
|
110
|
+
BOOL first = !cxpLaunchReported;
|
|
111
|
+
cxpLaunchReported = YES;
|
|
112
|
+
resolve(@{
|
|
113
|
+
@"firstInProcess": @(first),
|
|
114
|
+
@"processStartMs": @(cxpProcessStartMs),
|
|
115
|
+
@"prewarmed": @(cxpPrewarmed),
|
|
116
|
+
@"finishedLaunchingMs": @(cxpFinishedLaunchingMs),
|
|
117
|
+
@"foregroundMs": @(cxpForegroundMs),
|
|
118
|
+
@"activityCreatedMs": @0,
|
|
119
|
+
@"nowMs": @(CXPNowMs()),
|
|
120
|
+
@"frameSource": @"display_link",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
RCT_EXPORT_METHOD(startFrameMonitor)
|
|
125
|
+
{
|
|
126
|
+
if (!cxpMonitor) cxpMonitor = [CXPFrameMonitor new];
|
|
127
|
+
[cxpMonitor start];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Android's "Fully drawn" has no iOS equivalent; kept so JS calls one API on both.
|
|
131
|
+
RCT_EXPORT_METHOD(reportFullyDrawn) {}
|
|
132
|
+
|
|
133
|
+
RCT_EXPORT_METHOD(getFrameStats:(RCTPromiseResolveBlock)resolve
|
|
134
|
+
reject:(__unused RCTPromiseRejectBlock)reject)
|
|
135
|
+
{
|
|
136
|
+
resolve(@{
|
|
137
|
+
@"total": @(cxpMonitor ? cxpMonitor.total : 0),
|
|
138
|
+
@"slow": @(cxpMonitor ? cxpMonitor.slow : 0),
|
|
139
|
+
@"frozen": @(cxpMonitor ? cxpMonitor.frozen : 0),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@end
|
package/lib/CXPinsight.d.ts
CHANGED
|
@@ -100,13 +100,20 @@ declare class CXPinsightSDK {
|
|
|
100
100
|
private verifyConnectivity;
|
|
101
101
|
track(eventName: string, data?: Record<string, unknown>, eventType?: string, teeToRecording?: boolean,
|
|
102
102
|
/** Internal: preserves the original time of a call buffered before init(). */
|
|
103
|
-
eventTimestamp?: string
|
|
103
|
+
eventTimestamp?: string,
|
|
104
|
+
/** Internal: top-level performance values (vitals), as the web SDK sends them. */
|
|
105
|
+
performance?: Record<string, number>): void;
|
|
104
106
|
/**
|
|
105
107
|
* The screenshot capturer for a recording that is starting now: the host's
|
|
106
108
|
* captureScreen, else the SDK's own (nativeScreenshot.ts), unless the
|
|
107
109
|
* dashboard turned screenshots off.
|
|
108
110
|
*/
|
|
109
111
|
private screenshotProvider;
|
|
112
|
+
/**
|
|
113
|
+
* One mobile vital, as a `web_vitals` event in the web SDK's shape, so the Performance tab reads
|
|
114
|
+
* both platforms the same way (docs/architecture/performance-vitals-web-and-mobile.md).
|
|
115
|
+
*/
|
|
116
|
+
private trackVital;
|
|
110
117
|
/** Track a screen view (triggers remote page_view rules) */
|
|
111
118
|
trackScreen(screenName: string, data?: Record<string, unknown>): void;
|
|
112
119
|
/** Identify the current user */
|
package/lib/CXPinsight.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CXPinsight.d.ts","sourceRoot":"","sources":["../src/CXPinsight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EAGZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"CXPinsight.d.ts","sourceRoot":"","sources":["../src/CXPinsight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EAGZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AA6BjB,OAAO,EAA8J,KAAK,UAAU,EAAE,KAAK,eAAe,EAA0B,MAAM,gBAAgB,CAAC;AAiB3P,KAAK,eAAe,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAClD,KAAK,cAAc,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;AAE9C,cAAM,aAAa;IACjB,OAAO,CAAC,WAAW,CAAS;IAC5B,2EAA2E;IAC3E,OAAO,CAAC,aAAa,CAAqG;IAC1H,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,MAAM,CAAc;IAK5B,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,cAAc,CAA+B;IAErD,OAAO,CAAC,cAAc,CAA6B;IACnD,uEAAuE;IACvE,OAAO,CAAC,UAAU,CAAiD;IAEnE,OAAO,CAAC,aAAa,CAAuB;IAE5C,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,gBAAgB,CAA4B;IAEpD,OAAO,CAAC,WAAW,CAA8G;IACjI,OAAO,CAAC,cAAc,CAAuB;IAG7C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,gBAAgB,CAAS;IAEjC,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,qBAAqB;IAC7B,OAAO,CAAC,eAAe;IAEvB,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAS;IAY7C,OAAO,CAAC,OAAO,CAAwG;IACvH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAM;IACzC,OAAO,CAAC,cAAc,CAAK;IAE3B,oDAAoD;IAC9C,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0QnF;IAED,2FAA2F;IAC3F,OAAO,CAAC,aAAa;IAyDrB;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,iBAAiB,CAA8B;IACvD,OAAO,CAAC,mBAAmB;IAgB3B,6FAA6F;YAC/E,kBAAkB;IAOhC;;;;;;OAMG;IAEH;;;;;;;OAOG;IACH;;;;;;;;;OASG;IACH,OAAO,CAAC,cAAc;IAoBtB,qEAAqE;IACrE,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAA0C;IAExF,OAAO,CAAC,sBAAsB;YAsDhB,oBAAoB;IAyFlC,4EAA4E;YAC9D,iBAAiB;IAqD/B,yFAAyF;IACzF,OAAO,CAAC,kBAAkB;IAU1B,kGAAkG;IAClG,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,KAAK,GAAG,aAAa,GAAG,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAEpG;IAED,qEAAqE;IACrE,OAAO,CAAC,YAAY;IAgBpB,sEAAsE;IACtE,OAAO,CAAC,eAAe;IA8NvB,2BAA2B;IAC3B;;;;OAIG;YACW,kBAAkB;IAiChC,KAAK,CACH,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,UAAO;IACrB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM;IACvB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,IAAI,CA4FN;IAED;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;;OAGG;IACH,OAAO,CAAC,UAAU;IAclB,4DAA4D;IAC5D,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAqCpE;IAED,gCAAgC;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhD;IAED,0BAA0B;IAC1B,eAAe,CACb,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,IAAI,CAqCN;IAED,qCAAqC;IACrC,eAAe,CACb,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,IAAI,CAmCN;IAED,+BAA+B;IAC/B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAErF;IAED;;;;;;;OAOG;IACH,cAAc,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAErG;IAED,yEAAyE;IACzE,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAE3E;IAED,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAkDpE;IAID,uDAAuD;IACvD,cAAc,IAAI,IAAI,CAUrB;IAED,oCAAoC;IAC9B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAKnC;IAED,+CAA+C;IAC/C,aAAa,IAAI,OAAO,CAEvB;IAED,2CAA2C;IAC3C,aAAa,+DAEZ;IAED,8DAA8D;IACxD,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAEnC;IAED,qCAAqC;IACrC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMjC;IAED;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAgBpC;IAED,+DAA+D;IACzD,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAED,2CAA2C;IAC3C;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI,CAAoB;IACjC,MAAM,IAAI,IAAI,CAAqB;IAEnC;;;;;;OAMG;IACH,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI,CAMpD;IAED;;;;;;;OAOG;IACH,UAAU,IAAI,UAAU,GAAG,IAAI,CAAyB;IAExD,sEAAsE;IACtE,iBAAiB,IAAI,SAAS,eAAe,EAAE,CAA+B;IAE9E,8CAA8C;IAC9C,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAA6B;IACvE,iBAAiB,IAAI,OAAO,CAA8B;IAE1D,qDAAqD;YACvC,eAAe;IA+BvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAkC3B;IAED,+BAA+B;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3B;IAED,sEAAsE;IACtE,wBAAwB,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI,CAEzD;IAED,gDAAgD;IAChD,0BAA0B,IAAI,IAAI,CAEjC;IAED,qEAAqE;IACrE,uBAAuB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAEvD;IAED,+CAA+C;IAC/C,yBAAyB,IAAI,IAAI,CAEhC;IAED,8BAA8B;IAC9B,aAAa,IAAI,mBAAmB,CAEnC;IAED,2BAA2B;IAC3B,UAAU,IAAI,MAAM,CAEnB;IAED;;;;wEAIoE;IACpE,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAEjD;IAED,2BAA2B;IAC3B;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAI3C;IAED,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,oBAAoB,CAAS;IAErC,8EAA8E;IAC9E,iBAAiB,IAAI,IAAI,CAGxB;IAED,UAAU,IAAI,gBAAgB,GAAG,IAAI,CAEpC;IAED;;mFAE+E;IAC/E,uBAAuB,IAAI;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAOlG;IAED,qCAAqC;IACrC,cAAc,IAAI,OAAO,CAExB;IAED,OAAO,CAAC,GAAG;CAKZ;AAED,6BAA6B;AAC7B,eAAO,MAAM,UAAU,eAAsB,CAAC"}
|
package/lib/CXPinsight.js
CHANGED
|
@@ -52,6 +52,7 @@ const SetupController_1 = require("./setup/SetupController");
|
|
|
52
52
|
const recorder = __importStar(require("./core/mobileRecorder"));
|
|
53
53
|
const interactionObserver_1 = require("./instrumented/interactionObserver");
|
|
54
54
|
const lifecycle_1 = require("./core/lifecycle");
|
|
55
|
+
const vitals_1 = require("./core/vitals");
|
|
55
56
|
const networkCapture_1 = require("./core/networkCapture");
|
|
56
57
|
const scrollCapture_1 = require("./core/scrollCapture");
|
|
57
58
|
const version_1 = require("./version");
|
|
@@ -877,6 +878,8 @@ class CXPinsightSDK {
|
|
|
877
878
|
}
|
|
878
879
|
// App lifecycle → navigation events (app_opened / foregrounded / backgrounded).
|
|
879
880
|
// Also pause/resume the engagement clock so time-on-app excludes backgrounded time.
|
|
881
|
+
// Mobile vitals (core/vitals.ts): screen load, INP, JS stalls — reported in the web's shape.
|
|
882
|
+
(0, vitals_1.startVitals)((r) => this.trackVital(r));
|
|
880
883
|
(0, lifecycle_1.startLifecycleTracking)((evt) => {
|
|
881
884
|
if (evt.type === 'app_backgrounded') {
|
|
882
885
|
this.pauseEngagementClock();
|
|
@@ -982,7 +985,9 @@ class CXPinsightSDK {
|
|
|
982
985
|
}
|
|
983
986
|
track(eventName, data, eventType, teeToRecording = true,
|
|
984
987
|
/** Internal: preserves the original time of a call buffered before init(). */
|
|
985
|
-
eventTimestamp
|
|
988
|
+
eventTimestamp,
|
|
989
|
+
/** Internal: top-level performance values (vitals), as the web SDK sends them. */
|
|
990
|
+
performance) {
|
|
986
991
|
if (!this.initialized || !this.config) {
|
|
987
992
|
// Held, not dropped. Drained by flushPending() once init() completes.
|
|
988
993
|
if (this.pending.length < CXPinsightSDK.MAX_PENDING) {
|
|
@@ -1024,6 +1029,7 @@ class CXPinsightSDK {
|
|
|
1024
1029
|
// Active foreground time this session (parity with web's engagementTimeMs).
|
|
1025
1030
|
engagementTimeMs: this.getEngagementMs(),
|
|
1026
1031
|
...(userProperties ? { userProperties } : {}),
|
|
1032
|
+
...(performance ? { performance } : {}),
|
|
1027
1033
|
};
|
|
1028
1034
|
// Product analytics consent, finally enforced. CONSENT_CATEGORIES has always
|
|
1029
1035
|
// offered `analytics` and the banner has always shown "Product analytics",
|
|
@@ -1085,6 +1091,23 @@ class CXPinsightSDK {
|
|
|
1085
1091
|
this.screenshotSource = r.source;
|
|
1086
1092
|
return r.provider;
|
|
1087
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* One mobile vital, as a `web_vitals` event in the web SDK's shape, so the Performance tab reads
|
|
1096
|
+
* both platforms the same way (docs/architecture/performance-vitals-web-and-mobile.md).
|
|
1097
|
+
*/
|
|
1098
|
+
trackVital(r) {
|
|
1099
|
+
this.track('web_vitals', {
|
|
1100
|
+
metric_name: r.name,
|
|
1101
|
+
metric_value: r.value,
|
|
1102
|
+
metric_rating: r.rating,
|
|
1103
|
+
page_type: r.screen,
|
|
1104
|
+
page: r.screen,
|
|
1105
|
+
page_load_id: r.visitId,
|
|
1106
|
+
navigation_type: r.navigationType,
|
|
1107
|
+
vitals_source: 'cxpinsight-rn@' + version_1.SDK_VERSION,
|
|
1108
|
+
attribution: r.attribution,
|
|
1109
|
+
}, 'performance', false, undefined, { [r.name.toLowerCase()]: r.value });
|
|
1110
|
+
}
|
|
1088
1111
|
/** Track a screen view (triggers remote page_view rules) */
|
|
1089
1112
|
trackScreen(screenName, data) {
|
|
1090
1113
|
// IDEMPOTENT FOR THE SCREEN ALREADY SHOWING.
|
|
@@ -1105,6 +1128,7 @@ class CXPinsightSDK {
|
|
|
1105
1128
|
this.currentScreen = screenName;
|
|
1106
1129
|
if (recorder.isRecording())
|
|
1107
1130
|
recorder.recordScreen(screenName, this.previousScreen);
|
|
1131
|
+
(0, vitals_1.onScreen)(screenName); // ends the previous visit's vitals, times this screen's load
|
|
1108
1132
|
// Cross-platform normalization: emit as `page_view` (web's canonical navigation
|
|
1109
1133
|
// event name) so the eventSignature (eventType|page|elementKey) collapses web
|
|
1110
1134
|
// and mobile navigations to the same key when they share a page identifier.
|
|
@@ -1443,6 +1467,7 @@ class CXPinsightSDK {
|
|
|
1443
1467
|
(0, autoTapCapture_1.uninstallAutoTapCapture)();
|
|
1444
1468
|
(0, session_1.setRolloverHandler)(null);
|
|
1445
1469
|
(0, lifecycle_1.stopLifecycleTracking)();
|
|
1470
|
+
(0, vitals_1.stopVitals)();
|
|
1446
1471
|
(0, networkCapture_1.stopNetworkCapture)();
|
|
1447
1472
|
this.engagementActive = false;
|
|
1448
1473
|
this.engagementAccumMs = 0;
|