내 프로젝트에 Sentry를 사용해서 생산성을 높여보자

최주희·7일 전
0
post-thumbnail

Sentry를 도입하게 된 이유는 마치 "불이 난 집을 찾는 소방관의 이야기"처럼 시작됐다.

한창 프로젝트를 진행하며 새로운 기능을 개발하는 데에 집중했고, 갑자기 예상치 못한 에러들이 쏟아지기 시작했다. 문제는 어디서 에러가 발생했는지 알 수 없었다. 마치 넓은 숲에서 어디선가 연기는 보이는데 불이 난 정확한 위치를 모르는 것처럼 말이다. 멘토님 추천으로 Sentry라는 도구를 발견하게 되었고, Sentry는 실시간으로 에러를 감지하고, 에러가 발생한 지점과 그 순간의 상황을 바로 기록해주었다.

프로젝트 중에는 Sentry를 제대로 활용은 해보지 못했지만, 인상깊었던 도구이니 만큼 다시 Sentry에 대해 알아가보고자한다.

프론트엔드에서 발생할 수 있는 에러

  • 데이터 영역에서의 에러
  • 화면 영역에서의 에러
  • 외부요인에 의한 에러
  • 런타임 에러

Sentry

실시간 로그 취합 및 분석 도구이자 모니터링 플랫폼

1. 에러 추적 개선

1) 에러데이터 쌓기

(1) captureException

: 에러 객체 & 문자열 전송 ⇒ Sentry 서버

import * as Sentry from '@sentry/react';

try {
  fetchAccountInfoApi();
} catch (error) {
  Sentry.captureException(error);
}

(2) captureMessage

: 문자열만 전송 ⇒ Sentry 서버

import * as Sentry from '@sentry/react';
Sentry.captureNessage('API 서버 에러가 발생하였습니다! ')

[ 내 코드 접목시키기 ]

Response 인터셉터: API 호출이 실패할 때 Sentry에 에러 정보를 전송하도록 설정

[401 Unauthorized 처리]
세션 만료로 인한 401 Unauthorized 에러 발생 시,
Sentry.captureException(error)를 통해 새로고침 토큰 요청 실패 시 에러를 Sentry에 기록

axiosInstance.interceptors.response.use(
    (res) => {
      return res;
    },
    async (err) => {
      const { isLoggedIn, setIsLoggedIn } = useLoginStore.getState();
      const { refreshToken } = getTokenFromStorage();

      // 401 Unauthorized 처리
      if (isLoggedIn && err.response?.status === 401) {
        ...
          return axiosInstance(originRequest);
        } catch (error) {
          // Sentry로 에러 전송
          Sentry.captureException(error);
         ...
          return Promise.reject(error);
        }
      }
      return Promise.reject(err);
	}
  )

Q.모든 에러를 Sentry에 전송해야 하나?

  • 모든 에러를 sentry로 전송하는 것이 유용하지만 주의해야할 점이 있다.
  • 중복된 에러 : fingerprint 기능을 사용해서 중복된 에러를 그룹화 할 수 있다.
  • 불필요한 잡음 : 에러의 심각도에 따라 sentry에 보고하지 않아도 되는 사소한 오류나 네트워크 관련 경고 등이 있을 수 있다. 예를 들어 클라이언트 쪽의 일시적인 네트워크 문제로 인한 실패는 모든 API 호출마다 보고하지 않도록 할 수 있다.
  • 자동화된 인터셉터를 사용

2) 에러 데이터 풍부하게 쌓기

(1) scope

sentry는 기본적으로 스코프 단위로 데이터를 관리한다.

스코프 : 각각의 이벤트(에러나 이슈)가 발생할 때 함께 전송되는 추가적인 정보를 관리하는 컨텍스트

  • 스코프를 기본적으로 자동 관리하지만, withScopeconfigureScope라는 두 가지 함수를 통해 더 세밀하게 제어할 수 있다.
  • configureScope: 전역 스코프를 수정할 때 사용. 즉, 이후의 모든 이벤트에 영향을 미침.
  • withScope: 특정 이벤트에만 영향을 미치는 스코프를 임시로 생성하여 관리. 일시적인 스코프 조정이 필요할 때 유용.

configureScope로는 공통 정보를, withScope로는 각 에러 상황 시 추가 정보를 전송한다.

