glitchgrab
v1.58.1
Published
Turn production errors and your users' bug reports into structured GitHub issues. Drop-in SDK for Next.js apps.
Readme
glitchgrab
Turn production errors and your users' bug reports into structured GitHub issues. Drop-in SDK for Next.js apps.
This page covers the SDK. Step-by-step guides for everything else — the Chrome extension, QA testers, call recording, the AI report assistant, MCP for Claude — are at glitchgrab.dev/guides.
How do I install Glitchgrab?
npm install glitchgrab
# or
bun add glitchgrabHow do I get a token?
- Sign in at glitchgrab.dev/login with GitHub.
- Connect your GitHub org and install the Glitchgrab GitHub App on the repo that should receive issues.
- Open API Tokens, pick the repo and create a token. It starts with
gg_. - Put it in your environment as
NEXT_PUBLIC_GLITCHGRAB_TOKEN.
With screenshots: Connect your GitHub org and Create an API token.
How do I get started?
Wrap your app with GlitchgrabProvider:
// app/layout.tsx
import { GlitchgrabProvider } from "glitchgrab";
export default function RootLayout({ children }) {
return (
<GlitchgrabProvider token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!}>
{children}
</GlitchgrabProvider>
);
}How does user session tracking work?
Pass a session prop so bug reports include the reporter's identity. This lets you trace which user reported each bug.
import { GlitchgrabProvider, type GlitchgrabSession } from "glitchgrab";
import { useSession } from "next-auth/react"; // or your auth library
function Providers({ children }) {
const { data: authSession } = useSession();
// Map your auth session to GlitchgrabSession
const session: GlitchgrabSession | null = authSession?.user
? {
userId: authSession.user.id, // required - your DB primary key
name: authSession.user.name, // required - display name
email: authSession.user.email, // optional
phone: authSession.user.phone, // optional
}
: null;
return (
<GlitchgrabProvider
token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!}
session={session}
>
{children}
</GlitchgrabProvider>
);
}GlitchgrabSession type
interface GlitchgrabSession {
userId: string; // required - primary key from your database
name: string; // required - reporter's display name
email?: string | null; // optional
phone?: string | null; // optional
signature?: string | null; // optional - signSession() from "glitchgrab/server"
[key: string]: unknown; // any extra fields
}The userId is stored with every report. Use it to look up which user reported a bug in your own database.
How do I protect my token?
Your gg_ token ships in your app's JavaScript, so anyone can copy it. That is by design — it can only file reports into one repo — but the AI features (the assistant, AI enhance, voice input, "already reported?") cost money on every call. Three things stop a copied token being abused:
- Rate limits. Every token route is limited per token and per IP address. Nothing to configure.
- Allowed domains. On the Tokens page, list the sites allowed to use a token from a browser (
https://app.example.com— subdomains are included). Requests from any other website are refused. Empty means any site. A server-side script can fake theOriginheader, so this stops the token being pasted into another website, not a determined attacker. Report filing fromglitchgrab/server, MCP and CI is never affected. - Signed users. On the repo card, generate a signing secret and set Signed users to Warn or Enforce. Your server signs the logged-in user's id; the SDK sends that signature with its AI requests.
// app/layout.tsx — a server component
import { signSession } from "glitchgrab/server";
import { auth } from "@/lib/auth"; // your auth library
import { Providers } from "./providers";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const user = (await auth())?.user;
const session = user
? {
userId: user.id,
name: user.name ?? "User",
email: user.email,
// Reads GLITCHGRAB_SIGNING_SECRET. Server env only — never NEXT_PUBLIC_.
signature: signSession({ userId: user.id }),
}
: null;
return <Providers session={session}>{children}</Providers>;
}
// app/providers.tsx — "use client"
// <GlitchgrabProvider token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!} session={session}>| Mode | Signed user | Unsigned or anonymous caller | |------|-------------|------------------------------| | Off | everything works | everything works | | Warn | everything works | everything works; the SDK logs a warning in development | | Enforce | everything works | AI features are hidden — the plain report form still files |
- A signature lasts 24 hours by default (
signSession({ userId, ttlSeconds })). Build it per request so it refreshes. - Rotating the secret breaks signatures already issued until your pages render again with the new one.
signSessionthrows when the secret oruserIdis missing — it runs on your server, where that is a deploy mistake worth seeing. The rest of the SDK never throws.
What data does Glitchgrab collect about reporters?
Besides what the reporter types and the session you pass, every report records the IP address it came from, an approximate location (city, region, country) and the network (ISP / ASN). Only you, the repo owner, can see it in Glitchgrab — it is never written into the GitHub issue. The raw IP is deleted after 90 days; a one-way hash, the location and the network are kept so repeat reports from one address can still be grouped.
Reports sent from glitchgrab/server record your server's address, not an end user's.
If your users are in India or the EU, mention this in your privacy policy (DPDP Act / GDPR treat an IP address as personal data).
How do I add a report button?
Default floating button
import { ReportButton } from "glitchgrab";
// Floating button at bottom-right (default)
<ReportButton position="bottom-right" label="Report Bug" />Custom trigger (headless)
Use the render prop to bring your own button UI:
import { ReportButton } from "glitchgrab";
<ReportButton>
{({ onClick, capturing }) => (
<button onClick={onClick} disabled={capturing}>
{capturing ? "Capturing..." : "Report a Bug"}
</button>
)}
</ReportButton>The modal handles screenshot capture, preview, upload, retake, and submission. Your custom button just triggers it.
How do I report bugs programmatically?
Use the useGlitchgrab hook to report bugs from code:
import { useGlitchgrab } from "glitchgrab";
function MyComponent() {
const { reportBug, report, addBreadcrumb, openReportDialog } = useGlitchgrab();
// Report a bug silently (no UI)
await reportBug("Button not working on mobile");
// Report with a specific type
await report("FEATURE_REQUEST", "Add dark mode support");
// Open the Report Bug modal (captures screenshot + shows dialog)
openReportDialog();
// Open with pre-filled description
openReportDialog({ description: "Error on /settings: Something went wrong" });
// Add custom breadcrumbs for debugging context
addBreadcrumb("User clicked checkout", { cartSize: "3" });
}Open report dialog on bad feedback
function FeedbackWidget() {
const { openReportDialog } = useGlitchgrab();
return (
<div>
<button onClick={() => alert("Thanks!")}>Good</button>
<button onClick={() => openReportDialog()}>Bad — report a bug</button>
</div>
);
}Note: openReportDialog() requires a <ReportButton> to be mounted somewhere in the component tree. It triggers the same modal with screenshot capture.
How do I collect feedback about my app?
Reports are for bugs. Feedback is for how your users feel about your app — a 1–5 star rating with an optional message. Glitchgrab stores it, so you don't write a table, a route, or a migration. Feedback never becomes a GitHub issue.
Drop-in button
import { FeedbackButton } from "glitchgrab";
<FeedbackButton /> // floating, bottom-left
<FeedbackButton position="bottom-right" label="Rate us" />
// Your own trigger
<FeedbackButton>
{({ onClick }) => <button onClick={onClick}>How are we doing?</button>}
</FeedbackButton>The dialog (stars + message) ships inside GlitchgrabProvider — the button is only the trigger. Open it from anywhere with openFeedbackDialog().
Your own UI
function RatingRow() {
const { sendFeedback } = useGlitchgrab();
return [1, 2, 3, 4, 5].map((stars) => (
<button key={stars} onClick={() => sendFeedback(stars, "Loved the new export flow")}>
{stars}★
</button>
));
}sendFeedback(rating, message?, metadata?) never throws — it returns null on failure. The reporter is taken from the session prop on GlitchgrabProvider, so pass a session if you want to know who rated you.
Reading it back
Every entry shows up on your Glitchgrab Feedback page, where you press publish on the ones you want to reuse. Published entries are the only ones returned with approvedOnly — so a testimonials wall can never leak an unvetted complaint:
import { useGlitchgrabFeedback } from "glitchgrab";
function Testimonials() {
const { feedback, isLoading } = useGlitchgrabFeedback({
token: process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!,
approvedOnly: true,
minRating: 4,
});
if (isLoading) return null;
return feedback.map((f) => (
<blockquote key={f.id}>
{f.message} — {f.reporterName} ({f.rating}★)
</blockquote>
));
}Pass userId instead to show one user their own past ratings. fetchGlitchgrabFeedback(...) is the standalone fetcher for TanStack Query. Neither response includes email or phone, so both are safe to render on a public page.
REST
# Submit
curl -X POST https://glitchgrab.dev/api/v1/sdk/feedback \
-H "Authorization: Bearer gg_xxxxx" \
-H "Content-Type: application/json" \
-d '{"rating":5,"message":"Fast and simple","metadata":{"sessionUserId":"user_123","sessionUserName":"Asha"}}'
# Read published entries
curl "https://glitchgrab.dev/api/v1/sdk/feedback?approved=true&minRating=4&limit=20" \
-H "Authorization: Bearer gg_xxxxx"The repo is always derived from the token — there is no repoId to pass. Rate limit: 30 submissions per token per hour.
How do I save a lead without filing an issue?
A demo form or contact page asks a prospect for their details. Those are a lead, not a bug — submitLead() stores them in Glitchgrab and never creates a GitHub issue:
const { submitLead } = useGlitchgrab();
const result = await submitLead({
name: "Asha Mehta",
phone: "+91 98765 43210",
source: "demo", // which form — filter on it later
email: "[email protected]", // optional
company: "Mehta & Co", // optional
address: "Pune", // optional
metadata: { practice: "CA", language: "Hindi" }, // optional, flat values
});
// { success: true, id: "…", created: true } — or null on failure. Never throws.One lead per phone number per project: the same visitor submitting twice updates their lead (created: false) instead of adding a second row. Leads show under Calls → Leads in Glitchgrab, next to any demo the same phone or email went on to book. The owner can switch on a WhatsApp alert for each new lead there.
Read them in your own admin with the project's ggc_ server key — the same key as listBookings():
import { listLeads } from "glitchgrab/server";
const leads = await listLeads({ source: "demo", since: "2026-09-01" }); // GLITCHGRAB_CALLS_KEY=ggc_…What keyboard shortcuts are available?
Once GlitchgrabProvider is mounted, these shortcuts work globally:
| Shortcut | Action |
|----------|--------|
| Cmd+Shift+G / Ctrl+Shift+G | Open the report dialog |
| Cmd+V / Ctrl+V (dialog open) | Paste a screenshot from clipboard |
| Escape | Close the dialog |
No configuration needed — shortcuts are active as long as the provider is in the tree.
Showing the shortcut in your own UI
The report dialog shows the shortcut on its first step. To advertise it elsewhere — a support menu, a sidebar hint — read shortcutLabel instead of hardcoding the string. It resolves to ⌘⇧G on Mac and Ctrl+Shift+G everywhere else, and stays in sync with the handler:
function SupportHint() {
const { shortcutLabel } = useGlitchgrab();
return <p>Found a bug? Press {shortcutLabel} anywhere to report it.</p>;
}It is SSR-safe: it renders Ctrl+Shift+G on the server and corrects to the platform label after mount.
How do I fetch reports by user?
React hook (recommended)
import { useGlitchgrabReports } from "glitchgrab";
function MyReports() {
const { reports, isLoading, error, refetch } = useGlitchgrabReports({
token: process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!,
userId: session.user.id, // your DB primary key
limit: 20, // optional, default 100
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{reports.map((r) => (
<li key={r.id}>
{r.issue?.title ?? r.rawInput} — {r.issue?.githubState ?? r.status}
</li>
))}
</ul>
);
}With TanStack Query
import { fetchGlitchgrabReports } from "glitchgrab";
import { useQuery } from "@tanstack/react-query";
const { data: reports, isLoading } = useQuery({
queryKey: ["glitchgrab-reports", session.user.id],
queryFn: () => fetchGlitchgrabReports({
token: process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!,
userId: session.user.id,
limit: 50,
}),
});REST API
Use the REST API directly to fetch reports:
# Fetch all reports
curl -H "Authorization: Bearer gg_your_token" \
https://glitchgrab.dev/api/v1/sdk/reports
# Fetch reports by a specific user
curl -H "Authorization: Bearer gg_your_token" \
"https://glitchgrab.dev/api/v1/sdk/reports?reporterPrimaryKey=user_123"
# Filter by status
curl -H "Authorization: Bearer gg_your_token" \
"https://glitchgrab.dev/api/v1/sdk/reports?status=CREATED&limit=20"Response
{
"success": true,
"data": [
{
"id": "cmn7abc123",
"source": "SDK_USER_REPORT",
"status": "CREATED",
"rawInput": "Button not working",
"reporterPrimaryKey": "user_123",
"reporterName": "John Doe",
"reporterEmail": "[email protected]",
"reporterPhone": null,
"pageUrl": "/dashboard/settings",
"createdAt": "2026-03-26T12:00:00.000Z",
"issue": {
"githubNumber": 42,
"githubUrl": "https://github.com/your/repo/issues/42",
"title": "Button not working",
"labels": ["bug"],
"severity": "medium",
"githubState": "open"
}
}
]
}Response fields
| Field | Description |
|-------|-------------|
| id | Report ID — use this for managing issues |
| source | SDK_AUTO (crash) or SDK_USER_REPORT (user clicked report) |
| status | PENDING, PROCESSING, CREATED, FAILED |
| reporterPrimaryKey | The userId you passed in the session prop |
| reporterName | Reporter's display name |
| issue.githubState | Live GitHub issue state: open, closed, or null if deleted |
| issue.labels | Labels on the GitHub issue (e.g., ["bug", "approved"]) |
| issue.severity | AI-assigned severity |
## How do I approve, reject, or close issues?
### React hook
```tsx
import { useGlitchgrabActions } from "glitchgrab";
function ReportActions({ reportId }: { reportId: string }) {
const { approve, reject, close, isPending, error } = useGlitchgrabActions({
token: process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!,
onSuccess: () => refetch(), // refresh your reports list
onError: (err) => alert(err.message),
});
return (
<div>
<button onClick={() => approve(reportId)} disabled={isPending}>Approve</button>
<button onClick={() => reject(reportId)} disabled={isPending}>Reject</button>
<button onClick={() => close(reportId)} disabled={isPending}>Close</button>
{error && <p>{error}</p>}
</div>
);
}Dashboard
Go to Reports > Product Issues. Each open report shows:
- Approve — adds
approvedlabel to GitHub issue - Reject — adds
rejectedlabel to GitHub issue - Close — closes the GitHub issue
REST API
POST /api/v1/reports/{reportId}/actions
Auth: Bearer gg_ token or dashboard session.
Approve a report
curl -X POST \
-H "Authorization: Bearer gg_your_token" \
-H "Content-Type: application/json" \
-d '{"action": "label", "label": "approved"}' \
https://glitchgrab.dev/api/v1/reports/REPORT_ID/actionsReject a report
curl -X POST \
-H "Authorization: Bearer gg_your_token" \
-H "Content-Type: application/json" \
-d '{"action": "label", "label": "rejected"}' \
https://glitchgrab.dev/api/v1/reports/REPORT_ID/actionsClose an issue
curl -X POST \
-H "Authorization: Bearer gg_your_token" \
-H "Content-Type: application/json" \
-d '{"action": "close"}' \
https://glitchgrab.dev/api/v1/reports/REPORT_ID/actionsReopen an issue
curl -X POST \
-H "Authorization: Bearer gg_your_token" \
-H "Content-Type: application/json" \
-d '{"action": "reopen"}' \
https://glitchgrab.dev/api/v1/reports/REPORT_ID/actionsRemove a label
curl -X POST \
-H "Authorization: Bearer gg_your_token" \
-H "Content-Type: application/json" \
-d '{"action": "unlabel", "label": "rejected"}' \
https://glitchgrab.dev/api/v1/reports/REPORT_ID/actionsAdd any custom label
curl -X POST \
-H "Authorization: Bearer gg_your_token" \
-H "Content-Type: application/json" \
-d '{"action": "label", "label": "high-priority"}' \
https://glitchgrab.dev/api/v1/reports/REPORT_ID/actionsHow to get the report ID
Use the Fetching Reports API to list reports. Each report has an id field — use that as REPORT_ID.
How it works
- End-user reports a bug via SDK -> GitHub issue created
- You fetch reports via API or view on dashboard
- Approve/reject/close via API or dashboard buttons
- Labels and state sync directly to GitHub — GitHub is the source of truth
How do I add comments to a report?
Each report has a conversation thread powered by GitHub issue comments. No extra database — comments live on GitHub.
View a report with comments
curl -H "Authorization: Bearer gg_your_token" \
https://glitchgrab.dev/api/v1/sdk/reports/REPORT_IDReturns the full issue body + all comments:
{
"success": true,
"data": {
"id": "cmn7abc123",
"issue": {
"title": "Button not working",
"body": "## Description\n\nThe submit button...",
"githubState": "open",
"labels": ["bug"]
},
"comments": [
{
"author": "WebNaresh",
"body": "Can you share your browser version?",
"createdAt": "2026-03-27T10:00:00Z"
},
{
"author": "WebNaresh",
"body": "It's Chrome 120 on Windows\n\n---\n> Commented by: **Vivek** ([email protected])",
"createdAt": "2026-03-27T10:05:00Z"
}
]
}
}Reply to a report
curl -X POST \
-H "Authorization: Bearer gg_your_token" \
-H "Content-Type: application/json" \
-d '{"message": "I can reproduce this, fixing now", "reporterName": "Vivek", "reporterEmail": "[email protected]"}' \
https://glitchgrab.dev/api/v1/sdk/reports/REPORT_ID/commentsThe comment is posted to the GitHub issue with attribution: "Commented by: Vivek ([email protected])".
Dashboard
Click any report on the Reports page to see the full conversation thread. Reply directly from the dashboard — comments sync to GitHub.
How do I add an error boundary?
Wrap components to auto-capture React errors:
import { GlitchgrabErrorBoundary } from "glitchgrab";
<GlitchgrabErrorBoundary fallback={<p>Something went wrong</p>}>
<MyComponent />
</GlitchgrabErrorBoundary>⚠️ This does not cover the Next.js App Router. If your app has an app/error.tsx, Next's own boundary sits closer to the crashing component and catches first — GlitchgrabErrorBoundary never runs, and the crash is never reported. See How do I capture App Router crashes? below.
How do I capture App Router crashes?
Read this if you use app/error.tsx or app/global-error.tsx — otherwise your render crashes are silently lost.
A React error that a boundary handles never reaches window.onerror, so provider auto-capture cannot see it. In an App Router app, Next's error.tsx is that boundary. The user sees the fallback screen, and the message and stack are gone.
Report it yourself with captureError:
// app/error.tsx
"use client";
import { useEffect } from "react";
import { useGlitchgrab } from "glitchgrab";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const { captureError } = useGlitchgrab();
useEffect(() => {
captureError(error, { digest: error.digest, boundary: "next-app-router" });
}, [error, captureError]);
return (
<div>
<p>Something went wrong</p>
<button onClick={reset}>Try again</button>
</div>
);
}global-error.tsx replaces the root layout, so it renders outside the provider tree and useGlitchgrab() would throw. Use the standalone export there — it reads the token from the last mounted provider:
// app/global-error.tsx
"use client";
import { useEffect } from "react";
import { captureError } from "glitchgrab";
export default function GlobalError({
error,
}: {
error: Error & { digest?: string };
}) {
useEffect(() => {
captureError(error, { digest: error.digest, boundary: "next-global-error" });
}, [error]);
return (
<html>
<body>
<p>Something went wrong</p>
</body>
</html>
);
}The same applies to React Router errorElement, Remix ErrorBoundary, and any hand-rolled componentDidCatch — call captureError from each.
Catching every boundary error in one place (React 19)
React 19 lets you intercept all boundary-caught errors at the root, so you don't have to wire each boundary by hand:
// app/instrumentation-client.ts (or your custom hydrateRoot call)
import { captureError } from "glitchgrab";
hydrateRoot(document, <App />, {
onCaughtError: (error, errorInfo) => {
captureError(error, {
componentStack: errorInfo.componentStack ?? undefined,
boundary: "react-onCaughtError",
});
},
});Next.js does not expose hydrateRoot options, so App Router apps should use the error.tsx snippets above.
captureError options
captureError(error: unknown, options?: {
componentStack?: string; // from componentDidCatch / onCaughtError
digest?: string; // Next.js error digest — also feeds dedup
boundary?: string; // which boundary caught it, stored as metadata
metadata?: Record<string, string>;
})- Sends
source: "SDK_AUTO",type: "BUG"with the message, stack, component stack, breadcrumbs, device info, page URL and session identity — same shape as auto-capture. - Deduped. An identical error repeating within 5 minutes files one issue, not N — a crash loop won't spam your repo.
- Honours the provider's
ignoreErrors. - Fire-and-forget. Never throws, never blocks your fallback UI from rendering.
- Runs in development too (unlike passive auto-capture), so you can verify the wiring the moment you add it.
- Pass
digestwhenever you have it. In production Next replaces server-boundary error messages with one generic string — without the digest, every distinct server crash on a page collapses into a single deduped issue. - No-ops if no
GlitchgrabProviderhas rendered yet.
How do I catch errors on the server (cron jobs, API routes, workers)?
Everything above is browser-side — it hooks window.onerror and needs a
rendered provider. A cron job at 3am has neither, so a nightly digest that
throws, an SMTP timeout, or a payment webhook a provider rejects is invisible
to it. glitchgrab/server is the same pipeline for code with no tab open.
GLITCHGRAB_TOKEN=gg_your_token// app/api/cron/daily-digest/route.ts
import { reportServerError } from "glitchgrab/server";
export async function GET() {
try {
await sendDigest();
return Response.json({ ok: true });
} catch (error) {
await reportServerError(error, { context: "cron/daily-digest" });
throw error;
}
}That is a GitHub issue with the message, the stack, the Node version and the region — no browser, no screenshot, no provider.
Await it. On a serverless platform your function can be frozen the instant the handler returns; a floating promise dies with it and the report never leaves the machine.
Reporting a failure that isn't a thrown error
The useful failures are often values, not exceptions — a provider that answers
{ ok: false }, a send the API rejects. Report those the same way:
const result = await sendWhatsApp(payload);
if (!result.success) {
await reportServerError(result.error, {
context: "whatsapp/task-reminder",
description: `Template ${payload.template} rejected for ${payload.to}`,
severity: "high",
});
}Attach the logs that led up to it
A stack says where a job broke; the lines it printed before that usually say
why. Pass them as logs — an array of lines or one string:
const lines: string[] = [];
const log = (line: string) => { lines.push(line); console.log(line); };
try {
log(`[filing] starting return ${returnId}`);
await submitReturn(returnId);
} catch (error) {
await reportServerError(error, { context: "worker/filing", logs: lines });
throw error;
}They arrive on the issue as a .log file named after context: the start is
previewed in the issue body, the whole file is kept for download. Only the last
200,000 characters are sent. Logs are not part of the grouping key — the same
error with different logs still lands on one issue.
Set the token once
// instrumentation.ts
import { configureServerReporter, captureServerErrors } from "glitchgrab/server";
export function register() {
configureServerReporter({
token: process.env.GLITCHGRAB_TOKEN,
metadata: { service: "web" },
});
// Optional: report every uncaught exception and unhandled rejection.
captureServerErrors();
}captureServerErrors() listens on uncaughtExceptionMonitor, which observes
without taking over — your process still crashes exactly as it would have. A
reporter that keeps a broken process alive is worse than no reporter.
context is the grouping key
It is sent as the report's pageUrl (as server://<context>), and pageUrl
feeds the dedup signature. Two different jobs throwing the same "Timeout"
therefore stay two issues instead of collapsing into one. Give every call site
its own context.
Options
reportServerError(error: unknown, options?: {
token?: string; // default: process.env.GLITCHGRAB_TOKEN
baseUrl?: string; // default: process.env.GLITCHGRAB_BASE_URL
context?: string; // "cron/daily-digest" — the grouping key
description?: string; // what the job was doing
type?: ReportType; // default "BUG"
severity?: ReportSeverity; // "low" | "medium" | "high" | "critical" — becomes a severity:<value> label
// anything else is rejected 400 by the API
pageUrl?: string; // a real request URL, when there is one
metadata?: Record<string, string>;
logs?: string | string[]; // attached as <context>.log — last 200,000 chars
reporter?: { id?: string; name?: string; email?: string; phone?: string };
enableInDevelopment?: boolean;
})- Deduped server-side: one issue per signature per 24h, and nothing new for 7 days while an issue for it is open. A job failing hourly files one issue, not twenty-four.
- Silent in development unless
enableInDevelopment: true. The browser SDK is stopped by the API's localhost check; a server sends noOriginheader, so this flag is the only thing standing between a refactor and real issues. - Never throws. Returns
nullwhen nothing was filed — no token, development, or the API refused it. - No React, no DOM, no
"use client". Safe in any Node runtime.
How do I show my guides on my own site?
Write guides in Glitchgrab (the Guides page, or an agent with save_guide), then
render them on your site from server components. Uses the same GLITCHGRAB_TOKEN
(or NEXT_PUBLIC_GLITCHGRAB_TOKEN) as the rest of the SDK, and reads published
guides only.
// app/guides/page.tsx
import { listGuides } from "glitchgrab/server";
export default async function GuidesPage() {
const guides = await listGuides();
return (
<ul>
{guides.map((g) => (
<li key={g.slug}>
<a href={`/guides/${g.slug}`}>{g.title}</a> — {g.summary}
</li>
))}
</ul>
);
}// app/guides/[slug]/page.tsx
import { notFound } from "next/navigation";
import { escapeJsonForScript, getGuide } from "glitchgrab/server";
export default async function GuidePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const guide = await getGuide(slug);
if (!guide) notFound();
const jsonLd = { "@context": "https://schema.org", "@type": "HowTo", name: guide.title, description: guide.summary };
return (
<article>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: escapeJsonForScript(jsonLd) }} />
<h1>{guide.title}</h1>
<div dangerouslySetInnerHTML={{ __html: guide.html }} />
</article>
);
}- Never throws.
listGuides()returns[]andgetGuide()returnsnullon any failure — no token, Glitchgrab down, a slow response (5s timeout), an unexpected shape. An outage leaves the page empty instead of a 500. - Cached for 5 minutes by default. The API route is dynamic, so the cache is
yours: pass
{ revalidate: 60 }, or0to read fresh every request. - Use
escapeJsonForScriptfor JSON-LD.JSON.stringifyleaves</script>intact, so a guide title containing it would break out of the tag. htmlis sanitized by Glitchgrab — raw HTML in the markdown arrives as text, and only http(s), mailto and relative links survive. On a domain that holds your users' sessions, add your own sanitize pass as defence in depth; the SDK ships no sanitizer to stay dependency-free.- Headings have ids —
<h2 id="guide-before-you-start">— for a table of contents or a link to one step. Theguide-prefix is there so a heading can never shadow awindowglobal on your page. linkis where the guide lives on your site (its own link, else your guides base URL + slug), ornullwhen neither is set.
listGuides(options?: GuideReadOptions): Promise<GuideSummary[]>
getGuide(slug: string, options?: GuideReadOptions): Promise<Guide | null>
interface GuideReadOptions {
token?: string; // default: GLITCHGRAB_TOKEN, then NEXT_PUBLIC_GLITCHGRAB_TOKEN
baseUrl?: string; // default: GLITCHGRAB_BASE_URL, then https://glitchgrab.dev
revalidate?: number; // seconds, default 300
timeoutMs?: number; // default 5000
}Never pass the ggc_ server key here for a public page — it reads drafts too, and client calls.
How do I show recorded client calls in my admin panel?
On a call page in Glitchgrab, press share with client, then issue a calls
key (ggc_…) on API Tokens → Server keys. Put it in your server env as GLITCHGRAB_CALLS_KEY — never
a NEXT_PUBLIC_ variable, because it plays and deletes calls. Your admin panel
then reads only the calls you shared:
// app/admin/calls/[id]/page.tsx — server component, behind your admin auth
import { notFound } from "next/navigation";
import { getCall } from "glitchgrab/server";
export default async function CallPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const call = await getCall(id);
if (!call) notFound();
return (
<article>
<h1>{call.title}</h1>
{call.audio.map((a) => (
<audio key={a.track} controls src={a.url} />
))}
<p>{call.summary?.overview}</p>
<pre>{call.transcript}</pre>
</article>
);
}- Shared calls only. Everything else on the project — internal calls, prospects — is a 404 to this key.
- Links expire after 15 minutes (
linksExpireInSeconds). Read the call when the page renders; never storeurls. Nothing is cached for the same reason. - Audio tracks: a bot recording has one (
call). An extension recording has two —client(everyone else on the call) andrecorder(your side). - Match a call to a customer by
participants[].email. - Screenshots come a page at a time —
getCall(id, { framesOffset: 50, framesLimit: 50 });frames.totalsays how many exist. deleteCall(id)is permanent — recording, screenshots and transcript. It resolves{ deleted: false, error }rather than throwing, e.g. while the bot is still in that call.- Never throws.
listCalls()→[],getCall()→nullon any failure.
listCalls(options?: { limit?: number; before?: string } & CallsOptions): Promise<CallListItem[]>
getCall(id: string, options?: { framesOffset?: number; framesLimit?: number } & CallsOptions): Promise<Call | null>
deleteCall(id: string, options?: CallsOptions): Promise<{ deleted: true } | { deleted: false; error: string }>
interface CallsOptions {
key?: string; // default: GLITCHGRAB_CALLS_KEY
baseUrl?: string; // default: GLITCHGRAB_BASE_URL, then https://glitchgrab.dev
timeoutMs?: number; // default 5000
}How do I change demo booking settings from my admin panel?
The same server key reads and writes the project's demo booking settings — whether prospects can book, working hours, slot length, notice, and the WhatsApp code. Which Google calendar demos land in is not exposed: that stays set in Glitchgrab → Calls → Booking.
// app/api/admin/booking/route.ts — behind your admin auth
import { getBookingSettings, updateBookingSettings } from "glitchgrab/server";
const current = await getBookingSettings(); // GLITCHGRAB_CALLS_KEY=ggc_…
if (current.ok && !current.data.calendarConnected) {
// no calendar → no slots, whatever `enabled` says
}
const saved = await updateBookingSettings({ enabled: true, slotMinutes: 45 });
if (!saved.ok) console.error(saved.error); // e.g. WhatsApp code already taken- Send only what changed. A field left out keeps its value.
- Values are clamped (slot 15–180 min, gap 0–120, 1–90 days ahead, notice
0–10080 min) and invalid working hours are ignored — read
data.settingsback instead of trusting what you sent. - Never throws. Failures come back as
{ ok: false, error }, never as defaults.
Asking questions before a demo is booked
Set questions and the booking dialog asks them before it shows the calendar;
the WhatsApp booking chat asks the same ones. Each is a list of choices the
prospect taps — which language they want the demo in, what kind of firm they
run — so the right person on your team takes the call.
await updateBookingSettings({
questions: [
{ id: "profession", label: "What best describes you?", options: ["CA", "CS", "Tax consultant", "Other"] },
{ id: "language", label: "Which language would you like the demo in?", options: ["English", "Hindi", "Marathi"] },
],
});
const demos = await listBookings();
demos[0].answers; // [{ id: "language", question: "Which language…", answer: "Hindi" }, …]- Up to 5 questions, 2–9 choices each.
required: falselets a prospect skip one. - Send the whole list to change it;
[]asks nothing. Left out, it is kept. - Keep a question's
idwhen you reword it, so earlier answers still line up. - The answers are also on the Google Calendar event and in the WhatsApp alert.
- The dialog needs a
glitchgrabversion newer than 1.57 on the site. An older one still books, without asking.
getBookingSettings(options?: CallsOptions): Promise<BookingSettingsResult>
updateBookingSettings(changes: BookingSettingsChanges, options?: CallsOptions): Promise<BookingSettingsResult>
type BookingSettingsResult =
| { ok: true; data: { settings: BookingSettings; saved: boolean; calendarConnected: boolean } }
| { ok: false; error: string };
interface BookingSettings {
enabled: boolean;
slotMinutes: number;
bufferMinutes: number;
timezone: string; // IANA, e.g. "Asia/Kolkata"
workingHours: Record<string, [string, string][]>; // { "1": [["09:00","17:00"]] }, 1 = Monday
title: string | null;
description: string | null;
hostName: string | null; // who the visitor meets, shown beside the calendar
hostAvatarUrl: string | null; // https only; anything else is ignored on save
horizonDays: number;
noticeMinutes: number;
whatsappCode: string | null;
notifyRecipients: { name: string; phone: string }[]; // extra WhatsApp numbers alerted on a booking, max 5
questions: BookingQuestion[]; // asked before the calendar, max 5
}
interface BookingQuestion {
id: string; // stable name, e.g. "language"; made from the label when left out
label: string; // up to 80 characters
options: string[]; // 2–9 choices, up to 40 characters each
required: boolean; // false lets the prospect skip it
}What configuration options are available?
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| token | string | required | Your Glitchgrab API token (gg_...) |
| session | GlitchgrabSession \| null | null | Logged-in user info for report attribution |
| baseUrl | string | https://glitchgrab.dev | API base URL |
| breadcrumbs | boolean | true | Enable automatic breadcrumb tracking |
| maxBreadcrumbs | number | 50 | Max breadcrumbs to keep |
| onError | (error: Error) => void | - | Called on unhandled errors |
| onReportSent | (result: ReportResult) => void | - | Called after a report is sent |
| fallback | ReactNode | - | Error boundary fallback UI |
| ignoreErrors | (string \| RegExp)[] | - | Skip auto-capture for errors whose message matches (substring for string, .test() for RegExp) |
| release | string | env fallback | Build identifier on every report — version, tag, or commit SHA |
| context | Record<string, unknown> | - | App-owned key-values on every report (orgId, plan, flags) |
| responseBodyOrigins | string[] | - | Extra origins whose failed-request bodies may be recorded. Same-origin is always recorded; third parties never are unless listed |
Ignoring known-noisy errors
Some errors that reach window.onerror aren't app bugs — browser extension bridges (Grammarly, password managers, etc.) can throw errors that look like they come from your page. If you keep seeing the same non-actionable signature auto-filed as a report, suppress it:
<GlitchgrabProvider
token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!}
ignoreErrors={[/Object Not Found Matching Id.*MethodName:update/]}
>
{children}
</GlitchgrabProvider>How do I attach my own context to reports?
The SDK captures what a browser can see. It cannot know that this user is on the enterprise plan, in org 42, with the new billing flow switched on — and that is usually the difference between "a crash" and "a crash for one tenant on one flag".
Attach your own keys once; every report from then on carries them:
const { setContext, setContexts } = useGlitchgrab();
setContext("orgId", org.id);
setContexts({ plan: org.plan, role: user.role, newBilling: flags.newBilling });
// Pass null to remove a key — a user who leaves an org stops reporting it
setContext("orgId", null);Or set them declaratively on the provider:
<GlitchgrabProvider
token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!}
context={{ plan: org.plan, region: "in-south" }}
>
{children}
</GlitchgrabProvider>They arrive on the report prefixed with ctx_ (ctx_orgId, ctx_plan), so an app key named timestamp or status can never overwrite a field the dashboard relies on. setContext / setContexts are also exported standalone for non-React code, and the values survive navigation and provider remounts. Limits: 30 keys, 200 chars per value.
How do I tell which deploy broke?
Pass a release and every report names the build it came from:
<GlitchgrabProvider
token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!}
release={process.env.NEXT_PUBLIC_APP_VERSION}
>If you don't pass one, the SDK falls back to NEXT_PUBLIC_APP_VERSION, then NEXT_PUBLIC_RELEASE, then NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA — so on Vercel this works with no configuration at all.
Do I need a Content-Security-Policy allowance?
If your app sets a CSP header (e.g. via proxy.ts / middleware.ts in Next.js), allow Glitchgrab's API host so fetch calls from the SDK aren't blocked:
// proxy.ts / middleware.ts
response.headers.set(
"Content-Security-Policy",
"connect-src 'self' https://glitchgrab.dev; ..." // plus your existing directives
);connect-src https://glitchgrab.dev— required forsendReport,enhanceText,transcribeAudio, and the report-fetching hooks/REST calls. All SDK requests go to this single host by default.- If you pass a custom
baseUrlprop, allow that host instead (self-hosted or proxied deployments). - Screenshot capture runs entirely client-side — no network request, no extra
img-src/connect-srcneeded for it. In Chrome and Edge the browser asks "Allow this site to see this tab?" when the dialog opens, and the screenshot is the tab's real pixels (the share stops after one frame). If the reporter clicks Cancel, or in Safari/Firefox, it falls back to re-drawingdocument.bodywithhtml2canvas-pro, which needs no permission but can drift from the real UI. If the SDK runs inside an iframe, the frame needsallow="display-capture"for the real-pixel path. img-srconly matters if yoursessioncarries an avatar URL — the report dialog renders it. If your CSP blocks that host the dialog falls back to the reporter's initials, so it degrades rather than breaking.- No
script-src,style-src, orframe-srcallowances are required — the SDK doesn't load remote scripts, styles, or iframes.
How does auto-capture work?
In production (NODE_ENV=production), the SDK automatically captures:
- Unhandled JavaScript errors
- Unhandled promise rejections
- Console errors (as breadcrumbs)
- Navigation events (as breadcrumbs)
- API calls, both
fetchandXMLHttpRequest(as breadcrumbs)
Both HTTP paths are patched, so axios works — axios uses XMLHttpRequest in the browser, not fetch.
For a failed request the breadcrumb also carries the response body, so a → 500 says why. Guardrails:
- Same-origin only. Your own API's error shape is yours; a third-party 422 from Stripe or Auth0 echoes fields you don't control (dates of birth, phone numbers) that would end up in a public GitHub issue. Third-party calls are still recorded — status, method, duration — just never their body.
- Add trusted hosts explicitly when your API is on another origin:
<GlitchgrabProvider token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!} responseBodyOrigins={["https://api.myapp.com"]} > - Truncated to 500 chars; sensitively-named JSON keys dropped; emails, JWTs and bearer tokens scrubbed.
- Only non-2xx responses are read. Successful traffic is never buffered.
Auto-capture is disabled in development to avoid noisy issues.
It does not cover React errors that a framework error boundary handles — those never reach window.onerror. Call captureError from your boundary to report them.
What data is included in each report?
- Description from the user
- Screenshot (auto-captured or uploaded)
- Page URL and user agent
- Device info (screen size, viewport, platform, language, color scheme)
- Page navigation history
- Activity log (last 15 breadcrumbs), including failed API calls with their response body
- Session info (userId, name, email, phone)
- Release / build identifier
- Your own context keys (
ctx_orgId,ctx_plan, …) - Runtime health — time on page, error count this session, tab visibility, JS heap usage, connection type and RTT (heap and connection are Chromium-only)
License
Proprietary — free to install and use with the Glitchgrab service. See LICENSE.
