import { DyFM_AnyError, DyFM_DBFilter, DyFM_DataModel_Params, DyFM_Error, DyFM_ErrorLevel, DyFM_Error_Settings, DyFM_Log, DyFM_Object, DyFM_errorFlag, DyFM_Errors, DyFM_RelativeDate, DyFM_Array, DyFM_Paged, DyFM_Time, DyFM_SearchQuery, DyFM_SearchResult, DyFM_Errors_FixAttempt, DyFM_ErrorStatus } from '@futdevpro/fsm-dynamo'; import { DyNTS_Errors_ControlService } from './errors.control-service'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; import { DyNTS_DBUpdate } from '../../../_models/types/db-update.type'; import { DyNTS_DataService } from '../../../_services/base/data.service'; export class DyNTS_Errors_DataService< T_Error extends DyFM_Error, T_ErrorsRecord extends DyFM_Errors > extends DyNTS_DataService implements DyNTS_Errors_ControlService { debugLog: boolean = false; readonly version: string = DyNTS_global_settings.systemVersion; duplicationCounter: number; /* constructor( set: { data?: T_Error, issuer: string, }, dataParams: DyFM_DataModel_Params ) { super( new FDP_Errors(set?.data) as T_Error, dataParams, set.issuer ); } */ async recordError( errorsRecord: T_ErrorsRecord, issuer: string, alwaysRecord?: boolean ): Promise { try { if (issuer && !this.issuer) { this.issuer = issuer; } const errorVersion = this.data?.versions?.length ? this.data?.versions[0] : ('SERVER-' + this.version); if ( !alwaysRecord && errorVersion.includes('dev') || errorVersion.includes('alpha') || errorVersion.includes('beta') ) { DyFM_Log.warn('error not saved (dev version)'); return; } if (!errorsRecord && this.data) { errorsRecord = this.data; } if (!errorsRecord) { throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'recordError', new Error('error data is not defined') ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-RE1`, }); } // FR-069 (2026-06-06): user-level errorokat IS rogzitjuk. // Korabban a user-level errorokat (pl. login: "No account found", "Wrong password", // register: "Email already taken" — auth-service `validateNRegisterLogin` chain) // eldobtak SAVE elott. Eredmeny: a login/register failures nem jelentek meg az // errors-tablaban, igy a Overseer dashboard-on nem voltak lathato-k, a fejleszto // nem latta hany failed-login-attempt volt. // Per memory [[feedback_rich_error_handling]] "EVERY error, rich descriptive, // persisted to errors table". Plusz [[feedback_error_logging_policy]] "EVERY error // from a system logs in its OWN table; sister-system-unreachable filtered at source". // A user-level errorokat is mentjuk — a dashboard-on level-szerinti filter szuri // (info-debug-warn-error-critical-user), de a rekord MEG van. if (errorsRecord.message?.includes?.('NullReferenceException')) { errorsRecord.message += '\n' + errorsRecord.exception.split('() (at <')[0]; } const duplicateError: T_ErrorsRecord = await this.findData({ message: errorsRecord.message, } as DyFM_DBFilter, true); errorsRecord.d_error = this.checkErrorIsStringifyableOrResolvable(errorsRecord.d_error, this.issuer) as T_Error; try { JSON.stringify(errorsRecord); } catch (err) { DyFM_Log.error('error data is not stringifyable!'); return; } if (JSON.stringify(errorsRecord).length > 16793000) { errorsRecord.additionalContent = { message: 'error data was too big to save, removed additionalContent', }; errorsRecord.error ??= {}; errorsRecord.error.additionalContent = { message: 'error data was too big to save, removed additionalContent', }; if (JSON.stringify(errorsRecord).length > 16793000) { errorsRecord.error.errors = []; } } if ( duplicateError && duplicateError?.message === errorsRecord?.message ) { if (!errorsRecord.__created) { errorsRecord.__created = new Date(); } this.duplicationCounter = duplicateError.count + 1; const duplicateRecordUpdate: DyNTS_DBUpdate = { $inc: { count: 1, priority: this.getPriorityMultiplierByLevel(errorsRecord?.level), }, } as DyNTS_DBUpdate; // 2026-06-02 fix: a `$addToSet: { versions: { $push: errorVersion } }` // formátum a MongoDB-be LITERAL `{$push: ''}`-objektumot ír be // a tömbbe, NEM a `$addToSet`-szemantikát alkalmazza. A helyes alak // egyszerűen `$addToSet: { versions: errorVersion }` — a `$addToSet` // önállóan idempotensen ad hozzá értéket a tömbhöz. errorsRecord.versions ??= []; if (!errorsRecord.versions.includes(errorVersion)) { (duplicateRecordUpdate as any).$addToSet = { versions: errorVersion, }; } // Regression detect — a duplicate-ag-on egy NEW occurrence érkezett. // Ha mar volt fix-attempt erre az error-ra, es a jelenlegi version // ujabb vagy egyenlo mint a fix-attempt verzioja, akkor a hipotezis // nem zarta le a problemat → flag-eljuk a dashboardnak. if ( duplicateError.lastFixedInVersion && DyNTS_Errors_DataService.isVersionAtLeast(errorVersion, duplicateError.lastFixedInVersion) ) { // Auto-reactivation: a recurrence carrying a version >= the last // claimed-fix version means the fix did not hold → flip back to an // active-again state. `regressed` (not plain `active`) so the next // investigator knows a prior hypothesis already failed. (duplicateRecordUpdate as any).$set = { ...((duplicateRecordUpdate as any).$set ?? {}), regressedAfterFix: true, status: DyFM_ErrorStatus.regressed, }; } await this.updateData({ filterBy: { _id: duplicateError._id } as DyFM_DBFilter, update: duplicateRecordUpdate, }); // ezt majd ha leülepedett, hogy biztos nem kellenek a deep duplication-ök // (a WB-ban kellhettek a user és pc config info-k) /* if (!duplicateError.duplications.includes(data.stackTrace)) { await this.updateData({ filterBy: { _id: duplicateError._id } as DyFM_DBFilter, update: { $push: { duplications: DyFM_Object.clone(data) }, } as DyNTS_DBUpdate, }).catch((pushError) => { DyFM_Log.error('pushError:', pushError); }); } */ DyFM_Log.error( 'error found, not saving ("known" error), but increased count, ' + '\ncount:', duplicateError.count + 1 ); DyFM_Log.error( 'ErrorMsg:', errorsRecord?.message?.replace(/\n/g, ' \n') ); if (this.debugLog) DyFM_Log.error('Error:', errorsRecord); } else { DyFM_Log.log('error not found, saving....'); DyFM_Log.error( 'ErrorMsg:', errorsRecord?.message?.replace(/\n/g, ' \n') ); if (this.debugLog) DyFM_Log.error('Error:', errorsRecord); errorsRecord.priority = this.getPriorityMultiplierByLevel(errorsRecord?.level); // 2026-06-02 fix: count default — a `recordError`-t loose `any`-obj- // ektummal hivo path-ok (pl. systemUnreachable / buildFailure hook a // host-app-ban) nem inicializaltak a `count: 1`-et a DyFM_Errors class // konstruktor-default helyett. A schema-ban sincs default, igy a // Mongoose-record-on `undefined` maradt. Most explicit. errorsRecord.count ??= 1; errorsRecord.duplications ??= []; errorsRecord.duplications.push(DyFM_Object.clone(errorsRecord)); errorsRecord.versions ??= []; errorsRecord.versions.push(errorVersion); // New error → starts life as `active` (default on the data-model, set // explicitly here for loose-`any` record paths that bypass the ctor default). errorsRecord.status ??= DyFM_ErrorStatus.active; this.duplicationCounter = 1; // FR-027 (2026-05-24) — Pass `errorsRecord` explicitly. Korabban // `saveData()` no-arg fallback `ensureData()`-on keresztul `this.data`-t // hasznalt, ami a controller-szinten csak a `{issuer}` constructor // wrapper, NEM az actual req.body adat. Eredmenykent minden client- // forwarded error EMPTY rekordkent landolt: csak `issuer` mezo, // semmilyen message/source/stackTrace/error/level/additionalContent // mezo NEM kerult MongoDB-be. (Overseer-en 14454 ilyen empty record // halmozodott fel — debug-level details elveszve.) await this.saveData(errorsRecord); DyFM_Log.warn('error saved'); } } catch (error) { DyFM_Error.logSimple('recordError error', error); /* if (error instanceof DyFM_Error) { error.logSimple('recordError error'); } else { DyFM_Log.error('recordError error:', error); } */ /* throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'recordError', error ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-RE0`, }); */ } } protected async createErrorRecord( error: DyFM_AnyError | any, issuer: string, ): Promise { try { if ((error as DyFM_Error)?.flag?.includes(DyFM_errorFlag)) { return new DyFM_Errors({ issuer: this.issuer, versions: [ 'SERVER-' + this.version ], source: 'INTERNAL-SERVER-ERROR: ' + (error as DyFM_Error)?.___issuerService, stackTrace: (error as DyFM_Error)?._errorCodes, error: (error as DyFM_Error)?.getErrorSimplified?.(), level: (error as DyFM_Error)?.level ?? DyFM_ErrorLevel.error, message: (error as DyFM_Error)?._message, exceptionObj: (error as DyFM_Error)?.errors?.map( (e: DyFM_Error): DyFM_Error_Settings | '' => e?.getErrorSimplified?.() ?? '' ), additionalContent: (error as DyFM_Error)?.additionalContent, }) as T_ErrorsRecord; } else { return new DyFM_Errors({ issuer: this.issuer, versions: [ 'SERVER-' + this.version ], source: 'UNEXPECTED-SERVER-ERROR', stackTrace: (error as Error)?.stack?.split?.('\n'), error: error, level: DyFM_ErrorLevel.critical, message: (error as Error)?.message, exceptionObj: (error as Error)?.stack?.split?.('\n'), }) as T_ErrorsRecord; } } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('createErrorRecord', error), }); } } async handleExternalError( error: T_Error, issuer: string, alwaysRecord?: boolean, ): Promise { try { if (this.debugLog) DyFM_Log.log('handleExternalError'); if (issuer && !this.issuer) { this.issuer = issuer; } const errorsRecord: T_ErrorsRecord = await this.createErrorRecord(error, this.issuer); await this.recordError(errorsRecord, this.issuer, alwaysRecord); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('handleExternalError', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-HE0`, }); } } async handleInternalError( error: DyFM_AnyError | any, issuer: string, alwaysRecord?: boolean, ): Promise { try { if (this.debugLog) DyFM_Log.log('handleInternalError'); if (issuer && !this.issuer) { this.issuer = issuer; } const errorsRecord: T_ErrorsRecord = await this.createErrorRecord(error, this.issuer); /* if (!(error as FDP_Errors)?.flag?.includes('WB-ERROR-OBJECT')) { DyFM_Log.H_error('__ERROR__:', error); } */ await this.recordError(errorsRecord, this.issuer, alwaysRecord); DyFM_Error.logSimple(`DyNTS_Error_DataService handling InternalError...`, error); /* if ( DyNTS_global_settings.log_settings.highDetailedLogs || !(error instanceof DyFM_Error) ) { DyFM_Log.H_error( `DyNTS_Error_DataService handling InternalError...`, `\n ERROR:`, error ); } else { error.logSimple( `DyNTS_Error_DataService handling InternalError...` ); } */ } catch (error) { DyFM_Error.logSimple('handleInternalError error', error); /* if (error instanceof DyFM_Error) { error.logSimple('handleInternalError error'); } else { DyFM_Log.error('handleInternalError error:', error); } */ } } checkErrorIsStringifyableOrResolvable(error: T_Error, issuer: string): T_Error | 'UNRESOLVABLE' { try { if (issuer && !this.issuer) { this.issuer = issuer; } JSON.stringify(error); } catch (err) { DyFM_Error.logSimple('checkErrorIsStringifyableOrResolvable error', err); /* if (err instanceof DyFM_Error) { err.logSimple('checkErrorIsStringifyableOrResolvable error'); } else { DyFM_Log.error('checkErrorIsStringifyableOrResolvable error:', err); } */ error = DyFM_Object.resolveCirculation(error); try { JSON.stringify(error); } catch (err) { delete error.additionalContent; try { JSON.stringify(error); } catch (errr) { DyFM_Error.logSimple('checkErrorIsStringifyableOrResolvable error', errr); /* if (err instanceof DyFM_Error) { err.logSimple('error data is not stringifyable! resolution failed! will not save!'); } else { DyFM_Log.error('error data is not stringifyable! resolution failed! will not save!', err); } */ return 'UNRESOLVABLE'; } DyFM_Log.error('error data was too big to save, removed additionalContent'); } } return error; } getPriorityMultiplierByLevel(level: DyFM_ErrorLevel): number { switch (level) { case DyFM_ErrorLevel.critical: case DyFM_ErrorLevel.fatal: return 1000; case DyFM_ErrorLevel.error: case DyFM_ErrorLevel.serious: return 100; case DyFM_ErrorLevel.buildFailure: return 50; case DyFM_ErrorLevel.systemUnreachable: return 20; case DyFM_ErrorLevel.warning: return 10; case DyFM_ErrorLevel.info: case DyFM_ErrorLevel.debug: case DyFM_ErrorLevel.known: return 0.01; case DyFM_ErrorLevel.user: default: return 0; } } async getErrorsFromDate( date: Date, issuer: string, ): Promise> { try { if (issuer && !this.issuer) { this.issuer = issuer; } const errors: T_ErrorsRecord[] = await this.findDataList({ __created: { $gte: date } }); return DyFM_Array.paged(errors, 0, 100); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'getErrorsFromDate', error ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-GED0`, }); } } async deleteError(errorId: string, issuer: string, alwaysDelete?: boolean): Promise { try { if (issuer && !this.issuer) { this.issuer = issuer; } await this.deleteData(errorId, alwaysDelete); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('deleteError', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-DE0`, }); } } async deleteAllErrors(issuer: string, alwaysDelete?: boolean): Promise { try { if (issuer && !this.issuer) { this.issuer = issuer; } await this.deleteAllData(alwaysDelete); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('deleteAllErrors', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-DAE0`, }); } } /** * Record a "we think this is fixed" claim on an existing error. * * Append-only: every call pushes a new `DyFM_Errors_FixAttempt` to * `fixAttempts[]` and updates the `last*` aliases for quick reads. * Also clears `regressedAfterFix` so the dashboard treats the error as * freshly-claimed-fixed; if a new occurrence arrives with a version * `>= version`, `recordError` will flip the flag back on. */ async recordFixAttempt( errorId: string, version: string, hypothesis: string, issuer: string, notes?: string ): Promise { try { if (issuer && !this.issuer) { this.issuer = issuer; } const existing = await this.findData({ _id: errorId } as DyFM_DBFilter); if (!existing) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('recordFixAttempt', new Error(`error not found: ${errorId}`)), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-RFA0`, }); } const attempt: DyFM_Errors_FixAttempt = { at: new Date(), by: issuer, version: version, hypothesis: hypothesis, ...(notes ? { notes: notes } : {}), }; const nextAttempts: DyFM_Errors_FixAttempt[] = [ ...((existing as any).fixAttempts ?? []), attempt, ]; await this.updateData({ filterBy: { _id: errorId } as DyFM_DBFilter, update: { $set: { fixAttempts: nextAttempts, lastFixedAt: attempt.at, lastFixedBy: attempt.by, lastFixedInVersion: attempt.version, lastFixHypothesis: attempt.hypothesis, regressedAfterFix: false, status: DyFM_ErrorStatus.fixed, }, } as DyNTS_DBUpdate, }); return attempt; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('recordFixAttempt', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-RFA1`, }); } } /** * Soft-resolve an error: set `status = fixed` and KEEP the record (unlike * `deleteError` / `markErrorDone` which hard-delete). Keeping the record is * what enables auto-reactivation — a later recurrence (`recordError`) with a * version >= the last claimed-fix version flips it to `regressed`. * * Optional `notes` are stored on `resolutionNotes`. Clears `regressedAfterFix`. * Does NOT append a `fixAttempt` (use `recordFixAttempt` when there is a * version + hypothesis to log); this is the lightweight "mark as handled" path. */ async resolveError( errorId: string, issuer: string, notes?: string ): Promise { try { if (issuer && !this.issuer) { this.issuer = issuer; } const existing: T_ErrorsRecord = await this.findData({ _id: errorId } as DyFM_DBFilter); if (!existing) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('resolveError', new Error(`error not found: ${errorId}`)), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-RES0`, }); } await this.updateData({ filterBy: { _id: errorId } as DyFM_DBFilter, update: { $set: { status: DyFM_ErrorStatus.fixed, regressedAfterFix: false, ...(notes ? { resolutionNotes: notes } : {}), }, } as DyNTS_DBUpdate, }); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('resolveError', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-RES1`, }); } } /** * Paged errors filtered by lifecycle `status` (active / investigating / fixed * / regressed). An empty or `'all'` status returns everything (same semantics * as `getErrorsPaged`). Mirrors `getErrorsByCategoryPaged`. */ async getErrorsByStatusPaged( status: string, range: DyFM_RelativeDate, pageSize: number, pageIndex: number, issuer: string, ): Promise> { try { if (issuer && !this.issuer) { this.issuer = issuer; } const fromDate: Date = DyFM_Time.getDateByRelativeDate(range); const filter: any = { __lastModified: { $gte: fromDate } }; if (status && status !== 'all') { filter.status = status; } let errors: T_ErrorsRecord[] = await this.findDataList(filter); errors = errors.sort((a, b): number => b.priority - a.priority); return DyFM_Array.paged(errors, pageIndex, pageSize); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('getErrorsByStatusPaged', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-GBSP0`, }); } } /** * Loose numeric compare of two version strings of shape `MAJOR.MINOR.PATCH` * (`01.15.117`). Pads missing segments with 0. Non-numeric segments are * treated as 0 — version strings carrying prefixes like `SERVER-` from * `recordError` are caller-stripped before this is invoked. * * Returns true when `a >= b`. */ static isVersionAtLeast(a: string, b: string): boolean { if (!a || !b) { return false; } const strip = (v: string): string => v.replace(/^[^0-9]+/, ''); const parse = (v: string): number[] => strip(v).split('.').map(s => Number(s) || 0); const aParts = parse(a); const bParts = parse(b); const len = Math.max(aParts.length, bParts.length); for (let i = 0; i < len; i++) { const av = aParts[i] ?? 0; const bv = bParts[i] ?? 0; if (av > bv) { return true; } if (av < bv) { return false; } } return true; } async getErrorsInRange(range: DyFM_RelativeDate, issuer: string): Promise> { try { if (issuer && !this.issuer) { this.issuer = issuer; } const fromDate: Date = DyFM_Time.getDateByRelativeDate(range); let errors: T_ErrorsRecord[] = await this.findDataList({ __lastModified: { $gte: fromDate } }); errors = errors.sort( (a, b): number => b.priority - a.priority ); return DyFM_Array.paged(errors, 0, 10); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('getErrorsInRange', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-GIR0`, }); } } async getErrorsPaged( range: DyFM_RelativeDate, pageSize: number, pageIndex: number, issuer: string, ): Promise> { try { if (issuer && !this.issuer) { this.issuer = issuer; } const fromDate: Date = DyFM_Time.getDateByRelativeDate(range); let errors: T_ErrorsRecord[] = await this.findDataList({ __lastModified: { $gte: fromDate } }); errors = errors.sort( (a, b): number => b.priority - a.priority ); return DyFM_Array.paged(errors, pageIndex, pageSize); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('getErrorsPaged', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-GIP0`, }); } } /** * Paged-eredmenyek a `category` mezo szerint. A frontend "Errors" page tab-jai * (Application / System Unreachable / Build Failure) ezt hivjak. Egy ures- * vagy `'all'` kategoria mindent visszaad (azonos szemantikaval mint a * `getErrorsPaged`-tel). */ async getErrorsByCategoryPaged( category: string, range: DyFM_RelativeDate, pageSize: number, pageIndex: number, issuer: string, ): Promise> { try { if (issuer && !this.issuer) { this.issuer = issuer; } const fromDate: Date = DyFM_Time.getDateByRelativeDate(range); const filter: any = { __lastModified: { $gte: fromDate } }; if (category && category !== 'all') { filter.category = category; } let errors: T_ErrorsRecord[] = await this.findDataList(filter); errors = errors.sort((a, b): number => b.priority - a.priority); return DyFM_Array.paged(errors, pageIndex, pageSize); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('getErrorsByCategoryPaged', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-GBCP0`, }); } } async getLastErrors( range: DyFM_RelativeDate, pageSize: number, pageIndex: number, issuer: string, ): Promise> { try { if (issuer && !this.issuer) { this.issuer = issuer; } const fromDate: Date = DyFM_Time.getDateByRelativeDate(range); let errors: T_ErrorsRecord[] = await this.findDataList({ __lastModified: { $gte: fromDate } }); errors = errors.sort( (a, b): number => +b.__lastModified - +a.__lastModified ); return DyFM_Array.paged(errors, pageIndex, pageSize); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('getLastErrors', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-GLE0`, }); } } async searchErrors( searchQuery: DyFM_SearchQuery, issuer: string, ): Promise> { try { if (issuer && !this.issuer) { this.issuer = issuer; } return await this.searchData(searchQuery); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('searchErrors', error), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-EDS-SE0`, }); } } }