[configureScope]

import * as Sentry from '@sentry/react';

Sentry.configureScope((scope) => {
  scope.setUser({
    accountId: 2022,
    email: 'chloe.ykim@nana.na',
  });
  
  scope.setTag('webviewType', 'WEB');
});
  • 글로벌 스코프에서 설정한 사용자 정보와 웹뷰 타입

[withScope]

import * as Sentry from '@sentry/react';

Sentry.withScope((scope) => {
  scope.setTag('type', 'api');
  scope.setLevel(Sentry.Severity.Error);
  
  Sentry.captureException(new Error('API Internal Server Error'));
});
  • 복제본을 생성해서 추가 정보를 병합한 뒤에 이벤트를 전송한다.
  • 예시 코드에서는 스코프에 api 에러에 대한 태그와 레벨 정보를 설정함

[ 내 코드 접목시키기 ]

일반적인 에러 처리:

  • sentry.withScope를 사용하여 발생한 API 에러를 기록하고, API url이나 기타 중요 정보를 setTag로 추가
  • setLevel('error')로 에러의 심각도를 설정하여 Sentry에서 필터링하거나 분류
axiosInstance.interceptors.response.use(
    (res) => {
      return res;
    },
    async (err) => {
      const { isLoggedIn, setIsLoggedIn } = useLoginStore.getState();
      const { refreshToken } = getTokenFromStorage();

      // 401 Unauthorized 처리
      if (isLoggedIn && err.response?.status === 401) {
        ...
          return axiosInstance(originRequest);
        } catch (error) {
          // Sentry로 에러 전송
          Sentry.captureException(error);
         ...
          return Promise.reject(error);
        }
      }
      
       // Sentry로 일반적인 에러 전송
      Sentry.withScope((scope) => {
        scope.setTag('type', 'api');
        scope.setTag('url', err.config.url);
        scope.setLevel('error');
        Sentry.captureException(err);
      });

      return Promise.reject(err);
	}
  )

(2) context

이벤트에 임의의 데이터를 연결할 수 있는 context를 이용해 추가 정보를 전송,
검색은 할 수 없고 해당 이벤트가 발생한 이벤트 로그에서 확인 가능

const { method, url, params, data: requestData, headers } = error.config;
const { data: responseData, status } = error.response;

Sentry.setContext('API Request Detail', {
  method, 
  url, 
  params, 
  data: requestData, 
  headers
});

Sentry.setContext('API Response Detail', {
  status, 
  data: responseData
});

[API Request details]

(3) Customized Tags

에러 추적을 신속하게 하기 위해서는 검색이 중요하다.
태그는 인덱싱이 가능하기 때문에 이벤트에 빠른 접근이 가능하다

import * as Sentry from '@sentry/react';

Sentry.withScope((scope) => {
  scope.setTag('type', 'api');
  scope.setTag('api', 'general');
  scope.setLevel(Sentry.Severity.Warning);

  Sentry.captureException(new Error('API Internal Server Error'));
});
  • api 관련 에러를 빠르게 검색하기 위해서 에러의 타입과 api 에러 유형을 의미하는 두 개의 태그 값을 추가

  • 검색 뿐만 아니라 알람에도 활용가능하다.

(4) Level

중요도를 식별할 수 있다.

import * as Sentry from '@sentry/react';

Sentry.withScope((scope) => {
  scope.setTag('type', 'api'); 
  scope.setTag('api', 'general');
  scope.setLevel(Sentry.Severity.Error); // Error level #8
  
  Sentry.captureException(new Error('API Internal Server Error'));
});

Sentry.withScope((scope) => {
  scope.setTag('type', 'api');
  scope.setTag('api', 'timeout');
  scope.setLevel(Sentry.Severity.Warning); // Warning level 18
  
  Sentry.captureException(new ApiError('API Timeout Error'));
});

  • 이렇게 중요도를 설정하면, 이슈 그룹핑이나 알람 조건을 세분화하여 설정하는데 도움이 된다.

(5) lssue Grouping

각각의 이벤트들은 내재화된 그룹화 알고리즘에 의해 생성된 fingerprint를 가지고 있다.
fingerprint는 자동으로 하나의 이슈로 그룹화 되는데, 이슈가 예상한 것과 다르게 보여서 재설정이 필요한 경우가 있다.

