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

@qzsy/yunxiao-reporter

v0.9.2

Published

云效问题上报浏览器 SDK:悬浮按钮、截图标注、用户图片、查看记录与 console 采集,提交到云效 Bug 平台

Readme

@qzsy/yunxiao-reporter

浏览器端问题上报 SDK。终端用户点悬浮按钮(或宿主调用 open())后,SDK 截取当前页面,在标注面板里画框、箭头、写文字,填写标题和描述后提交。平台收到后在绑定的云效项目里创建 Bug 工作项。

  • UI 用 Preact 实现并打包进产物,渲染在 <div data-yunxiao-reporter> 的 Shadow DOM 里,宿主样式和 SDK 样式互不影响。宿主不需要安装 Preact。
  • 悬浮按钮支持拖动:位置按项目 Key 存在当前浏览器的 localStorage,刷新和重开页面都保留,不影响其他访客;平台配置的位置只作为默认值。
  • 截图默认走「系统截屏优先、DOM 快照兜底」:Chromium 桌面端截当前标签页视口(用户允许一次),取消或不可用时自动回退到 snapdom 整页快照。详见「截图方式」。
  • 截图用 @zumer/snapdom,按需动态加载,截图时排除 SDK 自身。
  • 自动附带页面 URL、浏览器、屏幕和视口尺寸、语言、时区,以及最近 50 条 console.error / console.warn / 未捕获异常(提交前脱敏,浏览器扩展注入的日志不采集)。
  • 自动截图之外,还可以在「图片附件」里添加最多 6 张用户图片(PNG / JPG / WebP,单张 ≤ 5 MB);面板里任意输入框按 Ctrl/⌘+V 粘贴图片会自动加入附件,并在描述光标处补 @图片N 占位引用;描述里输入 @ 也会弹出图片1、图片2…下拉,选中后插入 @图片N 引用。
  • 面板顶部有「上报问题 / 查看记录」两个 Tab:查看记录从平台云端读取当前上报人的最近 20 条记录(标题、受理号、状态、时间、云效链接),可二次确认删除;已创建的工作项会由服务端一并尝试删除。
  • 模块顶层不访问 window / document,可以在 SSR 代码里 import。
  • 可选适配层:./react(Provider + Hook)与 ./vue(插件 + Provider + composable),按宿主框架选装;./version 只导出当前插件版本。

安装

bun add @qzsy/yunxiao-reporter
# 或 npm i @qzsy/yunxiao-reporter

初始化

import { init } from '@qzsy/yunxiao-reporter'

const reporter = init({ key: 'yxr_pk_xxxxxxxx', endpoint: 'https://bug.example.com' })

右下角会出现悬浮按钮。按住按钮可以拖动位置,单击打开标注面板。自定义入口时:

reporter.open()                                   // 截图并打开标注面板
reporter.close()                                  // 关闭面板
reporter.setUser({ id: 'u_1', name: '张三' })      // 登录态变化时更新上报人
reporter.destroy()                                // 卸载 UI,还原 console,移除监听

同一页面重复调用 init 时,旧实例会先被 destroy。

版本检测

平台在 /config 里下发它当前支持的插件版本(sdkVersion)。宿主内嵌的插件版本与它不一致时,标注面板右上角会提示「请联系维护人员升级插件版本」;版本一致、或平台没有下发(老平台)时不提示。

  • 只在面板打开时展示,不影响悬浮按钮、权限判断和提交。
  • 每条上报都会带 sdkVersion,云效工作项描述里以「插件版本」列出。
  • 平台侧可直接读 @qzsy/yunxiao-reporter/version 的 version 作为期望版本;SDK 内部也用它做比较与上报。

Vue

vue >= 3.3 是可选 peerDependency,只有用 ./vue 入口时才需要。

插件(推荐)

// main.ts
import { createApp } from 'vue'
import { createYunxiaoReporterPlugin } from '@qzsy/yunxiao-reporter/vue'
import App from './App.vue'

createApp(App)
  .use(
    createYunxiaoReporterPlugin({
      publicKey: 'yxr_pk_xxxxxxxx',
      endpoint: 'https://bug.example.com',
    }),
  )
  .mount('#app')

