11import axios from 'axios' ;
2+ import os from 'os' ;
23
34export type ReportErrorPayload = {
45 endpoint : string ;
@@ -50,8 +51,10 @@ class HttpErrorReporter implements ErrorReporter {
5051 headers : { 'Content-Type' : 'application/json' , 'x-api-key' : this . apiKey } ,
5152 timeout : 30000 ,
5253 } ) ;
53- } catch {
54- console . error ( '[ERROR][MONITORING][REPORT_ERROR] Failed to report error:' , error ) ;
54+ } catch ( error ) {
55+ const err = error instanceof Error ? error : new Error ( String ( error ) ) ;
56+ err . message = `[ERROR][MONITORING][REPORT_ERROR] Failed to report error: ${ err . message } ` ;
57+ console . error ( err ) ;
5558 }
5659 }
5760}
@@ -78,7 +81,10 @@ export function initializeErrorMonitoring(): void {
7881 const reporter = new HttpErrorReporter ( monitoringUrl , apiKey ) ;
7982 const originalConsoleError = console . error . bind ( console ) ;
8083
81- const classifySeverity = ( message : string , extra : unknown ) : 'major' | 'degraded' | 'minimal' => {
84+ const classifySeverity = (
85+ message : string ,
86+ extra : unknown ,
87+ ) : 'major' | 'degraded' | 'minimal' | 'none' => {
8288 const msg = ( message || '' ) . toLowerCase ( ) ;
8389 const extraText = ( ( ) => {
8490 try {
@@ -127,33 +133,138 @@ export function initializeErrorMonitoring(): void {
127133 ] ;
128134 if ( degradedHints . some ( h => hay . includes ( h ) ) ) return 'degraded' ;
129135
130- return 'minimal' ;
136+ // Minimal (Severity 3) — explicit low-impact signals per table
137+ const minimalHints = [
138+ // Minor parsing, non-standard cases
139+ 'minor parsing' ,
140+ 'non standard' ,
141+ 'non-standard' ,
142+ // Occasional transient/slow behavior, small impact
143+ 'transient' ,
144+ 'slow query' ,
145+ 'slow queries' ,
146+ 'occasionally retry' ,
147+ 'occasional retry' ,
148+ // Small gaps / less critical segments
149+ 'small gaps' ,
150+ 'less critical' ,
151+ 'less-critical' ,
152+ // Logging/audit minor issues
153+ 'minor formatting' ,
154+ 'sporadic log gaps' ,
155+ 'log gaps' ,
156+ 'formatting issue' ,
157+ ] ;
158+ if ( minimalHints . some ( h => hay . includes ( h ) ) ) return 'minimal' ;
159+
160+ // Default fallback if no category matched
161+ return 'none' ;
131162 } ;
132163
133164 console . error = ( ...args : unknown [ ] ) => {
134165 originalConsoleError ( ...args ) ;
135166
167+ // Normalize message and extra to centralize formatting
168+ const firstError = args . find ( a => a instanceof Error ) as Error | undefined ;
169+ const firstString = args . find ( a => typeof a === 'string' ) as string | undefined ;
170+ const allObjects = args . filter (
171+ a => a !== null && typeof a === 'object' && ! ( a instanceof Error ) ,
172+ ) as Record < string , unknown > [ ] ;
173+
136174 let message = 'Unknown error' ;
137- let extra : unknown ;
138-
139- if ( args . length > 0 ) {
140- const first = args [ 0 ] as unknown ;
141- if ( first instanceof Error ) {
142- message = first . message || 'Error' ;
143- extra = { stack : first . stack , ...( args [ 1 ] as object ) } ;
144- } else if ( typeof first === 'string' ) {
145- message = first as string ;
146- if ( args . length > 1 ) extra = { args : args . slice ( 1 ) } ;
147- } else {
148- try {
149- message = JSON . stringify ( first ) ;
150- } catch {
151- message = String ( first ) ;
152- }
153- if ( args . length > 1 ) extra = { args : args . slice ( 1 ) } ;
175+ let extra : Record < string , unknown > = { } ;
176+ // Merge all non-Error context objects (shallow)
177+ if ( allObjects . length > 0 ) {
178+ try {
179+ extra = Object . assign ( { } , ...allObjects ) ;
180+ } catch {
181+ // noop
154182 }
155183 }
156184
185+ if ( firstError && firstString ) {
186+ message = `${ firstString } : ${ firstError . message || 'Error' } ` ;
187+ if ( firstError . stack ) extra . stack = firstError . stack ;
188+ } else if ( firstError ) {
189+ message = firstError . message || 'Error' ;
190+ if ( firstError . stack ) extra . stack = firstError . stack ;
191+ } else if ( firstString ) {
192+ message = firstString ;
193+ } else if ( args . length > 0 ) {
194+ try {
195+ message = JSON . stringify ( args [ 0 ] ) ;
196+ } catch {
197+ message = String ( args [ 0 ] ) ;
198+ }
199+ if ( args . length > 1 ) extra . args = args . slice ( 1 ) as unknown [ ] ;
200+ }
201+
202+ // Compute raw error message (without tags/prefixes)
203+ const rawErrorMessage = ( ( ) => {
204+ if ( firstError ?. message ) return firstError . message ;
205+ if ( typeof firstString === 'string' ) {
206+ // Strip bracket tags like [ERROR][CACHE] from the beginning
207+ return firstString . replace ( / ^ (?: \[ [ ^ \] ] + \] ) + \s * / g, '' ) . trim ( ) ;
208+ }
209+ return typeof message === 'string' ? message : 'Error' ;
210+ } ) ( ) ;
211+
212+ // Attach runtime/env info
213+ try {
214+ ( extra as Record < string , unknown > ) [ 'pid' ] = process . pid ;
215+ ( extra as Record < string , unknown > ) [ 'node' ] = process . version ;
216+ ( extra as Record < string , unknown > ) [ 'host' ] = os . hostname ( ) ;
217+ ( extra as Record < string , unknown > ) [ 'uptime' ] = Math . round ( process . uptime ( ) ) ;
218+ } catch { }
219+
220+ // Derive callsite (function, file, line, column) from stack (error or synthetic)
221+ try {
222+ const stackSource = firstError ?. stack || new Error ( ) . stack || '' ;
223+ const frames = stackSource . split ( '\n' ) . map ( s => s . trim ( ) ) ;
224+ const frame = frames . find (
225+ f => f && ! f . includes ( 'services/monitoring' ) && ( f . includes ( 'at ' ) || f . includes ( '@' ) ) ,
226+ ) ;
227+ if ( frame ) {
228+ // Patterns: "at fn (file:line:col)" or "at file:line:col"
229+ const m = / a t \s + (?: (?< fn > [ ^ \s ( ] + ) \s + \( ) ? (?< loc > [ ^ ) ] + ) \) ? / . exec ( frame ) ;
230+ const loc = m ?. groups ?. loc ?? '' ;
231+ const [ filePath , line , column ] = ( ( ) => {
232+ const parts = loc . split ( ':' ) ;
233+ if ( parts . length >= 3 ) return [ parts . slice ( 0 , - 2 ) . join ( ':' ) , parts . at ( - 2 ) , parts . at ( - 1 ) ] ;
234+ return [ loc , undefined , undefined ] ;
235+ } ) ( ) ;
236+ ( extra as any ) . function = m ?. groups ?. fn ?? undefined ;
237+ ( extra as any ) . file = filePath ;
238+ if ( line ) ( extra as any ) . line = Number ( line ) ;
239+ if ( column ) ( extra as any ) . column = Number ( column ) ;
240+
241+ // Phase derivation from file path
242+ const phase = ( ( ) => {
243+ const p = String ( filePath || '' ) . toLowerCase ( ) ;
244+ if ( p . includes ( '/cache/' ) ) return 'cache' ;
245+ if ( p . includes ( '/services/streaming' ) ) return 'streaming' ;
246+ if ( p . includes ( '/services/payload' ) || p . includes ( '/models/' ) || p . includes ( 'sequelize' ) )
247+ return 'db' ;
248+ if ( p . includes ( '/kadena-server/' ) ) return 'graphql' ;
249+ if ( p . includes ( '/services/price' ) ) return 'price' ;
250+ if ( p . includes ( '/services/missing' ) ) return 'missing' ;
251+ if ( p . includes ( '/services/define-canonical' ) ) return 'canonical' ;
252+ if ( p . includes ( '/services/guards' ) ) return 'guards' ;
253+ return 'app' ;
254+ } ) ( ) ;
255+ ( extra as any ) . phase = phase ;
256+ }
257+ } catch { }
258+
259+ // Derive tags from bracket prefixes in the string (if any)
260+ try {
261+ const tagSource = firstString || ( typeof message === 'string' ? message : '' ) ;
262+ const tags = Array . from ( tagSource . matchAll ( / \[ ( [ ^ \] ] + ) \] / g) )
263+ . map ( m => m [ 1 ] )
264+ . filter ( Boolean ) ;
265+ if ( tags . length ) ( extra as any ) . tags = tags ;
266+ } catch { }
267+
157268 // Ignore GraphQL-internal logs if requested (by tag)
158269 if ( typeof message === 'string' && message . includes ( '[GRAPHQL]' ) ) {
159270 return ;
@@ -184,7 +295,7 @@ export function initializeErrorMonitoring(): void {
184295 instance,
185296 operation,
186297 error : message ,
187- extra : extraWithSeverity ,
298+ extra : { ... ( extraWithSeverity as Record < string , unknown > ) , message : rawErrorMessage } ,
188299 } ) ;
189300 } ;
190301}
0 commit comments