AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Free Chrome extension for bug reporting and QA. Capture bugs from the side panel and automatically attach screenshots, console and network logs, and environment details. AI drafts a complete report. A
Developer toolsTypeScript · HTMLGitHub @SinhyeokKangOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Open-source Chrome side panel that turns a browser bug into a complete report and files it straight to your issue tracker.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Bug Reporting in One Shot.
Open-source bug reporting from your browser, straight to your tracker.
BugShot is a Chrome side panel that captures what happened — screenshots,
recordings, console and network logs, user actions, environment details, and
even CSS before/after diffs — and turns it into a report for Jira, GitHub,
Linear, Notion, GitLab, Asana, ClickUp, Slack — or a server of your own over a
custom webhook.Read the full README ↗English · 한국어
Bug Reporting in One Shot.
Open-source bug reporting from your browser, straight to your tracker.
BugShot is a Chrome side panel that captures what happened — screenshots, recordings, console and network logs, user actions, environment details, and even CSS before/after diffs — and turns it into a report for Jira, GitHub, Linear, Notion, GitLab, Asana, ClickUp, Slack — or a server of your own over a custom webhook.
No BugShot account. No hosted workspace. Your capture data never passes through a BugShot server. BugShot is free, with no paid tier planned: there is no capture backend to run, no usage to meter, and no hosting bill to recover.
Install from the Chrome Web Store · Read the docs · Privacy
I'm a product designer. When I found a UI bug, I used to inspect the page in DevTools, change the CSS, take screenshots, move over to Jira, and manually build a before/after table for the issue.
That was tedious, so I made a tool that let me finish the whole workflow in one place and send the result to Jira in the format I wanted.
As I used it at work, our QA team asked if it could handle general bug reports too. That added screenshots and recordings, followed by network requests, console logs, and the rest of the evidence needed to reproduce a bug. It became an internal tool for the QA team; I still use it every day and keep improving it.
What started as a way to avoid making CSS diff tables by hand grew into this product in its first three months.
Bug reports can contain some of the most sensitive parts of a production session: customer data in screenshots, tokens in network traffic, internal identifiers in console output, and the contents of form interactions.
I didn't want BugShot to become another server that received and stored that data. These are the core data paths:
Your browser ────────────────────────→ Your tracker, Slack, or webhook server
│
├── when you use AI ────────────→ The LLM endpoint you selected
│
├── OAuth exchange only ────────→ BugShot OAuth proxy
│
└── anonymous usage events ─────→ PostHog (via a proxy domain we own)
Screenshots, recordings, logs, CSS changes, attachments, and report bodies are
assembled in the extension and sent directly to the destination you connected.
For Jira, GitHub, Notion, Asana, ClickUp, and Slack, the BugShot-operated OAuth
proxy relays authorization-code exchanges, and token refreshes for those three
(Jira, GitHub, Asana). Linear and GitLab exchange and refresh tokens directly
via PKCE, with no proxy in the path — five of the eight expire and refresh;
Notion, ClickUp, and Slack issue tokens that don't. (GitHub depends on how its
OAuth app is configured: with token expiration off, its tokens don't expire
either.) Capture data never goes through the
proxy. Anonymous analytics contain no capture or report content. Those events
leave through in.bug-shot.com, a domain we own whose DNS record points
straight at PostHog Cloud — it exists so ad and DNS blockers don't silently
drop the numbers, and no BugShot server sits on that path either.
This has deliberate trade-offs: there is no BugShot account, team workspace, cloud sync, or server-side capture processing. The project is open source so you can inspect that architecture instead of taking the privacy claim on trust.
See every external request BugShot makes.
Requires Chrome 116 or later. Other Chromium browsers that ship the Side Panel API (Edge, Brave, Arc) should work, but Chrome is what's tested. There is no Firefox or Safari build — the whole UI is a Chrome MV3 side panel, and neither browser implements that API.
Cmd/Ctrl+Shift+E, or pick BugShot from the page's right-click menu.Full walkthrough in the Quick Start guide.
Fix the bug visually before you even describe it.
w × h, color and background swatches, font, and radius — the DevTools trip you're not taking. Works on nested and deeply styled elements; elements inside a shadow root resolve to their host (the browser's own hit-testing, not something we special-case), and pseudo-elements are not hit-testable.!important). Changes apply to the live page instantly, so you can dial in the exact fix and see it in place.var() chains and shows the token name (e.g. --color-primary) instead of the raw computed value, so the report speaks your design system's language. When editing a value, you can pick one of the page's tokens instead of typing a raw value.Grab exactly what's on screen and mark it up.
When a still image isn't enough, record the behavior.
Reproduction context, collected for you in the background.
authorization, cookie, CSRF and x-*-token families), secrets in URL queries and fragments, and body keys like token / password / client_secret are masked at capture time — and the masking survives into Copy as cURL.W × H, and capture time are filled into every report. These automatic metadata rows are read-only. You can add custom rows and edit or delete those rows and the API Hosts row.logs.html report with a video-synced timeline: click any console/network/action entry to jump to that exact moment in the recording. It also carries a Report tab (issue body preview + copy as markdown) and per-tab exports (HAR, console/action JSON).All three logs ride along with every capture except element style editing, and the whole logs.html bundle can be toggled off with one switch before you submit (also toggleable on already-saved issues).
Connect via OAuth or a token. Every tracker supports destination selection and attachment upload. GitHub uploads require a signed-in github.com session; GitLab uploads use the PAT or OAuth token from your connected account. Assignee covers every tracker except Notion; label selection is GitHub, Linear and GitLab only. Slack is a messenger rather than a tracker — it sends to a channel or DM instead. A custom webhook sends the report to a server you run, as described in the webhook contract.
Set defaults once in the Integrations tab — destination (project, repo, team, workspace, database, channel) plus assignee, label, issue type (Jira), and status / select properties (Notion) — and every new report comes pre-filled. File to the same destination again and whoever you assigned last wins over the default, so the common case stays one click; switch destination and the default takes over. CC is not a default either: it carries over from your last submission to that same destination, and is dropped when you switch.
| Platform | Auth | Highlights |
|---|---|---|
| Jira | OAuth 3LO / API Token | project metadata, sprint picker, priority, parent epic, linked issue, auto-upload attachments |
| GitHub | OAuth / PAT | repo, labels, assignees, file upload (needs a signed-in github.com session) |
| Linear | OAuth PKCE / API Key | team, project, labels, priority |
| Notion | OAuth / Internal Token | database picker, status & select properties |
| GitLab | OAuth PKCE (gitlab.com only) / PAT | gitlab.com + self-managed instances |
| Asana | OAuth / PAT | workspace, project, assignee |
| ClickUp | OAuth / API Token | workspace → space → list, assignee |
| Slack | OAuth (user token) | channel/DM, @mentions, title + threaded details & files |
| Custom Webhook | URL + optional Bearer secret | multipart (files + cid: references, adds an issue-list row) or JSON template (no media, no history), custom headers, test send |
logs.html reportpnpm install
pnpm dev # dev server (HMR via @crxjs/vite-plugin)
pnpm build # production build → dist/ (runs build:log-viewer first)
pnpm build:log-viewer # log viewer bundle only (build/build:store/build:e2e run it automatically)
pnpm build:store # store upload build (strips manifest key)
pnpm preview # preview the production build
pnpm typecheck # type check only
pnpm test # unit tests (Vitest)
pnpm test:watch # unit tests in watch mode
pnpm build:e2e # e2e-only build → dist-e2e/ (test fixture — never load/upload)
pnpm test:e2e # Playwright e2e suite (run build:e2e first)
pnpm test:coverage # coverage report
pnpm check:prearm # verify the recorder pre-arm chunk stayed a sync IIFE (CI gate)
pnpm sync:agents # regenerate the Codex mirror (AGENTS.md, .agents/skills/)
pnpm sync:agents:check # fail if the mirror drifted (CI gate)
Load the unpacked extension from dist/ at chrome://extensions (developer mode).
Opening the panel page (src/sidepanel/index.html) in a regular tab runs it in tab mode for browser automation agents; their manual is embedded in that page's source (#__BUGSHOT_AGENT__).
The e2e suite lives in e2e/ — see e2e/README.md for coverage and gotchas.
Patches are welcome: CONTRIBUTING.md covers which branch to
target, what CI checks, and what won't be merged.
Don't take the privacy claim on faith — build from source and watch the network tab:
git clone https://github.com/SinhyeokKang/bugshot-2.git
cd bugshot-2
pnpm install
cp .env.example .env.local # optional — see below
pnpm build
Then load dist/ as an unpacked extension. Every key in .env.example is
optional for building: capture and token-based connections work without OAuth configuration.
OAuth connections require their client IDs and, where applicable, the proxy URL.
Slack is OAuth-only, so its connection requires OAuth configuration. Without a
PostHog key, analytics is a no-op and the build makes zero analytics requests.
Open DevTools on the side panel and inspect BugShot’s service worker from
chrome://extensions. GitHub file uploads run in a dedicated github.com tab;
inspect that tab for its upload requests. Compare the requests against the
transmission table — the only
BugShot-operated endpoint in the whole list is the OAuth token-exchange proxy,
which relays the code→token swap and token refreshes and never carries capture data.
| Runtime | Chrome MV3 — Side Panel + Service Worker + Content Script |
| UI | React 18, TypeScript, Tailwind CSS v3, shadcn/ui |
| State | Zustand + chrome.storage (session / local) |
| Build | Vite + @crxjs/vite-plugin |
| Test | Vitest (unit) · Playwright (e2e) |
The decisions that shaped the codebase, and what each one cost:
No backend, by construction. There is no BugShot server in the data path — not "we don't look at it," but nowhere to look. Captures, logs, and report bodies are assembled in the extension and POSTed straight to your tracker with your own credentials. The one BugShot-operated endpoint is a Cloudflare Worker that swaps an OAuth code for a token (and relays refreshes for Jira, GitHub, and Asana), because the client secret can't ship in a public bundle; Linear and GitLab skip even that via PKCE. The cost is real: no cross-device sync, no team workspace, no server-side processing — features that would each require breaking this property, so they don't get built.
Recorders are a synchronous IIFE, and that constrains the build. Console and
network hooks have to be installed at document_start before the page's own
inline scripts run, or the first errors are already gone. That only holds if the
recorder chunk is self-contained — a single external static import turns it into
an async loader and the hooks land too late. So the throttle helper is
deliberately duplicated between the recorder and the side panel rather than
shared, and a sessionStorage flag lets recording start buffering before the
side panel has even connected.
Full-page capture is orchestrated across three contexts. The side panel
drives it, a content-script executor scrolls the page, and each tile goes
through the background worker's captureVisibleTab. Repeated fixed headers
and late-appearing sticky elements are suppressed by visibility between
tiles — including elements that appear or change position mid-capture — then
the original styles and scroll offset are restored.
iframe support is one level deep, and says so. The picker is injected into
all frames; a child registers with the top frame over postMessage and elements
are identified by a selector + frameId composite key. Frames that don't
register — nested two levels deep, or sandboxed — hit an explicit refusal path
with a dialog rather than failing silently. The registration token is a
correlation hint, not an authentication boundary: the child broadcasts it, so a
hostile parent page can read it. That's a documented, accepted limit, not an
oversight.
Styles are resolved against your design system. The CSS pipeline follows
var() chains to report --color-primary instead of a computed RGB triple —
which means fetching cross-origin stylesheets from the background worker at a
page-supplied URL, i.e. an SSRF surface. It runs behind
src/lib/ssrf-guard.ts: http(s) only, with loopback,
private, link-local, CGNAT, multicast, IPv6 ULA, and IPv4-mapped-IPv6 bypasses
all rejected. The caller in
src/background/messages.ts adds the rest:
credentials: "omit", redirect: "manual", a timeout, a content-type check,
and size caps. One residual gap is documented in ssrf-guard.ts itself
rather than papered over: a hostname that resolves to an internal address can't
be caught statically, since fetch re-resolves DNS. The fetched CSS is used
on-device only.
Eight platform integrations behind one adapter seam. Each platform gets its
own auth (OAuth or token), field schema, and upload path, but the report body is
built once and rendered per target — Markdown, Jira ADF, Notion blocks, Slack
mrkdwn. A ninth target, the custom webhook, rides the same seam without an auth
axis at all — the receiving server is the user's, so there is no token to
exchange or refresh. In multipart mode, upload and creation share one request,
so the body carries cid: references instead of URLs. JSON template mode sends
only the rendered JSON, without file uploads or an issue-history entry.
Much of this can't be unit-tested — DOM measurement, pointer drags, canvas, and media APIs need a real browser — so a Playwright suite carries what Vitest can't, and the coverage metric deliberately excludes browser-bound files so the number stays an honest signal.
Full detail lives in docs/ARCHITECTURE.md (Korean).
Every outbound request the extension can make is enumerated here — trackers, Slack, your webhook server, your LLM endpoint, the OAuth proxy, and analytics, plus a few auxiliary requests such as AI key issuance and stylesheet fetches — with the exact data each one carries:
<all_urls>Full policy: English · 한국어 · bug-shot.com
Chrome renders this permission as "Read and change all your data on all
websites" — the most alarming line in the install dialog, and it's required
rather than optional. BugShot did ship the optional version first, requesting
the permission at runtime, and it was dropped because it broke in use:
captureVisibleTab runs on either activeTab or a broad host permission, and
activeTab is revoked when the page navigates to a different origin,
with no way to re-acquire it programmatically. A 30-second replay that relies on
repeated captureVisibleTab calls would stop when the bug you were chasing crossed origins — which is
precisely when you need it.
So the permission is broad. The data path isn't — everything it unlocks happens on your machine, and the transmission table above is the complete list of what leaves it. If you'd rather narrow the grant, Chrome lets you — Extensions → BugShot → Site access — with the understanding that the features which need the breadth will degrade accordingly.
A note on language. This README is kept in sync with its Korean edition, as are the privacy policy and the user guide. The engineering docs under
docs/and the source comments are written in Korean — the project is built and maintained by one person, and that's the language it was thought in. Everything you need to audit the privacy claims above is in English.
MIT © 2026 Sinhyeok Kang