🔒 Nothing is ever uploaded — you have full control over your data. Everything stays on your machine; the entire bug report is a single local file that only travels if you choose to share it. It's a digital-ownership ethos we share with FULU.
An open-source take on Jam.dev: a Chrome extension that captures console logs, network requests, JS errors, screenshots, device/environment info, a full DOM session replay (rrweb), and opt-in local mic narration onto a single correlated timeline, then exports a self-contained HTML bug report — open it offline and watch the session play back.
No backend, no account, no telemetry. Everything stays on your machine (privacy policy).
Curious exactly what OpenJam captures and how it behaves? The feature set documents each feature for transparency — what it does, what to expect, and the test data behind it.
Screenshots are generated, not hand-taken — npm run screenshots drives the real
extension over the deterministic e2e fixture with Playwright
and regenerates docs/screenshots/, so they always match the current code. Icons are
generated too: npm run icons composes the jammable-fruit logo (🍓🫐🍇🍒 — Unicode has no
raspberry or blackberry, blueberries stand in) from
Microsoft Fluent Emoji 3D art (MIT),
fetched once via Iconify and vendored in assets/iconify/.
rrweb must be bundled into the extension — MV3 forbids loading remote code (Chrome docs):
npm install
npm run build # bundles dist/rrweb-recorder.js + generates src/generated/player-assets.jsOpenJam attaches the Chrome DevTools Protocol
(chrome.debugger) to the active tab — the same mechanism DevTools itself uses — and listens to:
| Source | CDP domain | What you get |
|---|---|---|
| Console | Runtime.consoleAPICalled |
log/info/warn/error messages + stack traces |
| Errors | Runtime.exceptionThrown |
uncaught exceptions with stack + source location |
| Network | Network.* |
method, URL, status, headers, payloads, timing, size, response bodies (text, <100 KB) |
| Browser log | Log.entryAdded |
browser-level warnings |
| Screenshots | Page.captureScreenshot |
at start/stop, on every error, and on demand |
| Environment | Runtime.evaluate |
UA, platform, viewport, screen, timezone, memory |
Every event is normalised to a wall-clock timestamp so the report renders one ordered, filterable timeline.
Each report embeds a small <script id="openjam-ai" type="application/json"> manifest
before the full <script id="openjam-data"> blob. Read the manifest first: it carries a
_doc description, a per-kind schema legend, counts, and a failures[] index whose
i fields point into the sorted events[] array in #openjam-data. Orient from the
manifest, then extract only the events you need by index — no need to parse the whole blob.
Add OpenJam from the Chrome Web Store — one click, auto-updates. Works in Chrome and other Chromium browsers.
Prefer to load it yourself, or on a browser without the store? See Install (unpacked) below.
No build needed: download the pre-built zip from the latest release and unzip it somewhere permanent. (Building from a clone works too — see Development below.)
- Open
chrome://extensions(Edge:edge://extensions, Vivaldi:vivaldi://extensions). - Enable Developer mode (top right).
- Click Load unpacked and select the unzipped (or cloned) folder.
- Pin OpenJam from the extensions menu.
- Go to the page with the bug.
- Click the OpenJam icon → Start recording. Chrome shows a "being debugged" banner — that's the CDP attachment; leave it.
- Reproduce the bug. Hit 📸 Capture screenshot at key moments if you want extra frames.
- Click Stop & open report. A new tab opens with the session replay on top and the timeline below.
- Click ⬇ Download self-contained HTML to save a shareable file (replay included).
- Filter by type (console / network / error / log / screenshot).
- Full-text search across titles and payloads.
- Click any row to expand: headers, request/response bodies (pretty-printed JSON), stack traces, full screenshots.
Dev → build → test loop:
git clone https://github.com/SaintPepsi/openjam.git && cd openjam
npm install # pinned deps: rrweb@2.0.1, @rrweb/replay@2.0.1, esbuild
npm run build # see "When to rebuild" below
npm test # bun unit suite (memory behaviors, export safety, issue links)
npm run test:e2e # Playwright end-to-end suite (real extension, headless)Then load the extension: chrome://extensions → Developer mode → Load unpacked →
this folder. After each code change: rebuild (if needed), click the ↻ reload icon on
the OpenJam card, and reload the target page (so the content script re-injects).
When to rebuild (npm run build):
| You changed | Rebuild? | Why |
|---|---|---|
src/rrweb-recorder.js |
Yes | esbuild bundles it (+rrweb) into dist/rrweb-recorder.js |
| rrweb / @rrweb/replay versions | Yes | regenerates the bundle and src/generated/player-assets.js |
background.js, popup.*, viewer.*, renderer.js, report-builder.js |
No | loaded directly by the extension — just reload it |
manifest.json |
No | reload the extension |
dist/ and src/generated/ are build outputs (gitignored) — a fresh clone won't load
until you run npm run build once.
Testing: npm test runs the Bun unit suite in test/ — recorder
buffer drainage, orphaned-recorder stop, session isolation, storage-quota degradation,
export size/escaping bounds, issue-link prefills, the non-recordable-tab guard, and
packaging completeness (packaging.test.js walks the manifest/import graph and fails if
the release zip would omit a file an extension page imports). npm run test:e2e runs the
Playwright end-to-end suite in e2e/
(npx playwright install chromium once): it loads the real unpacked extension headless,
records the deterministic fixture (test/e2e/fixture.html), and asserts console/network/
screenshot rows land on the timeline, the replay plays back to the fixture's final state
(passwords masked), the downloaded export replays fully offline, restricted chrome://
pages fail with a reportable error, and storage keeps only the newest report. Shared
driving helpers live in test/e2e/harness.mjs — the screenshot generator uses the same
ones.
Visual regression: the e2e suite also pins toHaveScreenshot baselines (e.g. the
popup error callout). Text renders differently across OSes, so baselines are generated
and compared in one pinned image (mcr.microsoft.com/playwright:v1.60.0-jammy): CI runs
the whole job in that image, and comparison is skipped on local macOS (ignoreSnapshots
when CI is unset, so npm test won't fail on Linux baselines). Regenerate baselines
with npm run test:snapshots, which runs that image via Docker (your host node_modules
is left untouched).
| File | Role |
|---|---|
manifest.json |
MV3 manifest |
background.js |
Capture engine — CDP attach, event routing, rrweb orchestration, report assembly |
src/rrweb-recorder.js |
Content-script session recorder (bundled to dist/rrweb-recorder.js) |
build.mjs |
esbuild: bundles the recorder, generates src/generated/player-assets.js |
report-builder.js |
Generates the self-contained HTML export (timeline + replay player) |
renderer.js |
Shared timeline renderer (extension page + embedded in exports) |
popup.html / popup.js |
Start/stop/screenshot controls |
viewer.html / viewer.js |
Renders the report and handles file export |
icons/ |
Extension icons, generated by npm run icons |
assets/iconify/ |
Vendored Fluent Emoji SVGs (MIT) the icons are built from |
scripts/ |
Asset generators: screenshots.mjs, icons.mjs (Playwright) |
docs/screenshots/ |
Generated store/README screenshots |
test/ |
Bun test suite |
plans/ |
Verified phase plans + MVP plan; REPLAY_DESIGN.md is the architecture |
While recording, an rrweb recorder (content script, src/rrweb-recorder.js) captures the
DOM and its mutations. The replay plays in the in-extension report page AND in the
exported HTML — scrubbable, offline, no dependencies. Replay uses rrweb defaults
(passwords masked; other inputs visible).
Playback is @rrweb/replay's Replayer
driven by OpenJam's own controller (mountReplay in renderer.js). We don't use
rrweb-player: its 2.x dist builds ship without the code that constructs the Replayer
(verified across the 2.0.0/2.0.1 UMD and ESM artifacts — the player shell mounts but no
replay iframe is ever created), and build.mjs bundles the engine directly instead.
- Console/network history before Start is not captured — recording is forward-only.
- Response bodies are captured only for text-like types under 100 KB (configurable via
BODY_CAPTURE_MAX_BYTESinbackground.js). - Replay events are held in memory uncompressed — keep captures short (minutes, not hours). The manifest requests
unlimitedStorage, so the ~10 MBchrome.storage.localquota doesn't apply; if a save still fails (disk pressure), the report degrades in layers: replay dropped (noted on the timeline), then screenshot pixels. - Only the most recent report is kept in extension storage (quota); download the HTML to keep a capture.
- Canvas/WebGL, video frames, and cross-origin iframes replay imperfectly (DOM replay, not pixels — see
plans/PHASE_3_PLAN.md). - Images may not render in offline replay (rrweb
inlineImagesdefault off); structure and text replay faithfully. - Chromium-only (Chrome, Vivaldi, Edge, Brave), Chrome ≥118 required: from 118 an active
chrome.debuggersession keeps the background service worker alive for the whole recording (SW lifecycle docs); on older versions a long idle recording can be evicted. Firefox/Safari need the injection pivot inplans/PHASE_4_PLAN.md.
PHASE_1_PLAN.md— bounded ring buffer + IndexedDB for long sessions, in-extension playerPHASE_2_PLAN.md— compressed exports (fflate) for large capturesPHASE_3_PLAN.md— hybrid CDP pixel keyframes for canvas/WebGL/cross-originPHASE_4_PLAN.md— Firefox/Safari via injection-based capture- Tab audio capture on the timeline (mic narration shipped in 0.5.0) — requested by first users
When OpenJam itself fails (not the page you're recording — that's the product working), the popup and report viewer show a Report this on GitHub → link that pre-fills an issue with the error, extension version, and browser UA — nothing else. Issues are public: remove any PII (names, emails, internal URLs, tokens) before submitting.
If OpenJam saves you a bug-report headache, you can sponsor @SaintPepsi on GitHub ☕
GPL-3.0-or-later — free to use, modify, and redistribute; copies and
derivatives must remain open source under the same terms. The vendored
Fluent Emoji SVGs in assets/iconify/
are MIT (GPL-compatible).


