@goliapkg/sentori-react-native 0.8.4 → 0.9.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/android/src/main/java/com/sentori/SentoriMobileVitals.kt +100 -0
- package/android/src/main/java/com/sentori/SentoriModule.kt +18 -0
- package/ios/SentoriMobileVitals.swift +104 -0
- package/ios/SentoriModule.swift +17 -0
- package/lib/feedback-widget.d.ts.map +1 -1
- package/lib/feedback-widget.js +8 -1
- package/lib/feedback-widget.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +21 -1
- package/lib/init.js.map +1 -1
- package/lib/launch-crash-guard.d.ts.map +1 -1
- package/lib/launch-crash-guard.js +8 -0
- package/lib/launch-crash-guard.js.map +1 -1
- package/lib/mobile-vitals.d.ts +35 -0
- package/lib/mobile-vitals.d.ts.map +1 -0
- package/lib/mobile-vitals.js +89 -0
- package/lib/mobile-vitals.js.map +1 -0
- package/lib/native-loader.d.ts +6 -0
- package/lib/native-loader.d.ts.map +1 -0
- package/lib/native-loader.js +31 -0
- package/lib/native-loader.js.map +1 -0
- package/lib/native.d.ts +11 -0
- package/lib/native.d.ts.map +1 -1
- package/lib/native.js +37 -0
- package/lib/native.js.map +1 -1
- package/lib/navigation.d.ts.map +1 -1
- package/lib/navigation.js +14 -2
- package/lib/navigation.js.map +1 -1
- package/lib/netinfo.d.ts.map +1 -1
- package/lib/netinfo.js +6 -12
- package/lib/netinfo.js.map +1 -1
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +6 -0
- package/lib/transport.js.map +1 -1
- package/package.json +1 -9
- package/src/feedback-widget.tsx +8 -1
- package/src/index.ts +12 -0
- package/src/init.ts +21 -1
- package/src/launch-crash-guard.ts +9 -0
- package/src/mobile-vitals.ts +114 -0
- package/src/native-loader.ts +33 -0
- package/src/native.ts +60 -0
- package/src/navigation.ts +16 -2
- package/src/netinfo.ts +7 -14
- package/src/transport.ts +6 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// v0.9.4 #1 — Mobile Vitals.
|
|
2
|
+
//
|
|
3
|
+
// Three measurements, three call paths, one server schema:
|
|
4
|
+
//
|
|
5
|
+
// • Cold start: native side measures at app launch (iOS
|
|
6
|
+
// mach_absolute_time / Android Process.getStartElapsedRealtime)
|
|
7
|
+
// and exposes via the bundled native module — read once on JS
|
|
8
|
+
// side at init() and ride along on the first event.
|
|
9
|
+
// • TTID (Time-To-Initial-Display): automatic via
|
|
10
|
+
// useTraceNavigation extension — span from navigation.dispatch
|
|
11
|
+
// to first frame after route mount.
|
|
12
|
+
// • TTFD (Time-To-Full-Display): manual. Host calls
|
|
13
|
+
// sentori.markTimeToFullDisplay('Home').end() when the screen's
|
|
14
|
+
// data has loaded.
|
|
15
|
+
// • Slow / frozen frame counts: native side hooks CADisplayLink /
|
|
16
|
+
// Choreographer.FrameCallback; counters flush per navigation
|
|
17
|
+
// span.
|
|
18
|
+
//
|
|
19
|
+
// JS-side first: TTFD API + bundle-level vital captures. Native
|
|
20
|
+
// pieces ship as a separate native module method `getColdStartMs`
|
|
21
|
+
// + `getFrameCounts()` — graceful no-op if not linked.
|
|
22
|
+
|
|
23
|
+
import { startSpan } from '@goliapkg/sentori-core';
|
|
24
|
+
|
|
25
|
+
import { getNativeColdStartMs } from './native';
|
|
26
|
+
|
|
27
|
+
let _coldStartMs: null | number = null;
|
|
28
|
+
let _coldStartCaptured = false;
|
|
29
|
+
|
|
30
|
+
/** Read the native-side cold start measurement once. Cached. Returns
|
|
31
|
+
* null when the native module isn't linked (Expo Go / tests). */
|
|
32
|
+
export function getColdStartMs(): null | number {
|
|
33
|
+
if (_coldStartCaptured) return _coldStartMs;
|
|
34
|
+
_coldStartCaptured = true;
|
|
35
|
+
try {
|
|
36
|
+
_coldStartMs = getNativeColdStartMs();
|
|
37
|
+
} catch {
|
|
38
|
+
_coldStartMs = null;
|
|
39
|
+
}
|
|
40
|
+
return _coldStartMs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* v0.9.4 #1 — Time-To-Full-Display marker. Host calls this at the
|
|
45
|
+
* point the screen is functionally "ready" (data fetched, images
|
|
46
|
+
* loaded, etc.) so the dashboard can show real perceived load time
|
|
47
|
+
* vs auto-detected TTID.
|
|
48
|
+
*
|
|
49
|
+
* const h = sentori.markTimeToFullDisplay('Home');
|
|
50
|
+
* // ...data fetched, images rendered...
|
|
51
|
+
* h.end();
|
|
52
|
+
*
|
|
53
|
+
* If `.end()` is never called, the handle's span is finished as
|
|
54
|
+
* `cancelled` at the next `markTimeToFullDisplay` call (one TTFD in
|
|
55
|
+
* flight at a time is the typical case).
|
|
56
|
+
*/
|
|
57
|
+
export type TimeToFullDisplayHandle = {
|
|
58
|
+
end: (opts?: { status?: 'cancelled' | 'error' | 'ok' }) => void;
|
|
59
|
+
cancel: () => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let _activeTtfd: null | {
|
|
63
|
+
finish: (status: 'cancelled' | 'error' | 'ok') => void;
|
|
64
|
+
route: string;
|
|
65
|
+
} = null;
|
|
66
|
+
|
|
67
|
+
export function markTimeToFullDisplay(route: string): TimeToFullDisplayHandle {
|
|
68
|
+
if (_activeTtfd && _activeTtfd.route !== route) {
|
|
69
|
+
_activeTtfd.finish('cancelled');
|
|
70
|
+
}
|
|
71
|
+
const span = startSpan('react.navigation.ttfd', {
|
|
72
|
+
name: route,
|
|
73
|
+
tags: { 'nav.route': route, 'vital.kind': 'ttfd' },
|
|
74
|
+
});
|
|
75
|
+
let finished = false;
|
|
76
|
+
const finish = (status: 'cancelled' | 'error' | 'ok'): void => {
|
|
77
|
+
if (finished) return;
|
|
78
|
+
finished = true;
|
|
79
|
+
span.finish({ status });
|
|
80
|
+
if (_activeTtfd && _activeTtfd.route === route) _activeTtfd = null;
|
|
81
|
+
};
|
|
82
|
+
_activeTtfd = { finish, route };
|
|
83
|
+
return {
|
|
84
|
+
cancel: () => finish('cancelled'),
|
|
85
|
+
end: (opts?: { status?: 'cancelled' | 'error' | 'ok' }) =>
|
|
86
|
+
finish(opts?.status ?? 'ok'),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** v0.9.4 #1 — read the per-screen slow/frozen frame counts since
|
|
91
|
+
* the most recent navigation transition. Native module reads
|
|
92
|
+
* CADisplayLink / Choreographer counters; returns null when not
|
|
93
|
+
* linked. */
|
|
94
|
+
export function getFrameCounters(): null | { slow: number; frozen: number } {
|
|
95
|
+
try {
|
|
96
|
+
// native binding lazily required in native.ts
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
98
|
+
const nativeMod = require('./native') as {
|
|
99
|
+
getNativeFrameCounters?: () => null | { frozen: number; slow: number };
|
|
100
|
+
};
|
|
101
|
+
if (typeof nativeMod.getNativeFrameCounters !== 'function') return null;
|
|
102
|
+
return nativeMod.getNativeFrameCounters();
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Test-only. */
|
|
109
|
+
export function __resetMobileVitalsForTests(): void {
|
|
110
|
+
if (_activeTtfd) _activeTtfd.finish('cancelled');
|
|
111
|
+
_activeTtfd = null;
|
|
112
|
+
_coldStartCaptured = false;
|
|
113
|
+
_coldStartMs = null;
|
|
114
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// v0.8.5 — shared guard for "JS package is in node_modules but native
|
|
2
|
+
// module isn't linked".
|
|
3
|
+
//
|
|
4
|
+
// The pattern that bit us in 0.8.0–0.8.3 (NetInfo, AsyncStorage,
|
|
5
|
+
// expo-sensors, ...): host had the JS package via npm hoisting (or
|
|
6
|
+
// via our old `optionalDependencies` mistake) but never ran
|
|
7
|
+
// `pod install` / `prebuild` / `react-native link`. We `require(...)`
|
|
8
|
+
// successfully, call a method, then the lib's internal native bridge
|
|
9
|
+
// access throws — and most of those errors arrive on an emitter or
|
|
10
|
+
// microtask where our try/catch can't reach.
|
|
11
|
+
//
|
|
12
|
+
// Fix: before requireing such a package, check that the registered
|
|
13
|
+
// NativeModule actually exists. `null` → skip the whole feature; cost
|
|
14
|
+
// is one nullable map read.
|
|
15
|
+
|
|
16
|
+
export function isNativeModuleLinked(name: string): boolean {
|
|
17
|
+
try {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
|
+
const RN = require('react-native') as {
|
|
20
|
+
NativeModules?: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
return RN.NativeModules?.[name] != null;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Some native modules are registered under multiple alternative names
|
|
29
|
+
* across RN versions / Expo packaging. Returns `true` iff *any* name
|
|
30
|
+
* is linked. */
|
|
31
|
+
export function isAnyNativeModuleLinked(names: string[]): boolean {
|
|
32
|
+
return names.some(isNativeModuleLinked);
|
|
33
|
+
}
|
package/src/native.ts
CHANGED
|
@@ -11,6 +11,29 @@ type SentoriNativeModule = {
|
|
|
11
11
|
release: string
|
|
12
12
|
token: string
|
|
13
13
|
}) => void
|
|
14
|
+
/**
|
|
15
|
+
* v0.9.4 #1 — cold start measurement. iOS:
|
|
16
|
+
* `mach_absolute_time` from `applicationDidFinishLaunching` to first
|
|
17
|
+
* JS bridge ready. Android: `Process.getStartElapsedRealtime()`.
|
|
18
|
+
* Returns null when native side hasn't captured yet.
|
|
19
|
+
*/
|
|
20
|
+
getColdStartMs?: () => null | number
|
|
21
|
+
/**
|
|
22
|
+
* v0.9.4 #1 — call once at JS init() to finalize the cold-start
|
|
23
|
+
* measurement. iOS subtracts from the app-delegate anchor;
|
|
24
|
+
* Android uses Process.getStartElapsedRealtime() so the call is
|
|
25
|
+
* idempotent if missed.
|
|
26
|
+
*/
|
|
27
|
+
markJsBridgeReady?: () => void
|
|
28
|
+
/**
|
|
29
|
+
* v0.9.4 #1 — slow/frozen frame counters since the most recent
|
|
30
|
+
* navigation transition. Native side hooks `CADisplayLink` (iOS)
|
|
31
|
+
* / `Choreographer.FrameCallback` (Android). Frame > 16.67ms =
|
|
32
|
+
* slow; > 700ms = frozen.
|
|
33
|
+
*/
|
|
34
|
+
getFrameCounters?: () => null | { frozen: number; slow: number }
|
|
35
|
+
/** Reset counters on navigation transition (called by useTraceNavigation). */
|
|
36
|
+
resetFrameCounters?: () => void
|
|
14
37
|
/**
|
|
15
38
|
* v0.7.3 — JS-triggered screenshot with consumer-supplied mask IDs.
|
|
16
39
|
* `maskedIds` are RN `nativeID` strings; native walks the view
|
|
@@ -127,6 +150,43 @@ export function stopAnrWatchdog(): void {
|
|
|
127
150
|
}
|
|
128
151
|
}
|
|
129
152
|
|
|
153
|
+
/** v0.9.4 #1 — finalize cold-start measurement. Idempotent. */
|
|
154
|
+
export function markNativeJsBridgeReady(): void {
|
|
155
|
+
try {
|
|
156
|
+
native()?.markJsBridgeReady?.()
|
|
157
|
+
} catch {
|
|
158
|
+
// ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** v0.9.4 #1 — read cold start ms once. null when native unavailable. */
|
|
163
|
+
export function getNativeColdStartMs(): null | number {
|
|
164
|
+
try {
|
|
165
|
+
const v = native()?.getColdStartMs?.()
|
|
166
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null
|
|
167
|
+
} catch {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** v0.9.4 #1 — read slow/frozen frame counters since last reset. */
|
|
173
|
+
export function getNativeFrameCounters(): null | { frozen: number; slow: number } {
|
|
174
|
+
try {
|
|
175
|
+
return native()?.getFrameCounters?.() ?? null
|
|
176
|
+
} catch {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** v0.9.4 #1 — reset frame counters on navigation transition. */
|
|
182
|
+
export function resetNativeFrameCounters(): void {
|
|
183
|
+
try {
|
|
184
|
+
native()?.resetFrameCounters?.()
|
|
185
|
+
} catch {
|
|
186
|
+
// ignore
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
130
190
|
/**
|
|
131
191
|
* v0.7.3 — drives the native screenshot path. JS side passes the
|
|
132
192
|
* current list of mask `nativeID`s (read from the consumer's
|
package/src/navigation.ts
CHANGED
|
@@ -24,6 +24,10 @@ import { useEffect, useRef } from 'react';
|
|
|
24
24
|
|
|
25
25
|
import { setActiveSpan, startSpan, type SpanHandle } from '@goliapkg/sentori-core';
|
|
26
26
|
|
|
27
|
+
import {
|
|
28
|
+
getNativeFrameCounters,
|
|
29
|
+
resetNativeFrameCounters,
|
|
30
|
+
} from './native';
|
|
27
31
|
import { captureStep } from './trail';
|
|
28
32
|
|
|
29
33
|
/** Minimal contract: anything with `addListener('state', cb)` and
|
|
@@ -101,11 +105,21 @@ export function useTraceNavigation(navigationRef: NavigationRefLike): void {
|
|
|
101
105
|
const enteredAt = lastRouteEnteredAtRef.current;
|
|
102
106
|
if (!span) return null;
|
|
103
107
|
const dwellMs = enteredAt !== null ? Math.max(0, Date.now() - enteredAt) : null;
|
|
108
|
+
// v0.9.4 #1 — drain native frame counters at screen-leave.
|
|
109
|
+
// Empty/null when native module not linked; tags omitted.
|
|
110
|
+
const fc = getNativeFrameCounters();
|
|
111
|
+
const finishTags: Record<string, string> = {};
|
|
112
|
+
if (dwellMs !== null) finishTags['nav.dwell_ms'] = String(dwellMs);
|
|
113
|
+
if (fc) {
|
|
114
|
+
finishTags['vital.slow_frames'] = String(fc.slow);
|
|
115
|
+
finishTags['vital.frozen_frames'] = String(fc.frozen);
|
|
116
|
+
}
|
|
104
117
|
span.finish({
|
|
105
118
|
status: 'ok',
|
|
106
|
-
|
|
107
|
-
tags: dwellMs !== null ? { 'nav.dwell_ms': String(dwellMs) } : undefined,
|
|
119
|
+
tags: Object.keys(finishTags).length > 0 ? finishTags : undefined,
|
|
108
120
|
});
|
|
121
|
+
// Reset counters for the next screen.
|
|
122
|
+
resetNativeFrameCounters();
|
|
109
123
|
return dwellMs;
|
|
110
124
|
};
|
|
111
125
|
|
package/src/netinfo.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import type { Device } from '@goliapkg/sentori-core';
|
|
18
18
|
|
|
19
|
+
import { isNativeModuleLinked } from './native-loader';
|
|
20
|
+
|
|
19
21
|
type NetworkType = Device['networkType'];
|
|
20
22
|
|
|
21
23
|
type NetInfoState = {
|
|
@@ -55,20 +57,11 @@ export function startNetworkTypeWatch(): void {
|
|
|
55
57
|
if (_started) return;
|
|
56
58
|
_started = true;
|
|
57
59
|
try {
|
|
58
|
-
// v0.8.4 hotfix
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
// emitter — past where this try/catch can reach. Gate on the
|
|
64
|
-
// NativeModules entry first so we no-op cleanly in that case.
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
|
-
const RN = require('react-native') as {
|
|
67
|
-
NativeModules?: Record<string, unknown>;
|
|
68
|
-
};
|
|
69
|
-
const native = RN.NativeModules?.RNCNetInfo;
|
|
70
|
-
if (native == null) {
|
|
71
|
-
// JS package present but native module isn't linked. Skip.
|
|
60
|
+
// v0.8.4 hotfix → v0.8.5 generalised via isNativeModuleLinked.
|
|
61
|
+
// host may have the JS package via hoisted node_modules but
|
|
62
|
+
// never ran pod install / prebuild → calling addEventListener
|
|
63
|
+
// crashes deep inside the lib's emitter past try/catch reach.
|
|
64
|
+
if (!isNativeModuleLinked('RNCNetInfo')) {
|
|
72
65
|
return;
|
|
73
66
|
}
|
|
74
67
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
package/src/transport.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { drainSpans } from '@goliapkg/sentori-core';
|
|
2
2
|
|
|
3
3
|
import { getConfig } from './config';
|
|
4
|
+
import { isAnyNativeModuleLinked } from './native-loader';
|
|
4
5
|
import type { Event } from './types';
|
|
5
6
|
|
|
6
7
|
const FLUSH_INTERVAL_MS = 5_000;
|
|
@@ -166,6 +167,11 @@ type AsyncStorageLike = {
|
|
|
166
167
|
};
|
|
167
168
|
|
|
168
169
|
const getAsyncStorage = async (): Promise<AsyncStorageLike | null> => {
|
|
170
|
+
// v0.8.5 — host may have the JS package without pod install /
|
|
171
|
+
// prebuild → getItem crashes from a microtask outside our reach.
|
|
172
|
+
if (!isAnyNativeModuleLinked(['RNCAsyncStorage', 'AsyncStorageModule'])) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
169
175
|
try {
|
|
170
176
|
const mod = (await import(
|
|
171
177
|
'@react-native-async-storage/async-storage'
|