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

  1. Create a project in the dashboard (Insights → New project).
  2. Copy the snippet from the project's Setup tab. It already contains your project key.
  3. Click Send a test report on the Setup tab to check the whole path.
  4. Paste the snippet into your app, open it, click Report a bug and send one. The Setup tab shows Connected.
index.html
<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.

AttributeMeaning
data-keyYour project key. Required for the widget to start.
data-app-versionYour app's version, e.g. 2.4.0. Lets you filter reports by release.
data-environmentproduction (default), staging, or any name up to 32 characters.
data-launcher"false" hides the floating button; open the form with window.Bugfed.open().
data-positionbottom-right (default) or bottom-left.
data-themeauto (default, follows the user's system), light or dark.
data-accent-colorAny 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)

app/layout.tsx
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.

ReportBugButton.tsx
// 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).

OptionDefaultWhat it does
keyrequiredProject key (bfp_…).
appVersionnoneYour release, shown on every report.
environment"production"Separates staging from production reports.
usernone{ id, email }: see Users and tags.
collectUrl"path""path": origin and path. "full": with the query string. false: no address.
screenshotstrueLet users attach or capture a screenshot.
askForEmailtrueShow an optional email field when no user email is known.
metadata{}Tags sent with every report (flat string, number or boolean values).
beforeSendnone(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.textEnglishOverride any label in the form, e.g. for translations.
persisttrueUse localStorage for the install id and the offline queue.
onSubmittednone({ id, number }) => void, called after each accepted report.
captureErrorsfalseSend uncaught errors and unhandled promise rejections. See Errors.
errorSampleRate1Fraction of page loads whose errors are sent (0 to 1).
beforeSendErrornone(error) => error | null. Edit or drop an error before it's sent.
sessionsfalseOne "active today" ping per install per day. See Sessions and impact.
app.js
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:

after sign-in
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.

report.ts
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:

anywhere
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).

lib/main.dart
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 reportNever collected
Title, description and steps the user typedPage content, form fields, passwords
Screenshot, only if the user adds oneCookies, localStorage contents
App version, environment, SDK versionConsole logs, network requests
Browser, OS, screen size, languageAnything before the user presses Send
Page address without query string or hashIP addresses (rate limits use a keyed hash)
Your user id, tags and email, if you set themPrecise device identifiers
With captureErrors: error type, message, stack, countEmails and tokens in messages (removed on arrival)
With sessions: one daily ping with a random install idAnything 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.

request
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" }
}
FieldRules
titleRequired. Up to 200 characters.
descriptionOptional. Up to 5000 characters.
stepsOptional. Up to 5000 characters.
client_report_idOptional UUID. Send the same one when retrying: the report is stored once.
emailOptional. Must look like an email, or the request is refused (400).
attachmentsUp 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.
metadataUp to 20 keys with string, number or boolean values. Nested values are dropped.
Everything elseOptional context. Over-long values are shortened, unknown fields ignored.
StatusBody errorWhat to do
201noneStored. Body: { id, number, status: "received", attachments: { accepted, rejected } }.
200noneAlready stored (same client_report_id). Same body as before.
400invalid_json, invalid_title, invalid_emailFix the request. Don't retry as is.
401invalid_keyCheck the key. It may have been rotated.
403ingest_disabled, origin_not_allowedThe project owner switched reports off, or this website isn't on the allowed list.
413too_largeSend smaller or fewer screenshots.
429rate_limitedWait for the Retry-After header (seconds), then retry.
503 / 500unavailable, server_errorRetry later with backoff.
curl
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

request
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

LimitValue
Reports per project per day200 by default, adjustable up to 1000
Reports per project per minute30
Reports per device (IP) per project10 a minute, 60 a day
Request size4 MB
Error occurrences per project per day5,000 by default, adjustable up to 50,000
Distinct error issues per project1,000
Installs tracked per project per day25,000
AI summaries per project per day5
Monthly allowances (projects, reports, errors, screenshots, AI)Set by your plan: see /pricing#insights
Retention30, 90 or 180 days

Platform support

PlatformStatus
Web: HTML, React, Next.js, Vue, Svelte…SDK: widget, errors, sessions (modern browsers with ES modules and <dialog>)
FlutterSDK: report sheet, screenshots, errors, sessions. Not on pub.dev yet
React NativePlanned. Use the HTTP API
iOS, Android (native)HTTP API. Native crash capture (signals, ANRs) is planned
Server, CLI, desktopHTTP 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.