import * as Sentry from "@sentry/react";

// axios error
const { method, url } = error.config;
const { status } = error.response;

// Using withScope to set fingerprint
Sentry.withScope((scope) => {
  scope.setTag('type', 'api');
  scope.setTag('api', 'general');
  
  // Corrected the fingerprint array
  scope.setFingerprint([method, status, url]);
  
  Sentry.captureException(new Error('API Internal Server Error'));
});
  • 하나의 api에서 발생할 수 있는 에러는 다양하다.
  • api 응답이 서로 다른 상태값을 가지게 되어도 내재화 알고리즘에 의해 그룹화된다.
    • 이런 경우 재설정이 필요할 수 있다.
    • 재설정하게 되면, 해당 메소드로 호출된 api 에러가 그룹화 된 것을 확인 할 수 있다.

[ 내 코드 접목시키기 ]

Fingerprint 설정:

scope.setFingerprint([method, status.toString(), url]);에서 method(GET, POST 등), status(응답 상태 코드), url(API 요청 경로)을 기반으로 fingerprint를 설정하여 각각의 API 요청과 응답 상태에 따라 에러를 구분

   // Sentry에 에러와 함께 fingerprint 설정
      Sentry.withScope((scope) => {
        scope.setTag('type', 'api');
        scope.setTag('url', err.config.url);
        scope.setLevel('error');
        
        // Fingerprint 설정 (HTTP 메서드, 상태 코드, 요청 URL)
        const { method, url } = err.config;
        const { status } = err.response;
        scope.setFingerprint([method, status.toString(), url]);

        Sentry.captureException(err);
      });

3) Severity 기준 설정 및 모니터링

에러 쌓기 작업 후 에러 상황을 빠르게 감지하기 위해 severity 기준 설정과 그에 따른 모니터링 필요

(1) 알람 조건 설정

  • When
    • 처음 보는 이슈가 생길 경우
    • 해결된 이슈가 다시 발생할 경우
    • 무시하고 있던 이슈가 해제될 경우
  • If
    • 이벤트의 Level이 조건에 맞는 경우
    • 이벤트의 Attribute가 조건에 맞는 경우
    • 이벤트의 Tag가 조건에 맞는 경우
    • 이슈가 N번 중복 발생할 경우
    • 원하는 레벨의 이벤트가 N번 발생할 경우
  • Then
    • slack
    • kakaowork
    • jira

2. 유의미한 데이터를 수집하자

  • chunk load 에러나 network에러는 수집 제외 (timeout 에러는 수집⇒ cs대응, 사용자 경험 개선할 수 있는 지표 체크를 위함)
  • 분석하고자하는 api의 http status를 구분하여 수집 (4XX에러는 부분적 수집 제외)
  • 에러 데이터뿐만 아니라 디버깅과 분석에 필요한 추가적인 정보 수집
  • 에러 데이터가 아닌 정보성 데이터들을 센트리의 태그 레벨, 이슈 그루핑 등의 다양한 기능과 에러 확장을 통해 수집하고 분석할 수 있습니다.

3. 서버와의 로그 분석 정합성 높이기

  • 서버와 약속된 custom header를 추가하여 api를 요청한다.
    • 이는 에러 상황 시 서버 로그와 프론트엔드에 남은 데이터를 서로 대조하여 분석의 정합성을 높일 수 있다.
  • 태그 기능을 이용해 커스텀 헤더 정보를 남겨준다면 cs나 장애 상황시 서버 로그를 함께 더 빠르게 데이터를 추적해 볼 수 있게 된다.

4. 에러 추적 개선 후, 좋아진점

  • 브라우저 버전 문제나 빌드 설정과 같은 문제로 발생한 예상치 못한 에러를 발견해 사용자 경험을 개선함

  • 장애 탐지 시간, 원인 파악, 해결까지의 시간이 줄어듬

  • cs 인입 시 사용자의 환경에서 재현하지 않아도 에러 원인을 파악하고 이전보다 정확하게 안내할 수 있게 됨

  • 개발자 경험이 좋아짐

[참고자료]
https://www.youtube.com/watch?v=012IPbMX_y4&t=385s
https://tech.kakaopay.com/post/frontend-sentry-monitoring/

profile
큰 목표보단 꾸준한 습관 만들기

0개의 댓글