glitchgrab 1.29.1 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -146,6 +146,84 @@ function FeedbackWidget() {
146
146
 
147
147
  Note: `openReportDialog()` requires a `<ReportButton>` to be mounted somewhere in the component tree. It triggers the same modal with screenshot capture.
148
148
 
149
+ ## How do I collect feedback about my app?
150
+
151
+ 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.
152
+
153
+ ### Drop-in button
154
+
155
+ ```tsx
156
+ import { FeedbackButton } from "glitchgrab";
157
+
158
+ <FeedbackButton /> // floating, bottom-left
159
+ <FeedbackButton position="bottom-right" label="Rate us" />
160
+
161
+ // Your own trigger
162
+ <FeedbackButton>
163
+ {({ onClick }) => <button onClick={onClick}>How are we doing?</button>}
164
+ </FeedbackButton>
165
+ ```
166
+
167
+ The dialog (stars + message) ships inside `GlitchgrabProvider` — the button is only the trigger. Open it from anywhere with `openFeedbackDialog()`.
168
+
169
+ ### Your own UI
170
+
171
+ ```tsx
172
+ function RatingRow() {
173
+ const { sendFeedback } = useGlitchgrab();
174
+
175
+ return [1, 2, 3, 4, 5].map((stars) => (
176
+ <button key={stars} onClick={() => sendFeedback(stars, "Loved the new export flow")}>
177
+ {stars}★
178
+ </button>
179
+ ));
180
+ }
181
+ ```
182
+
183
+ `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.
184
+
185
+ ### Reading it back
186
+
187
+ 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:
188
+
189
+ ```tsx
190
+ import { useGlitchgrabFeedback } from "glitchgrab";
191
+
192
+ function Testimonials() {
193
+ const { feedback, isLoading } = useGlitchgrabFeedback({
194
+ token: process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!,
195
+ approvedOnly: true,
196
+ minRating: 4,
197
+ });
198
+
199
+ if (isLoading) return null;
200
+
201
+ return feedback.map((f) => (
202
+ <blockquote key={f.id}>
203
+ {f.message} — {f.reporterName} ({f.rating}★)
204
+ </blockquote>
205
+ ));
206
+ }
207
+ ```
208
+
209
+ 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.
210
+
211
+ ### REST
212
+
213
+ ```bash
214
+ # Submit
215
+ curl -X POST https://glitchgrab.dev/api/v1/sdk/feedback \
216
+ -H "Authorization: Bearer gg_xxxxx" \
217
+ -H "Content-Type: application/json" \
218
+ -d '{"rating":5,"message":"Fast and simple","metadata":{"sessionUserId":"user_123","sessionUserName":"Asha"}}'
219
+
220
+ # Read published entries
221
+ curl "https://glitchgrab.dev/api/v1/sdk/feedback?approved=true&minRating=4&limit=20" \
222
+ -H "Authorization: Bearer gg_xxxxx"
223
+ ```
224
+
225
+ The repo is always derived from the token — there is no `repoId` to pass. Rate limit: 30 submissions per token per hour.
226
+
149
227
  ## What keyboard shortcuts are available?
150
228
 
151
229
  Once `GlitchgrabProvider` is mounted, these shortcuts work globally:
@@ -455,6 +533,114 @@ import { GlitchgrabErrorBoundary } from "glitchgrab";
455
533
  </GlitchgrabErrorBoundary>
456
534
  ```
457
535
 
536
+ ⚠️ **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?](#how-do-i-capture-app-router-crashes) below.
537
+
538
+ ## How do I capture App Router crashes?
539
+
540
+ **Read this if you use `app/error.tsx` or `app/global-error.tsx` — otherwise your render crashes are silently lost.**
541
+
542
+ 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.
543
+
544
+ Report it yourself with `captureError`:
545
+
546
+ ```tsx
547
+ // app/error.tsx
548
+ "use client";
549
+
550
+ import { useEffect } from "react";
551
+ import { useGlitchgrab } from "glitchgrab";
552
+
553
+ export default function Error({
554
+ error,
555
+ reset,
556
+ }: {
557
+ error: Error & { digest?: string };
558
+ reset: () => void;
559
+ }) {
560
+ const { captureError } = useGlitchgrab();
561
+
562
+ useEffect(() => {
563
+ captureError(error, { digest: error.digest, boundary: "next-app-router" });
564
+ }, [error, captureError]);
565
+
566
+ return (
567
+ <div>
568
+ <p>Something went wrong</p>
569
+ <button onClick={reset}>Try again</button>
570
+ </div>
571
+ );
572
+ }
573
+ ```
574
+
575
+ `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:
576
+
577
+ ```tsx
578
+ // app/global-error.tsx
579
+ "use client";
580
+
581
+ import { useEffect } from "react";
582
+ import { captureError } from "glitchgrab";
583
+
584
+ export default function GlobalError({
585
+ error,
586
+ }: {
587
+ error: Error & { digest?: string };
588
+ }) {
589
+ useEffect(() => {
590
+ captureError(error, { digest: error.digest, boundary: "next-global-error" });
591
+ }, [error]);
592
+
593
+ return (
594
+ <html>
595
+ <body>
596
+ <p>Something went wrong</p>
597
+ </body>
598
+ </html>
599
+ );
600
+ }
601
+ ```
602
+
603
+ The same applies to React Router `errorElement`, Remix `ErrorBoundary`, and any hand-rolled `componentDidCatch` — call `captureError` from each.
604
+
605
+ ### Catching every boundary error in one place (React 19)
606
+
607
+ React 19 lets you intercept *all* boundary-caught errors at the root, so you don't have to wire each boundary by hand:
608
+
609
+ ```tsx
610
+ // app/instrumentation-client.ts (or your custom hydrateRoot call)
611
+ import { captureError } from "glitchgrab";
612
+
613
+ hydrateRoot(document, <App />, {
614
+ onCaughtError: (error, errorInfo) => {
615
+ captureError(error, {
616
+ componentStack: errorInfo.componentStack ?? undefined,
617
+ boundary: "react-onCaughtError",
618
+ });
619
+ },
620
+ });
621
+ ```
622
+
623
+ Next.js does not expose `hydrateRoot` options, so App Router apps should use the `error.tsx` snippets above.
624
+
625
+ ### captureError options
626
+
627
+ ```ts
628
+ captureError(error: unknown, options?: {
629
+ componentStack?: string; // from componentDidCatch / onCaughtError
630
+ digest?: string; // Next.js error digest — also feeds dedup
631
+ boundary?: string; // which boundary caught it, stored as metadata
632
+ metadata?: Record<string, string>;
633
+ })
634
+ ```
635
+
636
+ - 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.
637
+ - **Deduped.** An identical error repeating within 5 minutes files one issue, not N — a crash loop won't spam your repo.
638
+ - Honours the provider's `ignoreErrors`.
639
+ - Fire-and-forget. Never throws, never blocks your fallback UI from rendering.
640
+ - Runs in development too (unlike passive auto-capture), so you can verify the wiring the moment you add it.
641
+ - Pass `digest` whenever 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.
642
+ - No-ops if no `GlitchgrabProvider` has rendered yet.
643
+
458
644
  ## What configuration options are available?
459
645
 
460
646
  | Prop | Type | Default | Description |
@@ -468,6 +654,9 @@ import { GlitchgrabErrorBoundary } from "glitchgrab";
468
654
  | `onReportSent` | `(result: ReportResult) => void` | - | Called after a report is sent |
469
655
  | `fallback` | `ReactNode` | - | Error boundary fallback UI |
470
656
  | `ignoreErrors` | `(string \| RegExp)[]` | - | Skip auto-capture for errors whose message matches (substring for `string`, `.test()` for `RegExp`) |
657
+ | `release` | `string` | env fallback | Build identifier on every report — version, tag, or commit SHA |
658
+ | `context` | `Record<string, unknown>` | - | App-owned key-values on every report (orgId, plan, flags) |
659
+ | `responseBodyOrigins` | `string[]` | - | Extra origins whose failed-request bodies may be recorded. Same-origin is always recorded; third parties never are unless listed |
471
660
 
472
661
  ### Ignoring known-noisy errors
473
662
 
@@ -482,6 +671,48 @@ Some errors that reach `window.onerror` aren't app bugs — browser extension br
482
671
  </GlitchgrabProvider>
483
672
  ```
484
673
 
674
+ ## How do I attach my own context to reports?
675
+
676
+ 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".
677
+
678
+ Attach your own keys once; every report from then on carries them:
679
+
680
+ ```tsx
681
+ const { setContext, setContexts } = useGlitchgrab();
682
+
683
+ setContext("orgId", org.id);
684
+ setContexts({ plan: org.plan, role: user.role, newBilling: flags.newBilling });
685
+
686
+ // Pass null to remove a key — a user who leaves an org stops reporting it
687
+ setContext("orgId", null);
688
+ ```
689
+
690
+ Or set them declaratively on the provider:
691
+
692
+ ```tsx
693
+ <GlitchgrabProvider
694
+ token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!}
695
+ context={{ plan: org.plan, region: "in-south" }}
696
+ >
697
+ {children}
698
+ </GlitchgrabProvider>
699
+ ```
700
+
701
+ 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.
702
+
703
+ ## How do I tell which deploy broke?
704
+
705
+ Pass a `release` and every report names the build it came from:
706
+
707
+ ```tsx
708
+ <GlitchgrabProvider
709
+ token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!}
710
+ release={process.env.NEXT_PUBLIC_APP_VERSION}
711
+ >
712
+ ```
713
+
714
+ 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.
715
+
485
716
  ## Do I need a Content-Security-Policy allowance?
486
717
 
487
718
  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:
@@ -497,6 +728,7 @@ response.headers.set(
497
728
  - `connect-src https://glitchgrab.dev` — required for `sendReport`, `enhanceText`, `transcribeAudio`, and the report-fetching hooks/REST calls. All SDK requests go to this single host by default.
498
729
  - If you pass a custom `baseUrl` prop, allow that host instead (self-hosted or proxied deployments).
499
730
  - Screenshot capture (`html2canvas-pro`) runs entirely client-side against `document.body` — no network request, no extra `img-src`/`connect-src` needed for it.
731
+ - `img-src` only matters if your `session` carries 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.
500
732
  - No `script-src`, `style-src`, or `frame-src` allowances are required — the SDK doesn't load remote scripts, styles, or iframes.
501
733
 
502
734
  ## How does auto-capture work?
@@ -506,10 +738,27 @@ In production (`NODE_ENV=production`), the SDK automatically captures:
506
738
  - Unhandled promise rejections
507
739
  - Console errors (as breadcrumbs)
508
740
  - Navigation events (as breadcrumbs)
509
- - API calls (as breadcrumbs)
741
+ - API calls, both `fetch` and `XMLHttpRequest` (as breadcrumbs)
742
+
743
+ Both HTTP paths are patched, so **axios works** — axios uses `XMLHttpRequest` in the browser, not `fetch`.
744
+
745
+ For a failed request the breadcrumb also carries the response body, so a `→ 500` says *why*. Guardrails:
746
+
747
+ - **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.
748
+ - Add trusted hosts explicitly when your API is on another origin:
749
+ ```tsx
750
+ <GlitchgrabProvider
751
+ token={process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!}
752
+ responseBodyOrigins={["https://api.myapp.com"]}
753
+ >
754
+ ```
755
+ - Truncated to 500 chars; sensitively-named JSON keys dropped; emails, JWTs and bearer tokens scrubbed.
756
+ - **Only non-2xx responses are read.** Successful traffic is never buffered.
510
757
 
511
758
  Auto-capture is **disabled in development** to avoid noisy issues.
512
759
 
760
+ It does **not** cover React errors that a framework error boundary handles — those never reach `window.onerror`. Call [`captureError`](#how-do-i-capture-app-router-crashes) from your boundary to report them.
761
+
513
762
  ## What data is included in each report?
514
763
 
515
764
  - Description from the user
@@ -517,8 +766,11 @@ Auto-capture is **disabled in development** to avoid noisy issues.
517
766
  - Page URL and user agent
518
767
  - Device info (screen size, viewport, platform, language, color scheme)
519
768
  - Page navigation history
520
- - Activity log (last 15 breadcrumbs)
769
+ - Activity log (last 15 breadcrumbs), including failed API calls with their response body
521
770
  - Session info (userId, name, email, phone)
771
+ - Release / build identifier
772
+ - Your own context keys (`ctx_orgId`, `ctx_plan`, …)
773
+ - Runtime health — time on page, error count this session, tab visibility, JS heap usage, connection type and RTT (heap and connection are Chromium-only)
522
774
 
523
775
  ## License
524
776
 
package/dist/index.d.mts CHANGED
@@ -35,6 +35,21 @@ interface ReportPayload {
35
35
  deviceInfo?: DeviceInfo;
36
36
  metadata?: Record<string, string>;
37
37
  }
38
+ /** Extra context for an error your app already caught — see `captureError` */
39
+ interface CaptureErrorOptions {
40
+ /** React component stack, e.g. from `componentDidCatch` / `onCaughtError` */
41
+ componentStack?: string;
42
+ /**
43
+ * Next.js error digest. In production Next replaces server-boundary error
44
+ * messages with a generic string — the digest is the only thing that tells
45
+ * two different crashes apart, so it also feeds the dedup signature.
46
+ */
47
+ digest?: string;
48
+ /** Which boundary caught it, e.g. `"next-app-router"` — attached as metadata */
49
+ boundary?: string;
50
+ /** Extra metadata merged into the report */
51
+ metadata?: Record<string, string>;
52
+ }
38
53
  interface ReportResult {
39
54
  success: boolean;
40
55
  reportId?: string;
@@ -44,6 +59,34 @@ interface ReportResult {
44
59
  intent?: string;
45
60
  message?: string;
46
61
  }
62
+ interface FeedbackPayload {
63
+ token: string;
64
+ /** 1–5 stars */
65
+ rating: number;
66
+ message?: string;
67
+ pageUrl?: string;
68
+ userAgent?: string;
69
+ metadata?: Record<string, string>;
70
+ }
71
+ interface FeedbackResult {
72
+ success: boolean;
73
+ feedbackId?: string;
74
+ rating?: number;
75
+ createdAt?: string;
76
+ message?: string;
77
+ }
78
+ /** One stored feedback entry, as returned by the read endpoint. */
79
+ interface GlitchgrabFeedback {
80
+ id: string;
81
+ rating: number;
82
+ message: string | null;
83
+ pageUrl: string | null;
84
+ /** Whether the repo owner published this entry for public display */
85
+ approved: boolean;
86
+ reporterPrimaryKey: string;
87
+ reporterName: string;
88
+ createdAt: string;
89
+ }
47
90
  type BreadcrumbType = "console" | "navigation" | "api" | "click" | "error" | "custom";
48
91
  interface Breadcrumb {
49
92
  type: BreadcrumbType;
@@ -62,6 +105,26 @@ interface DeviceInfo {
62
105
  colorScheme: string;
63
106
  devicePixelRatio: number;
64
107
  }
108
+ /**
109
+ * Machine state at the moment of the crash, as opposed to `DeviceInfo`, which
110
+ * describes the machine itself. Most fields are Chromium-only and simply absent
111
+ * elsewhere — never assume one is present.
112
+ */
113
+ interface RuntimeInfo {
114
+ /** ms since this page loaded */
115
+ timeOnPageMs: number;
116
+ /** How many errors the SDK has already auto-captured in this page session */
117
+ errorCount: number;
118
+ /** `document.visibilityState` — a crash in a background tab reads differently */
119
+ visibility: string;
120
+ jsHeapUsedMb?: number;
121
+ jsHeapLimitMb?: number;
122
+ /** `navigator.connection.effectiveType` — "4g", "3g", "2g", "slow-2g" */
123
+ connectionType?: string;
124
+ downlinkMbps?: number;
125
+ rttMs?: number;
126
+ saveData?: boolean;
127
+ }
65
128
  interface CapturedContext {
66
129
  url: string;
67
130
  userAgent: string;
@@ -69,6 +132,12 @@ interface CapturedContext {
69
132
  visitedPages: string[];
70
133
  breadcrumbs: Breadcrumb[];
71
134
  deviceInfo: DeviceInfo | null;
135
+ /** Runtime health snapshot — null when unavailable (SSR) */
136
+ runtime: RuntimeInfo | null;
137
+ /** Key-values the host app attached via `setContext` */
138
+ appContext: Record<string, string>;
139
+ /** Build identifier this crash came from, if known */
140
+ release?: string;
72
141
  }
73
142
  interface GlitchgrabSession {
74
143
  /** Primary key of the user in your database (required) */
@@ -103,17 +172,69 @@ interface GlitchgrabProviderProps {
103
172
  * signatures that aren't app bugs — e.g. browser extension bridge errors.
104
173
  */
105
174
  ignoreErrors?: (string | RegExp)[];
175
+ /**
176
+ * Build identifier attached to every report — a version, a tag, a commit SHA.
177
+ * Tells you which deploy introduced a crash. Falls back to
178
+ * `NEXT_PUBLIC_APP_VERSION`, `NEXT_PUBLIC_RELEASE`, then
179
+ * `NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA` when not passed.
180
+ */
181
+ release?: string;
182
+ /**
183
+ * App-owned context attached to every report — orgId, plan, role, feature
184
+ * flags. Merged with anything set imperatively via `setContext`.
185
+ */
186
+ context?: Record<string, unknown>;
187
+ /**
188
+ * Extra origins whose failed-request bodies may be recorded in breadcrumbs,
189
+ * e.g. `["https://api.myapp.com"]`. Same-origin requests are always recorded;
190
+ * third-party APIs are excluded by default because their error envelopes carry
191
+ * data you don't control and shouldn't forward into a GitHub issue.
192
+ */
193
+ responseBodyOrigins?: string[];
106
194
  }
107
195
  interface ReportButtonProps {
108
196
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
109
197
  label?: string;
110
198
  className?: string;
111
199
  }
200
+ interface FeedbackButtonProps {
201
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
202
+ label?: string;
203
+ className?: string;
204
+ /** Heading inside the dialog (default: "How are we doing?") */
205
+ title?: string;
206
+ /** Placeholder for the message box */
207
+ placeholder?: string;
208
+ /** Shown after a successful submit */
209
+ thanksMessage?: string;
210
+ }
112
211
  interface UseGlitchgrabReturn {
113
212
  /** Report a bug programmatically */
114
213
  reportBug: (description: string, metadata?: Record<string, string>) => Promise<ReportResult | null>;
115
214
  /** Report with a specific type */
116
215
  report: (type: ReportType, description: string, metadata?: Record<string, string>) => Promise<ReportResult | null>;
216
+ /**
217
+ * Report an error your app already caught — a framework error boundary
218
+ * (`app/error.tsx`, React Router `errorElement`, Remix `ErrorBoundary`) or a
219
+ * `try/catch`. These never reach `window.onerror`, so auto-capture misses them.
220
+ * Sends `SDK_AUTO`. Fire-and-forget, never throws.
221
+ */
222
+ captureError: (error: unknown, options?: CaptureErrorOptions) => void;
223
+ /**
224
+ * Attach a key-value to every future report — orgId, plan, role, feature flag.
225
+ * Pass `null` to remove a key. Survives navigation and provider remounts.
226
+ */
227
+ setContext: (key: string, value: unknown) => void;
228
+ /** Set several context keys at once. Merges; does not replace. */
229
+ setContexts: (values: Record<string, unknown>) => void;
230
+ /**
231
+ * Save a 1–5 star rating your end-user left about your app. Stored by
232
+ * Glitchgrab and shown on your Feedback page — never becomes a GitHub issue.
233
+ * Returns null on any failure (never throws).
234
+ */
235
+ sendFeedback: (rating: number, message?: string, metadata?: Record<string, string>) => Promise<FeedbackResult | null>;
236
+ /** Open the built-in feedback dialog (stars + message) programmatically */
237
+ openFeedbackDialog: () => void;
117
238
  /** Add a custom breadcrumb */
118
239
  addBreadcrumb: (message: string, data?: Record<string, string>) => void;
119
240
  /** Open the ReportButton modal programmatically (captures screenshot + shows dialog) */
@@ -144,11 +265,14 @@ interface UseGlitchgrabReturn {
144
265
  *
145
266
  * @example
146
267
  * ```tsx
147
- * const { reportBug, report, addBreadcrumb } = useGlitchgrab();
268
+ * const { reportBug, report, captureError, addBreadcrumb } = useGlitchgrab();
148
269
  *
149
270
  * // Report a bug
150
271
  * reportBug("Login button crashes on mobile");
151
272
  *
273
+ * // Report an error your own boundary already caught (e.g. app/error.tsx)
274
+ * captureError(error, { digest: error.digest, boundary: "next-app-router" });
275
+ *
152
276
  * // Report a feature request
153
277
  * report("FEATURE_REQUEST", "Add dark mode");
154
278
  *
@@ -188,6 +312,31 @@ declare function ReportButton({ position, label, className, children, }: ReportB
188
312
  }) => ReactNode;
189
313
  }): react_jsx_runtime.JSX.Element | null;
190
314
 
315
+ /**
316
+ * Trigger for the built-in feedback dialog (stars + message).
317
+ *
318
+ * The dialog itself lives inside `GlitchgrabProvider` — this is only the visible
319
+ * trigger. Open it programmatically with `useGlitchgrab().openFeedbackDialog()`,
320
+ * or skip the dialog entirely and call `sendFeedback(rating, message)` from your
321
+ * own UI.
322
+ *
323
+ * @example
324
+ * ```tsx
325
+ * // Default floating button
326
+ * <FeedbackButton />
327
+ *
328
+ * // Your own trigger
329
+ * <FeedbackButton>
330
+ * {({ onClick }) => <button onClick={onClick}>Rate us</button>}
331
+ * </FeedbackButton>
332
+ * ```
333
+ */
334
+ declare function FeedbackButton({ position, label, className, children, }: FeedbackButtonProps & {
335
+ children?: (props: {
336
+ onClick: () => void;
337
+ }) => ReactNode;
338
+ }): react_jsx_runtime.JSX.Element | null;
339
+
191
340
  interface ErrorBoundaryProps {
192
341
  token: string;
193
342
  baseUrl?: string;
@@ -207,6 +356,52 @@ declare class GlitchgrabErrorBoundary extends React.Component<ErrorBoundaryProps
207
356
  render(): React.ReactNode;
208
357
  }
209
358
 
359
+ /**
360
+ * Report an error your app already caught — a framework error boundary
361
+ * (`app/error.tsx`, `app/global-error.tsx`, React Router `errorElement`, Remix
362
+ * `ErrorBoundary`), a `componentDidCatch`, or a `try/catch` you want filed.
363
+ *
364
+ * These never reach `window.onerror`, so provider auto-capture cannot see them.
365
+ *
366
+ * Fire-and-forget. Never throws, never blocks the fallback UI render. No-ops if
367
+ * no `GlitchgrabProvider` has rendered yet (nothing to authenticate with).
368
+ *
369
+ * @example
370
+ * ```tsx
371
+ * // app/global-error.tsx — renders outside the provider tree
372
+ * "use client";
373
+ * import { captureError } from "glitchgrab";
374
+ *
375
+ * export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
376
+ * useEffect(() => {
377
+ * captureError(error, { digest: error.digest, boundary: "next-global-error" });
378
+ * }, [error]);
379
+ * return <html><body><p>Something went wrong</p></body></html>;
380
+ * }
381
+ * ```
382
+ */
383
+ declare function captureError(error: unknown, options?: CaptureErrorOptions): void;
384
+
385
+ /**
386
+ * App-owned context: key-values the host app attaches once and every report
387
+ * carries thereafter — orgId, plan, role, active feature flags. The SDK cannot
388
+ * guess these, and they are usually the difference between "a crash" and "a
389
+ * crash for enterprise tenants on the new billing flow".
390
+ *
391
+ * Module-level, not React state, so it survives the provider unmount that
392
+ * `global-error.tsx` causes and is readable from non-React call sites.
393
+ */
394
+ /**
395
+ * Attach one key-value to every future report. Passing `null`/`undefined`
396
+ * removes the key — so a value that goes away (user logs out of an org) stops
397
+ * being reported instead of going stale.
398
+ */
399
+ declare function setContext(key: string, value: unknown): void;
400
+ /** Set several keys at once. Merges — it does not replace what's already set. */
401
+ declare function setContexts(values: Record<string, unknown>): void;
402
+ declare function getAppContext(): Record<string, string>;
403
+ declare function clearAppContext(): void;
404
+
210
405
  interface GlitchgrabReport {
211
406
  id: string;
212
407
  source: string;
@@ -299,7 +494,60 @@ declare function useGlitchgrabActions({ token, onSuccess, onError, }: {
299
494
  error: string | null;
300
495
  };
301
496
 
302
- declare function initBreadcrumbs(max?: number): void;
497
+ interface FeedbackQuery {
498
+ token: string;
499
+ /** Only entries the repo owner published — use this for a public testimonials wall */
500
+ approvedOnly?: boolean;
501
+ /** Only this end-user's feedback (their primary key in your DB) */
502
+ userId?: string;
503
+ /** Floor on the star rating, 1–5 */
504
+ minRating?: number;
505
+ /** Max results (default 50, max 100) */
506
+ limit?: number;
507
+ baseUrl?: string;
508
+ }
509
+ /**
510
+ * Standalone fetcher — use with TanStack Query or any data fetching library.
511
+ *
512
+ * ```tsx
513
+ * const { data } = useQuery({
514
+ * queryKey: ["testimonials"],
515
+ * queryFn: () => fetchGlitchgrabFeedback({
516
+ * token: process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!,
517
+ * approvedOnly: true,
518
+ * minRating: 4,
519
+ * }),
520
+ * });
521
+ * ```
522
+ */
523
+ declare function fetchGlitchgrabFeedback(query: FeedbackQuery): Promise<GlitchgrabFeedback[]>;
524
+ /**
525
+ * Hook to read back the feedback your end-users left — render it as a
526
+ * testimonials wall, or show a user their own past ratings.
527
+ *
528
+ * ```tsx
529
+ * const { feedback, isLoading, error, refetch } = useGlitchgrabFeedback({
530
+ * token: process.env.NEXT_PUBLIC_GLITCHGRAB_TOKEN!,
531
+ * approvedOnly: true,
532
+ * });
533
+ * ```
534
+ */
535
+ declare function useGlitchgrabFeedback(query: FeedbackQuery): {
536
+ feedback: GlitchgrabFeedback[];
537
+ isLoading: boolean;
538
+ isFetching: boolean;
539
+ error: string | null;
540
+ refetch: () => Promise<void>;
541
+ };
542
+
543
+ interface BreadcrumbOptions {
544
+ /**
545
+ * Additional origins to capture error response bodies from, e.g.
546
+ * `["https://api.myapp.com"]`. Same-origin requests are always captured.
547
+ */
548
+ responseBodyOrigins?: string[];
549
+ }
550
+ declare function initBreadcrumbs(max?: number, options?: BreadcrumbOptions): void;
303
551
  declare function addBreadcrumb(type: BreadcrumbType, message: string, data?: Record<string, string>): void;
304
552
  declare function getBreadcrumbs(): Breadcrumb[];
305
553
  declare function clearBreadcrumbs(): void;
@@ -330,6 +578,15 @@ declare function captureContext(visitedPages: string[]): CapturedContext;
330
578
  * Never throws — returns null on failure.
331
579
  */
332
580
  declare function sendReport(payload: ReportPayload, baseUrl?: string): Promise<ReportResult | null>;
581
+ /**
582
+ * Save a star rating your end-user left about your app.
583
+ * Never throws — returns null on failure.
584
+ *
585
+ * Unlike `sendReport`, this is always a deliberate user action with a dialog
586
+ * open in front of them, so it retries on transient failure but skips the
587
+ * sendBeacon fallback: the caller needs a real result to show a thank-you.
588
+ */
589
+ declare function sendFeedback(payload: FeedbackPayload, baseUrl?: string): Promise<FeedbackResult | null>;
333
590
  interface EnhanceContext {
334
591
  url?: string;
335
592
  visitedPages?: string[];
@@ -349,8 +606,14 @@ declare function computeSignature(params: {
349
606
  errorMessage: string | undefined;
350
607
  pageUrl: string | undefined;
351
608
  errorStack?: string | undefined;
609
+ /**
610
+ * Next.js error digest. Production strips server-boundary errors to a generic
611
+ * message with no useful stack, so without the digest every distinct server
612
+ * crash on one page collapses into a single signature.
613
+ */
614
+ digest?: string | undefined;
352
615
  }): string;
353
616
  declare function shouldSkipDuplicate(signature: string, windowMs?: number, now?: number): boolean;
354
617
  declare function clearDedupCache(): void;
355
618
 
356
- export { type Breadcrumb, type BreadcrumbType, type CapturedContext, type DeviceInfo, GLITCHGRAB_SHORTCUT, GLITCHGRAB_SHORTCUT_MAC, type GlitchgrabConfig, GlitchgrabErrorBoundary, GlitchgrabProvider, type GlitchgrabProviderProps, type GlitchgrabReport, type GlitchgrabSession, ReportButton, type ReportButtonProps, type ReportPayload, type ReportResult, type ReportSeverity, type ReportType, type UseGlitchgrabReturn, addBreadcrumb, captureContext, captureDeviceInfo, clearBreadcrumbs, clearDedupCache, computeSignature, enhanceText, fetchGlitchgrabReports, getBreadcrumbs, getShortcutLabel, initBreadcrumbs, matchesShortcut, sanitizeUrl, sendReport, shouldSkipDuplicate, useGlitchgrab, useGlitchgrabActions, useGlitchgrabReports };
619
+ export { type Breadcrumb, type BreadcrumbType, type CaptureErrorOptions, type CapturedContext, type DeviceInfo, FeedbackButton, type FeedbackButtonProps, type FeedbackPayload, type FeedbackQuery, type FeedbackResult, GLITCHGRAB_SHORTCUT, GLITCHGRAB_SHORTCUT_MAC, type GlitchgrabConfig, GlitchgrabErrorBoundary, type GlitchgrabFeedback, GlitchgrabProvider, type GlitchgrabProviderProps, type GlitchgrabReport, type GlitchgrabSession, ReportButton, type ReportButtonProps, type ReportPayload, type ReportResult, type ReportSeverity, type ReportType, type RuntimeInfo, type UseGlitchgrabReturn, addBreadcrumb, captureContext, captureDeviceInfo, captureError, clearAppContext, clearBreadcrumbs, clearDedupCache, computeSignature, enhanceText, fetchGlitchgrabFeedback, fetchGlitchgrabReports, getAppContext, getBreadcrumbs, getShortcutLabel, initBreadcrumbs, matchesShortcut, sanitizeUrl, sendFeedback, sendReport, setContext, setContexts, shouldSkipDuplicate, useGlitchgrab, useGlitchgrabActions, useGlitchgrabFeedback, useGlitchgrabReports };