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

sfcom-acquisition

v0.1.4

Published

前端日志采集库,支持捕获网络请求/JS异常/心跳/Vue Router/手动发送

Downloads

75

Readme

sfcom-acquisition

浏览器端前端日志采集库,零运行时依赖。调用一次 init() 即可自动采集环境信息、网络异常、运行时报错、路由跳转与心跳,同时支持手动上报业务日志。


5 分钟上手

npm install sfcom-acquisition
import { init } from 'sfcom-acquisition'

const acq = init({
  url: 'https://log.example.com/collect',
  app: 'my-app',
  user: getCurrentUserId(),
})

// 手动上报业务日志
acq.send({ event: 'purchase', amount: 99 })

// Vue Router 集成(在 router 实例创建之后调用)
acq.useVueRouter(router)

库加载后会自动发送 INIT 日志(附带设备信息);之后网络错误、未捕获异常、路由变化、30 秒一次的心跳均自动上报,无需额外配置。


安装

npm / pnpm / yarn

npm install sfcom-acquisition
# pnpm add sfcom-acquisition
# yarn add sfcom-acquisition

| 产物 | 路径 | 格式 | | :--------- | :------------------------- | :----------------------------- | | ES Module | dist/acquisition.js | ESM(推荐,支持 Tree-shaking) | | CommonJS | dist/acquisition.umd.cjs | UMD / require() | | 浏览器直引 | dist/acquisition.iife.js | IIFE,全局变量 Acquisition |

CDN 直接引入

<script src="https://unpkg.com/sfcom-acquisition/dist/acquisition.iife.js"></script>
<script>
  const acq = Acquisition.init({
    url: 'https://log.example.com/collect',
    app: 'my-site',
    user: 'anonymous',
  })
</script>

初始化

init(options): AcquisitionInstance

const acq = init(options: AcquisitionOptions)

AcquisitionOptions

| 字段 | 类型 | 必填 | 说明 | | :----------- | :--------------- | :--: | :-------------------------------------------------------- | | url | string | ✓ | 日志接收接口地址,所有日志通过 POST 发送到此地址 | | app | string | ✓ | 应用标识,会话内不变,会附在每条日志中 | | user | string | ✓ | 用户标识,会话内不变,会附在每条日志中 | | sendMethod | SendMethod | | 发送方式,默认 'fetch',详见发送方式 | | features | FeaturesConfig | | 各采集功能的开关,默认全部开启,详见功能开关 |


实例方法

init() 返回一个 AcquisitionInstance 实例,提供以下方法:

acq.send(data?)

手动发送一条 MANUAL 类型的日志。data 可以是任意可序列化的值。

acq.send({ event: 'click', target: '#submit-btn' })
acq.send('用户完成引导流程')
acq.send() // data 可省略

acq.useVueRouter(router)

注册 Vue Router 实例,之后每次路由跳转完成时自动发送 VUE-ROUTER 日志。

  • 须在 Vue Router 实例创建完成后调用
  • 若初始化时 features.vueRouterfalse,此方法为空操作
acq.useVueRouter(router)

acq.destroy()

销毁实例,清理所有事件监听器、XHR/Fetch 拦截器和心跳定时器。在 SPA 组件卸载或需要重新初始化时调用。

// Vue 3 示例
onUnmounted(() => {
  acq.destroy()
})

发送方式

通过 sendMethod 选项设置,所有方式均为 fire-and-forget,不影响主程序。

| 值 | 说明 | | :--------- | :-------------------------------------------------------------------------- | | 'fetch' | 使用 Fetch API,开启 keepalive: true(页面卸载时仍可发出)。默认值 | | 'xhr' | 使用异步 XMLHttpRequest,兼容性最好 | | 'beacon' | 使用 navigator.sendBeacon,最适合页面卸载场景。发送失败时自动降级为 fetch | | 'test' | 仅在控制台打印,不发起网络请求。开发调试专用 |

// 开发环境:仅打印到控制台
const acq = init({
  url: '...',
  app: 'my-app',
  user: 'dev',
  sendMethod: 'test',
})

功能开关

features 选项中所有字段默认为开启(true),可以按需关闭。

| 字段 | 类型 | 默认值 | 说明 | | :--------------- | :---------------------- | :----: | :----------------------------------------------------------- | | init | boolean | true | 库加载完成时发送 INIT 日志(附带设备信息) | | netErr | boolean | true | 拦截 XHR/Fetch 请求失败时发送 NET_ERR 日志 | | frontException | boolean | true | 捕获未处理的异常和 Promise 拒绝,发送 FRONT_EXCEPTION 日志 | | beat | boolean \| BeatConfig | true | 定期发送 BEAT 心跳日志,默认间隔 30 秒 | | vueRouter | boolean | true | 注册 Vue Router 路由变化监听,发送 VUE-ROUTER 日志 |

