Bugfed Insights · SDK v0.2.0
Insights docs
Add a bug-reporting form to your web app, send reports from any platform over HTTP, and see them in your Bugfed dashboard. New to Insights? Read what it is first.
Quickstart
- Create a project in the dashboard (Insights → New project).
- Copy the snippet from the project's Setup tab. It already contains your project key.
- Click Send a test report on the Setup tab to check the whole path.
- Paste the snippet into your app, open it, click Report a bug and send one. The Setup tab shows Connected.
<script type="module" src="https://bugfed.com/sdk/v0/insights.js"
data-key="bfp_your_project_key"
data-app-version="1.0.0"></script>The project key (bfp_…) is public by design, like the key analytics and error-monitoring SDKs put in client code. It can only send reports to its project. It can't read reports or change anything, so it's safe in your app's source. Never put a Bugfed API key (bf_…, used for the Testing API) in client code.
Install
Any website
Add the script before </body>. Attributes configure it, and a launcher button appears in the bottom corner.
| Attribute | Meaning |
|---|---|
data-key | Your project key. Required for the widget to start. |
data-app-version | Your app's version, e.g. 2.4.0. Lets you filter reports by release. |
data-environment | production (default), staging, or any name up to 32 characters. |
data-launcher | "false" hides the floating button; open the form with window.Bugfed.open(). |
data-position | bottom-right (default) or bottom-left. |
data-theme | auto (default, follows the user's system), light or dark. |
data-accent-color | Any CSS colour for the button and primary action. |
data-errors | "true" captures uncaught errors and failed promises. Off by default. |
data-sessions | "true" sends one anonymous "active today" ping per install per day. Off by default. |
Next.js (App Router)
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script
type="module"
src="https://bugfed.com/sdk/v0/insights.js"
data-key="bfp_your_project_key"
data-app-version={process.env.NEXT_PUBLIC_APP_VERSION}
strategy="afterInteractive"
/>
</body>
</html>
);
}React, Vue and other single-page apps
Add the script tag to index.html, then call the SDK through window.Bugfed from your code.
// Add once, e.g. in a global.d.ts
declare global {
interface Window {
Bugfed?: {
open(): void;
identify(user: { id?: string; email?: string } | null): void;
setMetadata(tags: Record<string, string | number | boolean | null>): void;
};
}
}
export function ReportBugButton() {
return <button onClick={() => window.Bugfed?.open()}>Report a bug</button>;
}The npm package @bugfed/insightsisn't published yet. Until it is, load the SDK from the URL above. It is a single ES module of about 45 KB (about 13 KB gzipped) with no dependencies.
Options
With JavaScript instead of attributes: window.Bugfed.init(options) (load the script without data-key).
| Option | Default | What it does |
|---|---|---|
key | required | Project key (bfp_…). |
appVersion | none | Your release, shown on every report. |
environment | "production" | Separates staging from production reports. |
user | none | { id, email }: see Users and tags. |
collectUrl | "path" | "path": origin and path. "full": with the query string. false: no address. |
screenshots | true | Let users attach or capture a screenshot. |
askForEmail | true | Show an optional email field when no user email is known. |
metadata | {} | Tags sent with every report (flat string, number or boolean values). |
beforeSend | none | (report) => report | null. Edit or drop a report before it's sent. |
widget | {} | { launcher, position, theme, accentColor, zIndex, text } or false for no UI. |
widget.text | English | Override any label in the form, e.g. for translations. |
persist | true | Use localStorage for the install id and the offline queue. |
onSubmitted | none | ({ id, number }) => void, called after each accepted report. |
captureErrors | false | Send uncaught errors and unhandled promise rejections. See Errors. |
errorSampleRate | 1 | Fraction of page loads whose errors are sent (0 to 1). |
beforeSendError | none | (error) => error | null. Edit or drop an error before it's sent. |
sessions | false | One "active today" ping per install per day. See Sessions and impact. |
window.Bugfed.init({
key: "bfp_your_project_key",
appVersion: "2.4.0",
environment: location.hostname.startsWith("staging") ? "staging" : "production",
widget: { launcher: false, theme: "dark", text: { launcher: "Feedback", heading: "Something wrong?" } },
beforeSend(report) {
// Redact anything that looks like a card number.
report.description = report.description?.replace(/\b(?:\d[ -]?){13,19}\b/g, "[card]");
return report;
},
});Users and tags
Each report carries a random install id the SDK makes up (it is not tied to a person). It lets the dashboard count how many different people reported the same bug. If your users sign in, you can add your own id:
window.Bugfed.identify({ id: user.id }); // your id, not an email or name
window.Bugfed.identify({ id: user.id, email: user.email }); // only if you want to reply
window.Bugfed.setMetadata({ plan: "pro", screen: "checkout" });
// on sign-out
window.Bugfed.clearUser();Emails are dropped on arrival if the project's Contact email setting is off.
Custom UI
Keep your own form and send reports with submit(). createClient() makes a client that never touches the page.
const bugfed = window.Bugfed.createClient({ key: "bfp_your_project_key", appVersion: "2.4.0" });
const result = await bugfed.submit({
title: "Checkout button does nothing", // required, up to 200 characters
description: "Tapped Pay, nothing happened.",
steps: "1. Add an item\n2. Change address\n3. Tap Pay",
email: "optional@example.com",
screenshot: fileInput.files[0], // optional Blob or File
metadata: { cart_items: 3 },
});
if (result.ok) console.log("Report #" + result.number);
else if (result.queued) console.log("Offline: it will be sent later");
else console.error(result.error, result.message);The script fires a bugfed:ready event on window once window.Bugfed exists.
Screenshots
Screenshots are always the user's choice. They can attach an image, or on desktop browsers press Capture screen, which shows the browser's own screen-sharing prompt and takes one frame. Phones and tablets offer the file picker only, because mobile browsers don't allow screen capture. Before sending, the SDK resizes images to at most 1600 px and re-encodes them as JPEG, which also strips metadata such as a photo's location. The user sees a preview and can remove it.
Offline and retries
If a report can't be sent (no connection, a server error, or a rate limit), the SDK keeps it inlocalStorage (at most 10) and retries with backoff when the browser comes back online and on the next page load, for up to 7 days. A report too large to store is kept without its screenshot. Every report has aclient_report_id, so a retry never creates a second copy. Validation errors (400) are not retried.
Errors
With captureErrors (or data-errors="true"), uncaught exceptions and unhandled promise rejections are sent to the project's Errors tab, separate from user reports. You can send errors you catch yourself too:
try {
await saveCart();
} catch (err) {
window.Bugfed.captureException(err); // counted as handled
showError();
}The SDK keeps requests to a minimum: identical errors on a page are counted rather than resent, a batch goes at most every 10 seconds (and when the page is hidden), a page sends at most 500 occurrences, and once the project's daily error budget is used the SDK stops sending until it resets. Errors from cross-origin scripts without CORS ("Script error.") and from browser extensions are ignored.
On arrival, errors are grouped into issues by their type, their message with numbers and IDs taken out, and the top frames of your own code (file and function, never line numbers, so a rebuild doesn't split an issue). Emails, tokens and query strings are removed from stored samples. A resolved issue that happens again is reopened and marked as regressed. Source maps aren't supported yet, so minified stacks show minified names. When a user sends a report, the last few errors on that page go with it, and the report links to them.
Sessions and impact
With sessionson, each install sends one ping per day saying it was active, carrying the SDK's random install id (which the server hashes per project). That is what makes these possible:
- Error-free installs per release on the Releases tab: the share of installs on a version that hit no error.
- After hitting it on an issue page: how many installs came back within 7 days after first hitting the issue, next to installs active the same days that never hit it.
- New users: how many affected installs hit the issue in their first two days.
These numbers are shown only with at least 30 installs on each side, with their 95% ranges, and a difference is called clear only when the ranges don't overlap. They describe what installs did. They don't show that an error made anyone leave: people who hit an error may use the app differently in other ways. An install is one browser or device, so one person on two devices counts twice.
Flutter
The bugfed_insights package has the same features: a report sheet, screenshots of your own app (no permission needed), error capture through FlutterError.onError and PlatformDispatcher.onError, sessions, and an offline queue. It isn't on pub.dev yet; add it from the repository (see its README).
import 'package:bugfed_insights/bugfed_insights.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Bugfed.init(const BugfedOptions(
key: 'bfp_your_project_key',
appVersion: '1.4.0',
captureErrors: true, // optional
sessions: true, // optional
));
runApp(const BugfedScreenshotScope(child: MyApp()));
}
// From any button:
showBugfedReportSheet(context);
// Your own form:
await Bugfed.submit(BugfedReport(title: title, description: details));
// Errors you catch:
Bugfed.captureException(error, stackTrace);Release builds report frames as package:your_app/...; frames from package:flutter and dart: are treated as framework code when grouping.
AI summaries
The project's Summarytab writes a short summary of the last 14 days: recurring themes, what to look at first, and what changed between releases. It is off until the project owner turns it on in Settings. When someone asks for one, recent report titles and descriptions, error titles and release numbers are sent to our AI provider's API (Google Gemini or Anthropic Claude, on paid tiers that don't use the data for training), without emails or user ids. Every point links to the reports (#) and errors (E) it is based on; anything citing items that don't exist is removed before it's shown. With fewer than 3 reports and no errors, it doesn't summarise. Each project can make 5 summaries a day, and the same data returns the saved summary instead of a new one.
Privacy and data
| Sent with a report | Never collected |
|---|---|
| Title, description and steps the user typed | Page content, form fields, passwords |
| Screenshot, only if the user adds one | Cookies, localStorage contents |
| App version, environment, SDK version | Console logs, network requests |
| Browser, OS, screen size, language | Anything before the user presses Send |
| Page address without query string or hash | IP addresses (rate limits use a keyed hash) |
| Your user id, tags and email, if you set them | Precise device identifiers |
| With captureErrors: error type, message, stack, count | Emails and tokens in messages (removed on arrival) |
| With sessions: one daily ping with a random install id | Anything that links installs across projects |
Reports are kept for the project's retention period (30, 90 or 180 days, 90 by default) and then deleted with their screenshots. Screenshots are stored privately and only shown to your team through short-lived links. Depending on where your users are, your privacy policy may need to say that bug reports are sent to Bugfed.
HTTP API
For Flutter, native iOS and Android, React Native, or your server. One endpoint, JSON in and out, CORS open to any origin, no cookies.
POST https://bugfed.com/api/ingest/v1/reports
Content-Type: application/json
X-Bugfed-Key: bfp_your_project_key
{
"client_report_id": "7d1c3c1e-5a3c-4a8e-9a4f-3c2b1a0f9e8d",
"title": "Checkout button does nothing",
"description": "Tapped Pay, nothing happened.",
"steps": "1. Add an item\n2. Tap Pay",
"email": "optional@example.com",
"user": { "id": "user-42" },
"anonymous_id": "install-uuid",
"app": { "version": "2.4.0", "environment": "production" },
"device": { "platform": "flutter", "os_name": "Android", "os_version": "14",
"model": "Pixel 7", "locale": "en-GB", "viewport": "412x915" },
"page_url": "https://app.example.com/checkout",
"metadata": { "plan": "pro" },
"attachments": [{ "content_type": "image/jpeg", "data": "<base64>" }],
"sdk": { "name": "my-flutter-app", "version": "1.0.0" }
}| Field | Rules |
|---|---|
title | Required. Up to 200 characters. |
description | Optional. Up to 5000 characters. |
steps | Optional. Up to 5000 characters. |
client_report_id | Optional UUID. Send the same one when retrying: the report is stored once. |
email | Optional. Must look like an email, or the request is refused (400). |
attachments | Up to 2 PNG, JPEG or WebP images, base64, 1.5 MB each. The type is read from the file's bytes; anything else is dropped. |
metadata | Up to 20 keys with string, number or boolean values. Nested values are dropped. |
| Everything else | Optional context. Over-long values are shortened, unknown fields ignored. |
| Status | Body error | What to do |
|---|---|---|
| 201 | none | Stored. Body: { id, number, status: "received", attachments: { accepted, rejected } }. |
| 200 | none | Already stored (same client_report_id). Same body as before. |
| 400 | invalid_json, invalid_title, invalid_email | Fix the request. Don't retry as is. |
| 401 | invalid_key | Check the key. It may have been rotated. |
| 403 | ingest_disabled, origin_not_allowed | The project owner switched reports off, or this website isn't on the allowed list. |
| 413 | too_large | Send smaller or fewer screenshots. |
| 429 | rate_limited | Wait for the Retry-After header (seconds), then retry. |
| 503 / 500 | unavailable, server_error | Retry later with backoff. |
curl -X POST https://bugfed.com/api/ingest/v1/reports \
-H "Content-Type: application/json" \
-H "X-Bugfed-Key: bfp_your_project_key" \
-d '{"title":"Test from curl","app":{"version":"1.0.0"}}'Errors and sessions
POST https://bugfed.com/api/ingest/v1/events?key=bfp_your_project_key
Content-Type: text/plain
{
"install": "random-install-id",
"session": true,
"app": { "version": "2.4.0", "environment": "production" },
"device": { "platform": "flutter", "os_name": "android", "os_version": "14" },
"errors": [
{ "type": "StateError", "message": "cart total is null",
"stack": "#0 CartPage.build (package:shop/cart_page.dart:42:7)", "count": 3, "handled": false }
]
}Up to 20 distinct errors per request, 128 KB at most. Answers 202 with { accepted, dropped, session }. A 429 means the project's daily error budget is used up: wait for the Retry-After seconds instead of retrying. Both endpoints take the key as ?key= or the X-Bugfed-Key header; with ?key= and a text/plain body, browsers send no CORS preflight.
Limits
| Limit | Value |
|---|---|
| Reports per project per day | 200 by default, adjustable up to 1000 |
| Reports per project per minute | 30 |
| Reports per device (IP) per project | 10 a minute, 60 a day |
| Request size | 4 MB |
| Error occurrences per project per day | 5,000 by default, adjustable up to 50,000 |
| Distinct error issues per project | 1,000 |
| Installs tracked per project per day | 25,000 |
| AI summaries per project per day | 5 |
| Monthly allowances (projects, reports, errors, screenshots, AI) | Set by your plan: see /pricing#insights |
| Retention | 30, 90 or 180 days |
Platform support
| Platform | Status |
|---|---|
| Web: HTML, React, Next.js, Vue, Svelte… | SDK: widget, errors, sessions (modern browsers with ES modules and <dialog>) |
| Flutter | SDK: report sheet, screenshots, errors, sessions. Not on pub.dev yet |
| React Native | Planned. Use the HTTP API |
| iOS, Android (native) | HTTP API. Native crash capture (signals, ANRs) is planned |
| Server, CLI, desktop | HTTP API |
Versioning
The script URL carries the major version (/sdk/v0/) and the endpoint carries the API version (/api/ingest/v1). Within a major version, changes are backwards compatible: new optional fields, never removed ones. A breaking change gets a new path, and the old one keeps working.