Conversation
…stion - error_log_service with never-raising dedup record, list/stats/resolve/delete - /api/v1/error-logs admin blueprint and rate-limited /client endpoint - 500 errorhandler now records unhandled exceptions without changing the response - 12 tests covering dedup, filters, resolution, client validation, 500 hook
ErrorBoundary now fire-and-forgets crashes to /error-logs/client via raw fetch so reporting works with an expired token and can never throw
…l drawer Stat strip, backend/frontend + resolution SegControls, paginated DataTable, 640px drawer with traceback/context and resolve/delete actions
Separate user docs, extension-author docs and internal working docs; note that Swagger UI at /api/v1/docs is the source of truth over API.md
There was a problem hiding this comment.
Pull request overview
Adds a first-party, deduplicated error tracker to ServerKit, spanning backend 500s (captured from the Flask 500 handler) and frontend React crashes (reported from ErrorBoundary), with an admin-only API and a new “Errors” tab under Monitoring to browse, resolve, and delete tracked issues.
Changes:
- Backend: new
error_logspersistence (model + Alembic migration), recording/dedup service, admin browsing endpoints, and 500-handler hook to record uncaught exceptions. - Frontend: new Monitoring → Errors tab/page with server-side pagination + stats strip + detail drawer, plus client-side crash reporting via
ErrorBoundaryand a dedicated ingestion API method. - Docs/UI wiring: docs index updates, new styles, new route/tab registration, and version bump.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
VERSION |
Bumps release version to include the new feature. |
frontend/src/styles/pages/_errors.scss |
Adds page-specific styling for the Errors list and detail drawer. |
frontend/src/styles/main.scss |
Imports the new Errors page SCSS. |
frontend/src/services/api/index.js |
Mixes errorLogs API methods into ApiService. |
frontend/src/services/api/errorLogs.js |
Adds admin error-log CRUD + public-ish client ingestion request. |
frontend/src/pages/Errors.jsx |
Implements Monitoring → Errors list page, filters, stats, and drawer actions. |
frontend/src/components/monitoring/monitorTabs.jsx |
Adds the “Errors” tab under Monitoring. |
frontend/src/components/ErrorBoundary.jsx |
Reports React error boundary crashes to the backend tracker. |
frontend/src/App.jsx |
Registers the /monitoring/errors route + page title. |
docs/README.md |
Updates API docs guidance and adds a documentation map. |
backend/tests/test_error_logs.py |
Adds tests for service dedup, admin API, ingestion endpoint, and 500-hook. |
backend/migrations/versions/085_error_logs.py |
Creates the new error_logs table + indexes. |
backend/app/services/error_log_service.py |
Implements fingerprinting, record/dedup, listing, and stats. |
backend/app/models/error_log.py |
Adds the ErrorLog model + context helpers + serialization. |
backend/app/models/__init__.py |
Exports ErrorLog from the models package. |
backend/app/api/error_logs.py |
Adds admin endpoints and the unauthenticated client ingestion endpoint + limiter. |
backend/app/__init__.py |
Registers the blueprint and records errors in the 500 handler. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+46
to
+51
| try { | ||
| await fetch(`${this.baseUrl}/error-logs/client`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(payload), | ||
| }); |
Comment on lines
+75
to
+80
| const load = useCallback(async () => { | ||
| try { | ||
| const params = { page, per_page: PAGE_SIZE }; | ||
| if (source !== 'all') params.source = source; | ||
| if (status !== 'all') params.resolved = status === 'resolved'; | ||
| if (q) params.search = q; |
Comment on lines
+46
to
+56
| resolved_arg = request.args.get('resolved') | ||
| resolved = None | ||
| if resolved_arg is not None: | ||
| resolved = resolved_arg.lower() in ('1', 'true', 'yes') | ||
| per_page = min(request.args.get('per_page', 25, type=int), 100) | ||
| result = error_log_service.list_errors( | ||
| source=request.args.get('source'), | ||
| resolved=resolved, | ||
| search=request.args.get('search'), | ||
| page=request.args.get('page', 1, type=int), | ||
| per_page=per_page, |
Comment on lines
+31
to
+45
| sa.Column('fingerprint', sa.String(length=64), nullable=False), | ||
| sa.Column('source', sa.String(length=20), nullable=False), | ||
| sa.Column('level', sa.String(length=20), nullable=False), | ||
| sa.Column('exception_type', sa.String(length=200), nullable=True), | ||
| sa.Column('message', sa.Text(), nullable=False), | ||
| sa.Column('traceback', sa.Text(), nullable=True), | ||
| sa.Column('endpoint', sa.String(length=255), nullable=True), | ||
| sa.Column('method', sa.String(length=10), nullable=True), | ||
| sa.Column('user_id', sa.Integer(), nullable=True), | ||
| sa.Column('context_json', sa.Text(), nullable=True), | ||
| sa.Column('count', sa.Integer(), nullable=False), | ||
| sa.Column('first_seen', sa.DateTime(), nullable=True), | ||
| sa.Column('last_seen', sa.DateTime(), nullable=True), | ||
| sa.Column('resolved', sa.Boolean(), nullable=True), | ||
| sa.ForeignKeyConstraint(['user_id'], ['users.id']), |
Comment on lines
+52
to
+56
| fp = _fingerprint(source, exception_type, message, endpoint) | ||
| existing = ErrorLog.query.filter_by(fingerprint=fp, resolved=False).first() | ||
| if existing: | ||
| existing.count = (existing.count or 1) + 1 | ||
| existing.last_seen = datetime.utcnow() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Until now, when the panel threw a 500 the only trace of it was a line in the Gunicorn log, and when the React tree blew up the only trace was a stack in somebody's browser console — which is to say, no trace at all. This PR gives ServerKit its own small error tracker: a new
error_logstable, a service that records exceptions deduplicated by fingerprint, an admin API, and an Errors tab in the Monitoring group where the whole thing is browsable. Backend crashes are captured by hooking the existing 500 handler; frontend crashes are reported fromErrorBoundarythrough a dedicated ingestion endpoint. Dedup is scoped to unresolved rows on purpose — once you mark something resolved, a later recurrence opens a fresh row instead of silently ticking a counter on an issue you thought you'd closed. The whole recording path is written to swallow its own failures: an error tracker that can throw while tracking an error is worse than no error tracker, sorecord_errorrolls back and returnsNonerather than ever propagating into the request that was already failing. The docs index also picked up a map of thedocs/folder and a pointer at the auto-generated OpenAPI docs, sinceAPI.mdhad quietly drifted from the routes it describes.Highlights
docs/and which parts are user-facing versus internal working notes, and point at the panel's live Swagger UI at/api/v1/docsas the authoritative API reference.Technical changes
ErrorLogmodel (backend/app/models/error_log.py) withfingerprint,source,level,exception_type,message,traceback,endpoint,method,user_id,context_json,count,first_seen,last_seen,resolved;get_context/set_contextwrap the JSON column,to_dictjoins the username through theUserrelationship. Exported fromapp.models.085_error_logscreates the table with indexes matching the actual access patterns —fingerprint(dedup lookup),sourceandresolved(list filters),last_seen(ordering). Bothupgrade()anddowngrade()are guarded by a_has_tableinspector check so re-runs are no-ops, and index drops are individually tolerant of absence.error_log_service(backend/app/services/error_log_service.py):_fingerprintissha256(source|exception_type|message[:200]|endpoint);record_errorlooks up an existing unresolved row with the same fingerprint and bumpscount+last_seen, otherwise inserts. Inputs are truncated before hashing (MAX_MESSAGE_LEN2000,MAX_TRACEBACK_LEN10000, plus per-column caps) and an unknownsourcefalls back tobackend.record_errorwraps its entire body in a catch-all that rolls the session back, logs, and returns(None, False)— it is called from inside a failing request and must not add a second failure on top of the first.list_errorsbuilds a filtered,last_seen-descending query withilikesearch over message/exception_type/endpoint and returns a paginated envelope (items,total,page,pages);get_statsreturns total, unresolved, last-24h, and aby_sourcegroup-by.app/api/error_logs.pyregistered at/api/v1/error-logsincreate_app.GET /,GET /stats,GET /<id>,POST /<id>/resolve, andDELETE /<id>are all@jwt_required()and@admin_required— error tracebacks are sensitive, so no non-admin read path exists.per_pageis clamped to 100.POST /error-logs/clientis the ingestion endpoint and is deliberately not JWT-gated: it usesverify_jwt_in_request(optional=True)so an authenticated report attachesuser_id, while a report from an expired or missing session still lands. Every field is type- and length-validated (_optional_strraises on non-string or oversized values) and a non-dictcontextis rejected with a 400.CLIENT_RATE_LIMIT10 perCLIENT_RATE_WINDOW60s,dequeof hit timestamps) returning 429. Per-process state — correct given the panel's single-worker deployment model, and worth revisiting if that ever changes.backend/app/__init__.pynow formats the original exception's traceback, resolvesget_jwt_identity()(tolerating anonymous crashes), and callsrecord_error. The call is itself wrapped in a baretry/excepton top of the service's own guarantee, so a recording failure cannot alter the 500 response. The response body is unchanged.frontend/src/services/api/errorLogs.jsmixed intoApiService, withgetErrorLogs/getErrorLogStats/getErrorLog/resolveErrorLog/deleteErrorLoggoing throughthis.request.reportClientErrordeliberately uses rawfetchinstead — it must fire with an expired or absent token, and it swallows network failures because it is called fromcomponentDidCatch, where throwing would re-trip the boundary it is reporting.ErrorBoundary.componentDidCatchfires a fire-and-forget report witherror.message/error.name/error.stack,window.location.pathname, and a context carrying the component stack (sliced to 2000 chars) and user agent, wrapped so payload assembly can't throw either.frontend/src/pages/Errors.jsxfollowing the Monitoring group's tab-page pattern:SearchField+ Refresh portal into the sharedPageTopbarviauseTopbarActions, twoSegControlquick filters in a singleListToolbar, and a server-paginatedDataTablewhoseDataTableFooterowns the count and pager. Any filter or search change resetspageto 1 so you can't strand yourself on an out-of-range page.EmptyStateinstead of a request that would 403.Drawer; the row already carries the full record, so the detailGETonly merges in freshcount/resolved, and is guarded against resolving after the drawer has moved on to another entry. Resolve and delete live in the drawer (delete behinduseConfirm), not as row actions.levelKindbuckets the free-formlevelstring onto thePillpalette with a gray fallback rather than guessing at unknown severities;contextTextrenders an object as pretty JSON and passes a string through verbatim.frontend/src/styles/pages/_errors.scssimported frommain.scss; routemonitoring/errorsadded toApp.jsxwith itsPAGE_TITLESentry, and anErrorstab (Bug icon) added toMONITOR_TABSbetween Jobs and Doctor.docs/README.mdgains a documentation map splitting the folder into user/operator docs, extension-author docs, and internal working docs, plus a callout that the running panel serves a route-derived OpenAPI 3.0 spec at/api/v1/docs(/api/v1/docs/openapi.json) which supersedesAPI.mdwhen the two disagree.VERSIONbumped to 1.7.105 by the release automation.