Files
ComfyUI_frontend/src/utils/preloadErrorUtil.test.ts
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

93 lines
2.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { parsePreloadError } from './preloadErrorUtil'
describe('parsePreloadError', () => {
it('parses CSS preload error', () => {
const error = new Error(
'Unable to preload CSS for /assets/vendor-vue-core-abc123.css'
)
const result = parsePreloadError(error)
expect(result.url).toBe('/assets/vendor-vue-core-abc123.css')
expect(result.fileType).toBe('css')
expect(result.chunkName).toBe('vendor-vue-core')
expect(result.message).toBe(error.message)
})
it('parses dynamically imported module error', () => {
const error = new Error(
'Failed to fetch dynamically imported module: https://example.com/assets/vendor-three-def456.js'
)
const result = parsePreloadError(error)
expect(result.url).toBe('https://example.com/assets/vendor-three-def456.js')
expect(result.fileType).toBe('js')
expect(result.chunkName).toBe('vendor-three')
})
it('extracts URL from generic error message', () => {
const error = new Error(
'Something went wrong loading https://cdn.example.com/assets/app-9f8e7d.js'
)
const result = parsePreloadError(error)
expect(result.url).toBe('https://cdn.example.com/assets/app-9f8e7d.js')
expect(result.fileType).toBe('js')
expect(result.chunkName).toBe('app')
})
it('returns null url when no URL found', () => {
const error = new Error('Something failed')
const result = parsePreloadError(error)
expect(result.url).toBeNull()
expect(result.fileType).toBe('unknown')
expect(result.chunkName).toBeNull()
})
it('detects font file types', () => {
const error = new Error(
'Unable to preload CSS for /assets/inter-abc123.woff2'
)
const result = parsePreloadError(error)
expect(result.fileType).toBe('font')
})
it('detects image file types', () => {
const error = new Error('Unable to preload CSS for /assets/logo-abc123.png')
const result = parsePreloadError(error)
expect(result.fileType).toBe('image')
})
it('handles mjs extension', () => {
const error = new Error(
'Failed to fetch dynamically imported module: /assets/chunk-abc123.mjs'
)
const result = parsePreloadError(error)
expect(result.fileType).toBe('js')
})
it('handles URLs with query parameters', () => {
const error = new Error(
'Unable to preload CSS for /assets/style-abc123.css?v=2'
)
const result = parsePreloadError(error)
expect(result.url).toBe('/assets/style-abc123.css?v=2')
expect(result.fileType).toBe('css')
})
it('extracts chunk name from filename without hash', () => {
const error = new Error(
'Failed to fetch dynamically imported module: /assets/index.js'
)
const result = parsePreloadError(error)
expect(result.chunkName).toBe('index')
})
})