@goliapkg/sentori-react-native 0.6.0 → 0.7.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/SentoriAnrWatchdog.kt +46 -0
- package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +53 -0
- package/android/src/main/java/com/sentori/SentoriScreenshotCapture.kt +271 -0
- package/android/src/test/java/com/sentori/SentoriScreenshotCaptureTest.kt +93 -0
- package/ios/SentoriCrashHandler.swift +33 -1
- package/ios/SentoriScreenshotCapture.swift +169 -0
- package/ios/Tests/SentoriScreenshotCaptureTests.swift +59 -0
- package/lib/capture.d.ts +1 -0
- package/lib/capture.d.ts.map +1 -1
- package/lib/capture.js +42 -0
- package/lib/capture.js.map +1 -1
- package/lib/config.d.ts +8 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +17 -0
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +23 -3
- package/lib/init.js.map +1 -1
- package/lib/navigation.d.ts.map +1 -1
- package/lib/navigation.js +7 -0
- package/lib/navigation.js.map +1 -1
- package/lib/trail.d.ts +12 -0
- package/lib/trail.d.ts.map +1 -0
- package/lib/trail.js +28 -0
- package/lib/trail.js.map +1 -0
- package/package.json +3 -3
- package/src/capture.ts +51 -0
- package/src/config.ts +8 -0
- package/src/index.ts +9 -2
- package/src/init.ts +67 -4
- package/src/navigation.ts +8 -0
- package/src/trail.ts +32 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sentori SDK for React Native
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|
|
7
7
|
"repository": {
|
|
@@ -69,6 +69,6 @@
|
|
|
69
69
|
"access": "public"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@goliapkg/sentori-core": "0.
|
|
72
|
+
"@goliapkg/sentori-core": "0.6.0"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/capture.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { sealTrail, shouldSample } from '@goliapkg/sentori-core';
|
|
2
|
+
|
|
1
3
|
import { addBreadcrumb, getBreadcrumbs } from './breadcrumbs';
|
|
2
4
|
import { getConfig, isInitialized } from './config';
|
|
3
5
|
import { symbolicateErrorViaMetro } from './handlers/dev-symbolicate';
|
|
4
6
|
import { captureScreenshot } from './handlers/screenshot';
|
|
5
7
|
import { markSessionErrored } from './session-tracker';
|
|
6
8
|
import { parseStack } from './stack';
|
|
9
|
+
import { getTrailBuffer } from './trail';
|
|
7
10
|
import { enqueue, uploadAttachment } from './transport';
|
|
8
11
|
import { uuidV7 } from './uuid';
|
|
9
12
|
import type { App, AttachmentMeta, Device, Event, SentoriError, Tags, User } from './types';
|
|
10
13
|
|
|
14
|
+
export { captureStep, __resetTrailForTests } from './trail';
|
|
15
|
+
|
|
11
16
|
declare const __DEV__: boolean | undefined;
|
|
12
17
|
|
|
13
18
|
let _user: User | null = null;
|
|
@@ -60,6 +65,14 @@ export const captureError = (error: Error, extras?: CaptureExtras): void => {
|
|
|
60
65
|
const config = getConfig();
|
|
61
66
|
if (!config) return;
|
|
62
67
|
|
|
68
|
+
// Phase 44 sub-B: client-side sampling. Skip the whole pipeline
|
|
69
|
+
// (no screenshot capture either) when the sample dice come up
|
|
70
|
+
// wrong. Default rate = null = keep, so existing callers unaffected.
|
|
71
|
+
if (!shouldSample(config.errorSampleRate)) {
|
|
72
|
+
addBreadcrumb({ type: 'custom', data: { reason: 'sampled-out', kind: 'error' } });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
const event: Event = {
|
|
64
77
|
id: uuidV7(),
|
|
65
78
|
timestamp: new Date().toISOString(),
|
|
@@ -97,11 +110,49 @@ export const captureError = (error: Error, extras?: CaptureExtras): void => {
|
|
|
97
110
|
if (wantScreenshot) {
|
|
98
111
|
await captureAndAttachScreenshot(event);
|
|
99
112
|
}
|
|
113
|
+
const trail = getTrailBuffer();
|
|
114
|
+
if (config.sessionTrailEnabled && trail.size() > 0) {
|
|
115
|
+
await captureAndAttachSessionTrail(event);
|
|
116
|
+
}
|
|
100
117
|
enqueue(event);
|
|
101
118
|
};
|
|
102
119
|
void pipeline();
|
|
103
120
|
};
|
|
104
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Phase 46 — seal the trail buffer, upload it as a `sessionTrail`
|
|
124
|
+
* attachment, attach the ref. Best-effort: any failure leaves a
|
|
125
|
+
* breadcrumb and lets the event ship without the trail.
|
|
126
|
+
*
|
|
127
|
+
* The trail is **always cleared** after `captureException`, even if
|
|
128
|
+
* upload fails — we don't want a stale 30-step buffer leaking into
|
|
129
|
+
* the next crash's trail.
|
|
130
|
+
*/
|
|
131
|
+
async function captureAndAttachSessionTrail(event: Event): Promise<void> {
|
|
132
|
+
const trail = getTrailBuffer();
|
|
133
|
+
const payload = sealTrail(trail);
|
|
134
|
+
trail.clear();
|
|
135
|
+
const json = JSON.stringify(payload);
|
|
136
|
+
// base64 the JSON for the `data:` URI multipart bridge (same
|
|
137
|
+
// trick the screenshot path uses).
|
|
138
|
+
const base64 =
|
|
139
|
+
typeof globalThis.btoa === 'function'
|
|
140
|
+
? globalThis.btoa(unescape(encodeURIComponent(json)))
|
|
141
|
+
: Buffer.from(json, 'utf-8').toString('base64');
|
|
142
|
+
const attachment = await uploadAttachment(
|
|
143
|
+
event.id,
|
|
144
|
+
'sessionTrail',
|
|
145
|
+
{ base64, mediaType: 'application/json' },
|
|
146
|
+
{ source: 'js' },
|
|
147
|
+
);
|
|
148
|
+
if (!attachment) {
|
|
149
|
+
addBreadcrumb({ type: 'custom', data: { reason: 'session-trail-upload-failed' } });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!event.attachments) event.attachments = [];
|
|
153
|
+
event.attachments.push(attachment);
|
|
154
|
+
}
|
|
155
|
+
|
|
105
156
|
export const captureException = captureError;
|
|
106
157
|
|
|
107
158
|
/** Phase 42 sub-D.08: per-session screenshot quota gate. */
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,14 @@ export type Config = {
|
|
|
6
6
|
enabled: boolean;
|
|
7
7
|
/** Phase 42 sub-D.07: opt-in screenshot capture on captureException. */
|
|
8
8
|
screenshotsEnabled: boolean;
|
|
9
|
+
/** Phase 44 sub-B: per-event-class sampling rates 0..1.
|
|
10
|
+
* `null` = keep everything (default). */
|
|
11
|
+
errorSampleRate: null | number;
|
|
12
|
+
traceSampleRate: null | number;
|
|
13
|
+
/** Phase 46: when true, every `captureException` seals the
|
|
14
|
+
* session-trail buffer and uploads it as a `sessionTrail`
|
|
15
|
+
* attachment. Defaults to false. */
|
|
16
|
+
sessionTrailEnabled: boolean;
|
|
9
17
|
};
|
|
10
18
|
|
|
11
19
|
let _config: Config | null = null;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { init } from './init';
|
|
2
2
|
import { addBreadcrumb } from './breadcrumbs';
|
|
3
|
-
import { setUser, getUser, captureError, captureException } from './capture';
|
|
3
|
+
import { setUser, getUser, captureError, captureException, captureStep } from './capture';
|
|
4
4
|
import { ErrorBoundary } from './error-boundary';
|
|
5
5
|
import { MaskRegion, setMaskedNode, unsetMaskedNode } from './mask';
|
|
6
6
|
import {
|
|
@@ -16,6 +16,7 @@ export const sentori = {
|
|
|
16
16
|
getUser,
|
|
17
17
|
captureError,
|
|
18
18
|
captureException,
|
|
19
|
+
captureStep,
|
|
19
20
|
ErrorBoundary,
|
|
20
21
|
MaskRegion,
|
|
21
22
|
setMaskedNode,
|
|
@@ -29,7 +30,13 @@ export default sentori;
|
|
|
29
30
|
|
|
30
31
|
export { init, init as initSentori } from './init';
|
|
31
32
|
export { addBreadcrumb } from './breadcrumbs';
|
|
32
|
-
export {
|
|
33
|
+
export {
|
|
34
|
+
captureError,
|
|
35
|
+
captureException,
|
|
36
|
+
captureStep,
|
|
37
|
+
getUser,
|
|
38
|
+
setUser,
|
|
39
|
+
} from './capture';
|
|
33
40
|
export { ErrorBoundary } from './error-boundary';
|
|
34
41
|
export { MaskRegion, setMaskedNode, unsetMaskedNode } from './mask';
|
|
35
42
|
export {
|
package/src/init.ts
CHANGED
|
@@ -5,8 +5,13 @@ import { installPromiseHandler } from './handlers/promise';
|
|
|
5
5
|
import { installNetworkHandler } from './handlers/network';
|
|
6
6
|
import { drainNativePending, setNativeConfig } from './native';
|
|
7
7
|
import { startSession } from './session-tracker';
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
drainOfflineQueue,
|
|
10
|
+
enqueue,
|
|
11
|
+
startTransport,
|
|
12
|
+
uploadAttachment,
|
|
13
|
+
} from './transport';
|
|
14
|
+
import type { AttachmentKind, AttachmentMeta, AttachmentSource, Event } from './types';
|
|
10
15
|
|
|
11
16
|
declare const __DEV__: boolean | undefined;
|
|
12
17
|
|
|
@@ -33,6 +38,21 @@ export type InitOptions = {
|
|
|
33
38
|
* installed and `<MaskRegion>` placed over any sensitive UI. The
|
|
34
39
|
* image is webp q=70 480 px max, < 100 KB typical. */
|
|
35
40
|
screenshot?: boolean;
|
|
41
|
+
/** Phase 46: record the last N steps (route changes, custom
|
|
42
|
+
* breadcrumbs) leading up to a crash. On `captureException`
|
|
43
|
+
* the buffer is sealed and uploaded as a `sessionTrail`
|
|
44
|
+
* attachment. Defaults to false. */
|
|
45
|
+
sessionTrail?: boolean;
|
|
46
|
+
};
|
|
47
|
+
/** Phase 44 sub-B: client-side sampling. Each rate is `[0, 1]`;
|
|
48
|
+
* absent / null keeps everything. Defaults to 1.0 for both
|
|
49
|
+
* (no drop). Set traces to e.g. 0.1 once the app's at user
|
|
50
|
+
* volume to keep ingest budget under control without changing
|
|
51
|
+
* the server-side quota. Decisions are made per-event for
|
|
52
|
+
* errors and per-trace (all spans together) for traces. */
|
|
53
|
+
sampling?: {
|
|
54
|
+
errors?: null | number;
|
|
55
|
+
traces?: null | number;
|
|
36
56
|
};
|
|
37
57
|
};
|
|
38
58
|
|
|
@@ -57,6 +77,9 @@ export const init = (options: InitOptions): void => {
|
|
|
57
77
|
ingestUrl: options.ingestUrl ?? DEFAULT_INGEST_URL,
|
|
58
78
|
enabled: true,
|
|
59
79
|
screenshotsEnabled: options.capture?.screenshot === true,
|
|
80
|
+
errorSampleRate: options.sampling?.errors ?? null,
|
|
81
|
+
traceSampleRate: options.sampling?.traces ?? null,
|
|
82
|
+
sessionTrailEnabled: options.capture?.sessionTrail === true,
|
|
60
83
|
});
|
|
61
84
|
|
|
62
85
|
// Tell the native crash handler about the config so the JSON it writes
|
|
@@ -85,10 +108,33 @@ export const init = (options: InitOptions): void => {
|
|
|
85
108
|
// - native crashes from <Documents>/sentori/pending/*.json
|
|
86
109
|
// - JS transport offline queue from AsyncStorage
|
|
87
110
|
drainNativePending()
|
|
88
|
-
.then((items) => {
|
|
111
|
+
.then(async (items) => {
|
|
89
112
|
for (const json of items) {
|
|
90
113
|
try {
|
|
91
|
-
|
|
114
|
+
const event = JSON.parse(json) as Event & {
|
|
115
|
+
_pendingAttachments?: PendingAttachment[];
|
|
116
|
+
};
|
|
117
|
+
// Phase 42 sub-E.05 / F.09: the native crash handler couldn't
|
|
118
|
+
// upload attachments at crash time (the app was dying); it
|
|
119
|
+
// base64-encoded them into `_pendingAttachments` instead.
|
|
120
|
+
// On next launch we upload each before enqueueing the event,
|
|
121
|
+
// so the dashboard sees the refs in `event.attachments[]`.
|
|
122
|
+
if (event._pendingAttachments && event._pendingAttachments.length > 0) {
|
|
123
|
+
for (const p of event._pendingAttachments) {
|
|
124
|
+
const meta = await uploadAttachment(
|
|
125
|
+
event.id,
|
|
126
|
+
p.kind,
|
|
127
|
+
{ base64: p.base64, mediaType: p.mediaType },
|
|
128
|
+
{ source: p.source },
|
|
129
|
+
);
|
|
130
|
+
if (meta) {
|
|
131
|
+
if (!event.attachments) event.attachments = [];
|
|
132
|
+
event.attachments.push(meta);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
delete event._pendingAttachments;
|
|
136
|
+
}
|
|
137
|
+
enqueue(event);
|
|
92
138
|
} catch {
|
|
93
139
|
// skip malformed
|
|
94
140
|
}
|
|
@@ -97,3 +143,20 @@ export const init = (options: InitOptions): void => {
|
|
|
97
143
|
.catch(() => {});
|
|
98
144
|
drainOfflineQueue().catch(() => {});
|
|
99
145
|
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Phase 42 sub-E.05: shape of each entry in the native crash JSON's
|
|
149
|
+
* `_pendingAttachments` array. Mirrors what
|
|
150
|
+
* `SentoriCrashHandler.write` writes on iOS and (sub-F) what
|
|
151
|
+
* `SentoriCrashWriter` writes on Android.
|
|
152
|
+
*/
|
|
153
|
+
type PendingAttachment = {
|
|
154
|
+
base64: string;
|
|
155
|
+
kind: AttachmentKind;
|
|
156
|
+
mediaType: string;
|
|
157
|
+
source: AttachmentSource;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Keep AttachmentMeta in the imports — it's part of the public type
|
|
161
|
+
// surface re-exported from this module's bundle.
|
|
162
|
+
export type { AttachmentMeta };
|
package/src/navigation.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { useEffect, useRef } from 'react';
|
|
|
24
24
|
|
|
25
25
|
import { setActiveSpan, startSpan, type SpanHandle } from '@goliapkg/sentori-core';
|
|
26
26
|
|
|
27
|
+
import { captureStep } from './trail';
|
|
28
|
+
|
|
27
29
|
/** Minimal contract: anything with `addListener('state', cb)` and
|
|
28
30
|
* `getCurrentRoute()` works. The real @react-navigation/native
|
|
29
31
|
* NavigationContainer ref matches this shape. */
|
|
@@ -72,6 +74,12 @@ export function useTraceNavigation(navigationRef: NavigationRefLike): void {
|
|
|
72
74
|
openSpanRef.current = span;
|
|
73
75
|
setActiveSpan(span);
|
|
74
76
|
lastRouteRef.current = to;
|
|
77
|
+
// Phase 46 — record the screen transition into the session trail.
|
|
78
|
+
// No-op when sessionTrail isn't enabled (the buffer just grows
|
|
79
|
+
// unbounded until cleared by captureException, but stays cheap).
|
|
80
|
+
captureStep(`screen:${to}`, {
|
|
81
|
+
breadcrumb: { type: 'navigation', message: from ? `${from} → ${to}` : to },
|
|
82
|
+
});
|
|
75
83
|
};
|
|
76
84
|
|
|
77
85
|
// Open a span for the initial screen so its requests are grouped
|
package/src/trail.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Phase 46 — singleton TrailBuffer for the RN SDK.
|
|
2
|
+
//
|
|
3
|
+
// Kept in its own module so callers (including navigation.ts, which
|
|
4
|
+
// is intentionally lightweight) can record steps without pulling in
|
|
5
|
+
// capture.ts → handlers/screenshot.ts → react-native. The buffer is
|
|
6
|
+
// drained inside capture.ts when an event captures and
|
|
7
|
+
// `sessionTrailEnabled` is on.
|
|
8
|
+
|
|
9
|
+
import { TrailBuffer, type TrailStep } from '@goliapkg/sentori-core';
|
|
10
|
+
|
|
11
|
+
const _trail = new TrailBuffer(30);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Phase 46 — record a step into the session-trail buffer. The buffer
|
|
15
|
+
* is a fixed-size FIFO (default 30 steps); pushing past capacity
|
|
16
|
+
* drops the oldest entry. Steps are only uploaded if
|
|
17
|
+
* `init({ capture: { sessionTrail: true } })` is on AND a
|
|
18
|
+
* `captureException` follows.
|
|
19
|
+
*/
|
|
20
|
+
export const captureStep = (label: string, opts?: Partial<TrailStep>): void => {
|
|
21
|
+
_trail.push({
|
|
22
|
+
ts: Date.now(),
|
|
23
|
+
label,
|
|
24
|
+
...(opts ?? {}),
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getTrailBuffer = (): TrailBuffer => _trail;
|
|
29
|
+
|
|
30
|
+
export const __resetTrailForTests = (): void => {
|
|
31
|
+
_trail.clear();
|
|
32
|
+
};
|