📺 Web Tape

Web Tape — Guide

A lightweight, rrweb-based bug-scene recording SDK. Capture user actions, network requests and console logs, then replay them with a scrubbing timeline, on-frame annotations, cURL reproduction and AI summaries — so "cannot reproduce" is a thing of the past.

⚠️ Data masking is on the roadmap. For now, inject only in test / staging. The SDK starts background recording on load — use caution in production user environments.

Web Tape 使用指南

基于 rrweb 的轻量级 Bug 现场录制 SDK。采集用户操作、网络请求与控制台日志,配合时间轴回放、画面批注、cURL 复现与 AI 摘要——彻底告别「无法复现」。

⚠️ 数据脱敏仍在规划中,当前建议仅在测试 / 预发环境注入。SDK 加载即开始后台录制,生产用户环境请谨慎。

How it works

Web Tape has two cooperating parts. Understanding this first makes the rest of the guide obvious:

  • Capture SDK (@webtapejs/toolbox) — a companion ("parasitic") library that you inject into the front-end app you want to record, via npm or a <script> tag. It runs inside that page, records events and uploads them. After install it offers two UI modes:
    • Built-in FAB — call mountFab() and a floating record button appears (zero UI work);
    • Custom UI — skip the FAB and drive recording via the Core API from your own buttons/flows.
  • Replay service — stores recordings and serves the replay UI. You self-host it (one curl command) and point the SDK's serverUrl at it. There is no public hosted service.

Order to set it up: ① deploy the replay service (get its URL) → ② install the SDK in your app and set serverUrl to that URL → ③ choose a UI mode. Flow at runtime: SDK records a sliding window (or active recording) → uploads to /api/replayer → you get a shareable replay link.

工作原理

Web Tape 由两部分协作,先理解这层关系,后面的步骤就顺理成章:

  • 采集 SDK@webtapejs/toolbox)——一个寄生(伴随)库,通过 npm 或一行 <script> 注入到你要录制的前端应用里,随该页面运行、录制事件并上传。安装后提供两种 UI 模式
    • 内置悬浮按钮(FAB)——调用 mountFab() 即出现悬浮录制按钮(零 UI 工作量);
    • 自渲染 UI——不挂 FAB,用 Core API 从你自己的按钮/流程里控制录制。
  • 回放服务——存储录制、提供回放界面。由你自建(一行 curl),并把 SDK 的 serverUrl 指向它。没有公共托管服务。

上手顺序:① 部署回放服务(拿到它的地址)→ ② 在你的应用里装 SDK,把 serverUrl 设为该地址 → ③ 选一种 UI 模式。运行时流程:SDK 录制滑动窗口(或主动录制)→ 上传到 /api/replayer → 得到可分享的回放链接。

Install

npm install @webtapejs/toolbox
# or: pnpm add @webtapejs/toolbox / yarn add @webtapejs/toolbox

The npm entry is side-effect free — nothing runs until you call it. Two ways to use it: the built-in floating button (fastest), or the Core API (full control).

安装

npm install @webtapejs/toolbox
# 或:pnpm add @webtapejs/toolbox / yarn add @webtapejs/toolbox

npm 入口无副作用——不调用就什么都不会跑。两种用法:内置悬浮按钮(最快),或 Core API(完全掌控)。

Mode 1 · Built-in FAB

recommended The fastest path. Call mountFab() and a floating record button appears bottom-right with the full record UI — start/stop, a confirm popover, a duration guard, and a "report the last N seconds" shortcut.

import { configure, mountFab } from '@webtapejs/toolbox'

// Load only in test/staging, and only once (e.g. your app entry).
configure({
  serverUrl: 'https://your-replayer.example.com/api/replayer', // required
  errorPrompt: true,   // sentinel mode
  locale: 'en',        // 'en' (default) | 'zh'
})
mountFab()

Background sliding-window recording starts automatically regardless of the FAB.

模式一 · 内置 FAB

推荐 最快接入。调用 mountFab(),页面右下角出现悬浮录制按钮,内置完整录制 UI——开始/停止、二次确认、时长保护,以及「上报最近 N 秒」快捷入口。

