에러를 놓치지 않는 방법, Sentry 도입 가이드
1. 개요
“혹시 어떤 상황에서 발생하셨나요?, 저는 재현이 안되는데요??”
회사에서 개발을 하다가 팀원분께서 때때로 에러상황에 대해 전달해주실때가 종종 있습니다.
문제는 발생상황을 말씀해주셔도 재현이 안되거나, 하나하나 전달받을때마다 처리하는게 좋은 구조는 아니라고 생각했어요.
발생 당시의 로그를 파악해야하고, 그나마 배포전에 발견해서 망정이지 실 사용자에게 로그를 찍어달라고 부탁할 수는 없기때문입니다.
더군다나 사용자의 브라우저에 따라 호환성문제도 각기 다르게 발생할 수 있기 때문에 에러를 추척하고 모니터링하기 위한 기술은 필수적이라고 생각했습니다.
이런 상황이 발생했을 때 빠르게 대응할 수 있는 도구가 있으면 어떨까요?
2. Sentry
Sentry란 실시간으로 에러를 추적하고, 분석하여 알려주는 오픈 소스 서비스입니다.
에러에 대한 알림, 사용자의 브라우저나 기기 정보 및 이러한 정보를 대시보드형태로 시각화하고 에러를 추적하기 쉽게 도와주는 여러가지 서비스를 제공해주고 있습니다.
Sentry는 이러한 과제를 효과적으로 수행할 수 있는 강력한 도구로, 팀의 생산성 향상과 사용자 만족도 향상에 크게 기여할 수 있습니다
2.1 사용법
-
https://sentry.io/welcome/ 사이트를 통해 회원가입을 진행
-
프로젝트를 생성
-
React에서 SDK 설정
1
yarn add @sentry/react
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// index.tsx // Sentry에서 제공하는 기본 시작 세팅 Sentry.init({ dsn: "your key", // Sentry 서버에 로그를 보내는데 필요한 인증 정보 integrations: [ new Sentry.BrowserTracing({ tracePropagationTargets: [ "localhost", /^https:\/\/yourserver\.io\/api/, ], }), new Sentry.Replay(), ], // Performance Monitoring tracesSampleRate: 1.0, // 데이터 샘플링 비율 설정 // Session Replay replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, });
-
에러 발생시키기
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
import * as Sentry from "@sentry/react"; const causeCustomError = () => { try { throw new Error("커스텀 에러 발생"); } catch (error) { console.error(error); Sentry.captureException(error); // 에러를 Sentry에 전송 } }; {...} return ( <div> <button onClick={causeCustomError}> Custom Error</button> </div> );
2.2 기본 옵션
[Sentry SDK 설정 옵션]
-
dsn
- SDK에게 이벤트를 어디로 보낼지 알려주는 식별자
- 보안을 위해 별도의 env파일로 관리
-
release
- 애플리케이션의 버전을 표현하기 위해 사용
- 각 릴리즈별로 오류 추적이 가능하기때문에 릴리즈별로 오류를 분석하기 용이
-
tracesSampleRate
- Sentry에 보내는 트랜잭션의 샘플 비율 (0.0 ~ 1.0)
- 너무 많은 이벤트를 기록하지 않도록 부하를 조절
-
replaysSessionSampleRate
- Sentry SDK에서 세션 리플레이 기능의 샘플링 비율을 제어
-
replaysOnErrorSampleRate
- 오류가 발생한 세션 중 어느 정도의 비율로 리플레이 데이터를 수집할지 결정
Replay란 위 영상처럼 에러가 발생한 상황이 그대로 녹화되는 기능
- maxBreadcrumbs
- 최대 breadcrumb의 양을 제어 (최근 n개의 데이터만 유지하고 삭제한다)
breadcrumb란 오류가 발생하기 이전에 어떤 일이 일어났는지 표현해주는 Sentry의 기능 중 하나
tracePropagationTargets
- 설정된 URL 패턴에 대해서만 추적 정보를 전파
그 밖의 옵션은 공식문서을 참고 부탁드립니다.
[에러 전송 메서드]
- captureException
- 예외 객체를 인자로 받아 Sentry에 에러를 보고
1
2
3
4
5
6
try {
// Potentially erroneous code
const value = someFunctionThatMightThrow();
} catch (error) {
Sentry.captureException(error);
}
- captureMessage
- 문자열 메시지를 인자로 받아 Sentry에 에러를 보고
1
2
3
if (someUnexpectedCondition) {
Sentry.captureMessage("에러가 발생했습니다.");
}
2.3 에러를 구분짓기
위 방식에 더해서, 수 많은 에러들을 무분별하게 수집하고 관리한다면 문제점을 빠르게 파악하고 관리하기 쉽지 않을 것 입니다. 사실 중요하게 확인해야하는 에러는 따로 있을테니까요
이를 위해 Sentry에서는 에러의 중요도를 구분짓거나 정보를 보내는 방법을 제공하고있습니다.
2.3.1 Scope
Scope는 Sentry에서 이벤트를 캡처하기 전에 이벤트 데이터에 추가 정보를 추가하거나 수정할 수 있는 방법으로
특정 범위(Scope) 내에서 발생한 모든 이벤트는 해당 범위에 설정된 매개변수와 함께 전송됩니다.
- configureScope
- 글로벌 scope에 대한 설정을 적용합니다 (모든 이벤트에 대해 공통적용)
- 사용자 정보 등
1
2
3
4
5
6
Sentry.configureScope(function (scope) {
scope.setTag(userType, "admin");
scope.setUser({ email: "admin@example.com" });
});
Sentry.captureException(new Error("This is another error"));
- withScope
- 해당 범위에서만 설정을 적용합니다
- 에러 상황에 대한 상세 내용
1
2
3
4
5
6
Sentry.withScope(function (scope) {
scope.setTag("page_locale", "en-US");
scope.setLevel(Sentry.Severity.Warning);
Sentry.captureException(new Error("This is an error"));
});
Scope에서 사용할 수 있는 메서드들
- setTag: 이벤트에 태그를 추가합니다.
- setUser: 이벤트에 사용자 정보를 추가합니다.
- setLevel: 이벤트의 중요도 레벨을 설정합니다.
- setExtras: 이벤트에 추가 데이터를 추가합니다.
- addEventProcessor: Scope에 이벤트 프로세서를 추가하여 이벤트 데이터를 수정하거나 조작할 수 있습니다.
즉 Scope에 따라 에러 추적에 있어서 필요한 메타데이터를 추가하고, 로깅을 더욱 효과적으로 수행할 수 있도록 돕습니다.
2.3.2 Level
레벨 설정은 로깅 시스템에서 중요한 부분으로, 로그 메시지가 얼마나 심각한지를 나타내는 데 사용됩니다
-
개별 이벤트에 대한 레벨 설정
1
Sentry.captureMessage("디버그 레벨의 에러 발생", "debug");
-
Scope 내부에서 정의
1 2 3 4 5
Sentry.withScope(function (scope) { scope.setLevel("fatal"); Sentry.captureException(new Error("매우 심각한 에러 발생")); });
순서대로 fatal, error, warning, log, info, debug
레벨정보가 있습니다.
Sentry 이벤트의 레벨을 세밀하게 제어할 수 있으며, 이를 통해 로그의 중요도를 구분하고 관리할 수 있다
2.3.3 Context
개별 에러 이벤트에 대해 추가정보를 제공하기 위해 context를 설정할 수 있습니다.
기본적으로 에러 발생 시각, 사용자 기기, 브라우저, 에러 타입등 발생정보를 확인할 수 있으나, 기타 세부정보를 추가적으로 기록하면 발생한 에러에 대한 정보와 영향을 쉽게 확인할 수 있겠죠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const causeNetworkError = async () => {
try {
const response = await fetch(
"https://api.causeNetworkError.com/causeNetworkError"
);
if (!response.ok) {
throw new Error("Network error: " + response.statusText);
}
} catch (error) {
console.error(error);
Sentry.withScope((scope) => {
scope.setContext("apiInfo", {
url: "https://api.causeNetworkError.com/causeNetworkError",
method: "GET",
status: 401,
// (헤더, 파라미터 등의 추가정보 등록)
});
scope.setTag("errorType", "NetworkError"); // 태그 설정
scope.setLevel("fatal"); // 레벨 설정
Sentry.captureException(error); // 에러를 Sentry에 전송
});
}
};
api와 같이 문제상황을 식별할 수 있는 정보를 추가로 담으면 로깅에 더 용이
2.3.3 Fingerprint
Sentry는 기본적으로 오류 스택 트레이스를 사용하여 오류를 그룹화합니다.
그러나 경우에 따라 사용자는 이러한 그룹화 방식을 더 세밀하게 제어하고 싶을 수 있겠죠. 이 때 fingerprint를 사용하여 오류를 원하는 방식으로 그룹화할 수 있습니다.
AxiosError가 모두 하나로 그룹화되어 표현된다.
에러 상태를 확인하려면 그룹내부에서 하나하나 내용을 확인해서 불편
이때 Sentry.withScope 메서드를 사용하여 fingerprint를 설정할 수 있습니다.
Api의 경우 상태코드에 따라 name속성을 바꾸면 더 구체적인 명칭으로 에러를 그룹화하고 확인할 수 있겠죠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{...}
Sentry.withScope((scope) => {
scope.setFingerprint([
originalRequest.method,
error.response?.status,
originalRequest.url,
]);
error.name = errorType;
Sentry.captureException(error);
});
method, url , status에 따라 그룹핑을 하고, 상태코드에 따라 적절한 명칭을 부여
3 Source Map
소스맵을 사용하지 않으면 기본적으로 에러가 발생된 코드의 위치가 표현되지 않습니다.
왜냐하면 배포 프로세스를 거치면 최적화,난독화,최소화 같은 변환 프로세스를 거쳐 원본 소스코드와 다르게 배포되기때문에 에러가 발생한 위치도 원본 소스 코드와 매칭할 수 없기 때문입니다.
소스맵은 이러한 문제점을 해결하기 위해 변환된 코드와 원본 코드 사이의 매핑을 제공합니다.
3.1 소스맵 업로드
Webpack을 기준으로 아래의 방법을 사용했습니다. 상세한 내용은 공식문서를 참고해주세요
- 플러그인 설치
1
yarn add @sentry/webpack-plugin --save-dev
- 웹팩 설정 수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
module.exports = {
// ... other config above ...
devtool: "source-map",
plugins: [
sentryWebpackPlugin({
org: "example-org",
project: "example-project",
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
};
여기서 SENTRY_AUTH_TOKEN의 경우 Sentry 사이트에서 API 토큰을 생성해야합니다.
이 정보는 Sentry API에 요청을 보낼 때 사용되며, 이를 통해 사용자의 인증을 검증하고 Sentry 프로젝트에 접근을 허용합니다.
환경변수 파일은 .env.sentry-build-plugin
파일에 정의해 빌드시 자동으로 소스맵을 서버에 자동으로 업로드 될 수 있도록 합니다.
소스맵 사용 전 / 후
4. 알림 보내기
자 이제, 수집된 로그를 분석하고 에러를 빠르게 해결해야합니다.
가장 좋은 방법은 특정 문제상황에 대해 메일이나 Slack을 통해 빠르게 발생한 에러를 리포트해야겠죠.
횟수, 에러 종류, 문제 상황을 필터링하고 알림을 보낼 수 있다.
문서참고
결과
5. 사용 후기
Sentry를 사용하고나서, 몇 가지 실수했던 부분과 추가했던사항들이 있었습니다.
[클래스 상속을 통한 타입명시]
초기에는, 각 에러 로깅을 명확히 하기 위해 status코드에 따라 별도의 상수를 만들고 error.name속성을 직접 바꾸는 식으로 작업을 했었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (error.response) {
const { status } = error.response;
errorType = STATUS_CODE_ERROR_MAP[status] || DEFAULT_ERROR_TYPE;
}
// {...}
Sentry.withScope((scope) => {
scope.setContext("API Request Info", {
url: originalRequest.url,
method: originalRequest.method,
status: error.response?.status,
params: originalRequest.params,
headers: originalRequest.headers,
});
scope.setContext("API Response Info", {
data: JSON.stringify(error.response?.data, null, 2),
headers: error.response?.headers,
});
scope.setFingerprint([
originalRequest.method,
error.response?.status,
originalRequest.url,
]);
error.name = errorType; // 여기서 이름을 지정해서, Sentry로깅에 에러타입이 명시되도록 수정
Sentry.captureException(error);
});
그런데, 기존 플로우에 문제가 생겨서 에러를 표현하는 Snackbar가 나타나지 않는 문제가 발생했습니다.
원인은 참조타입인 에러 객체 자체의 속성을 변경해버려서, 다른 곳의 코드에도 영향을 미쳤던게 원인이였고
아래와 같이 에러 로깅을 위한 에러타입의 지정은 별도의 객체를 새로 생성해서 요청하도록 코드를 수정했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Sentry.withScope((scope) => {
scope.setContext("API Request Info", {
url: originalRequest.url,
method: originalRequest.method,
status: error.response?.status,
params: originalRequest.params,
headers: originalRequest.headers,
});
scope.setContext("API Response Info", {
data: JSON.stringify(error.response?.data, null, 2),
headers: error.response?.headers,
});
scope.setFingerprint([
originalRequest.method,
error.response?.status,
originalRequest.url,
]);
Sentry.captureException(new SentryLoggingError(error)); // 별도의 Error객체를 새로 생성 후 처리
});
export class SentryLoggingError extends Error {
constructor(error) {
super(error.message);
this.name = error.response.statusText;
this.stack = error.stack;
this.config = error.config;
this.response = error.response;
}
}
추가로 statusText를 사용해서 에러이름을 처리했는데, 별도로 에러 타입에 따른 상수를 처리하지 않아도 의미가 식별되고,
모든 상태코드에 대한 상수를 다 정의하지 않아도 된다는 장점이 있어서 일단 해당 필드값을 이용했습니다.
물론 향후에 상태코드에 따라 다른 statusText를 분리해야한다면 별도의 상수로 분리를 해야겠지만요.
[Error Boundary의 도입]
사실 처음 Sentry의 도입 의도는 버그를 빠르게 고치고, 디버깅을 용이하게 하기 위함이였습니다.
그리고 거기서 가장 중요했던 버그는 예상치 못한 에러로 인해 앱이 중단되고, 흰 페이지가 화면상에 표현되는 문제점이였죠.
이 에러를 포착했을때, 별도의 에러로깅을 남기기 위해서는 이 트리거를 포착하고 로그를 보낼 필요가 있었습니다.
여기서 React의 Error Boundary를 도입해 로깅과 더불와 사용자에게 별도의 페이지를 렌더링 할 수 있도록 추가적인 작업을 진행했습니다.
ErrorBoundary란?
React 컴포넌트로, 그 하위 컴포넌트 트리에서 발생하는 JavaScript 오류를 잡아내고, 오류 메시지를 표시하거나 대체 UI를 렌더링하는 데 사용.
일반적으로, JavaScript에서 오류가 발생하면 전체 앱이 중단될 수 있지만, ErrorBoundary를 사용하면 특정 컴포넌트의 하위 트리에서만 오류가 처리되고 앱의 나머지 부분은 정상적으로 동작한다.
앱이 중단되었을때 별도의 페이지를 렌더링함으로써 사용자 경험을 높이고, 이 상황에 대한 Slack알림을 보내도록 처리
[배포와 개발환경의 분리]
1
2
3
4
5
6
7
8
9
10
11
if (process.env.NODE_ENV === "production") {
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
integrations: [
new Sentry.BrowserTracing({
tracePropagationTargets: [process.env.example_1, process.env.example_2],
}),
new Sentry.Replay(),
],
});
}
로컬환경에서 발생하는 무분별한 로그기록을 분리하기 위해 production환경에서만 에러를 수집할 수 있도록 조건문을 추가하였습니다.
그래서 저는 결과적으로 Sentry도입을 통해 아래와 같은 이점을 얻을 수 있었다고 생각합니다.
-
에러 상황을 빠르게 감지하고, 해결에 용이하도록 디버깅 제공
-
사용자 환경 (특히 브라우저..!)에 따른 특정 에러 확인
실제 웹사이트 버전에 따라 Array.at()함수를 지원하지 않아 브라우저환경에 종속되는 에러가 발생한 케이스도 있었습니다.
-
에러를 재현하지 못해 겪었던 찝찝함의 해소🙌
버그가 발생하지 않도록 개발하는게 가장 이상적이겠지만, 완전한 무결점의 프로그램을 만드는건 불가능하다고 생각합니다.
다만 빠르게 버그를 찾아 수정할 수 있는 개발환경을 마련하고 계속해서 개선해나아가야했고 이런 부분에 도움을 주는 Sentry를 도입했던게 좋은 경험이 되었던거같습니다.
참고자료
https://tech.kakaopay.com/post/frontend-sentry-monitoring/
https://speakerdeck.com/kakao/sentryreul-iyonghan-ereo-cujeoggi-reactyi-seoneonjeog-ereo-ceori
댓글남기기