组件里打开面板、同步登录人:

<script setup lang="ts">
import { useReporterUser, useYunxiaoReporter } from '@qzsy/yunxiao-reporter/vue'

const { open, ready } = useYunxiaoReporter()
// me 是 ref / computed / getter,按 id、name、email、phone 内容比较,变化时才 setUser
useReporterUser(me)
</script>

<template>
  <button :disabled="!ready" type="button" @click="open">反馈问题</button>
</template>
  • 插件在 app.use() 时 init,app.unmount() 时 destroy(Vue 3.5+ 用 app.onUnmount,更早版本退化为包一层 unmount)。
  • useYunxiaoReporter() 返回 { ready, open, close, setUser };不在 Provider / 插件下调用时返回 no-op,不抛错。
  • useReporterUser(source) 在实例还没创建时先缓存,创建时补上。

Provider 组件

不想装插件、或需要按组件树挂载时:

<script setup lang="ts">
import { YunxiaoReporterProvider } from '@qzsy/yunxiao-reporter/vue'
</script>

<template>
  <YunxiaoReporterProvider public-key="yxr_pk_xxxxxxxx" endpoint="https://bug.example.com" :user="me">
    <App />
  </YunxiaoReporterProvider>
</template>
  • Provider 本身不渲染 DOM:mounted 时 init,unmounted 时 destroy。
  • 选项与 init 相同,Key 用 publicKey(key 是 Vue 的保留属性名)。
  • user / branding 按内容比较:user 变化只 setUser,branding 变化重建实例。
  • enabled=false 时不初始化,只渲染插槽。

Nuxt 3 / 4

放客户端插件,SSR 阶段不会执行(Nuxt 3 在 plugins/,Nuxt 4 在 app/plugins/):

// Nuxt 3:plugins/yunxiao-reporter.client.ts
// Nuxt 4:app/plugins/yunxiao-reporter.client.ts
import { createYunxiaoReporterPlugin } from '@qzsy/yunxiao-reporter/vue'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(
    createYunxiaoReporterPlugin({
      publicKey: 'yxr_pk_xxxxxxxx',
      endpoint: 'https://bug.example.com',
    }),
  )
})

Vue 的 runtime-dom 在模块加载时读取 document,所以不要把 createApp().mount() 放在服务端进程;SDK 入口本身可以在服务端 import(模块顶层不访问 window)。

React

react 是可选 peerDependency(>=18),只有用 ./react 入口时才需要。

import { YunxiaoReporterProvider, useYunxiaoReporter } from '@qzsy/yunxiao-reporter/react'

export function App({ children, me }) {
  return (
    <YunxiaoReporterProvider publicKey="yxr_pk_xxxxxxxx" endpoint="https://bug.example.com" user={me}>
      {children}
    </YunxiaoReporterProvider>
  )
}

function FeedbackLink() {
  const { open, ready } = useYunxiaoReporter()
  return (
    <button type="button" disabled={!ready} onClick={() => open()}>
      反馈问题
    </button>
  )
}
  • Key 通过 publicKey 传入。key 是 React 的保留属性,组件拿不到。其余 props 与 init 的选项相同。
  • Provider 在 useEffect 里 init,卸载时 destroy,兼容 StrictMode 的 init → destroy → init。
  • user 变化时调用 setUser,不会重建实例。
  • enabled={false} 时不初始化,只渲染 children。
  • Provider 本身不渲染任何 DOM。

选项

