Post

Sentry로 효율적인 에러수집하기

Sentry

Sentry는 소프트웨어 개발 및 운영에서 발생하는 버그와 에러를 모니터링하고 추적하는 플랫폼. 주요 기능 및 특징은 다음과 같다:

  1. 실시간 에러 모니터링: Sentry는 실시간으로 애플리케이션에서 발생하는 에러를 모니터링하고, 사용자가 발생한 오류를 신속하게 확인 가능.

  2. 스택 추적: 에러가 발생한 곳의 스택 추적을 제공하여 버그의 원인을 빠르게 파악. 이를 통해 빠르고 정확한 디버깅이 가능.

  3. 알림 및 경고: 발생한 에러에 대한 알림 및 경고 기능을 제공하여 개발자가 문제를 놓치지 않고 신속하게 대응.

  4. 세분화된 데이터: Sentry는 에러가 발생한 환경, 사용자 정보, 디바이스 정보 등을 세분화하여 제공하여, 문제의 원인을 더 정확하게 파악.

Sentry를 사용하면 애플리케이션의 안정성을 높이고, 사용자 경험을 개선.

Sentry의 문제점

Sentry는 에러를 그룹핑하여 유사한 에러를 하나의 이슈로 묶어서 보여준다. 에러 그룹핑은 주로 다음과 같은 원리에 기반한다:

  1. 스택 트레이스 비교
    Sentry는 에러가 발생한 스택 트레이스를 분석하여 비슷한 스택 트레이스를 갖는 에러를 하나의 그룹으로 묶는다. 스택 트레이스에는 에러가 발생한 코드의 호출 스택 정보가 포함되어 있어, 유사한 호출 스택을 갖는 에러를 하나의 그룹으로 묶을 수 있다.
  2. 에러 메시지 비교
    에러 메시지를 분석하여 유사한 내용을 갖는 에러를 하나의 그룹으로 묶는다. 에러 메시지에는 주로 에러 유형 및 발생 원인에 대한 정보가 포함되어 있으며, 이를 기반으로 유사한 에러를 그룹핑한다.

정확성 문제: Error가 하나로 그룹화 되어 표현됨

에러는 기본 에러객체로 표현된다. 실제로 별다른 설정 없이 에러를 던지면 아래 사진과 같은 에러들이 수집된다.

같은 종류의 에러를 효과적으로 추적하고 분석하기 어렵다. 이를 해결하기 위해서는 명확한 에러 그룹화(Grouping) 전략을 수립하여 서버에서 발생한 다양한 종류의 에러를 효율적으로 관리해야 한다.

  • method, status, 발생한 장소가 다름에도 title이 모두 AxiosError로 나타나서 명확한 에러를 파악하기 힘들다.

또한 소스맵이 없는 에러의 경우 번들링되고 minify된 코드가 보여기 때문에 디버깅이 쉽지 않다.

과도한 로깅

권고사항대로 withSentry로 앱 전체를 감싸게되면 앱 내에서 발생하는 모든 오류를 잡아준다. 하지만 서비스가 커지다보면 그 에러의 종류가 많아지게 되고 throw만하고 catch는 안하는 상황 속에서 많은 에러들이 우후죽순 로깅되게 된다.

거기에 다양한 라이브러리에서 던져지는 에러들까지 합쳐지면 거의 에러를 방치하는 수준이 되어버린다..

해결방안

1. 에러 기준 성립 및 모듈화

