UNPKG

@dudousxd/nestjs-telescope

Version:

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

83 lines 4.72 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; // packages/core/src/nest/telescope-exception.interceptor.ts import { Inject, Injectable, } from '@nestjs/common'; import { catchError, throwError } from 'rxjs'; import { captureException } from './exception-capture.js'; import { TELESCOPE_OPTIONS } from './telescope.options.js'; import { TelescopeService } from './telescope.service.js'; /** * Captures exceptions thrown out of route handlers as `exception` entries so * they group into families, drive the `new-exception` alert, and feed AI * diagnosis. * * This is the HTTP/RPC/WS door. It is not the only one: a NestInterceptor runs * on the Nest execution pipeline and nowhere else, so a job body, a `@Cron` * callback or a durable step needs its own door. The decision (the 4xx policy), * the family hash and the entry shape live in `exception-capture.ts` and are * shared by every door — see the WHY there. If they were reimplemented per door * they would drift, and the dashboard would group the same error two ways * depending on which thread it was thrown on. * * WHY a 4xx default-skip: expected 4xx control flow is NOT an incident. A * `ForbiddenException` (403), `NotFoundException` (404) or a validation 400 is * the framework doing its job — permission denied, resource missing, bad input. * Recording those as exception entries means every permission denial in * production opens a NEW exception family (the family hash keys on * name+message+top-frame, so each call site is its own family), which fires the * `new-exception` Slack alert and, in AI auto-mode, spends Bedrock tokens on a * "diagnosis" of intended behaviour. We hit exactly this: Telescope's own * client-errors `authorize` gate threw a 403, the interceptor captured it as a * brand-new family, paged Slack, and burned an AI diagnosis. So by default a * NestJS `HttpException` whose status is < 500 is dropped here. * * The information is NOT lost: the request-capture middleware records the 4xx * `statusCode` on its own `request` entry (independently, on the response * `finish` event), so the dashboard still shows the 4xx — it just doesn't spawn * an exception family, can't fire `new-exception`, and can't trigger diagnosis. * * The escape hatch is `exceptions.captureHttp4xx: true`, which restores the * pre-change behaviour (capture everything) for hosts that genuinely treat 4xx * as exceptions worth grouping/alerting on. * * NOT affected by this filter: 5xx HttpExceptions (real server errors), * non-HTTP errors (any thrown `Error` that isn't an `HttpException`), and the * client-errors ingestion endpoint's `client_exception` entries (recorded * directly in the controller, never through this interceptor — those are * deliberate browser reports and are always kept). */ let TelescopeExceptionInterceptor = class TelescopeExceptionInterceptor { service; options; constructor(service, options) { this.service = service; this.options = options; } intercept(_context, next) { return next.handle().pipe(catchError((error) => { // `captureException` applies the 4xx skip and never throws, so whatever // it decides, the error re-thrown below is the ORIGINAL one and the real // exception filter still builds the response exactly as before. captureException((input) => this.service.record(input), error, this.options.exceptions); return throwError(() => error); })); } }; TelescopeExceptionInterceptor = __decorate([ Injectable(), __param(0, Inject(TelescopeService)), __param(1, Inject(TELESCOPE_OPTIONS)), __metadata("design:paramtypes", [TelescopeService, Object]) ], TelescopeExceptionInterceptor); export { TelescopeExceptionInterceptor }; //# sourceMappingURL=telescope-exception.interceptor.js.map