User feedback
User feedback closes the loop between "something broke" and "here is what the person was actually trying to do". Errora accepts it from three places and funnels all of them into one inbox, linked to the error the user saw.
| Source | Stored source | Who sends it |
|---|---|---|
| Feedback widget | widget / new_feedback_envelope | The browser SDK's feedbackIntegration() |
| Crash-report modal | crash_report_embed_form | Sentry.showReportDialog({ eventId }) |
| User report API | user_report_envelope | Your backend, or the legacy user_report envelope item |
Pick whichever fits. Most teams run the widget everywhere and the crash-report modal on their error page.
Option 1 — the feedback widget
The widget is a floating button that opens a form. It is part of the official Sentry browser SDK, so there is nothing extra to install.
Setup
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://<public_key>@<your-errora-host>/<project_id>",
environment: "production",
release: "web@1.4.2",
integrations: [
Sentry.feedbackIntegration({
colorScheme: "system", // "light" | "dark" | "system"
showBranding: false,
autoInject: true, // render the floating button
showName: true,
showEmail: true,
isNameRequired: false,
isEmailRequired: false,
// Localized copy — the widget ships English defaults.
triggerLabel: "Send feedback",
formTitle: "Send feedback",
messagePlaceholder: "What happened?",
submitButtonLabel: "Send",
successMessageText: "Thank you!",
}),
],
});
That is the whole setup. Submissions arrive in Feedback in the dashboard within a second or two.
Attaching it to your own button
Turn off auto-injection and open the form yourself:
const feedback = Sentry.feedbackIntegration({ autoInject: false });
document.querySelector("#feedback-button").addEventListener("click", async () => {
const form = await feedback.createForm();
form.appendToDom();
form.open();
});
Or attach the widget to an existing element:
const unsubscribe = feedback.attachTo(document.querySelector("#feedback-button"), {
formTitle: "Report a problem",
});
Sending feedback without any UI
If you already have your own form, send its contents straight through the SDK:
Sentry.captureFeedback({
name: "Nima",
email: "nima@example.com",
message: "The invoice PDF is blank on Safari.",
associatedEventId: lastEventId, // optional — links the feedback to that error
});
What Errora stores
The widget sends a feedback envelope item whose payload sits in contexts.feedback.
Errora keeps:
| Field | Notes |
|---|---|
message | The feedback text (max 4096 chars) |
name, contact_email | Whatever the form collected |
url | The page the user was on |
associated_event_id | The error the user saw; used to link the feedback to its issue |
replay_id | Links to the session replay, when replay is on |
environment, release, platform | From the SDK's scope |
user_id, user_ip, user_agent | Only when the SDK sends them |
source, status, created_at | Set by Errora |
Feedback that names an associated_event_id is attached to that event's issue, so the
issue page shows what the user said about the exact error.
Option 2 — the crash-report modal
The crash-report modal is the classic "It looks like we're having issues" dialog you show after catching an error.
Sentry.init({
dsn: "https://<public_key>@<your-errora-host>/<project_id>",
beforeSend(event, hint) {
// Only prompt for real, user-visible failures.
if (event.exception && event.event_id) {
Sentry.showReportDialog({ eventId: event.event_id });
}
return event;
},
});
Or explicitly, wherever you handle a failure:
const eventId = Sentry.captureException(err);
Sentry.showReportDialog({
eventId,
title: "مشکلی پیش آمد",
subtitle: "تیم ما مطلع شد.",
subtitle2: "اگر مایلید کمک کنید، بنویسید چه اتفاقی افتاد.",
labelName: "نام",
labelEmail: "ایمیل",
labelComments: "چه اتفاقی افتاد؟",
labelSubmit: "ارسال گزارش",
labelClose: "بستن",
user: { name: "Nima", email: "nima@example.com" },
});
How it works
The SDK injects a <script> pointing at Errora's embed endpoint:
GET https://<host>/api/embed/error-page/?dsn=<dsn>&eventId=<event_id> → the dialog's JavaScript
POST https://<host>/api/embed/error-page/?dsn=<dsn>&eventId=<event_id> → stores the report
- Authentication is the DSN public key, not a session — the dialog runs on your users' pages. It is a public write endpoint by design.
- Every label is a query parameter. Any key in the dialog's copy (
title,subtitle,subtitle2,labelName,labelEmail,labelComments,labelClose,labelSubmit,errorGeneric,errorFormEntry,successMessage) can be overridden — which is how you ship a Persian dialog. - Rate limit: 10 submissions per client IP per minute.
- The report is linked to the event id you passed, and therefore to its issue.
Server-rendered pages
You don't need the SDK loaded to show the dialog — render the script tag yourself on your error page:
<script
src="https://<host>/api/embed/error-page/?dsn=<dsn>&eventId=<event_id>&labelSubmit=%D8%A7%D8%B1%D8%B3%D8%A7%D9%84">
</script>
Option 3 — the server-side report API
When your backend already captured the exception and you collect the comment yourself (a support form, a Telegram bot, a phone call), post it to the project's feedback endpoint:
curl -X POST "$ERRORA/api/v1/projects/$PROJECT_ID/feedback" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_id": "9a1f4c2e7b8d4a1fa0c3e5d6b7a8c9d0",
"name": "Nima",
"email": "nima@example.com",
"comments": "Checkout froze after I pressed pay."
}'
- Requires a user token with the
issue:writecapability on the project (a JWT access token or a personal access token). event_idaccepts both the 32-hex and dashed UUID forms.commentsis required (max 4096 chars); empty feedback is rejected with400.
Legacy SDKs that send a user_report envelope item land here too, with no extra work.
Working the inbox
Feedback in the sidebar is the inbox. Each row shows the message, who sent it (or anonymous), the source, the page URL, the linked issue and the time.
You can:
- Search the message text (
q), filter by status, environment, and only feedback linked to an issue (has_issue), over a time period. - Change status —
unresolved→resolved, or markspam. - Open the linked issue to see the stack trace behind the complaint.
- Open the session replay, when the feedback carries a
replay_id. - Delete a submission.
Headline counters (total, unresolved, linked to an issue) sit at the top of the page.
API
GET /api/v1/projects/{project_id}/feedback?q=&status=&environment=&has_issue=&stats_period=24h&limit=25&offset=0
POST /api/v1/projects/{project_id}/feedback # the report API above
GET /api/v1/projects/{project_id}/feedback/summary
GET /api/v1/projects/{project_id}/feedback/{id}
PATCH /api/v1/projects/{project_id}/feedback/{id} # {"status": "resolved" | "unresolved" | "spam"}
DELETE /api/v1/projects/{project_id}/feedback/{id}
GET /api/v1/issues/{issue_id}/feedback # everything said about one issue
Getting notified
Feedback fires the feedback.received event, so you can route it to any channel —
Telegram, Bale, Mattermost, email, SMS or a webhook:
- Settings → Webhooks → create a channel (see Alerts).
- Add an alert rule for New user feedback, scoped to the project.
A useful template for a chat channel:
📣 {{ title }}
{{ body }}
{{ url }}
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Widget button never appears | feedbackIntegration() missing from integrations, or autoInject: false with no attachTo/createForm call |
| Widget submits but nothing arrives | The DSN is wrong, or an ad blocker ate the request — check the browser network tab for the envelope call, and consider the SDK's tunnel option |
| Modal shows "unknown error" on submit | You are past the 10-per-minute-per-IP limit, or the eventId is not a valid event id |
| Modal never opens | showReportDialog was called before Sentry.init, or with no eventId on a page where no event was captured |
| Feedback arrives but is not linked to an issue | No associated_event_id/event_id was sent, or the event it names was sampled away |
403 from the report API | The token lacks issue:write on that project |
Everything is anonymous | The SDK sends no user; set Sentry.setUser({...}) or turn on showEmail/showName in the widget |