@goliapkg/sentori-react-native 0.8.1 → 0.8.2
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/lib/init.d.ts +8 -0
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +9 -0
- package/lib/init.js.map +1 -1
- package/lib/pre-crash-sentinel.d.ts +12 -0
- package/lib/pre-crash-sentinel.d.ts.map +1 -0
- package/lib/pre-crash-sentinel.js +116 -0
- package/lib/pre-crash-sentinel.js.map +1 -0
- package/lib/sentinel-context.d.ts +4 -0
- package/lib/sentinel-context.d.ts.map +1 -0
- package/lib/sentinel-context.js +52 -0
- package/lib/sentinel-context.js.map +1 -0
- package/package.json +2 -2
- package/src/init.ts +16 -0
- package/src/pre-crash-sentinel.ts +140 -0
- package/src/sentinel-context.ts +60 -0
package/lib/init.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PreCrashChannel } from './pre-crash-sentinel';
|
|
1
2
|
import type { AttachmentMeta } from './types';
|
|
2
3
|
export type InitOptions = {
|
|
3
4
|
/** Project token starting with `st_pk_`. Required. */
|
|
@@ -36,6 +37,13 @@ export type InitOptions = {
|
|
|
36
37
|
* the buffer is sealed and uploaded as a `sessionTrail`
|
|
37
38
|
* attachment. Defaults to false. */
|
|
38
39
|
sessionTrail?: boolean;
|
|
40
|
+
/** v0.9.1 +S4 — pre-crash sentinel. Subscribes to JS-thread
|
|
41
|
+
* frame timing; when ≥ 50% of a 60-frame window misses the
|
|
42
|
+
* budget (default 32 ms / < 30 fps), emits a `kind: nearCrash`
|
|
43
|
+
* event proactively so dashboards see the "about-to-die"
|
|
44
|
+
* signal before an actual crash. */
|
|
45
|
+
preCrashSentinel?: boolean;
|
|
46
|
+
sentinelChannels?: PreCrashChannel[];
|
|
39
47
|
/** v0.9.0 #3 — launch-crash loop guard. When two consecutive
|
|
40
48
|
* launches don't reach `markLaunchCompleted()` (typical of an
|
|
41
49
|
* OTA update with a fatal bug), invoke the host callback with
|
package/lib/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAaA,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQnF,OAAO,KAAK,EAAkB,cAAc,EAA2B,MAAM,SAAS,CAAC;AAIvF,MAAM,MAAM,WAAW,GAAG;IACxB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,OAAO,CAAC,EACJ,OAAO,GACP;YACE;;qEAEyD;YACzD,OAAO,CAAC,EAAE,OAAO,CAAC;SACnB,CAAC;QACN;;8DAEsD;QACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB;;;;;;;uBAOe;QACf,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB;;;6CAGqC;QACrC,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB;;;;6CAIqC;QACrC,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;QACrC;;;sEAG8D;QAC9D,gBAAgB,CAAC,EAAE;YACjB,OAAO,EAAE,OAAO,CAAC;YACjB,qBAAqB,CAAC,EAAE,CACtB,IAAI,EAAE,OAAO,sBAAsB,EAAE,eAAe,KAElD,OAAO,sBAAsB,EAAE,iBAAiB,GAChD,OAAO,CAAC,OAAO,sBAAsB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;YACnB,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH,CAAC;IACF;;;;;gEAK4D;IAC5D,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;KACxB,CAAC;CACH,CAAC;AAIF,eAAO,MAAM,IAAI,GAAI,SAAS,WAAW,KAAG,IA4H3C,CAAC;AAiBF,YAAY,EAAE,cAAc,EAAE,CAAC"}
|
package/lib/init.js
CHANGED
|
@@ -8,6 +8,7 @@ import { markLaunchCompleted, runLaunchCrashGuard, } from './launch-crash-guard'
|
|
|
8
8
|
import { startMetricsTimer } from './metrics';
|
|
9
9
|
import { drainNativePending, setNativeConfig } from './native';
|
|
10
10
|
import { startNetworkTypeWatch } from './netinfo';
|
|
11
|
+
import { startPreCrashSentinel } from './pre-crash-sentinel';
|
|
11
12
|
import { startSession } from './session-tracker';
|
|
12
13
|
import { drainOfflineQueue, enqueue, startTransport, uploadAttachment, } from './transport';
|
|
13
14
|
const DEFAULT_INGEST_URL = 'https://ingest.sentori.golia.jp';
|
|
@@ -53,6 +54,14 @@ export const init = (options) => {
|
|
|
53
54
|
startNetworkTypeWatch();
|
|
54
55
|
// v0.8.3 — drain custom-metric ring every 30 s.
|
|
55
56
|
startMetricsTimer();
|
|
57
|
+
// v0.9.1 +S4 — pre-crash sentinel. Off by default; opt-in via
|
|
58
|
+
// `capture.preCrashSentinel: true`.
|
|
59
|
+
if (options.capture?.preCrashSentinel === true) {
|
|
60
|
+
startPreCrashSentinel({
|
|
61
|
+
enabled: true,
|
|
62
|
+
channels: options.capture.sentinelChannels,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
56
65
|
const capture = options.capture ?? {};
|
|
57
66
|
if (capture.globalErrors !== false)
|
|
58
67
|
installGlobalHandler();
|
package/lib/init.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EACL,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,gBAAgB,GACjB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type PreCrashChannel = 'frame-budget-overrun' | 'memory-pressure' | 'oom-warning' | 'storage-low';
|
|
2
|
+
export type PreCrashSentinelOptions = {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
channels?: PreCrashChannel[];
|
|
5
|
+
/** Lower → more sensitive. Default 32 ms (< 30 fps). */
|
|
6
|
+
frameBudgetMs?: number;
|
|
7
|
+
/** Fraction of frames in the window that must miss budget. Default 0.5. */
|
|
8
|
+
tripRatio?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function startPreCrashSentinel(opts: PreCrashSentinelOptions): void;
|
|
11
|
+
export declare function stopPreCrashSentinel(): void;
|
|
12
|
+
//# sourceMappingURL=pre-crash-sentinel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-crash-sentinel.d.ts","sourceRoot":"","sources":["../src/pre-crash-sentinel.ts"],"names":[],"mappings":"AAmCA,MAAM,MAAM,eAAe,GACvB,sBAAsB,GACtB,iBAAiB,GACjB,aAAa,GACb,aAAa,CAAC;AAElB,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;IAC7B,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,uBAAuB,GAAG,IAAI,CAUzE;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAK3C"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// v0.9.1 +S4 — pre-crash sentinel.
|
|
2
|
+
//
|
|
3
|
+
// Predictive (vs reactive) telemetry. Subscribes to JS-thread frame
|
|
4
|
+
// timing via requestAnimationFrame and, when a rolling 60-frame
|
|
5
|
+
// window has ≥ 50% of frames slower than 32 ms (i.e. < 30 fps for
|
|
6
|
+
// half the window), emits an `event.kind = nearCrash` to the server.
|
|
7
|
+
// Backend stores it in the same events stream so the dashboard shows
|
|
8
|
+
// "X user sessions had near-crash signals 4 minutes before the
|
|
9
|
+
// actual NSException".
|
|
10
|
+
//
|
|
11
|
+
// Memory pressure / OOM / storage low signals will need native
|
|
12
|
+
// system observers and ship in v1.0. v0.9.1 covers the most common
|
|
13
|
+
// runaway-render-loop case purely from JS.
|
|
14
|
+
import { startSpan } from '@goliapkg/sentori-core';
|
|
15
|
+
import { getBundleInfo } from './bundle-info';
|
|
16
|
+
import { collectDeviceForSentinel, getAppForSentinel } from './sentinel-context';
|
|
17
|
+
import { getConfig, isInitialized } from './config';
|
|
18
|
+
import { enqueue } from './transport';
|
|
19
|
+
import { uuidV7 } from './uuid';
|
|
20
|
+
const FRAME_BUDGET_MS = 32; // < 30 fps
|
|
21
|
+
const WINDOW_FRAMES = 60; // ~1 s at 60 fps
|
|
22
|
+
const TRIP_RATIO = 0.5;
|
|
23
|
+
const COOLDOWN_MS = 60_000; // don't spam: one nearCrash event per minute
|
|
24
|
+
let _running = false;
|
|
25
|
+
let _lastFrameAt = 0;
|
|
26
|
+
let _slowFrames = 0;
|
|
27
|
+
let _totalFrames = 0;
|
|
28
|
+
let _lastEmitAt = 0;
|
|
29
|
+
let _channels = new Set();
|
|
30
|
+
export function startPreCrashSentinel(opts) {
|
|
31
|
+
if (!opts.enabled || _running)
|
|
32
|
+
return;
|
|
33
|
+
_running = true;
|
|
34
|
+
_channels = new Set(opts.channels ?? ['frame-budget-overrun']);
|
|
35
|
+
if (_channels.has('frame-budget-overrun')) {
|
|
36
|
+
startFrameBudgetWatch(opts.frameBudgetMs ?? FRAME_BUDGET_MS, opts.tripRatio ?? TRIP_RATIO);
|
|
37
|
+
}
|
|
38
|
+
// Native channels (memory-pressure, oom-warning, storage-low) hook
|
|
39
|
+
// through a TODO native module in v1.0.
|
|
40
|
+
}
|
|
41
|
+
export function stopPreCrashSentinel() {
|
|
42
|
+
_running = false;
|
|
43
|
+
_slowFrames = 0;
|
|
44
|
+
_totalFrames = 0;
|
|
45
|
+
_channels.clear();
|
|
46
|
+
}
|
|
47
|
+
function startFrameBudgetWatch(budgetMs, tripRatio) {
|
|
48
|
+
if (typeof requestAnimationFrame !== 'function')
|
|
49
|
+
return;
|
|
50
|
+
_lastFrameAt = Date.now();
|
|
51
|
+
function tick() {
|
|
52
|
+
if (!_running)
|
|
53
|
+
return;
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const delta = now - _lastFrameAt;
|
|
56
|
+
_lastFrameAt = now;
|
|
57
|
+
if (delta >= budgetMs)
|
|
58
|
+
_slowFrames++;
|
|
59
|
+
_totalFrames++;
|
|
60
|
+
if (_totalFrames >= WINDOW_FRAMES) {
|
|
61
|
+
const ratio = _slowFrames / _totalFrames;
|
|
62
|
+
if (ratio >= tripRatio && now - _lastEmitAt > COOLDOWN_MS) {
|
|
63
|
+
_lastEmitAt = now;
|
|
64
|
+
emitNearCrash({
|
|
65
|
+
slowFrames: _slowFrames,
|
|
66
|
+
totalFrames: _totalFrames,
|
|
67
|
+
ratio,
|
|
68
|
+
windowMs: WINDOW_FRAMES * (budgetMs / 2), // approximate
|
|
69
|
+
channel: 'frame-budget-overrun',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
_slowFrames = 0;
|
|
73
|
+
_totalFrames = 0;
|
|
74
|
+
}
|
|
75
|
+
requestAnimationFrame(tick);
|
|
76
|
+
}
|
|
77
|
+
requestAnimationFrame(tick);
|
|
78
|
+
}
|
|
79
|
+
function emitNearCrash(data) {
|
|
80
|
+
if (!isInitialized())
|
|
81
|
+
return;
|
|
82
|
+
const config = getConfig();
|
|
83
|
+
if (!config)
|
|
84
|
+
return;
|
|
85
|
+
const span = startSpan('sentori.nearCrash', {
|
|
86
|
+
name: data.channel,
|
|
87
|
+
tags: {
|
|
88
|
+
'nearCrash.channel': data.channel,
|
|
89
|
+
'nearCrash.ratio': data.ratio.toFixed(3),
|
|
90
|
+
'nearCrash.slow_frames': String(data.slowFrames),
|
|
91
|
+
'nearCrash.total_frames': String(data.totalFrames),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
span.finish({ status: 'ok' });
|
|
95
|
+
const event = {
|
|
96
|
+
id: uuidV7(),
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
kind: 'nearCrash',
|
|
99
|
+
platform: 'javascript',
|
|
100
|
+
release: config.release,
|
|
101
|
+
environment: config.environment,
|
|
102
|
+
device: collectDeviceForSentinel(),
|
|
103
|
+
app: getAppForSentinel(config.release),
|
|
104
|
+
...(getBundleInfo() ? { bundle: getBundleInfo() } : {}),
|
|
105
|
+
error: {
|
|
106
|
+
type: 'NearCrash',
|
|
107
|
+
message: `frame budget overrun: ${(data.ratio * 100).toFixed(0)}% of last ${data.totalFrames} frames slow`,
|
|
108
|
+
stack: [],
|
|
109
|
+
},
|
|
110
|
+
tags: {
|
|
111
|
+
'nearCrash.channel': data.channel,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
enqueue(event);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=pre-crash-sentinel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-crash-sentinel.js","sourceRoot":"","sources":["../src/pre-crash-sentinel.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinel-context.d.ts","sourceRoot":"","sources":["../src/sentinel-context.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE3C,eAAO,MAAM,wBAAwB,QAAO,MAgC3C,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,GAkBnD,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Shared device + app collectors. Used by both capture.ts (errors)
|
|
2
|
+
// and pre-crash-sentinel.ts (nearCrash). Same shape so the dashboard
|
|
3
|
+
// can render both kinds of events through the same UI components.
|
|
4
|
+
import { getCachedNetworkType } from './netinfo';
|
|
5
|
+
export const collectDeviceForSentinel = () => {
|
|
6
|
+
let os = 'other';
|
|
7
|
+
let osVersion = '0';
|
|
8
|
+
let locale;
|
|
9
|
+
const networkType = getCachedNetworkType();
|
|
10
|
+
try {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
12
|
+
const RN = require('react-native');
|
|
13
|
+
const rnOS = RN.Platform.OS;
|
|
14
|
+
os = rnOS === 'android' || rnOS === 'ios' || rnOS === 'web' ? rnOS : 'other';
|
|
15
|
+
osVersion = String(RN.Platform.Version);
|
|
16
|
+
if (rnOS === 'ios') {
|
|
17
|
+
const s = RN.NativeModules.SettingsManager?.settings;
|
|
18
|
+
locale = s?.AppleLocale ?? s?.AppleLanguages?.[0];
|
|
19
|
+
}
|
|
20
|
+
else if (rnOS === 'android') {
|
|
21
|
+
locale = RN.NativeModules.I18nManager?.localeIdentifier;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// not in RN runtime
|
|
26
|
+
}
|
|
27
|
+
const device = { os, osVersion };
|
|
28
|
+
if (locale)
|
|
29
|
+
device.locale = locale;
|
|
30
|
+
if (networkType)
|
|
31
|
+
device.networkType = networkType;
|
|
32
|
+
return device;
|
|
33
|
+
};
|
|
34
|
+
export const getAppForSentinel = (release) => {
|
|
35
|
+
const m = /^(?:[^@]+@)?([^+]+)(?:\+(.+))?$/.exec(release);
|
|
36
|
+
const version = m?.[1] ?? '0.0.0';
|
|
37
|
+
const build = m?.[2];
|
|
38
|
+
let rnVersion = 'unknown';
|
|
39
|
+
try {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
rnVersion = require('react-native/package.json').version;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// not in RN runtime
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
build,
|
|
48
|
+
framework: { name: 'react-native', version: rnVersion },
|
|
49
|
+
version,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=sentinel-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinel-context.js","sourceRoot":"","sources":["../src/sentinel-context.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,qEAAqE;AACrE,kEAAkE;AAElE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAGjD,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAW,EAAE;IACnD,IAAI,EAAE,GAAiB,OAAO,CAAC;IAC/B,IAAI,SAAS,GAAG,GAAG,CAAC;IACpB,IAAI,MAA0B,CAAC;IAC/B,MAAM,WAAW,GAAG,oBAAoB,EAAE,CAAC;IAC3C,IAAI,CAAC;QACH,iEAAiE;QACjE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAQhC,CAAC;QACF,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,EAAE,GAAG,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;QAC7E,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;YACnB,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,eAAe,EAAE,QAAQ,CAAC;YACrD,MAAM,GAAG,CAAC,EAAE,WAAW,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC;aAAM,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,gBAAgB,CAAC;QAC1D,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oBAAoB;IACtB,CAAC;IACD,MAAM,MAAM,GAAW,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC;IACzC,IAAI,MAAM;QAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACnC,IAAI,WAAW;QAAE,MAAM,CAAC,WAAW,GAAG,WAAW,CAAC;IAClD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAe,EAAO,EAAE;IACxD,MAAM,CAAC,GAAG,iCAAiC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC;IAClC,MAAM,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAErB,IAAI,SAAS,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC;QACH,iEAAiE;QACjE,SAAS,GAAI,OAAO,CAAC,2BAA2B,CAAyB,CAAC,OAAO,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,oBAAoB;IACtB,CAAC;IAED,OAAO;QACL,KAAK;QACL,SAAS,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE;QACvD,OAAO;KACR,CAAC;AACJ,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-react-native",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://sentori.golia.jp",
|
|
@@ -80,6 +80,6 @@
|
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@goliapkg/sentori-core": "0.8.
|
|
83
|
+
"@goliapkg/sentori-core": "0.8.1"
|
|
84
84
|
}
|
|
85
85
|
}
|
package/src/init.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { startMetricsTimer } from './metrics';
|
|
12
12
|
import { drainNativePending, setNativeConfig } from './native';
|
|
13
13
|
import { startNetworkTypeWatch } from './netinfo';
|
|
14
|
+
import { startPreCrashSentinel, type PreCrashChannel } from './pre-crash-sentinel';
|
|
14
15
|
import { startSession } from './session-tracker';
|
|
15
16
|
import {
|
|
16
17
|
drainOfflineQueue,
|
|
@@ -61,6 +62,13 @@ export type InitOptions = {
|
|
|
61
62
|
* the buffer is sealed and uploaded as a `sessionTrail`
|
|
62
63
|
* attachment. Defaults to false. */
|
|
63
64
|
sessionTrail?: boolean;
|
|
65
|
+
/** v0.9.1 +S4 — pre-crash sentinel. Subscribes to JS-thread
|
|
66
|
+
* frame timing; when ≥ 50% of a 60-frame window misses the
|
|
67
|
+
* budget (default 32 ms / < 30 fps), emits a `kind: nearCrash`
|
|
68
|
+
* event proactively so dashboards see the "about-to-die"
|
|
69
|
+
* signal before an actual crash. */
|
|
70
|
+
preCrashSentinel?: boolean;
|
|
71
|
+
sentinelChannels?: PreCrashChannel[];
|
|
64
72
|
/** v0.9.0 #3 — launch-crash loop guard. When two consecutive
|
|
65
73
|
* launches don't reach `markLaunchCompleted()` (typical of an
|
|
66
74
|
* OTA update with a fatal bug), invoke the host callback with
|
|
@@ -142,6 +150,14 @@ export const init = (options: InitOptions): void => {
|
|
|
142
150
|
startNetworkTypeWatch();
|
|
143
151
|
// v0.8.3 — drain custom-metric ring every 30 s.
|
|
144
152
|
startMetricsTimer();
|
|
153
|
+
// v0.9.1 +S4 — pre-crash sentinel. Off by default; opt-in via
|
|
154
|
+
// `capture.preCrashSentinel: true`.
|
|
155
|
+
if (options.capture?.preCrashSentinel === true) {
|
|
156
|
+
startPreCrashSentinel({
|
|
157
|
+
enabled: true,
|
|
158
|
+
channels: options.capture.sentinelChannels,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
145
161
|
|
|
146
162
|
const capture = options.capture ?? {};
|
|
147
163
|
if (capture.globalErrors !== false) installGlobalHandler();
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// v0.9.1 +S4 — pre-crash sentinel.
|
|
2
|
+
//
|
|
3
|
+
// Predictive (vs reactive) telemetry. Subscribes to JS-thread frame
|
|
4
|
+
// timing via requestAnimationFrame and, when a rolling 60-frame
|
|
5
|
+
// window has ≥ 50% of frames slower than 32 ms (i.e. < 30 fps for
|
|
6
|
+
// half the window), emits an `event.kind = nearCrash` to the server.
|
|
7
|
+
// Backend stores it in the same events stream so the dashboard shows
|
|
8
|
+
// "X user sessions had near-crash signals 4 minutes before the
|
|
9
|
+
// actual NSException".
|
|
10
|
+
//
|
|
11
|
+
// Memory pressure / OOM / storage low signals will need native
|
|
12
|
+
// system observers and ship in v1.0. v0.9.1 covers the most common
|
|
13
|
+
// runaway-render-loop case purely from JS.
|
|
14
|
+
|
|
15
|
+
import { startSpan } from '@goliapkg/sentori-core';
|
|
16
|
+
|
|
17
|
+
import { getBundleInfo } from './bundle-info';
|
|
18
|
+
import { collectDeviceForSentinel, getAppForSentinel } from './sentinel-context';
|
|
19
|
+
import { getConfig, isInitialized } from './config';
|
|
20
|
+
import { enqueue } from './transport';
|
|
21
|
+
import { uuidV7 } from './uuid';
|
|
22
|
+
import type { Event } from './types';
|
|
23
|
+
|
|
24
|
+
const FRAME_BUDGET_MS = 32; // < 30 fps
|
|
25
|
+
const WINDOW_FRAMES = 60; // ~1 s at 60 fps
|
|
26
|
+
const TRIP_RATIO = 0.5;
|
|
27
|
+
const COOLDOWN_MS = 60_000; // don't spam: one nearCrash event per minute
|
|
28
|
+
|
|
29
|
+
let _running = false;
|
|
30
|
+
let _lastFrameAt = 0;
|
|
31
|
+
let _slowFrames = 0;
|
|
32
|
+
let _totalFrames = 0;
|
|
33
|
+
let _lastEmitAt = 0;
|
|
34
|
+
let _channels: Set<string> = new Set();
|
|
35
|
+
|
|
36
|
+
export type PreCrashChannel =
|
|
37
|
+
| 'frame-budget-overrun'
|
|
38
|
+
| 'memory-pressure' // native, v1.0
|
|
39
|
+
| 'oom-warning' // native, v1.0
|
|
40
|
+
| 'storage-low'; // native, v1.0
|
|
41
|
+
|
|
42
|
+
export type PreCrashSentinelOptions = {
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
channels?: PreCrashChannel[];
|
|
45
|
+
/** Lower → more sensitive. Default 32 ms (< 30 fps). */
|
|
46
|
+
frameBudgetMs?: number;
|
|
47
|
+
/** Fraction of frames in the window that must miss budget. Default 0.5. */
|
|
48
|
+
tripRatio?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function startPreCrashSentinel(opts: PreCrashSentinelOptions): void {
|
|
52
|
+
if (!opts.enabled || _running) return;
|
|
53
|
+
_running = true;
|
|
54
|
+
_channels = new Set(opts.channels ?? ['frame-budget-overrun']);
|
|
55
|
+
|
|
56
|
+
if (_channels.has('frame-budget-overrun')) {
|
|
57
|
+
startFrameBudgetWatch(opts.frameBudgetMs ?? FRAME_BUDGET_MS, opts.tripRatio ?? TRIP_RATIO);
|
|
58
|
+
}
|
|
59
|
+
// Native channels (memory-pressure, oom-warning, storage-low) hook
|
|
60
|
+
// through a TODO native module in v1.0.
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function stopPreCrashSentinel(): void {
|
|
64
|
+
_running = false;
|
|
65
|
+
_slowFrames = 0;
|
|
66
|
+
_totalFrames = 0;
|
|
67
|
+
_channels.clear();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function startFrameBudgetWatch(budgetMs: number, tripRatio: number): void {
|
|
71
|
+
if (typeof requestAnimationFrame !== 'function') return;
|
|
72
|
+
_lastFrameAt = Date.now();
|
|
73
|
+
function tick() {
|
|
74
|
+
if (!_running) return;
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const delta = now - _lastFrameAt;
|
|
77
|
+
_lastFrameAt = now;
|
|
78
|
+
if (delta >= budgetMs) _slowFrames++;
|
|
79
|
+
_totalFrames++;
|
|
80
|
+
if (_totalFrames >= WINDOW_FRAMES) {
|
|
81
|
+
const ratio = _slowFrames / _totalFrames;
|
|
82
|
+
if (ratio >= tripRatio && now - _lastEmitAt > COOLDOWN_MS) {
|
|
83
|
+
_lastEmitAt = now;
|
|
84
|
+
emitNearCrash({
|
|
85
|
+
slowFrames: _slowFrames,
|
|
86
|
+
totalFrames: _totalFrames,
|
|
87
|
+
ratio,
|
|
88
|
+
windowMs: WINDOW_FRAMES * (budgetMs / 2), // approximate
|
|
89
|
+
channel: 'frame-budget-overrun',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
_slowFrames = 0;
|
|
93
|
+
_totalFrames = 0;
|
|
94
|
+
}
|
|
95
|
+
requestAnimationFrame(tick);
|
|
96
|
+
}
|
|
97
|
+
requestAnimationFrame(tick);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function emitNearCrash(data: {
|
|
101
|
+
slowFrames: number;
|
|
102
|
+
totalFrames: number;
|
|
103
|
+
ratio: number;
|
|
104
|
+
windowMs: number;
|
|
105
|
+
channel: PreCrashChannel;
|
|
106
|
+
}): void {
|
|
107
|
+
if (!isInitialized()) return;
|
|
108
|
+
const config = getConfig();
|
|
109
|
+
if (!config) return;
|
|
110
|
+
const span = startSpan('sentori.nearCrash', {
|
|
111
|
+
name: data.channel,
|
|
112
|
+
tags: {
|
|
113
|
+
'nearCrash.channel': data.channel,
|
|
114
|
+
'nearCrash.ratio': data.ratio.toFixed(3),
|
|
115
|
+
'nearCrash.slow_frames': String(data.slowFrames),
|
|
116
|
+
'nearCrash.total_frames': String(data.totalFrames),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
span.finish({ status: 'ok' });
|
|
120
|
+
const event: Event = {
|
|
121
|
+
id: uuidV7(),
|
|
122
|
+
timestamp: new Date().toISOString(),
|
|
123
|
+
kind: 'nearCrash',
|
|
124
|
+
platform: 'javascript',
|
|
125
|
+
release: config.release,
|
|
126
|
+
environment: config.environment,
|
|
127
|
+
device: collectDeviceForSentinel(),
|
|
128
|
+
app: getAppForSentinel(config.release),
|
|
129
|
+
...(getBundleInfo() ? { bundle: getBundleInfo() as { id: string } } : {}),
|
|
130
|
+
error: {
|
|
131
|
+
type: 'NearCrash',
|
|
132
|
+
message: `frame budget overrun: ${(data.ratio * 100).toFixed(0)}% of last ${data.totalFrames} frames slow`,
|
|
133
|
+
stack: [],
|
|
134
|
+
},
|
|
135
|
+
tags: {
|
|
136
|
+
'nearCrash.channel': data.channel,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
enqueue(event);
|
|
140
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Shared device + app collectors. Used by both capture.ts (errors)
|
|
2
|
+
// and pre-crash-sentinel.ts (nearCrash). Same shape so the dashboard
|
|
3
|
+
// can render both kinds of events through the same UI components.
|
|
4
|
+
|
|
5
|
+
import { getCachedNetworkType } from './netinfo';
|
|
6
|
+
import type { App, Device } from './types';
|
|
7
|
+
|
|
8
|
+
export const collectDeviceForSentinel = (): Device => {
|
|
9
|
+
let os: Device['os'] = 'other';
|
|
10
|
+
let osVersion = '0';
|
|
11
|
+
let locale: string | undefined;
|
|
12
|
+
const networkType = getCachedNetworkType();
|
|
13
|
+
try {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
|
+
const RN = require('react-native') as {
|
|
16
|
+
NativeModules: {
|
|
17
|
+
I18nManager?: { localeIdentifier?: string };
|
|
18
|
+
SettingsManager?: {
|
|
19
|
+
settings?: { AppleLanguages?: string[]; AppleLocale?: string };
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
Platform: { OS: string; Version: number | string };
|
|
23
|
+
};
|
|
24
|
+
const rnOS = RN.Platform.OS;
|
|
25
|
+
os = rnOS === 'android' || rnOS === 'ios' || rnOS === 'web' ? rnOS : 'other';
|
|
26
|
+
osVersion = String(RN.Platform.Version);
|
|
27
|
+
if (rnOS === 'ios') {
|
|
28
|
+
const s = RN.NativeModules.SettingsManager?.settings;
|
|
29
|
+
locale = s?.AppleLocale ?? s?.AppleLanguages?.[0];
|
|
30
|
+
} else if (rnOS === 'android') {
|
|
31
|
+
locale = RN.NativeModules.I18nManager?.localeIdentifier;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// not in RN runtime
|
|
35
|
+
}
|
|
36
|
+
const device: Device = { os, osVersion };
|
|
37
|
+
if (locale) device.locale = locale;
|
|
38
|
+
if (networkType) device.networkType = networkType;
|
|
39
|
+
return device;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const getAppForSentinel = (release: string): App => {
|
|
43
|
+
const m = /^(?:[^@]+@)?([^+]+)(?:\+(.+))?$/.exec(release);
|
|
44
|
+
const version = m?.[1] ?? '0.0.0';
|
|
45
|
+
const build = m?.[2];
|
|
46
|
+
|
|
47
|
+
let rnVersion = 'unknown';
|
|
48
|
+
try {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
rnVersion = (require('react-native/package.json') as { version: string }).version;
|
|
51
|
+
} catch {
|
|
52
|
+
// not in RN runtime
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
build,
|
|
57
|
+
framework: { name: 'react-native', version: rnVersion },
|
|
58
|
+
version,
|
|
59
|
+
};
|
|
60
|
+
};
|