| 选项 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | key | string | 必填 | 项目公开 Key,yxr_pk_ 开头。React / Vue 里写作 publicKey | | endpoint | string | 必填 | 平台根地址,如 https://bug.example.com,末尾 / 会去掉。必须是 http(s) 绝对地址 | | user | { id?, name?, email? } \| null | — | 上报人,只作展示,平台标注「宿主提供,未验证」 | | fab | boolean | true | 是否显示悬浮按钮 | | position | 'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left' | 'bottom-right' | 悬浮按钮位置(别名 fabPosition) | | shortcut | string | 不绑定 | 打开面板的快捷键,如 'ctrl+shift+b' | | zIndex | number | 2147483000 | 宿主元素层级 | | captureConsole | boolean | true | 是否采集 console.error / warn、error、unhandledrejection(浏览器扩展注入的日志始终丢弃) | | captureMode | 'auto' \| 'dom' \| 'screen' | 'auto' | 截图方式,见下方「截图方式」 | | onSubmitted | ({ reportId }) => void | — | 提交成功,reportId 即受理号 | | onError | (error: ReporterError) => void | — | 提交失败、截图失败、截图上传被跳过或用户图片上传失败时调用 |

截图失败或截图上传失败时,仍然可以不带截图提交,并通过 onError 告知宿主。

截图方式

浏览器没有「无授权的页面截图」API,能截真实画面的只有 Screen Capture API(getDisplayMedia)。它每次调用都要用户点一次允许,而且只能截当前视口,所以 SDK 把它做成了增强路径,失败时始终回退到原来的 DOM 快照:

| captureMode | 行为 | | --- | --- | | auto(默认) | Chromium 桌面端(Chrome / Edge 等)优先弹「允许共享此标签页」,截当前视口;取消、拒绝、iframe 未授权或超时都会静默改用 DOM 快照。Safari / Firefox / 移动端直接用 DOM 快照,避免弹出源选择器 | | dom | 始终 DOM 快照:静默、截整页,不弹授权。对一键上报体验最友好 | | screen | 强制尝试系统截屏(所有支持 getDisplayMedia 的浏览器,Safari / Firefox 会弹源选择),失败回退 DOM |

DOM 快照用 snapdom 克隆页面,截的是完整可滚动页面;系统截屏截的是当前视口,但能拿到 canvas / WebGL / 视频 / 跨域 iframe 的真实像素。宿主在跨域 iframe 里接入时,需要 allow="display-capture" 才可能用上系统截屏,否则自动回退。

宿主弹窗里上报

宿主的弹层(shadcn/Radix、Base UI、Ant Design、Element Plus 等)靠「点击落在弹层外」来关闭。上报器挂在 document.body 下,如果不处理,点悬浮按钮对宿主来说就是一次外部点击,弹窗会在截图开始前被关掉。SDK 做了两件事:

  • 悬浮按钮的指针事件不传给宿主:pointerdown / pointermove / pointerup / mousedown / click / touchstart 在 window 捕获阶段被截下,再在上报器自己的 Shadow DOM 里重放,按钮的点击和拖动位置照常工作,宿主的 outside click 不会触发。
  • 面板打开后宿主页面被标记为 inert:宿主 modal 的焦点陷阱(例如 Radix 的 FocusScope)抢不动 inert 里的元素,面板里的输入框才拿得住焦点;关掉面板后原样还原。此时截图已经完成,宿主弹窗之后被关掉不影响上报内容。

限制与建议:

  • 如果宿主在 window 捕获阶段 自己处理指针事件,或者在 init 之后把整个 body 设成 inert / pointer-events: none,悬浮按钮可能收不到点击。这时用快捷键(shortcut)或由宿主自己的「反馈」按钮调用 reporter.open(),这两个入口都不产生 pointerdown。
  • 老浏览器不支持 inert 时自动退化成不冻结页面,截图与提交仍然正常。
  • 面板里的 Esc 只关闭面板,不会传导给宿主的弹层。

截图标注