import { configure, mountFab } from '@webtapejs/toolbox'

// 仅测试/预发加载,且只加载一次(如放在应用入口)。
configure({
  serverUrl: 'https://your-replayer.example.com/api/replayer', // 必填
  errorPrompt: true,   // 哨兵模式
  locale: 'zh',        // 'en'(默认)| 'zh'
})
mountFab()

无论是否挂 FAB,后台滑动窗口录制都会自动启动。

Mode 2 · Core API

Skip mountFab() and drive recording yourself — wire it into your own buttons, feedback entry, or automated flows.

import {
  configure, startRecord, stopRecord, discardRecord,
  reportRecent, getState, onStateChange,
} from '@webtapejs/toolbox'

configure({ serverUrl: 'https://your-replayer.example.com/api/replayer' })

模式二 · Core API

跳过 mountFab(),自己掌控录制时机——接到你自己的按钮、反馈入口或自动化流程里。

import {
  configure, startRecord, stopRecord, discardRecord,
  reportRecent, getState, onStateChange,
} from '@webtapejs/toolbox'

configure({ serverUrl: 'https://your-replayer.example.com/api/replayer' })

configure(config)

Inject runtime configuration. Can be called repeatedly — values are merged and take effect immediately (no need to reload rrweb).

configure({
  serverUrl: 'https://your-replayer.example.com/api/replayer',
  autoBackgroundRecord: true,
  backgroundWindowMs: 60_000,
  errorPrompt: true,
  errorPromptIgnore: { statusCodes: [404, 499], urls: ['/health', /\/ping/] },
  locale: 'en',
})
FieldTypeDefaultDescription
serverUrlstringUpload endpoint of your replay service. Required in production.
autoBackgroundRecordbooleantrueAuto-start the background sliding-window recording.
backgroundWindowMsnumber30000Sliding window length (ms). Larger = more memory; keep ≤ 60000.
errorPromptbooleanfalseSentinel mode: prompt a one-click report on HTTP ≥ 400.
errorPromptIgnore.statusCodesnumber[]Status codes to ignore (no toast).
errorPromptIgnore.urls(string|RegExp)[]URLs to ignore (substring or RegExp).
locale'en'|'zh''en'Language of the injected UI.
Performance: the SDK only records DOM events (no screenshots / video encoding), so overhead is far smaller than canvas recording. If you only need manual recording and no rewind, set autoBackgroundRecord: false.

configure() 配置

注入运行时配置。可重复调用——配置合并且立即生效(无需重启 rrweb)。

configure({
  serverUrl: 'https://your-replayer.example.com/api/replayer',
  autoBackgroundRecord: true,
  backgroundWindowMs: 60_000,
  errorPrompt: true,
  errorPromptIgnore: { statusCodes: [404, 499], urls: ['/health', /\/ping/] },
  locale: 'zh',
})
字段类型默认说明
serverUrlstring回放服务上传地址。生产必填。
autoBackgroundRecordbooleantrue是否自动启动后台滑动窗口录制。
backgroundWindowMsnumber30000滑动窗口长度(毫秒)。越大占用越多,建议 ≤ 60000。
errorPromptbooleanfalse哨兵模式:HTTP ≥ 400 时弹一键上报提示。
errorPromptIgnore.statusCodesnumber[]忽略的状态码(不弹提示)。
errorPromptIgnore.urls(string|RegExp)[]忽略的 URL(子串或正则)。
locale'en'|'zh''en'注入 UI 的语言。
性能:SDK 只录制 DOM 事件(不截图/不编码视频),开销远小于 canvas 录制。若只需手动录制、不需要回溯,设 autoBackgroundRecord: false

startRecord / stopRecord / discardRecord

await startRecord()                 // only callable in the `finished` state
const result = await stopRecord()   // stop & upload → RecordingResult | false
discardRecord()                     // discard without uploading; back to `finished`
MethodReturnsNotes
startRecord()Promise<void>Start active recording; state → recording.
stopRecord()Promise<RecordingResult|false>Stop & upload; result on success, false otherwise.
discardRecord()voidDrop the current recording without uploading.
interface RecordingResult {
  sourceId: string // 32-hex recording id
  url: string      // full, shareable replay link
}

