From 931b2a6c2067a9b8d8c1a502db32fe672ca1a0ea Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:33:17 +0100 Subject: [PATCH 1/3] fix: error logging so server errors are actually captured in Sentry --- .changeset/fix-error-logging.md | 16 ++++++++++++++++ docker-compose.yml | 14 +++++++------- fdm-app/app/entry.client.tsx | 5 ++++- fdm-app/app/entry.server.tsx | 9 ++++----- fdm-app/app/lib/error.ts | 15 ++++++--------- fdm-app/instrument.server.mjs | 34 ++++++++++++++------------------- 6 files changed, 51 insertions(+), 42 deletions(-) create mode 100644 .changeset/fix-error-logging.md diff --git a/.changeset/fix-error-logging.md b/.changeset/fix-error-logging.md new file mode 100644 index 000000000..5434808c9 --- /dev/null +++ b/.changeset/fix-error-logging.md @@ -0,0 +1,16 @@ +--- +"@nmi-agro/fdm-app": patch +--- + +Fix error logging so server errors are actually captured in Sentry + +Server errors were silently dropped from Sentry in several scenarios, leaving only an uninformative client-side "Unexpected Server Error" event with no stack trace or error code + +- `reportError()` now always calls `Sentry.captureException()` when the SDK is initialized (guarded via `Sentry.getClient()`), removing the dependency on `clientConfig` which could silently evaluate to `null` server-side +- `errorId` is now stored in Sentry **tags** (`error_id`) in addition to `extra`, making it searchable — users can report their error code and you can find the exact event with `error_id:XXXX-XXXX` +- `console.error` is now always called in `reportError()`, regardless of whether Sentry is configured +- `"Unexpected Server Error"` is added to `ignoreErrors` on the client — this React Router shadow event is always a duplicate of the real server-side error +- `handleError` in `entry.server.tsx` now uses `reportError()` instead of raw `Sentry.captureException()`, so unhandled errors also get a trackable `errorId` +- Streaming `onError` callbacks now call `reportError()` instead of `console.error()` only +- `VITE_SENTRY_DSN`, `VITE_SENTRY_TRACE_SAMPLE_RATE`, and `VITE_SENTRY_PROFILE_SAMPLE_RATE` renamed to `PUBLIC_SENTRY_*` for consistency with the rest of the app +- Sentry server-side initialization is now conditional on `PUBLIC_SENTRY_DSN` being set; the app starts normally without Sentry configured diff --git a/docker-compose.yml b/docker-compose.yml index 4e301236f..db9a97c41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,14 +23,14 @@ services: - MS_CLIENT_SECRET=YOUR_MS_CLIENT_SECRET # Replace with your Microsoft Client Secret - AVAILABLE_FIELDS_URL=YOUR_STORAGE_ULR # Replace with the url of the storage location - FDM_APP_URL=YOUR_DOMAIN # Replace with your domain - - VITE_SENTRY_ORG=YOUR_SENTRY_ORG # Replace with your Sentry organization - - VITE_SENTRY_PROJECT=YOUR_SENTRY_PROJECT # Replace with your Sentry project - - VITE_SENTRY_DSN=YOUR_SENTRY_DSN # Replace with your Sentry DSN + - PUBLIC_SENTRY_ORG=YOUR_SENTRY_ORG # Replace with your Sentry organization + - PUBLIC_SENTRY_PROJECT=YOUR_SENTRY_PROJECT # Replace with your Sentry project + - PUBLIC_SENTRY_DSN=YOUR_SENTRY_DSN # Replace with your Sentry DSN - SENTRY_AUTH_TOKEN=YOUR_SENTRY_AUTH_TOKEN # Replace with your Sentry authentication token - - VITE_SENTRY_TRACE_SAMPLE_RATE=1 - - VITE_SENTRY_REPLAY_SAMPLE_RATE=0 - - VITE_SENTRY_REPLAY_SAMPLE_RATE_ON_ERROR=1 - - VITE_SENTRY_PROFILE_SAMPLE_RATE=1 + - PUBLIC_SENTRY_TRACE_SAMPLE_RATE=1 + - PUBLIC_SENTRY_REPLAY_SAMPLE_RATE=0 + - PUBLIC_SENTRY_REPLAY_SAMPLE_RATE_ON_ERROR=1 + - PUBLIC_SENTRY_PROFILE_SAMPLE_RATE=1 depends_on: postgres: condition: service_healthy diff --git a/fdm-app/app/entry.client.tsx b/fdm-app/app/entry.client.tsx index 90fc32f53..ac01b73ef 100644 --- a/fdm-app/app/entry.client.tsx +++ b/fdm-app/app/entry.client.tsx @@ -18,7 +18,10 @@ if (clientConfig.analytics.sentry) { dsn: sentryConfig.dsn, release: import.meta.env.PUBLIC_APP_VERSION, environment: import.meta.env.NODE_ENV, - ignoreErrors: [/BodyStreamBuffer was aborted/], + ignoreErrors: [ + /BodyStreamBuffer was aborted/, + /Unexpected Server Error/, + ], integrations: [ Sentry.reactRouterTracingIntegration(), Sentry.replayIntegration(), diff --git a/fdm-app/app/entry.server.tsx b/fdm-app/app/entry.server.tsx index c403f1512..cc6f4df01 100644 --- a/fdm-app/app/entry.server.tsx +++ b/fdm-app/app/entry.server.tsx @@ -5,7 +5,6 @@ import { createReadableStreamFromReadable } from "@react-router/node" * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ * For more information, see https://remix.run/file-conventions/entry.server */ -import * as Sentry from "@sentry/react-router" import { getMetaTagTransformer, wrapSentryHandleRequest, @@ -18,6 +17,7 @@ import type { HandleErrorFunction, } from "react-router" import { ServerRouter } from "react-router" +import { reportError } from "~/lib/error" import { addSecurityHeaders, getCacheControlHeaders } from "./lib/cache.server" export const streamTimeout = 90000 @@ -115,7 +115,7 @@ function handleBotRequest( // errors encountered during initial shell rendering since they'll // reject and get logged in handleDocumentRequest. if (shellRendered) { - console.error(error) + reportError(error, { scope: "streaming-bot" }) } }, }, @@ -164,7 +164,7 @@ function handleBrowserRequest( // errors encountered during initial shell rendering since they'll // reject and get logged in handleDocumentRequest. if (shellRendered) { - console.error(error) + reportError(error, { scope: "streaming" }) } }, }, @@ -180,7 +180,6 @@ export default wrapSentryHandleRequest(handleRequest) export const handleError: HandleErrorFunction = (error, { request }) => { // React Router may abort some interrupted requests, report those if (!request.signal.aborted) { - Sentry.captureException(error) - console.error(error) + reportError(error, { scope: "unhandled" }) } } diff --git a/fdm-app/app/lib/error.ts b/fdm-app/app/lib/error.ts index 49b45d897..760408483 100644 --- a/fdm-app/app/lib/error.ts +++ b/fdm-app/app/lib/error.ts @@ -2,7 +2,6 @@ import * as Sentry from "@sentry/react-router" import { customAlphabet } from "nanoid" import { data, redirect } from "react-router" import { dataWithError, dataWithWarning } from "remix-toast" -import { clientConfig } from "~/lib/config" const customErrorAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" // No lookalikes (0, 1, I, O, S, Z) const errorIdSize = 8 // Number of characters in ID @@ -19,18 +18,19 @@ export function reportError( .match(/.{1,4}/g) ?.join("-") || createErrorId() // Format as XXXX-XXXX - if (clientConfig.analytics.sentry?.dsn) { + console.error(`Error (code: ${errorId}):`, error, context ?? "") + + if (Sentry.getClient()) { Sentry.captureException(error, { tags: { ...tags, + error_id: errorId, }, extra: { ...context, errorId: errorId, }, }) - } else { - console.error(`Error (code: ${errorId}):`, error, context) } return errorId @@ -131,9 +131,7 @@ export function handleLoaderError(error: unknown) { ) } - // All other errors - console.error("Loader Error: ", error) - // Forward error to Sentry + // All other errors — reportError handles logging and Sentry capture const errorId = reportError(error, { scope: "loader", }) @@ -275,8 +273,7 @@ export function handleActionError(error: unknown) { ) } - // All other errors - console.error("Error: ", error) + // All other errors — reportError handles logging and Sentry capture const errorId = reportError(error, { scope: "action", }) diff --git a/fdm-app/instrument.server.mjs b/fdm-app/instrument.server.mjs index 715d468bf..deb00ac6c 100644 --- a/fdm-app/instrument.server.mjs +++ b/fdm-app/instrument.server.mjs @@ -1,24 +1,18 @@ import { nodeProfilingIntegration } from "@sentry/profiling-node" import * as Sentry from "@sentry/react-router" -const requiredEnvVars = [ - "VITE_SENTRY_DSN", - "VITE_SENTRY_TRACE_SAMPLE_RATE", - "VITE_SENTRY_PROFILE_SAMPLE_RATE", -] - -for (const envVar of requiredEnvVars) { - if (!process.env[envVar]) { - throw new Error(`Missing required environment variable: ${envVar}`) - } +if (process.env.PUBLIC_SENTRY_DSN) { + Sentry.init({ + dsn: process.env.PUBLIC_SENTRY_DSN, + integrations: [nodeProfilingIntegration()], + tracesSampleRate: Number( + process.env.PUBLIC_SENTRY_TRACE_SAMPLE_RATE ?? 1, + ), + profilesSampleRate: Number( + process.env.PUBLIC_SENTRY_PROFILE_SAMPLE_RATE ?? 1, + ), + ignoreErrors: [/BodyStreamBuffer was aborted/], + environment: process.env.NODE_ENV ?? "development", + release: process.env.npm_package_version, + }) } - -Sentry.init({ - dsn: String(process.env.VITE_SENTRY_DSN), - integrations: [nodeProfilingIntegration()], - tracesSampleRate: Number(process.env.VITE_SENTRY_TRACE_SAMPLE_RATE), - profilesSampleRate: Number(process.env.VITE_SENTRY_PROFILE_SAMPLE_RATE), - ignoreErrors: [/BodyStreamBuffer was aborted/], - environment: process.env.NODE_ENV ?? "development", - release: process.env.npm_package_version, -}) From 5fa76623551c162618a2edc285e51a31f6ec2526 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:52:35 +0100 Subject: [PATCH 2/3] fix: error reporting for 500 --- fdm-app/app/lib/error.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/fdm-app/app/lib/error.ts b/fdm-app/app/lib/error.ts index 760408483..80663f0b5 100644 --- a/fdm-app/app/lib/error.ts +++ b/fdm-app/app/lib/error.ts @@ -67,10 +67,11 @@ export function handleLoaderError(error: unknown) { userMessage = "De gevraagde data kon niet worden gevonden." break // case 500: - default: - userMessage = - "Er is een onverwachte fout opgetreden. Probeer het later opnieuw of neem contact op met Ondersteuning." + default: { + const errorId = reportError(error, { scope: "loader" }) + userMessage = `Er is een onverwachte fout opgetreden. Probeer het later opnieuw of neem contact op met Ondersteuning en meldt de volgende foutcode: ${errorId}.` break + } } return data( { @@ -219,11 +220,12 @@ export function handleActionError(error: unknown) { dataStatus = "warning" break // case 500: - default: - userMessage = - "Er is een onverwachte fout opgetreden. Probeer het later opnieuw of neem contact op met Ondersteuning." + default: { + const errorId = reportError(error, { scope: "action" }) + userMessage = `Er is een onverwachte fout opgetreden. Probeer het later opnieuw of neem contact op met Ondersteuning en meldt de volgende foutcode: ${errorId}.` dataStatus = "error" break + } } if (dataStatus === "warning") { return dataWithWarning( From ada7694af83eb7de5862c73ac623120e12179e38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Mar 2026 10:22:29 +0000 Subject: [PATCH 3/3] chore: bump version of packages for release --- .changeset/fix-error-logging.md | 16 ---------------- fdm-app/CHANGELOG.md | 16 ++++++++++++++++ fdm-app/package.json | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) delete mode 100644 .changeset/fix-error-logging.md diff --git a/.changeset/fix-error-logging.md b/.changeset/fix-error-logging.md deleted file mode 100644 index 5434808c9..000000000 --- a/.changeset/fix-error-logging.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@nmi-agro/fdm-app": patch ---- - -Fix error logging so server errors are actually captured in Sentry - -Server errors were silently dropped from Sentry in several scenarios, leaving only an uninformative client-side "Unexpected Server Error" event with no stack trace or error code - -- `reportError()` now always calls `Sentry.captureException()` when the SDK is initialized (guarded via `Sentry.getClient()`), removing the dependency on `clientConfig` which could silently evaluate to `null` server-side -- `errorId` is now stored in Sentry **tags** (`error_id`) in addition to `extra`, making it searchable — users can report their error code and you can find the exact event with `error_id:XXXX-XXXX` -- `console.error` is now always called in `reportError()`, regardless of whether Sentry is configured -- `"Unexpected Server Error"` is added to `ignoreErrors` on the client — this React Router shadow event is always a duplicate of the real server-side error -- `handleError` in `entry.server.tsx` now uses `reportError()` instead of raw `Sentry.captureException()`, so unhandled errors also get a trackable `errorId` -- Streaming `onError` callbacks now call `reportError()` instead of `console.error()` only -- `VITE_SENTRY_DSN`, `VITE_SENTRY_TRACE_SAMPLE_RATE`, and `VITE_SENTRY_PROFILE_SAMPLE_RATE` renamed to `PUBLIC_SENTRY_*` for consistency with the rest of the app -- Sentry server-side initialization is now conditional on `PUBLIC_SENTRY_DSN` being set; the app starts normally without Sentry configured diff --git a/fdm-app/CHANGELOG.md b/fdm-app/CHANGELOG.md index a3bb8e803..f71704308 100644 --- a/fdm-app/CHANGELOG.md +++ b/fdm-app/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog fdm-app +## 0.28.4 + +### Patch Changes + +- [#522](https://github.com/nmi-agro/fdm/pull/522) [`931b2a6`](https://github.com/nmi-agro/fdm/commit/931b2a6c2067a9b8d8c1a502db32fe672ca1a0ea) Thanks [@SvenVw](https://github.com/SvenVw)! - Fix error logging so server errors are actually captured in Sentry + + Server errors were silently dropped from Sentry in several scenarios, leaving only an uninformative client-side "Unexpected Server Error" event with no stack trace or error code + - `reportError()` now always calls `Sentry.captureException()` when the SDK is initialized (guarded via `Sentry.getClient()`), removing the dependency on `clientConfig` which could silently evaluate to `null` server-side + - `errorId` is now stored in Sentry **tags** (`error_id`) in addition to `extra`, making it searchable — users can report their error code and you can find the exact event with `error_id:XXXX-XXXX` + - `console.error` is now always called in `reportError()`, regardless of whether Sentry is configured + - `"Unexpected Server Error"` is added to `ignoreErrors` on the client — this React Router shadow event is always a duplicate of the real server-side error + - `handleError` in `entry.server.tsx` now uses `reportError()` instead of raw `Sentry.captureException()`, so unhandled errors also get a trackable `errorId` + - Streaming `onError` callbacks now call `reportError()` instead of `console.error()` only + - `VITE_SENTRY_DSN`, `VITE_SENTRY_TRACE_SAMPLE_RATE`, and `VITE_SENTRY_PROFILE_SAMPLE_RATE` renamed to `PUBLIC_SENTRY_*` for consistency with the rest of the app + - Sentry server-side initialization is now conditional on `PUBLIC_SENTRY_DSN` being set; the app starts normally without Sentry configured + ## 0.28.3 ### Patch Changes diff --git a/fdm-app/package.json b/fdm-app/package.json index db6d41c29..1c4217db5 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -1,6 +1,6 @@ { "name": "@nmi-agro/fdm-app", - "version": "0.28.3", + "version": "0.28.4", "private": true, "sideEffects": false, "type": "module",