---
id: observability-error-reporting
title: Error Reporting — Sentry / 분류 / 우선순위
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [observability, error-reporting, sentry, vibe-coding]
tech_stack: { language: "TypeScript / Sentry / Bugsnag", applicable_to: ["Web", "Backend", "Mobile"] }
applied_in: []
aliases: [Sentry, exception capture, source maps, fingerprinting]
---

# Error Reporting

> 로그만으로는 에러 우선순위 판단 어려움. **Sentry / Bugsnag 같은 도구**가 fingerprint / count / 영향 사용자 수 / breadcrumb / source map / 알림을 묶어줌. 단 **모든 throw 가 capture 대상은 아님**.

## 📖 핵심 개념
- Fingerprint: 같은 종류 에러 묶기. 1만번 발생해도 하나의 issue.
- Breadcrumb: 에러 직전의 사용자/시스템 액션 시퀀스.
- Source map: minified 코드 stack 을 원본 매핑.
- Release: 어떤 빌드에서 발생했는지. SHA / version 으로.
- Sample rate: 트래픽 큰 곳은 sampling.

## 💻 코드 패턴

### Sentry Node
```ts
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  release: process.env.GIT_SHA,
  tracesSampleRate: 0.1,
  profilesSampleRate: 0.1,
  beforeSend(event, hint) {
    // PII redact
    if (event.user?.email) event.user.email = '[REDACTED]';
    // 무시할 에러
    if (hint.originalException instanceof KnownClientError) return null;
    return event;
  },
});

// Express integration
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
// ... routes ...
app.use(Sentry.Handlers.errorHandler());
```

### Sentry Browser (Vite/Next)
```ts
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn,
  release,
  integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
  tracesSampleRate: 0.1,
  replaysSessionSampleRate: 0.01,
  replaysOnErrorSampleRate: 1.0,
});

// React error boundary
<Sentry.ErrorBoundary fallback={<ErrorPage />}>{children}</Sentry.ErrorBoundary>
```

### 사용자 컨텍스트
```ts
Sentry.setUser({ id: user.id, email: user.email });
// 로그아웃
Sentry.setUser(null);
```

### Custom fingerprint — 잘못 묶이는 경우
```ts
Sentry.captureException(err, {
  fingerprint: ['stripe-decline', err.code],
  tags: { provider: 'stripe', errorCode: err.code },
  extra: { orderId, userId },
});
```

### 의도적 무시
```ts
// 사용자가 cancel 한 fetch — 에러 X
fetch(...).catch(e => {
  if (e.name === 'AbortError') return; // Sentry 안 보냄
  Sentry.captureException(e);
});
```

## 🤔 의사결정 기준
| 에러 종류 | capture |
|---|---|
| 예측된 도메인 실패 (잔액 부족) | ❌ — 정상 흐름 |
| 4xx (클라이언트 잘못) | 보통 ❌ |
| 5xx (서버 버그) | ✅ |
| 외부 API 실패 (재시도 후 final fail) | ✅ + provider tag |
| Browser unhandled rejection | ✅ (자동) |
| React render error | ✅ (ErrorBoundary onError) |
| AbortError | ❌ |
| Network offline | ❌ 또는 별도 이벤트 |

## ❌ 안티패턴
- **모든 에러 capture**: 노이즈 → 진짜 버그 파묻힘.
- **PII 그대로**: 이메일 / 카드 / 토큰. beforeSend 에서 redact.
- **source map 안 업로드**: stack 이 minified — 디버깅 불가. CI 가 release 와 함께 업로드.
- **release 태그 없음**: 어떤 버전 사고인지 모름. git SHA 자동.
- **fingerprint 잘못**: 같은 에러가 1000개 issue 로 분리. 또는 다른 에러가 한 issue 로.
- **알림 없음 / 알림 폭주**: critical 레벨만 PagerDuty. 나머지는 Slack/이메일.
- **breadcrumb 에 민감정보**: form 입력 자동 캡처가 비밀번호 leak. denyUrls / mask.
- **dev 에서도 Sentry on**: 노이즈. environment 분리.

## 🤖 LLM 활용 힌트
- 새 catch 블록: "예측 가능 vs 시스템 에러 분류 후 system 만 capture" 강제.
- ErrorBoundary onError + captureException + extra context.

## 🔗 관련 문서
- [[Error_Handling_Result_vs_Throw]]
- [[Observability_Structured_Logging]]