startRecord / stopRecord / discardRecord

await startRecord()                 // 仅 `finished` 态可调
const result = await stopRecord()   // 停止并上传 → RecordingResult | false
discardRecord()                     // 丢弃不上传,回到 `finished`
方法返回说明
startRecord()Promise<void>开始主动录制;状态 → recording
stopRecord()Promise<RecordingResult|false>停止并上传;成功返回结果,否则 false
discardRecord()void丢弃当前录制,不上传。
interface RecordingResult {
  sourceId: string // 32 位 hex 录制标识
  url: string      // 完整可分享回放链接
}

reportRecent()

Upload the last backgroundWindowMs from the background sliding window — no need to start recording ahead of time. Ideal for auto-reporting on form-submit failures or request timeouts.

async function onSubmitError() {
  const result = await reportRecent() // RecordingResult | false
  if (result) submitBugReport({ replayUrl: result.url })
}
Only callable in the finished state; returns false while recording / uploading.

reportRecent() 回溯

上传后台滑动窗口最近 backgroundWindowMs 的内容——无需提前开录。适合表单提交失败、接口超时等场景的自动上报。

async function onSubmitError() {
  const result = await reportRecent() // RecordingResult | false
  if (result) submitBugReport({ replayUrl: result.url })
}
finished 态可调;recording / uploading 态返回 false

getState() / onStateChange()

Read the current state or subscribe to changes, to drive your own UI.

const state = getState()  // 'finished' | 'recording' | 'uploading'

const unsubscribe = onStateChange((state) => {
  // fires once immediately with the current state
})
unsubscribe() // on unmount

State machine: finished → startRecord → recording → stopRecord → uploading → finished (discardRecord returns to finished directly).

getState() / onStateChange() 状态与订阅

读取当前状态或订阅变化,驱动你自己的 UI。

const state = getState()  // 'finished' | 'recording' | 'uploading'

const unsubscribe = onStateChange((state) => {
  // 订阅时立即回调一次当前状态
})
unsubscribe() // 组件卸载时取消

状态机:finished → startRecord → recording → stopRecord → uploading → finisheddiscardRecord 直接回到 finished)。

Background sliding window — internals

On load the SDK records continuously, but memory only ever holds the most recent slice of events. This is what powers reportRecent() / the FAB's "upload last N seconds" — you can capture a bug after it happened, without recording ahead.

Double buffer + checkout signal

Two buffers are kept:

  • prev — events from the previous checkout cycle;
  • curr — the current cycle, whose first event is always a FullSnapshot.

rrweb is started with checkoutEveryNms = backgroundWindowMs. Periodically it emits a fresh FullSnapshot flagged isCheckout=true; on that signal the buffers rotate:

on emit(event, isCheckout):
  if isCheckout && event is FullSnapshot:
    prev = curr        // archive the finished cycle
    curr = []          // start a new cycle at the fresh FS
  curr.push(event)

A report returns prev.concat(curr), so it always begins with a FullSnapshot (needed to rebuild the DOM) and spans [windowMs, 2×windowMs).

Why two buffers?

Trimming a single buffer purely by time could cut off the FullSnapshot at the start, leaving events that can't be replayed (blank screen). Keeping the whole previous cycle guarantees a valid FullSnapshot anchor while bounding memory.

Long-idle fallback

checkoutEveryNms only fires when increments arrive. If the user is idle for a long time, no checkout happens and prev's span can far exceed the window. On report, if the total span exceeds 2×windowMs, prev is discarded and only curr (a fresh FullSnapshot) is used — so the worst case is roughly one static snapshot, not cross-day data.

Memory & config

  • Only the last [windowMs, 2×windowMs) of events is retained; older events are dropped on rotation.
  • backgroundWindowMs sets the window (default 30s; keep ≤ 60s). Larger = more memory.
  • autoBackgroundRecord: false disables the background window entirely (manual recording only).

