Sentry, 오류 알림을 넘어 AI 연결하기- 1부

들어가며

회사 프로젝트는 Sentry가 연결돼있다. 작년 여름 기본적인 틀을 구현해두었는데, 이후 프로젝트가 고도화되며 구현된 부분이 제대로 동작하지 않게 됐다. 아직 실 서비스 전이고, 이런저런 핑계로 미뤄두었다가 잠시 틈이 생겨 Sentry를 파보게 됐다.

여러 아티클을 읽어보며, 에러 메시지의 디테일을 중점적으로 구현하였다. 완성 후 조금 더 욕심이 생겨, AI를 연결하면 어떻게 활용할 수 있을지에 대한 맛보기까지 진행하였다.

1부에는 Sentry의 전반적인 설정을 통해 명확히 에러 보기에 대해,

2부에는 ai를 통해 문제점 미리 파악하기

에 대해 작성하려한다.

목차

  1. Sentry
  2. Sourcemap
  3. Sentry 이슈 해상도 높이기
  4. 노이즈 줄이기
  5. development/production 구분하기
  6. 트러블슈팅

1. Sentry

Sentry는 웹, 모바일, 백엔드 애플리케이션에서 발생하는 에러를 실시간으로 추적, 수집 및 분석하여 시각화해주는 개발자 친화적 모니터링 플랫폼.

설명처럼, Sentry(센트리)는 에러 발생 시 실시간으로 알림을 전달해주는 도구이다. 간단하게 연결할 수 있지만, 실제 사용에는 몇몇 디테일한 설정을 알고 해주어야 한다.

기존에 설치가 돼있었지만, 글 작성을 위해 설치부터 살펴보려한다.

1.1 Sentry 설치

npx @sentry/wizard@latest -i nextjs 최신 버전 설치

회사 프로젝트는 Nextjs로 구현돼있는데, 공식 홈페이지 보면 sentry는 정말 거의 모든 개발언어에 지원하는 툴같다.

설치시 물어보는 내용이 정~말 많다.

설치 시 나오는 내용 번역

◇  Do you want to continue anyway? → 계속 진행할까요?

◇  Are you using Sentry SaaS or self-hosted Sentry? → Sentry SaaS를 사용하나요, 아니면 자체 호스팅 Sentry를 사용하나요? ** 자체 호스팅 센트리를 사용한다고 설정하면 질문이 달라질 듯 하다.

◇  Do you already have a Sentry account? → 이미 Sentry 계정이 있나요?

●  If the browser window didn't open automatically, please open the following link to log into Sentry: → 브라우저 창이 자동으로 열리지 않았다면, 아래 링크를 열어 Sentry에 로그인하세요:

◇  Login complete. → 로그인 완료. ◇  Selected project 프로젝트명 → 선택한 프로젝트: 프로젝트명

◇  Please select your package manager. → 패키지 매니저를 선택하세요.

◇  Installed @sentry/nextjs with NPM. → NPM으로 @sentry/nextjs를 설치했습니다.

◇  Do you want to route Sentry requests in the browser through your Next.js server to avoid ad blockers? → 광고 차단기를 피하기 위해, 브라우저의 Sentry 요청을 Next.js 서버를 통해 라우팅할까요?

◇  Do you want to enable Tracing to track the performance of your application? → 애플리케이션 성능을 추적하기 위해 Tracing을 활성화할까요?

◇  Do you want to enable Session Replay to get a video-like reproduction of errors during a user session? → 사용자 세션 중 오류를 비디오처럼 재현할 수 있도록 Session Replay를 활성화할까요?

** 이러한 안내들이 센트리의 비용을 늘리는 듯. 일단 예를 디폴트로 생각하고 작성했다.

◇  Do you want to enable Logs to send your application logs to Sentry? → 애플리케이션 로그를 Sentry로 보내기 위해 Logs를 활성화할까요?

◆  Created fresh sentry.server.config.ts. → 새 sentry.server.config.ts를 생성했습니다.

◆  Created fresh sentry.edge.config.ts. → 새 sentry.edge.config.ts를 생성했습니다.

◆  Added new src/instrumentation.ts file. → 새 src/instrumentation.ts 파일을 추가했습니다.

◆  Added new src/instrumentation-client.ts file. → 새 src/instrumentation-client.ts 파일을 추가했습니다.

