Files
ComfyUI_frontend/src/App.vue
Johnpaul Chiwetelu fcdc08fb27 feat: structured preload error logging with Sentry enrichment (#8928)
## Summary

Add structured preload error logging with Sentry context enrichment and
a user-facing toast notification when chunk loading fails (e.g. after a
deploy with new hashed filenames).

## Changes

- **`parsePreloadError` utility** (`src/utils/preloadErrorUtil.ts`):
Extracts structured info from `vite:preloadError` events — URL, file
type (JS/CSS/unknown), chunk name, and whether it looks like a hash
mismatch.
- **Sentry enrichment** (`src/App.vue`): Sets Sentry context and tags on
preload errors so they are searchable/filterable in the Sentry
dashboard.
- **User-facing toast**: Shows an actionable "please refresh" message
when a preload error occurs, across all distributions (cloud, desktop,
localhost).
- **Capture-phase resource error listener** (`src/App.vue`): Catches
CSS/script load failures that bypass `vite:preloadError` and reports
them to Sentry with the same structured context.
- **Unit tests** (`src/utils/preloadErrorUtil.test.ts`): 9 tests
covering URL parsing, chunk name extraction, hash mismatch detection,
and edge cases.

## Files Changed

| File | What |
|------|------|
| `src/App.vue` | Preload error handler + resource error listener |
| `src/locales/en/main.json` | Toast message string |
| `src/utils/preloadErrorUtil.ts` | `parsePreloadError()` utility |
| `src/utils/preloadErrorUtil.test.ts` | Unit tests |

## Review Focus

- Toast fires for all distributions (cloud/desktop/localhost) —
intentional so all users see stale chunk errors
- `parsePreloadError` is defensive — returns `unknown` for any field it
cannot parse
- Capture-phase listener filters to only `<script>` and `<link
rel="stylesheet">` elements

## References

- [Vite preload error
handling](https://vite.dev/guide/build#load-error-handling)

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
2026-03-13 10:29:33 -07:00

131 lines
3.7 KiB
Vue

<template>
<router-view />
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
</template>
<script setup lang="ts">
import { captureException } from '@sentry/vue'
import BlockUI from 'primevue/blockui'
import { computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { isDesktop } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI } from '@/utils/envUtil'
import { parsePreloadError } from '@/utils/preloadErrorUtil'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
app.extensionManager = useWorkspaceStore()
const conflictDetection = useConflictDetection()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
watch(
isLoading,
(loading, prevLoading) => {
if (prevLoading && !loading) {
document.getElementById('splash-loader')?.remove()
}
},
{ flush: 'post' }
)
const showContextMenu = (event: MouseEvent) => {
const { target } = event
switch (true) {
case target instanceof HTMLTextAreaElement:
case target instanceof HTMLInputElement && target.type === 'text':
// TODO: Context input menu explicitly for text input
electronAPI()?.showContextMenu({ type: 'text' })
return
}
}
function handleResourceError(url: string, tagName: string) {
console.error('[resource:loadError]', { url, tagName })
if (__DISTRIBUTION__ === 'cloud') {
captureException(new Error(`Resource load failed: ${url}`), {
tags: {
error_type: 'resource_load_error',
tag_name: tagName
}
})
}
}
onMounted(() => {
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
if (isDesktop) {
document.addEventListener('contextmenu', showContextMenu)
}
// Handle preload errors that occur during dynamic imports (e.g., stale chunks after deployment)
// See: https://vite.dev/guide/build#load-error-handling
window.addEventListener('vite:preloadError', (event) => {
event.preventDefault()
const info = parsePreloadError(event.payload)
console.error('[vite:preloadError]', {
url: info.url,
fileType: info.fileType,
chunkName: info.chunkName,
message: info.message
})
if (__DISTRIBUTION__ === 'cloud') {
captureException(event.payload, {
tags: {
error_type: 'vite_preload_error',
file_type: info.fileType,
chunk_name: info.chunkName ?? undefined
},
contexts: {
preload: {
url: info.url,
fileType: info.fileType,
chunkName: info.chunkName
}
}
})
}
useToastStore().add({
severity: 'error',
summary: t('g.preloadErrorTitle'),
detail: t('g.preloadError'),
life: 10000
})
})
// Capture resource load failures (CSS, scripts) in non-localhost distributions
if (__DISTRIBUTION__ !== 'localhost') {
window.addEventListener(
'error',
(event) => {
const target = event.target
if (target instanceof HTMLScriptElement) {
handleResourceError(target.src, 'script')
} else if (
target instanceof HTMLLinkElement &&
target.rel === 'stylesheet'
) {
handleResourceError(target.href, 'link')
}
},
true
)
}
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()
})
</script>