Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Re-Open] Sentry 연동, 코드베이스에 Exception Filter, Response Interceptor 적용 #8

Merged
merged 11 commits into from
Jun 29, 2024
Merged
5 changes: 3 additions & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
MONGO_URL=mongodb+srv://linkit-dev:[email protected]/linkit-dev
MONGO_URL="mongodb+srv://<UserName>:<Password>@<Host>/<DB>?<External Options>"
SENTRY_DSN="Sentry DSN"
JWT_SECRET="JWT Secret"
JWT_EXPIRE_TIME="Expire Time"
JWT_EXPIRE_TIME="Expire Time"
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ jobs:
- name: pnpm settings
uses: pnpm/action-setup@v4
with:
version: '9.4'
run_install: false
- name: Install dependencies
run: pnpm install
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.1",
"@sentry/node": "^8.9.2",
"@sentry/profiling-node": "^8.9.2",
"aws-lambda": "^1.0.7",
"aws-serverless-express": "^3.4.0",
"class-transformer": "^0.5.1",
Expand Down
2,109 changes: 1,414 additions & 695 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

36 changes: 34 additions & 2 deletions src/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// Nest Packages
import {
ClassSerializerInterceptor,
INestApplication,
ValidationPipe,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { CommonResponseInterceptor, RootExceptionFilter } from './common';

export async function nestAppConfig<
T extends INestApplication = INestApplication,
Expand All @@ -17,6 +20,35 @@ export async function nestAppConfig<
transform: true,
}),
);

app.useGlobalInterceptors(new ClassSerializerInterceptor(reflector));
}

export function nestResponseConfig<
T extends INestApplication = INestApplication,
>(app: T, connectSentry = false) {
app.useGlobalInterceptors(new CommonResponseInterceptor());
connectSentry
? configExceptionFilterWithSentry(app)
: configFilterStandAlone(app);
}

// Enable Exception Filter stand-alone
function configFilterStandAlone<T extends INestApplication = INestApplication>(
app: T,
) {
app.useGlobalFilters(new RootExceptionFilter());
}

// Enalbe Exception Filter with Sentry Connection
function configExceptionFilterWithSentry<
T extends INestApplication = INestApplication,
>(app: T) {
const config = app.get<ConfigService>(ConfigService);
Sentry.init({
dsn: config.get<string>('SENTRY_DSN'),
integrations: [nodeProfilingIntegration()],
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
});
Sentry.setupNestErrorHandler(app, new RootExceptionFilter());
}
4 changes: 3 additions & 1 deletion src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ExpressAdapter } from '@nestjs/platform-express';
import express from 'express';
import { AppModule } from './app.module';
import { nestSwaggerConfig } from './app.swagger';
import { nestAppConfig } from './app.config';
import { nestAppConfig, nestResponseConfig } from './app.config';

export async function bootstrap() {
const expressInstance: express.Express = express();
Expand All @@ -17,6 +17,8 @@ export async function bootstrap() {
nestAppConfig(app);
// Config application Swagger
nestSwaggerConfig(app);
// Config Response
nestResponseConfig(app, false);

return { app, expressInstance };
}
Expand Down
15 changes: 15 additions & 0 deletions src/common/error/exception.abstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ExceptionPayload } from '../types/type';

export abstract class RootException<
T extends ExceptionPayload = ExceptionPayload,
U extends number = number,
> extends Error {
constructor(
public readonly payload: T,
public readonly statuscode: U,
public readonly name: string,
) {
super();
this.message = payload.message as string;
}
}
23 changes: 23 additions & 0 deletions src/common/error/exception.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ExceptionPayload } from '../types/type';
import { RootException } from './exception.abstract';

const codeUnknown = 'Unknown';

export const createException = (
statusCode: number,
message: string,
code = codeUnknown,
) => {
const payload: ExceptionPayload = {
code: code,
message: message,
};

const errorContextName =
code === codeUnknown ? `${codeUnknown} - ${message}` : code;
return class extends RootException {
constructor() {
super(payload, statusCode, errorContextName);
}
};
};
2 changes: 2 additions & 0 deletions src/common/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './exception.abstract';
export * from './exception.factory';
70 changes: 70 additions & 0 deletions src/common/filter/base.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { captureException } from '@sentry/node';
import { Response } from 'express';
import { RootException, createException } from '../error';
import { ExceptionPayload, ICommonResponse } from '../types/type';