• 에러 기준 정의: 각각의 에러 유형에 대한 명확한 정의와 우선순위를 설정했습니다. 예를 들어 네트워크 에러, 데이터베이스 에러, 그리고 기타 일반적인 에러 등에 대한 기준을 세웠다.

  • 에러 타입에 따른 그룹화
    AxiosError와 TypeError와 같은 다른 에러 유형을 구분하여 각각의 타입에 맞는 그룹을 형성. 같은 종류의 에러끼리 묶여서 관리.
  • 에러 메시지에 따른 그룹화
    에러 메시지의 내용을 기반으로 에러를 그룹화하여 관리합니다. (ex. “Network Error”나 “Internal Server Error”와 같은 특정한 문자열이 포함된 에러 메시지를 가진 에러들을 한 그룹으로 묶음.)
  • 에러 발생 장소에 따른 그룹화
    에러가 발생한 코드의 위치를 기준으로 에러를 그룹화. 이를 통해 특정한 코드 영역에서 발생한 다양한 종류의 에러들을 한 그룹으로 묶음.
  • 사용자 정의 그룹화: 특정한 비즈니스 로직이나 처리 흐름에 따라 에러를 그룹화하는 방식을 정의하여 적용.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
  // Axios를 사용하여 API 요청을 보냄
  const response = await axios.get("https://api.example.com/data");
} catch (error) {
  // 에러 타입에 따라 그룹화
  if (error instanceof AxiosError) {
    // Axios 에러 처리
    Sentry.captureException(error, { tags: { type: "AxiosError" } });
  } else if (error instanceof TypeError) {
    // Type 에러 처리
    Sentry.captureException(error, { tags: { type: "TypeError" } });
  } else {
    // 기타 에러 처리
    Sentry.captureException(error, { tags: { type: "OtherError" } });
  }
}

위의 예시에서는 AxiosError와 TypeError를 각각의 타입에 따라 그룹화하여 Sentry에 전송하는 방식이다. 이러한 방식으로 서버에서 발생한 다양한 종류의 에러를 효율적으로 관리하고 추적할 수 있다.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//src/util/log-error.ts

import {
  captureException,
  type SeverityLevel,
  withScope
} from "@sentry/nextjs";
import { AxiosError } from "axios";

import { SentryAxiosError, SentryCommonError } from "@/sentry/type";

interface IExtraData {
  page: string;

  query?: string;
}

interface SentryCommonErrorProps {
  name: string;
  message: string;
  type: string;
  level: SeverityLevel;
  extraData: IExtraData;
}

export const logCustomError = ({
  name = "default Error Name",
  message = "default Error Message",
  type,
  level,
  extraData
}: SentryCommonErrorProps) => {
  captureException(new SentryCommonError({ name, message }), {
    level,
    extra: { ...extraData, type }
  });
};

export const logAxiosError = (error: AxiosError, extraData: IExtraData) => {
  const sentryErr = new SentryAxiosError(error, extraData.page);

  const { config, status } = sentryErr;

  withScope((scope) => {
    /**
     * 다음 정보들을 이벤트의 그룹화를 위해 fingerprint 조건으로 설정.
     * HTTP method(GET, POST)
     * status(400, 404, 500)
     * page: pathname
     */
    scope.setFingerprint([
      String(status),
      config?.method ?? "",
      config?.url ?? "",
      extraData.page
    ]);
    captureException(sentryErr, { level: "error", extra: { ...extraData } });
  });
};

2. Sentry에 sourcMap 업로드 하기.

  • source map 설정을 통해 에러가 발생한 정확한 코드라인을 파악할 수 있다.