后台滑动窗口 — 实现细节

SDK 加载即持续录制,但内存里始终只保留最近一段事件。这正是 reportRecent() / FAB「上报最近 N 秒」的底层——出问题后再回溯,无需提前开录。

双 buffer + checkout 信号

维护两个 buffer:

  • prev —— 上一个 checkout 周期的事件;
  • curr —— 当前周期,其首个事件必为 FullSnapshot(全量快照)

rrweb 以 checkoutEveryNms = backgroundWindowMs 启动,会周期性 emit 一个带 isCheckout=true 的全量快照;收到该信号即轮换:

emit(event, isCheckout) 时:
  若 isCheckout 且 event 为 FullSnapshot:
    prev = curr        // 归档已完成的周期
    curr = []          // 从新的全量快照开始新周期
  curr.push(event)

上报返回 prev.concat(curr),因此必以 FullSnapshot 开头(重建 DOM 所需),时间跨度稳定在 [windowMs, 2×windowMs)

为什么用两个 buffer?

若只用单 buffer 按时间裁剪,可能把开头的全量快照裁掉,剩下的事件无法回放(白屏)。保留完整的上一周期,既能保证有效的 FullSnapshot 起点,又能约束内存占用。

长静默兜底

checkoutEveryNms 仅在收到增量事件时才检查触发。用户长时间不操作时不会产生 checkout,prev 的跨度可能远超窗口。上报时若总跨度超过 2×windowMs,则丢弃 prev、只用 curr(起点是 fresh 全量快照)——最坏情况约等于一帧静态截图,而不是跨天数据。

内存与配置

  • 只保留最近 [windowMs, 2×windowMs) 的事件,轮换时丢弃更早的。
  • backgroundWindowMs 设定窗口(默认 30s,建议 ≤ 60s)。越大占用越多。
  • autoBackgroundRecord: false 可完全关闭后台窗口(仅手动录制)。

Sentinel mode

With errorPrompt: true, the SDK watches every HTTP request. On status ≥ 400 it pops a toast (bottom-right) guiding the user to report the scene in one click.

  • Only one toast at a time; consecutive errors are de-duplicated.
  • Auto-dismisses after ~6s.
  • Never interrupts while recording / uploading.
  • Use errorPromptIgnore to exclude health checks and other harmless endpoints.

哨兵模式

开启 errorPrompt: true 后,SDK 监听每个请求;状态码 ≥ 400 时在右下角弹 toast,引导用户一键上报现场。

  • 同时只显示一个 toast,连续错误自动去重。
  • 约 6 秒后自动消失。
  • recording / uploading 期间不打扰。
  • errorPromptIgnore 排除健康检查等无害接口。

AI analysis

On the replay page, the AI Analysis panel sends the recording's network requests + console logs + DOM stats to an AI workflow and returns a root-cause summary and suggestions. It is opt-in and requires configuration on the replay service — if unset, the feature degrades gracefully (a message says it's not configured) while replay / network / annotations keep working.

Built with a Dify workflow

The analysis is powered by a Dify AI workflow (any compatible Workflow API works). Build a workflow that:

  • accepts these input variables: network_requests_info, console_logs_info, dom_nodes_info;
  • runs in blocking mode and exposes the final answer as an output variable named result.

Configure the replay service

Set these in the replayer's .env (server-side only):

# Dify workflow run endpoint, e.g. https://api.dify.ai/v1/workflows/run
AI_WORKFLOW_API_URL=...
# Dify workflow API key (kept on the server, never shipped to the browser)
AI_WORKFLOW_API_KEY=...

Restart the replayer; the AI Analysis button in the replay UI now works.

Security: the browser never sees the key. The replayer exposes a server-side proxy POST /api/ai/analyze that attaches the key and forwards to the workflow — so AI_WORKFLOW_API_KEY stays out of the client bundle.

AI 分析

