diff --git a/web/package-lock.json b/web/package-lock.json index 038a9f85..860a2e2a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -41,6 +41,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", + "@sentry/react": "^10.26.0", "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.56.2", "@types/uuid": "^10.0.0", @@ -2788,6 +2789,98 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.26.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.26.0.tgz", + "integrity": "sha512-rPg1+JZlfp912pZONQAWZzbSaZ9L6R2VrMcCEa+2e2Gqk9um4b+LqF5RQWZsbt5Z0n0azSy/KQ6zAe/zTPXSOg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.26.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.26.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.26.0.tgz", + "integrity": "sha512-0vk9eQP0CXD7Y2WkcCIWHaAqnXOAi18/GupgWLnbB2kuQVYVtStWxtW+OWRe8W/XwSnZ5m6JBTVeokuk/O16DQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.26.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.26.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.26.0.tgz", + "integrity": "sha512-FMySQnY2/p0dVtFUBgUO+aMdK2ovqnd7Q/AkvMQUsN/5ulyj6KZx3JX3CqOqRtAr1izoCe4Kh2pi5t//sQmvsg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.26.0", + "@sentry/core": "10.26.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.26.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.26.0.tgz", + "integrity": "sha512-vs7d/P+8M1L1JVAhhJx2wo15QDhqAipnEQvuRZ6PV7LUcS1un9/Vx49FMxpIkx6JcKADJVwtXrS1sX2hoNT/kw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.26.0", + "@sentry/core": "10.26.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.26.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.26.0.tgz", + "integrity": "sha512-uvV4hnkt8bh8yP0disJ0fszy8FdnkyGtzyIVKdeQZbNUefwbDhd3H0KJrAHhJ5ocULMH3B+dipdPmw2QXbEflg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.26.0", + "@sentry-internal/feedback": "10.26.0", + "@sentry-internal/replay": "10.26.0", + "@sentry-internal/replay-canvas": "10.26.0", + "@sentry/core": "10.26.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/core": { + "version": "10.26.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.26.0.tgz", + "integrity": "sha512-TjDe5QI37SLuV0q3nMOH8JcPZhv2e85FALaQMIhRILH9Ce6G7xW5GSjmH91NUVq8yc3XtiqYlz/EenEZActc4Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react": { + "version": "10.26.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.26.0.tgz", + "integrity": "sha512-Qi0/FVXAalwQNr8zp0tocViH3+MRelW8ePqj3TdMzapkbXRuh07czdGgw8Zgobqcb7l4rRCRAUo2sl/H3KVkIw==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -3132,20 +3225,6 @@ } } }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -6014,6 +6093,21 @@ "node": ">=12.0.0" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", diff --git a/web/package.json b/web/package.json index 5d8b6a89..dd7e3d30 100644 --- a/web/package.json +++ b/web/package.json @@ -46,6 +46,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", + "@sentry/react": "^10.26.0", "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.56.2", "@types/uuid": "^10.0.0", diff --git a/web/src/App.tsx b/web/src/App.tsx index 92987f6c..520737d7 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,9 +12,13 @@ import { ReferralProvider } from '@/contexts/ReferralContext'; import { ProtectedRoute } from '@/components/ProtectedRoute'; import { Toaster } from '@/components/ui/toaster'; import { ErrorBoundary } from '@/components/ErrorBoundary'; +import { initSentry } from '@/lib/sentry'; import './App.css'; import './i18n/config'; +// Initialize Sentry error monitoring +initSentry(); + // Lazy load pages for code splitting const Index = lazy(() => import('@/pages/Index')); const Login = lazy(() => import('@/pages/Login')); diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 215d1ca0..26aa4ab4 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -42,6 +42,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children localStorage.setItem(LAST_ACTIVITY_KEY, Date.now().toString()); }, []); + const signOut = useCallback(async () => { + setIsAdmin(false); + setUser(null); + localStorage.removeItem(LAST_ACTIVITY_KEY); + await supabase.auth.signOut(); + }, []); + // Check if session has timed out const checkSessionTimeout = useCallback(async () => { if (!user) return; @@ -49,8 +56,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const lastActivity = localStorage.getItem(LAST_ACTIVITY_KEY); if (!lastActivity) { updateLastActivity(); - - return; } @@ -62,7 +67,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children if (import.meta.env.DEV) console.log('⏱️ Session timeout - logging out due to inactivity'); await signOut(); } - }, [user]); + }, [user, updateLastActivity, signOut]); // Setup activity listeners useEffect(() => { @@ -129,9 +134,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children subscription.unsubscribe(); window.removeEventListener('walletChanged', handleWalletChange); }; - }, []); + }, [checkAdminStatus]); - const checkAdminStatus = async () => { + const checkAdminStatus = useCallback(async () => { // Admin wallet whitelist (blockchain-based auth) const ADMIN_WALLETS = [ '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', // Founder (original) @@ -170,12 +175,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children if (import.meta.env.DEV) console.log('❌ Admin access denied'); setIsAdmin(false); return false; - } catch { + } catch (err) { if (import.meta.env.DEV) console.error('Admin check error:', err); setIsAdmin(false); return false; } - }; + }, []); const signIn = async (email: string, password: string) => { try { @@ -238,22 +243,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } }; - const signOut = async () => { - setIsAdmin(false); - setUser(null); - localStorage.removeItem(LAST_ACTIVITY_KEY); - await supabase.auth.signOut(); - }; - return ( - {children} diff --git a/web/src/lib/sentry.ts b/web/src/lib/sentry.ts new file mode 100644 index 00000000..26576a9f --- /dev/null +++ b/web/src/lib/sentry.ts @@ -0,0 +1,87 @@ +import * as Sentry from '@sentry/react'; + +export const initSentry = () => { + const dsn = import.meta.env.VITE_SENTRY_DSN; + + // Only initialize if DSN is provided and not in development + if (!dsn || import.meta.env.DEV) { + if (import.meta.env.DEV) { + console.log('📊 Sentry disabled in development'); + } + return; + } + + Sentry.init({ + dsn, + environment: import.meta.env.VITE_SENTRY_ENVIRONMENT || 'production', + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration({ + maskAllText: false, + blockAllMedia: false, + }), + ], + + // Performance Monitoring + tracesSampleRate: parseFloat(import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE || '0.1'), + + // Session Replay + replaysSessionSampleRate: 0.1, // 10% of sessions + replaysOnErrorSampleRate: 1.0, // 100% of sessions with errors + + // Filter out sensitive data + beforeSend(event) { + // Don't send errors in development + if (import.meta.env.DEV) { + return null; + } + + // Filter out wallet addresses and sensitive data + if (event.request?.url) { + event.request.url = event.request.url.replace(/5[A-HJ-NP-Za-km-z]{47}/g, '[REDACTED_WALLET]'); + } + + if (event.breadcrumbs) { + event.breadcrumbs = event.breadcrumbs.map(breadcrumb => { + if (breadcrumb.data) { + breadcrumb.data = JSON.parse( + JSON.stringify(breadcrumb.data).replace(/5[A-HJ-NP-Za-km-z]{47}/g, '[REDACTED_WALLET]') + ); + } + return breadcrumb; + }); + } + + return event; + }, + + // Ignore common non-critical errors + ignoreErrors: [ + // Browser extensions + 'top.GLOBALS', + 'canvas.contentDocument', + 'MyApp_RemoveAllHighlights', + 'atomicFindClose', + // Network errors that are expected + 'NetworkError', + 'Failed to fetch', + 'Load failed', + // Polkadot.js expected disconnections + 'WebSocket is not connected', + 'RPC connection closed', + ], + }); + + // Set user context when available + const selectedWallet = localStorage.getItem('selectedWallet'); + if (selectedWallet) { + Sentry.setUser({ + id: selectedWallet.slice(0, 8), // Only first 8 chars for privacy + }); + } + + console.log('📊 Sentry initialized'); +}; + +// Export Sentry for use in error boundaries and manual reporting +export { Sentry };