diff --git a/package-lock.json b/package-lock.json index 11192f21..d8297901 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vite_react_shadcn_ts", "version": "0.0.0", "dependencies": { + "@cronitorio/cronitor-rum": "^0.4.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -654,6 +655,15 @@ "solid-js": "^1.8" } }, + "node_modules/@cronitorio/cronitor-rum": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cronitorio/cronitor-rum/-/cronitor-rum-0.4.1.tgz", + "integrity": "sha512-myOOC8Cvv+881hasEV/fsnUswKXMVKfHtioZIy5SCcRPvH2jCV7T/DyZLXjm97FaeWIfTlu4s5P0dKW1zi7T3A==", + "license": "Apache-2.0", + "dependencies": { + "web-vitals": "^3.3.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -12819,6 +12829,12 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/web-vitals": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.2.tgz", + "integrity": "sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index b2329cd3..c1dfb536 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "preview": "vite preview" }, "dependencies": { + "@cronitorio/cronitor-rum": "^0.4.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", diff --git a/src/App.tsx b/src/App.tsx index 82170995..8d7e817d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom"; +import * as Cronitor from '@cronitorio/cronitor-rum'; import { AuthProvider } from "@/hooks/useAuth"; import { AuthModalProvider } from "@/contexts/AuthModalContext"; import { MFAStepUpProvider } from "@/contexts/MFAStepUpContext"; @@ -387,20 +388,33 @@ function AppContent(): React.JSX.Element { ); } -const App = (): React.JSX.Element => ( - - - - - - - - - - - {import.meta.env.DEV && } - - -); +const App = (): React.JSX.Element => { + // Initialize Cronitor RUM on app mount + useEffect(() => { + Cronitor.load("0b5d17d3f7625ce8766c2c4c85c1895d", { + debug: import.meta.env.DEV, // Enable debug logs in development only + trackMode: 'history', // Automatically track page views with React Router + }); + + // Log successful initialization + console.log('[Cronitor] RUM initialized'); + }, []); + + return ( + + + + + + + + + + + {import.meta.env.DEV && } + + + ); +}; export default App; diff --git a/src/lib/errorBreadcrumbs.ts b/src/lib/errorBreadcrumbs.ts index 1ae5de05..07022386 100644 --- a/src/lib/errorBreadcrumbs.ts +++ b/src/lib/errorBreadcrumbs.ts @@ -58,15 +58,42 @@ export const breadcrumb = { level: 'info', data, }); + + // Track critical user actions in Cronitor + const criticalActions = ['submit', 'delete', 'update', 'create', 'login', 'logout']; + const isCritical = criticalActions.some(ca => action.toLowerCase().includes(ca)); + + if (isCritical && typeof window !== 'undefined' && window.cronitor) { + window.cronitor.track('critical_user_action', { + action, + component, + timestamp: new Date().toISOString(), + }); + } }, apiCall: (endpoint: string, method: string, status?: number) => { + const isError = status && status >= 400; + breadcrumbManager.add({ category: 'api_call', message: `API ${method} ${endpoint}`, - level: status && status >= 400 ? 'error' : 'info', + level: isError ? 'error' : 'info', data: { endpoint, method, status }, }); + + // Track significant API calls in Cronitor + if (typeof window !== 'undefined' && window.cronitor) { + // Only track errors and slow requests + if (isError || (status && status >= 500)) { + window.cronitor.track('api_error', { + endpoint, + method, + status, + severity: status >= 500 ? 'high' : 'medium', + }); + } + } }, stateChange: (description: string, data?: Record) => { diff --git a/src/lib/errorHandler.ts b/src/lib/errorHandler.ts index 255c8572..79e21935 100644 --- a/src/lib/errorHandler.ts +++ b/src/lib/errorHandler.ts @@ -4,6 +4,16 @@ import { supabase } from '@/integrations/supabase/client'; import { breadcrumbManager } from './errorBreadcrumbs'; import { captureEnvironmentContext } from './environmentContext'; +// Cronitor RUM integration +declare global { + interface Window { + cronitor?: { + track: (eventName: string, data?: Record) => void; + error: (error: Error | string, metadata?: Record) => void; + }; + } +} + export type ErrorContext = { action: string; userId?: string; @@ -123,6 +133,25 @@ export const handleError = ( }); } + // Track error in Cronitor RUM + if (typeof window !== 'undefined' && window.cronitor) { + try { + window.cronitor.error( + error instanceof Error ? error : new Error(errorMessage), + { + errorId, + errorName, + action: context.action, + userId: context.userId, + supabaseError: supabaseErrorDetails, + breadcrumbCount: breadcrumbManager.getAll().length, + } + ); + } catch (cronitorError) { + logger.error('Failed to track error in Cronitor', { cronitorError }); + } + } + // Log to database with breadcrumbs (non-blocking) try { const envContext = captureEnvironmentContext(); @@ -218,6 +247,21 @@ export const handleNonCriticalError = ( severity: 'low', }); + // Track non-critical error in Cronitor (lower severity) + if (typeof window !== 'undefined' && window.cronitor) { + try { + window.cronitor.track('non_critical_error', { + errorId, + errorMessage, + action: context.action, + userId: context.userId, + severity: 'low', + }); + } catch (cronitorError) { + logger.error('Failed to track non-critical error in Cronitor', { cronitorError }); + } + } + // Log to database with breadcrumbs (non-blocking, fire-and-forget) try { const envContext = captureEnvironmentContext(); diff --git a/src/lib/retryHelpers.ts b/src/lib/retryHelpers.ts index 643ad6bd..d33e6292 100644 --- a/src/lib/retryHelpers.ts +++ b/src/lib/retryHelpers.ts @@ -243,9 +243,28 @@ export async function withRetry( error: error instanceof Error ? error.message : String(error) }); + // Track exhausted retries in Cronitor + if (typeof window !== 'undefined' && window.cronitor) { + window.cronitor.track('retries_exhausted', { + totalAttempts: config.maxAttempts, + error: error instanceof Error ? error.name : 'UnknownError', + severity: 'high', + }); + } + throw error; } + // Track retry attempts in Cronitor + if (attempt > 0 && typeof window !== 'undefined' && window.cronitor) { + window.cronitor.track('retry_attempt', { + attempt: attempt + 1, + maxAttempts: config.maxAttempts, + error: error instanceof Error ? error.name : 'UnknownError', + willRetry: attempt < config.maxAttempts - 1, + }); + } + // Calculate delay for next attempt const delay = calculateBackoffDelay(attempt, config); diff --git a/src/types/cronitor.d.ts b/src/types/cronitor.d.ts new file mode 100644 index 00000000..808fda32 --- /dev/null +++ b/src/types/cronitor.d.ts @@ -0,0 +1,23 @@ +declare module '@cronitorio/cronitor-rum' { + export interface CronitorConfig { + debug?: boolean; + trackMode?: 'history' | 'off'; + } + + export function load(apiKey: string, config?: CronitorConfig): void; + + export function track(eventName: string, data?: Record): void; + + export function error(error: Error | string, metadata?: Record): void; +} + +declare global { + interface Window { + cronitor?: { + track: (eventName: string, data?: Record) => void; + error: (error: Error | string, metadata?: Record) => void; + }; + } +} + +export {};