# @arms/rum-electron

阿里云 ARMS 用户体验监控 Electron SDK。一次主进程 `init()` 即覆盖主进程与渲染进程：自动采集异常、原生崩溃、HTTP/tRPC、应用启动指标、页面 PV、性能、Web Vitals 等数据，由主进程统一上报。

## 核心特性

- **零配置自动注入**：主进程 `init()` 后，所有 `BrowserWindow` 自动注入 Browser SDK 与 IPC Bridge，渲染进程零改造
- **统一上报通道**：渲染进程事件经 `arms:rum-bridge` IPC 通道回流至主进程，由主进程统一组织与上报
- **主进程监控全覆盖**：
  - 未捕获异常、未处理 Promise 拒绝、`console.error` 拦截
  - 原生崩溃（集成 WASM `minidump-processor`，解析 `.dmp` 获取完整堆栈与模块）
  - 应用启动指标（`app ready` 耗时、进程数、主进程 CPU/内存）
  - HTTP 请求自动采集：`globalThis.fetch` 与 `http`/`https` 模块双层 patch（覆盖 **axios 默认 adapter**、got、node-fetch 等），自动产出 `type='api'` 资源事件，附带 DNS/TCP/TLS/TTFB/下载等细分耗时
  - tRPC server 端 procedure 调用（`type='rpc'`），通过 `armsRum.instrumentTRPC()` 一行接入
- **渲染进程自动注入**：PV、性能、Web Vitals、白屏、API、长任务等
- **分布式链路追踪**：内置 `tracing` 配置，支持 W3C tracecontext / B3 / B3 multi / Jaeger / SW8；主进程 fetch、http/https 模块与 tRPC procedure 共享同一份决策
- **远程配置**：支持 ARMS 控制台下发配置，动态调整采样率、采集器开关等

## 安装

```bash
npm install @arms/rum-electron
```

| 依赖 | 版本要求 | 说明 |
|------|----------|------|
| `electron` | `>= 28.0.0` | `peerDependency`（optional），由项目自行安装 |
| `@trpc/server` | `*` | 仅在使用 `instrumentTRPC()` 时由业务侧安装（optional peer） |

> 需要 Electron 28+ 是因为 SDK 使用 `session.registerPreloadScript()`（v28 新增），低版本会自动回退到 `session.setPreloads()`。

## 快速接入

### 1. 主进程顶层 `import` 并 `init()`

**SDK 必须在 `app.ready` 之前被 `import`**——`src/index.ts` 顶层会调用 `protocol.registerSchemesAsPrivileged()` 注册 `rum-event` 协议（占位用，预留给后续降级通道，不影响功能）。

```typescript
// main/index.ts —— 文件顶部
import armsRum from '@arms/rum-electron';
import { app } from 'electron';

armsRum.init({
  endpoint: '<your-endpoint>',  // 控制台「用户体验监控 > 应用列表」创建应用后获取
  env: 'prod',                  // 'prod' | 'gray' | 'pre' | 'daily' | 'local'
  version: '1.0.0',
});

app.whenReady().then(() => {
  // 创建 BrowserWindow 等
});
```

### 2. 渲染进程零改造

默认 `autoInject: true`：SDK 监听 `web-contents-created`，在每个 `BrowserWindow` 的 `dom-ready` 时机注入 Browser SDK 脚本与 IPC Bridge。渲染进程**既不需要** `import` SDK，**也不需要**修改 preload。

### 3.（可选）验证数据

```typescript
armsRum.init({
  endpoint: '<your-endpoint>',
  beforeReport(bundle) {
    console.log('[RUM]', bundle);
    return bundle;     // 返回 undefined 不会丢弃；若需丢弃请显式处理
  },
});
```

### 4.（可选）自定义 partition

`BrowserWindow` 使用了自定义 `partition`（如 `'persist:main'`）时，必须显式声明，否则该 partition 下的窗口不会注入 IPC Bridge：

```typescript
// 方式一：init 时声明（单 partition 推荐）
await armsRum.init({
  endpoint: '<your-endpoint>',
  partition: 'persist:main',
});

// 方式二：init 之后动态注册（多 partition 场景）
await armsRum.init({ endpoint: '<your-endpoint>' });
await armsRum.registerSession('persist:other');
```