◆  Added Sentry configuration to next.config.js. (you probably want to clean this up a bit!) → next.config.js에 Sentry 설정을 추가했습니다. (추후 정리하는 것을 권장합니다.)

◆  Created src/app/global-error.tsx. → src/app/global-error.tsx를 생성했습니다.

◇  Do you want to create an example page ("/sentry-example-page") to test your Sentry setup? → Sentry 설정을 테스트하기 위한 예제 페이지("/sentry-example-page")를 만들까요?

◆  Created src/app/sentry-example-page/page.tsx. → src/app/sentry-example-page/page.tsx를 생성했습니다.

◆  Created src/app/api/sentry-example-api/route.ts. → src/app/api/sentry-example-api/route.ts를 생성했습니다.

◆  Created .env.sentry-build-plugin with auth token for you to test source map uploading locally. → 로컬에서 소스맵 업로드를 테스트할 수 있도록 auth token이 들어 있는 .env.sentry-build-plugin을 생성했습니다.

◆  Added .env.sentry-build-plugin to .gitignore. → .env.sentry-build-plugin을 .gitignore에 추가했습니다.

◇  Warning: The Sentry SDK is only compatible with Turbopack on Next.js version 15.4.1 or later. → 경고: Sentry SDK는 Next.js 15.4.1 이상에서 Turbopack과 호환됩니다.

◇  Are you using a CI/CD tool to build and deploy your application? → 애플리케이션을 빌드하고 배포하기 위해 CI/CD 도구를 사용하나요?

◇  Add the Sentry authentication token as an environment variable to your CI setup: → CI 설정에 Sentry 인증 토큰을 환경 변수로 추가하세요: SENTRY_AUTH_TOKEN=토큰정보

▲  DO NOT commit this auth token to your repository! → 이 인증 토큰을 저장소에 커밋하지 마세요!

◇  Did you configure CI as shown above? → 위 내용대로 CI를 설정했나요?

◇  Optionally add a project-scoped MCP server configuration for the Sentry MCP? → (선택) Sentry MCP를 위한 프로젝트 범위 MCP 서버 설정을 추가할까요?

** 추후 필요하다면? MCP 설정해놔도 될 듯하다.

◇  Looks like you have Prettier in your project. Do you want to run it on your files? → 프로젝트에 Prettier가 있는 것 같습니다. 파일에 실행할까요?

◇  Formatters have processed your files. → 포매터가 파일을 처리했습니다.

└ Successfully installed the Sentry Next.js SDK! → Sentry Next.js SDK 설치를 완료했습니다! You can validate your setup by (re)starting your dev environment (e.g. npm run dev) and visiting "/sentry-example-page" → 개발 환경을 (재)시작한 뒤(예: npm run dev) "/sentry-example-page"에 방문해서 설정을 검증할 수 있습니다. If you encounter any issues, let us know here: https://github.com/getsentry/sentry-javascript/issues → 문제가 있으면 여기로 알려주세요: https://github.com/getsentry/sentry-javascript/issues

  • 설치하면 기본적으로 4개 파일 생성과 next.config.js에 Sentry 설정이 자동으로 추가 된다.
    • Client (instrumentation-client.ts) — Runs in the browser
    • Server (sentry.server.config.ts) — Runs in Node.js
    • Edge (sentry.edge.config.ts) — Runs in edge runtimes
    • src/app/global-error.tsx

** 참고로 회사 프로젝트의 경우 서버컴포넌트(SSR)를 사용하지 않는다. Edge는 미들웨어 쪽. Client인 instrumentation-client.ts 을(를) 중점으로 확인하였다.

1.2 instrumentation-client.ts

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

// Sentry.init의 설명에 불필요한 설정은 제외하였음. 

Sentry.init({
  dsn: ENV.sentryDsn,

  environment: nodeEnv,
  enabled: !!ENV.sentryDsn,
  sampleRate: isProduction ? 1.0 : 0.1,

  integrations: [
    Sentry.replayIntegration(),
    Sentry.consoleLoggingIntegration({ levels: ["warn", "error"] }),
    Sentry.browserTracingIntegration(),
  ],

  beforeSend(event, hint) {
    const error = hint.originalException;

    // 네트워크 에러 태깅
    if (error instanceof Error && error.message === "Network Error") {
      event.tags = { ...event.tags, error_type: "network" };
    }

    return event;
  },

  tracesSampleRate: isProduction ? 1.0 : 0.1,
  enableLogs: true,

  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  sendDefaultPii: true,
});

