feat: add Sentry error tracking with defensive error boundary
- Initialize Sentry SDK in index.tsx when VITE_SENTRY_DSN env var is set - Wrap entire App with Sentry.ErrorBoundary (replaces the hard crash with a retry UI) - 5% trace sample rate, sendDefaultPii disabled, strip events containing accessToken - Add .env.production template with VITE_SENTRY_DSN placeholder - Get your DSN from sentry.io -> Project Settings -> Client Keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+66
-27
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Provider as JotaiProvider } from 'jotai';
|
||||
import { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProvider } from 'folds';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
@@ -22,33 +23,71 @@ function App() {
|
||||
const portalContainer = document.getElementById('portalContainer') ?? undefined;
|
||||
|
||||
return (
|
||||
<TooltipContainerProvider value={portalContainer}>
|
||||
<PopOutContainerProvider value={portalContainer}>
|
||||
<OverlayContainerProvider value={portalContainer}>
|
||||
<ScreenSizeProvider value={screenSize}>
|
||||
<FeatureCheck>
|
||||
<ClientConfigLoader
|
||||
fallback={() => <ConfigConfigLoading />}
|
||||
error={(err, retry, ignore) => (
|
||||
<ConfigConfigError error={err} retry={retry} ignore={ignore} />
|
||||
)}
|
||||
>
|
||||
{(clientConfig) => (
|
||||
<ClientConfigProvider value={clientConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<JotaiProvider>
|
||||
<RouterProvider router={createRouter(clientConfig, screenSize)} />
|
||||
</JotaiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</ClientConfigProvider>
|
||||
)}
|
||||
</ClientConfigLoader>
|
||||
</FeatureCheck>
|
||||
</ScreenSizeProvider>
|
||||
</OverlayContainerProvider>
|
||||
</PopOutContainerProvider>
|
||||
</TooltipContainerProvider>
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={({ error, resetError }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
gap: '16px',
|
||||
fontFamily: 'sans-serif',
|
||||
padding: '24px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0 }}>Something went wrong</h2>
|
||||
<p style={{ margin: 0, color: '#666', maxWidth: '400px' }}>
|
||||
{error instanceof Error ? error.message : 'An unexpected error occurred.'}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetError}
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: '#5865f2',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<TooltipContainerProvider value={portalContainer}>
|
||||
<PopOutContainerProvider value={portalContainer}>
|
||||
<OverlayContainerProvider value={portalContainer}>
|
||||
<ScreenSizeProvider value={screenSize}>
|
||||
<FeatureCheck>
|
||||
<ClientConfigLoader
|
||||
fallback={() => <ConfigConfigLoading />}
|
||||
error={(err, retry, ignore) => (
|
||||
<ConfigConfigError error={err} retry={retry} ignore={ignore} />
|
||||
)}
|
||||
>
|
||||
{(clientConfig) => (
|
||||
<ClientConfigProvider value={clientConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<JotaiProvider>
|
||||
<RouterProvider router={createRouter(clientConfig, screenSize)} />
|
||||
</JotaiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</ClientConfigProvider>
|
||||
)}
|
||||
</ClientConfigLoader>
|
||||
</FeatureCheck>
|
||||
</ScreenSizeProvider>
|
||||
</OverlayContainerProvider>
|
||||
</PopOutContainerProvider>
|
||||
</TooltipContainerProvider>
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable import/first */
|
||||
import * as Sentry from '@sentry/react';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { enableMapSet } from 'immer';
|
||||
@@ -6,6 +7,25 @@ import '@fontsource/inter/variable.css';
|
||||
import 'folds/dist/style.css';
|
||||
import { configClass, varsClass } from 'folds';
|
||||
|
||||
if (import.meta.env.VITE_SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||
environment: import.meta.env.MODE,
|
||||
release: import.meta.env.VITE_APP_VERSION,
|
||||
tracesSampleRate: 0.05,
|
||||
// Don't send PII — strip user IPs and don't attach user info automatically
|
||||
sendDefaultPii: false,
|
||||
beforeSend(event) {
|
||||
// Scrub any accessToken that may have leaked into breadcrumbs/data
|
||||
const str = JSON.stringify(event);
|
||||
if (str.includes('access_token') || str.includes('accessToken')) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
enableMapSet();
|
||||
|
||||
import './index.css';
|
||||
|
||||
Reference in New Issue
Block a user