### 5.（可选）启用 tRPC 监控

主进程使用 tRPC 定义 server router 时（如 `electron-trpc`），用 `armsRum.instrumentTRPC()` 包一层 `t`，所有 procedure 自动带监控 middleware：

```typescript
import { initTRPC } from '@trpc/server';
import armsRum from '@arms/rum-electron';

const t = armsRum.instrumentTRPC(initTRPC.create());

export const appRouter = t.router({
  greeting: t.procedure.input(...).query(...),
});
```

详见 [`docs/接入Electron应用.md`](./docs/接入Electron应用.md) 中的 tRPC 监控章节。

> 主进程作为 tRPC client 调用云端 HTTP 服务时**无需额外接入**：底层 fetch 或 http/https 请求会被 `ApiCollector` 自动采集为 `type='api'` 事件。

## 配置一览

`init(config)` 接收 `IElectronConfig`，继承自 `@arms/rum-core` 的 `IConfiguration`。下表为常用字段，完整版见 [`docs/Electron SDK配置参考.md`](./docs/Electron%20SDK配置参考.md)。

### 基础

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `endpoint` | `string` | 是 | — | ARMS 数据上报地址 |
| `enable` | `boolean` | 否 | `true` | 关闭后所有采集器与上报均不工作 |
| `env` | `'prod' \| 'gray' \| 'pre' \| 'daily' \| 'local' \| string` | 否 | — | 应用环境标识 |
| `version` | `string` | 否 | — | 应用版本号 |
| `app` | `object` | 否 | — | 应用扩展信息（`name` / `channel` / `framework` 等） |
| `user` | `object` | 否 | — | 用户信息（`id` / `name` / `tags`） |

### Electron 专属

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `autoInject` | `boolean` | `true` | 是否自动向 `BrowserWindow` 注入 Browser SDK |
| `partition` | `string` | — | 自定义 session partition；与 `BrowserWindow.webPreferences.partition` 对应 |
| `spaMode` | `false \| true \| 'auto' \| 'hash' \| 'history'` | `false` | SPA 路由追踪模式 |
| `tracing` | `boolean \| ITracingOption` | — | 分布式链路追踪（主进程 fetch + tRPC procedure 共用） |
| `evaluateApi` | `(request, response, error?) => Promise<IApiBaseAttr>` | — | 自定义 API/RPC payload 解析，返回值经 `reviseApiAttr` 裁剪后合并 |
| `parseViewName` | `(url: string) => string` | — | 自定义页面 name 解析（兜底用 SpaMode 解析 URL） |
| `parseResourceName` | `(url: string) => string` | — | 自定义资源 name 解析；默认取 URL pathname |
| `beforeReport` | `(bundle) => any` | — | 上报前回调，可改写 bundle |
| `browserCollectors` | `Record<string, boolean \| ICollectorConfig>` | 全部启用 | 渲染进程 Browser SDK 采集器开关（仅 `autoInject` 模式生效） |
| `properties` | `Record<string, number \| string>` | — | 全局自定义属性，附加到所有上报事件 |

### 主进程采集器（collectors）

```typescript
armsRum.init({
  endpoint: '<your-endpoint>',
  collectors: {
    crash: false,            // 关闭崩溃采集
    consoleError: false,     // 关闭 console.error 拦截
    api: {
      enable: true,
      filters: [/\.internal\.example\.com/],   // 命中即不上报
    },
    rpc: {
      enable: true,
      filters: [/^internal\./],                // 按 procedure path 跳过
    },
    memory: {
      enable: true,
      interval: 1_800_000,                     // 窗口聚合上报间隔 ms，默认 30min（最小值同样为 30min）
      onLaunch: true,                          // app ready 时发出启动基线事件
      onInterval: true,                        // 启用窗口聚合周期采集
      onCrash: true,                           // render/child-process-gone 时立即 flush
      onQuit: true,                            // before-quit 时立即 flush
    },
    anr: {
      enable: true,                            // 默认关闭，需显式开启
      anrThreshold: 5000,                      // ANR 阈值 ms（3000–30000）
      captureStackTrace: true,                 // 渲染进程调用栈采集（需 Electron >= 34 + Feature Flag）
      main: true,
      renderer: true,
    },
  },
});
```

