If your app already has a "Report a problem" dialog — built with your own components, in your own design — you can keep it. Call submit() with whatever your form collected, and the report arrives in BugDump with the same diagnostics the widget would have attached. BugDump's own panel never appears.
Your form or BugDump's panel?
There are two ways to run BugDump without its floating button:
Trigger | Form | Screenshot, recording and annotation tools | |
|---|---|---|---|
| Yours | BugDump's panel | Yes |
| Yours | Yours | No — pass any files you collect yourself |
Pick open() when you want BugDump's capture tools and don't mind its panel. Pick submit() when the form has to look and behave like the rest of your app. See Choosing an installation method for the open() route.
Set up
Install the SDK and initialize it once, as early as you can, with hideButton: true:
npm install @bugdump/sdk
import { Bugdump } from '@bugdump/sdk';
const bugdump = Bugdump.init({
apiKey: 'your-widget-key',
hideButton: true,
});
Initialize at app start rather than when the dialog opens: console and network capture only cover what happened after init(), and the error your user is about to describe has usually already happened.
submit() needs version 0.0.40 or later.
Send a report
const { taskPublicId } = await bugdump.submit({
description: 'I clicked Pay and nothing happened',
reporterName: 'Jane Doe',
reporterEmail: 'jane@example.com',
priority: 'High',
files: Array.from(fileInput.files ?? []),
});
Only description is required. submit() resolves with { id, taskId, taskPublicId } — taskPublicId is the task number your team sees on the board, handy for a "Thanks, your report is #42" message.
Option | Type | Description |
|---|---|---|
|
| Required. What happened, up to 10,000 characters. |
|
| Falls back to the name passed to |
|
| Must be a valid email address. Falls back to the email passed to |
|
| A priority label as your project defines it, such as |
|
| Attach the report to an existing task by its number instead of creating a new task. Falls back to |
|
| Uploaded as attachments. A |
The default priority labels are Low, Medium, High and Critical. If you renamed yours, use your labels — see Customizing priorities.
What is attached for you
You send the fields above; the SDK adds the rest, exactly as it does for widget reports:
console logs and network requests, after your
consoleFilterandnetworkFilterthe user's recent clicks, typing and navigation
performance timings
the page URL, referrer, browser, and viewport
the user from
identify()and the data fromsetContext()a session replay, when the
sessionReplayfeature is on (the default, on every plan)
After a successful report the console, network and action buffers start fresh, so the next report does not repeat them. To keep something out of reports, use Filtering console and network capture and Feature toggles and handling sensitive data — they apply to submit() too.
React
With @bugdump/react, put hideButton: true in the provider's config and send from your form through the hook:
import { useState, type FormEvent } from 'react';
import { BugdumpApiError } from '@bugdump/sdk';
import { useBugdump } from '@bugdump/react';
// Rendered under <BugdumpProvider config={{ apiKey: import.meta.env.VITE_BUGDUMP_KEY, hideButton: true }}>
export function ReportForm({ onSent }: { onSent: (taskNumber: number) => void }) {
const bugdump = useBugdump();
const [sending, setSending] = useState(false);
const [error, setError] = useState('');
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!bugdump) return;
const data = new FormData(event.currentTarget);
setSending(true);
setError('');
try {
const { taskPublicId } = await bugdump.submit({
description: String(data.get('description')),
reporterEmail: String(data.get('email')) || undefined,
files: data.getAll('files').filter((f): f is File => f instanceof File && f.size > 0),
});
onSent(taskPublicId);
} catch (e) {
setError(e instanceof BugdumpApiError ? e.message : 'Could not send the report.');
} finally {
setSending(false);
}
}
return (
<form onSubmit={handleSubmit}>
<textarea name="description" required maxLength={10000} />
<input name="email" type="email" />
<input name="files" type="file" multiple />
{error && <p role="alert">{error}</p>}
<button disabled={!bugdump || sending}>{sending ? 'Sending…' : 'Send report'}</button>
</form>
);
}
useBugdump() is null until the widget has started, so the button waits for it. Keep the provider at the root, so capture starts with the app and already covers what happened before the user opened your dialog.
The f.size > 0 filter matters: an empty file input still submits one nameless, empty file.
Angular
Initialize in a service and inject it into your root component, so capture starts when the app does:
import { Injectable, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { Bugdump, type SubmitOptions } from '@bugdump/sdk';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class BugReportService {
private readonly bugdump = isPlatformBrowser(inject(PLATFORM_ID))
? Bugdump.init({ apiKey: environment.bugdumpKey, hideButton: true })
: null;
submit(options: SubmitOptions) {
if (!this.bugdump) {
return Promise.reject(new Error('Bug reports can only be sent from the browser.'));
}
return this.bugdump.submit(options);
}
}
Then call it from the form in your own dialog:
import { Component, inject, output, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { BugdumpApiError } from '@bugdump/sdk';
import { BugReportService } from './bug-report.service';
@Component({
selector: 'app-report-form',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="send()">
<textarea formControlName="description"></textarea>
<input formControlName="email" type="email" />
<input type="file" multiple (change)="onFiles($event)" />
@if (error()) {
<p role="alert">{{ error() }}</p>
}
<button type="submit" [disabled]="form.invalid || sending()">Send report</button>
</form>
`,
})
export class ReportFormComponent {
private readonly reports = inject(BugReportService);
private files: File[] = [];
/** Emits the new task's number, so the dialog can close and thank the user. */
readonly sent = output<number>();
readonly form = inject(FormBuilder).nonNullable.group({
description: ['', [Validators.required, Validators.maxLength(10000)]],
email: ['', Validators.email],
});
readonly sending = signal(false);
readonly error = signal('');
onFiles(event: Event) {
this.files = Array.from((event.target as HTMLInputElement).files ?? []);
}
async send() {
const { description, email } = this.form.getRawValue();
this.sending.set(true);
this.error.set('');
try {
const { taskPublicId } = await this.reports.submit({
description,
reporterEmail: email || undefined,
files: this.files,
});
this.sent.emit(taskPublicId);
} catch (e) {
this.error.set(e instanceof BugdumpApiError ? e.message : 'Could not send the report.');
} finally {
this.sending.set(false);
}
}
}
Script tag
submit() returns a promise, so it is not one of the queued bugdump('...') commands. Call it on the instance once the script has loaded:
async function sendReport(description) {
const bugdump = window.Bugdump?.getInstance();
if (!bugdump) throw new Error('BugDump has not loaded yet.');
const { taskPublicId } = await bugdump.submit({ description });
return taskPublicId;
}
This needs the SDK script to have loaded, so keep your send button disabled until window.Bugdump exists, or load the SDK from npm instead.
Errors and limits
submit() rejects with a BugdumpApiError. Its statusCode is the HTTP status and its code is the error code:
Status | Code | Meaning |
|---|---|---|
400 |
| A field is invalid — most often a malformed email or a description over 10,000 characters. |
400 |
| A file is over your plan's per-report media size, or storage is full. |
401 |
| The widget key is wrong, revoked, or expired. |
403 |
| The page's origin is not in the project's allowed origins. |
403 |
| The project has used its monthly widget reports. It accepts reports again at 00:00 UTC on the 1st. |
403 |
| The project is on Free and holds 10,000 tasks. |
429 |
| Too many requests this minute. |
500 |
| The report could not be filed — including when |
Neither REPORT_QUOTA_EXCEEDED nor TASK_LIMIT_REACHED is worth retrying. Tell the user that reporting is paused, as the widget does with "This site is not accepting feedback right now. Please try again later.":
if (e instanceof BugdumpApiError && (e.code === 'REPORT_QUOTA_EXCEEDED' || e.code === 'TASK_LIMIT_REACHED')) {
setError('Reporting is paused right now. Please try again later.');
}
Every file is uploaded in its own request before the report is sent, and so is the session replay, so a report with two files is up to four requests. All of them count toward your project's per-minute limit — see Plans, limits, and usage. Disable your send button while submit() runs, so a double click does not file the report twice.
submit() only runs in a browser. In server-rendered code, call it from an event handler or a browser-only lifecycle hook — never during rendering.
Without the SDK
If you cannot load the SDK — a native app, a backend job — you can file a report with one HTTP request. You lose the automatic telemetry: the report contains only what you send.
curl -X POST https://api.bugdump.com/api/widget/v1/reports \
-H "X-API-Key: your-widget-key" \
-H "Content-Type: application/json" \
-d '{"description": "Export to CSV times out", "reporterEmail": "jane@example.com", "priority": "High"}'
Only description is required. The body also accepts reporterName, reporterExternalId, taskPublicId, pageUrl, and customContext (any JSON object). A successful request returns 201 with { "id", "taskId", "taskPublicId" }. The same rate limits and monthly report limit apply, and requests from a browser must come from an allowed origin.
The widget key is public by design — it can only file reports. See Managing API keys safely.
See it working
The bugdump-custom-form-react repository is a small Vite + React app with its own dialog, a priority picker, and file attachments. Clone it, paste your widget key, and send yourself a report.