In-Product Support Reports¶
Last reviewed: 2026-08-22
Reference for the support_reports subsystem — the in-product route by which a member reports a problem, and the queue in which staff triage it. Written by reading SupportReportController, AdminSupportReportController, SupportReportNotificationService, the routes file and the table definition. The source remains authoritative.
Tenant-scoped throughout.
This subsystem previously had no page in docs/, which meant anyone working from the documentation concluded the platform had no support intake. It has one, and it is more capable than it looks.
Member submission¶
POST /v2/support/reports — authenticated, throttled 10/min.
Member-facing pages on the accessible frontend: /contact and /report-a-problem.
| Field | Rules |
|---|---|
summary |
required, 3–180 chars |
description |
required, 10–5000 chars |
impact |
required, one of blocked, major, minor, cosmetic |
module, route, page_url |
optional context |
sentry_event_id, sentry_issue_url |
optional, if the client already has them |
include_diagnostics |
boolean opt-in |
diagnostics |
optional array, only stored when include_diagnostics is true |
The server additionally records user_agent and a hashed IP (ip_hash), never the address itself. source defaults to in_app and is a 40-char column, so other intake sources can be distinguished.
Response returns the id, reference, status, impact, summary and creation time.
Reference format¶
NXR-YYMMDD-XXXXXX — six random uppercase characters, regenerated on collision against a global-scope-free uniqueness check, backed by a unique index.
Diagnostics are redacted before storage¶
When diagnostics are included they pass through redactDiagnosticValue() before the row is written:
- Keys matching
/(authorization|password|passcode|token|secret|cookie|csrf|session|email|phone|address|credit|card|cvv|iban|sort_code)/iare replaced with[filtered]. - Depth capped at 6, then
[truncated]. - 80 items per level, then
__truncated: true. - Strings capped at 2000 characters.
Stored as {captured_at, payload} in a json_valid-checked column.
Sentry linkage¶
On creation, SupportReportSentryService::captureCreated() captures an event and the returned id is written back to sentry_event_id. sentry_issue_url is stored alongside. There is an index on (tenant_id, sentry_event_id).
This means a member's report usually arrives already joined to the server-side error, and matching a symptom to a Sentry issue by hand is often unnecessary.
The capture is best-effort. It is wrapped in
try/catchand only logs a warning on failure. An emptysentry_event_idtherefore means either no error was captured or the capture itself failed — it is not evidence that nothing went wrong.
Notification¶
SupportReportNotificationService::notifyCreated() emails active tenant admins — admin, tenant_admin, super_admin, god, or is_admin — with a deep link to /admin/support-reports?report={id}. Also best-effort: failure logs a warning and does not block submission.
Staff triage¶
Admin panel: /admin/support-reports.
| Endpoint | Purpose |
|---|---|
GET /v2/admin/support-reports |
Paginated list, newest first, 20/page default, 100 max |
GET /v2/admin/support-reports/stats |
Counts by status, plus unassigned |
GET /v2/admin/support-reports/assignees |
Assignable active admins |
GET /v2/admin/support-reports/{id} |
Single report |
PUT / PATCH /v2/admin/support-reports/{id} |
Update status, assignee, triage notes |
Statuses: open → triaged → resolved → closed, with triaged_at, resolved_at and closed_at timestamps. triage_notes is text, max 10,000. An assignee must be an assignable admin of the report's own tenant, validated on write.
The stats endpoint counts unassigned as assigned_user_id IS NULL AND status IN ('open','triaged') — so resolved and closed reports never appear as a backlog.
Triage is admin-only¶
Every method calls requireAdmin().
This is a deliberate contrast with
safeguarding_reports, where triage is open to brokers and coordinators as well as admins (see../SAFEGUARDING-AND-CONSENT.md). A broker who can act on a safeguarding concern in their own community cannot see the support queue. If a tenant's day-to-day operator is a broker rather than an admin, support reports will queue unattended.
Known gaps¶
Documented so they are not rediscovered as bugs.
-
A member cannot look up their own report. There is one member-facing route — the
POST. There is noGETequivalent, and no member-facing status view. A member receives anNXR-reference they can quote but can never resolve themselves. Comparesafeguarding_reports, which hasGET /v2/caring-community/safeguarding/my-reports. -
No append-only action trail.
triage_notesis a single overwritable text column, and status changes leave only their timestamps. There is no per-action log with actor attribution, so "who reassigned this and why" is not answerable from the record. Bothsafeguarding_reports(safeguarding_report_actions) andmember_vetting_attestationshave the closed-vocabulary append-only pattern worth copying here. -
Only creation is notified. A verified notification exists for report creation, to admins. No status-change notification to the reporting member was found — so a member who reports something and hears nothing has no in-product signal that it was triaged or resolved.
-
impactis member-declared and is not a severity. It records how the problem feels to the reporter, not how the organisation has ranked it. There is no staff-assigned severity or priority column and no due-by field — contrast the severity-drivenreview_due_atonsafeguarding_reports. Any response-time commitment therefore lives outside this table. -
No exchange linkage. Consistent with the wider caveat in
../SAFEGUARDING-AND-CONSENT.md: no reporting system in the platform can reference a time exchange. A member disputing a completed exchange cannot raise it here in a way that points at the record.
If you are extending this area¶
- Add the member read path before adding fields. A reference the member cannot look up is the defect most visible to them.
- Copy the append-only action-log pattern from
safeguarding_report_actionsbefore adding more mutable staff fields. - Do not widen
requireAdmin()casually — but do decide deliberately whether brokers should see this queue, because the safeguarding subsystem already answered that question the other way. - Keep the redaction list in step with any new client-supplied field. The filter is keyed on names, so a new key that carries personal data passes through unless its name matches.
Related¶
../SAFEGUARDING-AND-CONSENT.md— the safeguarding reporting workflow, its severity SLA, and its append-only trail../SENTRY.md— Sentry configurationINTAKE-AND-TRIAGE.md— how a report is worked once it arrives