UNPKG

@dudousxd/nestjs-telescope

Version:

Laravel Telescope-style observability console for NestJS — core: watchers, recorder, correlation, SQLite store, headless API.

140 lines 6.34 kB
import { EntryType } from '../entry/entry.js'; /** * Cap on the number of in-batch query SQL strings carried into a diagnosis. The * NEWEST queries are the ones nearest the failure, so we keep the tail. A bound * matters because a single request can issue dozens of queries (N+1s especially); * an unbounded list would blow the model's context budget and the prompt cost. */ const RECENT_QUERY_LIMIT = 10; /** * Per-query SQL length cap. A pathological generated statement (huge IN-lists) * would otherwise dominate the prompt; clipping keeps each query scannable while * preserving its shape (table/columns/joins) for the model. */ const QUERY_SQL_CHAR_LIMIT = 2_000; /** * Build the {@link DiagnoseContext} for an exception entry. This is the SHARED * context builder reused by BOTH the on-demand diagnose endpoint and auto-mode — * it mirrors how the `new-exception` alert assembles its rich context (the * exception's own fields + the sibling request entry from the same batch), but * additionally collects the batch's recent query SQL (the queries leading up to * the failure are prime root-cause signal). * * Reads ONLY already-redacted stored content; SQL is taken without bindings, so * no query VALUES are ever handed to the diagnoser. * * @param storage Where to read the exception's batch from. * @param entry The exception (or client_exception) entry to diagnose. * @param occurrenceCount Times this family was seen in the window (>= 1). */ export async function buildDiagnoseContext(storage, entry, occurrenceCount) { const isClient = entry.type === EntryType.ClientException; const batch = await storage.batch(entry.batchId); const recentQueries = collectRecentQueries(batch); if (isClient) { const content = asPartialClientExceptionContent(entry.content); return { exceptionClass: typeof content.name === 'string' ? content.name : 'Error', message: typeof content.message === 'string' ? content.message : '', stack: typeof content.stack === 'string' ? content.stack : null, request: null, url: typeof content.url === 'string' ? content.url : null, userAgent: typeof content.userAgent === 'string' ? content.userAgent : null, recentQueries, client: true, occurrenceCount, }; } const content = asPartialExceptionContent(entry.content); const request = findSiblingRequest(batch); const requestContent = request === null ? null : asPartialRequestContent(request.content); return { exceptionClass: typeof content.class === 'string' ? content.class : 'Error', message: typeof content.message === 'string' ? content.message : '', stack: typeof content.stack === 'string' ? content.stack : null, request: { route: requestContent !== null && typeof requestContent.uri === 'string' ? requestContent.uri : null, method: requestContent !== null && typeof requestContent.method === 'string' ? requestContent.method : null, statusCode: requestContent !== null && typeof requestContent.statusCode === 'number' ? requestContent.statusCode : null, durationMs: request?.durationMs ?? null, }, url: null, userAgent: null, recentQueries, client: false, occurrenceCount, }; } /** The sibling REQUEST entry in the batch (or `null`). */ function findSiblingRequest(batch) { return batch.find((member) => member.type === EntryType.Request) ?? null; } /** * Collect the batch's query SQL, newest-last, SQL-only and clipped: take the most * recent {@link RECENT_QUERY_LIMIT} queries by `sequence`, drop bindings, and cap * each statement's length. Bindings are intentionally excluded so query VALUES * never reach the diagnoser. */ function collectRecentQueries(batch) { const queries = batch .filter((member) => member.type === EntryType.Query) .sort((a, b) => a.sequence - b.sequence); const recent = queries.slice(-RECENT_QUERY_LIMIT); const sql = []; for (const query of recent) { const content = asPartialQueryContent(query.content); if (typeof content.sql === 'string' && content.sql !== '') { sql.push(clipSql(content.sql)); } } return sql; } /** Clip an over-long SQL string, preserving its leading shape for the model. */ function clipSql(sql) { if (sql.length <= QUERY_SQL_CHAR_LIMIT) return sql; return `${sql.slice(0, QUERY_SQL_CHAR_LIMIT)}…`; } /** A non-null object as a string-keyed record (else `{}`), read without a cast. */ function asContentRecord(content) { return typeof content === 'object' && content !== null ? { ...content } : {}; } function asPartialExceptionContent(content) { const record = asContentRecord(content); return { ...(typeof record.class === 'string' ? { class: record.class } : {}), ...(typeof record.message === 'string' ? { message: record.message } : {}), ...(typeof record.stack === 'string' ? { stack: record.stack } : {}), }; } function asPartialClientExceptionContent(content) { const record = asContentRecord(content); return { ...(typeof record.name === 'string' ? { name: record.name } : {}), ...(typeof record.message === 'string' ? { message: record.message } : {}), ...(typeof record.stack === 'string' ? { stack: record.stack } : {}), ...(typeof record.url === 'string' ? { url: record.url } : {}), ...(typeof record.userAgent === 'string' ? { userAgent: record.userAgent } : {}), }; } function asPartialRequestContent(content) { const record = asContentRecord(content); return { ...(typeof record.uri === 'string' ? { uri: record.uri } : {}), ...(typeof record.method === 'string' ? { method: record.method } : {}), ...(typeof record.statusCode === 'number' ? { statusCode: record.statusCode } : {}), }; } function asPartialQueryContent(content) { const record = asContentRecord(content); return { ...(typeof record.sql === 'string' ? { sql: record.sql } : {}), }; } //# sourceMappingURL=diagnose-context-builder.js.map