export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
  • 해당 파일에 대한 설명은 최종 수정한 내용이라 wizard로 설치한 내용과 다른 부분이 있을 수 있다.
  • Sentry.init 에서 사용하는 메서드를 설명하는데 초점을 두기 위함이라 참고하면 된다.

1.2.1 Sentry.init 내부 설명

dsn — Sentry 프로젝트의 고유 주소.

  • 에러 데이터를 어느 Sentry 프로젝트로 보낼지 결정하는 엔드포인트 URL.

environment — 이벤트에 환경 태그를 붙임.

  • "production", "development" 등으로 구분되어 Sentry 대시보드에서 환경별로 필터링할 수 있다.

enabled — Sentry 초기화 자체를 켜고 끄는 스위치.

  • !!ENV.sentryDsn - 여기서는 DSN이 없으면 false가 되어 Sentry가 아예 동작하지 않는다.

integrations — Sentry에 추가 기능을 플러그인 형태로 연결하는 배열.

  • replayIntegration(): 사용자 세션을 비디오처럼 녹화하여 에러 발생 전후 상황을 재현할 수 있게 함.
    • DOM 변경사항을 기록하는 방식이라 실제 영상 녹화는 아님.
  • consoleLoggingIntegration({ levels: ["warn", "error"] }): console.warnconsole.error 호출을 Sentry Logs로 전송.
    • 에러 이벤트와는 별개로, 로그 레벨 기반 모니터링에 쓰인다.
  • browserTracingIntegration(): 페이지 로드, 라우트 전환, API 호출 등의 성능 트랜잭션을 자동으로 수집. tracesSampleRate와 함께 동작.

beforeSend(event, hint)— 이벤트가 Sentry 서버로 전송되기 직전에 호출되는 훅.

  • 이벤트를 수정하거나 null을 반환하면 전송을 막을 수 있다.
  • 여기서는 "Network Error" 메시지를 가진 에러에 error_type: "network" 태그를 붙여서 Sentry에서 네트워크 에러만 따로 필터링할 수 있게 하고 있음.

enableLogs — Sentry Logs 기능을 활성화.

  • 위의 consoleLoggingIntegration이 실제로 로그를 수집하려면 이 옵션이 true여야 한다.

replaysSessionSampleRate — 일반 세션 리플레이의 샘플링 비율.

  • 0.1이면 전체 사용자 세션 중 10%만 리플레이를 녹화.
  • 리플레이 데이터는 용량이 크기 때문에 보통 낮게 설정.

replaysOnErrorSampleRate — 에러가 발생한 세션의 리플레이 샘플링 비율.

  • 1.0이면 에러가 발생한 세션은 100% 리플레이를 저장.
  • 에러 디버깅에 가장 유용한 리플레이이므로 높게 설정하는 게 일반적.

sendDefaultPii — 사용자 IP 주소, 쿠키 등 개인 식별 정보(PII)를 이벤트에 포함할지 여부.

  • true로 설정하면 에러 발생 시 어떤 사용자에게서 발생했는지 추적이 쉬워지지만, 개인정보 규정(GDPR 등)에 따라 주의가 필요.

onRouterTransitionStart — Next.js App Router의 라우트 전환을 Sentry 트랜잭션으로 캡처하는 함수.

  • Next.js가 라우트 전환 시 이 함수를 호출하면 Sentry가 해당 전환의 성능 데이터를 기록한다.

sampleRate — 에러 이벤트의 샘플링 비율.

  • captureException이나 captureMessage로 전송되는 에러/메시지 중 몇 퍼센트를 실제로 Sentry에 보낼지 결정. 1.0이면 모든 에러를 전송하고, 0.1이면 10%만 전송한다.

tracesSampleRate — 성능 트레이싱(트랜잭션)의 샘플링 비율.

  • 페이지 로드, API 호출, 라우트 전환 등의 성능 데이터를 몇 퍼센트나 수집할지 결정. 트레이싱 데이터는 에러보다 양이 훨씬 많기 때문에 비용과 직결된다.
  • 실무에서도 production에 0.1 ~ 0.3 정도로 쓴다고 하는데, 이는 실제 사용량이 많아졌을 때 다시 조절할 계획이다.

