diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 5c093d9f1..f71cac918 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -25,6 +25,8 @@ jobs:
id: current_version
run: echo ::set-output name=version::$(node -p "require('./package.json').version")
- name: Build project
+ env:
+ SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: |
npm ci
npm run build
diff --git a/browser_tests/dialog.spec.ts b/browser_tests/dialog.spec.ts
index ea856091d..4218f79df 100644
--- a/browser_tests/dialog.spec.ts
+++ b/browser_tests/dialog.spec.ts
@@ -44,6 +44,18 @@ test.describe('Execution error', () => {
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
+
+ test('Can display Issue Report form', async ({ comfyPage }) => {
+ await comfyPage.loadWorkflow('execution_error')
+ await comfyPage.queueButton.click()
+ await comfyPage.nextFrame()
+
+ await comfyPage.page.getByLabel('Help Fix This').click()
+ const issueReportForm = comfyPage.page.getByText(
+ 'Submit Error Report (Optional)'
+ )
+ await expect(issueReportForm).toBeVisible()
+ })
})
test.describe('Missing models warning', () => {
diff --git a/global.d.ts b/global.d.ts
index 41aa0c1ef..a9361846e 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -1 +1,3 @@
declare const __COMFYUI_FRONTEND_VERSION__: string
+declare const __SENTRY_ENABLED__: boolean
+declare const __SENTRY_DSN__: string
diff --git a/package-lock.json b/package-lock.json
index b21c8c7bb..3e60baea0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@comfyorg/comfyui-electron-types": "^0.4.7",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
+ "@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
@@ -4462,6 +4463,96 @@
"string-argv": "~0.3.1"
}
},
+ "node_modules/@sentry-internal/browser-utils": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.48.0.tgz",
+ "integrity": "sha512-pLtu0Fa1Ou0v3M1OEO1MB1EONJVmXEGtoTwFRCO1RPQI2ulmkG6BikINClFG5IBpoYKZ33WkEXuM6U5xh+pdZg==",
+ "dependencies": {
+ "@sentry/core": "8.48.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry-internal/feedback": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.48.0.tgz",
+ "integrity": "sha512-6PwcJNHVPg0EfZxmN+XxVOClfQpv7MBAweV8t9i5l7VFr8sM/7wPNSeU/cG7iK19Ug9ZEkBpzMOe3G4GXJ5bpw==",
+ "dependencies": {
+ "@sentry/core": "8.48.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry-internal/replay": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.48.0.tgz",
+ "integrity": "sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "8.48.0",
+ "@sentry/core": "8.48.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry-internal/replay-canvas": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.48.0.tgz",
+ "integrity": "sha512-LdivLfBXXB9us1aAc6XaL7/L2Ob4vi3C/fEOXElehg3qHjX6q6pewiv5wBvVXGX1NfZTRvu+X11k6TZoxKsezw==",
+ "dependencies": {
+ "@sentry-internal/replay": "8.48.0",
+ "@sentry/core": "8.48.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/browser": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.48.0.tgz",
+ "integrity": "sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==",
+ "dependencies": {
+ "@sentry-internal/browser-utils": "8.48.0",
+ "@sentry-internal/feedback": "8.48.0",
+ "@sentry-internal/replay": "8.48.0",
+ "@sentry-internal/replay-canvas": "8.48.0",
+ "@sentry/core": "8.48.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.48.0.tgz",
+ "integrity": "sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==",
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/@sentry/vue": {
+ "version": "8.48.0",
+ "resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-8.48.0.tgz",
+ "integrity": "sha512-hqm9X7hz1vMQQB1HBYezrDBQihZk6e/MxWIG1wMJoClcBnD1Sh7y+D36UwaQlR4Gr/Ftiz+Bb0DxuAYHoUS4ow==",
+ "dependencies": {
+ "@sentry/browser": "8.48.0",
+ "@sentry/core": "8.48.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "peerDependencies": {
+ "pinia": "2.x",
+ "vue": "2.x || 3.x"
+ },
+ "peerDependenciesMeta": {
+ "pinia": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
diff --git a/package.json b/package.json
index 8324bfb5b..28dcac30e 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"@comfyorg/comfyui-electron-types": "^0.4.7",
"@comfyorg/litegraph": "^0.8.60",
"@primevue/themes": "^4.0.5",
+ "@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
diff --git a/src/components/common/CheckboxGroup.vue b/src/components/common/CheckboxGroup.vue
new file mode 100644
index 000000000..490f54b7f
--- /dev/null
+++ b/src/components/common/CheckboxGroup.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/dialog/content/ExecutionErrorDialogContent.vue b/src/components/dialog/content/ExecutionErrorDialogContent.vue
index 722355333..1c6bbab8f 100644
--- a/src/components/dialog/content/ExecutionErrorDialogContent.vue
+++ b/src/components/dialog/content/ExecutionErrorDialogContent.vue
@@ -5,12 +5,20 @@
:message="props.error.exception_message"
/>
-
+
+
+
+
@@ -18,9 +26,12 @@
-
+
-
{
reportOpen.value = true
}
-const showSendError = isElectron()
-
+const sendReportOpen = ref(false)
+const showSendReport = () => {
+ sendReportOpen.value = true
+}
const toast = useToast()
+const { t } = useI18n()
+
+const stackTraceField = computed(() => {
+ return {
+ label: t('issueReport.stackTrace'),
+ value: 'StackTrace',
+ optIn: true,
+ data: {
+ nodeType: props.error.node_type,
+ stackTrace: props.error.traceback?.join('\n')
+ }
+ }
+})
onMounted(async () => {
try {
diff --git a/src/components/dialog/content/error/ReportIssueButton.vue b/src/components/dialog/content/error/ReportIssueButton.vue
deleted file mode 100644
index 0bf2e7721..000000000
--- a/src/components/dialog/content/error/ReportIssueButton.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
diff --git a/src/components/dialog/content/error/ReportIssuePanel.vue b/src/components/dialog/content/error/ReportIssuePanel.vue
new file mode 100644
index 000000000..12cfd2e8a
--- /dev/null
+++ b/src/components/dialog/content/error/ReportIssuePanel.vue
@@ -0,0 +1,197 @@
+
+
+
+
+ {{ $t('issueReport.submitErrorReport') }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/dialog/content/error/__tests__/ReportIssuePanel.spec.ts b/src/components/dialog/content/error/__tests__/ReportIssuePanel.spec.ts
new file mode 100644
index 000000000..90db6b9ad
--- /dev/null
+++ b/src/components/dialog/content/error/__tests__/ReportIssuePanel.spec.ts
@@ -0,0 +1,230 @@
+// @ts-strict-ignore
+import { createTestingPinia } from '@pinia/testing'
+import { mount } from '@vue/test-utils'
+import Button from 'primevue/button'
+import PrimeVue from 'primevue/config'
+import InputText from 'primevue/inputtext'
+import Panel from 'primevue/panel'
+import Textarea from 'primevue/textarea'
+import Tooltip from 'primevue/tooltip'
+import { beforeAll, describe, expect, it, vi } from 'vitest'
+import { createApp } from 'vue'
+import { createI18n } from 'vue-i18n'
+
+import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
+import enMesages from '@/locales/en/main.json'
+import { DefaultField, ReportField } from '@/types/issueReportTypes'
+
+import ReportIssuePanel from '../ReportIssuePanel.vue'
+
+type ReportIssuePanelProps = {
+ errorType: string
+ defaultFields?: DefaultField[]
+ extraFields?: ReportField[]
+}
+
+const i18n = createI18n({
+ legacy: false,
+ locale: 'en',
+ messages: {
+ en: enMesages
+ }
+})
+
+vi.mock('primevue/usetoast', () => ({
+ useToast: vi.fn(() => ({
+ add: vi.fn()
+ }))
+}))
+
+vi.mock('@/scripts/api', () => ({
+ api: {
+ getLogs: vi.fn().mockResolvedValue('mock logs'),
+ getSystemStats: vi.fn().mockResolvedValue('mock stats'),
+ getSettings: vi.fn().mockResolvedValue('mock settings')
+ }
+}))
+
+vi.mock('@/scripts/app', () => ({
+ app: {
+ graph: {
+ asSerialisable: vi.fn().mockReturnValue({})
+ }
+ }
+}))
+
+vi.mock('@sentry/core', () => ({
+ captureMessage: vi.fn()
+}))
+
+describe('ReportIssuePanel', () => {
+ beforeAll(() => {
+ const app = createApp({})
+ app.use(PrimeVue)
+ })
+
+ const mountComponent = (props: ReportIssuePanelProps, options = {}): any => {
+ return mount(ReportIssuePanel, {
+ global: {
+ plugins: [PrimeVue, createTestingPinia(), i18n],
+ directives: { tooltip: Tooltip },
+ components: { InputText, Button, Panel, Textarea, CheckboxGroup }
+ },
+ props,
+ ...options
+ })
+ }
+
+ it('renders the panel with all required components', () => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+ expect(wrapper.find('.p-panel').exists()).toBe(true)
+ expect(wrapper.findAllComponents(CheckboxGroup).length).toBe(2)
+ expect(wrapper.findComponent(InputText).exists()).toBe(true)
+ expect(wrapper.findComponent(Textarea).exists()).toBe(true)
+ expect(wrapper.findComponent(Button).exists()).toBe(true)
+ })
+
+ it('updates selection when checkboxes are selected', async () => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+ const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
+ await checkboxes?.setValue(['Workflow', 'Logs'])
+ expect(wrapper.vm.selection).toEqual(['Workflow', 'Logs'])
+ })
+
+ it('updates contactInfo when input is changed', async () => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+ const input = wrapper.findComponent(InputText)
+ await input.setValue('test@example.com')
+ expect(wrapper.vm.contactInfo).toBe('test@example.com')
+ })
+
+ it('updates additional details when textarea is changed', async () => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+ const textarea = wrapper.findComponent(Textarea)
+ await textarea.setValue('This is a test detail.')
+ expect(wrapper.vm.details).toBe('This is a test detail.')
+ })
+
+ it('updates contactPrefs when preferences are selected', async () => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+ const preferences = wrapper.findAllComponents(CheckboxGroup).at(1)
+ await preferences?.setValue(['FollowUp'])
+ expect(wrapper.vm.contactPrefs).toEqual(['FollowUp'])
+ })
+
+ it('does not allow submission if the form is empty', async () => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+ await wrapper.vm.reportIssue()
+ expect(wrapper.vm.submitted).toBe(false)
+ })
+
+ it('renders with overridden default fields', () => {
+ const wrapper = mountComponent({
+ errorType: 'Test Error',
+ defaultFields: ['Settings']
+ })
+ const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
+ expect(checkboxes?.props('checkboxes')).toEqual([
+ { label: 'Settings', value: 'Settings' }
+ ])
+ })
+
+ it('renders additional fields when extraFields prop is provided', () => {
+ const extraFields = [
+ { label: 'Custom Field', value: 'CustomField', optIn: true, data: {} }
+ ]
+ const wrapper = mountComponent({ errorType: 'Test Error', extraFields })
+ const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
+ expect(checkboxes?.props('checkboxes')).toContainEqual({
+ label: 'Custom Field',
+ value: 'CustomField'
+ })
+ })
+
+ it('does not submit unchecked fields', async () => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+ const textarea = wrapper.findComponent(Textarea)
+
+ await textarea.setValue('Report with only text but no fields selected')
+ await wrapper.vm.reportIssue()
+
+ const { captureMessage } = (await import('@sentry/core')) as any
+ const captureContext = captureMessage.mock.calls[0][1]
+
+ expect(captureContext.extra.logs).toBeNull()
+ expect(captureContext.extra.systemStats).toBeNull()
+ expect(captureContext.extra.settings).toBeNull()
+ expect(captureContext.extra.workflow).toBeNull()
+ })
+
+ it.each([
+ {
+ checkbox: 'Logs',
+ apiMethod: 'getLogs',
+ expectedKey: 'logs',
+ mockValue: 'mock logs'
+ },
+ {
+ checkbox: 'SystemStats',
+ apiMethod: 'getSystemStats',
+ expectedKey: 'systemStats',
+ mockValue: 'mock stats'
+ },
+ {
+ checkbox: 'Settings',
+ apiMethod: 'getSettings',
+ expectedKey: 'settings',
+ mockValue: 'mock settings'
+ }
+ ])(
+ 'submits (%s) when the (%s) checkbox is selected',
+ async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+
+ const { api } = (await import('@/scripts/api')) as any
+ vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
+
+ const { captureMessage } = await import('@sentry/core')
+
+ // Select the checkbox
+ const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
+ await checkboxes?.vm.$emit('update:modelValue', [checkbox])
+
+ await wrapper.vm.reportIssue()
+ expect(api[apiMethod]).toHaveBeenCalled()
+
+ // Verify the message includes the associated data
+ expect(captureMessage).toHaveBeenCalledWith(
+ 'User reported issue',
+ expect.objectContaining({
+ extra: expect.objectContaining({ [expectedKey]: mockValue })
+ })
+ )
+ }
+ )
+
+ it('submits workflow when the Workflow checkbox is selected', async () => {
+ const wrapper = mountComponent({ errorType: 'Test Error' })
+
+ const { app } = (await import('@/scripts/app')) as any
+ const { captureMessage } = await import('@sentry/core')
+
+ const mockWorkflow = { nodes: [], edges: [] }
+ vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
+
+ // Select the "Workflow" checkbox
+ const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
+ await checkboxes?.vm.$emit('update:modelValue', ['Workflow'])
+
+ await wrapper.vm.reportIssue()
+ expect(app.graph.asSerialisable).toHaveBeenCalled()
+
+ // Verify the message includes the workflow
+ expect(captureMessage).toHaveBeenCalledWith(
+ 'User reported issue',
+ expect.objectContaining({
+ extra: expect.objectContaining({ workflow: mockWorkflow })
+ })
+ )
+ })
+})
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 66b149877..78708f576 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -69,7 +69,18 @@
"command": "Command",
"keybinding": "Keybinding",
"upload": "Upload",
- "export": "Export"
+ "export": "Export",
+ "workflow": "Workflow"
+ },
+ "issueReport": {
+ "submitErrorReport": "Submit Error Report (Optional)",
+ "provideEmail": "Give us your email (Optional)",
+ "provideAdditionalDetails": "Provide additional details (optional)",
+ "stackTrace": "Stack Trace",
+ "systemStats": "System Stats",
+ "contactFollowUp": "Contact me for follow up",
+ "notifyResolve": "Notify me when resolved",
+ "helpFix": "Help Fix This"
},
"color": {
"default": "Default",
diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json
index e67c7da30..66c58091f 100644
--- a/src/locales/fr/main.json
+++ b/src/locales/fr/main.json
@@ -132,7 +132,8 @@
"systemInfo": "Informations système",
"terminal": "Terminal",
"upload": "Téléverser",
- "videoFailedToLoad": "Échec du chargement de la vidéo"
+ "videoFailedToLoad": "Échec du chargement de la vidéo",
+ "workflow": "Flux de travail"
},
"graphCanvasMenu": {
"fitView": "Adapter la vue",
@@ -226,6 +227,16 @@
"systemLocations": "Emplacements système",
"unhandledError": "Erreur inconnue"
},
+ "issueReport": {
+ "contactFollowUp": "Contactez-moi pour un suivi",
+ "helpFix": "Aidez à résoudre cela",
+ "notifyResolve": "Prévenez-moi lorsque résolu",
+ "provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
+ "provideEmail": "Donnez-nous votre email (Facultatif)",
+ "stackTrace": "Trace de la pile",
+ "submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
+ "systemStats": "Statistiques du système"
+ },
"menu": {
"autoQueue": "File d'attente automatique",
"batchCount": "Nombre de lots",
diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json
index 79eae5f5f..a9ad84652 100644
--- a/src/locales/ja/main.json
+++ b/src/locales/ja/main.json
@@ -132,7 +132,8 @@
"systemInfo": "システム情報",
"terminal": "ターミナル",
"upload": "アップロード",
- "videoFailedToLoad": "ビデオの読み込みに失敗しました"
+ "videoFailedToLoad": "ビデオの読み込みに失敗しました",
+ "workflow": "ワークフロー"
},
"graphCanvasMenu": {
"fitView": "ビューに合わせる",
@@ -226,6 +227,16 @@
"systemLocations": "システムの場所",
"unhandledError": "未知のエラー"
},
+ "issueReport": {
+ "contactFollowUp": "フォローアップのために私に連絡する",
+ "helpFix": "これを修正するのを助ける",
+ "notifyResolve": "解決したときに通知する",
+ "provideAdditionalDetails": "追加の詳細を提供する(オプション)",
+ "provideEmail": "あなたのメールアドレスを教えてください(オプション)",
+ "stackTrace": "スタックトレース",
+ "submitErrorReport": "エラーレポートを提出する(オプション)",
+ "systemStats": "システム統計"
+ },
"menu": {
"autoQueue": "自動キュー",
"batchCount": "バッチ数",
diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json
index 2d7b1d4fb..758f69b59 100644
--- a/src/locales/ko/main.json
+++ b/src/locales/ko/main.json
@@ -132,7 +132,8 @@
"systemInfo": "시스템 정보",
"terminal": "터미널",
"upload": "업로드",
- "videoFailedToLoad": "비디오를 로드하지 못했습니다."
+ "videoFailedToLoad": "비디오를 로드하지 못했습니다.",
+ "workflow": "워크플로우"
},
"graphCanvasMenu": {
"fitView": "보기 맞춤",
@@ -226,6 +227,16 @@
"systemLocations": "시스템 위치",
"unhandledError": "알 수 없는 오류"
},
+ "issueReport": {
+ "contactFollowUp": "추적 조사를 위해 연락해 주세요",
+ "helpFix": "이 문제 해결에 도움을 주세요",
+ "notifyResolve": "해결되었을 때 알려주세요",
+ "provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
+ "provideEmail": "이메일을 알려주세요 (선택 사항)",
+ "stackTrace": "스택 추적",
+ "submitErrorReport": "오류 보고서 제출 (선택 사항)",
+ "systemStats": "시스템 통계"
+ },
"menu": {
"autoQueue": "자동 실행 큐",
"batchCount": "배치 수",
diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json
index f615616f1..817e250ca 100644
--- a/src/locales/ru/main.json
+++ b/src/locales/ru/main.json
@@ -132,7 +132,8 @@
"systemInfo": "Информация о системе",
"terminal": "Терминал",
"upload": "Загрузить",
- "videoFailedToLoad": "Не удалось загрузить видео"
+ "videoFailedToLoad": "Не удалось загрузить видео",
+ "workflow": "Рабочий процесс"
},
"graphCanvasMenu": {
"fitView": "Подгонять под выделенные",
@@ -226,6 +227,16 @@
"systemLocations": "Системные места",
"unhandledError": "Неизвестная ошибка"
},
+ "issueReport": {
+ "contactFollowUp": "Свяжитесь со мной для уточнения",
+ "helpFix": "Помочь исправить это",
+ "notifyResolve": "Уведомить меня, когда проблема будет решена",
+ "provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
+ "provideEmail": "Укажите вашу электронную почту (необязательно)",
+ "stackTrace": "Трассировка стека",
+ "submitErrorReport": "Отправить отчет об ошибке (необязательно)",
+ "systemStats": "Статистика системы"
+ },
"menu": {
"autoQueue": "Автоочередь",
"batchCount": "Количество пакетов",
diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json
index 79405c663..d825748e1 100644
--- a/src/locales/zh/main.json
+++ b/src/locales/zh/main.json
@@ -132,7 +132,8 @@
"systemInfo": "系统信息",
"terminal": "终端",
"upload": "上传",
- "videoFailedToLoad": "视频加载失败"
+ "videoFailedToLoad": "视频加载失败",
+ "workflow": "工作流"
},
"graphCanvasMenu": {
"fitView": "适应视图",
@@ -226,6 +227,16 @@
"systemLocations": "系统位置",
"unhandledError": "未知错误"
},
+ "issueReport": {
+ "contactFollowUp": "跟进联系我",
+ "helpFix": "帮助修复这个",
+ "notifyResolve": "解决时通知我",
+ "provideAdditionalDetails": "提供额外的详细信息(可选)",
+ "provideEmail": "提供您的电子邮件(可选)",
+ "stackTrace": "堆栈跟踪",
+ "submitErrorReport": "提交错误报告(可选)",
+ "systemStats": "系统状态"
+ },
"menu": {
"autoQueue": "自动执行",
"batchCount": "批次数量",
diff --git a/src/main.ts b/src/main.ts
index 937d7c0ce..2b6d44e46 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -2,6 +2,7 @@
import '@comfyorg/litegraph/style.css'
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
+import * as Sentry from '@sentry/vue'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
@@ -24,6 +25,17 @@ const ComfyUIPreset = definePreset(Aura, {
const app = createApp(App)
const pinia = createPinia()
+Sentry.init({
+ app,
+ dsn: __SENTRY_DSN__,
+ enabled: __SENTRY_ENABLED__,
+ release: __COMFYUI_FRONTEND_VERSION__,
+ integrations: [],
+ autoSessionTracking: false,
+ defaultIntegrations: false,
+ normalizeDepth: 8,
+ tracesSampleRate: 0
+})
app.directive('tooltip', Tooltip)
app
.use(router)
diff --git a/src/types/issueReportTypes.ts b/src/types/issueReportTypes.ts
new file mode 100644
index 000000000..3ae4f63e9
--- /dev/null
+++ b/src/types/issueReportTypes.ts
@@ -0,0 +1,24 @@
+export type DefaultField = 'Workflow' | 'Logs' | 'SystemStats' | 'Settings'
+
+export interface ReportField {
+ /**
+ * The label of the field, shown next to the checkbox if the field is opt-in.
+ */
+ label: string
+
+ /**
+ * A unique identifier for the field, used internally as the key for this field's value.
+ */
+ value: string
+
+ /**
+ * The data associated with this field, sent as part of the report.
+ */
+ data: Record
+
+ /**
+ * Indicates whether the field requires explicit opt-in from the user
+ * before its data is included in the report.
+ */
+ optIn: boolean
+}
diff --git a/vite.config.mts b/vite.config.mts
index 845ea56c0..7aa9e8e38 100644
--- a/vite.config.mts
+++ b/vite.config.mts
@@ -178,7 +178,15 @@ export default defineConfig({
define: {
__COMFYUI_FRONTEND_VERSION__: JSON.stringify(
process.env.npm_package_version
- )
+ ),
+ __SENTRY_ENABLED__: JSON.stringify(
+ !(
+ process.env.CI === 'true' ||
+ process.env.NODE_ENV === 'development' ||
+ !process.env.SENTRY_DSN
+ )
+ ),
+ __SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || '')
},
resolve: {