Screen recording + web telemetry tool. Upload via browser extension, replay via web viewer.
Extension (MV3) ──upload──► Worker (CF) ◄──fetch── Viewer (HTML/JS/CSS)
│
┌────┴────┐
│ R2 D1 │
└─────────┘
| Component | Role |
|---|---|
| Browser Extension | Captures screen + telemetry, uploads to backend |
| Cloudflare Worker | API + serves viewer/dashboard HTML |
Cloudflare R2 (BUCKET) |
Video .webm + telemetry .json blobs |
Cloudflare D1 (DB) |
Session metadata (SQLite) |
| Web Viewer | Plays video synced with telemetry (embedded in Worker) |
- Vanilla JS only — no frameworks, no CSS libraries, no new npm packages
- Only dependency:
jose(JWT). Already inworker/package.json - Worker type: ES module (
"type": "module"in package.json) - All viewer/dashboard code is embedded as template literals in
worker/src/index.js(VIEWER_CSS, VIEWER_JS, VIEWER_HTML, DASHBOARD_CSS, DASHBOARD_JS, DASHBOARD_HTML, LANDING_HTML) - No comments in code unless requested
- Parameterized SQL — always use
?placeholders for D1 queries - Error responses — always
{ "error": "message" }format - Single user system — one creator, infinite anonymous viewers
extension/
├── manifest.json # MV3 — permissions: tabCapture, storage, activeTab, offscreen; host: https://*/*
├── popup/
│ ├── popup.html # UI + inline <style> (NO separate popup.css)
│ └── popup.js # Clipboard, auth status display
├── background/
│ └── service-worker.js # Orchestrator: capture lifecycle, telemetry routing, offscreen mgmt
├── offscreen/
│ ├── offscreen.html # Offscreen document host
│ └── offscreen.js # MediaRecorder + upload + telemetry buffer (cannot run in SW)
├── content/
│ ├── injector.js # Content script → relays page-script events to SW
│ ├── page-script.js # MAIN world: wraps console/fetch/XHR, captures events (isWrapped guard)
│ └── widget.js # Isolated world: draggable floating widget (pause/stop), closed Shadow DOM, document_idle
└── icons/ # icon16.svg, icon48.svg, icon128.svg
worker/
├── wrangler.toml # R2 binding: BUCKET, D1 binding: DB
├── package.json # jose@^6.2.1, wrangler@^4.71.0, type: module
├── migrations/
│ ├── 0001_create_sessions.sql
│ └── 0002_add_duration.sql
└── src/
├── index.js # Router + handlers + embedded viewer/dashboard HTML/CSS/JS
├── auth.js # JWT sign/verify via jose (HS256, 30d expiry)
└── utils.js # generateId() → "bugreplay_" + 8 random chars
| Location | Variable | Value/Purpose |
|---|---|---|
offscreen.js:1 |
MAX_TELEMETRY_EVENTS |
10000 — memory cap (environment events preserved on eviction) |
service-worker.js:1 |
BACKEND_URL |
Loaded from gitignored config.local.js via importScripts; placeholder fallback https://YOUR-WORKER.workers.dev |
service-worker.js:5 |
recordingTabId |
Tab ID of the recorded tab (filters telemetry, routes commands) |
service-worker.js:6 |
recordingStartTime |
Recording epoch; passed to page-script for relative event timestamps |
service-worker.js:8 |
isPaused |
Pause flag; mirrored to chrome.storage.local key isPaused; rehydrated on SW restart |
index.js:3-9 |
CORS_HEADERS |
Allow-Origin: * (all routes) |
index.js upload |
MAX_VIDEO_SIZE |
50 MB |
index.js upload |
MAX_TELEMETRY_SIZE |
10 MB |
| JWT storage | key | 'jwtToken' in chrome.storage.local |
| Method | Route | Auth | Handler | Notes |
|---|---|---|---|---|
| POST | /api/auth/login |
Password body | handleLogin |
Returns JWT |
| POST | /api/upload |
JWT Bearer | withAuth(handleUpload) |
FormData: video + telemetry Blob |
| GET | /api/session/:id |
None | handleRetrieve |
Returns metadata + R2 URLs |
| GET | /api/dashboard/sessions |
JWT | withAuth(handleListSessions) |
Paginated (?page=N) |
| DELETE | /api/dashboard/sessions/:id |
JWT | withAuth(handleDeleteSession) |
R2 + D1 cleanup |
| GET | /bugreplay_{id}/video.webm |
None | handleR2Proxy |
Streams from R2 |
| GET | /bugreplay_{id}/telemetry.json |
None | handleR2Proxy |
Returns from R2 |
| GET | / |
None | LANDING_HTML |
Landing page |
| GET | /dashboard |
None | DASHBOARD_HTML |
Dashboard SPA |
| GET | /{sessionId} |
None | VIEWER_HTML |
Viewer SPA (regex: /^[a-zA-Z0-9_-]+$/) |
Router: Sequential if-chain in index.js fetch handler. No framework.
Auth middleware: withAuth(handler) — verifies Bearer token via authenticateRequest, passes auth.payload as 3rd arg.
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
video_key TEXT NOT NULL,
telem_key TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
browser TEXT,
os TEXT,
viewport TEXT,
url TEXT
);
CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON sessions(created_at DESC);R2 keys: {id}/video.webm, {id}/telemetry.json
Every event: { type, timestamp, data }
| type | data fields |
|---|---|
console |
level (log/warn/error/info), args |
network |
method, url, requestHeaders, requestBody, status, responseHeaders, responseBody, duration |
click |
target (CSS selector), x, y |
navigation |
from, to |
environment |
userAgent, viewportWidth, viewportHeight, url |
- Popup sends
startRecording/stopRecordingmessage to service worker - SW captures
recordingTabIdfrom active tab; for tab mode callschrome.tabCapture.getMediaStreamId(), for screen mode offscreen will callgetDisplayMedia() - SW creates/ensures offscreen document, forwards start message with streamId + JWT + backendUrl
- Offscreen:
MediaRecorder(VP9, VP8 fallback),timeslice: 5000; buffers chunks indata[]and telemetry intelemetry[] - Content scripts run independently:
injector.jsrelayswindow.postMessagefrompage-script.js(MAIN world) to SW, which forwards to offscreen (filtered byrecordingTabId); page-script wraps console/fetch/XHR + captures clicks/nav/environment - On stop: SW sends
stop-recordingto offscreen; offscreen flushesMediaRecorder, builds FormData fromdata[]+telemetry[], uploads. SW concurrently sendsstopTelemetryto the recorded tab. - Upload: POST
/api/uploadwith JWT Bearer → returns{ id, viewerUrl } - Offscreen notifies SW (
recording-complete/recording-error); SW persists result tochrome.storage.localaspendingResultand closes the offscreen document; popup readspendingResulton open if it was closed during upload → copies URL vianavigator.clipboard.writeText()
- On
startTelemetryreaching the recorded tab,widget.js(isolated world,document_idle) shows a draggable Shadow-DOM widget with Pause + Stop buttons. It queriesgetRecordingStateon load to show only on the recorded tab (recordingThisTab) and to recover the correct paused state. - Pause click → SW
pauseRecording→ SW sets/persistsisPaused, sendspause-recordingto offscreen andpauseTelemetryto the recorded tab. - Offscreen calls
MediaRecorder.pause()and gates its telemetry buffer with!paused(events arriving during pause are dropped). injector.jsrelayspauseTelemetry→bugreplay-pause;page-script.jsflipsisRecording=false. This is provably sufficient to halt all telemetry — every emission funnels through the singlesendTelemetrygate.- Resume is the mirror. Stop while paused is valid (
MediaRecorder.stop()works from the paused state). - Timestamp rebase lives in the offscreen (
rebaseTelemetry), NOT page-script: page-script is re-injected fresh on every navigation (its state resets), so a local accumulator would lose state. On stop, each event's timestamp is reduced by the paused duration occurring strictly before the event's wall time;durationMssubtracts total paused time. This keeps the telemetry timeline aligned with the paused video. startTelemetrycarries apausedflag so a widget injected mid-pause (tab navigation) shows the correct state immediately.
- Fetch
/api/session/{id}→ get videoUrl + telemetryUrl - Sort telemetry by timestamp (required for binary search)
- On
timeupdate(~4x/sec): binary search for events ≤ currentTime - Incremental DOM render — full rebuild only on seek backward or tab switch
- Auto-scroll only when user is at bottom (<60px from end)
- Tabs: Console | Network | Events
Creator ──(password)──► POST /api/auth/login ──► JWT (HS256, 30d)
Creator ──(JWT)────────► protected endpoints
Viewer ──(no auth)────► public endpoints
JWT: jose library, issuer 'bugreplay-app', audience 'bugreplay-viewer'
Secrets set via wrangler secret put: AUTH_PASSWORD, JWT_SECRET
| Issue | Why |
|---|---|
| Path traversal in route matching | Private repo, no public attack surface |
| Timing attack on password compare | Requires network-level attacker |
Math.random() ID generation |
Negligible collision risk for single user |
CORS * on all endpoints |
Localhost/dev only |
Hardcoded BACKEND_URL in extension |
Single-deployment use |
| XHR request headers not captured | Incomplete but functional |
| Response body capture (10KB limit) | Acceptable for single-user use |
wrangler r2 bucket create bugreplay-storage
wrangler d1 create bugreplay-db
# Update database_id in wrangler.toml
echo "your_password" | wrangler secret put AUTH_PASSWORD
openssl rand -base64 32 | wrangler secret put JWT_SECRET
wrangler d1 migrations apply bugreplay-db --remote
wrangler deploy- No comments in code
- Vanilla JS — no React, no Tailwind, no CSS libs
- Cloudflare Workers — ES module export
{ fetch(request, env) } - Template literals — all HTML/CSS/JS embedded in
index.js - MV3 offscreen pattern —
MediaRecordercannot run in the service worker; offscreen document owns capture + upload + telemetry buffer for the full recording creatingOffscreenmemo — guards against doublecreateDocumentraces- Binary search — O(log n) telemetry sync, handles 10k+ events
- Content script bridge — main world (page APIs) ↔ isolated world (messaging)
- SVG-safe selectors — use
getAttribute('class')notclassName - Upload reliability — 3 attempts, linear backoff (1s, 2s); R2 rollback on D1 failure
escapeHtml()— always sanitize beforeinnerHTMLin viewerisWrappedguard — page-script wrappers installed once per page load (re-injected on tab navigation viachrome.tabs.onUpdated)sendToRecordingTab— routes commands to the recorded tab by ID (not active tab)- Client-side duration — wall-clock
Date.now() - recordingStartTimecomputed in offscreen, minus any paused time; sent via FormDatadurationfield; viewer falls back tovideo.seekable.end()if absent pendingResultstorage — upload result survives popup closure viachrome.storage.localisPausedpersistence — pause flag mirrored tochrome.storage.localso SW restarts and widget re-injection (tab navigation) recover the correct state- Pause rebase is in offscreen —
rebaseTelemetry()subtracts pre-event paused intervals on stop; centralized offscreen because page-script resets on every navigation - Widget click exclusion —
page-script.jsclick capture skipsclosest('[data-bugreplay-widget]')(Shadow-DOM clicks retarget to the host)