2. Sourcemap

소스맵(source map) 변환된 코드와 원본 코드 사이의 매핑 정보를 담은 파일

왜 필요할까?

  • 오류 발생한 곳이 어디인지 명확하게 파악하기 위해.

프로덕션에 배포되는 JavaScript(또는 CSS)는 보통 원본 그대로가 아니다. 빌드 과정에서 minification(변수명 축소, 공백 제거), bundling(여러 파일 합치기), transpilation(TypeScript → JS, JSX → JS) 등을 거치게 되고 그 결과 브라우저에서 실행되는 코드는 아래와 같은 형태다.

function a(b,c){return b.map(function(d){return d*c+1})}

위 함수의 원본은 아래와 같았을 수 있다.

function scaleValues(values: number[], factor: number): number[] {
  return values.map((value) => value * factor + 1);
}

에러가 발생했을 때 스택 트레이스가 a 함수의 1번째 줄이라고만 알려주면 디버깅이 거의 불가능에 가깝다.

소스맵이 있으면 "원본 scaleValues 함수의 3번째 줄"로 변환해서 보여주게 된다.

  • 소스맵은 wizard SDK를 사용하면 자동으로 업로드된다고 하는데, 나는 되지 않았다.
  • 짧은 트러블슈팅은 글 제일 하단에 모아두었다.

3. Sentry 이슈 해상도 높이기

설정이 없는 기본적인 sentry는 이슈에 대해 즉각적으로 파악하기 어렵다.  sentry util 함수를 통해 이슈 발생의 이유를 명확하게 알아볼 수 있게 해본다.

수정전, 후의 이미지를 보면 어떻게 변경됐는지 한 눈에 보인다. 이 설정에 대해 알아본다.

수정전

  • 어떤 오류인지 알아보기 어려움


수정후

  • HTTP 상태 코드, Error Name 확인
  • 타이틀, 서브타이틀 구분 으로 가시성 높임!

3.1. Sentry.ts

  • 사용자 정보 설정, 현장 컨텍스트 설정, 에러 해상도 높이는 함수에 대해 모아놓은 파일
코드
// utils/sentry.ts

import * as Sentry from "@sentry/nextjs";
import axios from "axios";

/** 로그인 후 사용자 정보 설정 */
export const setSentryUser = (user: { id?: number; email?: string; name?: string }) => {
  Sentry.setUser({ id: String(user.id), email: user.email, username: user.name });
};

/** 로그아웃 시 사용자 정보 초기화 */
export const clearSentryUser = () => {
  Sentry.setUser(null);
};

/** 현장(construction) 컨텍스트 설정 */
export const setSentryConstruction = (constructionId: string) => {
  Sentry.setTag("construction_id", constructionId);
};

class ApiError extends Error {
  constructor(name: string, message: string, cause?: unknown) {
    super(message);
    this.name = name;
    this.cause = cause;
  }
}

/** 쿼리스트링 제거 + path params를 {id}로 치환 */
const normalizeUrl = (url: string): string => {
  try {
    return url
      .split("?")[0]
      .replace(/\/\d+/g, "/{id}");
  } catch {
    return url;
  }
};

/** API 에러를 Sentry에 캡처 */
export const captureApiError = (error: unknown) => {
  if (!axios.isAxiosError(error)) {
    Sentry.captureException(error);
    return;
  }

  const response = error.response;
  const request = error.config;
  const endpoint = request?.url || "unknown";
  const method = request?.method?.toUpperCase() || "UNKNOWN";
  const status = response?.status;
  const errorName = response?.data?.errorName;
  const normalizedEndpoint = normalizeUrl(endpoint);

  // 타이틀: [status errorName] AxiosError
  const statusPart = status
    ? errorName
      ? `${status} ${errorName}`
      : `${status}`
    : "NETWORK_ERROR";
  const title = `[${statusPart}] ${error.name}`;

  // 서브타이틀: METHOD /normalized/path
  const subtitle = `${method} ${normalizedEndpoint}`;

  Sentry.withScope((scope) => {
    scope.setTag("api_endpoint", endpoint);
    scope.setTag("api_method", method);
    scope.setTag("http_status", String(status || "NETWORK_ERROR"));
    if (response?.data?.errorCode) {
      scope.setTag("error_code", response.data.errorCode);
    }
    if (errorName) {
      scope.setTag("error_name", errorName);
    }

    scope.setFingerprint(["api-error", normalizedEndpoint, method, String(status || "NETWORK_ERROR")]);

    if (!status || status >= 500) {
      scope.setLevel("error");
    } else {
      scope.setLevel("warning");
    }

    scope.setExtra("request_url", `${request?.baseURL || ""}${endpoint}`);
    scope.setExtra("request_params", request?.params);
    scope.setExtra("request_body", request?.data);
    scope.setExtra("response_data", response?.data);

    Sentry.captureException(new ApiError(title, subtitle, error));
  });
};

