右边是真的在跑的组件,不是效果图。在左边假装是「桐桐画坊」的窗口里画两笔, 然后拖动右下角的「反馈」按钮、点它 —— 看它抓走了什么。
贴在 </body> 之前就行。它会自动把现场采集、表单、样式一起拉起来,你不需要额外引任何东西。
凭证(data-zy-key)不用申请:后台 → 建项目/版本 → 版本详情 →「包内反馈入口」一键生成(一版一个,稳定不变);上面第 1 节选好版本,这里的代码会自动填上真凭证。
| 属性 | 作用 | 取值 |
|---|---|---|
data-zy-key | 这一版的反馈凭证。系统凭它把反馈归到对应的项目和版本上 | FK-XXXXX-XXXXX(后台版本详情里生成) |
data-zy-mode | 哪一种触发姿势 | test 浮动按钮 · release 连击 5 下 · 不加则按有没有 key 自动定 |
data-zy-endpoint | 上报地址。默认回到「组件从哪来,就往哪报」 | 只有把组件文件搬到别处托管时才需要写 |
data-zy-label | 按钮文案 | 默认「反馈」 |
data-zy-snapshot | off 可以关掉自动页面快照 | 默认开 |
| 版本 | 入口 | 为什么 |
|---|---|---|
测试版data-zy-mode="test" |
可移动浮动按钮常显 | 测试者要能随时报。找不到入口这件事本身就是最大的障碍 —— 他会攒到测试结束再说,那时细节已经忘了一半 |
正式版data-zy-mode="release" |
单指连击 5 下 | 线上用户不该看见调试入口。但真出事时,他得有办法报上来 —— 连击 5 下不会被误触,也不需要他会描述「请把日志发给我」 |
连击判定:1.6 秒内 5 次,每一下都要落在起点 26px 以内 —— 边走边点不会误触发。桌面浏览器用鼠标连点 5 下等效。
| 内容 | 怎么来的 | 可靠性 |
|---|---|---|
| 页面快照 | 抓当前页面渲染结果,缩到 1280 宽、JPEG 0.72 | 尽力而为。跨域图片会变灰块,iframe 内拿不到 |
| 屏幕上的文字 | 可见文本前 400 字 | 基本不会失败 |
| 控制台错误 | 组件装载时就挂钩子,最多 20 条 | 只捞装载之后抛的错 |
| 失败的网络请求 | 4xx / 5xx / 断网,最多 10 条 | 记录在载入组件之后 |
| 环境 | 机型、系统、屏幕、DPR、语言、当前网址、触发方式 | 必定带上 |
| 你写的复现方法 / 错误点 | 必填,前后端各挡一道 | 提交的硬门槛 |
页面快照只能拿到 WebView 里的内容。如果你想要一张连原生界面一起的全屏截图, 让原生侧接一根桥即可 —— 协议只有一句话:我们喊一声,你把图回灌给我们。
// 1. 注册桥。网页侧会 postMessage {cmd:"shot"} let script = """ window.addEventListener('DOMContentLoaded', function () { window.webkit.messageHandlers.zyfb && null; }); """ let ucc = WKUserContentController() ucc.add(self, name: "zyfb") // 网页喊的就是这个名字 cfg.userContentController = ucc // 2. 收到喊话 → 截图 → 回灌 func userContentController(_ uc: WKUserContentController, didReceive message: WKScriptMessage) { guard (message.body as? [String: Any])?["cmd"] as? String == "shot" else { return } let cfg = WKSnapshotConfiguration() webView.takeSnapshot(with: cfg) { image, _ in guard let d = image?.jpegData(compressionQuality: 0.72) else { return } let b64 = "data:image/jpeg;base64," + d.base64EncodedString() webView.evaluateJavaScript( "window.ZYSnapshot && window.ZYSnapshot.attach('\(b64)')") } }
// 1. 暴露桥对象 webView.addJavascriptInterface(object { // 组件会调用这个方法 @JavascriptInterface fun shot() { webView.post { val bmp = Bitmap.createBitmap(webView.width, webView.height, Bitmap.Config.ARGB_8888) webView.draw(Canvas(bmp)) // 含 WebView 内的全部渲染结果 val bos = ByteArrayOutputStream() bmp.compress(Bitmap.CompressFormat.JPEG, 72, bos) val b64 = Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP) webView.post { webView.evaluateJavascript( "window.ZYSnapshot \&\& window.ZYSnapshot.attach('data:image/jpeg;base64,$b64')", null) } } } }, "ZYFeedbackAndroid") // 2. 想要连状态栏一起拍,用 MediaProjection;只在 WebView 内就够的话上面这段已经是了
接上桥之后,组件会优先用原生给的那张图。900 毫秒内没等到就自动退回页面快照 —— 原生侧不该让用户卡在提交页上。
整个页面都是纯原生(没有 WebView)?不走组件,直接打 POST /api/v1/reports(对任意来源开放、匿名)。字段与组件提交的完全一致,截图原生侧自己截
(SwiftUI ImageRenderer / Compose View.draw),转 data URL 塞进 media;
摇一摇等原生手势唤面板用 evaluateJavascript("window.ZYFeedback.open()")。
POST /api/v1/reports Content-Type: application/json { "feedbackKey": "FK-…", // 后台版本详情里生成的凭证,决定反馈挂到哪一版 "steps": "1. … 2. …", // 复现方法(带凭证时 steps / actual 至少一个要有内容) "actual": "哪不对", "version": "1.4.0-beta.3", "reporter": { "channel": "native-ios" }, "env": { "model": "iPhone16,2", "os": "iOS 20.0" }, "media": [ { "kind": "shot", "mime": "image/jpeg", "name": "01-autoshot.jpg", "data": "data:image/jpeg;base64,…" } ] } // ≤12 张、每张 ≤8MB
fetch('/api/v1/onsite/check?key=FK-XXXXX-XXXXX')
.then(r => r.json()).then(console.log)
found: true 说明这个凭证认识这一版;release 里能看到它挂到哪个项目哪一版。
返回 found: false 也别慌 —— 反馈照样能提交,只是会进总池、不挂具体版本。
POST /api/v1/reports 与 GET /api/v1/feedback-key/:key
允许任何来源直接调用(匿名上报,不带 Cookie)。组件文件托管在别的域也没关系。