Frontend error reporting — minimal GlitchTip client¶
1. Problem¶
The frontend shipped a full @sentry/vue integration plus a dead, half-written custom reporter at frontend/scripts/glitchtip.js. Goals: keep crash visibility only (no tracing/replay), shrink to a tiny dependency-free client, and capture every error source. Two things blocked any event from reaching GlitchTip:
- DSN never loaded.
quasar.config.jsloads only.env.local(gitignored) intoprocess.env.APP_*. The DSN had been placed in.env, which neither that loader nor Vite reads, soruntimeConfig.sentryDsnwas empty and init was skipped —throw new Error(...)went nowhere. - The draft client could not ingest. Its envelope omitted the mandatory
{"type":"event"}item-header line, and it discarded the DSN public key during parse, so GlitchTip rejected the request.
2. Solution¶
A module-level singleton client speaking the Sentry envelope protocol (which GlitchTip accepts). No-op without a DSN, so dev/CI stay silent.
New — frontend/src/utils/glitchtip.ts:
initGlitchTip({ dsn, release, environment, ignoreErrors, maxBreadcrumbs }),captureError(error, ctx?),addBreadcrumb(message, category?).- Protocol fixes vs the draft: keeps the public key and sends it as
?sentry_key=(+dsnin the envelope header); emits the 3-line envelope (envelope header / item header / payload); breadcrumbs as{ values: [...] }; minimal Chrome+Firefox stack parser →stacktrace.frames(oldest-first); per-capturemechanism { type, handled }. - Retained from the draft: consecutive-error dedupe, best-effort
localStorageoffline buffer + flush on init,keepalivePOST. - Context parity with the SDK: ships
request.headers["User-Agent"]so GlitchTip derives thebrowser/os/devicetags (+ icons) server-side; sends aculturepanel (locale / timezone / calendar fromIntl) on every event; and auto-records breadcrumbs (fetch— skipping its own ingest POSTs —console.error/warn,ui.click;navigationfromrouter.afterEach). - Non-Error rejections (
Promise.reject({...}), numbers) keep their payload in the title (NonError: Non-Error rejection: …) instead of "Unknown error". Real Errors carry their throw/reject-site stack; synthesized stacks (string / non-Error captures) are flaggedmechanism.synthetic = true— the browser retains no origin trace for a plain-value rejection, so reject with anErrorto keep one.
Rewired — frontend/src/boot/sentry.ts (boot slot name kept as sentry; the APP_SENTRY_DSN env name is wired through runtime.ts, quasar.config.js, docker/entrypoint.sh and Helm — unchanged). Calls initGlitchTip when a DSN is set and wires every source to captureError, keeping the existing ignoreErrors list and user-facing toasts:
| Source | Hook | mechanism |
|---|---|---|
| Vue component errors | app.config.errorHandler | vue |
| Vue Router (chunk load, guards) | router.onError (new) | vue-router |
| Global synchronous / DOM-event | window 'error' (ResizeObserver suppressed) | onerror |
| Unhandled promise rejection | window 'unhandledrejection' | onunhandledrejection (handled:false) |
| ky HTTP 5xx | afterResponse in src/api/http.ts | generic |
Vue component errors also attach a contexts.vue panel (component name, lifecycle hook, depth-1 props — nested values collapse to [Object]/[Array] to dodge circular refs) — the @sentry/vue equivalent. captureError accepts an optional contexts map for this.
router.onError also detects stale route-chunk load failures (cross-engine message match) and shows a single sticky "new version available — reload" toast, so a client holding an old index.html after a deploy can recover in one click instead of hitting a dead navigation. The error is still captured. Strings new_version_available / reload added to src/i18n/common.ts.
Removed: @sentry/vue dependency (package.json) and both its usages (boot/sentry.ts, api/http.ts's lazy captureMessage); deleted the dead scripts/glitchtip.js. No prod Docker/Helm change — docker/entrypoint.sh writes any APP_* var into injectEnv.js generically.
A contexts.trace (trace_id rotated per navigation + span_id) tags every event so errors within one navigation group together (Tier A). No transaction/span events are emitted — GlitchTip's Performance tab stays empty by design. See the header comment in glitchtip.ts for what full performance / distributed tracing would require (out of scope).
Trade-off (in scope): no performance/transaction tracing (only the trace IDs above), no session replay, no server-side source-map frame resolution. Scoped to "errors only".
3. Configuration¶
Dev: cp .env .env.local (DSN must be in .env.local — .env is not loaded). Prod: APP_SENTRY_DSN from the pod env via Helm → injectEnv.js.
4. Verification¶
make type-check(vue-tsc) andnpm run lint— both green; no@sentryimports remain.- Manual (
quasar dev, with the DSN in.env.localand the dev server restarted): from the console callwindow.__gtTest('<kind>')— kinds:throw|reject|reject-nonerror|capture|chunk— to fire each path; each produces aPOST …/api/<projectId>/envelope/?sentry_key=…→ 200 and a visible GlitchTip event with browser/os tags and a breadcrumb trail. (__gtTestis dev-only, stripped from prod. A barethrowtyped at the console REPL does not firewindow.onerrorin WebKit — hencesetTimeoutinside the helper.) Confirm ResizeObserver /AbortError/Failed to fetchare not sent. HTTP 5xx capture needs a backend endpoint that genuinely 500s, called via theapiky instance (4xx is intentionally not reported).