1- import { Analytics } from '@vercel/analytics/react' ;
21import { useEffect , useState } from 'react' ;
32import { usePerformanceMonitoring } from '~/lib/performance' ;
3+ import * as Sentry from '@sentry/react' ;
44
5- interface PerformanceMonitorProps {
5+ interface DevToolsProps {
66 enabled ?: boolean ;
77}
88
9- export function PerformanceMonitor ( {
9+ type DevToolTab = 'performance' | 'errors' | 'network' | 'info' ;
10+
11+ export function DevTools ( {
1012 enabled = process . env . NODE_ENV === 'development' ,
11- } : PerformanceMonitorProps ) {
13+ } : DevToolsProps ) {
1214 const [ isVisible , setIsVisible ] = useState ( false ) ;
15+ const [ activeTab , setActiveTab ] = useState < DevToolTab > ( 'performance' ) ;
1316 const [ metrics , setMetrics ] = useState < any [ ] > ( [ ] ) ;
17+ const [ errors , setErrors ] = useState < any [ ] > ( [ ] ) ;
1418 const analytics = usePerformanceMonitoring ( ) ;
1519
1620 useEffect ( ( ) => {
@@ -21,16 +25,55 @@ export function PerformanceMonitor({
2125 setMetrics ( prev => [ event . detail , ...prev ] . slice ( 0 , 10 ) ) ;
2226 } ;
2327
28+ // Listen for error events
29+ const handleErrorEvent = ( event : CustomEvent ) => {
30+ setErrors ( prev => [ event . detail , ...prev ] . slice ( 0 , 10 ) ) ;
31+ } ;
32+
33+ // Listen for global errors
34+ const handleGlobalError = ( event : ErrorEvent ) => {
35+ setErrors ( prev =>
36+ [
37+ {
38+ type : 'JavaScript Error' ,
39+ message : event . message ,
40+ filename : event . filename ,
41+ lineno : event . lineno ,
42+ colno : event . colno ,
43+ timestamp : new Date ( ) . toISOString ( ) ,
44+ } ,
45+ ...prev ,
46+ ] . slice ( 0 , 10 )
47+ ) ;
48+ } ;
49+
2450 window . addEventListener (
2551 'performance-metric' ,
2652 handlePerformanceEvent as EventListener
2753 ) ;
54+ window . addEventListener (
55+ 'dev-tools-error' ,
56+ handleErrorEvent as EventListener
57+ ) ;
58+ window . addEventListener ( 'error' , handleGlobalError ) ;
59+
60+ // Initialize Sentry breadcrumb (silent)
61+ Sentry . addBreadcrumb ( {
62+ message : 'Dev Tools initialized' ,
63+ level : 'info' ,
64+ data : { component : 'DevTools' } ,
65+ } ) ;
2866
2967 return ( ) => {
3068 window . removeEventListener (
3169 'performance-metric' ,
3270 handlePerformanceEvent as EventListener
3371 ) ;
72+ window . removeEventListener (
73+ 'dev-tools-error' ,
74+ handleErrorEvent as EventListener
75+ ) ;
76+ window . removeEventListener ( 'error' , handleGlobalError ) ;
3477 } ;
3578 } , [ enabled , analytics ] ) ;
3679
@@ -65,40 +108,137 @@ export function PerformanceMonitor({
65108 return value > budget ? 'text-red-600' : 'text-green-600' ;
66109 } ;
67110
111+ const tabs = [
112+ { id : 'performance' as const , label : 'Perf' , icon : '📊' } ,
113+ { id : 'errors' as const , label : 'Errors' , icon : '🚨' } ,
114+ { id : 'network' as const , label : 'Network' , icon : '🌐' } ,
115+ { id : 'info' as const , label : 'Info' , icon : 'ℹ️' } ,
116+ ] ;
117+
118+ const renderTabContent = ( ) => {
119+ switch ( activeTab ) {
120+ case 'performance' :
121+ return (
122+ < div className = 'space-y-3' >
123+ { metrics . length > 0 ? (
124+ < div className = 'space-y-1 max-h-32 overflow-y-auto' >
125+ { metrics . map ( ( metric , index ) => (
126+ < div
127+ key = { index }
128+ className = 'text-xs border-l-2 border-gray-200 pl-2'
129+ >
130+ < div className = 'flex justify-between items-center' >
131+ < span className = 'font-mono text-gray-600' >
132+ { metric . name }
133+ </ span >
134+ < span
135+ className = { `font-semibold ${ getStatusColor ( metric . name , metric . value ) } ` }
136+ >
137+ { formatValue ( metric . value , metric . name ) }
138+ </ span >
139+ </ div >
140+ </ div >
141+ ) ) }
142+ </ div >
143+ ) : (
144+ < div className = 'text-gray-500 text-center py-2 text-xs' >
145+ No metrics yet
146+ </ div >
147+ ) }
148+ </ div >
149+ ) ;
150+
151+ case 'errors' :
152+ return (
153+ < div className = 'space-y-3' >
154+ { errors . length > 0 ? (
155+ < div className = 'space-y-1 max-h-32 overflow-y-auto' >
156+ { errors . map ( ( error , index ) => (
157+ < div
158+ key = { index }
159+ className = 'text-xs border-l-2 border-red-200 pl-2'
160+ >
161+ < div className = 'font-mono text-red-600' >
162+ { error . type || 'Error' }
163+ </ div >
164+ < div className = 'text-gray-500 truncate' >
165+ { error . message }
166+ </ div >
167+ { error . filename && (
168+ < div className = 'text-gray-400 text-xs' >
169+ { error . filename } :{ error . lineno }
170+ </ div >
171+ ) }
172+ </ div >
173+ ) ) }
174+ </ div >
175+ ) : (
176+ < div className = 'text-gray-500 text-center py-2 text-xs' >
177+ No errors detected
178+ </ div >
179+ ) }
180+ </ div >
181+ ) ;
182+
183+ case 'network' :
184+ return (
185+ < div className = 'space-y-3' >
186+ < div className = 'text-gray-500 text-center py-2 text-xs' >
187+ Network monitoring coming soon
188+ </ div >
189+ </ div >
190+ ) ;
191+
192+ case 'info' :
193+ return (
194+ < div className = 'space-y-3' >
195+ < div className = 'space-y-1 text-xs' >
196+ < div className = 'flex justify-between' >
197+ < span className = 'text-gray-500' > Environment:</ span >
198+ < span className = 'font-mono' > { import . meta. env . MODE } </ span >
199+ </ div >
200+ < div className = 'flex justify-between' >
201+ < span className = 'text-gray-500' > Sentry:</ span >
202+ < span className = 'font-mono' >
203+ { import . meta. env . VITE_SENTRY_DSN ? 'Enabled' : 'Disabled' }
204+ </ span >
205+ </ div >
206+ < div className = 'flex justify-between' >
207+ < span className = 'text-gray-500' > React Router:</ span >
208+ < span className = 'font-mono' > v7</ span >
209+ </ div >
210+ </ div >
211+ </ div >
212+ ) ;
213+
214+ default :
215+ return null ;
216+ }
217+ } ;
218+
68219 return (
69220 < >
70221 { /* Toggle Button */ }
71222 < button
72223 onClick = { ( ) => setIsVisible ( ! isVisible ) }
73- className = 'fixed bottom-4 right -4 z-50 bg-blue-600 text-white p-3 rounded-full shadow-lg hover:bg-blue-700 transition-colors'
74- title = 'Performance Monitor '
224+ className = 'fixed bottom-4 left -4 z-50 bg-gray-300 text-gray-700 p-2 rounded-lg shadow-lg hover:bg-gray-400 transition-colors'
225+ title = 'Development Tools '
75226 >
76- < svg
77- className = 'w-6 h-6'
78- fill = 'none'
79- stroke = 'currentColor'
80- viewBox = '0 0 24 24'
81- >
82- < path
83- strokeLinecap = 'round'
84- strokeLinejoin = 'round'
85- strokeWidth = { 2 }
86- d = 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
87- />
88- </ svg >
227+ < img src = '/stratos-icon.png' alt = 'Stratos' className = 'w-4 h-4' />
89228 </ button >
90229
91- { /* Monitor Panel */ }
230+ { /* Dev Tools Panel */ }
92231 { isVisible && (
93- < div className = 'fixed bottom-20 right-4 z-50 bg-white border border-gray-200 rounded-lg shadow-xl w-96 max-h-96 overflow-hidden' >
94- < div className = 'bg-gray-50 px-4 py-2 border-b border-gray-200 flex justify-between items-center' >
95- < h3 className = 'font-semibold text-gray-800' > Performance Monitor</ h3 >
232+ < div className = 'fixed bottom-16 left-4 z-50 bg-white border border-gray-200 rounded-lg shadow-xl w-80 max-h-80 overflow-hidden' >
233+ { /* Header */ }
234+ < div className = 'bg-gray-50 px-3 py-2 border-b border-gray-200 flex justify-between items-center' >
235+ < h3 className = 'font-semibold text-gray-800 text-sm' > Dev Tools</ h3 >
96236 < button
97237 onClick = { ( ) => setIsVisible ( false ) }
98238 className = 'text-gray-500 hover:text-gray-700'
99239 >
100240 < svg
101- className = 'w-4 h-4 '
241+ className = 'w-3 h-3 '
102242 fill = 'none'
103243 stroke = 'currentColor'
104244 viewBox = '0 0 24 24'
@@ -113,55 +253,28 @@ export function PerformanceMonitor({
113253 </ button >
114254 </ div >
115255
116- < div className = 'p-4 space-y-4' >
117- { /* Web Vitals Info */ }
118- < div className = 'text-sm text-gray-600' >
119- < p > Web Vitals are automatically tracked by Vercel Analytics.</ p >
120- < p > Check your Vercel dashboard for detailed metrics.</ p >
121- </ div >
122-
123- { /* Recent Metrics */ }
124- { metrics . length > 0 && (
125- < div >
126- < h4 className = 'text-sm font-medium text-gray-700 mb-2' >
127- Recent Metrics
128- </ h4 >
129- < div className = 'space-y-2 max-h-48 overflow-y-auto' >
130- { metrics . map ( ( metric , index ) => (
131- < div
132- key = { index }
133- className = 'text-xs border-l-2 border-gray-200 pl-2'
134- >
135- < div className = 'flex justify-between items-center' >
136- < span className = 'font-mono text-gray-600' >
137- { metric . name }
138- </ span >
139- < span
140- className = { `font-semibold ${ getStatusColor ( metric . name , metric . value ) } ` }
141- >
142- { formatValue ( metric . value , metric . name ) }
143- </ span >
144- </ div >
145- </ div >
146- ) ) }
147- </ div >
148- </ div >
149- ) }
150-
151- { metrics . length === 0 && (
152- < div className = 'text-gray-500 text-center py-4' >
153- < p > No custom metrics yet</ p >
154- < p className = 'text-xs mt-1' >
155- Web Vitals are tracked automatically
156- </ p >
157- </ div >
158- ) }
256+ { /* Tabs */ }
257+ < div className = 'flex border-b border-gray-200' >
258+ { tabs . map ( tab => (
259+ < button
260+ key = { tab . id }
261+ onClick = { ( ) => setActiveTab ( tab . id ) }
262+ className = { `flex-1 px-2 py-1 text-xs font-medium transition-colors ${
263+ activeTab === tab . id
264+ ? 'bg-blue-50 text-blue-600 border-b-2 border-blue-600'
265+ : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
266+ } `}
267+ >
268+ < span className = 'mr-1' > { tab . icon } </ span >
269+ { tab . label }
270+ </ button >
271+ ) ) }
159272 </ div >
273+
274+ { /* Content */ }
275+ < div className = 'p-3' > { renderTabContent ( ) } </ div >
160276 </ div >
161277 ) }
162-
163- { /* Vercel Analytics */ }
164- < Analytics />
165278 </ >
166279 ) ;
167280}
0 commit comments