@goliapkg/sentori-react-native 0.5.5 → 0.5.7
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/capture.d.ts.map +1 -1
- package/lib/capture.js +13 -1
- package/lib/capture.js.map +1 -1
- package/lib/handlers/dev-symbolicate.d.ts +15 -0
- package/lib/handlers/dev-symbolicate.d.ts.map +1 -0
- package/lib/handlers/dev-symbolicate.js +118 -0
- package/lib/handlers/dev-symbolicate.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/dev-symbolicate.test.ts +170 -0
- package/src/capture.ts +15 -1
- package/src/handlers/dev-symbolicate.ts +138 -0
package/lib/capture.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAoC,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAM5E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,IAAI,GAAG,IAAI,KAAG,IAE3C,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,IAAI,GAAG,IAAa,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,OAAO,KAAK,EAAE,SAAS,aAAa,KAAG,IAoCnE,CAAC;AAEF,eAAO,MAAM,gBAAgB,UAtCO,KAAK,WAAW,aAAa,KAAG,IAsCxB,CAAC"}
|
package/lib/capture.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getConfig, isInitialized } from './config';
|
|
2
2
|
import { getBreadcrumbs } from './breadcrumbs';
|
|
3
|
+
import { symbolicateErrorViaMetro } from './handlers/dev-symbolicate';
|
|
3
4
|
import { markSessionErrored } from './session-tracker';
|
|
4
5
|
import { parseStack } from './stack';
|
|
5
6
|
import { enqueue } from './transport';
|
|
@@ -45,7 +46,18 @@ export const captureError = (error, extras) => {
|
|
|
45
46
|
// Phase 26 sub-B: a captured error promotes the current session to
|
|
46
47
|
// `errored` so the next AppState=background ping reports unhealthy.
|
|
47
48
|
markSessionErrored();
|
|
48
|
-
|
|
49
|
+
// Phase 40 sub-E: in dev there's no uploaded source map, so ask
|
|
50
|
+
// Metro to symbolicate the stack before we send it (best-effort,
|
|
51
|
+
// short timeout). Release builds skip straight to enqueue and let
|
|
52
|
+
// the server symbolicate at ingest against the uploaded map.
|
|
53
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
54
|
+
void symbolicateErrorViaMetro(event.error)
|
|
55
|
+
.catch(() => { })
|
|
56
|
+
.then(() => enqueue(event));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
enqueue(event);
|
|
60
|
+
}
|
|
49
61
|
};
|
|
50
62
|
export const captureException = captureError;
|
|
51
63
|
const errorToObject = (error) => {
|
package/lib/capture.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Frame, SentoriError } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* POST the frames to Metro's `/symbolicate` and return the mapped-back
|
|
4
|
+
* stack, or `null` if it can't be done (not a dev server, Metro down,
|
|
5
|
+
* timeout, malformed response). `url` is overridable for tests.
|
|
6
|
+
*/
|
|
7
|
+
export declare function symbolicateStackViaMetro(frames: Frame[], opts?: {
|
|
8
|
+
url?: string;
|
|
9
|
+
}): Promise<Frame[] | null>;
|
|
10
|
+
/** Replace `err.stack` (and the cause chain) in place with the
|
|
11
|
+
* Metro-symbolicated version, when possible. Never throws. */
|
|
12
|
+
export declare function symbolicateErrorViaMetro(err: SentoriError, opts?: {
|
|
13
|
+
url?: string;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=dev-symbolicate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-symbolicate.d.ts","sourceRoot":"","sources":["../../src/handlers/dev-symbolicate.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAqFpD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,KAAK,EAAE,EACf,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1B,OAAO,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAwBzB;AAED;+DAC+D;AAC/D,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,YAAY,EACjB,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1B,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Phase 40 sub-E: in __DEV__, symbolicate a JS stack against the Metro
|
|
2
|
+
// dev server's /symbolicate endpoint before sending the event — the
|
|
3
|
+
// same thing RN's LogBox does. Release builds upload a source map and
|
|
4
|
+
// the server symbolicates at ingest; in dev there's no uploaded map,
|
|
5
|
+
// so without this dev errors arrive as `index.bundle:1:288432`.
|
|
6
|
+
//
|
|
7
|
+
// Best-effort and dev-only: any failure (Metro not running, timeout,
|
|
8
|
+
// bad response) leaves the stack untouched. Never throws.
|
|
9
|
+
const TIMEOUT_MS = 2000;
|
|
10
|
+
/** Resolve `<devServer>/symbolicate`, or null if we're not running
|
|
11
|
+
* from a Metro dev server (release build, or not in RN).
|
|
12
|
+
*
|
|
13
|
+
* Order matters:
|
|
14
|
+
* 1. `react-native/Libraries/Core/Devtools/getDevServer` — the same
|
|
15
|
+
* helper LogBox + RN's own symbolicateStackTrace use. Works under
|
|
16
|
+
* both the legacy bridge and the new architecture (TurboModule),
|
|
17
|
+
* because internally it calls `NativeSourceCode.getConstants()`
|
|
18
|
+
* which is the correct path on new arch.
|
|
19
|
+
* 2. `NativeModules.SourceCode.getConstants().scriptURL` — direct
|
|
20
|
+
* TurboModule fallback if (1) ever moves.
|
|
21
|
+
* 3. `NativeModules.SourceCode.scriptURL` — legacy bridge (pre-new-
|
|
22
|
+
* arch RN). On new arch this property is `undefined` because
|
|
23
|
+
* constants aren't hoisted onto the module object — which is
|
|
24
|
+
* exactly the symptom Insight hit on RN 0.83 + new arch.
|
|
25
|
+
*/
|
|
26
|
+
function metroSymbolicateUrl() {
|
|
27
|
+
try {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
+
const mod = require('react-native/Libraries/Core/Devtools/getDevServer');
|
|
30
|
+
const getDevServer = mod.default ?? mod;
|
|
31
|
+
const ds = getDevServer();
|
|
32
|
+
if (ds.bundleLoadedFromServer && typeof ds.url === 'string') {
|
|
33
|
+
return ds.url.replace(/\/$/, '') + '/symbolicate';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Older RN / non-RN runtime / path moved → fall through to NativeModules.
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const rn = require('react-native');
|
|
42
|
+
const sc = rn.NativeModules?.SourceCode;
|
|
43
|
+
const scriptURL = sc?.scriptURL ?? sc?.getConstants?.()?.scriptURL;
|
|
44
|
+
if (!scriptURL || !/^https?:\/\//.test(scriptURL))
|
|
45
|
+
return null;
|
|
46
|
+
const u = new URL(scriptURL);
|
|
47
|
+
return `${u.protocol}//${u.host}/symbolicate`;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function toMetroFrame(f) {
|
|
54
|
+
return {
|
|
55
|
+
column: f.column ?? 0,
|
|
56
|
+
file: f.absolutePath ?? f.file,
|
|
57
|
+
lineNumber: f.line,
|
|
58
|
+
methodName: f.function ?? null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function fromMetroFrame(m, fallback) {
|
|
62
|
+
// Metro couldn't resolve this frame (file null) → keep the original.
|
|
63
|
+
if (!m.file)
|
|
64
|
+
return fallback;
|
|
65
|
+
return {
|
|
66
|
+
absolutePath: m.file,
|
|
67
|
+
column: typeof m.column === 'number' ? m.column : undefined,
|
|
68
|
+
file: m.file,
|
|
69
|
+
function: m.methodName ?? undefined,
|
|
70
|
+
inApp: !m.collapse && !m.file.includes('node_modules'),
|
|
71
|
+
line: typeof m.lineNumber === 'number' ? m.lineNumber : 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* POST the frames to Metro's `/symbolicate` and return the mapped-back
|
|
76
|
+
* stack, or `null` if it can't be done (not a dev server, Metro down,
|
|
77
|
+
* timeout, malformed response). `url` is overridable for tests.
|
|
78
|
+
*/
|
|
79
|
+
export async function symbolicateStackViaMetro(frames, opts = {}) {
|
|
80
|
+
const url = opts.url ?? metroSymbolicateUrl();
|
|
81
|
+
if (!url || frames.length === 0)
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
const ctrl = new AbortController();
|
|
85
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
86
|
+
let resp;
|
|
87
|
+
try {
|
|
88
|
+
resp = await fetch(url, {
|
|
89
|
+
body: JSON.stringify({ stack: frames.map(toMetroFrame) }),
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
method: 'POST',
|
|
92
|
+
signal: ctrl.signal,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
}
|
|
98
|
+
if (!resp.ok)
|
|
99
|
+
return null;
|
|
100
|
+
const body = (await resp.json());
|
|
101
|
+
if (!Array.isArray(body.stack) || body.stack.length !== frames.length)
|
|
102
|
+
return null;
|
|
103
|
+
return body.stack.map((m, i) => fromMetroFrame(m, frames[i]));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Replace `err.stack` (and the cause chain) in place with the
|
|
110
|
+
* Metro-symbolicated version, when possible. Never throws. */
|
|
111
|
+
export async function symbolicateErrorViaMetro(err, opts = {}) {
|
|
112
|
+
const sym = await symbolicateStackViaMetro(err.stack, opts);
|
|
113
|
+
if (sym)
|
|
114
|
+
err.stack = sym;
|
|
115
|
+
if (err.cause)
|
|
116
|
+
await symbolicateErrorViaMetro(err.cause, opts);
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=dev-symbolicate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-symbolicate.js","sourceRoot":"","sources":["../../src/handlers/dev-symbolicate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-react-native",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Sentori SDK for React Native
|
|
3
|
+
"version": "0.5.7",
|
|
4
|
+
"description": "Sentori SDK for React Native — 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": {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { symbolicateErrorViaMetro, symbolicateStackViaMetro } from '../handlers/dev-symbolicate';
|
|
4
|
+
import type { Frame, SentoriError } from '../types';
|
|
5
|
+
|
|
6
|
+
const URL = 'http://localhost:8081/symbolicate';
|
|
7
|
+
|
|
8
|
+
const minified = (col: number): Frame => ({
|
|
9
|
+
column: col,
|
|
10
|
+
file: 'http://localhost:8081/index.bundle?platform=ios&dev=true',
|
|
11
|
+
function: 'anonymous',
|
|
12
|
+
inApp: false,
|
|
13
|
+
line: 1,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// What Metro's /symbolicate sends back, positionally aligned to input.
|
|
17
|
+
const metroReply = (frames: { file: string; line: number; col: number; fn: string; collapse?: boolean }[]) =>
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
stack: frames.map((f) => ({
|
|
20
|
+
collapse: f.collapse ?? false,
|
|
21
|
+
column: f.col,
|
|
22
|
+
file: f.file,
|
|
23
|
+
lineNumber: f.line,
|
|
24
|
+
methodName: f.fn,
|
|
25
|
+
})),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const origFetch = globalThis.fetch;
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
globalThis.fetch = origFetch;
|
|
31
|
+
});
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
globalThis.fetch = origFetch;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('symbolicateStackViaMetro', () => {
|
|
37
|
+
test('returns null with no URL (not running from a Metro dev server)', async () => {
|
|
38
|
+
// In bun:test `require("react-native")` throws → metroSymbolicateUrl() → null
|
|
39
|
+
expect(await symbolicateStackViaMetro([minified(10)])).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('returns null for an empty stack', async () => {
|
|
43
|
+
expect(await symbolicateStackViaMetro([], { url: URL })).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('maps Metro frames back to SDK frames', async () => {
|
|
47
|
+
const calls: { body: unknown; url: string }[] = [];
|
|
48
|
+
globalThis.fetch = (async (url: Request | string | URL, init?: RequestInit) => {
|
|
49
|
+
calls.push({ body: JSON.parse((init?.body as string) ?? '{}'), url: String(url) });
|
|
50
|
+
return new Response(
|
|
51
|
+
metroReply([
|
|
52
|
+
{ col: 18, file: '/proj/src/screens/Checkout.tsx', fn: 'handleSubmit', line: 142 },
|
|
53
|
+
{ col: 4, collapse: true, file: '/proj/node_modules/react-native/Libraries/x.js', fn: 'r', line: 9 },
|
|
54
|
+
]),
|
|
55
|
+
{ headers: { 'content-type': 'application/json' }, status: 200 },
|
|
56
|
+
);
|
|
57
|
+
}) as typeof fetch;
|
|
58
|
+
|
|
59
|
+
const out = await symbolicateStackViaMetro([minified(100), minified(200)], { url: URL });
|
|
60
|
+
expect(calls).toHaveLength(1);
|
|
61
|
+
expect(calls[0]?.url).toBe(URL);
|
|
62
|
+
// request used Metro's frame shape
|
|
63
|
+
expect((calls[0]?.body as { stack: { lineNumber: number }[] }).stack[0]?.lineNumber).toBe(1);
|
|
64
|
+
|
|
65
|
+
expect(out).not.toBeNull();
|
|
66
|
+
expect(out![0]).toMatchObject({
|
|
67
|
+
column: 18,
|
|
68
|
+
file: '/proj/src/screens/Checkout.tsx',
|
|
69
|
+
function: 'handleSubmit',
|
|
70
|
+
inApp: true,
|
|
71
|
+
line: 142,
|
|
72
|
+
});
|
|
73
|
+
// node_modules + collapse → not in-app
|
|
74
|
+
expect(out![1]?.inApp).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('keeps the original frame when Metro can’t resolve it (file null)', async () => {
|
|
78
|
+
globalThis.fetch = (async () =>
|
|
79
|
+
new Response(JSON.stringify({ stack: [{ column: null, file: null, lineNumber: null, methodName: null }] }), {
|
|
80
|
+
headers: { 'content-type': 'application/json' },
|
|
81
|
+
status: 200,
|
|
82
|
+
})) as typeof fetch;
|
|
83
|
+
const input = minified(42);
|
|
84
|
+
const out = await symbolicateStackViaMetro([input], { url: URL });
|
|
85
|
+
expect(out![0]).toEqual(input);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('returns null on a non-2xx response', async () => {
|
|
89
|
+
globalThis.fetch = (async () => new Response('nope', { status: 500 })) as typeof fetch;
|
|
90
|
+
expect(await symbolicateStackViaMetro([minified(1)], { url: URL })).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('returns null when fetch throws (Metro down)', async () => {
|
|
94
|
+
globalThis.fetch = (async () => {
|
|
95
|
+
throw new TypeError('ECONNREFUSED');
|
|
96
|
+
}) as typeof fetch;
|
|
97
|
+
expect(await symbolicateStackViaMetro([minified(1)], { url: URL })).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('returns null when the reply length doesn’t match', async () => {
|
|
101
|
+
globalThis.fetch = (async () =>
|
|
102
|
+
new Response(metroReply([{ col: 1, file: '/a.ts', fn: 'a', line: 2 }]), {
|
|
103
|
+
headers: { 'content-type': 'application/json' },
|
|
104
|
+
status: 200,
|
|
105
|
+
})) as typeof fetch;
|
|
106
|
+
expect(await symbolicateStackViaMetro([minified(1), minified(2)], { url: URL })).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('metroSymbolicateUrl resolution (RN 0.83 new-arch regression)', () => {
|
|
111
|
+
// Without `opts.url`, the function must resolve a real Metro URL.
|
|
112
|
+
// RN 0.83 + new architecture leaves `NativeModules.SourceCode.scriptURL`
|
|
113
|
+
// undefined; the fix is to prefer RN's own `getDevServer()` helper,
|
|
114
|
+
// which internally calls `NativeSourceCode.getConstants().scriptURL`
|
|
115
|
+
// and works on both old and new arch.
|
|
116
|
+
test('prefers getDevServer() when available (works on new arch)', async () => {
|
|
117
|
+
mock.module('react-native/Libraries/Core/Devtools/getDevServer', () => ({
|
|
118
|
+
default: () => ({ bundleLoadedFromServer: true, url: 'http://192.168.1.100:8081/' }),
|
|
119
|
+
}));
|
|
120
|
+
const calls: string[] = [];
|
|
121
|
+
globalThis.fetch = (async (url: Request | string | URL) => {
|
|
122
|
+
calls.push(String(url));
|
|
123
|
+
return new Response(metroReply([{ col: 1, file: '/proj/src/a.ts', fn: 'a', line: 5 }]), {
|
|
124
|
+
headers: { 'content-type': 'application/json' },
|
|
125
|
+
status: 200,
|
|
126
|
+
});
|
|
127
|
+
}) as typeof fetch;
|
|
128
|
+
|
|
129
|
+
const out = await symbolicateStackViaMetro([minified(1)]);
|
|
130
|
+
expect(calls[0]).toBe('http://192.168.1.100:8081/symbolicate');
|
|
131
|
+
expect(out![0]?.file).toBe('/proj/src/a.ts');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('returns null when getDevServer says bundle was not loaded from Metro', async () => {
|
|
135
|
+
mock.module('react-native/Libraries/Core/Devtools/getDevServer', () => ({
|
|
136
|
+
default: () => ({ bundleLoadedFromServer: false, url: 'http://localhost:8081/' }),
|
|
137
|
+
}));
|
|
138
|
+
// No fallback chain hit either (NativeModules require still throws in bun env)
|
|
139
|
+
expect(await symbolicateStackViaMetro([minified(1)])).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('symbolicateErrorViaMetro', () => {
|
|
144
|
+
test('replaces stack in place and recurses into the cause chain', async () => {
|
|
145
|
+
globalThis.fetch = (async () =>
|
|
146
|
+
new Response(metroReply([{ col: 1, file: '/proj/src/a.ts', fn: 'a', line: 5 }]), {
|
|
147
|
+
headers: { 'content-type': 'application/json' },
|
|
148
|
+
status: 200,
|
|
149
|
+
})) as typeof fetch;
|
|
150
|
+
|
|
151
|
+
const err: SentoriError = {
|
|
152
|
+
cause: { cause: null, message: 'root', stack: [minified(2)], type: 'Error' },
|
|
153
|
+
message: 'boom',
|
|
154
|
+
stack: [minified(1)],
|
|
155
|
+
type: 'TypeError',
|
|
156
|
+
};
|
|
157
|
+
await symbolicateErrorViaMetro(err, { url: URL });
|
|
158
|
+
expect(err.stack[0]?.file).toBe('/proj/src/a.ts');
|
|
159
|
+
expect(err.stack[0]?.line).toBe(5);
|
|
160
|
+
expect(err.cause?.stack[0]?.file).toBe('/proj/src/a.ts');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('leaves the error untouched when symbolication isn’t possible', async () => {
|
|
164
|
+
globalThis.fetch = (async () => new Response('x', { status: 404 })) as typeof fetch;
|
|
165
|
+
const err: SentoriError = { cause: null, message: 'boom', stack: [minified(7)], type: 'Error' };
|
|
166
|
+
const before = JSON.stringify(err);
|
|
167
|
+
await symbolicateErrorViaMetro(err, { url: URL });
|
|
168
|
+
expect(JSON.stringify(err)).toBe(before);
|
|
169
|
+
});
|
|
170
|
+
});
|
package/src/capture.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { getConfig, isInitialized } from './config';
|
|
2
2
|
import { getBreadcrumbs } from './breadcrumbs';
|
|
3
|
+
import { symbolicateErrorViaMetro } from './handlers/dev-symbolicate';
|
|
3
4
|
import { markSessionErrored } from './session-tracker';
|
|
4
5
|
import { parseStack } from './stack';
|
|
5
6
|
import { enqueue } from './transport';
|
|
6
7
|
import { uuidV7 } from './uuid';
|
|
7
8
|
import type { App, Device, Event, SentoriError, Tags, User } from './types';
|
|
8
9
|
|
|
10
|
+
declare const __DEV__: boolean | undefined;
|
|
11
|
+
|
|
9
12
|
let _user: User | null = null;
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -56,7 +59,18 @@ export const captureError = (error: Error, extras?: CaptureExtras): void => {
|
|
|
56
59
|
// Phase 26 sub-B: a captured error promotes the current session to
|
|
57
60
|
// `errored` so the next AppState=background ping reports unhealthy.
|
|
58
61
|
markSessionErrored();
|
|
59
|
-
|
|
62
|
+
|
|
63
|
+
// Phase 40 sub-E: in dev there's no uploaded source map, so ask
|
|
64
|
+
// Metro to symbolicate the stack before we send it (best-effort,
|
|
65
|
+
// short timeout). Release builds skip straight to enqueue and let
|
|
66
|
+
// the server symbolicate at ingest against the uploaded map.
|
|
67
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
68
|
+
void symbolicateErrorViaMetro(event.error)
|
|
69
|
+
.catch(() => {})
|
|
70
|
+
.then(() => enqueue(event));
|
|
71
|
+
} else {
|
|
72
|
+
enqueue(event);
|
|
73
|
+
}
|
|
60
74
|
};
|
|
61
75
|
|
|
62
76
|
export const captureException = captureError;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Phase 40 sub-E: in __DEV__, symbolicate a JS stack against the Metro
|
|
2
|
+
// dev server's /symbolicate endpoint before sending the event — the
|
|
3
|
+
// same thing RN's LogBox does. Release builds upload a source map and
|
|
4
|
+
// the server symbolicates at ingest; in dev there's no uploaded map,
|
|
5
|
+
// so without this dev errors arrive as `index.bundle:1:288432`.
|
|
6
|
+
//
|
|
7
|
+
// Best-effort and dev-only: any failure (Metro not running, timeout,
|
|
8
|
+
// bad response) leaves the stack untouched. Never throws.
|
|
9
|
+
|
|
10
|
+
import type { Frame, SentoriError } from '../types';
|
|
11
|
+
|
|
12
|
+
const TIMEOUT_MS = 2000;
|
|
13
|
+
|
|
14
|
+
/** Metro frame shape on the wire (note `lineNumber` / `methodName`). */
|
|
15
|
+
type MetroFrame = {
|
|
16
|
+
collapse?: boolean;
|
|
17
|
+
column?: null | number;
|
|
18
|
+
file?: null | string;
|
|
19
|
+
lineNumber?: null | number;
|
|
20
|
+
methodName?: null | string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Resolve `<devServer>/symbolicate`, or null if we're not running
|
|
24
|
+
* from a Metro dev server (release build, or not in RN).
|
|
25
|
+
*
|
|
26
|
+
* Order matters:
|
|
27
|
+
* 1. `react-native/Libraries/Core/Devtools/getDevServer` — the same
|
|
28
|
+
* helper LogBox + RN's own symbolicateStackTrace use. Works under
|
|
29
|
+
* both the legacy bridge and the new architecture (TurboModule),
|
|
30
|
+
* because internally it calls `NativeSourceCode.getConstants()`
|
|
31
|
+
* which is the correct path on new arch.
|
|
32
|
+
* 2. `NativeModules.SourceCode.getConstants().scriptURL` — direct
|
|
33
|
+
* TurboModule fallback if (1) ever moves.
|
|
34
|
+
* 3. `NativeModules.SourceCode.scriptURL` — legacy bridge (pre-new-
|
|
35
|
+
* arch RN). On new arch this property is `undefined` because
|
|
36
|
+
* constants aren't hoisted onto the module object — which is
|
|
37
|
+
* exactly the symptom Insight hit on RN 0.83 + new arch.
|
|
38
|
+
*/
|
|
39
|
+
function metroSymbolicateUrl(): null | string {
|
|
40
|
+
try {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
const mod = require('react-native/Libraries/Core/Devtools/getDevServer') as {
|
|
43
|
+
default?: () => { bundleLoadedFromServer: boolean; url: string };
|
|
44
|
+
};
|
|
45
|
+
const getDevServer = mod.default ?? (mod as unknown as () => { bundleLoadedFromServer: boolean; url: string });
|
|
46
|
+
const ds = getDevServer();
|
|
47
|
+
if (ds.bundleLoadedFromServer && typeof ds.url === 'string') {
|
|
48
|
+
return ds.url.replace(/\/$/, '') + '/symbolicate';
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Older RN / non-RN runtime / path moved → fall through to NativeModules.
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
55
|
+
const rn = require('react-native') as {
|
|
56
|
+
NativeModules?: {
|
|
57
|
+
SourceCode?: {
|
|
58
|
+
getConstants?: () => { scriptURL?: string };
|
|
59
|
+
scriptURL?: string;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const sc = rn.NativeModules?.SourceCode;
|
|
64
|
+
const scriptURL = sc?.scriptURL ?? sc?.getConstants?.()?.scriptURL;
|
|
65
|
+
if (!scriptURL || !/^https?:\/\//.test(scriptURL)) return null;
|
|
66
|
+
const u = new URL(scriptURL);
|
|
67
|
+
return `${u.protocol}//${u.host}/symbolicate`;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toMetroFrame(f: Frame): MetroFrame {
|
|
74
|
+
return {
|
|
75
|
+
column: f.column ?? 0,
|
|
76
|
+
file: f.absolutePath ?? f.file,
|
|
77
|
+
lineNumber: f.line,
|
|
78
|
+
methodName: f.function ?? null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fromMetroFrame(m: MetroFrame, fallback: Frame): Frame {
|
|
83
|
+
// Metro couldn't resolve this frame (file null) → keep the original.
|
|
84
|
+
if (!m.file) return fallback;
|
|
85
|
+
return {
|
|
86
|
+
absolutePath: m.file,
|
|
87
|
+
column: typeof m.column === 'number' ? m.column : undefined,
|
|
88
|
+
file: m.file,
|
|
89
|
+
function: m.methodName ?? undefined,
|
|
90
|
+
inApp: !m.collapse && !m.file.includes('node_modules'),
|
|
91
|
+
line: typeof m.lineNumber === 'number' ? m.lineNumber : 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* POST the frames to Metro's `/symbolicate` and return the mapped-back
|
|
97
|
+
* stack, or `null` if it can't be done (not a dev server, Metro down,
|
|
98
|
+
* timeout, malformed response). `url` is overridable for tests.
|
|
99
|
+
*/
|
|
100
|
+
export async function symbolicateStackViaMetro(
|
|
101
|
+
frames: Frame[],
|
|
102
|
+
opts: { url?: string } = {},
|
|
103
|
+
): Promise<Frame[] | null> {
|
|
104
|
+
const url = opts.url ?? metroSymbolicateUrl();
|
|
105
|
+
if (!url || frames.length === 0) return null;
|
|
106
|
+
try {
|
|
107
|
+
const ctrl = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
109
|
+
let resp: Response;
|
|
110
|
+
try {
|
|
111
|
+
resp = await fetch(url, {
|
|
112
|
+
body: JSON.stringify({ stack: frames.map(toMetroFrame) }),
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
method: 'POST',
|
|
115
|
+
signal: ctrl.signal,
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
}
|
|
120
|
+
if (!resp.ok) return null;
|
|
121
|
+
const body = (await resp.json()) as { stack?: MetroFrame[] };
|
|
122
|
+
if (!Array.isArray(body.stack) || body.stack.length !== frames.length) return null;
|
|
123
|
+
return body.stack.map((m, i) => fromMetroFrame(m, frames[i]!));
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Replace `err.stack` (and the cause chain) in place with the
|
|
130
|
+
* Metro-symbolicated version, when possible. Never throws. */
|
|
131
|
+
export async function symbolicateErrorViaMetro(
|
|
132
|
+
err: SentoriError,
|
|
133
|
+
opts: { url?: string } = {},
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const sym = await symbolicateStackViaMetro(err.stack, opts);
|
|
136
|
+
if (sym) err.stack = sym;
|
|
137
|
+
if (err.cause) await symbolicateErrorViaMetro(err.cause, opts);
|
|
138
|
+
}
|