https://stackoverflow.com/questions/61011281/next-js-source-maps-with-typescript-on-sentry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// next.config.js
  ...
  webpack(config, options) {
    ...
    // Sentry Webpack configurations for when all the env variables are configured
    // Can be used to build source maps on CI services
    if (SENTRY_DNS && SENTRY_ORG && SENTRY_PROJECT) {
      config.plugins.push(
        new SentryWebpackPlugin({
          include: ".next",
          ignore: ["node_modules"],
          urlPrefix: "~/_next",
        }),
      );
    }
    ...

1. sentry-cli를 사용한 sourceMap 업로드

설정파일이 따로 설치되지 않기 때문에 직접 만들어줘야한다.

sentry-cli 설치

brew install getsentry/tools/sentry-cli

(1) sentry-cli를 위한 config 파일 설정 (공식문서 : https://docs.sentry.io/cli/configuration/ )

1
2
3
4
5
6
7
8
9
10
11
12
// .sentryclirc
[defaults]

url = 전송할 센트리 서버

org = 전송할 센트리 서버에 있고 token을 발급한 유저가 권한이 있는 조직

project= 전송할 센트리 서버에 있고 token을 발급한 유저가 권한이 있는 조직안의 프로젝트

[auth]

token=발급된 Auth Token 붙여넣기

(2) 릴리즈 정보를 (임의로 설정) 생성한다.

1
2
3
4
sentry-cli releases new ${package.version}

// example
sentry-cli releases new 0.3.5

(3) sourceMap이 있는 폴더로 이동하여 생성한 릴리즈 정보로 sourcemap을 전송한다.(https://docs.sentry.io/platforms/javascript/guides/nextjs/sourcemaps/troubleshooting_js/ )

1
sentry-cli releases files ${package.version} upload-sourcemaps build

(4) Sentry > Releases 확인

(5) 릴리즈 관련 설정을 마무리.

1
Sentry-cli releases finalize ${package.version}

(6) 센트리 초기화 부분에 릴리즈 정보를 입력

1
2
3
4
5
6
7
8
9
Sentry.init({

dsn: “클라이언트 DSN“,

release: "Test",

maxBreadcrumbs: 20,

})

과 같이 추가 입력해주면된다.

(7) localhost:8000으로 띄워서 확인하기. localhost:8000/index.html

2. git push 시 Sentry에 sourcMap 업로드 하기.

(1) Git > Settings > Secrets and variables 에서  Secrets ‘SENTRY_AUTH_TOKEN’ 을 추가

(2) 프로젝트 상단의 .github/workflows/sentry-upload.yml을 작성

  • SENTRY_ORG, SENTRY_PROJECT도 Sentry 홈페이지에서 확인이 가능
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
name: sentry-dev

on:
  push:
    branches: [dev]

jobs:
  sentry-upload:
    runs-on: ubuntu-latest
    environment: dev

    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Install Sentry CLI
        run: npm install -g @sentry/cli

      - name: Sentry Release
        id: sentry_release
        run: |
          VERSION=$(sentry-cli release propose-version)
          echo "::set-output name=version:$VERSION"
          yarn
          yarn build

      # BUILD & DEPLOY
      - name: Check APP BUILD
        id: twinit-build
        uses: andstor/file-existence-action@v2
        with:
          files: 'build/index.html'

      - name: get-npm-version
        id: package-version
        uses: martinbeentjes/npm-get-version-action@v1.3.1

      - name: Source Map Upload
        if: steps.twinit-build.outputs.files_exists == 'true'
        run: |
          sentry-cli releases new $
          sentry-cli releases files $ upload-sourcemaps build
          sentry-cli releases finalize $
        env:
          SENTRY_AUTH_TOKEN: $
          SENTRY_ORG: 'zippu'
          SENTRY_PROJECT: 'zippu-web'

      - name: Remove sourcemap
        if: steps.twinit-build.outputs.files_exists == 'true'
        run: |
          rm -rf build/

참고자료

  • https://www.hojunin.com/sentry
  • https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Error
  • https://docs.sentry.io/platforms/javascript/guides/nextjs/
  • https://tech.kakaopay.com/post/frontend-sentry-monitoring/
  • https://mingg123.tistory.com/219
  • https://kok202.tistory.com/109
  • https://docs.sentry.io/product/cli/configuration/
  • https://helloinyong.tistory.com/311
  • https://pozafly.github.io/tripllo/(11)vue-sentry-error-monitoring-system/
  • https://github.com/marketplace/actions/sentry-release
  • https://stackoverflow.com/questions/61011281/next-js-source-maps-with-typescript-on-sentry
This post is licensed under CC BY 4.0 by the author.