@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/ 未捕获异常(提交前脱敏,浏览器扩展注入的日志不采集)。 - 模块顶层不访问
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" 才可能用上系统截屏,否则自动回退。
错误码
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 | 截图上传暂不可用(不带截图继续提交) |
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 头。 - 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 行为不受影响,只是不上报。 - 截图是公开读链接,会写进云效工作项。页面上的敏感数据请在提交前用矩形框标注提醒,或不上报该页面。
平台接口
SDK 调用 ${endpoint}/api/reporter/v1,请求头 X-Reporter-Key:
POST /uploads→ 预签名表单,SDK 直传截图(png 超过上限时转 jpeg 重试一次)。POST /reports→{ reportId }。
开发
bun run build # dist/:ESM + .d.ts
bun run type-check
bun test