diff --git a/.changeset/heavy-steaks-mix.md b/.changeset/heavy-steaks-mix.md new file mode 100644 index 000000000..a9cc488d5 --- /dev/null +++ b/.changeset/heavy-steaks-mix.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-app": patch +--- + +If a page load takes longer than 300 ms, log the total duration to Sentry to identify potentially slow pages. diff --git a/.changeset/shy-times-fly.md b/.changeset/shy-times-fly.md new file mode 100644 index 000000000..59d198464 --- /dev/null +++ b/.changeset/shy-times-fly.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-app": minor +--- + +If a page load takes longer than 300 ms, show a loading spinner and blur the page to prevent double-clicking the navigation action. diff --git a/.changeset/wide-animals-tan.md b/.changeset/wide-animals-tan.md new file mode 100644 index 000000000..13eb1f347 --- /dev/null +++ b/.changeset/wide-animals-tan.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-app": patch +--- + +Suppress logging `BodyStreamBuffer was aborted` to Sentry, as this is caused by users navigating to another page while the current page is still loading. diff --git a/fdm-app/app/components/custom/navigation-progress.tsx b/fdm-app/app/components/custom/navigation-progress.tsx new file mode 100644 index 000000000..a6a59e73a --- /dev/null +++ b/fdm-app/app/components/custom/navigation-progress.tsx @@ -0,0 +1,83 @@ +import * as Sentry from "@sentry/react-router" +import { AnimatePresence, motion } from "framer-motion" +import { Loader2 } from "lucide-react" +import { useEffect, useRef, useState } from "react" +import { useNavigation } from "react-router" +import { clientConfig } from "~/lib/config" + +/** + * Shows a blurred overlay with a loading card when navigation takes longer than 300ms. + * Fast navigations never trigger the indicator. + * Tracks show frequency and duration as Sentry metrics. + */ +export function NavigationProgress() { + const { state } = useNavigation() + const [show, setShow] = useState(false) + const startTimeRef = useRef(null) + + // Show after 300ms — emit a count metric when it appears + useEffect(() => { + if (state !== "idle") { + if (startTimeRef.current === null) { + startTimeRef.current = Date.now() + } + const timer = setTimeout(() => { + setShow(true) + if (clientConfig.analytics.sentry) { + Sentry.metrics.count("navigation_progress.shown", 1) + } + }, 300) + return () => clearTimeout(timer) + } + + // Navigation finished — emit duration metric and hide + if (show && startTimeRef.current !== null) { + const duration = Date.now() - startTimeRef.current + if (clientConfig.analytics.sentry) { + Sentry.metrics.distribution( + "navigation_progress.duration_ms", + duration, + ) + } + } + setShow(false) + startTimeRef.current = null + }, [state, show]) + + return ( + + {show && ( + <> + {/* Backdrop blur */} + + + {/* Loading card */} + + +

+ Even geduld… +

+
+ + )} +
+ ) +} diff --git a/fdm-app/app/entry.client.tsx b/fdm-app/app/entry.client.tsx index 5974074eb..c96658955 100644 --- a/fdm-app/app/entry.client.tsx +++ b/fdm-app/app/entry.client.tsx @@ -18,6 +18,7 @@ if (clientConfig.analytics.sentry) { dsn: sentryConfig.dsn, release: import.meta.env.PUBLIC_APP_VERSION, environment: import.meta.env.NODE_ENV, + ignoreErrors: [/BodyStreamBuffer was aborted/], integrations: [ Sentry.reactRouterTracingIntegration(), Sentry.replayIntegration(), diff --git a/fdm-app/app/root.tsx b/fdm-app/app/root.tsx index b347e78dd..542303430 100644 --- a/fdm-app/app/root.tsx +++ b/fdm-app/app/root.tsx @@ -19,6 +19,7 @@ import { getToast } from "remix-toast" import { toast as notify } from "sonner" import { Banner } from "~/components/custom/banner" import { ErrorBlock } from "~/components/custom/error" +import { NavigationProgress } from "~/components/custom/navigation-progress" import { Toaster } from "~/components/ui/sonner" import { clientConfig } from "~/lib/config" import { useChangelogStore } from "~/store/changelog" @@ -152,6 +153,7 @@ export function Layout() { + diff --git a/fdm-app/instrument.server.mjs b/fdm-app/instrument.server.mjs index 8febe8f08..715d468bf 100644 --- a/fdm-app/instrument.server.mjs +++ b/fdm-app/instrument.server.mjs @@ -18,6 +18,7 @@ Sentry.init({ 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, })