diff --git a/package.json b/package.json
index bf2895883b..2e331998a8 100644
--- a/package.json
+++ b/package.json
@@ -235,6 +235,7 @@
"react-responsive": "^10.0.1",
"react-textarea-autosize": "^8.5.3",
"setimmediate": "^1.0.5",
+ "slugify": "^1.6.9",
"sonner": "^2.0.7",
"sonner-native": "^0.21.0",
"tippy.js": "^6.3.7",
diff --git a/src/components/SendErrorReportDialog.tsx b/src/components/SendErrorReportDialog.tsx
new file mode 100644
index 0000000000..a677f54e03
--- /dev/null
+++ b/src/components/SendErrorReportDialog.tsx
@@ -0,0 +1,108 @@
+import {useState} from 'react'
+import {View} from 'react-native'
+import {useLingui} from '@lingui/react/macro'
+import {useMutation} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {sendErrorReport} from '#/logger/reporting/sendErrorReport'
+import {useSession} from '#/state/session'
+import {atoms as a, web} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import * as TextField from '#/components/forms/TextField'
+import * as Toast from '#/components/Toast'
+import {Text} from '#/components/Typography'
+
+export function SendErrorReportDialog({
+ control,
+}: {
+ control: Dialog.DialogControlProps
+}) {
+ return (
+
+
+
+
+
+ )
+}
+
+function SendErrorReportDialogInner() {
+ const {t: l} = useLingui()
+ const control = Dialog.useDialogContext()
+ const {currentAccount} = useSession()
+ const [title, setTitle] = useState('')
+ const [description, setDescription] = useState('')
+
+ const {mutate: onSubmit, isPending} = useMutation({
+ mutationFn: async () => {
+ sendErrorReport({
+ title,
+ description,
+ handle: currentAccount?.handle ?? '',
+ })
+ },
+ onSuccess: () => {
+ control.close(() => {
+ Toast.show(l`Report sent`)
+ })
+ },
+ onError: error => {
+ logger.error('Error sending user report', {safeMessage: error})
+ Toast.show(l`Failed to send report`, {type: 'error'})
+ },
+ })
+
+ const canSubmit = title.trim().length > 0 && !isPending
+
+ return (
+
+
+
+ {l`Send error report`}
+
+
+
+ {l`Title`}
+
+ setTitle(value.slice(0, 100))}
+ />
+
+
+
+ {l`Description`}
+ setDescription(value.slice(0, 1000))}
+ />
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/logger/reporting/sendErrorReport.ts b/src/logger/reporting/sendErrorReport.ts
new file mode 100644
index 0000000000..e0159c1703
--- /dev/null
+++ b/src/logger/reporting/sendErrorReport.ts
@@ -0,0 +1,30 @@
+import slugify from 'slugify'
+
+import {getEntries} from '#/logger/logDump'
+import {Sentry} from '#/logger/sentry/lib'
+
+export function sendErrorReport({
+ title,
+ description,
+ handle,
+}: {
+ title: string
+ description: string
+ handle: string
+}) {
+ const name = slugify(title, {lower: true, strict: true}).slice(0, 32)
+ Sentry.withScope(scope => {
+ scope.addAttachment({
+ filename: name + '.json',
+ data: JSON.stringify(getEntries()),
+ contentType: 'application/json',
+ // mimetype: 'application/json', // need to update Sentry
+ })
+ scope.setExtras({
+ title,
+ description,
+ handle,
+ })
+ scope.captureMessage(`[USER REPORT] ${handle} ${name}`, 'error')
+ })
+}
diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx
index c605a48f0b..9164f5ce0f 100644
--- a/src/screens/Settings/AboutSettings.tsx
+++ b/src/screens/Settings/AboutSettings.tsx
@@ -13,12 +13,15 @@ import {type CommonNavigatorParams} from '#/lib/routes/types'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
import {BroomSparkle_Stroke2_Corner2_Rounded as BroomSparkleIcon} from '#/components/icons/BroomSparkle'
+import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines'
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/icons/Newspaper'
import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
+import * as Prompt from '#/components/Prompt'
+import {SendErrorReportDialog} from '#/components/SendErrorReportDialog'
import * as Toast from '#/components/Toast'
import {getDeviceId} from '#/analytics/identifiers'
import * as env from '#/env'
@@ -32,6 +35,7 @@ export function AboutSettingsScreen({}: Props) {
const {_, i18n} = useLingui()
const [devModeEnabled, setDevModeEnabled] = useDevMode()
const [demoModeEnabled, setDemoModeEnabled] = useDemoMode()
+ const sendErrorReportControl = Prompt.usePromptControl()
const {mutate: onClearImageCache, isPending: isClearingImageCache} =
useMutation({
@@ -109,6 +113,14 @@ export function AboutSettingsScreen({}: Props) {
System log
+ sendErrorReportControl.open()}
+ label={_(msg`Send error report`)}>
+
+
+ Send error report
+
+
{IS_NATIVE && (
onClearImageCache()}
@@ -182,6 +194,7 @@ export function AboutSettingsScreen({}: Props) {
)}
+
)
}
diff --git a/yarn.lock b/yarn.lock
index 86ff68e803..609a88acec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15119,6 +15119,11 @@ slugify@^1.3.4, slugify@^1.6.6:
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
+slugify@^1.6.9:
+ version "1.6.9"
+ resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.9.tgz#610957dea21e56b65e3a153215ef7b265715c8e8"
+ integrity sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==
+
sockjs@^0.3.24:
version "0.3.24"
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"