beat 传入对象时可自定义间隔:

// 关闭心跳
features: {
  beat: false
}

// 自定义心跳间隔为 60 秒
features: {
  beat: {
    interval: 60_000
  }
}

日志格式

所有发送到后端的日志均为 JSON,通用结构如下:

{
  type:     string,   // 日志类型(见下表)
  datetime: number,   // 毫秒级时间戳,发送时自动填入
  app:      string,   // 初始化时配置的应用标识
  user:     string,   // 初始化时配置的用户标识
  data?:    unknown   // 各类型特有的详细数据(部分类型无此字段)
}

各类型说明

INIT — 库加载完成

init() 调用后的首个宏任务中触发,附带当前设备和浏览器信息。

{
  "type": "INIT",
  "datetime": 1711900000000,
  "app": "my-app",
  "user": "user-123",
  "data": {
    "userAgent": "Mozilla/5.0 ...",
    "language": "zh-CN",
    "languages": ["zh-CN", "zh", "en"],
    "platform": "Win32",
    "screenWidth": 1920,
    "screenHeight": 1080,
    "devicePixelRatio": 2,
    "timezone": "Asia/Shanghai",
    "href": "https://example.com/dashboard",
    "referrer": "https://google.com",
    "cookieEnabled": true,
    "onLine": true
  }
}

NET_ERR — 网络请求失败

拦截所有 XHR 和 Fetch 请求,在响应状态码为 0(网络层错误)或 ≥ 400 时触发。

{
  "type": "NET_ERR",
  "datetime": 1711900001000,
  "app": "my-app",
  "user": "user-123",
  "data": {
    "method": "POST",
    "url": "https://api.example.com/orders",
    "status": 500,
    "statusText": "Internal Server Error",
    "duration": 342,
    "requestBody": "{\"itemId\":42}",
    "responseText": "{\"error\":\"db connection failed\"}"
  }
}

status: 0 表示网络层失败(DNS 解析失败、CORS 预检被拒绝、连接中断等)。
requestBody 仅在请求体为字符串时采集。responseText 截断至 1024 字符。

FRONT_EXCEPTION — 未捕获异常

监听 window.errorwindow.unhandledrejection 事件。

{
  "type": "FRONT_EXCEPTION",
  "datetime": 1711900002000,
  "app": "my-app",
  "user": "user-123",
  "data": {
    "type": "error",
    "message": "Cannot read properties of undefined (reading 'id')",
    "source": "https://example.com/assets/app.js",
    "lineno": 42,
    "colno": 18,
    "stack": "TypeError: Cannot read properties of undefined..."
  }
}

data.type 有两个值:

  • "error":同步运行时错误
  • "unhandledrejection":未被 .catch() 处理的 Promise 拒绝

MANUAL — 手动上报

调用 acq.send(data) 时触发,data 由调用方决定。

{
  "type": "MANUAL",
  "datetime": 1711900003000,
  "app": "my-app",
  "user": "user-123",
  "data": {
    "event": "purchase",
    "amount": 99,
    "itemId": "SKU-001"
  }
}

VUE-ROUTER — 路由跳转

调用 acq.useVueRouter(router) 后,每次 router.afterEach 触发时上报。

{
  "type": "VUE-ROUTER",
  "datetime": 1711900004000,
  "app": "my-app",
  "user": "user-123",
  "data": {
    "from": {
      "path": "/dashboard",
      "fullPath": "/dashboard?tab=overview",
      "name": "Dashboard",
      "query": { "tab": "overview" },
      "params": {},
      "hash": ""
    },
    "to": {
      "path": "/orders/42",
      "fullPath": "/orders/42",
      "name": "OrderDetail",
      "query": {},
      "params": { "id": "42" },
      "hash": ""
    }
  }
}

BEAT — 心跳

按照设定间隔定期发送,无额外 data 字段。

{
  "type": "BEAT",
  "datetime": 1711900030000,
  "app": "my-app",
  "user": "user-123"
}

场景示例

Vue 3 + Vue Router 4 完整接入

// main.ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { init } from 'sfcom-acquisition'
import App from './App.vue'
import routes from './routes'

const router = createRouter({
  history: createWebHistory(),
  routes,
})