3.1.1. captureApiError 내 설정된 Sentry 함수/기능 설명

Sentry.withScope(callback) - 역할: 임시 스코프(Scope)를 생성하여 그 안에서 설정한 태그/extra/fingerprint 등이 해당 이벤트에만 적용되도록 격리 - 왜 필요한가: captureException 옵션의 직접 전달은 fingerprint, level, breadcrumb 설정이 불가능하다. withScope 없이 글로벌 scope에 설정하면 다른 에러 이벤트에도 영향을 주게 된다. - 스코프 격리: 콜백 내부에서 설정한 값은 콜백이 끝나면 자동으로 사라져서 다른 이벤트를 오염시키지 않는다. scope.setTag(key, value) - 역할: Sentry 이벤트에 태그를 추가. 태그는 인덱싱되어 대시보드에서 검색/필터/알림 조건으로 사용 가능 - 사용 예: api_endpoint, api_method, http_status, error_code, error_name - extra와의 차이: tag는 검색 가능한 짧은 문자열, extra는 검색 불가능한 상세 데이터

  • 태그 정보 차이

설정전

설정후

  • 설정이 안됐을 때와 달리, api_endpoint, api_method, construction_id, error_code, error_name등을 확인 할 수 있다.

scope.setFingerprint(array) - 역할: Sentry가 에러를 그룹핑하는 기준을 직접 지정한다. - 왜 필요한가: 기본적으로 Sentry는 스택 트레이스 기준으로 그룹핑된다. API 에러는 모두 axios 인터셉터에서 발생하므로 스택 트레이스가 동일 → 서로 다른 API 에러가 하나의 이슈로 합쳐지는 문제가 있는데, 이를 내가 직접 그룹핑을 설정하여 원하는 그룹별로 이슈를 모으는 것이다. - 설정값: ["api-error", endpoint, method, status] → 같은 엔드포인트 + 메서드 + 상태코드 조합만 하나의   이슈로 묶임 - 예시: POST /login 403과 GET /drawings 500이 별도 이슈로 분리

  • 설정한 fingerprint 확인하기
    • 이슈 상세보기에서 JSON을 누르게 되면, 설정한 fingerprint를 확인할 수 있다.

    설정전

    설정후

    • default라 적힌 설정전과 달리, 설정 후엔 오류에 대한 정보가 기록돼 있다.
    • 또, 이슈의 하단으로 내려보면 Event Grouping Information 탭이 있는데, 여기서도 확인 가능하다.

scope.setLevel(level) - 역할: 이벤트의 심각도를 설정 (fatal, error, warning, info, debug) - 사용 방식: 5xx 서버 에러는 error, 4xx 클라이언트 에러는 warning으로 구분해두었다. 에러 종류는 심각도(fatal, error, warning, info, debug)처럼 더 다양하지만, 필요한 에러를 설정하였다. - 효과: Sentry 대시보드에서 심각도별 필터링, 알림 규칙에서 level 기준 조건 설정 가능하다. scope.setExtra(key, value) - 역할: 이벤트에 상세 디버깅 정보를 첨부. 검색은 불가하지만 이벤트 상세 페이지에서 확인 가능 - tag와의 차이: 객체, 배열 등 복잡한 데이터 저장 가능. 검색/필터 불가 - 저장 항목: 요청 URL, 파라미터, 요청 바디, 응답 데이터, 서버 에러 메시지, requestId, 서버 타임스탬프 Sentry.captureException(error)

- 역할: 에러를 Sentry 서버로 전송. withScope 내부에서 호출하면 해당 scope의 모든 설정이 함께 전송됨

4. 노이즈 줄이기

