miaoda-expo-devkit 0.1.1-beta.112 → 0.1.1-beta.114
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/README.md +26 -107
- package/dist/index.d.mts +1 -110
- package/dist/index.d.ts +1 -110
- package/dist/index.js +0 -159
- package/dist/index.mjs +0 -132
- package/dist/stubs/entry-inject.js +0 -2
- package/dist/stubs/sentry-react-native-stub.js +15 -180
- package/package.json +2 -3
- package/pnpm-patches/@expo__cli@55.0.16-55.0.22.patch +1 -1
- package/dist/stubs/preview-control.js +0 -60
package/README.md
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Expo / React Native 开发环境工具集,通过 Metro 构建层注入以下能力:
|
|
4
4
|
|
|
5
|
-
- **Sentry
|
|
6
|
-
- **错误 / 网络捕获** — 内置 `SentryCapture`,自动调用 Metro `/symbolicate` 还原源码位置并输出到 `console.warn`
|
|
5
|
+
- **Sentry 初始化守卫** — Native 透传真实初始化;独立 Expo Web 使用安全 fallback;sandbox Web 由前置 polyfill 唯一初始化
|
|
7
6
|
- **Bundle 首部注入** — 在 expo-router 初始化之前执行自定义脚本
|
|
8
7
|
- **HMR postMessage 控制** — 通过 `window.postMessage` 在运行时启动或停止 Fast Refresh
|
|
9
8
|
- **LogBox 屏蔽** — web 平台禁用 Expo 全屏错误遮罩
|
|
@@ -60,7 +59,7 @@ module.exports = withExpoCalendarStub(withExpoMediaLibraryStub(withEntryInjectio
|
|
|
60
59
|
|
|
61
60
|
### Sentry 初始化
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
应用可照常调用 `Sentry.init`:
|
|
64
63
|
|
|
65
64
|
```ts
|
|
66
65
|
// app/_layout.tsx
|
|
@@ -68,34 +67,17 @@ import * as Sentry from '@sentry/react-native';
|
|
|
68
67
|
|
|
69
68
|
Sentry.init({
|
|
70
69
|
dsn: 'https://your-key@sentry.io/your-project',
|
|
71
|
-
// stub 自动拦截 beforeSend / beforeBreadcrumb,错误摘要输出到 console.warn
|
|
72
70
|
});
|
|
73
71
|
```
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
stub 的行为按平台区分:
|
|
76
74
|
|
|
77
|
-
|
|
75
|
+
- iOS/Android:原样调用真实 `@sentry/react-native` init,应用参数不变;
|
|
76
|
+
- 独立 Expo Web:用安全 fallback DSN 初始化,避免开发事件进入真实项目;
|
|
77
|
+
- vite-sandbox Expo Web:sandbox 生成的 Metro polyfill 已经把应用实际依赖的 `@sentry/browser` 与统一观测运行时打包,并先于应用完成唯一一次 init;stub 检测版本字符串在场标记后吞掉应用 init;
|
|
78
|
+
- 版本不一致:只输出 `console.error`,不向编辑器宿主发送消息。
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
import * as Sentry from '@sentry/react-native';
|
|
81
|
-
import { SentryCapture } from 'miaoda-expo-devkit';
|
|
82
|
-
import type { ErrorCaptureInfo, NetworkCaptureInfo } from 'miaoda-expo-devkit';
|
|
83
|
-
|
|
84
|
-
const capture = new SentryCapture({
|
|
85
|
-
onError(info: ErrorCaptureInfo) {
|
|
86
|
-
console.log('Error at:', info.symbolicatedFrames?.[0]);
|
|
87
|
-
},
|
|
88
|
-
onNetwork(info: NetworkCaptureInfo) {
|
|
89
|
-
console.log('Network:', info.method, info.url, info.statusCode);
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
Sentry.init({
|
|
94
|
-
dsn: 'https://your-key@sentry.io/your-project',
|
|
95
|
-
beforeSend: capture.beforeSend,
|
|
96
|
-
beforeBreadcrumb: capture.beforeBreadcrumb,
|
|
97
|
-
});
|
|
98
|
-
```
|
|
80
|
+
fetch/XHR 取数、网络与 console 事件、原始数据长度限制、Supabase 白名单、`GLOBAL_ERROR` 和 Metro `/symbolicate` 均由 vite-sandbox 的同一份运行时维护。devkit 不复制这些规则,也不再包含 capture 或 runtime bridge。
|
|
99
81
|
|
|
100
82
|
### HMR postMessage 控制
|
|
101
83
|
|
|
@@ -151,75 +133,6 @@ window.__DEVKIT_HMR_ENABLED__ // true | false
|
|
|
151
133
|
|
|
152
134
|
---
|
|
153
135
|
|
|
154
|
-
### `new SentryCapture(options)`
|
|
155
|
-
|
|
156
|
-
从 `miaoda-expo-devkit` 引入。
|
|
157
|
-
|
|
158
|
-
用于自定义捕获行为。stub 内置了一个默认实例(输出到 `console.warn`),此类用于叠加自定义处理逻辑。
|
|
159
|
-
|
|
160
|
-
| 选项 | 类型 | 说明 |
|
|
161
|
-
|---|---|---|
|
|
162
|
-
| `onError` | `(info: ErrorCaptureInfo) => void` | 捕获到错误时触发,携带符号化后的调用栈 |
|
|
163
|
-
| `onNetwork` | `(info: NetworkCaptureInfo) => void` | 捕获到 HTTP 请求时触发 |
|
|
164
|
-
| `symbolicator` | `MetroSymbolicator` | 可选,单测时注入 mock 以隔离网络依赖 |
|
|
165
|
-
|
|
166
|
-
构造完成后,将绑定方法传给 `Sentry.init`:
|
|
167
|
-
|
|
168
|
-
```ts
|
|
169
|
-
Sentry.init({
|
|
170
|
-
beforeSend: capture.beforeSend,
|
|
171
|
-
beforeBreadcrumb: capture.beforeBreadcrumb,
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
#### `ErrorCaptureInfo`
|
|
176
|
-
|
|
177
|
-
| 字段 | 类型 | 说明 |
|
|
178
|
-
|---|---|---|
|
|
179
|
-
| `message` | `string` | 错误信息 |
|
|
180
|
-
| `symbolicatedFrames` | `MetroFrame[] \| null` | 经 Metro 符号化后还原到源码位置的调用栈帧 |
|
|
181
|
-
| `componentStack` | `string \| null` | React 组件调用栈(ErrorBoundary 捕获的错误才有值) |
|
|
182
|
-
| `mechanismType` | `string \| null` | Sentry 捕获方式:`'onerror'`、`'generic'`、`'auto.function.react.error_boundary'` 等 |
|
|
183
|
-
|
|
184
|
-
#### `NetworkCaptureInfo`
|
|
185
|
-
|
|
186
|
-
| 字段 | 类型 | 说明 |
|
|
187
|
-
|---|---|---|
|
|
188
|
-
| `method` | `string \| null` | HTTP 方法 |
|
|
189
|
-
| `url` | `string \| null` | 请求 URL |
|
|
190
|
-
| `statusCode` | `number \| null` | HTTP 状态码(连接级失败为 `null`) |
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
### `new MetroSymbolicator(options?)`
|
|
195
|
-
|
|
196
|
-
从 `miaoda-expo-devkit` 引入。
|
|
197
|
-
|
|
198
|
-
通过 Metro `/symbolicate` 接口将 bundle 行列号还原为源码位置。`SentryCapture` 内部已默认使用,主要用途是单测时注入 mock。
|
|
199
|
-
|
|
200
|
-
| 选项 | 类型 | 说明 |
|
|
201
|
-
|---|---|---|
|
|
202
|
-
| `baseUrl` | `string` | Metro server 地址。不传则从 `NativeModules.SourceCode.scriptURL` 自动推断,fallback 到 `localhost:8081`。 |
|
|
203
|
-
|
|
204
|
-
**单测示例:**
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
const mockSym = {
|
|
208
|
-
parseErrorStack: jest.fn(() => []),
|
|
209
|
-
symbolicate: jest.fn(async () => null),
|
|
210
|
-
};
|
|
211
|
-
const onError = jest.fn();
|
|
212
|
-
const capture = new SentryCapture({ onError, symbolicator: mockSym });
|
|
213
|
-
|
|
214
|
-
await capture.beforeSend(fakeEvent, { originalException: new Error('oops') });
|
|
215
|
-
|
|
216
|
-
expect(onError).toHaveBeenCalledWith(
|
|
217
|
-
expect.objectContaining({ message: 'oops' }),
|
|
218
|
-
);
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
136
|
## 全局标记
|
|
224
137
|
|
|
225
138
|
注入脚本在 bundle 执行时(早于任何 React 代码)写入以下全局变量,可用于 E2E 测试断言:
|
|
@@ -233,7 +146,7 @@ expect(onError).toHaveBeenCalledWith(
|
|
|
233
146
|
|
|
234
147
|
| 变量 | 默认值 | 说明 |
|
|
235
148
|
|---|---|---|
|
|
236
|
-
| `
|
|
149
|
+
| `INJECT_SENTRY_DSN` | 安全 stub DSN | vite-sandbox 服务端已有变量;sandbox 会自动编入生成 runtime,应用开发者无需配置 |
|
|
237
150
|
| `METRO_CACHE_DIR` | `projectRoot/.metro-cache` | Metro transform 缓存目录绝对路径,优先级最高,适合容器/CI 挂载外部持久目录 |
|
|
238
151
|
| `METRO_TRANSFORM_LOG` | _(未设置时不记录)_ | 构建日志输出文件的绝对路径(JSONL 格式),仅开发模式(`__DEV__`)下生效 |
|
|
239
152
|
| `EXPO_AI_ERROR_COLLECTOR` | _(未设置时关闭)_ | 设为 `1` 后由 DevKit 管理的 Expo CLI patch 采集当前 Metro 编译状态 |
|
|
@@ -265,6 +178,14 @@ AI 进程直接读取这个文件即可,不需要额外的 HTTP 服务或 WebS
|
|
|
265
178
|
该范围以外的 CLI 版本不会静默套用补丁;升级时需要先检查发布产物,确认兼容后再调整
|
|
266
179
|
`pnpmfile.cjs` 中的范围和对应测试。
|
|
267
180
|
|
|
181
|
+
以下变量只服务于 sandbox 专项 E2E,不是应用或线上部署配置:
|
|
182
|
+
|
|
183
|
+
| 变量 | 默认值 | 设置方与用途 |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `EXPO_OVERRIDE_METRO_CONFIG` | 无 | 线上由 vite-sandbox 自动设置;专项 E2E 由准备流程指向生成的 `node_modules/.cache/vite-sandbox/metro.config.cjs` |
|
|
186
|
+
| `EXPO_E2E_PORT` | `3001` | 仅在 Playwright 端口冲突时由本地或 CI 覆盖 |
|
|
187
|
+
| `EXPO_E2E_SANDBOX_PROJECT_ID` | `0` | 仅用于断言 Sentry envelope 使用 sandbox DSN 的 project id |
|
|
188
|
+
|
|
268
189
|
## 工作原理
|
|
269
190
|
|
|
270
191
|
```
|
|
@@ -362,18 +283,16 @@ setupHMRMessageControl()
|
|
|
362
283
|
|
|
363
284
|
sentry-react-native-stub.js
|
|
364
285
|
└─ init(options)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
│ └─ onError({ message, frames, ... }) → console.warn(默认)
|
|
369
|
-
└─ options.beforeSend?(event, hint) ← 调用方自定义(可选,串联执行)
|
|
286
|
+
├─ Native: RealSentry.init(options)
|
|
287
|
+
├─ 独立 Web: RealSentry.init({...options, dsn: FALLBACK_DSN})
|
|
288
|
+
└─ sandbox Web: 校验版本标记后返回,不重复初始化
|
|
370
289
|
```
|
|
371
290
|
|
|
372
291
|
## Package Exports
|
|
373
292
|
|
|
374
293
|
| 子路径 | 文件 | 内容 |
|
|
375
294
|
|---|---|---|
|
|
376
|
-
| `.` | `dist/index.js` |
|
|
295
|
+
| `.` | `dist/index.js` | 主入口(当前不导出运行时 API) |
|
|
377
296
|
| `./metro` | `dist/metro.js` | `withDevkit`、`withDevStubs`、`withEntryInjection`、`withPersistentCache`、`withTransformLogger`、`withExpoNotificationsStub`、`withExpoMediaLibraryStub`、`withExpoCalendarStub` 等全部 Metro wrapper |
|
|
378
297
|
| `./babel-plugin-jsx-source` | `dist/babel/plugin-jsx-source.js` | Babel 插件:为 JSX 注入 source 信息 |
|
|
379
298
|
| `./babel-preset` | `dist/babel/preset.js` | Babel Preset:集成 jsx-source 和 lucide 插件 |
|
|
@@ -562,13 +481,13 @@ window.postMessage({ type: 'editor-refresh' }, '*') //
|
|
|
562
481
|
`@sentry/react-native` 模块替换 stub,由 `withDevStubs()` 在 Metro 层自动注入。
|
|
563
482
|
|
|
564
483
|
**功能:**
|
|
565
|
-
-
|
|
566
|
-
-
|
|
567
|
-
-
|
|
568
|
-
-
|
|
484
|
+
- Native 原样透传应用的 Sentry init
|
|
485
|
+
- 独立 Web 使用安全 fallback DSN
|
|
486
|
+
- sandbox Web 校验运行时版本标记并阻止重复 init
|
|
487
|
+
- 版本不一致时只向 console 输出诊断信息,不通知编辑器宿主
|
|
569
488
|
|
|
570
489
|
**环境变量:**
|
|
571
|
-
- `
|
|
490
|
+
- 应用开发者不需要新增环境变量;`INJECT_SENTRY_DSN` 由 vite-sandbox 启动链路自动处理
|
|
572
491
|
|
|
573
492
|
---
|
|
574
493
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,111 +1,2 @@
|
|
|
1
|
-
import { ErrorEvent, EventHint, Breadcrumb, BreadcrumbHint } from '@sentry/core';
|
|
2
1
|
|
|
3
|
-
|
|
4
|
-
* Sentry Capture 模块
|
|
5
|
-
*
|
|
6
|
-
* 提供 Metro 符号化(MetroSymbolicator)和 Sentry 事件捕获(SentryCapture)。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Metro /symbolicate 接口的帧格式(请求与响应共用)。 */
|
|
10
|
-
interface MetroFrame {
|
|
11
|
-
file: string;
|
|
12
|
-
lineNumber: number;
|
|
13
|
-
column: number;
|
|
14
|
-
methodName: string;
|
|
15
|
-
}
|
|
16
|
-
/** SentryCapture.onError 回调参数 */
|
|
17
|
-
interface ErrorCaptureInfo {
|
|
18
|
-
message: string;
|
|
19
|
-
symbolicatedFrames: MetroFrame[] | null;
|
|
20
|
-
componentStack: string | null;
|
|
21
|
-
/** Sentry Mechanism.type,如 'onerror' / 'generic' / 'auto.function.react.error_boundary' */
|
|
22
|
-
mechanismType: string | null;
|
|
23
|
-
}
|
|
24
|
-
/** SentryCapture.onNetwork 回调参数 */
|
|
25
|
-
interface NetworkCaptureInfo {
|
|
26
|
-
method: string | null;
|
|
27
|
-
url: string | null;
|
|
28
|
-
statusCode: number | null;
|
|
29
|
-
}
|
|
30
|
-
interface SentryCaptureOptions {
|
|
31
|
-
onError?: (info: ErrorCaptureInfo) => void;
|
|
32
|
-
onNetwork?: (info: NetworkCaptureInfo) => void;
|
|
33
|
-
/** 可注入 mock 实例以在单元测试中隔离网络依赖 */
|
|
34
|
-
symbolicator?: MetroSymbolicator;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Metro 符号化器。
|
|
38
|
-
* 将 bundle 行列号还原为源码位置(通过 Metro /symbolicate 接口)。
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* const sym = new MetroSymbolicator();
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* // 单测:注入 baseUrl 并 mock fetch
|
|
45
|
-
* const sym = new MetroSymbolicator({ baseUrl: 'http://localhost:8081' });
|
|
46
|
-
* global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ stack: [...] }) });
|
|
47
|
-
*/
|
|
48
|
-
declare class MetroSymbolicator {
|
|
49
|
-
readonly baseUrl: string;
|
|
50
|
-
constructor({ baseUrl }?: {
|
|
51
|
-
baseUrl?: string;
|
|
52
|
-
});
|
|
53
|
-
/**
|
|
54
|
-
* 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
|
|
55
|
-
* web 平台使用相对路径;native 回退到 localhost:8081。
|
|
56
|
-
*/
|
|
57
|
-
private static _getDefaultBaseUrl;
|
|
58
|
-
/**
|
|
59
|
-
* 解析 Error.stack 字符串为 Metro 所需的帧数组。
|
|
60
|
-
*
|
|
61
|
-
* 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
|
|
62
|
-
* V8/Node.js/Hermes(新):at methodName (url:line:col)
|
|
63
|
-
* Gecko/Firefox/Hermes(旧):methodName@url:line:col
|
|
64
|
-
* JavaScriptCore(iOS):methodName@url:line:col
|
|
65
|
-
*
|
|
66
|
-
* 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
|
|
67
|
-
*/
|
|
68
|
-
parseErrorStack(stack: string): MetroFrame[];
|
|
69
|
-
/**
|
|
70
|
-
* 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
|
|
71
|
-
*/
|
|
72
|
-
symbolicate(stack: MetroFrame[]): Promise<MetroFrame[] | null>;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Sentry 事件捕获器。
|
|
76
|
-
* 封装 beforeSend / beforeBreadcrumb 逻辑,通过注入的回调将事件数据交给调用方处理。
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* const capture = new SentryCapture({
|
|
80
|
-
* onError(info) { console.log(info.message, info.symbolicatedFrames); },
|
|
81
|
-
* onNetwork(info) { console.log(info.method, info.url, info.statusCode); },
|
|
82
|
-
* });
|
|
83
|
-
* Sentry.init({ dsn: '...', beforeSend: capture.beforeSend, beforeBreadcrumb: capture.beforeBreadcrumb });
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* // 单测:注入 mock symbolicator
|
|
87
|
-
* const mockSym = { parseErrorStack: jest.fn(() => []), symbolicate: jest.fn(async () => null) };
|
|
88
|
-
* const onError = jest.fn();
|
|
89
|
-
* const capture = new SentryCapture({ onError, symbolicator: mockSym });
|
|
90
|
-
* await capture.beforeSend(fakeEvent, { originalException: new Error('oops') });
|
|
91
|
-
* expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'oops' }));
|
|
92
|
-
*/
|
|
93
|
-
declare class SentryCapture {
|
|
94
|
-
private readonly onError;
|
|
95
|
-
private readonly onNetwork;
|
|
96
|
-
private readonly symbolicator;
|
|
97
|
-
constructor({ onError, onNetwork, symbolicator }?: SentryCaptureOptions);
|
|
98
|
-
/**
|
|
99
|
-
* Sentry beforeSend hook。
|
|
100
|
-
* 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
|
|
101
|
-
* 符号化完成后触发 onError 回调,原样返回 event。
|
|
102
|
-
*/
|
|
103
|
-
beforeSend(event: ErrorEvent, hint: EventHint): Promise<ErrorEvent>;
|
|
104
|
-
/**
|
|
105
|
-
* Sentry beforeBreadcrumb hook。
|
|
106
|
-
* 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
|
|
107
|
-
*/
|
|
108
|
-
beforeBreadcrumb(breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export { type ErrorCaptureInfo, type MetroFrame, MetroSymbolicator, type NetworkCaptureInfo, SentryCapture, type SentryCaptureOptions };
|
|
2
|
+
export { }
|
package/dist/index.d.ts
CHANGED
|
@@ -1,111 +1,2 @@
|
|
|
1
|
-
import { ErrorEvent, EventHint, Breadcrumb, BreadcrumbHint } from '@sentry/core';
|
|
2
1
|
|
|
3
|
-
|
|
4
|
-
* Sentry Capture 模块
|
|
5
|
-
*
|
|
6
|
-
* 提供 Metro 符号化(MetroSymbolicator)和 Sentry 事件捕获(SentryCapture)。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Metro /symbolicate 接口的帧格式(请求与响应共用)。 */
|
|
10
|
-
interface MetroFrame {
|
|
11
|
-
file: string;
|
|
12
|
-
lineNumber: number;
|
|
13
|
-
column: number;
|
|
14
|
-
methodName: string;
|
|
15
|
-
}
|
|
16
|
-
/** SentryCapture.onError 回调参数 */
|
|
17
|
-
interface ErrorCaptureInfo {
|
|
18
|
-
message: string;
|
|
19
|
-
symbolicatedFrames: MetroFrame[] | null;
|
|
20
|
-
componentStack: string | null;
|
|
21
|
-
/** Sentry Mechanism.type,如 'onerror' / 'generic' / 'auto.function.react.error_boundary' */
|
|
22
|
-
mechanismType: string | null;
|
|
23
|
-
}
|
|
24
|
-
/** SentryCapture.onNetwork 回调参数 */
|
|
25
|
-
interface NetworkCaptureInfo {
|
|
26
|
-
method: string | null;
|
|
27
|
-
url: string | null;
|
|
28
|
-
statusCode: number | null;
|
|
29
|
-
}
|
|
30
|
-
interface SentryCaptureOptions {
|
|
31
|
-
onError?: (info: ErrorCaptureInfo) => void;
|
|
32
|
-
onNetwork?: (info: NetworkCaptureInfo) => void;
|
|
33
|
-
/** 可注入 mock 实例以在单元测试中隔离网络依赖 */
|
|
34
|
-
symbolicator?: MetroSymbolicator;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Metro 符号化器。
|
|
38
|
-
* 将 bundle 行列号还原为源码位置(通过 Metro /symbolicate 接口)。
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* const sym = new MetroSymbolicator();
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* // 单测:注入 baseUrl 并 mock fetch
|
|
45
|
-
* const sym = new MetroSymbolicator({ baseUrl: 'http://localhost:8081' });
|
|
46
|
-
* global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ stack: [...] }) });
|
|
47
|
-
*/
|
|
48
|
-
declare class MetroSymbolicator {
|
|
49
|
-
readonly baseUrl: string;
|
|
50
|
-
constructor({ baseUrl }?: {
|
|
51
|
-
baseUrl?: string;
|
|
52
|
-
});
|
|
53
|
-
/**
|
|
54
|
-
* 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
|
|
55
|
-
* web 平台使用相对路径;native 回退到 localhost:8081。
|
|
56
|
-
*/
|
|
57
|
-
private static _getDefaultBaseUrl;
|
|
58
|
-
/**
|
|
59
|
-
* 解析 Error.stack 字符串为 Metro 所需的帧数组。
|
|
60
|
-
*
|
|
61
|
-
* 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
|
|
62
|
-
* V8/Node.js/Hermes(新):at methodName (url:line:col)
|
|
63
|
-
* Gecko/Firefox/Hermes(旧):methodName@url:line:col
|
|
64
|
-
* JavaScriptCore(iOS):methodName@url:line:col
|
|
65
|
-
*
|
|
66
|
-
* 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
|
|
67
|
-
*/
|
|
68
|
-
parseErrorStack(stack: string): MetroFrame[];
|
|
69
|
-
/**
|
|
70
|
-
* 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
|
|
71
|
-
*/
|
|
72
|
-
symbolicate(stack: MetroFrame[]): Promise<MetroFrame[] | null>;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Sentry 事件捕获器。
|
|
76
|
-
* 封装 beforeSend / beforeBreadcrumb 逻辑,通过注入的回调将事件数据交给调用方处理。
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* const capture = new SentryCapture({
|
|
80
|
-
* onError(info) { console.log(info.message, info.symbolicatedFrames); },
|
|
81
|
-
* onNetwork(info) { console.log(info.method, info.url, info.statusCode); },
|
|
82
|
-
* });
|
|
83
|
-
* Sentry.init({ dsn: '...', beforeSend: capture.beforeSend, beforeBreadcrumb: capture.beforeBreadcrumb });
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* // 单测:注入 mock symbolicator
|
|
87
|
-
* const mockSym = { parseErrorStack: jest.fn(() => []), symbolicate: jest.fn(async () => null) };
|
|
88
|
-
* const onError = jest.fn();
|
|
89
|
-
* const capture = new SentryCapture({ onError, symbolicator: mockSym });
|
|
90
|
-
* await capture.beforeSend(fakeEvent, { originalException: new Error('oops') });
|
|
91
|
-
* expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'oops' }));
|
|
92
|
-
*/
|
|
93
|
-
declare class SentryCapture {
|
|
94
|
-
private readonly onError;
|
|
95
|
-
private readonly onNetwork;
|
|
96
|
-
private readonly symbolicator;
|
|
97
|
-
constructor({ onError, onNetwork, symbolicator }?: SentryCaptureOptions);
|
|
98
|
-
/**
|
|
99
|
-
* Sentry beforeSend hook。
|
|
100
|
-
* 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
|
|
101
|
-
* 符号化完成后触发 onError 回调,原样返回 event。
|
|
102
|
-
*/
|
|
103
|
-
beforeSend(event: ErrorEvent, hint: EventHint): Promise<ErrorEvent>;
|
|
104
|
-
/**
|
|
105
|
-
* Sentry beforeBreadcrumb hook。
|
|
106
|
-
* 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
|
|
107
|
-
*/
|
|
108
|
-
beforeBreadcrumb(breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export { type ErrorCaptureInfo, type MetroFrame, MetroSymbolicator, type NetworkCaptureInfo, SentryCapture, type SentryCaptureOptions };
|
|
2
|
+
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,161 +1,2 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var src_exports = {};
|
|
22
|
-
__export(src_exports, {
|
|
23
|
-
MetroSymbolicator: () => MetroSymbolicator,
|
|
24
|
-
SentryCapture: () => SentryCapture
|
|
25
|
-
});
|
|
26
|
-
module.exports = __toCommonJS(src_exports);
|
|
27
|
-
|
|
28
|
-
// src/capture.ts
|
|
29
|
-
var import_stacktrace_parser = require("stacktrace-parser");
|
|
30
|
-
var import_react_native = require("react-native");
|
|
31
|
-
var MetroSymbolicator = class _MetroSymbolicator {
|
|
32
|
-
constructor({ baseUrl } = {}) {
|
|
33
|
-
this.baseUrl = baseUrl ?? _MetroSymbolicator._getDefaultBaseUrl();
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
|
|
37
|
-
* web 平台使用相对路径;native 回退到 localhost:8081。
|
|
38
|
-
*/
|
|
39
|
-
static _getDefaultBaseUrl() {
|
|
40
|
-
try {
|
|
41
|
-
const scriptURL = import_react_native.NativeModules?.SourceCode?.scriptURL;
|
|
42
|
-
if (scriptURL) {
|
|
43
|
-
const url = new URL(scriptURL);
|
|
44
|
-
return `${url.protocol}//${url.host}`;
|
|
45
|
-
}
|
|
46
|
-
} catch (_) {
|
|
47
|
-
}
|
|
48
|
-
return import_react_native.Platform.OS === "web" ? "" : "http://localhost:8081";
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* 解析 Error.stack 字符串为 Metro 所需的帧数组。
|
|
52
|
-
*
|
|
53
|
-
* 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
|
|
54
|
-
* V8/Node.js/Hermes(新):at methodName (url:line:col)
|
|
55
|
-
* Gecko/Firefox/Hermes(旧):methodName@url:line:col
|
|
56
|
-
* JavaScriptCore(iOS):methodName@url:line:col
|
|
57
|
-
*
|
|
58
|
-
* 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
|
|
59
|
-
*/
|
|
60
|
-
parseErrorStack(stack) {
|
|
61
|
-
return (0, import_stacktrace_parser.parse)(stack).filter(
|
|
62
|
-
(f) => f.file != null && f.lineNumber != null
|
|
63
|
-
).map((f) => ({
|
|
64
|
-
file: f.file,
|
|
65
|
-
lineNumber: f.lineNumber,
|
|
66
|
-
column: f.column != null ? f.column - 1 : 0,
|
|
67
|
-
methodName: f.methodName === "<unknown>" ? "(anonymous)" : f.methodName
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
|
|
72
|
-
*/
|
|
73
|
-
async symbolicate(stack) {
|
|
74
|
-
if (stack.length === 0) return null;
|
|
75
|
-
const endpoint = `${this.baseUrl}/symbolicate`;
|
|
76
|
-
try {
|
|
77
|
-
const res = await fetch(endpoint, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: { "Content-Type": "application/json" },
|
|
80
|
-
body: JSON.stringify({ stack })
|
|
81
|
-
});
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
console.warn(`[SentryCapture] /symbolicate returned ${res.status}`);
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
const data = await res.json();
|
|
87
|
-
return data.stack ?? null;
|
|
88
|
-
} catch (e) {
|
|
89
|
-
console.warn("[SentryCapture] symbolication failed:", e?.message ?? e);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
var SentryCapture = class {
|
|
95
|
-
constructor({ onError, onNetwork, symbolicator } = {}) {
|
|
96
|
-
this.onError = onError;
|
|
97
|
-
this.onNetwork = onNetwork;
|
|
98
|
-
this.symbolicator = symbolicator ?? new MetroSymbolicator();
|
|
99
|
-
this.beforeSend = this.beforeSend.bind(this);
|
|
100
|
-
this.beforeBreadcrumb = this.beforeBreadcrumb.bind(this);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Sentry beforeSend hook。
|
|
104
|
-
* 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
|
|
105
|
-
* 符号化完成后触发 onError 回调,原样返回 event。
|
|
106
|
-
*/
|
|
107
|
-
async beforeSend(event, hint) {
|
|
108
|
-
globalThis.__SENTRY_LAST_RAW__ = { event, hint };
|
|
109
|
-
const message = event.exception?.values?.[0]?.value ?? event.message ?? "(unknown)";
|
|
110
|
-
let symbolicatedFrames = null;
|
|
111
|
-
const originalException = hint?.originalException;
|
|
112
|
-
const rawStack = originalException instanceof Error && typeof originalException.stack === "string" ? originalException.stack : null;
|
|
113
|
-
if (rawStack !== null) {
|
|
114
|
-
const frames = this.symbolicator.parseErrorStack(rawStack);
|
|
115
|
-
if (frames.length > 0) {
|
|
116
|
-
symbolicatedFrames = await this.symbolicator.symbolicate(frames);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (symbolicatedFrames === null) {
|
|
120
|
-
const sentryFrames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
|
|
121
|
-
const mapped = sentryFrames.filter(
|
|
122
|
-
(f) => f.filename != null && f.lineno != null
|
|
123
|
-
).map(
|
|
124
|
-
(f) => ({
|
|
125
|
-
file: f.filename,
|
|
126
|
-
lineNumber: f.lineno,
|
|
127
|
-
column: f.colno ?? 0,
|
|
128
|
-
methodName: f.function ?? "(anonymous)"
|
|
129
|
-
})
|
|
130
|
-
);
|
|
131
|
-
if (mapped.length > 0) {
|
|
132
|
-
symbolicatedFrames = await this.symbolicator.symbolicate(mapped);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const componentStack = event.contexts?.react?.componentStack ?? null;
|
|
136
|
-
const mechanismType = event.exception?.values?.[0]?.mechanism?.type ?? null;
|
|
137
|
-
this.onError?.({ message, symbolicatedFrames, componentStack, mechanismType });
|
|
138
|
-
return event;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Sentry beforeBreadcrumb hook。
|
|
142
|
-
* 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
|
|
143
|
-
*/
|
|
144
|
-
beforeBreadcrumb(breadcrumb, _hint) {
|
|
145
|
-
if (breadcrumb.type === "http") {
|
|
146
|
-
const data = breadcrumb.data;
|
|
147
|
-
this.onNetwork?.({
|
|
148
|
-
method: data?.method ?? null,
|
|
149
|
-
url: data?.url ?? null,
|
|
150
|
-
statusCode: data?.status_code ?? null
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return breadcrumb;
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
157
|
-
0 && (module.exports = {
|
|
158
|
-
MetroSymbolicator,
|
|
159
|
-
SentryCapture
|
|
160
|
-
});
|
|
161
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,133 +1 @@
|
|
|
1
|
-
// src/capture.ts
|
|
2
|
-
import { parse as parseStackTrace } from "stacktrace-parser";
|
|
3
|
-
import { NativeModules, Platform } from "react-native";
|
|
4
|
-
var MetroSymbolicator = class _MetroSymbolicator {
|
|
5
|
-
constructor({ baseUrl } = {}) {
|
|
6
|
-
this.baseUrl = baseUrl ?? _MetroSymbolicator._getDefaultBaseUrl();
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
|
|
10
|
-
* web 平台使用相对路径;native 回退到 localhost:8081。
|
|
11
|
-
*/
|
|
12
|
-
static _getDefaultBaseUrl() {
|
|
13
|
-
try {
|
|
14
|
-
const scriptURL = NativeModules?.SourceCode?.scriptURL;
|
|
15
|
-
if (scriptURL) {
|
|
16
|
-
const url = new URL(scriptURL);
|
|
17
|
-
return `${url.protocol}//${url.host}`;
|
|
18
|
-
}
|
|
19
|
-
} catch (_) {
|
|
20
|
-
}
|
|
21
|
-
return Platform.OS === "web" ? "" : "http://localhost:8081";
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* 解析 Error.stack 字符串为 Metro 所需的帧数组。
|
|
25
|
-
*
|
|
26
|
-
* 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
|
|
27
|
-
* V8/Node.js/Hermes(新):at methodName (url:line:col)
|
|
28
|
-
* Gecko/Firefox/Hermes(旧):methodName@url:line:col
|
|
29
|
-
* JavaScriptCore(iOS):methodName@url:line:col
|
|
30
|
-
*
|
|
31
|
-
* 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
|
|
32
|
-
*/
|
|
33
|
-
parseErrorStack(stack) {
|
|
34
|
-
return parseStackTrace(stack).filter(
|
|
35
|
-
(f) => f.file != null && f.lineNumber != null
|
|
36
|
-
).map((f) => ({
|
|
37
|
-
file: f.file,
|
|
38
|
-
lineNumber: f.lineNumber,
|
|
39
|
-
column: f.column != null ? f.column - 1 : 0,
|
|
40
|
-
methodName: f.methodName === "<unknown>" ? "(anonymous)" : f.methodName
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
|
|
45
|
-
*/
|
|
46
|
-
async symbolicate(stack) {
|
|
47
|
-
if (stack.length === 0) return null;
|
|
48
|
-
const endpoint = `${this.baseUrl}/symbolicate`;
|
|
49
|
-
try {
|
|
50
|
-
const res = await fetch(endpoint, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: { "Content-Type": "application/json" },
|
|
53
|
-
body: JSON.stringify({ stack })
|
|
54
|
-
});
|
|
55
|
-
if (!res.ok) {
|
|
56
|
-
console.warn(`[SentryCapture] /symbolicate returned ${res.status}`);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
const data = await res.json();
|
|
60
|
-
return data.stack ?? null;
|
|
61
|
-
} catch (e) {
|
|
62
|
-
console.warn("[SentryCapture] symbolication failed:", e?.message ?? e);
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
var SentryCapture = class {
|
|
68
|
-
constructor({ onError, onNetwork, symbolicator } = {}) {
|
|
69
|
-
this.onError = onError;
|
|
70
|
-
this.onNetwork = onNetwork;
|
|
71
|
-
this.symbolicator = symbolicator ?? new MetroSymbolicator();
|
|
72
|
-
this.beforeSend = this.beforeSend.bind(this);
|
|
73
|
-
this.beforeBreadcrumb = this.beforeBreadcrumb.bind(this);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Sentry beforeSend hook。
|
|
77
|
-
* 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
|
|
78
|
-
* 符号化完成后触发 onError 回调,原样返回 event。
|
|
79
|
-
*/
|
|
80
|
-
async beforeSend(event, hint) {
|
|
81
|
-
globalThis.__SENTRY_LAST_RAW__ = { event, hint };
|
|
82
|
-
const message = event.exception?.values?.[0]?.value ?? event.message ?? "(unknown)";
|
|
83
|
-
let symbolicatedFrames = null;
|
|
84
|
-
const originalException = hint?.originalException;
|
|
85
|
-
const rawStack = originalException instanceof Error && typeof originalException.stack === "string" ? originalException.stack : null;
|
|
86
|
-
if (rawStack !== null) {
|
|
87
|
-
const frames = this.symbolicator.parseErrorStack(rawStack);
|
|
88
|
-
if (frames.length > 0) {
|
|
89
|
-
symbolicatedFrames = await this.symbolicator.symbolicate(frames);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (symbolicatedFrames === null) {
|
|
93
|
-
const sentryFrames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
|
|
94
|
-
const mapped = sentryFrames.filter(
|
|
95
|
-
(f) => f.filename != null && f.lineno != null
|
|
96
|
-
).map(
|
|
97
|
-
(f) => ({
|
|
98
|
-
file: f.filename,
|
|
99
|
-
lineNumber: f.lineno,
|
|
100
|
-
column: f.colno ?? 0,
|
|
101
|
-
methodName: f.function ?? "(anonymous)"
|
|
102
|
-
})
|
|
103
|
-
);
|
|
104
|
-
if (mapped.length > 0) {
|
|
105
|
-
symbolicatedFrames = await this.symbolicator.symbolicate(mapped);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const componentStack = event.contexts?.react?.componentStack ?? null;
|
|
109
|
-
const mechanismType = event.exception?.values?.[0]?.mechanism?.type ?? null;
|
|
110
|
-
this.onError?.({ message, symbolicatedFrames, componentStack, mechanismType });
|
|
111
|
-
return event;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Sentry beforeBreadcrumb hook。
|
|
115
|
-
* 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
|
|
116
|
-
*/
|
|
117
|
-
beforeBreadcrumb(breadcrumb, _hint) {
|
|
118
|
-
if (breadcrumb.type === "http") {
|
|
119
|
-
const data = breadcrumb.data;
|
|
120
|
-
this.onNetwork?.({
|
|
121
|
-
method: data?.method ?? null,
|
|
122
|
-
url: data?.url ?? null,
|
|
123
|
-
statusCode: data?.status_code ?? null
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
return breadcrumb;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
export {
|
|
130
|
-
MetroSymbolicator,
|
|
131
|
-
SentryCapture
|
|
132
|
-
};
|
|
133
1
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -4,12 +4,10 @@ var import_lgui_control = require("./lgui-control");
|
|
|
4
4
|
var import_css_control = require("./css-control");
|
|
5
5
|
var import_router_control = require("./router-control");
|
|
6
6
|
var import_screenshot_control = require("./screenshot-control");
|
|
7
|
-
var import_preview_control = require("./preview-control");
|
|
8
7
|
globalThis.__DEVKIT_INJECTED__ = true;
|
|
9
8
|
(0, import_hmr_control.setupHMRMessageControl)();
|
|
10
9
|
(0, import_lgui_control.setupLGUIMessageControl)();
|
|
11
10
|
(0, import_css_control.setupCSSInjectionControl)();
|
|
12
11
|
(0, import_router_control.setupRouterControl)();
|
|
13
12
|
(0, import_screenshot_control.setupScreenshotControl)();
|
|
14
|
-
(0, import_preview_control.setupPreviewControl)();
|
|
15
13
|
//# sourceMappingURL=entry-inject.js.map
|
|
@@ -24,193 +24,28 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
|
|
25
25
|
// src/stubs/sentry-react-native-stub.ts
|
|
26
26
|
var RealSentry = __toESM(require("@sentry/react-native"));
|
|
27
|
-
|
|
28
|
-
// src/capture.ts
|
|
29
|
-
var import_stacktrace_parser = require("stacktrace-parser");
|
|
27
|
+
var import_core = require("@sentry/core");
|
|
30
28
|
var import_react_native = require("react-native");
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
* 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
|
|
37
|
-
* web 平台使用相对路径;native 回退到 localhost:8081。
|
|
38
|
-
*/
|
|
39
|
-
static _getDefaultBaseUrl() {
|
|
40
|
-
try {
|
|
41
|
-
const scriptURL = import_react_native.NativeModules?.SourceCode?.scriptURL;
|
|
42
|
-
if (scriptURL) {
|
|
43
|
-
const url = new URL(scriptURL);
|
|
44
|
-
return `${url.protocol}//${url.host}`;
|
|
45
|
-
}
|
|
46
|
-
} catch (_) {
|
|
47
|
-
}
|
|
48
|
-
return import_react_native.Platform.OS === "web" ? "" : "http://localhost:8081";
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* 解析 Error.stack 字符串为 Metro 所需的帧数组。
|
|
52
|
-
*
|
|
53
|
-
* 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
|
|
54
|
-
* V8/Node.js/Hermes(新):at methodName (url:line:col)
|
|
55
|
-
* Gecko/Firefox/Hermes(旧):methodName@url:line:col
|
|
56
|
-
* JavaScriptCore(iOS):methodName@url:line:col
|
|
57
|
-
*
|
|
58
|
-
* 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
|
|
59
|
-
*/
|
|
60
|
-
parseErrorStack(stack) {
|
|
61
|
-
return (0, import_stacktrace_parser.parse)(stack).filter(
|
|
62
|
-
(f) => f.file != null && f.lineNumber != null
|
|
63
|
-
).map((f) => ({
|
|
64
|
-
file: f.file,
|
|
65
|
-
lineNumber: f.lineNumber,
|
|
66
|
-
column: f.column != null ? f.column - 1 : 0,
|
|
67
|
-
methodName: f.methodName === "<unknown>" ? "(anonymous)" : f.methodName
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
|
|
72
|
-
*/
|
|
73
|
-
async symbolicate(stack) {
|
|
74
|
-
if (stack.length === 0) return null;
|
|
75
|
-
const endpoint = `${this.baseUrl}/symbolicate`;
|
|
76
|
-
try {
|
|
77
|
-
const res = await fetch(endpoint, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: { "Content-Type": "application/json" },
|
|
80
|
-
body: JSON.stringify({ stack })
|
|
81
|
-
});
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
console.warn(`[SentryCapture] /symbolicate returned ${res.status}`);
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
const data = await res.json();
|
|
87
|
-
return data.stack ?? null;
|
|
88
|
-
} catch (e) {
|
|
89
|
-
console.warn("[SentryCapture] symbolication failed:", e?.message ?? e);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
var SentryCapture = class {
|
|
95
|
-
constructor({ onError, onNetwork, symbolicator } = {}) {
|
|
96
|
-
this.onError = onError;
|
|
97
|
-
this.onNetwork = onNetwork;
|
|
98
|
-
this.symbolicator = symbolicator ?? new MetroSymbolicator();
|
|
99
|
-
this.beforeSend = this.beforeSend.bind(this);
|
|
100
|
-
this.beforeBreadcrumb = this.beforeBreadcrumb.bind(this);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Sentry beforeSend hook。
|
|
104
|
-
* 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
|
|
105
|
-
* 符号化完成后触发 onError 回调,原样返回 event。
|
|
106
|
-
*/
|
|
107
|
-
async beforeSend(event, hint) {
|
|
108
|
-
globalThis.__SENTRY_LAST_RAW__ = { event, hint };
|
|
109
|
-
const message = event.exception?.values?.[0]?.value ?? event.message ?? "(unknown)";
|
|
110
|
-
let symbolicatedFrames = null;
|
|
111
|
-
const originalException = hint?.originalException;
|
|
112
|
-
const rawStack = originalException instanceof Error && typeof originalException.stack === "string" ? originalException.stack : null;
|
|
113
|
-
if (rawStack !== null) {
|
|
114
|
-
const frames = this.symbolicator.parseErrorStack(rawStack);
|
|
115
|
-
if (frames.length > 0) {
|
|
116
|
-
symbolicatedFrames = await this.symbolicator.symbolicate(frames);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (symbolicatedFrames === null) {
|
|
120
|
-
const sentryFrames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
|
|
121
|
-
const mapped = sentryFrames.filter(
|
|
122
|
-
(f) => f.filename != null && f.lineno != null
|
|
123
|
-
).map(
|
|
124
|
-
(f) => ({
|
|
125
|
-
file: f.filename,
|
|
126
|
-
lineNumber: f.lineno,
|
|
127
|
-
column: f.colno ?? 0,
|
|
128
|
-
methodName: f.function ?? "(anonymous)"
|
|
129
|
-
})
|
|
130
|
-
);
|
|
131
|
-
if (mapped.length > 0) {
|
|
132
|
-
symbolicatedFrames = await this.symbolicator.symbolicate(mapped);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const componentStack = event.contexts?.react?.componentStack ?? null;
|
|
136
|
-
const mechanismType = event.exception?.values?.[0]?.mechanism?.type ?? null;
|
|
137
|
-
this.onError?.({ message, symbolicatedFrames, componentStack, mechanismType });
|
|
138
|
-
return event;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Sentry beforeBreadcrumb hook。
|
|
142
|
-
* 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
|
|
143
|
-
*/
|
|
144
|
-
beforeBreadcrumb(breadcrumb, _hint) {
|
|
145
|
-
if (breadcrumb.type === "http") {
|
|
146
|
-
const data = breadcrumb.data;
|
|
147
|
-
this.onNetwork?.({
|
|
148
|
-
method: data?.method ?? null,
|
|
149
|
-
url: data?.url ?? null,
|
|
150
|
-
statusCode: data?.status_code ?? null
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return breadcrumb;
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// src/stubs/sentry-react-native-stub.ts
|
|
158
|
-
function getParentOrigin() {
|
|
159
|
-
try {
|
|
160
|
-
const w = globalThis.window;
|
|
161
|
-
if (!w) return "*";
|
|
162
|
-
return new URLSearchParams(w.location.search).get("parentOrigin") || "*";
|
|
163
|
-
} catch {
|
|
164
|
-
return "*";
|
|
165
|
-
}
|
|
29
|
+
var SANDBOX_MARKER = /* @__PURE__ */ Symbol.for("vite-sandbox.expo-browser-observability.v2");
|
|
30
|
+
var FALLBACK_DSN = process.env["INJECT_SENTRY_DSN"] ?? "https://stubPublicKey@o0.ingest.sentry.io/0";
|
|
31
|
+
function reportVersionMismatch(expected, actual) {
|
|
32
|
+
const message = `Sentry SDK \u7248\u672C\u4E0D\u4E00\u81F4\uFF1Asandbox=${expected}\uFF0C@sentry/react-native core=${actual || "unknown"}\u3002\u89C2\u6D4B\u4E8B\u4EF6\u53EF\u80FD\u9759\u9ED8\u4E22\u5931\u3002`;
|
|
33
|
+
console.error(`[miaoda-expo-devkit] ${message}`);
|
|
166
34
|
}
|
|
167
|
-
var OVERRIDE_DSN = process.env["INJECT_SENTRY_DSN"] ?? "https://stubPublicKey@o0.ingest.sentry.io/0";
|
|
168
|
-
var _capture = new SentryCapture({
|
|
169
|
-
onError(info) {
|
|
170
|
-
console.warn("[SentryStub] \u6355\u83B7\u5230\u9519\u8BEF\uFF1A", info.message);
|
|
171
|
-
if (info.symbolicatedFrames?.length) {
|
|
172
|
-
const top = info.symbolicatedFrames[0];
|
|
173
|
-
console.warn(` at ${top.methodName} (${top.file}:${top.lineNumber}:${top.column})`);
|
|
174
|
-
}
|
|
175
|
-
const w = globalThis.window;
|
|
176
|
-
if (w !== void 0 && w !== w.parent) {
|
|
177
|
-
w.parent.postMessage(
|
|
178
|
-
{
|
|
179
|
-
type: "GLOBAL_ERROR",
|
|
180
|
-
error: {
|
|
181
|
-
message: info.message,
|
|
182
|
-
symbolicatedFrames: info.symbolicatedFrames,
|
|
183
|
-
componentStack: info.componentStack,
|
|
184
|
-
mechanismType: info.mechanismType
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
getParentOrigin()
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
35
|
var stub = {
|
|
193
36
|
...RealSentry,
|
|
194
37
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
38
|
+
* Native 保留应用初始化;独立 Web 使用安全 DSN;sandbox Web 已由前置运行时
|
|
39
|
+
* 初始化,因此这里只校验版本并阻止第二次初始化。
|
|
197
40
|
*/
|
|
198
41
|
init: ((options) => {
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return upstreamBeforeSend ? await upstreamBeforeSend(processed, hint) : processed;
|
|
207
|
-
},
|
|
208
|
-
beforeBreadcrumb(breadcrumb, hint) {
|
|
209
|
-
const processed = _capture.beforeBreadcrumb(breadcrumb, hint);
|
|
210
|
-
return upstreamBeforeBreadcrumb ? upstreamBeforeBreadcrumb(processed, hint) : processed;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
return RealSentry.init(initOptions);
|
|
42
|
+
if (import_react_native.Platform.OS !== "web") return RealSentry.init(options);
|
|
43
|
+
const sandboxVersion = globalThis[SANDBOX_MARKER];
|
|
44
|
+
if (typeof sandboxVersion === "string") {
|
|
45
|
+
if (sandboxVersion !== import_core.SDK_VERSION) reportVersionMismatch(sandboxVersion, import_core.SDK_VERSION);
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
return RealSentry.init({ ...options, dsn: FALLBACK_DSN });
|
|
214
49
|
})
|
|
215
50
|
};
|
|
216
51
|
module.exports = stub;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miaoda-expo-devkit",
|
|
3
|
-
"version": "0.1.1-beta.
|
|
3
|
+
"version": "0.1.1-beta.114",
|
|
4
4
|
"description": "Expo 应用开发工具集:Sentry DSN 替换 stub、错误/网络捕获、Metro 符号化",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -90,8 +90,7 @@
|
|
|
90
90
|
"lodash.throttle": "^4.1.1",
|
|
91
91
|
"metro-minify-esbuild": "^0.2.0",
|
|
92
92
|
"oxc-resolver": "^11.19.1",
|
|
93
|
-
"package-up": "^5.0.0"
|
|
94
|
-
"stacktrace-parser": "^0.1.11"
|
|
93
|
+
"package-up": "^5.0.0"
|
|
95
94
|
},
|
|
96
95
|
"peerDependencies": {
|
|
97
96
|
"@expo/config-plugins": ">=55.0.0",
|
|
@@ -279,7 +279,7 @@ new file mode 100644
|
|
|
279
279
|
+ const errorBuildID = isHmrError ? activeBuildID : buildID ?? lastFailedBuildID ?? activeBuildID;
|
|
280
280
|
+ if (!isCurrentBuild(errorBuildID, true)) return;
|
|
281
281
|
+ if (isHmrError) {
|
|
282
|
-
+ // HMR
|
|
282
|
+
+ // HMR errors do not carry Metro build IDs; keep the change token for its recovery event.
|
|
283
283
|
+ activeHmrChangeID = event.changeID ?? activeHmrChangeID;
|
|
284
284
|
+ }
|
|
285
285
|
+ lastFailedBuildID = errorBuildID;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
var preview_control_exports = {};
|
|
20
|
-
__export(preview_control_exports, {
|
|
21
|
-
setupPreviewControl: () => setupPreviewControl
|
|
22
|
-
});
|
|
23
|
-
module.exports = __toCommonJS(preview_control_exports);
|
|
24
|
-
function getParentOrigin() {
|
|
25
|
-
try {
|
|
26
|
-
const w = globalThis.window;
|
|
27
|
-
if (!w) return "*";
|
|
28
|
-
return new URLSearchParams(w.location.search).get("parentOrigin") || "*";
|
|
29
|
-
} catch {
|
|
30
|
-
return "*";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function postPreviewSuccess() {
|
|
34
|
-
const w = globalThis.window;
|
|
35
|
-
if (!w || w.editorPreviewSuccessPosted) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
w.editorPreviewSuccessPosted = true;
|
|
39
|
-
w.parent.postMessage(
|
|
40
|
-
{
|
|
41
|
-
type: "editor-preview-success"
|
|
42
|
-
},
|
|
43
|
-
getParentOrigin()
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
function setupPreviewControl() {
|
|
47
|
-
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (document.readyState === "complete") {
|
|
51
|
-
postPreviewSuccess();
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
window.addEventListener("load", postPreviewSuccess, { once: true });
|
|
55
|
-
}
|
|
56
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
57
|
-
0 && (module.exports = {
|
|
58
|
-
setupPreviewControl
|
|
59
|
-
});
|
|
60
|
-
//# sourceMappingURL=preview-control.js.map
|