mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-16 10:29:36 +00:00
## 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>
78 lines
2.1 KiB
TypeScript
78 lines
2.1 KiB
TypeScript
type PreloadFileType = 'js' | 'css' | 'font' | 'image' | 'unknown'
|
|
|
|
interface PreloadErrorInfo {
|
|
url: string | null
|
|
fileType: PreloadFileType
|
|
chunkName: string | null
|
|
message: string
|
|
}
|
|
|
|
const CSS_PRELOAD_RE = /Unable to preload CSS for (.+)/
|
|
const JS_DYNAMIC_IMPORT_RE =
|
|
/Failed to fetch dynamically imported module:\s*(.+)/
|
|
const URL_FALLBACK_RE = /https?:\/\/[^\s"')]+/
|
|
|
|
const FONT_EXTENSIONS = new Set(['woff', 'woff2', 'ttf', 'otf', 'eot'])
|
|
const IMAGE_EXTENSIONS = new Set([
|
|
'png',
|
|
'jpg',
|
|
'jpeg',
|
|
'gif',
|
|
'svg',
|
|
'webp',
|
|
'avif',
|
|
'ico'
|
|
])
|
|
|
|
function extractUrl(message: string): string | null {
|
|
const cssMatch = message.match(CSS_PRELOAD_RE)
|
|
if (cssMatch) return cssMatch[1].trim()
|
|
|
|
const jsMatch = message.match(JS_DYNAMIC_IMPORT_RE)
|
|
if (jsMatch) return jsMatch[1].trim()
|
|
|
|
const fallbackMatch = message.match(URL_FALLBACK_RE)
|
|
if (fallbackMatch) return fallbackMatch[0]
|
|
|
|
return null
|
|
}
|
|
|
|
function detectFileType(url: string): PreloadFileType {
|
|
const pathname = new URL(url, 'https://cloud.comfy.org').pathname
|
|
const ext = pathname.split('.').pop()?.toLowerCase()
|
|
if (!ext) return 'unknown'
|
|
|
|
// Strip query params from extension
|
|
const cleanExt = ext.split('?')[0]
|
|
|
|
if (cleanExt === 'js' || cleanExt === 'mjs') return 'js'
|
|
if (cleanExt === 'css') return 'css'
|
|
if (FONT_EXTENSIONS.has(cleanExt)) return 'font'
|
|
if (IMAGE_EXTENSIONS.has(cleanExt)) return 'image'
|
|
return 'unknown'
|
|
}
|
|
|
|
function extractChunkName(url: string): string | null {
|
|
const pathname = new URL(url, 'https://cloud.comfy.org').pathname
|
|
const filename = pathname.split('/').pop()
|
|
if (!filename) return null
|
|
|
|
// Strip extension
|
|
const nameWithoutExt = filename.replace(/\.[^.]+$/, '')
|
|
// Strip hash suffix (e.g. "vendor-vue-core-abc123" -> "vendor-vue-core")
|
|
const withoutHash = nameWithoutExt.replace(/-[a-f0-9]{6,}$/, '')
|
|
return withoutHash || null
|
|
}
|
|
|
|
export function parsePreloadError(error: Error): PreloadErrorInfo {
|
|
const message = error.message || String(error)
|
|
const url = extractUrl(message)
|
|
|
|
return {
|
|
url,
|
|
fileType: url ? detectFileType(url) : 'unknown',
|
|
chunkName: url ? extractChunkName(url) : null,
|
|
message
|
|
}
|
|
}
|