Sentry에 모든 이슈가 다 들어오면, 어떤게 더 중요한 이슈인지 파악이 어렵다. 이러한 노이즈를 줄이는 작업이 필요하다.

이슈로 잡아내지 않는 조건들은 아래와 같다.

조건코드/메서드필터링 이유
요청 취소axios.isCancel(error)페이지 이동 시 발생하는 정상 동작
네트워크 단절ERR_NETWORK사용자 환경 문제이며 서버/코드 문제 아님
타임아웃ECONNABORTED느린 네트워크 환경에서 발생하는 간헐적 노이즈
AbortController 취소ERR_CANCELEDisCancel에 잡히지 않는 취소 케이스 커버

회사 프로젝트는 axios를 사용중이므로, axiosInstance에 해당 부분을 추가해주었다.

// axiosInstance.ts

const shouldCaptureError = (error: unknown): boolean => {
  // 사용자 요청 취소
  if (axios.isCancel(error)) return false;

  // Axios 에러가 아닌 식별 불가능한 에러는 캡처
  if (!axios.isAxiosError(error)) return true;

  // 네트워크 단절 및 타임아웃
  const code = error.code;
  if (code === "ERR_NETWORK" || code === "ECONNABORTED" || code === "ERR_CANCELED") {
    return false;
  }

  return true;
};

...중략

// 응답 인터셉터 - refresh
axiosInstance.interceptors.response.use(
  (response) => response,
 
 ... 중략

    // 401(인증) 외 API 에러를 Sentry에 캡처
    if (error.response?.status !== 401 && shouldCaptureError(error)) {
      captureApiError(error);
    }

    return Promise.reject(error);
  }
);
  • 이를 통해, 무분별한 에러를 방지하고 필요한 에러만 제대로 확인할 수 있게 됐다.

5. development / production 구분하기

회사 프로젝트의 개발서버는 도커로, 운영서버는 vercel로 배포돼 운영된다. 개발서버에도 오류 알림이 일부 필요하다 정해져, 알림의 빈도수를 다르게 설정하고 개발/운영을 구분하기로 했다.
  • 환경변수 추가
    • 백엔드에도 개발서버인 도커에 추가를 요청했다.
//.env.development
NEXT_PUBLIC_NODE_ENV=development 설정
// instrumentation-client.ts

import { ENV } from "@/utils/env";

// NODE_ENV 또는 NEXT_PUBLIC_NODE_ENV를 확인하여 프로덕션 환경 판단
const nodeEnv = String(ENV.nodeEnv || process.env.NODE_ENV || "development");
const isProduction = nodeEnv === "production" || nodeEnv === "prod";

...

5.1. Slack 알림 설정 구분

dev를 새로이 설정했고, 운영서버와의 알림을 다르게 구분하는 게 좋겠다 생각하여 알림 설정까지 진행하였다.


5.1.1 슬랙 설정

슬랙에 채널 생성 후 , 채널 ID를 필요로 한다.


통합 탭에서는 앱 추가를 통해 Sentry를 넣어둔다.


5.1.2 Sentry Alerts 설정

Sentry Issues → Configure/Alerts 안에서 설정할 수 있다.

  • 하지만 기존에 운영에서 사용하는 슬랙 내용이 있어, duplicate하여 작성하였다.

**참고. 대신 넣어줄 때 이 두 개 다 지우고, 특히 # 까지 다 지우고 채널 이름을 넣어줘야한다.

6. 트러블슈팅

Sourcemap 업로드 오류

배경

  • 프로젝트에 @sentry/nextjs가 설정되어 있었으나, Sentry에 소스맵이 업로드되지 않아 에러 스택 트레이스가 minified 상태로 표시됐다.
  • 앞서 언급했듯 운영서버는 Vercel로 배포 중이다.

문제 원인 분석

