Enhance error reporting functionality: add bug report option in settings, update error report modal, and refine feedback descriptions

This commit is contained in:
Mo Tarbin
2026-08-07 20:25:06 -04:00
parent 0215a78404
commit 83a5760959
4 changed files with 64 additions and 16 deletions

View File

@@ -171,7 +171,11 @@
}, },
"feedback": { "feedback": {
"title": "Send Feedback", "title": "Send Feedback",
"description": "Tell us how Donetick is working for you, report a bug, or request a feature." "description": "Tell us how Donetick is working for you or request a feature."
},
"bugReport": {
"title": "Report a Bug",
"description": "Something not working right? Send us the details along with a technical snapshot."
} }
} }
} }

View File

@@ -113,6 +113,9 @@ export const collectErrorReport = async ({ error, errorInfo, reportId }) => {
return { return {
reportId: reportId ?? newReportId(), reportId: reportId ?? newReportId(),
occurredAt: new Date().toISOString(), occurredAt: new Date().toISOString(),
// No error means the user came here deliberately from settings rather than
// off the back of a crash — same diagnostics, different story to tell.
kind: error ? 'crash' : 'bug',
error: describeError(error, errorInfo), error: describeError(error, errorInfo),
runtime: describeRuntime(), runtime: describeRuntime(),
app: context, app: context,
@@ -137,7 +140,11 @@ export const formatErrorReport = report => {
`Report ID: ${report.reportId}`, `Report ID: ${report.reportId}`,
`Time: ${report.occurredAt}`, `Time: ${report.occurredAt}`,
'', '',
`Error: ${error.name}${error.message ? `: ${error.message}` : ''}`, // A user-initiated report has no throw behind it; "Error: Unknown" would
// only be noise in the panel the user is being asked to read.
report.kind === 'bug'
? 'Reported manually (no crash)'
: `Error: ${error.name}${error.message ? `: ${error.message}` : ''}`,
error.status error.status
? `HTTP: ${error.status} ${error.statusText ?? ''}`.trim() ? `HTTP: ${error.status} ${error.statusText ?? ''}`.trim()
: null, : null,
@@ -210,11 +217,14 @@ export const formatErrorReport = report => {
* leaves infrastructure they control, and they see it before it is published. * leaves infrastructure they control, and they see it before it is published.
*/ */
export const buildErrorIssueUrl = ({ description, report }) => { export const buildErrorIssueUrl = ({ description, report }) => {
const title = `[crash] ${ const isBug = report.kind === 'bug'
report.error.message?.slice(0, 80) || const title = isBug
report.error.name || ? `[bug] ${description?.trim().slice(0, 80) || 'Reported from the app'}`
'Unexpected error' : `[crash] ${
}` report.error.message?.slice(0, 80) ||
report.error.name ||
'Unexpected error'
}`
const body = [ const body = [
'### What happened', '### What happened',
description?.trim() || '_no description provided_', description?.trim() || '_no description provided_',
@@ -241,7 +251,7 @@ export const submitErrorReport = async ({
}) => { }) => {
const payload = { const payload = {
source: 'donetick-app', source: 'donetick-app',
kind: 'error-report', kind: report.kind === 'bug' ? 'bug-report' : 'error-report',
reportId: report.reportId, reportId: report.reportId,
description: description?.trim() || null, description: description?.trim() || null,
contactEmail: contactEmail?.trim() || null, contactEmail: contactEmail?.trim() || null,

View File

@@ -100,9 +100,13 @@ const IconHalo = ({ color = 'primary', icon }) => (
* user, everything else gathered automatically. The diagnostics are shown * user, everything else gathered automatically. The diagnostics are shown
* before sending rather than after — people are more willing to send a report * before sending rather than after — people are more willing to send a report
* they can see, and this is the one moment they already distrust the app. * they can see, and this is the one moment they already distrust the app.
*
* Also reached deliberately from settings with no error attached, where the
* same diagnostics back a bug the user noticed but the app never threw on.
*/ */
const ErrorReportModal = ({ error, errorInfo, onClose, open }) => { const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const isBugReport = !error
const [report, setReport] = useState(null) const [report, setReport] = useState(null)
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
@@ -160,7 +164,10 @@ const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
{step === STEP.FORM && ( {step === STEP.FORM && (
<Stack spacing={2}> <Stack spacing={2}>
<Box sx={{ ...enter(0) }}> <Box sx={{ ...enter(0) }}>
<IconHalo icon={<BugReportRounded />} color='danger' /> <IconHalo
icon={<BugReportRounded />}
color={isBugReport ? 'warning' : 'danger'}
/>
</Box> </Box>
<Box sx={{ textAlign: 'center', ...enter(50) }}> <Box sx={{ textAlign: 'center', ...enter(50) }}>
@@ -168,26 +175,33 @@ const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
level='h4' level='h4'
sx={{ fontWeight: 700, letterSpacing: '-0.01em' }} sx={{ fontWeight: 700, letterSpacing: '-0.01em' }}
> >
Report this problem {isBugReport ? 'Report a bug' : 'Report this problem'}
</Typography> </Typography>
<Typography <Typography
level='body-sm' level='body-sm'
sx={{ color: 'text.secondary', mt: 0.5, textWrap: 'pretty' }} sx={{ color: 'text.secondary', mt: 0.5, textWrap: 'pretty' }}
> >
A sentence about what you were doing turns this into something we {isBugReport
can actually fix. ? 'Tell us what went wrong and well attach the technical details for you.'
: 'A sentence about what you were doing turns this into something we can actually fix.'}
</Typography> </Typography>
</Box> </Box>
<FormControl sx={{ ...enter(100) }}> <FormControl sx={{ ...enter(100) }}>
<FormLabel sx={{ fontWeight: 600 }}>What were you doing?</FormLabel> <FormLabel sx={{ fontWeight: 600 }}>
{isBugReport ? 'What went wrong?' : 'What were you doing?'}
</FormLabel>
<Textarea <Textarea
minRows={3} minRows={3}
maxRows={6} maxRows={6}
autoFocus autoFocus
value={description} value={description}
onChange={e => setDescription(e.target.value)} onChange={e => setDescription(e.target.value)}
placeholder='e.g. I tapped a chore in My Chores and the screen went blank' placeholder={
isBugReport
? 'e.g. Completing a chore from the list doesnt update the due date'
: 'e.g. I tapped a chore in My Chores and the screen went blank'
}
/> />
</FormControl> </FormControl>
@@ -279,7 +293,9 @@ const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
size='lg' size='lg'
fullWidth fullWidth
loading={submitting} loading={submitting}
disabled={!report} // A crash report stands on its own; a manual one is only the
// description, so there's nothing to send without it.
disabled={!report || (isBugReport && !description.trim())}
onClick={handleSubmit} onClick={handleSubmit}
> >
Send report Send report
@@ -293,7 +309,7 @@ const ErrorReportModal = ({ error, errorInfo, onClose, open }) => {
underline='hover' underline='hover'
onClick={onClose} onClick={onClose}
> >
Not now {isBugReport ? 'Cancel' : 'Not now'}
</Link> </Link>
</Box> </Box>
</Stack> </Stack>

View File

@@ -1,6 +1,7 @@
import { import {
AccountCircle, AccountCircle,
Api, Api,
BugReport,
ChevronRight, ChevronRight,
Circle, Circle,
Code, Code,
@@ -35,9 +36,11 @@ import {
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useUserProfile } from '../../queries/UserQueries' import { useUserProfile } from '../../queries/UserQueries'
import { isPlusAccount } from '../../utils/Helpers' import { isPlusAccount } from '../../utils/Helpers'
import { isParentUser } from '../../utils/UserHelpers' import { isParentUser } from '../../utils/UserHelpers'
import ErrorReportModal from '../Modals/ErrorReportModal'
import FeedbackModal from '../Modals/FeedbackModal' import FeedbackModal from '../Modals/FeedbackModal'
const SettingsOverview = () => { const SettingsOverview = () => {
@@ -45,6 +48,7 @@ const SettingsOverview = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { data: userProfile } = useUserProfile() const { data: userProfile } = useUserProfile()
const [feedbackOpen, setFeedbackOpen] = useState(false) const [feedbackOpen, setFeedbackOpen] = useState(false)
const [bugReportOpen, setBugReportOpen] = useState(false)
const settingsCards = [ const settingsCards = [
{ {
@@ -133,6 +137,13 @@ const SettingsOverview = () => {
icon: <Feedback />, icon: <Feedback />,
onSelect: () => setFeedbackOpen(true), onSelect: () => setFeedbackOpen(true),
}, },
{
id: 'bugreport',
title: t('overview.sections.bugReport.title'),
description: t('overview.sections.bugReport.description'),
icon: <BugReport />,
onSelect: () => setBugReportOpen(true),
},
] ]
const handleCardClick = setting => { const handleCardClick = setting => {
@@ -387,6 +398,13 @@ const SettingsOverview = () => {
open={feedbackOpen} open={feedbackOpen}
onClose={() => setFeedbackOpen(false)} onClose={() => setFeedbackOpen(false)}
/> />
{/* No error to pass: the report is about something the user saw, not
something the app threw, so the modal collects diagnostics only. */}
<ErrorReportModal
open={bugReportOpen}
onClose={() => setBugReportOpen(false)}
/>
</Container> </Container>
) )
} }