标注面板提供矩形、箭头、文字三种工具和 6 种颜色。画完的标注都可以再调整,不需要切换工具:

  • 悬停:鼠标移到标注上会出现虚线框(箭头是浅色描边),光标变为移动;移到手柄上变为对应方向的缩放光标。
  • 选中与拖动:点一下选中,按住直接拖动。未选中的矩形只认边框,框内仍可继续画新标注;选中后整块可拖。拖动不会把标注拖出图片。
  • 缩放:矩形 8 个手柄(边太短时省掉中点),箭头拖两端,文字拖右下角等比改字号。
  • 文字:双击文字原地编辑,回车确认,清空即删除。双击空白处恢复「适应宽度」。
  • 操作条:选中后标注上方出现删除按钮(文字另有编辑按钮);选中时点颜色直接给该标注换色。
  • 快捷键:Delete / Backspace 删除选中;方向键微调 1px,Shift 加方向键一次 10px,连续微调算一步撤销;Esc 先取消选中,再按才关闭面板;Ctrl/⌘+Z 撤销移动、缩放、换色、删除。
  • 移动端:单指点选并拖动,判定范围和手柄比鼠标更大;第一根手指落在标注上时双指捏合缩放该标注,落在空白处时双指捏合缩放整张图;双击文字编辑。

选中框和手柄画在单独的叠加层上,不会出现在提交的截图里。

用户图片与 @ 引用

自动截图之外,面板的「图片附件」支持用户自己添加图片:

  • 最多 6 张,单张 ≤ 5 MB,只支持 PNG / JPG / WebP;支持点击选择、拖拽和从剪贴板粘贴。
  • 面板打开时,在任意输入框(标题、描述等)按 Ctrl/⌘+V 粘贴剪贴板中的图片:图片自动加入附件,并在描述的光标处插入 @图片N 占位引用(多张按顺序编号、用空格分隔);描述区折叠时会自动展开,便于确认结果。剪贴板里没有图片时保持普通文本粘贴。
  • 图片按添加顺序编号:图片1、图片2……。缩略图只展示编号和移除入口,不再展示文件名;删除后编号自动平移,描述里已插入的引用会同步调整。
  • 描述输入框里输入 @(或全角 @)会自动弹出图片下拉,继续输入「图」「图片」「2」或文件名可过滤;↑ / ↓ 移动、Enter / Tab 选中、Esc 关闭,也可以直接点击。选中后插入 @图片1 这样的标签;「@ 引用图片」按钮可以在光标处手动触发。
  • 提交时先上传用户图片,再提交上报。用户图片上传失败不会静默丢图,面板会留在编辑态并提示重试;已经上传成功的图片会缓存,重试只补传失败 / 新增的图片。

平台侧把描述里的有效 @图片N 替换成对应图片;没有被描述引用到的图片会按编号追加到云效工作项末尾的「图片附件」。图片对象仍通过 /uploads 预签名直传 OSS,SDK 只上报 images: [{ objectKey, name? }],不传任意 URL。

查看记录

面板顶部的「查看记录」Tab 从平台云端读取当前上报人的最近 20 条记录:

  • 每行显示标题、受理号、状态(排队中 / 投递中 / 已创建 / 投递失败)、提交时间;已创建时提供云效链接。
  • 记录按 X-Reporter-Key 所属项目和 SDK user 身份隔离;身份优先级 id > email > phone > name。没有传 user 时不能查看 / 删除云端记录。
  • 平台不验证宿主传入的 user(和上报名单的信任模型一致);请不要在公开页面伪造上报人身份。
  • 删除需要二次确认:
    • QUEUED / FAILED:删除平台记录,尚未创建云效工作项;
    • SENDING:返回 409,稍后再试;
    • CREATED:服务端调用云效 DeleteWorkitem 删除工作项后再删平台记录;云效失败会提示重试,记录保留。
  • 查看 / 删除不占每日上报额度,单独按 IP 限流;记录不轮询状态,可点「刷新」重新读取。

错误码

ReporterError 带 code 和可选的 HTTP status。面板里展示的中文文案如下:

