diff --git a/services/apps/cron_service/src/jobs/integrationResultsReporting.job.ts b/services/apps/cron_service/src/jobs/integrationResultsReporting.job.ts index f95542526e..374273868b 100644 --- a/services/apps/cron_service/src/jobs/integrationResultsReporting.job.ts +++ b/services/apps/cron_service/src/jobs/integrationResultsReporting.job.ts @@ -20,7 +20,6 @@ interface IResultStateCount { interface IErrorGroup { errorMessage: string location: string - message: string count: number avgRetries: number maxRetries: number @@ -69,7 +68,6 @@ const job: IJobDefinition = { SELECT COALESCE(r.error->>'errorMessage', '[no errorMessage]') AS "errorMessage", COALESCE(r.error->>'location', '[no location]') AS location, - COALESCE(r.error->>'message', '[no message]') AS message, count(*)::int AS count, round(avg(r.retries), 1)::float AS "avgRetries", max(r.retries)::int AS "maxRetries", @@ -81,8 +79,7 @@ const job: IJobDefinition = { WHERE r.state = 'error' GROUP BY r.error->>'errorMessage', - r.error->>'location', - r.error->>'message' + r.error->>'location' ORDER BY count DESC LIMIT 20 `, @@ -125,7 +122,6 @@ const job: IJobDefinition = { `• *${group.count}x* \`${group.errorMessage}\``, ` _Location:_ \`${group.location}\` | _retries avg/max:_ ${group.avgRetries}/${group.maxRetries}${group.platforms ? ` | _platforms:_ \`${group.platforms}\`` : ''}`, ` _Age:_ ${ageLabel}`, - ` _Detail:_ ${group.message}`, '', ) } diff --git a/services/apps/data_sink_worker/src/service/dataSink.service.ts b/services/apps/data_sink_worker/src/service/dataSink.service.ts index 54fcd50dfa..0f7a616e9f 100644 --- a/services/apps/data_sink_worker/src/service/dataSink.service.ts +++ b/services/apps/data_sink_worker/src/service/dataSink.service.ts @@ -35,6 +35,35 @@ import MemberService from './member.service' /* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Extracts the first app-code frame from an error's stack trace as the location. + * Uses process.cwd() (the service root) to match only this service's own source + * files, skipping node_modules and shared library frames. + */ +function extractLocationFromError(error: unknown): string { + const stack = (error as any)?.stack + if (!stack) return 'unknown' + + const serviceDir = process.cwd() + for (const line of (stack as string).split('\n')) { + if (line.includes(serviceDir) && !line.includes('node_modules')) { + // Named frame: "at [async] FunctionName (file:line:col)" + const named = line.match(/at\s+(.+)\s+\((.+):(\d+):\d+\)/) + if (named) { + const file = named[2].replace(`${serviceDir}/`, '') + return `${file}:${named[3]} (${named[1].trim()})` + } + // Anonymous frame: "at file:line:col" + const anon = line.match(/at\s+\(?(.+):(\d+):\d+\)?/) + if (anon) { + return `${anon[1].replace(`${serviceDir}/`, '')}:${anon[2]}` + } + } + } + + return 'unknown' +} + export default class DataSinkService extends LoggerBase { private readonly repo: DataSinkRepository @@ -55,15 +84,12 @@ export default class DataSinkService extends LoggerBase { private async triggerResultError( resultInfo: IResultData, resultExists: boolean, - location: string, - message: string, error: any, metadata?: Record, ): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const errorData: any = { - location, - message, + location: extractLocationFromError(error), metadata, @@ -463,14 +489,7 @@ export default class DataSinkService extends LoggerBase { if (!result.success) { const resultInfo = single(results, (r) => r.id === resultId) - await this.triggerResultError( - resultInfo, - batchEntry.created, - 'process-result', - 'Error processing result.', - result.err, - result.metadata, - ) + await this.triggerResultError(resultInfo, batchEntry.created, result.err, result.metadata) errors++ } else {