@crashlensorg/nestjs 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,16 +1,45 @@
1
- import { DynamicModule, ExceptionFilter, ArgumentsHost, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
1
+ import { ArgumentsHost, NestModule, MiddlewareConsumer, DynamicModule, ExceptionFilter, NestInterceptor, ExecutionContext, CallHandler, NestMiddleware } from '@nestjs/common';
2
2
  import { NodeInitOptions, CrashLens } from '@crashlensorg/node';
3
- export { Breadcrumb, CaptureParams, CrashLens, CrashLensUser, CrashOptions, DeviceInfo, NodeInitOptions, Severity, initNode } from '@crashlensorg/node';
3
+ export { Breadcrumb, CaptureParams, CrashLens, CrashLensUser, CrashOptions, DeviceInfo, NodeInitOptions, Severity, initNode, runWithScope, withIsolationScope } from '@crashlensorg/node';
4
4
  import { BaseExceptionFilter } from '@nestjs/core';
5
5
  import { Observable } from 'rxjs';
6
6
 
7
+ /** Usuário retornado por `resolveUser`. */
8
+ interface ResolvedUser {
9
+ id: string | number;
10
+ email?: string;
11
+ username?: string;
12
+ }
13
+ interface HttpRequestContext {
14
+ method?: string;
15
+ /** Padrão da rota (ex.: "/users/:id"), ou o path sem query string como fallback. */
16
+ route?: string;
17
+ /** Path real da requisição, sem query string. */
18
+ path?: string;
19
+ requestId?: string;
20
+ }
21
+ /** Extrai método, padrão da rota e request id. Nunca lê body, cookies nem Authorization. */
22
+ declare function getHttpRequestContext(req: any): HttpRequestContext;
23
+ /** Resolução padrão do usuário: `req.user` com `id`, `userId` ou `sub`. */
24
+ declare function defaultResolveUser(req: any): ResolvedUser | null;
25
+
7
26
  interface CrashLensConfig extends NodeInitOptions {
8
27
  /** URL base da API CrashLens. */
9
28
  baseUrl?: string;
10
- /** Versão da release do app. */
29
+ /** Versão da release do app (padrão: env CRASHLENS_RELEASE, depois GIT_SHA). */
11
30
  release?: string;
12
- /** Ambiente: "production", "staging", etc. */
31
+ /** Ambiente: "production", "staging", etc. (padrão: env CRASHLENS_ENVIRONMENT, depois NODE_ENV). */
13
32
  environment?: string;
33
+ /**
34
+ * Decide se uma exceção HTTP com esse status deve ser capturada (padrão: status >= 500).
35
+ * Erros que não são HttpException contam como 500.
36
+ */
37
+ captureStatus?: (status: number) => boolean;
38
+ /**
39
+ * Resolve o usuário do request para anexar aos eventos.
40
+ * Padrão: `req.user` com `id`/`userId`/`sub`.
41
+ */
42
+ resolveUser?: (req: any) => ResolvedUser | null | undefined;
14
43
  /** Tags globais. */
15
44
  tags?: Record<string, unknown>;
16
45
  /** Extra global. */
@@ -22,9 +51,17 @@ interface CrashLensConfig extends NodeInitOptions {
22
51
  */
23
52
  declare class CrashLensService {
24
53
  private readonly client?;
54
+ private readonly captureStatus;
55
+ private readonly resolveUser;
25
56
  constructor(options?: CrashLensConfig);
26
57
  get isInitialized(): boolean;
27
58
  captureException(error: unknown, params?: Parameters<CrashLens['captureException']>[1]): void;
59
+ /**
60
+ * Captura uma exceção HTTP aplicando o filtro de status (`captureStatus`) e anexando o
61
+ * contexto do request (método, rota, request id, usuário). Pode ser chamado do
62
+ * `catch()` de um filtro de exceções próprio, sem substituí-lo. Nunca lança.
63
+ */
64
+ captureHttpException(exception: unknown, host?: ArgumentsHost): void;
28
65
  captureMessage(message: string, params?: Parameters<CrashLens['captureMessage']>[1]): void;
29
66
  captureEvent(params: Parameters<CrashLens['captureEvent']>[0]): void;
30
67
  addBreadcrumb(breadcrumb: Parameters<CrashLens['addBreadcrumb']>[0]): void;
@@ -38,19 +75,26 @@ interface CrashLensModuleOptions extends CrashLensConfig {
38
75
  globalFilter?: boolean;
39
76
  /** Registra o interceptor global de requests (padrão: true). */
40
77
  globalInterceptor?: boolean;
78
+ /** Roda cada request HTTP em um escopo isolado (padrão: true). */
79
+ isolateRequestScope?: boolean;
41
80
  }
42
81
  /**
43
82
  * Módulo do CrashLens para NestJS.
44
83
  *
45
84
  * @Module({ imports: [CrashLensModule.forRoot({ apiKey: 'cl_live_xxx' }) ] })
46
85
  */
47
- declare class CrashLensModule {
86
+ declare class CrashLensModule implements NestModule {
87
+ private static isolateRequestScope;
88
+ configure(consumer: MiddlewareConsumer): void;
48
89
  static forRoot(options: CrashLensModuleOptions): DynamicModule;
49
90
  }
50
91
 
51
92
  /**
52
- * Captura todas as exceções não tratadas e as envia ao CrashLens,
53
- * preservando o comportamento padrão do NestJS.
93
+ * Captura exceções e as envia ao CrashLens (respeitando `captureStatus`, padrão >= 500),
94
+ * preservando o comportamento padrão do NestJS. É o único ponto de captura do módulo.
95
+ *
96
+ * Se o app já tem um filtro global próprio, use `CrashLensService.captureHttpException`
97
+ * dentro dele (com `globalFilter: false`).
54
98
  */
55
99
  declare class CrashLensFilter extends BaseExceptionFilter implements ExceptionFilter {
56
100
  private readonly crashLens;
@@ -59,7 +103,11 @@ declare class CrashLensFilter extends BaseExceptionFilter implements ExceptionFi
59
103
  }
60
104
 
61
105
  /**
62
- * Interceptor que adiciona breadcrumbs de request e captura erros dos handlers.
106
+ * Interceptor que adiciona o breadcrumb HTTP de cada request.
107
+ *
108
+ * Ele NÃO captura erros: a captura é feita somente pelo `CrashLensFilter` (ou por
109
+ * `CrashLensService.captureHttpException` num filtro próprio). Usado sozinho, sem filtro,
110
+ * os erros não são enviados ao CrashLens.
63
111
  */
64
112
  declare class CrashLensInterceptor implements NestInterceptor {
65
113
  private readonly crashLens;
@@ -67,4 +115,13 @@ declare class CrashLensInterceptor implements NestInterceptor {
67
115
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
68
116
  }
69
117
 
70
- export { type CrashLensConfig, CrashLensFilter, CrashLensInterceptor, CrashLensModule, type CrashLensModuleOptions, CrashLensService };
118
+ /**
119
+ * Executa cada request HTTP dentro de um escopo isolado do CrashLens, para que
120
+ * setUser/setTag/breadcrumbs de um request não vazem para outros concorrentes.
121
+ * Registrado automaticamente pelo CrashLensModule para todas as rotas.
122
+ */
123
+ declare class CrashLensScopeMiddleware implements NestMiddleware {
124
+ use(_req: unknown, _res: unknown, next: (error?: unknown) => void): void;
125
+ }
126
+
127
+ export { type CrashLensConfig, CrashLensFilter, CrashLensInterceptor, CrashLensModule, type CrashLensModuleOptions, CrashLensScopeMiddleware, CrashLensService, type HttpRequestContext, type ResolvedUser, defaultResolveUser, getHttpRequestContext };
package/dist/index.d.ts CHANGED
@@ -1,16 +1,45 @@
1
- import { DynamicModule, ExceptionFilter, ArgumentsHost, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
1
+ import { ArgumentsHost, NestModule, MiddlewareConsumer, DynamicModule, ExceptionFilter, NestInterceptor, ExecutionContext, CallHandler, NestMiddleware } from '@nestjs/common';
2
2
  import { NodeInitOptions, CrashLens } from '@crashlensorg/node';
3
- export { Breadcrumb, CaptureParams, CrashLens, CrashLensUser, CrashOptions, DeviceInfo, NodeInitOptions, Severity, initNode } from '@crashlensorg/node';
3
+ export { Breadcrumb, CaptureParams, CrashLens, CrashLensUser, CrashOptions, DeviceInfo, NodeInitOptions, Severity, initNode, runWithScope, withIsolationScope } from '@crashlensorg/node';
4
4
  import { BaseExceptionFilter } from '@nestjs/core';
5
5
  import { Observable } from 'rxjs';
6
6
 
7
+ /** Usuário retornado por `resolveUser`. */
8
+ interface ResolvedUser {
9
+ id: string | number;
10
+ email?: string;
11
+ username?: string;
12
+ }
13
+ interface HttpRequestContext {
14
+ method?: string;
15
+ /** Padrão da rota (ex.: "/users/:id"), ou o path sem query string como fallback. */
16
+ route?: string;
17
+ /** Path real da requisição, sem query string. */
18
+ path?: string;
19
+ requestId?: string;
20
+ }
21
+ /** Extrai método, padrão da rota e request id. Nunca lê body, cookies nem Authorization. */
22
+ declare function getHttpRequestContext(req: any): HttpRequestContext;
23
+ /** Resolução padrão do usuário: `req.user` com `id`, `userId` ou `sub`. */
24
+ declare function defaultResolveUser(req: any): ResolvedUser | null;
25
+
7
26
  interface CrashLensConfig extends NodeInitOptions {
8
27
  /** URL base da API CrashLens. */
9
28
  baseUrl?: string;
10
- /** Versão da release do app. */
29
+ /** Versão da release do app (padrão: env CRASHLENS_RELEASE, depois GIT_SHA). */
11
30
  release?: string;
12
- /** Ambiente: "production", "staging", etc. */
31
+ /** Ambiente: "production", "staging", etc. (padrão: env CRASHLENS_ENVIRONMENT, depois NODE_ENV). */
13
32
  environment?: string;
33
+ /**
34
+ * Decide se uma exceção HTTP com esse status deve ser capturada (padrão: status >= 500).
35
+ * Erros que não são HttpException contam como 500.
36
+ */
37
+ captureStatus?: (status: number) => boolean;
38
+ /**
39
+ * Resolve o usuário do request para anexar aos eventos.
40
+ * Padrão: `req.user` com `id`/`userId`/`sub`.
41
+ */
42
+ resolveUser?: (req: any) => ResolvedUser | null | undefined;
14
43
  /** Tags globais. */
15
44
  tags?: Record<string, unknown>;
16
45
  /** Extra global. */
@@ -22,9 +51,17 @@ interface CrashLensConfig extends NodeInitOptions {
22
51
  */
23
52
  declare class CrashLensService {
24
53
  private readonly client?;
54
+ private readonly captureStatus;
55
+ private readonly resolveUser;
25
56
  constructor(options?: CrashLensConfig);
26
57
  get isInitialized(): boolean;
27
58
  captureException(error: unknown, params?: Parameters<CrashLens['captureException']>[1]): void;
59
+ /**
60
+ * Captura uma exceção HTTP aplicando o filtro de status (`captureStatus`) e anexando o
61
+ * contexto do request (método, rota, request id, usuário). Pode ser chamado do
62
+ * `catch()` de um filtro de exceções próprio, sem substituí-lo. Nunca lança.
63
+ */
64
+ captureHttpException(exception: unknown, host?: ArgumentsHost): void;
28
65
  captureMessage(message: string, params?: Parameters<CrashLens['captureMessage']>[1]): void;
29
66
  captureEvent(params: Parameters<CrashLens['captureEvent']>[0]): void;
30
67
  addBreadcrumb(breadcrumb: Parameters<CrashLens['addBreadcrumb']>[0]): void;
@@ -38,19 +75,26 @@ interface CrashLensModuleOptions extends CrashLensConfig {
38
75
  globalFilter?: boolean;
39
76
  /** Registra o interceptor global de requests (padrão: true). */
40
77
  globalInterceptor?: boolean;
78
+ /** Roda cada request HTTP em um escopo isolado (padrão: true). */
79
+ isolateRequestScope?: boolean;
41
80
  }
42
81
  /**
43
82
  * Módulo do CrashLens para NestJS.
44
83
  *
45
84
  * @Module({ imports: [CrashLensModule.forRoot({ apiKey: 'cl_live_xxx' }) ] })
46
85
  */
47
- declare class CrashLensModule {
86
+ declare class CrashLensModule implements NestModule {
87
+ private static isolateRequestScope;
88
+ configure(consumer: MiddlewareConsumer): void;
48
89
  static forRoot(options: CrashLensModuleOptions): DynamicModule;
49
90
  }
50
91
 
51
92
  /**
52
- * Captura todas as exceções não tratadas e as envia ao CrashLens,
53
- * preservando o comportamento padrão do NestJS.
93
+ * Captura exceções e as envia ao CrashLens (respeitando `captureStatus`, padrão >= 500),
94
+ * preservando o comportamento padrão do NestJS. É o único ponto de captura do módulo.
95
+ *
96
+ * Se o app já tem um filtro global próprio, use `CrashLensService.captureHttpException`
97
+ * dentro dele (com `globalFilter: false`).
54
98
  */
55
99
  declare class CrashLensFilter extends BaseExceptionFilter implements ExceptionFilter {
56
100
  private readonly crashLens;
@@ -59,7 +103,11 @@ declare class CrashLensFilter extends BaseExceptionFilter implements ExceptionFi
59
103
  }
60
104
 
61
105
  /**
62
- * Interceptor que adiciona breadcrumbs de request e captura erros dos handlers.
106
+ * Interceptor que adiciona o breadcrumb HTTP de cada request.
107
+ *
108
+ * Ele NÃO captura erros: a captura é feita somente pelo `CrashLensFilter` (ou por
109
+ * `CrashLensService.captureHttpException` num filtro próprio). Usado sozinho, sem filtro,
110
+ * os erros não são enviados ao CrashLens.
63
111
  */
64
112
  declare class CrashLensInterceptor implements NestInterceptor {
65
113
  private readonly crashLens;
@@ -67,4 +115,13 @@ declare class CrashLensInterceptor implements NestInterceptor {
67
115
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
68
116
  }
69
117
 
70
- export { type CrashLensConfig, CrashLensFilter, CrashLensInterceptor, CrashLensModule, type CrashLensModuleOptions, CrashLensService };
118
+ /**
119
+ * Executa cada request HTTP dentro de um escopo isolado do CrashLens, para que
120
+ * setUser/setTag/breadcrumbs de um request não vazem para outros concorrentes.
121
+ * Registrado automaticamente pelo CrashLensModule para todas as rotas.
122
+ */
123
+ declare class CrashLensScopeMiddleware implements NestMiddleware {
124
+ use(_req: unknown, _res: unknown, next: (error?: unknown) => void): void;
125
+ }
126
+
127
+ export { type CrashLensConfig, CrashLensFilter, CrashLensInterceptor, CrashLensModule, type CrashLensModuleOptions, CrashLensScopeMiddleware, CrashLensService, type HttpRequestContext, type ResolvedUser, defaultResolveUser, getHttpRequestContext };
package/dist/index.js CHANGED
@@ -29,28 +29,84 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
29
29
  // src/index.ts
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
- CrashLens: () => import_node3.CrashLens,
32
+ CrashLens: () => import_node5.CrashLens,
33
33
  CrashLensFilter: () => CrashLensFilter,
34
34
  CrashLensInterceptor: () => CrashLensInterceptor,
35
35
  CrashLensModule: () => CrashLensModule,
36
+ CrashLensScopeMiddleware: () => CrashLensScopeMiddleware,
36
37
  CrashLensService: () => CrashLensService,
37
- initNode: () => import_node2.initNode
38
+ defaultResolveUser: () => defaultResolveUser,
39
+ getHttpRequestContext: () => getHttpRequestContext,
40
+ initNode: () => import_node4.initNode,
41
+ runWithScope: () => import_node3.runWithScope,
42
+ withIsolationScope: () => import_node3.withIsolationScope
38
43
  });
39
44
  module.exports = __toCommonJS(index_exports);
40
45
 
41
46
  // src/crash-lens.module.ts
42
- var import_common4 = require("@nestjs/common");
47
+ var import_common5 = require("@nestjs/common");
43
48
  var import_core2 = require("@nestjs/core");
44
49
 
45
50
  // src/crash-lens.service.ts
46
51
  var import_common = require("@nestjs/common");
47
52
  var import_node = require("@crashlensorg/node");
53
+
54
+ // src/request-context.ts
55
+ function stripQuery(url) {
56
+ if (typeof url !== "string") return void 0;
57
+ const i = url.search(/[?#]/);
58
+ return i === -1 ? url : url.slice(0, i);
59
+ }
60
+ function firstHeader(value) {
61
+ const v = Array.isArray(value) ? value[0] : value;
62
+ return typeof v === "string" && v ? v : void 0;
63
+ }
64
+ function getHttpRequestContext(req) {
65
+ if (!req) return {};
66
+ const path = stripQuery(req.originalUrl ?? req.url);
67
+ let route;
68
+ const expressRoute = req.route?.path;
69
+ if (typeof expressRoute === "string") {
70
+ route = `${req.baseUrl ?? ""}${expressRoute}`;
71
+ } else {
72
+ const fastifyRoute = req.routeOptions?.url ?? req.routerPath;
73
+ route = typeof fastifyRoute === "string" ? fastifyRoute : path;
74
+ }
75
+ return {
76
+ method: req.method,
77
+ route,
78
+ path,
79
+ requestId: firstHeader(req.headers?.["x-request-id"]) ?? (req.requestId ? String(req.requestId) : void 0)
80
+ };
81
+ }
82
+ function defaultResolveUser(req) {
83
+ const u = req?.user;
84
+ if (!u || typeof u !== "object") return null;
85
+ const id = u.id ?? u.userId ?? u.sub;
86
+ if (id === void 0 || id === null || id === "") return null;
87
+ return { id, email: u.email, username: u.username };
88
+ }
89
+ function toCrashLensUser(user) {
90
+ if (!user || user.id === void 0 || user.id === null) return void 0;
91
+ const out = { id: String(user.id) };
92
+ if (user.email) out.email = user.email;
93
+ if (user.username) out.username = user.username;
94
+ return out;
95
+ }
96
+
97
+ // src/crash-lens.service.ts
48
98
  var CrashLensService = class {
49
99
  constructor(options) {
100
+ this.captureStatus = (status) => status >= 500;
101
+ this.resolveUser = defaultResolveUser;
50
102
  if (!options) return;
103
+ if (options.captureStatus) this.captureStatus = options.captureStatus;
104
+ if (options.resolveUser) this.resolveUser = options.resolveUser;
51
105
  this.client = (0, import_node.initNode)({
52
106
  ...options,
53
- enableGlobalHandlers: options.enableGlobalHandlers ?? false,
107
+ release: options.release ?? process.env.CRASHLENS_RELEASE ?? process.env.GIT_SHA,
108
+ environment: options.environment ?? process.env.CRASHLENS_ENVIRONMENT ?? process.env.NODE_ENV,
109
+ enableGlobalHandlers: options.enableGlobalHandlers ?? true,
54
110
  collectRuntimeInfo: options.collectRuntimeInfo ?? true,
55
111
  enableSessionTracking: options.enableSessionTracking ?? false
56
112
  });
@@ -71,6 +127,40 @@ var CrashLensService = class {
71
127
  captureException(error, params) {
72
128
  this.client?.captureException(error, params);
73
129
  }
130
+ /**
131
+ * Captura uma exceção HTTP aplicando o filtro de status (`captureStatus`) e anexando o
132
+ * contexto do request (método, rota, request id, usuário). Pode ser chamado do
133
+ * `catch()` de um filtro de exceções próprio, sem substituí-lo. Nunca lança.
134
+ */
135
+ captureHttpException(exception, host) {
136
+ try {
137
+ if (!this.client) return;
138
+ const status = exception instanceof import_common.HttpException ? exception.getStatus() : 500;
139
+ if (!this.captureStatus(status)) return;
140
+ const request = host && host.getType() === "http" ? host.switchToHttp().getRequest() : void 0;
141
+ const ctx = getHttpRequestContext(request);
142
+ const tags = { httpStatus: String(status) };
143
+ if (ctx.method) tags.httpMethod = ctx.method;
144
+ if (ctx.route) tags.route = ctx.route;
145
+ if (ctx.requestId) tags.requestId = ctx.requestId;
146
+ let user;
147
+ if (request) {
148
+ try {
149
+ user = toCrashLensUser(this.resolveUser(request));
150
+ } catch {
151
+ }
152
+ }
153
+ this.client.captureException(exception, {
154
+ context: "nestjs-exception-filter",
155
+ handled: exception instanceof import_common.HttpException,
156
+ url: ctx.path,
157
+ user,
158
+ tags,
159
+ extra: { request: { method: ctx.method, route: ctx.route, path: ctx.path, requestId: ctx.requestId } }
160
+ });
161
+ } catch {
162
+ }
163
+ }
74
164
  captureMessage(message, params) {
75
165
  this.client?.captureMessage(message, params);
76
166
  }
@@ -105,23 +195,7 @@ var CrashLensFilter = class extends import_core.BaseExceptionFilter {
105
195
  this.crashLens = crashLens;
106
196
  }
107
197
  catch(exception, host) {
108
- const status = exception instanceof import_common2.HttpException ? exception.getStatus() : import_common2.HttpStatus.INTERNAL_SERVER_ERROR;
109
- const request = host.switchToHttp().getRequest();
110
- const message = exception instanceof Error ? exception.message : String(exception);
111
- this.crashLens.captureException(exception, {
112
- context: "nestjs-exception-filter",
113
- handled: false,
114
- tags: {
115
- httpStatus: String(status),
116
- httpMethod: request?.method
117
- },
118
- extra: {
119
- url: request?.url,
120
- method: request?.method,
121
- statusCode: status,
122
- error: message
123
- }
124
- });
198
+ this.crashLens.captureHttpException(exception, host);
125
199
  super.catch(exception, host);
126
200
  }
127
201
  };
@@ -131,45 +205,52 @@ CrashLensFilter = __decorateClass([
131
205
 
132
206
  // src/crash-lens.interceptor.ts
133
207
  var import_common3 = require("@nestjs/common");
134
- var import_rxjs = require("rxjs");
135
- var import_operators = require("rxjs/operators");
136
208
  var CrashLensInterceptor = class {
137
209
  constructor(crashLens) {
138
210
  this.crashLens = crashLens;
139
211
  }
140
212
  intercept(context, next) {
141
- const request = context.switchToHttp().getRequest();
142
- if (request) {
143
- this.crashLens.addBreadcrumb({
144
- message: `${request.method ?? "GET"} ${request.url ?? "/"}`,
145
- type: "request",
146
- category: "http",
147
- data: { method: request.method, url: request.url }
148
- });
213
+ if (context.getType() === "http") {
214
+ const request = context.switchToHttp().getRequest();
215
+ if (request) {
216
+ const url = String(request.originalUrl ?? request.url ?? "/");
217
+ const path = url.split("?")[0];
218
+ this.crashLens.addBreadcrumb({
219
+ message: `${request.method ?? "GET"} ${path}`,
220
+ type: "request",
221
+ category: "http",
222
+ data: { method: request.method, url: path }
223
+ });
224
+ }
149
225
  }
150
- return next.handle().pipe(
151
- (0, import_operators.tap)({
152
- error: (error) => {
153
- this.crashLens.captureException(error, {
154
- handled: false,
155
- context: "context.interceptor",
156
- tags: { httpMethod: request?.method },
157
- extra: { url: request?.url }
158
- });
159
- }
160
- }),
161
- (0, import_operators.catchError)((error) => (0, import_rxjs.throwError)(() => error))
162
- );
226
+ return next.handle();
163
227
  }
164
228
  };
165
229
  CrashLensInterceptor = __decorateClass([
166
230
  (0, import_common3.Injectable)()
167
231
  ], CrashLensInterceptor);
168
232
 
233
+ // src/crash-lens.middleware.ts
234
+ var import_common4 = require("@nestjs/common");
235
+ var import_node2 = require("@crashlensorg/node");
236
+ var CrashLensScopeMiddleware = class {
237
+ use(_req, _res, next) {
238
+ (0, import_node2.runWithScope)(() => next());
239
+ }
240
+ };
241
+ CrashLensScopeMiddleware = __decorateClass([
242
+ (0, import_common4.Injectable)()
243
+ ], CrashLensScopeMiddleware);
244
+
169
245
  // src/crash-lens.module.ts
170
246
  var CRASH_LENS_OPTIONS = /* @__PURE__ */ Symbol("CRASH_LENS_OPTIONS");
171
247
  var CrashLensModule = class {
248
+ configure(consumer) {
249
+ if (!CrashLensModule.isolateRequestScope) return;
250
+ consumer.apply(CrashLensScopeMiddleware).forRoutes({ path: "*", method: import_common5.RequestMethod.ALL });
251
+ }
172
252
  static forRoot(options) {
253
+ CrashLensModule.isolateRequestScope = options.isolateRequestScope !== false;
173
254
  const providers = [
174
255
  { provide: CRASH_LENS_OPTIONS, useValue: options },
175
256
  CrashLensService
@@ -188,19 +269,26 @@ var CrashLensModule = class {
188
269
  };
189
270
  }
190
271
  };
272
+ CrashLensModule.isolateRequestScope = true;
191
273
  CrashLensModule = __decorateClass([
192
- (0, import_common4.Global)()
274
+ (0, import_common5.Global)()
193
275
  ], CrashLensModule);
194
276
 
195
277
  // src/index.ts
196
- var import_node2 = require("@crashlensorg/node");
197
278
  var import_node3 = require("@crashlensorg/node");
279
+ var import_node4 = require("@crashlensorg/node");
280
+ var import_node5 = require("@crashlensorg/node");
198
281
  // Annotate the CommonJS export names for ESM import in node:
199
282
  0 && (module.exports = {
200
283
  CrashLens,
201
284
  CrashLensFilter,
202
285
  CrashLensInterceptor,
203
286
  CrashLensModule,
287
+ CrashLensScopeMiddleware,
204
288
  CrashLensService,
205
- initNode
289
+ defaultResolveUser,
290
+ getHttpRequestContext,
291
+ initNode,
292
+ runWithScope,
293
+ withIsolationScope
206
294
  });
package/dist/index.mjs CHANGED
@@ -11,18 +11,69 @@ var __decorateClass = (decorators, target, key, kind) => {
11
11
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
12
12
 
13
13
  // src/crash-lens.module.ts
14
- import { Global } from "@nestjs/common";
14
+ import { Global, RequestMethod } from "@nestjs/common";
15
15
  import { APP_FILTER, APP_INTERCEPTOR } from "@nestjs/core";
16
16
 
17
17
  // src/crash-lens.service.ts
18
- import { Inject, Injectable, Optional } from "@nestjs/common";
18
+ import { HttpException, Inject, Injectable, Optional } from "@nestjs/common";
19
19
  import { initNode } from "@crashlensorg/node";
20
+
21
+ // src/request-context.ts
22
+ function stripQuery(url) {
23
+ if (typeof url !== "string") return void 0;
24
+ const i = url.search(/[?#]/);
25
+ return i === -1 ? url : url.slice(0, i);
26
+ }
27
+ function firstHeader(value) {
28
+ const v = Array.isArray(value) ? value[0] : value;
29
+ return typeof v === "string" && v ? v : void 0;
30
+ }
31
+ function getHttpRequestContext(req) {
32
+ if (!req) return {};
33
+ const path = stripQuery(req.originalUrl ?? req.url);
34
+ let route;
35
+ const expressRoute = req.route?.path;
36
+ if (typeof expressRoute === "string") {
37
+ route = `${req.baseUrl ?? ""}${expressRoute}`;
38
+ } else {
39
+ const fastifyRoute = req.routeOptions?.url ?? req.routerPath;
40
+ route = typeof fastifyRoute === "string" ? fastifyRoute : path;
41
+ }
42
+ return {
43
+ method: req.method,
44
+ route,
45
+ path,
46
+ requestId: firstHeader(req.headers?.["x-request-id"]) ?? (req.requestId ? String(req.requestId) : void 0)
47
+ };
48
+ }
49
+ function defaultResolveUser(req) {
50
+ const u = req?.user;
51
+ if (!u || typeof u !== "object") return null;
52
+ const id = u.id ?? u.userId ?? u.sub;
53
+ if (id === void 0 || id === null || id === "") return null;
54
+ return { id, email: u.email, username: u.username };
55
+ }
56
+ function toCrashLensUser(user) {
57
+ if (!user || user.id === void 0 || user.id === null) return void 0;
58
+ const out = { id: String(user.id) };
59
+ if (user.email) out.email = user.email;
60
+ if (user.username) out.username = user.username;
61
+ return out;
62
+ }
63
+
64
+ // src/crash-lens.service.ts
20
65
  var CrashLensService = class {
21
66
  constructor(options) {
67
+ this.captureStatus = (status) => status >= 500;
68
+ this.resolveUser = defaultResolveUser;
22
69
  if (!options) return;
70
+ if (options.captureStatus) this.captureStatus = options.captureStatus;
71
+ if (options.resolveUser) this.resolveUser = options.resolveUser;
23
72
  this.client = initNode({
24
73
  ...options,
25
- enableGlobalHandlers: options.enableGlobalHandlers ?? false,
74
+ release: options.release ?? process.env.CRASHLENS_RELEASE ?? process.env.GIT_SHA,
75
+ environment: options.environment ?? process.env.CRASHLENS_ENVIRONMENT ?? process.env.NODE_ENV,
76
+ enableGlobalHandlers: options.enableGlobalHandlers ?? true,
26
77
  collectRuntimeInfo: options.collectRuntimeInfo ?? true,
27
78
  enableSessionTracking: options.enableSessionTracking ?? false
28
79
  });
@@ -43,6 +94,40 @@ var CrashLensService = class {
43
94
  captureException(error, params) {
44
95
  this.client?.captureException(error, params);
45
96
  }
97
+ /**
98
+ * Captura uma exceção HTTP aplicando o filtro de status (`captureStatus`) e anexando o
99
+ * contexto do request (método, rota, request id, usuário). Pode ser chamado do
100
+ * `catch()` de um filtro de exceções próprio, sem substituí-lo. Nunca lança.
101
+ */
102
+ captureHttpException(exception, host) {
103
+ try {
104
+ if (!this.client) return;
105
+ const status = exception instanceof HttpException ? exception.getStatus() : 500;
106
+ if (!this.captureStatus(status)) return;
107
+ const request = host && host.getType() === "http" ? host.switchToHttp().getRequest() : void 0;
108
+ const ctx = getHttpRequestContext(request);
109
+ const tags = { httpStatus: String(status) };
110
+ if (ctx.method) tags.httpMethod = ctx.method;
111
+ if (ctx.route) tags.route = ctx.route;
112
+ if (ctx.requestId) tags.requestId = ctx.requestId;
113
+ let user;
114
+ if (request) {
115
+ try {
116
+ user = toCrashLensUser(this.resolveUser(request));
117
+ } catch {
118
+ }
119
+ }
120
+ this.client.captureException(exception, {
121
+ context: "nestjs-exception-filter",
122
+ handled: exception instanceof HttpException,
123
+ url: ctx.path,
124
+ user,
125
+ tags,
126
+ extra: { request: { method: ctx.method, route: ctx.route, path: ctx.path, requestId: ctx.requestId } }
127
+ });
128
+ } catch {
129
+ }
130
+ }
46
131
  captureMessage(message, params) {
47
132
  this.client?.captureMessage(message, params);
48
133
  }
@@ -69,7 +154,7 @@ CrashLensService = __decorateClass([
69
154
  ], CrashLensService);
70
155
 
71
156
  // src/crash-lens.filter.ts
72
- import { Catch, HttpException, HttpStatus } from "@nestjs/common";
157
+ import { Catch } from "@nestjs/common";
73
158
  import { BaseExceptionFilter } from "@nestjs/core";
74
159
  var CrashLensFilter = class extends BaseExceptionFilter {
75
160
  constructor(crashLens) {
@@ -77,23 +162,7 @@ var CrashLensFilter = class extends BaseExceptionFilter {
77
162
  this.crashLens = crashLens;
78
163
  }
79
164
  catch(exception, host) {
80
- const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
81
- const request = host.switchToHttp().getRequest();
82
- const message = exception instanceof Error ? exception.message : String(exception);
83
- this.crashLens.captureException(exception, {
84
- context: "nestjs-exception-filter",
85
- handled: false,
86
- tags: {
87
- httpStatus: String(status),
88
- httpMethod: request?.method
89
- },
90
- extra: {
91
- url: request?.url,
92
- method: request?.method,
93
- statusCode: status,
94
- error: message
95
- }
96
- });
165
+ this.crashLens.captureHttpException(exception, host);
97
166
  super.catch(exception, host);
98
167
  }
99
168
  };
@@ -103,45 +172,52 @@ CrashLensFilter = __decorateClass([
103
172
 
104
173
  // src/crash-lens.interceptor.ts
105
174
  import { Injectable as Injectable2 } from "@nestjs/common";
106
- import { throwError } from "rxjs";
107
- import { catchError, tap } from "rxjs/operators";
108
175
  var CrashLensInterceptor = class {
109
176
  constructor(crashLens) {
110
177
  this.crashLens = crashLens;
111
178
  }
112
179
  intercept(context, next) {
113
- const request = context.switchToHttp().getRequest();
114
- if (request) {
115
- this.crashLens.addBreadcrumb({
116
- message: `${request.method ?? "GET"} ${request.url ?? "/"}`,
117
- type: "request",
118
- category: "http",
119
- data: { method: request.method, url: request.url }
120
- });
180
+ if (context.getType() === "http") {
181
+ const request = context.switchToHttp().getRequest();
182
+ if (request) {
183
+ const url = String(request.originalUrl ?? request.url ?? "/");
184
+ const path = url.split("?")[0];
185
+ this.crashLens.addBreadcrumb({
186
+ message: `${request.method ?? "GET"} ${path}`,
187
+ type: "request",
188
+ category: "http",
189
+ data: { method: request.method, url: path }
190
+ });
191
+ }
121
192
  }
122
- return next.handle().pipe(
123
- tap({
124
- error: (error) => {
125
- this.crashLens.captureException(error, {
126
- handled: false,
127
- context: "context.interceptor",
128
- tags: { httpMethod: request?.method },
129
- extra: { url: request?.url }
130
- });
131
- }
132
- }),
133
- catchError((error) => throwError(() => error))
134
- );
193
+ return next.handle();
135
194
  }
136
195
  };
137
196
  CrashLensInterceptor = __decorateClass([
138
197
  Injectable2()
139
198
  ], CrashLensInterceptor);
140
199
 
200
+ // src/crash-lens.middleware.ts
201
+ import { Injectable as Injectable3 } from "@nestjs/common";
202
+ import { runWithScope } from "@crashlensorg/node";
203
+ var CrashLensScopeMiddleware = class {
204
+ use(_req, _res, next) {
205
+ runWithScope(() => next());
206
+ }
207
+ };
208
+ CrashLensScopeMiddleware = __decorateClass([
209
+ Injectable3()
210
+ ], CrashLensScopeMiddleware);
211
+
141
212
  // src/crash-lens.module.ts
142
213
  var CRASH_LENS_OPTIONS = /* @__PURE__ */ Symbol("CRASH_LENS_OPTIONS");
143
214
  var CrashLensModule = class {
215
+ configure(consumer) {
216
+ if (!CrashLensModule.isolateRequestScope) return;
217
+ consumer.apply(CrashLensScopeMiddleware).forRoutes({ path: "*", method: RequestMethod.ALL });
218
+ }
144
219
  static forRoot(options) {
220
+ CrashLensModule.isolateRequestScope = options.isolateRequestScope !== false;
145
221
  const providers = [
146
222
  { provide: CRASH_LENS_OPTIONS, useValue: options },
147
223
  CrashLensService
@@ -160,11 +236,13 @@ var CrashLensModule = class {
160
236
  };
161
237
  }
162
238
  };
239
+ CrashLensModule.isolateRequestScope = true;
163
240
  CrashLensModule = __decorateClass([
164
241
  Global()
165
242
  ], CrashLensModule);
166
243
 
167
244
  // src/index.ts
245
+ import { runWithScope as runWithScope2, withIsolationScope } from "@crashlensorg/node";
168
246
  import { initNode as initNode2 } from "@crashlensorg/node";
169
247
  import { CrashLens as CrashLens2 } from "@crashlensorg/node";
170
248
  export {
@@ -172,6 +250,11 @@ export {
172
250
  CrashLensFilter,
173
251
  CrashLensInterceptor,
174
252
  CrashLensModule,
253
+ CrashLensScopeMiddleware,
175
254
  CrashLensService,
176
- initNode2 as initNode
255
+ defaultResolveUser,
256
+ getHttpRequestContext,
257
+ initNode2 as initNode,
258
+ runWithScope2 as runWithScope,
259
+ withIsolationScope
177
260
  };
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "@crashlensorg/nestjs",
3
- "version": "0.1.0",
4
- "description": "CrashLens integração com NestJS — módulo, filtro global de exceções e interceptor HTTP",
5
- "license": "MIT",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.mjs",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.mjs",
13
- "require": "./dist/index.js"
14
- }
15
- },
16
- "files": ["dist"],
17
- "publishConfig": {
18
- "access": "public"
19
- },
20
- "scripts": {
21
- "build": "tsup src/index.ts --format cjs,esm --dts --clean --external @nestjs/common --external @nestjs/core --external @nestjs/platform-express --external @nestjs/websockets --external @nestjs/microservices --external rxjs --external reflect-metadata",
22
- "typecheck": "tsc --noEmit"
23
- },
24
- "dependencies": {
25
- "@crashlensorg/node": "0.1.0"
26
- },
27
- "peerDependencies": {
28
- "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
29
- "reflect-metadata": "^0.2.0",
30
- "rxjs": "^7.0.0"
31
- },
32
- "devDependencies": {
33
- "@nestjs/common": "^10.4.0",
34
- "@nestjs/core": "^10.4.0",
35
- "reflect-metadata": "^0.2.2",
36
- "rxjs": "^7.8.0"
37
- },
38
- "keywords": ["crashlens", "nestjs", "error-monitoring", "crash-reporting", "sentry-alternative"]
1
+ {
2
+ "name": "@crashlensorg/nestjs",
3
+ "version": "0.3.0",
4
+ "description": "CrashLens integração com NestJS — módulo, filtro global de exceções e interceptor HTTP",
5
+ "license": "MIT",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": ["dist"],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean --external @nestjs/common --external @nestjs/core --external @nestjs/platform-express --external @nestjs/websockets --external @nestjs/microservices --external rxjs --external reflect-metadata",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "dependencies": {
25
+ "@crashlensorg/node": "0.3.0"
26
+ },
27
+ "peerDependencies": {
28
+ "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
29
+ "reflect-metadata": "^0.2.0",
30
+ "rxjs": "^7.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@nestjs/common": "^10.4.0",
34
+ "@nestjs/core": "^10.4.0",
35
+ "reflect-metadata": "^0.2.2",
36
+ "rxjs": "^7.8.0"
37
+ },
38
+ "keywords": ["crashlens", "nestjs", "error-monitoring", "crash-reporting", "sentry-alternative"]
39
39
  }