| code | HTTP | 文案 | | --- | --- | --- | | KEY_INVALID | 401 | Key 无效 | | KEY_EXPIRED | 401 | Key 已过期 | | ORIGIN_NOT_ALLOWED | 403 | 当前域名未加入白名单 | | PROJECT_DISABLED | 403 | 项目未启用 | | QUOTA_EXCEEDED | 429 | 今日上报额度已用完 | | RATE_LIMITED | 429 | 提交太频繁,请稍后再试 | | VALIDATION_ERROR | 422 | 内容校验失败 | | UPLOAD_UNAVAILABLE | 503 | 图片上传暂不可用(截图会跳过;用户图片会中断提交并提示重试) | | IMAGE_UPLOAD_FAILED | — | 图片上传失败,请重试 | | IMAGE_TOO_LARGE | — | 单张图片不能超过 5 MB | | IMAGE_TYPE_UNSUPPORTED | — | 只支持 PNG、JPG、WebP 图片 | | IMAGE_TOO_MANY | — | 最多只能添加 6 张图片 | | IMAGE_EMPTY | — | 图片内容为空,请重新选择 | | REPORTER_IDENTITY_REQUIRED | 422 | 未提供上报人信息,无法查看云端记录 | | REPORT_NOT_FOUND | 404 | 记录不存在或不属于你 | | REPORT_DELETE_CONFLICT | 409 | 记录正在投递中,请稍后再试 | | YUNXIAO_UNAVAILABLE | 502 | 云效接口暂时不可用,请稍后再试 | | NETWORK_ERROR | — | 网络连接失败,请检查网络后重试 | | TIMEOUT | — | 请求超时,请稍后重试 | | NOT_BROWSER | — | 只能在浏览器中初始化(init 抛出) | | INVALID_OPTIONS | — | key / endpoint 缺失或格式不对(init 抛出) |

其它错误显示服务端返回的 error 字段;没有时显示「上报失败,请稍后重试」。

白名单与安全

  • Key 是公开的,会出现在宿主的前端产物和网络请求里,不是秘密。
  • 安全靠服务端:平台按请求的 Origin 校验 Key 的域名白名单,不在白名单直接返回 403;每把 Key 有每日额度,单 IP 有频率限制;Key 可以随时吊销或轮换。
  • 接入前请在平台「我的项目 → Key 管理」把宿主域名加进白名单。本地开发需要打开 localhost 开关。
  • SDK 只能传标题、描述、截图、用户图片、环境信息和 console 日志。云效项目、指派人、优先级等字段由平台配置决定,SDK 不能覆盖。
  • 请求不带 cookie(credentials: 'omit')。截图和用户图片都直传对象存储,不带 Key 头。
  • 服务端会校验每个图片 objectKey 是否属于本项目并去重,最多 6 张;不接收客户端拼的任意 URL。
  • console 日志和 URL 在离开浏览器前脱敏:键名含 token、password、passwd、pwd、authorization、cookie、secret、api_key、session 的值,URL 查询参数和 hash 里的同类参数,Bearer / Basic 凭据和 URL 里的密码都会替换为 ***,单条截断到 500 字符。
  • 浏览器扩展造成的噪音直接丢弃,不占 50 条缓冲:日志里出现 chrome-extension://、moz-extension://、safari-web-extension:// 等扩展脚本地址(例如扩展包装 window.fetch 后产生的 Failed to fetch),以及 Unchecked runtime.lastError、Extension context invalidated 这类扩展固定文案。宿主 console 行为不受影响,只是不上报。
  • 截图和用户图片都是公开读链接,会写进云效工作项。页面上的敏感数据请在提交前用矩形框标注提醒,或不上报该页面。
  • 查看记录从服务端读取,按项目 Key + 上报人身份隔离;平台不验证 user 真伪,请只在可信宿主里使用,并给 Key 配好 Origin 白名单。删除需要二次确认,CREATED 记录会一并调用云效 DeleteWorkitem。

平台接口

SDK 调用 ${endpoint}/api/reporter/v1,请求头 X-Reporter-Key:

  1. POST /uploads → 预签名表单,SDK 直传截图或用户图片(截图 png 超过上限时转 jpeg 重试一次;用户图片不转换,超限由面板提示)。
  2. POST /reports → { reportId }。请求体可带 images: [{ objectKey, name? }],顺序即 @图片N 的编号。
  3. POST /reports/mine → { data, total },按上报人身份返回最近 20 条云端记录。
  4. POST /reports/delete → { deleted },删除自己的记录;已创建的云效工作项会一并删除。

开发

bun run build         # dist/:ESM + .d.ts
bun run type-check
bun test
bun run verify:images # 仓库根:图片附件全链路自动验收