1- 'use client' ;
2- import { useEffect , useRef , useState , useMemo } from 'react' ;
3- import * as Sentry from '@sentry/nextjs' ;
1+ 'use client'
2+ import { useRef , useState , useMemo } from 'react'
43import { detectBotFromUserAgent } from '@/utils/logEvent'
5- import DOMPurify from 'dompurify' ;
4+ import DOMPurify from 'dompurify'
65
76function snapshotSanitizedSSR ( ) : string | null {
87 try {
9- if ( typeof document === 'undefined' ) return null ;
8+ if ( typeof document === 'undefined' ) return null
109
1110 // Clone current body and strip dangerous elements
12- const bodyClone = document . body . cloneNode ( true ) as HTMLElement ;
13- bodyClone . querySelectorAll ( 'script, object, embed, iframe' ) . forEach ( ( n ) => n . remove ( ) ) ;
14- bodyClone . querySelectorAll ( 'link[rel="modulepreload"], link[as="script"]' ) . forEach ( ( n ) => n . remove ( ) ) ;
15-
16- const html = bodyClone . innerHTML || '' ;
17-
18- if ( html . trim ( ) . length === 0 ) return null ;
19-
11+ const bodyClone = document . body . cloneNode ( true ) as HTMLElement
12+ bodyClone . querySelectorAll ( 'script, object, embed, iframe' ) . forEach ( ( n ) => n . remove ( ) )
13+ bodyClone
14+ . querySelectorAll ( 'link[rel="modulepreload"], link[as="script"]' )
15+ . forEach ( ( n ) => n . remove ( ) )
16+
17+ const html = bodyClone . innerHTML || ''
18+
19+ if ( html . trim ( ) . length === 0 ) return null
20+
2021 // Sanitize with DOMPurify for enhanced security
2122 const sanitized = DOMPurify . sanitize ( html , {
2223 ALLOWED_TAGS : [
23- 'div' , 'span' , 'p' , 'h1' , 'h2' , 'h3' , 'h4' , 'h5' , 'h6' ,
24- 'a' , 'img' , 'ul' , 'ol' , 'li' , 'br' , 'strong' , 'em' , 'b' , 'i' ,
25- 'article' , 'section' , 'nav' , 'header' , 'footer' , 'main' , 'aside' ,
26- 'table' , 'thead' , 'tbody' , 'tr' , 'td' , 'th' , 'code' , 'pre' ,
27- 'blockquote' , 'figure' , 'figcaption' , 'time' , 'small'
24+ 'div' ,
25+ 'span' ,
26+ 'p' ,
27+ 'h1' ,
28+ 'h2' ,
29+ 'h3' ,
30+ 'h4' ,
31+ 'h5' ,
32+ 'h6' ,
33+ 'a' ,
34+ 'img' ,
35+ 'ul' ,
36+ 'ol' ,
37+ 'li' ,
38+ 'br' ,
39+ 'strong' ,
40+ 'em' ,
41+ 'b' ,
42+ 'i' ,
43+ 'article' ,
44+ 'section' ,
45+ 'nav' ,
46+ 'header' ,
47+ 'footer' ,
48+ 'main' ,
49+ 'aside' ,
50+ 'table' ,
51+ 'thead' ,
52+ 'tbody' ,
53+ 'tr' ,
54+ 'td' ,
55+ 'th' ,
56+ 'code' ,
57+ 'pre' ,
58+ 'blockquote' ,
59+ 'figure' ,
60+ 'figcaption' ,
61+ 'time' ,
62+ 'small' ,
2863 ] ,
2964 ALLOWED_ATTR : [
30- 'class' , 'id' , 'href' , 'src' , 'alt' , 'title' , 'role' ,
31- 'aria-label' , 'aria-labelledby' , 'aria-describedby' ,
32- 'data-testid' , 'data-id' , 'width' , 'height'
65+ 'class' ,
66+ 'id' ,
67+ 'href' ,
68+ 'src' ,
69+ 'alt' ,
70+ 'title' ,
71+ 'role' ,
72+ 'aria-label' ,
73+ 'aria-labelledby' ,
74+ 'aria-describedby' ,
75+ 'data-testid' ,
76+ 'data-id' ,
77+ 'width' ,
78+ 'height' ,
3379 ] ,
34- FORBID_TAGS : [ 'script' , 'object' , 'embed' , 'iframe' , 'form' , 'input' , 'button' , 'select' , 'textarea' ] ,
35- FORBID_ATTR : [ 'onload' , 'onclick' , 'onerror' , 'onmouseover' , 'onmouseout' , 'onfocus' , 'onblur' , 'style' ] ,
36- KEEP_CONTENT : true // Keep text content even if wrapper is removed
37- } ) ;
38-
39- return sanitized . trim ( ) . length > 0 ? sanitized : null ;
80+ FORBID_TAGS : [
81+ 'script' ,
82+ 'object' ,
83+ 'embed' ,
84+ 'iframe' ,
85+ 'form' ,
86+ 'input' ,
87+ 'button' ,
88+ 'select' ,
89+ 'textarea' ,
90+ ] ,
91+ FORBID_ATTR : [
92+ 'onload' ,
93+ 'onclick' ,
94+ 'onerror' ,
95+ 'onmouseover' ,
96+ 'onmouseout' ,
97+ 'onfocus' ,
98+ 'onblur' ,
99+ 'style' ,
100+ ] ,
101+ KEEP_CONTENT : true , // Keep text content even if wrapper is removed
102+ } )
103+
104+ return sanitized . trim ( ) . length > 0 ? sanitized : null
40105 } catch {
41- return null ;
106+ return null
42107 }
43108}
44109
45- export default function GlobalError ( { error, reset } : { error : Error & { digest ?: string } , reset : ( ) => void } ) {
46- const preservedRef = useRef < string | null > ( null ) ;
110+ export default function GlobalError ( {
111+ reset,
112+ } : {
113+ error : Error & { digest ?: string }
114+ reset : ( ) => void
115+ } ) {
116+ const preservedRef = useRef < string | null > ( null )
47117 if ( preservedRef . current === null ) {
48118 // Attempt to grab SSR HTML immediately
49- preservedRef . current = snapshotSanitizedSSR ( ) ;
119+ preservedRef . current = snapshotSanitizedSSR ( )
50120 }
51121
52- const [ showOverlay , setShowOverlay ] = useState ( true ) ;
122+ const [ showOverlay , setShowOverlay ] = useState ( true )
53123
54124 // Overlay is not shown for bot requests, using the same bot detection as in middleware.ts
55125 const isBotUA = ( ( ) => {
56126 if ( typeof navigator === 'undefined' ) return false
57127 const { isBot } = detectBotFromUserAgent ( navigator . userAgent || '' )
58128 return isBot
59- } ) ( ) ;
129+ } ) ( )
60130
61- useEffect ( ( ) => {
62- try {
63- Sentry . withScope ( ( scope ) => {
64- scope . setTag ( 'errorBoundary' , 'global' )
65- scope . setTag ( 'severity' , 'critical' )
66- scope . setLevel ( 'fatal' )
67- if ( typeof window !== 'undefined' ) {
68- scope . setContext ( 'browser' , {
69- url : window . location . href ,
70- userAgent : window . navigator . userAgent ,
71- preservedSSR : preservedRef . current ,
72- } )
73- }
74- scope . setContext ( 'error' , {
75- name : error . name ,
76- message : error . message ,
77- fullError : error ,
78- } )
79- Sentry . captureException ( error )
80- } )
81- } catch ( _ ) { /* cannot do much if Sentry fails */ }
82- } , [ error ] )
83-
84- const hasPreserved = Boolean ( preservedRef . current && preservedRef . current . length > 200 ) ;
131+ const hasPreserved = Boolean ( preservedRef . current && preservedRef . current . length > 200 )
85132
86133 // Memoize the sanitized HTML to avoid re-processing
87134 const sanitizedHTML = useMemo ( ( ) => {
88- if ( ! preservedRef . current ) return '' ;
89- return preservedRef . current ; // Already sanitized in snapshotSanitizedSSR
90- } , [ ] ) ;
135+ if ( ! preservedRef . current ) return ''
136+ return preservedRef . current // Already sanitized in snapshotSanitizedSSR
137+ } , [ ] )
91138
92139 return (
93140 < html lang = "en" >
@@ -109,54 +156,139 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
109156 content = "SigNoz is an open-source observability tool powered by OpenTelemetry. Get APM, logs, traces, metrics, exceptions, & alerts in a single tool."
110157 />
111158 </ head >
112- < body style = { {
113- background : '#111113' , color : '#f3f4f6' , margin : 0 , padding : 0 , fontFamily : 'system-ui,sans-serif' , minHeight : '100vh' , maxHeight : 'fit-content'
114- } } >
159+ < body
160+ style = { {
161+ background : '#111113' ,
162+ color : '#f3f4f6' ,
163+ margin : 0 ,
164+ padding : 0 ,
165+ fontFamily : 'system-ui,sans-serif' ,
166+ minHeight : '100vh' ,
167+ maxHeight : 'fit-content' ,
168+ } }
169+ >
115170 { hasPreserved && showOverlay && ! isBotUA && (
116- < div style = { {
117- position : 'fixed' , bottom : 0 , left : 0 , right : 0 , zIndex : 100000 ,
118- backgroundColor : 'rgba(249, 115, 22, 0.95)' , color : '#fff' , padding : '10px 16px' ,
119- display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' ,
120- borderTop : '2px solid #ea580c' , boxShadow : '0 -8px 24px rgba(0,0,0,0.25)'
121- } } >
171+ < div
172+ style = { {
173+ position : 'fixed' ,
174+ bottom : 0 ,
175+ left : 0 ,
176+ right : 0 ,
177+ zIndex : 100000 ,
178+ backgroundColor : 'rgba(249, 115, 22, 0.95)' ,
179+ color : '#fff' ,
180+ padding : '10px 16px' ,
181+ display : 'flex' ,
182+ alignItems : 'center' ,
183+ justifyContent : 'space-between' ,
184+ borderTop : '2px solid #ea580c' ,
185+ boxShadow : '0 -8px 24px rgba(0,0,0,0.25)' ,
186+ } }
187+ >
122188 < div style = { { display : 'flex' , alignItems : 'center' , gap : 10 } } >
123- < div style = { { width : 22 , height : 22 , background : '#fff' , color : '#f97316' , borderRadius : '50%' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , fontSize : 13 , fontWeight : 700 } } > !</ div >
124- < div > < strong > Resource loading issue.</ strong > The page below is server-rendered and still readable.</ div >
189+ < div
190+ style = { {
191+ width : 22 ,
192+ height : 22 ,
193+ background : '#fff' ,
194+ color : '#f97316' ,
195+ borderRadius : '50%' ,
196+ display : 'flex' ,
197+ alignItems : 'center' ,
198+ justifyContent : 'center' ,
199+ fontSize : 13 ,
200+ fontWeight : 700 ,
201+ } }
202+ >
203+ !
204+ </ div >
205+ < div >
206+ < strong > Resource loading issue.</ strong > The page below is server-rendered and still
207+ readable.
208+ </ div >
125209 </ div >
126210 < div style = { { display : 'flex' , gap : 8 } } >
127- < button onClick = { ( ) => ( typeof window !== 'undefined' ? window . location . reload ( ) : reset ( ) ) } style = { { background : '#fff' , color : '#f97316' , border : 'none' , borderRadius : 6 , padding : '6px 10px' , fontSize : 12 , fontWeight : 600 , cursor : 'pointer' } } > Reload</ button >
128- < button onClick = { ( ) => setShowOverlay ( false ) } style = { { background : 'transparent' , color : '#fff' , border : '1px solid #fff' , borderRadius : 6 , padding : '6px 10px' , fontSize : 12 , fontWeight : 600 , cursor : 'pointer' } } > Dismiss</ button >
211+ < button
212+ onClick = { ( ) => ( typeof window !== 'undefined' ? window . location . reload ( ) : reset ( ) ) }
213+ style = { {
214+ background : '#fff' ,
215+ color : '#f97316' ,
216+ border : 'none' ,
217+ borderRadius : 6 ,
218+ padding : '6px 10px' ,
219+ fontSize : 12 ,
220+ fontWeight : 600 ,
221+ cursor : 'pointer' ,
222+ } }
223+ >
224+ Reload
225+ </ button >
226+ < button
227+ onClick = { ( ) => setShowOverlay ( false ) }
228+ style = { {
229+ background : 'transparent' ,
230+ color : '#fff' ,
231+ border : '1px solid #fff' ,
232+ borderRadius : 6 ,
233+ padding : '6px 10px' ,
234+ fontSize : 12 ,
235+ fontWeight : 600 ,
236+ cursor : 'pointer' ,
237+ } }
238+ >
239+ Dismiss
240+ </ button >
129241 </ div >
130242 </ div >
131243 ) }
132244
133245 { hasPreserved ? (
134246 < div dangerouslySetInnerHTML = { { __html : sanitizedHTML } } />
135247 ) : (
136- < div style = { {
137- margin : 'auto' , maxWidth : 560 , padding : 32 , background : '#1e1f24' ,
138- borderRadius : 16 , border : '1px solid #30323a' , boxShadow : '0 24px 48px #000a'
139- } } >
248+ < div
249+ style = { {
250+ margin : 'auto' ,
251+ maxWidth : 560 ,
252+ padding : 32 ,
253+ background : '#1e1f24' ,
254+ borderRadius : 16 ,
255+ border : '1px solid #30323a' ,
256+ boxShadow : '0 24px 48px #000a' ,
257+ } }
258+ >
140259 < div style = { { fontSize : 48 , marginBottom : 18 } } > 🚨</ div >
141260 < h1 style = { { fontSize : 28 , marginBottom : 12 } } > Critical Application Error</ h1 >
142261 < p style = { { color : '#9ca3af' , marginBottom : 12 } } >
143- The application failed to initialize due to a critical error. Server-rendered content may be unavailable.
262+ The application failed to initialize due to a critical error. Server-rendered content
263+ may be unavailable.
144264 </ p >
145265 < div style = { { display : 'flex' , gap : 10 , justifyContent : 'center' , marginTop : 16 } } >
146266 < button
147267 onClick = { ( ) => ( typeof window !== 'undefined' ? window . location . reload ( ) : reset ( ) ) }
148268 style = { {
149- background : '#3b82f6' , color : '#fff' , border : 'none' , borderRadius : 8 ,
150- padding : '10px 16px' , fontWeight : 600 , cursor : 'pointer'
269+ background : '#3b82f6' ,
270+ color : '#fff' ,
271+ border : 'none' ,
272+ borderRadius : 8 ,
273+ padding : '10px 16px' ,
274+ fontWeight : 600 ,
275+ cursor : 'pointer' ,
151276 } }
152277 >
153278 Reload
154279 </ button >
155280 < button
156- onClick = { ( ) => ( typeof window !== 'undefined' ? ( window . location . href = '/' ) : reset ( ) ) }
281+ onClick = { ( ) =>
282+ typeof window !== 'undefined' ? ( window . location . href = '/' ) : reset ( )
283+ }
157284 style = { {
158- background : 'transparent' , color : '#9ca3af' , border : '1px solid #4b5563' , borderRadius : 8 ,
159- padding : '10px 16px' , fontWeight : 600 , cursor : 'pointer'
285+ background : 'transparent' ,
286+ color : '#9ca3af' ,
287+ border : '1px solid #4b5563' ,
288+ borderRadius : 8 ,
289+ padding : '10px 16px' ,
290+ fontWeight : 600 ,
291+ cursor : 'pointer' ,
160292 } }
161293 >
162294 Go Home
@@ -166,5 +298,5 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
166298 ) }
167299 </ body >
168300 </ html >
169- ) ;
301+ )
170302}
0 commit comments