const acq = init({
  url: import.meta.env.VITE_LOG_URL,
  app: 'my-vue-app',
  user: store.getters.userId, // 从状态管理中获取
  sendMethod: import.meta.env.PROD ? 'beacon' : 'test',
})

acq.useVueRouter(router)

createApp(App).use(router).mount('#app')

关闭部分功能

const acq = init({
  url: 'https://log.example.com/collect',
  app: 'my-app',
  user: 'anonymous',
  features: {
    beat: false, // 不需要心跳
    netErr: false, // 不拦截网络请求
    beat: { interval: 60_000 }, // 心跳间隔改为 60 秒
  },
})

页面卸载前用 beacon 发送

// 在需要可靠上报页面停留时长的场景使用
const acq = init({
  url: 'https://log.example.com/collect',
  app: 'my-app',
  user: userId,
  sendMethod: 'beacon',
})

window.addEventListener('beforeunload', () => {
  acq.send({ stayDuration: Date.now() - pageEnterTime })
})

纯 HTML 页面(CDN)

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>My Page</title>
  </head>
  <body>
    <script src="https://unpkg.com/sfcom-acquisition/dist/acquisition.iife.js"></script>
    <script>
      var acq = Acquisition.init({
        url: 'https://log.example.com/collect',
        app: 'landing-page',
        user: 'visitor',
        sendMethod: 'beacon',
        features: { beat: false },
      })
    </script>
  </body>
</html>

TypeScript 类型参考

// 初始化选项
interface AcquisitionOptions {
  url: string
  app: string
  user: string
  sendMethod?: 'fetch' | 'xhr' | 'beacon' | 'test'
  features?: FeaturesConfig
}

// 功能开关
interface FeaturesConfig {
  init?: boolean
  netErr?: boolean
  frontException?: boolean
  beat?: boolean | BeatConfig
  vueRouter?: boolean
}

// 心跳配置
interface BeatConfig {
  interval?: number // 毫秒,最小 1000,默认 30000
}

// 实例
interface AcquisitionInstance {
  send(data?: unknown): void
  useVueRouter(router: RouterLike): void
  destroy(): void
}

// 日志类型
type LogType = 'INIT' | 'NET_ERR' | 'FRONT_EXCEPTION' | 'MANUAL' | 'VUE-ROUTER' | 'BEAT'

// 发送的日志结构
interface LogPayload {
  type: LogType
  datetime: number // 毫秒时间戳
  app: string
  user: string
  data?: unknown
}

// INIT 数据
interface InitData {
  userAgent: string
  language: string
  languages: string[]
  platform: string
  screenWidth: number
  screenHeight: number
  devicePixelRatio: number
  timezone: string
  href: string
  referrer: string
  cookieEnabled: boolean
  onLine: boolean
}

// NET_ERR 数据
interface NetErrData {
  method: string
  url: string
  status: number // 0 表示网络层错误
  statusText: string
  duration: number // 毫秒
  requestBody?: string
  responseText?: string
}

// FRONT_EXCEPTION 数据
interface FrontExceptionData {
  type: 'error' | 'unhandledrejection'
  message: string
  source?: string
  lineno?: number
  colno?: number
  stack?: string
}

// VUE-ROUTER 数据
interface VueRouterData {
  from: VueRouteInfo
  to: VueRouteInfo
}
interface VueRouteInfo {
  path: string
  fullPath: string
  name?: string | number | symbol | null
  query: Record<string, unknown>
  params: Record<string, unknown>
  hash: string
}

// Vue Router 兼容接口(v3 / v4 均可)
interface RouterLike {
  afterEach(guard: (to: RouteLocationLike, from: RouteLocationLike) => void): () => void
}

注意事项

库绝不抛出异常。 所有内部错误都被静默处理,采集功能的任何故障都不会影响主程序运行。

开发时使用 test 模式。 设置 sendMethod: 'test' 后,日志仅打印到控制台,不发出任何网络请求,方便调试时查看日志内容。

SPA 务必调用 destroy() 若同一页面需要切换用户或重新初始化,先调用旧实例的 destroy(),再创建新实例。否则旧实例的心跳和监听器不会被清理。

useVueRouter() 须在 router 实例创建后调用。 在调用 createRouter() 之前调用会导致钩子注册失败。

NET_ERR 不采集库自身的请求。 库内部发送日志的请求通过请求头 X-Acquisition-Own 标记,拦截器会自动跳过,不会产生循环上报。

心跳最小间隔为 1 秒。 传入小于 1000 的 interval 值会被自动修正为 1000。