npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jasw/web-monitor

v0.1.2

Published

Lightweight, plugin-based frontend monitoring SDK for web performance, errors, and user actions.

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-monitor
import 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 已经包装 pushStatereplaceStatepopstatehashchange,因此不需要在每个 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,因此应用入口初始化一次即可;PVPluginActionPlugin 会自动监听 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.onerrorunhandledrejection | | 只注册 PerformancePlugin | 只读取当前页面的 Performance API,不采集 click/PV | | 注册 ActionPlugin | 监听当前文档的 click 与 History API 路由变化,不读取输入框文本 | | 注册 PVPlugin | 记录首次 PV、SPA 路由变化和 bfcache 恢复(相同 URL 去重) | | 不注册某插件 | 该插件不安装任何监听器,也不会产生对应事件 |

这里的“全局”是当前 JavaScript 上下文中的 windowdocumenthistory。同一页面建议只有一个 Tracker;多个实例会各自注册监听器,可能造成重复事件。内置插件对象可以安全复用,但不会合并不同 Tracker 产生的事件。不同 Tab、iframe、Worker 都有独立上下文,需要单独初始化或由宿主转发事件。

采样率

sampleRate 必须是 01 的数字,并在 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_idapp_idtimestampdevice_idsession_idsdk_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;想增加自定义采集器时,实现 TrackerPluginnameapply(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 上报地址;
  • sampleRate01 的实例/页面级采样率,初始化时随机一次;
  • beforeReport:可返回变换后的事件,返回 null 丢弃事件;回调异常或非法事件结构会被 SDK 捕获并封装为 TrackerErrorevent_idapp_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 };

initusereportsetTagssetExtra 等同步 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_idevent_idevent_typetimestamp,并按 event_id 做幂等去重,避免重试导致重复入库。

浏览器端传输有三个重要语义:

  • 普通 idle/manual flush 优先使用 fetchPOST + keepalive + AbortSignal,只有环境完全没有 fetch API 时才尝试 Beacon;fetch 已经发起后的 HTTP/网络失败不会再发送同一批 Beacon,避免一批数据被发送两次;
  • hidden 阶段优先 Beacon;如果 navigator.sendBeacon 返回 true,只表示浏览器接受了传输任务,不代表服务端已经落库,SDK 会按成功处理并移除该批事件;Beacon 返回 false 或不可用时再尝试 fetch;
  • 跨域 reportUrl 必须允许当前站点的 CORS,并允许 fetch 的 POSTContent-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_idlocalStorage['_tracker_device_id'] 读取,不存在时优先使用 Web Crypto 生成 12 位 ID;旧环境中使用内置兼容随机实现;
  • session_id 保存在 sessionStorage,超过 30 分钟无事件后轮换;
  • 初始化时自动解析轻量 UA,采集 OS、浏览器、屏幕分辨率和 viewport;
  • page_url 的 query、SPA hash query 及错误堆栈 URL 中,passwordtokensecretaccess_tokenid_tokenauthorizationapi_key 等参数默认替换为 [REDACTED]
  • ActionPlugin 不读取输入框值、用户文本或 DOM id/class;后两者可能由邮箱、手机号等业务数据动态生成,所以默认不进入上报;
  • 可在不应采集的元素或祖先上添加 data-monitor-ignore;需要稳定业务标识时显式设置 data-monitor-id,值只接受以字母开头、最长 64 位的字母/数字/:/_/-,且业务侧仍应确保它不含个人信息;
  • 存储不可用、Performance API 缺失或 requestIdleCallback 缺失时,SDK 使用安全降级,不阻断页面。

内置插件

| 子路径 | 能力 | | ------------------------------- | ------------------------------------------------------- | | @jasw/web-monitor/error | 脚本/资源 error、window.onerrorunhandledrejection | | @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.reportcontext.addListener,这样才能共享采样、过滤、队列和生命周期管理。

如果页面确实需要多个 Tracker(例如不同租户或微前端隔离),同一个内置插件对象也可以跨实例使用:每次 apply 的监听器、观察器和 cleanup 都相互独立,ErrorPlugin 的全局捕获则由共享 dispatcher 分发。分别创建插件实例通常更容易阅读,但不再是正确性的要求。

内置事件的 payload 主要字段如下:

| 插件 | event_type | 典型 payload | | ----------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- | | ErrorPlugin | error | namemessagestacksourceline_numbercolumn_numberreason | | PerformancePlugin | performance | metricfcp/lcp/cls/fid)、valuestart_timeduration;导航事件另有 dns_mstcp_msssl_ms | | ActionPlugin | action | action_typenavigation_typeroute_urltarget_tag,以及显式配置的 target_monitor_id | | PVPlugin | pv | page_urlreferrernavigation_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