| 采集器 | 默认 | 说明 |
|--------|------|------|
| `jsError` | `true` | 主进程未捕获异常 + 未处理 Promise 拒绝 |
| `consoleError` | `true` | `console.error` 拦截上报 |
| `crash` | `true` | 原生崩溃采集（依赖 `crashReporter`） |
| `application` | `true` | 应用启动指标 |
| `memory` | `false` | 运行时内存水位（10s 后台采样 / 30min 窗口聚合 + crash/quit 触发；事件附 `context` 公用环境快照） |
| `api` | `true` | 主进程 HTTP 请求采集：`globalThis.fetch` + `http`/`https` 模块 patch，覆盖 axios 默认 adapter 等（`type='api'`，附细分耗时字段） |
| `rpc` | `true` | tRPC server middleware（配合 `instrumentTRPC`，`type='rpc'`） |
| `anr` | `false` | 应用未响应监控（主进程 worker + V8 Inspector 采集调用栈，降级为 setTimeout 探针；渲染进程心跳超时，Electron >= 34 可采集渲染进程调用栈） |

### ANR 监控

`AnrCollector` 检测主进程与渲染进程的事件循环长时间阻塞（Application Not Responding），作为 `EXCEPTION` 事件上报：`exception.type='anr'`，`exception.source='main_anr'`（主进程）或 `'renderer_anr'`（渲染进程）。

- **主进程**：独立 `worker_threads` watchdog 通过 IPC 心跳检测主线程阻塞，阻塞时经 V8 Inspector（`connectToMainThread` + `Debugger.pause`）采集主线程调用栈（`snapshots.timing='on-block'`，`snapshots.stack_source='inspector_worker'`）；worker 不可用或 Inspector 超时时自动降级为 `setTimeout` 探针漂移检测，此路径无调用栈、仅 post-block 进程快照（`snapshots.timing='post-block'`）。
- **渲染进程**：主进程作为外部观察者，监测渲染进程经 preload 定时器发送的 IPC 心跳超时；超时后经 `webContents.mainFrame.collectJavaScriptCallStack()` 采集调用栈（`snapshots.timing='on-detect'`）。

> ℹ️ **渲染进程心跳机制**：心跳由 SDK preload 脚本内的 `setInterval` 自主驱动，与渲染进程主线程共用同一事件循环——主线程被同步阻塞时心跳随之停发，从而被主进程 watchdog 判定为 ANR。无需主进程 `executeJavaScript` 注入。
- **防抖与限流**：同源同窗口 120s 内最多 1 条；全局 30min 内最多 5 条；ANR 触发后 5s 恢复静默窗；被抑制次数记入 `snapshots.suppressed_count`。
- **`caused_by`**（仅主进程，启发式推断）：系统可用内存 < 15% → `memory_pressure`；主进程 CPU > 80% → `cpu_saturation`；否则 `event_loop_blocked`。

> ⚠️ **渲染进程调用栈采集需用户设置 Feature Flag**：在 `app.ready` 之前、`import` SDK 之前调用
> `app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports')`。
> SDK 自动注入 `Document-Policy: include-js-call-stacks-in-crash-reports` 响应头；未设置 flag 时渲染进程 ANR 仍会上报，但 `stack` 为空。调用栈采集需 Electron >= 34，低版本自动降级为无栈事件。
>
> 已知限制：Electron 每个 session 每 event 仅允许一个 `onHeadersReceived` listener，SDK 注入会覆盖既有 listener；需保留自定义 listener 时设 `autoInjectDocumentPolicy: false`。

### 渲染进程采集器（browserCollectors）

`autoInject: true` 时控制注入到渲染进程的 Browser SDK 采集器：

| 采集器 | 说明 |
|--------|------|
| `perf` | 页面加载性能 |
| `webvitals` | Web Vitals（LCP / FID / CLS） |
| `exception` | 未捕获异常 + Promise 拒绝 |
| `whiteScreen` | 白屏检测 |
| `api` | XHR / Fetch HTTP 请求 |
| `staticResource` | 静态资源加载 |
| `click` / `action` | 用户交互 |
| `longTask` | 长任务（>50ms） |

