feat(frontend): show error ID for internal server errors
This commit is contained in:
		
							parent
							
								
									83b62b4845
								
							
						
					
					
						commit
						a0ba712632
					
				
					 8 changed files with 1554 additions and 25 deletions
				
			
		|  | @ -1,7 +1,18 @@ | |||
| # Example .env file--DO NOT EDIT, copy to .env or .env.local then edit | ||||
| 
 | ||||
| # The language the frontend will use. Valid languages are listed in src/lib/i18n/index.ts. | ||||
| PUBLIC_LANGUAGE=en | ||||
| # The public base URL, i.e. the one users will see. Used for building links. | ||||
| PUBLIC_BASE_URL=https://pronouns.cc | ||||
| # The base URL for the URL shortener service. Used for building short links. | ||||
| PUBLIC_SHORT_URL=https://prns.cc | ||||
| # The base public URL for the API. This is (almost) always the public base URL + /api. | ||||
| PUBLIC_API_BASE=https://pronouns.cc/api | ||||
| # The base *private* URL for the API's rate limiter proxy. The frontend will rewrite API URLs to use this. | ||||
| # In development, you can set this to the same value as $PRIVATE_INTERNAL_API_HOST, but be aware that this will disable rate limiting. | ||||
| PRIVATE_API_HOST=http://localhost:5003/api | ||||
| # The base private URL for the API, which bypasses the rate limiter. Used for /api/internal paths and unauthenticated GET requests. | ||||
| PRIVATE_INTERNAL_API_HOST=http://localhost:6000/api | ||||
| 
 | ||||
| # The Sentry URL to use. Optional. | ||||
| PRIVATE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ | |||
| 	"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c", | ||||
| 	"dependencies": { | ||||
| 		"@fontsource/firago": "^5.1.0", | ||||
| 		"@sentry/sveltekit": "^8.52.0", | ||||
| 		"base64-arraybuffer": "^1.0.2", | ||||
| 		"bootstrap-icons": "^1.11.3", | ||||
| 		"luxon": "^3.5.0", | ||||
|  |  | |||
							
								
								
									
										1528
									
								
								Foxnouns.Frontend/pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1528
									
								
								Foxnouns.Frontend/pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										3
									
								
								Foxnouns.Frontend/src/app.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								Foxnouns.Frontend/src/app.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -9,7 +9,8 @@ declare global { | |||
| 			message: string; | ||||
| 			status: number; | ||||
| 			code: ErrorCode; | ||||
| 			id: string; | ||||
| 			errors?: Array<{ key: string; errors: ValidationError[] }>; | ||||
| 			error_id?: string; | ||||
| 		} | ||||
| 		// interface Error {}
 | ||||
| 		// interface Locals {}
 | ||||
|  |  | |||
|  | @ -1,8 +1,10 @@ | |||
| import ApiError, { ErrorCode } from "$api/error"; | ||||
| import { PRIVATE_API_HOST, PRIVATE_INTERNAL_API_HOST } from "$env/static/private"; | ||||
| import { env } from "$env/dynamic/private"; | ||||
| import { PUBLIC_API_BASE } from "$env/static/public"; | ||||
| import log from "$lib/log"; | ||||
| import type { HandleFetch, HandleServerError } from "@sveltejs/kit"; | ||||
| import * as Sentry from "@sentry/sveltekit"; | ||||
| 
 | ||||
| export const handleFetch: HandleFetch = async ({ request, fetch }) => { | ||||
| 	if (request.url.startsWith(`${PUBLIC_API_BASE}/internal`)) { | ||||
|  | @ -14,12 +16,17 @@ export const handleFetch: HandleFetch = async ({ request, fetch }) => { | |||
| 	return await fetch(request); | ||||
| }; | ||||
| 
 | ||||
| Sentry.init({ | ||||
| 	dsn: env.PRIVATE_SENTRY_DSN, | ||||
| }); | ||||
| 
 | ||||
| export const handleError: HandleServerError = async ({ error, status, message }) => { | ||||
| 	const id = crypto.randomUUID(); | ||||
| 	// as far as i know, sentry IDs are just UUIDs with the dashes removed. use those here as well
 | ||||
| 	let id = crypto.randomUUID().replaceAll("-", ""); | ||||
| 
 | ||||
| 	if (error instanceof ApiError) { | ||||
| 		return { | ||||
| 			id, | ||||
| 			error_id: id, | ||||
| 			status: error.raw?.status || status, | ||||
| 			message: error.raw?.message || "Unknown error", | ||||
| 			code: error.code, | ||||
|  | @ -27,10 +34,18 @@ export const handleError: HandleServerError = async ({ error, status, message }) | |||
| 	} | ||||
| 
 | ||||
| 	if (status >= 400 && status <= 499) { | ||||
| 		return { id, status, message, code: ErrorCode.GenericApiError }; | ||||
| 		return { error_id: id, status, message, code: ErrorCode.GenericApiError }; | ||||
| 	} | ||||
| 
 | ||||
| 	// client errors and backend API errors just clog up sentry, so we don't send those.
 | ||||
| 	id = Sentry.captureException(error, { | ||||
| 		mechanism: { | ||||
| 			type: "sveltekit", | ||||
| 			handled: false, | ||||
| 		}, | ||||
| 	}); | ||||
| 
 | ||||
| 	log.error("[%s] error in handler:", id, error); | ||||
| 
 | ||||
| 	return { id, status, message, code: ErrorCode.InternalServerError }; | ||||
| 	return { error_id: id, status, message, code: ErrorCode.InternalServerError }; | ||||
| }; | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ export default class ApiError { | |||
| 
 | ||||
| 	toObject(): RawApiError { | ||||
| 		return { | ||||
| 			error_id: this.raw?.error_id, | ||||
| 			status: this.raw?.status || 500, | ||||
| 			code: this.code, | ||||
| 			message: this.raw?.message || "Internal server error", | ||||
|  | @ -23,6 +24,7 @@ export default class ApiError { | |||
| } | ||||
| 
 | ||||
| export type RawApiError = { | ||||
| 	error_id?: string; | ||||
| 	status: number; | ||||
| 	message: string; | ||||
| 	code: ErrorCode; | ||||
|  |  | |||
|  | @ -18,6 +18,14 @@ | |||
| 	</svelte:element> | ||||
| {/if} | ||||
| <p>{errorDescription($t, error.code)}</p> | ||||
| {#if error.error_id} | ||||
| 	<p> | ||||
| 		{$t("error.error-id")} | ||||
| 		<code> | ||||
| 			{error.error_id} | ||||
| 		</code> | ||||
| 	</p> | ||||
| {/if} | ||||
| {#if error.errors} | ||||
| 	<details> | ||||
| 		<summary>{$t("error.extra-info-header")}</summary> | ||||
|  |  | |||
|  | @ -110,7 +110,8 @@ | |||
| 		"back-to-prev-page-button": "Go back to the previous page", | ||||
| 		"400-description": "Something went wrong with your request. This error should never land you on this page, so it's probably a bug.", | ||||
| 		"500-description": "Something went wrong on the server. Please try again later.", | ||||
| 		"unknown-status-description": "Something went wrong, but we're not sure what. Please try again." | ||||
| 		"unknown-status-description": "Something went wrong, but we're not sure what. Please try again.", | ||||
| 		"error-id": "If you report this error to the developers, please give them this ID:" | ||||
| 	}, | ||||
| 	"settings": { | ||||
| 		"general-information-tab": "General information", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue