@jasw/web-monitor
v0.1.2
Published
Lightweight, plugin-based frontend monitoring SDK for web performance, errors, and user actions.
Maintainers
Readme
@jasw/web-monitor
一个面向 Web 应用的轻量级全链路监控 SDK。它以插件为边界采集错误、性能、行为和页面浏览,使用空闲批量上报,运行时零第三方依赖,并兼容 Chrome 80+ 与 Safari 14+。
特性
- TypeScript
strict类型与Result<T, E>错误模型; - 单一 npm 包,支持
@jasw/web-monitor/error等子路径按需引入; - ESM + CJS 双格式,并分别提供 NodeNext 可解析的
.d.mts/.d.cts声明; - 普通刷新使用可确认 HTTP 状态、可取消的
fetch,页面隐藏时优先navigator.sendBeacon; AbortController取消、5 秒默认超时、flush 并发锁;requestIdleCallback空闲聚合,每批最多 10 条;- 页面进入 hidden 时立即刷新队列;
- 可选 localStorage 离线队列,按标签页隔离并设置容量与过期边界;
- 默认脱敏 URL query、hash 路由和错误堆栈中的常见凭证参数;
- 内置 Error、Performance、Action、PV 四个插件,均可独立 Tree Shaking。
设计取舍
这个项目更看重“接入后不打扰业务”。核心包只负责事件信封、队列和传输,具体采集能力放在插件中,
这样可以按应用实际需要选择功能;平时利用空闲时间通过 fetch 批量发送,以便确认 HTTP 结果并支持取消,
页面离开时则优先把数据交给 Beacon。SDK 不试图替代后端的告警、查询和数据保留系统,reportUrl 指向你的
采集接口,服务端仍需要自行完成鉴权、限流和数据治理。
代码里的注释也遵循同一取舍:公共 API、浏览器兼容处理和容易被误改的状态转换会解释“为什么”, 简单的赋值和显而易见的分支不逐行加注释,避免源码被重复说明淹没。
安装与最小用法
pnpm add @jasw/web-monitor
# npm 用户也可以:npm install @jasw/web-monitorimport Tracker from '@jasw/web-monitor';
import { ErrorPlugin } from '@jasw/web-monitor/error';
import { PerformancePlugin } from '@jasw/web-monitor/performance';
import { ActionPlugin } from '@jasw/web-monitor/action';
import { PVPlugin } from '@jasw/web-monitor/pv';
const tracker = new Tracker();
tracker.use(new ErrorPlugin());
tracker.use(new PerformancePlugin());
tracker.use(new ActionPlugin());
tracker.use(new PVPlugin());
const initialized = tracker.init({
appId: 'demo-app',
reportUrl: 'https://collector.example.com/events',
sampleRate: 1,
});
if (!initialized.ok) {
console.error(initialized.error);
}
tracker.report({
event_type: 'checkout_started',
tags: { env: 'production' },
payload: { cart_size: 3 },
});Tracker 同时提供命名导出和 createTracker() 工厂。插件必须显式 use,不会在 init 时自动启用,因此可以按需控制采集权限与最终体积。
CommonJS 项目使用 require 时,默认导出位于 .default,命名导出保持不变:
const { default: Tracker } = require('@jasw/web-monitor');
const { ErrorPlugin } = require('@jasw/web-monitor/error');
const tracker = new Tracker();
tracker.use(new ErrorPlugin());
tracker.init({
appId: 'demo-app',
reportUrl: 'https://collector.example.com/events',
sampleRate: 1,
});应用层初始化(推荐)
生产环境通常只在应用启动层创建并初始化一个 Tracker,而不是在每个路由组件或业务页面中重复初始化。可以把实例放在一个独立模块中,之后所有组件只引用这个共享实例。
// src/monitoring.ts
import Tracker, { type Result, type TrackerError } from '@jasw/web-monitor';
import { ErrorPlugin } from '@jasw/web-monitor/error';
import { PerformancePlugin } from '@jasw/web-monitor/performance';
import { ActionPlugin } from '@jasw/web-monitor/action';
import { PVPlugin } from '@jasw/web-monitor/pv';
export const tracker = new Tracker();
let started = false;
export function startMonitoring(): Result<void, TrackerError> {
// 防止 React StrictMode、HMR 或重复调用导致重复注册。
if (started) {
return { ok: true, value: undefined };
}
for (const plugin of [
new ErrorPlugin(),
new PerformancePlugin(),
new ActionPlugin(),
new PVPlugin(),
]) {
const result = tracker.use(plugin);
if (!result.ok) {
return result;
}
}
const result = tracker.init({
appId: 'shop-web',
// Vite 示例:从 VITE_TRACKER_REPORT_URL 注入;CRA/Next/Nuxt 请替换为对应的环境变量读取方式。
reportUrl: import.meta.env.VITE_TRACKER_REPORT_URL,
sampleRate: 1,
persistQueue: true,
});
if (result.ok) {
started = true;
}
return result;
}在 Vue、React 或其他前端框架的应用入口调用一次即可:
// Vue: main.ts;React: main.tsx;其他框架放在 client 入口
import { startMonitoring } from './monitoring';
const result = startMonitoring();
if (!result.ok) {
console.error('监控 SDK 初始化失败', result.error);
}业务组件只需要上报自己的业务事件:
import { tracker } from '@/monitoring';
tracker.report({
event_type: 'checkout_error',
tags: { module: 'checkout' },
payload: { reason: 'payment_timeout' },
});Tracker 不会默认挂到 window.tracker,也不会在模块导入时自动注册监听器。只有调用 use 并成功 init 后,插件才会绑定当前文档的浏览器能力。把实例放在应用入口,依然可以监听当前页面的全局错误,同时保留测试和微前端需要的隔离性。
React(Vite / CRA)
上面的 src/monitoring.ts 可以直接复用。React 应用只需要在 main.tsx(或 index.tsx)调用一次初始化函数,组件本身不要创建新的 Tracker:
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { startMonitoring } from './monitoring';
const monitoringResult = startMonitoring();
if (!monitoringResult.ok) {
// 初始化失败时可以上报到自己的日志系统,但不要阻塞应用启动。
console.error('监控 SDK 初始化失败', monitoringResult.error);
}
const container = document.getElementById('root');
if (!container) {
throw new Error('找不到 React 根节点');
}
createRoot(container).render(
<StrictMode>
<App />
</StrictMode>,
);StrictMode 在开发环境可能会让组件执行两次,但由于 Tracker 实例和 started 标记位于模块作用域,监听器不会重复注册。上面的 monitoring.ts 使用的是 Vite 的 import.meta.env;如果是 Create React App,请改为 process.env.REACT_APP_TRACKER_REPORT_URL(或由构建系统注入一个等价的 reportUrl),不要直接复制 Vite 环境变量写法。
不同构建工具只需要替换 reportUrl 的读取方式,Tracker 初始化和插件注册保持不变:
| 构建工具 | 前端可公开的环境变量示例 |
| ---------------- | ----------------------------------------------------------------- |
| Vite | import.meta.env.VITE_TRACKER_REPORT_URL |
| Create React App | process.env.REACT_APP_TRACKER_REPORT_URL |
| Next.js | process.env.NEXT_PUBLIC_TRACKER_REPORT_URL(仅客户端模块) |
| Nuxt 3 | useRuntimeConfig().public.trackerReportUrl(仅 .client 插件) |
仅在 Vite 项目中,如需在热更新时主动释放旧实例,可以在入口增加:
import { tracker } from './monitoring';
if (import.meta.hot) {
import.meta.hot.dispose(() => {
void tracker.close(1000);
});
}业务组件只通过共享实例记录业务事件;全局 click、错误、性能和 PV 仍由插件自动采集:
// src/features/checkout/CheckoutButton.tsx
import type { ReactElement } from 'react';
import { tracker } from '../../monitoring';
export function CheckoutButton(): ReactElement {
const handleClick = (): void => {
const result = tracker.report({
event_type: 'checkout_started',
tags: { module: 'checkout' },
payload: { cart_size: 3 },
});
if (!result.ok) {
console.error('业务事件入队失败', result.error);
}
};
return <button onClick={handleClick}>去结算</button>;
}React 的 Error Boundary 会接住渲染阶段错误,这类错误不一定继续冒泡到 window.onerror。如果希望把组件堆栈也记录下来,可以在应用根部增加一个不依赖第三方库的边界:
// src/MonitoringErrorBoundary.tsx
import { Component, type ErrorInfo, type ReactNode } from 'react';
import { tracker } from './monitoring';
interface Props {
readonly children: ReactNode;
}
interface State {
readonly hasError: boolean;
}
export class MonitoringErrorBoundary extends Component<Props, State> {
public state: State = { hasError: false };
public static getDerivedStateFromError(): State {
return { hasError: true };
}
public componentDidCatch(error: Error, info: ErrorInfo): void {
tracker.report({
event_type: 'react_error',
payload: {
name: error.name,
message: error.message,
stack: error.stack,
component_stack: info.componentStack,
},
});
}
public render(): ReactNode {
return this.state.hasError ? <p>页面暂时不可用,请刷新重试。</p> : this.props.children;
}
}使用时把 <App /> 包在 <MonitoringErrorBoundary> 中即可。事件处理器里的 try/catch 业务异常仍建议显式调用 tracker.report,因为已经被捕获的异常不会触发全局 window.onerror。
Vue 3(Vite)
Vue 同样在 main.ts 初始化一次。PVPlugin 已经包装 pushState、replaceState、popstate 和 hashchange,因此不需要在每个 vue-router 路由组件中重复接入:
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { startMonitoring, tracker } from './monitoring';
const monitoringResult = startMonitoring();
if (!monitoringResult.ok) {
console.error('监控 SDK 初始化失败', monitoringResult.error);
}
const app = createApp(App);
// Vue 捕获到的组件错误可能不会到达 window.onerror,补充框架级错误上下文。
app.config.errorHandler = (error, _instance, info): void => {
const normalized =
error instanceof Error
? { name: error.name, message: error.message, stack: error.stack }
: { message: String(error) };
tracker.report({
event_type: 'vue_error',
payload: { ...normalized, info },
});
};
app.mount('#app');组件内的业务事件写法与 React 相同,只引用共享实例:
<!-- src/components/CheckoutButton.vue -->
<script setup lang="ts">
import { tracker } from '../monitoring';
function startCheckout(): void {
const result = tracker.report({
event_type: 'checkout_started',
tags: { module: 'checkout' },
payload: { cart_size: 3 },
});
if (!result.ok) {
console.error('业务事件入队失败', result.error);
}
}
</script>
<template>
<button type="button" @click="startCheckout">去结算</button>
</template>如果使用 Nuxt、Next 或其他 SSR 框架,请把 startMonitoring() 放到仅客户端执行的入口(例如 Nuxt plugin 的 *.client.ts 或带 'use client' 的 Next 客户端模块),不要在服务端渲染进程中注册浏览器监听器。
SPA、MPA、SSR 与微前端
- SPA 中,路由切换不会重新创建
window,因此应用入口初始化一次即可;PVPlugin和ActionPlugin会自动监听 History API,不需要每个路由组件重复注册。 - MPA 每次完整页面刷新都会创建新的 JavaScript 上下文,需要在每个页面共用的入口 bundle 中执行一次
startMonitoring(),仍然不需要在各个业务页面组件中写初始化代码。 - SSR 或 Nuxt/Next 等框架应在 client-only 入口初始化,避免在服务端渲染阶段注册浏览器监听器。
- 微前端建议由宿主应用创建一个 Tracker,再通过依赖注入或模块共享传给子应用;只有确实需要不同
appId或不同上报端点时,才创建多个实例。内置插件的每次apply都维护独立清理状态,同一个插件对象可以注册到不同 Tracker;不过分别创建插件实例通常更便于表达各子应用的配置边界。
“全局”指当前 window/document 范围,不包括其他 Tab、跨域 iframe、Worker 或 Service Worker。后几类运行环境需要分别注入 SDK,或通过消息机制把错误转发到宿主应用。
监听范围与采集颗粒度
插件采用显式注册,颗粒度由“启用哪些插件 + 采样率 + 事件过滤”三层共同决定。SDK 不会因为导入模块就修改全局对象:
| 选择 | 行为 |
| -------------------------- | -------------------------------------------------------------- |
| 只注册 ErrorPlugin | 只监听当前页面的 window.onerror 与 unhandledrejection |
| 只注册 PerformancePlugin | 只读取当前页面的 Performance API,不采集 click/PV |
| 注册 ActionPlugin | 监听当前文档的 click 与 History API 路由变化,不读取输入框文本 |
| 注册 PVPlugin | 记录首次 PV、SPA 路由变化和 bfcache 恢复(相同 URL 去重) |
| 不注册某插件 | 该插件不安装任何监听器,也不会产生对应事件 |
这里的“全局”是当前 JavaScript 上下文中的 window、document 和 history。同一页面建议只有一个 Tracker;多个实例会各自注册监听器,可能造成重复事件。内置插件对象可以安全复用,但不会合并不同 Tracker 产生的事件。不同 Tab、iframe、Worker 都有独立上下文,需要单独初始化或由宿主转发事件。
采样率
sampleRate 必须是 0 到 1 的数字,并在 init 时随机一次:
0:关闭当前 Tracker 实例的新事件采集,report仍返回成功 Result,但不会入队;1:采集全部事件;0.1:约 10% 的页面实例启用已注册插件。它是页面/实例级采样,不是每条事件独立随机;- 初始化后不能动态修改采样率,如需切换策略请创建新的 Tracker 实例。
如果开启 persistQueue,启动时会尝试恢复同一 appId 的离线队列;sampleRate 只控制当前实例产生的新事件,已经持久化的离线事件仍会尝试发送。要在隐私策略变更后硬性停发并删除当前标签页的历史缓存,请在关闭前调用 tracker.clear(false)(或在当前标签页用同一 appId 的新实例清理)。
事件级过滤与自定义颗粒度
全局标签/扩展字段适合放环境和版本,单次事件字段适合放页面或业务上下文:
tracker.setTags({ env: 'production', release: '2026.08.26' });
tracker.setExtra({ tenant_id: 'tenant-a' });
tracker.report({
event_type: 'payment_failed',
tags: { module: 'checkout' }, // 覆盖同名全局 tag
extra: { order_id: 'order-123' }, // 覆盖同名全局 extra
payload: { reason: 'timeout' },
});合并优先级为:基础环境 < setTags/setExtra < 当前事件的 tags/extra。对象字段采用浅层合并;payload 只承载当前事件数据,不能覆盖 event_id、app_id、timestamp、device_id、session_id、sdk_version 等信封字段。
beforeReport 适合做业务过滤、统一补充标签或二次脱敏。返回 null 会丢弃事件;回调抛错或返回非法结构时,report 返回 PLUGIN_ERROR,该事件不会进入队列,用户回调不会破坏 SDK:
const tracker = new Tracker();
tracker.use(new ErrorPlugin());
const initialized = tracker.init({
appId: 'shop-web',
reportUrl: 'https://collector.example.com/events',
sampleRate: 1,
beforeReport: (event) => {
const message = event.payload.message;
if (
event.event_type === 'error' &&
typeof message === 'string' &&
message.includes('ResizeObserver loop')
) {
return null; // 忽略已知噪声
}
return {
...event,
tags: { ...event.tags, source: 'web' },
};
},
});
if (!initialized.ok) {
console.error(initialized.error.code, initialized.error.message);
}只想监控某个业务模块时,可以不注册 ActionPlugin/PVPlugin,并在该模块显式调用 report;想增加自定义采集器时,实现 TrackerPlugin 的 name 与 apply(context),通过 context.report 上报,通过 context.addListener 注册并获得自动 cleanup。
数据流
flowchart LR
A[数据采集] --> B[事件校验与会话采样]
B --> C[队列聚合]
C --> D[空闲上报 requestIdleCallback 调度]
D --> E[最多 10 条且请求体约 60 KiB]
E --> I[普通刷新: fetch keepalive + AbortSignal]
I -->|fetch API 不可用| K[Beacon fallback]
I -->|网络或 HTTP 失败| J[保留事件并指数退避重试]
F[visibilitychange hidden] --> G[立即刷新全部队列]
G --> H[hidden 刷新: navigator.sendBeacon text/plain]
H -->|返回 false 或不可用| L[fetch keepalive + AbortSignal]
L -->|网络或 HTTP 失败| J主链路可概括为:数据采集 -> 队列聚合 -> 空闲上报;页面进入 hidden 时绕过等待立即 flush。
架构图
graph TD
SDK[Tracker SDK] --> CORE[core: Tracker + QueueManager]
CORE --> CTX[ContextManager]
CORE --> UTILS[utils: ID / URL 脱敏 / throttle / 安全存储]
CORE --> PLUGINS[插件层]
PLUGINS --> ERR[ErrorPlugin]
PLUGINS --> PERF[PerformancePlugin]
PLUGINS --> ACT[ActionPlugin]
PLUGINS --> PV[PVPlugin]
CORE --> TRANSPORT[Beacon / fetch 传输]公共 API
配置
interface TrackerConfig {
appId: string;
reportUrl: string;
sampleRate: number;
beforeReport?: (event: ReportEvent) => ReportEvent | null | void;
persistQueue?: boolean;
}appId:应用标识;reportUrl:POST 上报地址;sampleRate:0到1的实例/页面级采样率,初始化时随机一次;beforeReport:可返回变换后的事件,返回null丢弃事件;回调异常或非法事件结构会被 SDK 捕获并封装为TrackerError,event_id、app_id、会话标识等保留信封字段不会被覆盖;persistQueue:是否将未发送事件保存到 localStorage,默认关闭。
生命周期与队列
tracker.setTags({ env: 'prod' });
tracker.setExtra({ release: '2026.08' });
const abortController = new AbortController();
const flushResult = await tracker.flush(5000, abortController.signal);
// { success: true, sent: 1 }
tracker.clear(); // 清空内存与持久化队列
tracker.clear(true); // 只清空内存,保留离线缓存
await tracker.close(); // 插件提交最终指标并解除监听后,再做最后一次刷新;幂等
tracker.destroy(); // 立即解除监听器的同步清理方法report 只负责校验、组装并入队,不代表服务器已经收到事件。队列在 requestIdleCallback(不支持时为 setTimeout)的空闲时机发送,每批最多 10 条;调用 flush 才会主动处理当前队列。
内存队列最多保留 1000 条事件;达到上限时 report 会返回 QUEUE_FULL,不会覆盖已经入队的数据。单事件序列化后最多 48 KiB,每个网络请求最多 10 条且请求体控制在约 60 KiB,避免超过 Beacon/keepalive 的常见 64 KiB 限制。不是 JSON-safe 的数据和超限事件会在 report 边界被拒绝,不会成为永久阻塞后续事件的“毒事件”。
flush 永远 resolve,不会因为网络、超时或取消 reject。普通并发调用会复用同一个进行中的 Promise。超时默认 5000ms,hidden 页面自动使用 3000ms;超时会终止当前请求、释放并发锁,并让迟到的旧请求结果失效。close 不受调用方先前传入的短超时或 AbortSignal 牵连:它会取消旧 operation,并用自己的超时启动独立的最终刷新。请求可能已经到达服务端,因此仍须按 event_id 幂等去重。sent 只统计已经得到 2xx 响应(或 Beacon 接受)的事件数。网络失败会保留事件,并按 1、2、4、8、16 秒最多自动重试 5 轮;浏览器恢复 online 时会重置退避并立即尝试刷新。
启用 persistQueue 后,每个 Tracker 实例使用独立 owner 存储项和短租约,避免同页多实例、复制标签页或多个标签页用各自的内存快照相互覆盖。实例异常退出后,其他实例会在租约过期后恢复遗留队列,并写入包含 event_id 的接管标记(tombstone);来源 owner 恢复后先核销这些 ID,不能再次发送已经接管的事件。来源快照会保留到原 owner 确认,死亡 owner 的小型标记在 7 天后安全清理。目标队列只有能完整持久化时才接管,空间不足会等待后续批次释放容量,不会为了新事件覆盖 clear(true) 明确保留的旧缓存。持久化快照最多 2 MiB,结构非法的数据会被安全丢弃,SDK 也能读取首版数组格式并迁移。
localStorage 不提供跨标签页事务或原子 CAS:两个页面在极窄的竞争窗口内仍可能同时接管同一份过期队列。因此采集服务必须把 event_id 作为幂等键去重;如果业务要求更强的离线可靠性和严格一次投递,建议改用 IndexedDB 或 Service Worker 协调。
clear(false)(默认)清空内存队列并删除当前实例的 localStorage 离线缓存;clear(true) 只清空当前内存队列,并保留已持久化的离线事件。它不会主动删除其他仍在运行实例的独立缓存。两者都会取消当前上报请求。close 会先执行插件 cleanup,让 PerformancePlugin 等提交最终指标并解除监听器;随后阻止新的 report,执行最后一次刷新并释放引用。destroy 调用同一关闭流程但不等待网络结果,适合测试卸载或页面已经确定不再等待上报的场景。启用 persistQueue 时,关闭/销毁不会自动删除尚未成功发送的离线缓存,需要彻底清除当前实例缓存时显式调用 clear(false)。
Result
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };init、use、report、setTags、setExtra 等同步 API 不抛出业务异常,调用方应检查 ok。
上报协议
请求体固定为:
{
"app_id": "demo-app",
"events": [
{
"event_id": "uuid",
"app_id": "demo-app",
"event_type": "checkout_started",
"timestamp": 1720000000000,
"device_id": "short-id",
"session_id": "short-id",
"page_url": "https://example.com/checkout?token=%5BREDACTED%5D",
"os": "Windows",
"browser": "Chrome",
"screen_resolution": "1920x1080",
"viewport": "1280x720",
"sdk_version": "0.1.1",
"tags": { "env": "prod" },
"extra": {},
"payload": { "cart_size": 3 }
}
]
}协议字段统一使用 snake_case。事件内联 tags/extra 覆盖全局设置,全局设置覆盖基础上下文;保留信封字段不能被 payload 覆盖。
后端接收接口
SDK 不内置后端服务,也不会替你创建数据表。后端地址就是 init 配置中的 reportUrl:
tracker.init({
appId: 'shop-web',
reportUrl: 'https://collector.example.com/v1/track/events',
sampleRate: 1,
});该地址需要提供一个接收批量事件的 POST 接口。普通 fetch 请求使用 application/json;hidden 阶段的 Beacon 使用 CORS safelist 内的 text/plain;charset=UTF-8,但正文仍是同一段 JSON,因此后端应对这两种 Content-Type 使用相同的 JSON 解析和校验逻辑:
POST /v1/track/events HTTP/1.1
Content-Type: application/json
{
"app_id": "shop-web",
"events": [
{
"event_id": "01J...",
"app_id": "shop-web",
"event_type": "error",
"timestamp": 1720000000000,
"device_id": "device-id",
"session_id": "session-id",
"page_url": "https://example.com/checkout",
"sdk_version": "0.1.1",
"tags": {},
"extra": {},
"payload": { "message": "请求失败" }
}
]
}服务端成功接收并完成基本校验后建议返回 204 No Content(任意 2xx 都会被 SDK 视为成功)。非 2xx、网络错误、超时或取消都会得到 success: false,未成功的事件会保留在队列中等待重试。服务端至少应校验 app_id、event_id、event_type 和 timestamp,并按 event_id 做幂等去重,避免重试导致重复入库。
浏览器端传输有三个重要语义:
- 普通 idle/manual flush 优先使用
fetch的POST + keepalive + AbortSignal,只有环境完全没有 fetch API 时才尝试 Beacon;fetch 已经发起后的 HTTP/网络失败不会再发送同一批 Beacon,避免一批数据被发送两次; - hidden 阶段优先 Beacon;如果
navigator.sendBeacon返回true,只表示浏览器接受了传输任务,不代表服务端已经落库,SDK 会按成功处理并移除该批事件;Beacon 返回false或不可用时再尝试 fetch; - 跨域
reportUrl必须允许当前站点的 CORS,并允许 fetch 的POST与Content-Type: application/json;Beacon 使用text/plain以减少页面离开时依赖预检的风险。同时检查 CSP 的connect-src是否包含上报域名。
建议把 reportUrl 放在构建环境变量中(例如 Vite 的 VITE_TRACKER_REPORT_URL),不要把带有服务端密钥的地址或鉴权 token 写进前端。鉴权、限流、压缩、字段校验和敏感数据二次过滤应在采集服务端完成。
仓库根目录提供了不含密钥的 .env.example,本地接入时可以复制为 .env.local,再由构建工具读取。
错误捕获与发送链路
启用 ErrorPlugin 后,错误处理流程如下:
window.onerror / capture error / unhandledrejection
-> ErrorPlugin 规范化 message、name、stack、source、行列号或 reason
-> Tracker.report(采样、beforeReport、URL 脱敏、信封组装)
-> 内存队列 / 可选 localStorage
-> requestIdleCallback 空闲批量(每批最多 10 条)
-> 普通空闲阶段 fetch;hidden 阶段 sendBeacon 优先插件会保留并调用安装前的 window.onerror,并通过捕获阶段的 error 监听器采集脚本、图片和样式等资源加载失败;关闭 Tracker 时会解除自身订阅。监听 unhandledrejection 时不会阻止浏览器默认行为。SDK 只自动捕获“未处理”的浏览器错误,已经被业务 try/catch、React Error Boundary 或 Vue errorHandler 捕获的异常,需要显式记录:
try {
await submitOrder();
} catch (error: unknown) {
const normalized =
error instanceof Error
? { name: error.name, message: error.message, stack: error.stack }
: { message: String(error) };
const result = tracker.report({
event_type: 'business_error',
tags: { module: 'order' },
payload: { operation: 'submit_order', ...normalized },
});
if (!result.ok) {
console.error('错误事件入队失败', result.error);
}
}beforeReport 会对插件产生的错误事件同样生效。回调被 try-catch 隔离:返回 null 表示业务上主动忽略,抛出异常则返回类型化 PLUGIN_ERROR,不会反向抛到 window.onerror 形成错误循环。
上下文与隐私
device_id从localStorage['_tracker_device_id']读取,不存在时优先使用 Web Crypto 生成 12 位 ID;旧环境中使用内置兼容随机实现;session_id保存在 sessionStorage,超过 30 分钟无事件后轮换;- 初始化时自动解析轻量 UA,采集 OS、浏览器、屏幕分辨率和 viewport;
page_url的 query、SPA hash query 及错误堆栈 URL 中,password、token、secret、access_token、id_token、authorization、api_key等参数默认替换为[REDACTED];- ActionPlugin 不读取输入框值、用户文本或 DOM
id/class;后两者可能由邮箱、手机号等业务数据动态生成,所以默认不进入上报; - 可在不应采集的元素或祖先上添加
data-monitor-ignore;需要稳定业务标识时显式设置data-monitor-id,值只接受以字母开头、最长 64 位的字母/数字/:/_/-,且业务侧仍应确保它不含个人信息; - 存储不可用、Performance API 缺失或 requestIdleCallback 缺失时,SDK 使用安全降级,不阻断页面。
内置插件
| 子路径 | 能力 |
| ------------------------------- | ------------------------------------------------------- |
| @jasw/web-monitor/error | 脚本/资源 error、window.onerror、unhandledrejection |
| @jasw/web-monitor/performance | FCP、LCP、CLS、FID、Navigation Timing DNS/TCP/SSL |
| @jasw/web-monitor/action | click、pushState、replaceState、popstate、hashchange |
| @jasw/web-monitor/pv | 首次 PV、SPA 路由 PV、bfcache 恢复去重 |
PerformancePlugin 使用有界 Set<startTime> 去重 bfcache 恢复时重复出现的 PerformanceEntry;LCP 只在指标最终确定时提交最近候选值,CLS 按标准 session window 聚合,而不是把每次 layout shift 误当成完整 CLS。不支持的指标会被单独跳过。
插件生命周期与事件字段
use(plugin) 可以在 init 之前或之后调用。插件的 apply(context) 成功后可以返回一个 cleanup 函数;close/destroy 会统一执行这些 cleanup,重复 use 同名插件会返回 DUPLICATE_PLUGIN。一次根插件安装(包括 apply 内嵌套注册的插件)是同步事务:完成前暂停队列发送;任一插件抛错或重入 close 时,本轮监听器和事件都会回滚,不会把未提交事件先发到后端。插件内部不应直接调用传输层,而应使用 context.report 和 context.addListener,这样才能共享采样、过滤、队列和生命周期管理。
如果页面确实需要多个 Tracker(例如不同租户或微前端隔离),同一个内置插件对象也可以跨实例使用:每次 apply 的监听器、观察器和 cleanup 都相互独立,ErrorPlugin 的全局捕获则由共享 dispatcher 分发。分别创建插件实例通常更容易阅读,但不再是正确性的要求。
内置事件的 payload 主要字段如下:
| 插件 | event_type | 典型 payload |
| ----------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
| ErrorPlugin | error | name、message、stack、source、line_number、column_number、reason |
| PerformancePlugin | performance | metric(fcp/lcp/cls/fid)、value、start_time、duration;导航事件另有 dns_ms、tcp_ms、ssl_ms |
| ActionPlugin | action | action_type、navigation_type、route_url、target_tag,以及显式配置的 target_monitor_id |
| PVPlugin | pv | page_url、referrer、navigation_type |
所有 URL 字段都会经过 query/hash 参数脱敏,错误堆栈中的 URL 也会执行同一规则。若浏览器不支持某个 API,插件只跳过对应采集项,不会让应用启动失败。
自定义插件示例:
import type { TrackerPlugin } from '@jasw/web-monitor/core';
export const OnlinePlugin: TrackerPlugin = {
name: 'online-status',
apply(context) {
const report = (): void => {
context.report({
event_type: 'network_status',
payload: { online: navigator.onLine },
});
};
const removeOnline = context.addListener(window, 'online', report);
const removeOffline = context.addListener(window, 'offline', report);
report();
return (): void => {
removeOnline();
removeOffline();
};
},
};
tracker.use(OnlinePlugin);项目文档
- 贡献指南:开发环境、编码规范、测试、Commit 与 Pull Request;
- 发布指南:版本准备、发布检查、npm 发布与发布后验证;
- 更新日志:版本变更、修复和安全说明;
- 安全策略:漏洞报告与敏感信息处理。
许可证
MIT,详见 LICENSE。
