Imported from Zimquadery/BugReplay (
extension/AGENTS.md). Install upstream withnpx skills add Zimquadery/BugReplay --skill extension. Copyright stays with the author.
Extension Development Notes
Message Communication Patterns
Content Script ↔ Service Worker
- Content scripts use
chrome.runtime.sendMessage()to send messages to service worker - Service worker listens with
chrome.runtime.onMessage.addListener()and routes bymessage.actionormessage.type
Page Script ↔ Content Script
- Page scripts (injected into main world) use
window.postMessage()to communicate - Content scripts use
window.addEventListener('message')to receive and relay to service worker
Message Schema
// Action messages (popup/widget → service worker)
{ action: 'startRecording', mode: 'tab|screen' }
{ action: 'stopRecording' }
{ action: 'pauseRecording' }
{ action: 'resumeRecording' }
{ action: 'getRecordingState' } // → { recording, recordingThisTab, paused }
// SW → offscreen (target: 'offscreen')
{ target: 'offscreen', type: 'start-recording', mode, streamId, jwt, backendUrl, recordingTabId, recordingStartTime }
{ target: 'offscreen', type: 'stop-recording' }
{ target: 'offscreen', type: 'pause-recording' }
{ target: 'offscreen', type: 'resume-recording' }
// SW → recorded tab (via sendToRecordingTab / chrome.tabs.sendMessage)
{ action: 'startTelemetry', startTime, paused } // re-sent on tab navigation; paused flag recovers mid-pause injection
{ action: 'stopTelemetry' }
{ action: 'pauseTelemetry' }
{ action: 'resumeTelemetry' }
// Page-script ↔ injector (window.postMessage)
{ type: 'bugreplay-start', startTime } { type: 'bugreplay-stop' }
{ type: 'bugreplay-pause' } { type: 'bugreplay-resume' }
// Telemetry messages (content → service worker)
{ type: 'telemetry', data: { type, timestamp, data } }
// Notification messages (service worker → popup)
{ action: 'uploadComplete', viewerUrl: '...' }
{ action: 'uploadError', error: '...' }
Recording Flow
- User clicks "Start" in popup → sends
startRecordingaction - Service worker captures
recordingTabIdfrom active tab; for tab mode callschrome.tabCapture.getMediaStreamId(), for screen mode offscreen will callgetDisplayMedia() - Service worker ensures the offscreen document exists, then forwards start message (streamId + JWT + backendUrl + recordingTabId) to it
- Offscreen creates
MediaRecorder(VP9, VP8 fallback) on the stream, buffers chunks indata[]and telemetry intelemetry[]; service worker concurrently sendsstartTelemetryto the recorded tab - Page script wraps APIs and captures telemetry, posts back to content script
- Content script relays to service worker, which forwards to offscreen (filtered by
recordingTabId) - User clicks "Stop" → service worker sends
stop-recordingto offscreen; offscreen flushesMediaRecorder, builds FormData fromdata[]+telemetry[], uploads to backend - Offscreen notifies SW (
recording-complete/recording-error); SW persists result tochrome.storage.localaspendingResultand closes the offscreen document - Popup reads
pendingResulton open if it was closed during upload → URL copied to clipboard
Pause/Resume Flow (floating widget)
widget.js(isolated world,document_idle) shows a draggable Shadow-DOM widget (Pause + Stop) on the recorded tab only. On load it queriesgetRecordingStateand shows iffresponse.recordingThisTab.- Pause click → SW
pauseRecording(idempotent): sets/persistsisPaused, sendspause-recordingto offscreen andpauseTelemetryto the recorded tab. - Offscreen:
MediaRecorder.pause()+ gates telemetry buffer with!paused. Injector relayspauseTelemetry→bugreplay-pause; page-script flipsisRecording=false. All telemetry funnels through onesendTelemetrygate, so this flip provably halts emission. - Resume is the mirror. Stop while paused is valid (
MediaRecorder.stop()works from paused state). - Timestamp rebase lives in offscreen (
rebaseTelemetry), not page-script — page-script resets on every navigation 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. startTelemetrycarries apausedflag so a widget re-injected mid-pause (tab navigation) shows the correct state immediately.
Important Gotchas
Screen Capture
- Tab mode: service worker calls
chrome.tabCapture.getMediaStreamId({ targetTabId }), offscreen consumes the streamId viagetUserMedia({ chromeMediaSource: 'tab' }) - Screen mode: offscreen calls
navigator.mediaDevices.getDisplayMedia({ video: true }) - User can cancel the screen-capture prompt — offscreen reports
recording-errorwith aNotAllowedError-aware message
MV3 Offscreen Document
MediaRecordercannot run in the service worker; the offscreen document owns capture + upload + telemetry buffer for the full recordingcreatingOffscreenmemo guards against doublecreateDocumentraces- Reasons declared:
['USER_MEDIA', 'DISPLAY_MEDIA']
Stream Cleanup
- Always stop both MediaRecorder AND all tracks in the stream
- Use helper function to avoid duplication
Tab Communication
recordingTabIdtracks which tab is being recorded (set at start, cleared on error)sendToRecordingTab(message)sends to the recorded tab by ID (not active tab)chrome.tabs.queryused only once at recording start to capture the tab ID
Telemetry Timestamps
- All telemetry timestamps are relative to
recordingStartTime(ms since recording start) - Page script computes
Date.now() - recordingStartTimeitself;recordingStartTimeis delivered via thestartTelemetrymessage (re-sent on tab navigation viachrome.tabs.onUpdated) - On pause, page-script only flips
isRecording=false— no timestamp math there. The offscreen rebases timestamps on stop (rebaseTelemetry) since it persists across navigations and owns the pause-interval history.
Pause Behavior
isPausedis mirrored tochrome.storage.localso SW restarts and widget re-injection recover the correct state.- Pause is widget-only; the popup is unaffected and still shows "Recording".
getRecordingStateresponse is additive ({ recording, recordingThisTab, paused }) so the popup's existingresponse.recordingread keeps working. - Widget clicks must not pollute telemetry — page-script's click capture skips
closest('[data-bugreplay-widget]')(Shadow-DOM clicks retarget to the host). - The widget is a DOM element and is therefore captured into the video recording (no DOM-level capture exclusion exists); accepted tradeoff, user drags it to a corner.
History API Wrapping
pushStateandreplaceStatehave identical logic patterns- Extract to
wrapHistoryMethod()helper to follow DRY principle
Code Conventions
- Use vanilla JS - no frameworks
- Message listeners use a single listener with routing logic (not multiple listeners)
- Helper functions for repeated operations (tab query, stream cleanup, etc.)
- Telemetry events follow schema:
{ type, timestamp, data }