diff --git a/.gitea/workflows/continuous_integration.yaml b/.gitea/workflows/continuous_integration.yaml index 2add76f..8bd7bc9 100644 --- a/.gitea/workflows/continuous_integration.yaml +++ b/.gitea/workflows/continuous_integration.yaml @@ -93,6 +93,12 @@ jobs: - name: Build run: pnpm run build + env: + # Sentry DSN is niet gevoelig (veilig om in de client-bundle te zitten), + # daarom een Gitea Actions "vars"-waarde i.p.v. een secret. Optioneel: + # als deze niet is ingesteld, wordt Sentry-logging simpelweg overgeslagen + # (zie src/main.tsx) en blijft alleen console-logging actief. + VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }} - name: Upload build artifact uses: actions/upload-artifact@v3 diff --git a/aidlc-docs/features/react-frontend/audit.md b/aidlc-docs/features/react-frontend/audit.md index c04c7fe..bcd13bb 100644 --- a/aidlc-docs/features/react-frontend/audit.md +++ b/aidlc-docs/features/react-frontend/audit.md @@ -456,3 +456,15 @@ **Context**: Post-workflow follow-up (feature already marked Complete); clarifies wording of the previous follow-up's path-discrepancy note per user feedback. Changes not yet committed/pushed. --- + +## Post-Completion Follow-up: Client-Side Error Logging Destination Decided (Sentry) + +**Timestamp**: 2026-07-25T12:55:00Z + +**User Input**: "Als het goed is stonden er nog een paar punten open? Zullen we het volgende punt oppakken? Kan je ook een feature-branch aanmaken met een passende naam? [...]" — followed by selecting "Logging-bestemming kiezen" and then "Sentry (free tier)" as the concrete destination. + +**AI Response**: "Picked up the next open follow-up from `production-readiness-checklist.md`: the client-side error logging destination. Created feature branch `feature/sentry_error_logging` (carrying over the previously uncommitted path-documentation changes, committed there first). Added `@sentry/react` as a dependency, initialized it in `src/main.tsx` via `Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN })` (no-op if the DSN is unset), added `src/vite-env.d.ts` for `import.meta.env` typing, and hooked `ErrorBoundary.componentDidCatch` to also call `Sentry.captureException`. Wired the DSN as a Gitea Actions repository variable (`vars.VITE_SENTRY_DSN`, not a secret) into the `Build` step of `continuous_integration.yaml`. Updated `monitoring-plan.md`, `monitoring-setup.md`, and `production-readiness-checklist.md` to mark this open item resolved, with the remaining manual step (creating the actual `VITE_SENTRY_DSN` Gitea variable with a real Sentry DSN) called out explicitly." + +**Context**: Post-workflow follow-up (feature already marked Complete); resolves open follow-up #2 from `production-readiness-checklist.md`. Work done on new branch `feature/sentry_error_logging`. + +--- diff --git a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md index dc3b4c7..3ecef2a 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-plan.md @@ -13,7 +13,7 @@ Initial answers to the monitoring plan were contradictory: Question 2 selected " ## Chosen Approach(es) ### Logging -Client-side errors (JavaScript crashes, broken links) should be logged, but the concrete destination is not yet decided (original Question 3 = C, "not yet determined"). This is tracked as an **open action item** below rather than blocking this stage. +Client-side errors (JavaScript crashes, broken links) are logged. **Decided**: Sentry free tier (in addition to the browser console) — see the follow-up in `audit.md` and the implementation in `monitoring-setup.md`. ### Dashboards A combination of: @@ -27,7 +27,7 @@ A combination of: - **Reuse of existing infrastructure**: this feature does not plug into any pre-existing shared monitoring (original Question 7 = A) — there is none yet. Should a shared back-end/CMS monitoring stack be introduced later, this can be revisited. ## Open Action Items -1. **Decide logging destination**: choose between "browser console only" (no central storage, manual debugging) or a free/low-cost external error-tracking service (e.g. Sentry free tier) once this becomes a priority. Until decided, `monitoring-setup.md` documents both options so either can be adopted without re-doing this stage. +1. ~~Decide logging destination~~ — **Resolved**: Sentry free tier, wired into `ErrorBoundary`/`main.tsx` (see `monitoring-setup.md`). The Gitea Actions variable `VITE_SENTRY_DSN` still needs to be created by the user with a real Sentry project DSN; until then the build simply skips Sentry initialization and only console-logging is active. 2. **Pick concrete analytics + uptime tools**: `monitoring-setup.md` lists candidate free-tier tools; final tool selection/account creation is a manual follow-up outside this workflow (no code changes required to swap providers, since neither is wired into the codebase yet beyond an optional embed snippet). ## Rationale diff --git a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md index 7ecd24b..81371b2 100644 --- a/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md +++ b/aidlc-docs/features/react-frontend/operations/monitoring/monitoring-setup.md @@ -9,16 +9,17 @@ Concrete setup steps for the approaches chosen in `monitoring-plan.md`: **Loggin - Broken/failed navigation (e.g. an unexpected router error). - No user PII, form input, or sensitive data should ever be logged — this is a public marketing site, but keep this discipline regardless. -### Destination — open decision -The destination was not finalized (original Question 3 = C). Two supported options, either of which can be adopted later without further design work: +### Destination — decided: Sentry free tier + console +Both the console and Sentry are now active (original Question 3 resolved as a combination): -**Option 1: Browser console only (default today)** -- No code changes needed — errors already surface via `console.error` inside the existing `ErrorBoundary`. -- Zero cost, but not centrally visible; only useful for manual debugging (e.g. via a user's screenshot or a support request). +**Console (always on)** +- Errors already surface via `console.error` inside `ErrorBoundary.componentDidCatch` — unchanged, zero cost, useful for local/manual debugging. -**Option 2: External error-tracking service (e.g. Sentry free tier)** -- When decided, add `@sentry/react` as a dependency, initialize it once in the app entry point (e.g. `src/main.tsx`) with the project DSN, and report caught errors from the `ErrorBoundary`'s `componentDidCatch`/`onError` hook to Sentry in addition to the console. -- Store the DSN as a Gitea Actions variable (or a build-time `.env` value, since it's not a secret — Sentry DSNs are safe to expose client-side) and inject it via Vite's `import.meta.env`. +**Sentry free tier (implemented)** +- `@sentry/react` is a dependency; `src/main.tsx` calls `Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN })` at startup, but only when a DSN is present — if not configured, Sentry is silently skipped and only console-logging remains active (safe default, no crash on missing config). +- `ErrorBoundary.componentDidCatch` calls `Sentry.captureException(error, { extra: { componentStack: info.componentStack } })` in addition to `console.error`. +- The DSN is injected at build time via Vite's `import.meta.env.VITE_SENTRY_DSN` (typed in `src/vite-env.d.ts`). It is **not** a secret (Sentry DSNs are safe to expose client-side), so it is passed as a **Gitea Actions repository variable** (`vars.VITE_SENTRY_DSN`, not a secret) to the `Build` step in `continuous_integration.yaml`. +- **Manual follow-up required**: create a free Sentry project (https://sentry.io) for this app, copy its DSN, and set it as the `VITE_SENTRY_DSN` repository variable in Gitea (Repository Settings → Actions → Variables). Until that variable is set, the build still succeeds and the site still works — Sentry reporting simply stays inactive. - Free tier limits (error volume, retention) are typically sufficient for a low-traffic marketing site. **Log level strategy**: only errors are logged (no verbose/info-level client logging) — this is a static site with no meaningful "business events" beyond page views, which are covered by analytics (see Dashboards below), not logging. @@ -55,7 +56,7 @@ Pick one: | Concern | Approach | Status | |---|---|---| -| Client-side errors | Logging (console today; Sentry free tier optional later) | Destination open item | +| Client-side errors | Logging (console + Sentry free tier) | Implemented; `VITE_SENTRY_DSN` Gitea variable still needs to be created by the user | | Visitor/usage insight | Analytics dashboard (Plausible/Umami/GA4) | Tool selection open item | | Site reachability | Uptime dashboard (UptimeRobot/Better Uptime) | Tool selection + production URL open item | | Alerting | Out of scope | Not configured | diff --git a/aidlc-docs/features/react-frontend/operations/production-readiness-checklist.md b/aidlc-docs/features/react-frontend/operations/production-readiness-checklist.md index 6a7dbec..13876b9 100644 --- a/aidlc-docs/features/react-frontend/operations/production-readiness-checklist.md +++ b/aidlc-docs/features/react-frontend/operations/production-readiness-checklist.md @@ -19,6 +19,6 @@ - **Ready for Production**: Yes with caveats - **Open Follow-ups**: - Finalize the hosting/domain setup and extend `.gitea/workflows/deploy.yml`'s `deploy` job to actually upload `dist/` to the host, instead of only packaging it as a downloadable artifact (see `deployment-plan.md` "Open Item") - - Decide the client-side error logging destination (browser console only vs. an external service such as Sentry free tier) — see `monitoring-setup.md` + - ~~Decide the client-side error logging destination~~ — **Resolved**: console + Sentry free tier, implemented (see `monitoring-setup.md`); remaining manual step is creating the `VITE_SENTRY_DSN` Gitea Actions variable - Pick and configure the concrete analytics tool (e.g. Plausible/Umami/GA4) and uptime dashboard tool (e.g. UptimeRobot/Better Uptime), including registering the final production URL once hosting is finalized - Run a formal Lighthouse performance check before the first real production deployment (flagged as not yet run in `build-and-test-summary.md`) diff --git a/package.json b/package.json index 235a97f..454ff70 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@fontsource/instrument-sans": "^5.1.0", "@fontsource/jetbrains-mono": "^5.1.1", "@fontsource/sora": "^5.1.0", + "@sentry/react": "^10.68.0", "@tanstack/react-query": "^5.59.0", "@tanstack/react-router": "^1.79.0", "react": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73fc582..66043af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@fontsource/sora': specifier: ^5.1.0 version: 5.2.8 + '@sentry/react': + specifier: ^10.68.0 + version: 10.68.0(react@18.3.1) '@tanstack/react-query': specifier: ^5.59.0 version: 5.101.2(react@18.3.1) @@ -592,6 +595,40 @@ packages: cpu: [x64] os: [win32] + '@sentry/browser-utils@10.68.0': + resolution: {integrity: sha512-be8VtdjCngKc77cstJeV+gO15iH+blyXpBDk8yOehmtX4BkFO33mfTMNCWVR2LA0oOxjIHWRAhf77fIUEhzxPg==} + engines: {node: '>=18'} + + '@sentry/browser@10.68.0': + resolution: {integrity: sha512-8xVgk7oG2lajXnbXF6a7H1xMZ/U6icqSldHGzQu1+bajfrK8Gan9ULG/Xsj1VM1LlNeK6/7znDJ3u1jgvIwznw==} + engines: {node: '>=18'} + + '@sentry/conventions@0.16.0': + resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} + engines: {node: '>=14'} + + '@sentry/core@10.68.0': + resolution: {integrity: sha512-5Amhx8ltVz7vb1bRGyf3c4J69/iHW8R/H+SJxTRILHlsSOBrnVVc/IQEYDC6PTRdRdZ3x2u7RVjxZi2Mhe525g==} + engines: {node: '>=18'} + + '@sentry/feedback@10.68.0': + resolution: {integrity: sha512-XbdcXiBnpC3vgw46eHOPeD/ZQ+XzluP75ubdUcaPDW02hCh2nsdXiwjZ2DBImbpvIpTbJgjHf/sIlHWvcZJ2Mg==} + engines: {node: '>=18'} + + '@sentry/react@10.68.0': + resolution: {integrity: sha512-rIq4QR4ScMHHx9JJZv7Jgw31bMdUVJMx+ykHIJb7htjY6mj78sjKs+KpCsMDnvJxDhSmvftGM1KfKD4BggL7OQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + + '@sentry/replay-canvas@10.68.0': + resolution: {integrity: sha512-HusYcr+He+ohnUDHunYrc5St6vdDnBXpUAndnT5ReyUMVSCiWKfY3paXowU/0787HwYfxdcpZgwC5u79+XbEIg==} + engines: {node: '>=18'} + + '@sentry/replay@10.68.0': + resolution: {integrity: sha512-ZoG2n16vbkx4GWSCnLIqUUN9xlUmccQFbQ2US2rhruQeHTUnHl/ukr8NHOQXZaEbwKyMkX9bEMwfmZHJm+wSTQ==} + engines: {node: '>=18'} + '@tanstack/history@1.162.0': resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} engines: {node: '>=20.19'} @@ -2280,6 +2317,47 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@sentry/browser-utils@10.68.0': + dependencies: + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + + '@sentry/browser@10.68.0': + dependencies: + '@sentry/browser-utils': 10.68.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + '@sentry/feedback': 10.68.0 + '@sentry/replay': 10.68.0 + '@sentry/replay-canvas': 10.68.0 + + '@sentry/conventions@0.16.0': {} + + '@sentry/core@10.68.0': + dependencies: + '@sentry/conventions': 0.16.0 + + '@sentry/feedback@10.68.0': + dependencies: + '@sentry/core': 10.68.0 + + '@sentry/react@10.68.0(react@18.3.1)': + dependencies: + '@sentry/browser': 10.68.0 + '@sentry/conventions': 0.16.0 + '@sentry/core': 10.68.0 + react: 18.3.1 + + '@sentry/replay-canvas@10.68.0': + dependencies: + '@sentry/core': 10.68.0 + '@sentry/replay': 10.68.0 + + '@sentry/replay@10.68.0': + dependencies: + '@sentry/browser-utils': 10.68.0 + '@sentry/core': 10.68.0 + '@tanstack/history@1.162.0': {} '@tanstack/query-core@5.101.2': {} diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 182aa7f..3acecf2 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,5 +1,6 @@ import { Component } from 'react'; import type { ErrorInfo, ReactNode } from 'react'; +import * as Sentry from '@sentry/react'; interface ErrorBoundaryProps { children: ReactNode; @@ -22,6 +23,7 @@ export class ErrorBoundary extends Component ({ + captureException: vi.fn(), +})); + +function ThrowingChild(): never { + throw new Error('boom'); +} + +describe('ErrorBoundary', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders children when there is no error', () => { + render( + +