在回放页,AI 分析面板会把本次录制的网络请求 + 控制台日志 + DOM 统计发给 AI 工作流(workflow),返回根因摘要与改进建议。该功能默认关闭、需在回放服务端配置——未配置时会优雅降级(提示未配置),回放 / 网络 / 批注等其它能力不受影响。

结合 Dify 工作流构建

分析能力由 Dify AI 工作流驱动(兼容的 Workflow API 也可)。你需要搭一个工作流:

  • 接收输入变量:network_requests_infoconsole_logs_infodom_nodes_info
  • blocking 模式运行,并把最终结果输出为名为 result 的输出变量。

配置回放服务

在 replayer 的 .env 里设置(仅服务端):

# Dify 工作流运行端点,例如 https://api.dify.ai/v1/workflows/run
AI_WORKFLOW_API_URL=...
# Dify 工作流 API Key(只存在于服务端,绝不下发到浏览器)
AI_WORKFLOW_API_KEY=...

重启 replayer,回放界面里的 AI 分析 按钮即可使用。

安全:浏览器永远拿不到 Key。replayer 提供服务端代理 POST /api/ai/analyze,在服务端附上 Key 再转发给工作流——因此 AI_WORKFLOW_API_KEY 不会进前端 bundle。

CDN usage

For QA / staging pages without a build step, load the IIFE bundle. The same API is exposed on window._webTape.

<script src="https://unpkg.com/@webtapejs/toolbox/dist/web-tape.iife.js"></script>
<script>
  window._webTape.configure({ serverUrl: 'https://your-replayer.example.com/api/replayer' })
</script>

Add data-builtin-fab="false" to stay silent (no FAB), or data-locale="zh" for Chinese. Prefer the npm package for real projects — it's tree-shakeable, typed and side-effect free.

CDN 引入

无构建步骤的 QA / 预发页面,可直接引入 IIFE 产物;同一套 API 挂在 window._webTape 上。

<script src="https://unpkg.com/@webtapejs/toolbox/dist/web-tape.iife.js"></script>
<script>
  window._webTape.configure({ serverUrl: 'https://your-replayer.example.com/api/replayer' })
</script>

data-builtin-fab="false" 可静默不挂 FAB;加 data-locale="zh" 切中文。工程化项目建议用 npm 包——可摇树、带类型、无副作用。

Self-host the replay service

Recordings upload to a replay service you host. With Docker installed, one command brings up MySQL + the replayer (generates a random password, no manual .env):

curl -fsSL https://raw.githubusercontent.com/chernbo/webTape/main/deploy/install.sh | bash

Then use http://localhost:3100/api/replayer as your serverUrl. The upload endpoint is unauthenticated by design (Web Tape rides inside a host app rather than being a standalone service) — add auth at your gateway if exposing it publicly. See the GitHub repo for details.

自建回放服务

录制数据上传到你自建的回放服务。装了 Docker 后一行命令拉起 MySQL + 回放服务(自动生成随机密码,无需手写 .env):

curl -fsSL https://raw.githubusercontent.com/chernbo/webTape/main/deploy/install.sh | bash

然后把 http://localhost:3100/api/replayer 作为 serverUrl。上传接口默认不鉴权(Web Tape 是寄生在宿主应用内的伴随工具,而非独立服务)——公网暴露请在网关层自行加鉴权。详见 GitHub 仓库

Roadmap

  • 🤖 Deeper AI — structure recordings (action paths + network + console) into an agent-consumable format so an AI agent can auto-reproduce and locate root causes.
  • 📱 Multi-platform — explore recording for H5 / mini-programs, unifying the bug-report experience across web and mobile.
  • 🛡️ Data masking — configurable field/selector masking so it can be used beyond test/staging.

后期计划

  • 🤖 AI 深度探索——把录制(操作路径 + 网络 + 控制台)结构化为 Agent 可消费格式,让 AI Agent 自动复现并定位根因。
  • 📱 多端支持——探索 H5 / 小程序录制,统一 Web 与移动端的 Bug 上报体验。
  • 🛡️ 数据脱敏——可配置的字段/选择器脱敏,让它能走出测试/预发环境。