```typescript
armsRum.init({
  endpoint: '<your-endpoint>',
  browserCollectors: {
    longTask: false,
    whiteScreen: false,
    api: { enable: true },
  },
});
```

### 链路追踪（tracing）

```typescript
armsRum.init({
  endpoint: '<your-endpoint>',
  tracing: {
    enable: true,
    sample: 10,                                    // 10% 采样（取值 0–100）
    propagatorTypes: ['tracecontext', 'b3'],
    allowedUrls: [/^https:\/\/api\.example\.com/], // 仅向白名单注入追踪头
  },
});
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `enable` | `boolean` | 是否启用链路追踪 |
| `sample` | `number` | 采样率，取值 **0–100**（默认 100） |
| `propagatorTypes` | `Array<'tracecontext' \| 'b3' \| 'b3multi' \| 'jaeger' \| 'sw8'>` | 传播协议 |
| `allowedUrls` | `Array<MatchOption \| TraceOption>` | 命中规则的 URL/path 才会注入追踪头；未配置时默认全量命中 |
| `tracestate` / `baggage` | `boolean` | 是否携带 W3C tracestate / baggage |

主进程 fetch 与 http/https 模块请求（`ApiCollector`）、tRPC procedure（`RpcCollector`）共用同一份 `tracing` 决策，自动在 outbound 请求注入 `traceparent` 等头。

### evaluateApi：把 payload 写入 snapshots

`evaluateApi` 用于把请求/响应内容写入事件。**SDK 不会自动采集 payload**——业务侧按需提取并经返回值 `snapshots` 上报，SDK 自动经 `reviseApiAttr` 裁剪至 5KB：

```typescript
armsRum.init({
  endpoint: '<your-endpoint>',
  async evaluateApi(request, response, error) {
    const snapshot: Record<string, unknown> = {};
    if (request && typeof request === 'object' && 'input' in request) {
      snapshot.input = (request as { input?: unknown }).input;
    }
    if (response && typeof response === 'object' && 'data' in response) {
      snapshot.output = (response as { data?: unknown }).data;
    }
    return {
      success: error ? 0 : 1,
      snapshots: JSON.stringify(snapshot),
    };
  },
});
```

> 字段约定：`snapshots` 留给 SDK / `evaluateApi` 写采集元数据，`properties` 保留给用户业务标签。回调有 50ms 超时硬限，超时后回退到原始事件。

## 运行时 API

```typescript
// 修改单个字段
armsRum.setConfig('enable', false);

// 批量修改
armsRum.setConfig({ env: 'staging', version: '1.1.0' });

// 读取当前完整配置
const config = armsRum.getConfig();

// 为额外的 partition 注册 preload（init 之后调用）
await armsRum.registerSession('persist:other');

// 一行接入 tRPC server 监控（需 @trpc/server）
const t = armsRum.instrumentTRPC(initTRPC.create());
```

## 注意事项

1. **`init()` 必须在 `app.ready` 之前调用**，且 SDK 模块本身必须更早被 `import`——`src/index.ts` 顶层调用 `protocol.registerSchemesAsPrivileged()`，错过 `ready` 之前的窗口会导致协议注册失败并触发 Electron 警告。
2. **Electron 版本 `>= 28`**：SDK 优先使用 `session.registerPreloadScript()`（v28 新增），低版本自动降级到 `session.setPreloads()`。
3. **不要覆盖 SDK 注册的 preload**：若应用层调用 `session.setPreloads()` 覆盖全部 preload，渲染进程事件将无法回流。建议使用追加方式。
4. **`autoInject: true` 模式下禁止在渲染进程手动 `import '@arms/rum-electron/browser'`**：会导致重复初始化与事件双采。
5. **自定义 partition 必须显式声明**：未声明的 partition 下的窗口里 `window.ArmsEventBridge` 为 `undefined`，RUM 数据无法上报。
6. **`evaluateApi` 内禁止同步阻塞**：回调有 50ms 超时硬限，超时后回退到原始事件不抛错。
7. **tRPC 监控仅采 server 端**：主进程作为 tRPC client 调用云端 HTTP 时由 `ApiCollector` 在 fetch / http 模块层自动采集为 `type='api'`，不需要再包 link。