1. 환경변수 이름 오류   - 문제: NEXT_PUBLIC_SENTRY_AUTH_TOKEN으로 설정되어 있었음. @sentry/nextjs는 SENTRY_AUTH_TOKEN만 인식한다.    참고로 NEXT_PUBLIC_ 접두사 시 클라이언트에 토큰 노출 위험이 있다.   - 해결: SENTRY_AUTH_TOKEN으로 이름 변경 2. CI/Vercel 빌드 중복   - 문제: GitHub Actions CI와 Vercel에서 각각 빌드 → 소스맵 중복 업로드   - 해결: CI에서 제거, Vercel에서만 업로드

  • vercel 업로드라는 걸 확인하지 않고 소스맵 업로드라는 부분에 대해 확인하다보니, CI에만 올리는 걸 알려주었다.
  • CI설정을 마치고 업로드가 잘 되는 것까지 확인했다.
  • 그러다 Ci와 별개로, Vercel에서 다시 빌드해서 처리하지 않나? 생각들었고, 이게 맞았다.
  • Ci에서 굳이 소스맵을 다시 하는건 괜히 처리 시간만 더 늘어나는 일이었고, 이 부분은 다시 원상복귀 시켜 해결하였다.

참고. vercel 배포 시 sentry 설정

제목만 보고 어떤 이슈인지 파악하기 1차,2차 수정& fingerprint 그룹핑

  • 위에 작성된 captureApiError는 이미 수정해서 작성해뒀지만, 처음에 작성했을때는 없었던 부분이 있다.
  • 그리고 2차로 수정이 됐다.
class ApiError extends Error {
  constructor(name: string, message: string, cause?: unknown) {
    super(message);
    this.name = name;
    this.cause = cause;
  }
}

/** 쿼리스트링 제거 + path params를 {id}로 치환 */
const normalizeUrl = (url: string): string => {
  try {
    return url
      .split("?")[0]
      .replace(/\/\d+/g, "/{id}");
  } catch {
    return url;
  }
};

이 변경을 통해 Sentry Feed 제목에서부터 명확한 이슈를 확인할 수 있다.

변경내용

항목BeforeAfter
Sentry 제목AxiosError[GET 403] /api/drawings - INSUFFICIENT_PERMISSION
Fingerprint쿼리 파라미터 포함 (과도 분산)쿼리 파라미터 제거 (정확한 그룹핑)
원본 에러 보존원본 객체 직접 전달Error.cause에 보존
스택 트레이스Axios 내부 스택ApiError 생성 위치 + 원본 cause

변경 전

1차 변경

2차 변경

2차 수정

타이틀과 서브타이틀의 조금 더 명시적이고 직관적인 구분

  // 타이틀: [status errorName] AxiosError
  const statusPart = status
    ? errorName
      ? `${status} ${errorName}`
      : `${status}`
    : "NETWORK_ERROR";
  const title = `[${statusPart}] ${error.name}`;

  // 서브타이틀: METHOD /normalized/path
  const subtitle = `${method} ${normalizedEndpoint}`;
  • 실제 인입되는 오류를 확인하면 확실하게 구분된다.

development와 production 구분 오류

개발서버와 운영서버 모두 Sentry를 적용하는 게 맞는가? 고민이 있었다. 상의후, 둘 다 사용하되 개발서버는 알럿의 비율을 낮추는 것으로 하기로 했다.

개발서버는 Docker로, 운영서버는 Vercel 배포로 운영되는 프로젝트에서 왜 개발서버의 에러도 Sentry에서는 development가 아닌 production에서 울리는 지 몰랐다.

백엔드 분은 Docker 설정할 때 이미 build:dev로 처리한다 하였고, 그렇기에 process.env.NODE_ENV에 development로 처리 될것이라 하였다.

하지만 해당 부분은 따로 환경변수가 필요했다.

build:dev는 .env.development를 로드하지만, next build 시점에 Next.js가 process.env.NODE_ENV를 *강제로 production으로 설정합니다.

그래서 여태 sentry에는 production에 쌓이고 있었구나!

해당 부분의 환경변수를 수정했다.

//.env.development
NEXT_PUBLIC_NODE_ENV=development 설정
// instrumentation-client.ts

import { ENV } from "@/utils/env";

// NODE_ENV 또는 NEXT_PUBLIC_NODE_ENV를 확인하여 프로덕션 환경 판단
const nodeEnv = String(ENV.nodeEnv || process.env.NODE_ENV || "development");
const isProduction = nodeEnv === "production" || nodeEnv === "prod";

...
  sampleRate: isProduction ? 1.0 : 0.1,
  tracesSampleRate: isProduction ? 1.0 : 0.1,

위 설정 후 테스트 결과, development 피드에 10% 의 확률로 오류가 쌓임을 확인할 수 있었다.