Add temporary Sentry test button (dev/test only)

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-07-25 13:46:43 +02:00
co-authored by Junie
parent 524b5a6dc7
commit c90300b9a4
8 changed files with 107 additions and 0 deletions
+2
View File
@@ -1,6 +1,7 @@
import type { ReactNode } from 'react';
import { Nav } from './Nav';
import { Footer } from './Footer';
import { SentryTestButton } from './SentryTestButton';
export function RootLayout({ children }: { children: ReactNode }) {
return (
@@ -8,6 +9,7 @@ export function RootLayout({ children }: { children: ReactNode }) {
<Nav />
{children}
<Footer />
<SentryTestButton />
</div>
);
}
+47
View File
@@ -0,0 +1,47 @@
import * as Sentry from '@sentry/react';
/**
* Alleen zichtbaar tijdens lokale development (`pnpm dev`) en in de testomgeving
* (waar de build met `VITE_APP_ENV=test` wordt gebouwd, zie continuous_integration.yaml).
* In een productiebuild (VITE_APP_ENV ontbreekt of is 'production') is deze knop
* altijd verborgen, ook als iemand vergeet de variabele expliciet te zetten.
*/
const isVisible = import.meta.env.DEV || import.meta.env.VITE_APP_ENV === 'test';
/**
* Losstaand van het component zodat dit direct (zonder DOM/React event-dispatch)
* getest kan worden. React vangt fouten uit event handlers namelijk NIET op via
* een ErrorBoundary (die vangt alleen render-/lifecycle-fouten). De fout hieronder
* blijft dus bewust een onafgevangen (uncaught) fout, die door Sentry's eigen
* globale `window.onerror`-handler wordt opgepikt — precies zoals bedoeld voor
* deze test-knop.
*/
export function handleSentryTestErrorClick(): void {
// Send a log before throwing the error
Sentry.logger.info('User triggered test error', {
action: 'test_error_button_click',
});
// Send a test metric before throwing the error
Sentry.metrics.count('test_counter', 1);
throw new Error('This is your first error!');
}
/**
* Tijdelijke testknop om te verifiëren dat Sentry errors, logs en metrics
* daadwerkelijk binnenkomen.
*/
export function SentryTestButton() {
if (!isVisible) {
return null;
}
return (
<button
type="button"
onClick={handleSentryTestErrorClick}
className="fixed bottom-4 right-4 z-50 rounded bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-lg hover:bg-red-700"
>
Break the world
</button>
);
}
@@ -0,0 +1,30 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as Sentry from '@sentry/react';
import { SentryTestButton, handleSentryTestErrorClick } from '../SentryTestButton';
vi.mock('@sentry/react', () => ({
captureException: vi.fn(),
logger: { info: vi.fn() },
metrics: { count: vi.fn() },
}));
describe('SentryTestButton', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('is visible in the current (development/test) build', () => {
render(<SentryTestButton />);
expect(screen.getByRole('button', { name: 'Break the world' })).toBeInTheDocument();
});
it('sends a log and a metric to Sentry, then throws an intentional test error', () => {
expect(() => handleSentryTestErrorClick()).toThrow('This is your first error!');
expect(Sentry.logger.info).toHaveBeenCalledWith('User triggered test error', {
action: 'test_error_button_click',
});
expect(Sentry.metrics.count).toHaveBeenCalledWith('test_counter', 1);
});
});
+2
View File
@@ -15,6 +15,8 @@ if (sentryDsn) {
integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)],
// Low-traffic marketing site: capture all traces (lower this if traffic grows significantly).
tracesSampleRate: 1.0,
// Required for Sentry.logger.* calls (e.g. the temporary SentryTestButton) to actually be sent.
enableLogs: true,
});
}
+2
View File
@@ -3,6 +3,8 @@
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;
/** Build-time deployment environment tag ('test' | 'production' | undefined). Used to hide dev/test-only UI (e.g. SentryTestButton) from production builds. */
readonly VITE_APP_ENV?: string;
}
interface ImportMeta {