@qzsy/yunxiao-reporter
v0.9.2
Published
云效问题上报浏览器 SDK:悬浮按钮、截图标注、用户图片、查看记录与 console 采集,提交到云效 Bug 平台
Keywords
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所属项目和 SDKuser身份隔离;身份优先级 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:
POST /uploads→ 预签名表单,SDK 直传截图或用户图片(截图 png 超过上限时转 jpeg 重试一次;用户图片不转换,超限由面板提示)。POST /reports→{ reportId }。请求体可带images: [{ objectKey, name? }],顺序即@图片N的编号。POST /reports/mine→{ data, total },按上报人身份返回最近 20 条云端记录。POST /reports/delete→{ deleted },删除自己的记录;已创建的云效工作项会一并删除。
开发
bun run build # dist/:ESM + .d.ts
bun run type-check
bun test
bun run verify:images # 仓库根:图片附件全链路自动验收