ghostbug
Zero-config bug context collector for the browser.
Drop it in, forget about it. When something breaks, you already have everything you need.
Website ·
npm ·
GitHub
Why ghostbug?
Testers spend 90% of their time manually collecting bug context — error messages, console logs, network failures, steps to reproduce. ghostbug eliminates that by silently capturing everything in the background.
| Setup | Account, API keys, config | ghostbug.init() |
| Server needed | Yes (paid SaaS) | No — 100% client-side |
| Bundle size | 50–200KB+ | ~7KB gzipped |
| Dependencies | Multiple | Zero |
| Data privacy | Sent to their servers | Stays in the browser |
| Output | Dashboard (another tab) | JSON / Markdown for GitHub Issues |
| Pricing | Free tier / Paid | Free forever |
Install
npm install ghostbug
yarn add ghostbug
pnpm add ghostbug
Quick Start
import ghostbug from "ghostbug";
ghostbug.init();
That's it. ghostbug now silently auto-captures:
| Errors | window.onerror + unhandled promise rejections with stack traces |
| Console | console.error() and console.warn() with full arguments |
| Network | Failed fetch and XMLHttpRequest (4xx/5xx) with timing |
| Clicks | Last 20 user clicks with element selectors and positions |
| Interactions | Form input, scroll, and resize events |
| Performance | Long Tasks, FCP, LCP, layout shifts via PerformanceObserver |
| Memory | Heap usage sampling, high usage (>90%) and rapid growth (>50%) alerts |
| Context | URL, browser, viewport, device pixel ratio, referrer |
API
ghostbug.init(options?)
Initialize ghostbug. Call once when your app loads.
ghostbug.init({
widget: true,
widget: {
position: "bottom-right",
collapsed: true,
zIndex: 2147483647,
},
collectors: {
errors: true,
console: true,
network: true,
clicks: true,
interactions: true,
performance: true,
memory: true,
},
maxReports: 50,
maxBreadcrumbs: 20,
maxClicks: 20,
rateLimit: { maxEvents: 10, windowMs: 1000 },
beforeReport: (report) => {
return report;
},
debug: false,
});
ghostbug.getReports()
Returns all captured bug reports as an array.
const reports = ghostbug.getReports();
ghostbug.toMarkdown()
Returns a GitHub/Jira-ready markdown string of all captured bugs.
const md = ghostbug.toMarkdown();
Example markdown output
## 1. TypeError: Cannot read property 'id' of undefined
- **Type:** error
- **Time:** 2026-02-23T10:21:33.000Z
- **Occurrences:** 3
- **URL:** https://myapp.com/dashboard
- **Browser:** Chrome 122
### Stack Trace
TypeError: Cannot read property 'id' of undefined
at UserProfile (app.js:42:12)
### Breadcrumbs
| Time | Category | Event |
|------|----------|-------|
| 10:21:30 | click | Clicked button "Save" |
| 10:21:31 | network | POST /api/user -> 500 |
| 10:21:33 | error | TypeError: Cannot read... |
ghostbug.download(filename?)
Downloads all reports as a JSON file.
ghostbug.download();
ghostbug.download("my-bugs.json");
ghostbug.onBug(callback)
Subscribe to bugs in real-time. Returns an unsubscribe function.
const unsubscribe = ghostbug.onBug((report) => {
fetch("/api/slack-webhook", {
method: "POST",
body: JSON.stringify({ text: report.payload.message }),
});
});
unsubscribe();
ghostbug.setUser(user)
Attach user context to every report.
ghostbug.setUser({ id: "user-42", plan: "pro", email: "dev@example.com" });
ghostbug.setTags(tags)
Add custom tags to every report. Merges with existing tags.
ghostbug.setTags({ environment: "staging", version: "2.1.0" });
ghostbug.destroy()
Teardown everything — removes all listeners, restores patched APIs, unmounts widget.
ghostbug.destroy();
Framework Integration
React / Next.js
"use client";
import { useEffect } from "react";
import ghostbug from "ghostbug";
export default function GhostbugProvider({ children }) {
useEffect(() => {
ghostbug.init({ widget: true });
return () => ghostbug.destroy();
}, []);
return <>{children}</>;
}
import GhostbugProvider from "./components/GhostbugProvider";
export default function RootLayout({ children }) {
return (
<html>
<body>
<GhostbugProvider>{children}</GhostbugProvider>
</body>
</html>
);
}
Vue
import ghostbug from "ghostbug";
ghostbug.init({ widget: true });
Svelte
<!-- +layout.svelte -->
<script>
import { onMount, onDestroy } from "svelte";
import ghostbug from "ghostbug";
onMount(() => ghostbug.init({ widget: true }));
onDestroy(() => ghostbug.destroy());
</script>
<slot />
Plain HTML (CDN)
<script src="https://unpkg.com/ghostbug/dist/index.iife.js"></script>
<script>
ghostbug.default.init({ widget: true });
</script>
Widget
Enable the floating widget for testers:
ghostbug.init({ widget: true });
The widget:
- Shows a live bug count badge
- Click to expand and see all captured bugs
- Copy MD — copies markdown to clipboard
- Export — downloads JSON file
- Uses Shadow DOM — styles never leak into your app
- Fully isolated — your CSS won't break it
Bug Report Structure
Each captured bug contains:
{
id: string;
fingerprint: string;
type: "error" | "unhandled-rejection" | "console" | "network" | "performance" | "memory";
timestamp: string;
count: number;
payload: { ... };
breadcrumbs: [
{ timestamp, category, message, data }
];
context: {
url: string;
referrer: string;
userAgent: string;
language: string;
viewport: { width, height };
screen: { width, height };
devicePixelRatio: number;
memory?: { usedJSHeapSize, totalJSHeapSize };
user?: { ... };
tags?: { ... };
};
}
How It Works
| Error capture | Patches window.onerror and listens for unhandledrejection |
| Console capture | Monkey-patches console.error / console.warn (always calls originals) |
| Network capture | Patches fetch and XMLHttpRequest (only captures 4xx/5xx, never alters responses) |
| Click tracking | Uses capture-phase click listener (catches clicks even with stopPropagation) |
| Interactions | Listens for input, scroll, resize events as breadcrumbs |
| Performance | Uses PerformanceObserver for long-task, paint, and layout-shift entries |
| Memory | Samples performance.memory every 10s, flags high usage and rapid growth |
| Deduplication | Identical errors increment a counter instead of creating duplicate reports |
| Rate limiting | Token-bucket algorithm prevents floods from error loops |
| Ring buffer | Fixed-capacity storage prevents memory leaks in long-running apps |
| Safe teardown | destroy() restores all original APIs cleanly |
Development
npm install
npm run build
npm test
npm run test:watch
npm run typecheck
npm run lint
License
MIT — do whatever you want.