Alles werkt

+
, + ); + expect(screen.getByText('Alles werkt')).toBeInTheDocument(); + }); + + it('renders the generic fallback message when a child throws', () => { + // Suppress the expected React error boundary console.error noise for this test. + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + , + ); + + expect(screen.getByTestId('error-boundary-fallback')).toBeInTheDocument(); + expect(screen.getByText('Er ging iets mis. Probeer de pagina te vernieuwen.')).toBeInTheDocument(); + + consoleErrorSpy.mockRestore(); + }); + + it('reports the caught error to Sentry', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + , + ); + + expect(Sentry.captureException).toHaveBeenCalledTimes(1); + expect(Sentry.captureException).toHaveBeenCalledWith( + expect.any(Error), + expect.objectContaining({ extra: expect.objectContaining({ componentStack: expect.any(String) }) }), + ); + + consoleErrorSpy.mockRestore(); + }); +}); diff --git a/src/main.tsx b/src/main.tsx index 0811297..c9cc812 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,16 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { RouterProvider } from '@tanstack/react-router'; +import * as Sentry from '@sentry/react'; import './fonts'; import './index.css'; import { router } from './router'; +const sentryDsn = import.meta.env.VITE_SENTRY_DSN; +if (sentryDsn) { + Sentry.init({ dsn: sentryDsn }); +} + const rootElement = document.getElementById('root'); if (!rootElement) { throw new Error('Root element "#root" not found in index.html'); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..231ace6 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + /** Sentry DSN (Data Source Name) for client-side error reporting. Not a secret — safe to expose in the client bundle. */ + readonly VITE_SENTRY_DSN?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index e203f3a..afd3167 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/fonts.ts","./src/main.tsx","./src/queryclient.ts","./src/router.tsx","./src/components/aboutsection.tsx","./src/components/contactsection.tsx","./src/components/errorboundary.tsx","./src/components/footer.tsx","./src/components/hero.tsx","./src/components/nav.tsx","./src/components/packagecard.tsx","./src/components/packagessection.tsx","./src/components/processsection.tsx","./src/components/processstep.tsx","./src/components/rootlayout.tsx","./src/components/themetoggle.tsx","./src/components/__tests__/nav.test.tsx","./src/components/__tests__/packagessection.test.tsx","./src/data/content.ts","./src/hooks/usepackagesquery.ts","./src/routes/__root.tsx","./src/routes/index.tsx","./src/test/setup.ts","./src/theme/themeprovider.tsx","./src/theme/tokens.ts","./src/theme/__tests__/themeprovider.test.tsx","./src/utils/scrolltohash.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/fonts.ts","./src/main.tsx","./src/queryclient.ts","./src/router.tsx","./src/vite-env.d.ts","./src/components/aboutsection.tsx","./src/components/contactsection.tsx","./src/components/errorboundary.tsx","./src/components/footer.tsx","./src/components/hero.tsx","./src/components/nav.tsx","./src/components/packagecard.tsx","./src/components/packagessection.tsx","./src/components/processsection.tsx","./src/components/processstep.tsx","./src/components/rootlayout.tsx","./src/components/themetoggle.tsx","./src/components/__tests__/errorboundary.test.tsx","./src/components/__tests__/nav.test.tsx","./src/components/__tests__/packagessection.test.tsx","./src/data/content.ts","./src/hooks/usepackagesquery.ts","./src/routes/__root.tsx","./src/routes/index.tsx","./src/test/setup.ts","./src/theme/themeprovider.tsx","./src/theme/tokens.ts","./src/theme/__tests__/themeprovider.test.tsx","./src/utils/scrolltohash.ts"],"version":"5.9.3"} \ No newline at end of file