@Catch()
export class RootExceptionFilter implements ExceptionFilter {
private unknownCode = 'Unknown';

catch(exception: any, host: ArgumentsHost) {
const context = host.switchToHttp();
const response: Response = context.getResponse<Response>();
let targetException = exception;
let responseStatusCode = 500;
let responseErrorPayload: ExceptionPayload = {
code: this.unknownCode,
message: '',
};

// If exception is http exception instance
if (targetException instanceof HttpException) {
// Response Message
const response = targetException.getResponse();
// Response Status Code
responseStatusCode = targetException.getStatus();
responseErrorPayload = {
code: this.unknownCode,
message: response,
};
}
// Custom Exception
else if (targetException instanceof RootException) {
// Response Message
const response = targetException.payload;
// Response Status Code
const statusCode = targetException.statuscode;
responseErrorPayload = response;
responseStatusCode = statusCode;
}
// Error
else {
const errorMessage = targetException.message;
// Response Status Code
responseStatusCode = 500;
// Response Message
responseErrorPayload = {
code: this.unknownCode,
message: errorMessage,
};
targetException = new (class extends createException(
responseStatusCode,
errorMessage ?? exception.name,
this.unknownCode,
) {})();
}
captureException(targetException);
const exceptionResponse: ICommonResponse = {
success: false,
error: responseErrorPayload,
};

return response.status(responseStatusCode).json(exceptionResponse);
}
}
1 change: 1 addition & 0 deletions src/common/filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './base.filter';
3 changes: 3 additions & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './error';
export * from './filter';
export * from './interceptor';
export * from './decorators';
1 change: 1 addition & 0 deletions src/common/interceptor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './response.interceptor';
19 changes: 19 additions & 0 deletions src/common/interceptor/response.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { ICommonResponse } from '../types/type';

export class CommonResponseInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
return next.handle().pipe(
map((payload = {}): ICommonResponse => {
return {
success: true,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

200번대면 성공, 400, 500번대면 실패라는 싱크를 프론트와 맞춘다면 success필드를 없애고 depth를 하나 없앨 수 있겠다는 생각이 드는데 준호 생각은 어때?!
success필드나 response형태에 대해서 프론트 애들이랑 해커톤 시작 때 얘기해봐도 좋겠다는 생각이 드네!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우리 서버팀 다른 애들 생각도 궁금하다
요새 response 관련해서는 전부터 의견이 분분한 영역인 거 같아서!
@J-Hoplin @hye-on @JonghunAn

Copy link
Collaborator Author

@J-Hoplin J-Hoplin Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status Code로 실패 여부 확인하는것도 괜찮을꺼같아! success 프로퍼티를 추가한 이유는 Payload에서 succes랑 에러코드로 바로 구분하게 한다면 클라이언트에서 body만 가져와서 바로 판별 할 수 있으니까 더 편하지 않을까라는 생각으로 success 필드를 추가했었어!
형 그러면 response payload도 동일하게 data, error 필드 구분 없이 한 depth로 만 payload 전송하는거를 말하는거야?? 약간 에러케이스랑 성공케이스에서 동일하게 응답 payload형태를 맞출려고 ICommonResponse 타입으로 반환하는거로 작성했는데, 기본적으로 한 depth가 생기는 것에 대해 클라이언트 분들 의견을 아직 못 물어봤었어. 형이 말한 것처럼 같이 이야기 해서 맞춰보는 과정이 필요할것같아!

data: payload,
};
}),
);
}
}
20 changes: 20 additions & 0 deletions src/common/types/type.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
import { Types } from 'mongoose';
// Exception Payload
export type ExceptionPayload = {
code?: string;
message: string | object;
};

export type ICommonResponse = ICommonErrorResponse | ICommonSuccessResponse;

// Error Response
export type ICommonErrorResponse = {
success: true;
data: any;
};

// Success Response
export type ICommonSuccessResponse = {
success: false;
error: ExceptionPayload;
}


export type JwtPayload = {
id: string;
Expand Down
Loading