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 | # 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 | PUBLIC_LANGUAGE=en | ||||||
|  | # The public base URL, i.e. the one users will see. Used for building links. | ||||||
| PUBLIC_BASE_URL=https://pronouns.cc | 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 | 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 | 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 | 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 | 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", | 	"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c", | ||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
| 		"@fontsource/firago": "^5.1.0", | 		"@fontsource/firago": "^5.1.0", | ||||||
|  | 		"@sentry/sveltekit": "^8.52.0", | ||||||
| 		"base64-arraybuffer": "^1.0.2", | 		"base64-arraybuffer": "^1.0.2", | ||||||
| 		"bootstrap-icons": "^1.11.3", | 		"bootstrap-icons": "^1.11.3", | ||||||
| 		"luxon": "^3.5.0", | 		"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; | 			message: string; | ||||||
| 			status: number; | 			status: number; | ||||||
| 			code: ErrorCode; | 			code: ErrorCode; | ||||||
| 			id: string; | 			errors?: Array<{ key: string; errors: ValidationError[] }>; | ||||||
|  | 			error_id?: string; | ||||||
| 		} | 		} | ||||||
| 		// interface Error {}
 | 		// interface Error {}
 | ||||||
| 		// interface Locals {}
 | 		// interface Locals {}
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,10 @@ | ||||||
| import ApiError, { ErrorCode } from "$api/error"; | import ApiError, { ErrorCode } from "$api/error"; | ||||||
| import { PRIVATE_API_HOST, PRIVATE_INTERNAL_API_HOST } from "$env/static/private"; | 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 { PUBLIC_API_BASE } from "$env/static/public"; | ||||||
| import log from "$lib/log"; | import log from "$lib/log"; | ||||||
| import type { HandleFetch, HandleServerError } from "@sveltejs/kit"; | import type { HandleFetch, HandleServerError } from "@sveltejs/kit"; | ||||||
|  | import * as Sentry from "@sentry/sveltekit"; | ||||||
| 
 | 
 | ||||||
| export const handleFetch: HandleFetch = async ({ request, fetch }) => { | export const handleFetch: HandleFetch = async ({ request, fetch }) => { | ||||||
| 	if (request.url.startsWith(`${PUBLIC_API_BASE}/internal`)) { | 	if (request.url.startsWith(`${PUBLIC_API_BASE}/internal`)) { | ||||||
|  | @ -14,12 +16,17 @@ export const handleFetch: HandleFetch = async ({ request, fetch }) => { | ||||||
| 	return await fetch(request); | 	return await fetch(request); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | Sentry.init({ | ||||||
|  | 	dsn: env.PRIVATE_SENTRY_DSN, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| export const handleError: HandleServerError = async ({ error, status, message }) => { | 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) { | 	if (error instanceof ApiError) { | ||||||
| 		return { | 		return { | ||||||
| 			id, | 			error_id: id, | ||||||
| 			status: error.raw?.status || status, | 			status: error.raw?.status || status, | ||||||
| 			message: error.raw?.message || "Unknown error", | 			message: error.raw?.message || "Unknown error", | ||||||
| 			code: error.code, | 			code: error.code, | ||||||
|  | @ -27,10 +34,18 @@ export const handleError: HandleServerError = async ({ error, status, message }) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (status >= 400 && status <= 499) { | 	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); | 	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 { | 	toObject(): RawApiError { | ||||||
| 		return { | 		return { | ||||||
|  | 			error_id: this.raw?.error_id, | ||||||
| 			status: this.raw?.status || 500, | 			status: this.raw?.status || 500, | ||||||
| 			code: this.code, | 			code: this.code, | ||||||
| 			message: this.raw?.message || "Internal server error", | 			message: this.raw?.message || "Internal server error", | ||||||
|  | @ -23,6 +24,7 @@ export default class ApiError { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type RawApiError = { | export type RawApiError = { | ||||||
|  | 	error_id?: string; | ||||||
| 	status: number; | 	status: number; | ||||||
| 	message: string; | 	message: string; | ||||||
| 	code: ErrorCode; | 	code: ErrorCode; | ||||||
|  |  | ||||||
|  | @ -18,6 +18,14 @@ | ||||||
| 	</svelte:element> | 	</svelte:element> | ||||||
| {/if} | {/if} | ||||||
| <p>{errorDescription($t, error.code)}</p> | <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} | {#if error.errors} | ||||||
| 	<details> | 	<details> | ||||||
| 		<summary>{$t("error.extra-info-header")}</summary> | 		<summary>{$t("error.extra-info-header")}</summary> | ||||||
|  |  | ||||||
|  | @ -110,7 +110,8 @@ | ||||||
| 		"back-to-prev-page-button": "Go back to the previous page", | 		"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.", | 		"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.", | 		"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": { | 	"settings": { | ||||||
| 		"general-information-tab": "General information", | 		"general-information-tab": "General information", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue