부제: 월 $1.50로 만드는 AI 기반 Sentry 에러 분석 봇
들어가며
앞서 설정해둔 Sentry, 이게 최선일까를 고민하고 있었다. 물론 문제 발생을 슬랙으로 받아보고, 이를 대시보드에서 자세히 확인할 수 있었지만, 이게 의외로 번거로운 일이다. AI가 점차 발전하니 이를 활용할 수 있는 방법은 없을까 고민했다. 찾아보니 Sentry 팀 플랜 이상에서 Seer AI라는걸 통해 분석해주는 게 있는 걸 봤다. 근데 찾아보니 인당 40$를 추가로 지불해야 사용할 수 있는거였다. 다른 방법이 없을까 고민하다 찾은건, 똑같이 메시지로 받지만 AI를 한 번 거쳐 한번에 어떤 문제인지를 파악하는 일이다.
회사 차원에서 적용하기 전, 이게 실제로 사용성이 있을까를 고민하며 개인적으로 진행해보았다.
Sentry API를 폴링하여 새 이슈를 감지, Claude Haiku로 분석하고 Telegram으로 알림을 보내는 작은 서버리스 봇을 만들었다. 월 운영 비용은 약 $1.50.
전체 동작 한눈에 보기
Vercel Cron Job (5분마다)
→ Sentry API 폴링 (lastSeen:>5분전 인 이슈 조회)
→ Sentry API (이벤트 상세 - 스택트레이스/Request/Breadcrumbs 포함)
→ Claude Haiku API (에러 분석)
→ Telegram Bot API (분석 결과 알림 발송)전체 흐름은 한 번의 Cron 호출로 끝난다. 외부 저장소 없이 Sentry API 쿼리 자체로 중복을 방지하는 게 이 설계의 핵심이다.
프로젝트 구조
sentry-ai-analyzer/
├── api/
│ └── cron/
│ └── analyze.ts # Vercel Cron 진입점
├── src/
│ ├── services/
│ │ ├── sentry-client.ts # Sentry REST API 호출
│ │ ├── claude-analyzer.ts # Anthropic SDK 래퍼
│ │ └── telegram-notifier.ts # Telegram Bot API 호출
│ ├── prompts/
│ │ └── error-analysis.ts # 에러 분석용 프롬프트
│ └── types/
│ ├── sentry.ts # Sentry API 응답 타입
│ └── analysis.ts # AI 분석 결과 타입
├── test/
│ ├── fixtures/sentry-issues.json
│ ├── sentry-client.test.ts
│ └── claude-analyzer.test.ts
├── vercel.json # Cron 스케줄 + maxDuration
├── tsconfig.json
└── package.json런타임 의존성은 @anthropic-ai/sdk 딱 하나. 나머지(Sentry, Telegram)는 모두 fetch로 직접 호출해서 의존성을 최소화했다.
// package.json (요약)
{
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@vercel/node": "^5.1.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
}동작 흐름 - 5분에 한 번 일어나는 일
1. Cron이 깨운다 (vercel.json)
{
"crons": [
{ "path": "/api/cron/analyze", "schedule": "*/5 * * * *" }
],
"functions": {
"api/cron/analyze.ts": { "maxDuration": 60 }
}
}Vercel이 5분 간격으로 /api/cron/analyze를 호출한다. Hobby 플랜의 함수 최대 실행 시간은 60초. 이슈 10건 × (Sentry 1초 + Claude 3초 + Telegram 0.5초) ≈ 45초로 충분히 들어온다.
잠시, cron에 대해 설명해볼까?
Cron이란?
Cron은 Unix 계열 시스템에서 유래한, 정해진 시간/주기에 자동으로 작업을 실행해주는 스케줄러. 이름은 그리스어 chronos(시간)에서 왔다고 한다.
핵심 개념
"매일 새벽 3시에 이걸 실행해줘", "5분마다 이거 체크해줘" 같은 반복 작업을 자동화하는 도구. 사람이 매번 버튼을 누를 필요 없이, 정의해둔 일정에 맞춰 알아서 돌아간다.
* * * * *
│ │ │ │ │
│ │ │ │ └─ 요일 (0-6, 일요일=0)
│ │ │ └──── 월 (1-12)
│ │ └─────── 일 (1-31)
│ └────────── 시 (0-23)
└───────────── 분 (0-59)예시:
0 3 * * *→ 매일 새벽 3시 0분/5 * * * *→ 5분마다0 0 * * 1→ 매주 월요일 자정0 9 1 * *→ 매월 1일 오전 9시
Vercel Cron은 이 개념을 서버리스 환경에 옮긴 것이다. vercel.json에 일정을 적어두면, Vercel이 그 시간에 맞춰 지정한 API 라우트를 HTTP 요청으로 호출해주는 구조이다.
그래서 위에 내가 설정한 부분은
{
"crons": [
{ "path": "/api/cron/analyze", "schedule": "*/5 * * * *" }
],
"functions": {
"api/cron/analyze.ts": { "maxDuration": 60 }
}
}Vercel이 5분마다 프로젝트의 /api/cron/analyze 엔드포인트를 자동으로 호출하고, 그 함수는 최대 60초까지 실행될 수 있다.
- 이 함수가 실행될 때 최대 60초까지 돌 수 있도록 허용
- 기본 타임아웃(Hobby 플랜은 보통 10초)보다 길게 잡음
- 60초 안에 끝나지 않으면 강제 종료됨
2. 보안 검증
// api/cron/analyze.ts
const authHeader = req.headers['authorization'];
if (authHeader !== `Bearer${process.env.CRON_SECRET}`) {
return res.status(401).json({ error: 'Unauthorized' });
}CRON_SECRET을 헤더로 검증해서 외부에서 함부로 호출하지 못하게 한다. Vercel Cron은 자동으로 이 헤더를 붙여주고, 외부 cron(GitHub Actions, cron-job.org 등)에서도 동일한 시크릿으로 호출 가능하다.
3. Sentry에서 이슈 조회 (sentry-client.ts)
여기가 이 프로젝트의 가장 중요한 부분이다.
export async function fetchNewIssues(): Promise<SentryIssue[]> {
const org = process.env.SENTRY_ORG;
const project = process.env.SENTRY_PROJECT;
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
return sentryFetch<SentryIssue[]>(
`/projects/${org}/${project}/issues/` +
`?query=is:unresolved lastSeen:>${fiveMinAgo}` +
`&sort=date` +
`&limit=10` +
`&environment=production`,
);
}설계 포인트:
lastSeen:>5분전: 초기에는firstSeen을 썼지만, 실제 구현에서는lastSeen으로 바꾸었다.firstSeen은 “이슈가 처음 만들어진 시점”이라 한 번 분석하면 끝이지만,lastSeen은 “마지막으로 발생한 시점”이라 기존 이슈의 재발도 감지할 수 있다.is:unresolved: 이미 해결된 이슈는 무시.environment=production: 개발 서버 노이즈를 잘라낸다.limit=10: 5분당 최대 10건. 자연스러운 rate limiting + Cron 60초 안에 모두 처리 가능한 수치.- KV같은 외부 저장소가 필요 없는 이유: 시간 윈도우 기반 쿼리이므로 별도 dedup 저장소 없이도 같은 이슈를 무한 반복 알림하지 않는다. cron 경계에서 최대 1번 중복될 수 있지만 실용상 무시할 수준.
4. 이벤트 상세 조회 — webhook이 못 주는 데이터
export async function fetchLatestEvent(issueId: string): Promise<SentryEvent> {
return sentryFetch<SentryEvent>(`/issues/${issueId}/events/latest/`);
}이슈 ID로 최신 이벤트를 조회하면 다음 데이터가 같이 딸려온다.
exception.stacktrace.frames- 파일명/함수명/라인 번호/소스 코드 컨텍스트request- HTTP method/URL/headers/bodybreadcrumbs- 에러 발생 직전 사용자 액션 기록tags-captureApiError가 설정한 커스텀 태그(api_endpoint,http_status,error_code,error_name등)
타입 정의(src/types/sentry.ts)에서 SentryEventEntry를 discriminated union으로 만들어 두어, 각 entry 타입마다 안전하게 추출할 수 있게 했다.
5. 프롬프트 빌드 (error-analysis.ts)
이 프로젝트는 다음과 같이 컨텍스트를 잘게 잘라 넣는다.
// 스택트레이스: 마지막 5 프레임만 (토큰 절약)
const stacktrace =
exceptionEntry?.data?.values?.[0]?.stacktrace?.frames
?.slice(-5)
?.map((f) => `${f.filename}:${f.lineNo} in${f.function}`)
?.join('\n') ?? 'N/A';
// API 에러일 때만 endpoint 섹션을 붙인다
const apiSection = tags['api_endpoint']
? `
- API Endpoint:${tags['api_method'] ?? ''}${tags['api_endpoint']}
- HTTP Status:${tags['http_status'] ?? 'N/A'}
- Error Code:${tags['error_code'] ?? 'N/A'}
- Error Name:${tags['error_name'] ?? 'N/A'}`
: '';전체 프롬프트는 다음 정보를 포함한다:
- Persona: “Next.js 15 / React 19 / TypeScript / Chakra UI / TanStack Query / Axios / Zustand로 구성된 건설 관리 플랫폼의 프론트엔드 에러 분석가”
- Error Details: 제목, 레벨, 타입, 메시지, 위치, 발생 횟수, 영향받은 사용자 수
- API 컨텍스트:
api_endpoint태그가 있으면 추가 (API 에러 vs 렌더링 에러 자동 분기) - Stack Trace: 마지막 5 프레임
- Request: method + URL
- Output Format 강제:
*Severity**,*Root Cause**,*Suggested Fix**,*Pattern**4섹션 한국어로
6. Claude Haiku 호출 (claude-analyzer.ts)
const response = await client.messages.create({
model: 'claude-haiku-4-5-20251001',
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }],
});Haiku 4.5를 쓴 이유는 속도와 가격. 건당 ~700 input tokens + ~500 output tokens ≈ $0.0008. 50건/일 가정 시 월 $1.20 수준이다.
응답 파싱은 정규식 4줄로 끝낸다.
function parseAnalysisResponse(text: string): AnalysisResult {
const severity = text.match(/Severity[*]*:\s*(CRITICAL|HIGH|MEDIUM|LOW)/i)?.[1] ?? 'MEDIUM';
const rootCause = text.match(/Root Cause[*]*:\s*(.+?)(?=\n\*\*|$)/s)?.[1]?.trim() ?? text;
const suggestedFix = text.match(/Suggested Fix[*]*:\s*(.+?)(?=\n\*\*|$)/s)?.[1]?.trim() ?? '';
const pattern = text.match(/Pattern[*]*:\s*(.+?)(?=\n\*\*|$)/s)?.[1]?.trim() ?? '';
return { severity, rootCause, suggestedFix, pattern, rawResponse: text };
}LLM이 마크다운 별표(**)를 붙이는 경우와 안 붙이는 경우 모두를 정규식 [*]*로 흡수한다. 파싱이 실패해도 rawResponse에 원본이 남으므로 Telegram 메시지에서 fallback이 가능하다.
7. Telegram 발송 (telegram-notifier.ts)
심각도별 이모지를 매핑해서 한눈에 보이게 한다.
const SEVERITY_EMOJI = {
CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🟢',
};메시지는 Telegram의 HTML parse_mode로 보낸다. 사용자 입력이 들어가는 모든 필드는 escapeHtml로 이스케이프해서 마크업 깨짐과 잠재적 인젝션을 방지한다.
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}실제 알림 예시:
🟠 [HIGH] Sentry Error
Title: [500] AxiosError: Request failed with status code 500
Endpoint: POST /api/v1/sites/{id}/documents
Occurrences: 12 | Users: 3
--- AI Analysis ---
Root Cause: 백엔드에서 파일 크기 제한(10MB) 초과 시 500 에러를
반환하고 있습니다. 클라이언트 사전 검증이 없어 발생합니다.
Fix: DocumentUpload 컴포넌트에서 파일 선택 시 크기 검증을
추가하고, 10MB 초과 시 토스트로 안내하세요.
Pattern: 반복 발생 - 백엔드 파일 크기 제한과 프론트엔드 검증 부재
🔗 View in Sentry그리고 실제로 넘어온 알림

8. 에러 격리 — 한 건 실패해도 나머지는 진행
for (const issue of issues) {
try {
const event = await fetchLatestEvent(issue.id);
const analysis = await analyzeError(issue, event);
await sendTelegramMessage(issue, event, analysis);
analyzed++;
} catch (err) {
console.error(`Failed to process issue${issue.id}:`, err);
}
}내부 try-catch로 개별 이슈 처리 실패가 전체 파이프라인을 멈추지 않게 한다. 외부 try-catch는 인프라 단위 실패(Sentry 전체 장애 등)만 500으로 응답한다.
환경 변수 요약
SENTRY_AUTH_TOKEN=sntrys_eyJ...
SENTRY_ORG=sentry org 이름
SENTRY_PROJECT=sentry에 작성된 프로젝트이름
ANTHROPIC_API_KEY=sk-ant-xxx
TELEGRAM_BOT_TOKEN=123:ABC...
TELEGRAM_CHAT_ID=123456789
CRON_SECRET=... # Cron 인증총 7개.
테스트 전략
vitest로 두 개의 핵심 모듈만 단위 테스트한다.
sentry-client.test.ts:global.fetch를 모킹해서 URL 파라미터(is:unresolved,firstSeen:>,limit=10,environment=production)와Authorization헤더가 정확히 들어가는지 검증.claude-analyzer.test.ts: 프롬프트 빌더가 스택트레이스, API 섹션, request 정보를 포함하는지 / 태그 없을 때 API 섹션을 빼는지 / 스택트레이스 없을 때N/A로 잘 처리하는지 검증.
Claude API 호출 자체는 테스트하지 않는다. 그건 Anthropic SDK의 책임. 우리는 프롬프트 입력과 응답 파싱만 보장하면 된다.
비용 분석
| 리소스 | 무료 tier | 월 예상 비용 |
|---|---|---|
| Vercel Serverless (Hobby) | 100k req/월 | $0 |
| Claude Haiku API | ~50건/일 × ~1.5k tokens | ~$1.50 |
| Telegram Bot API | 무제한 | $0 |
| Sentry API | Rate limit 내 | $0 |
| 합계 | ~$1.50/월 |
5분마다 cron이 돌아도 새 이슈가 없으면 Sentry 1회 + 즉시 200 응답으로 끝나기 때문에 Anthropic 비용이 크게 늘지 않는다.
주요 설계 결정 — 트레이드오프
| 결정 | 선택 | 대안 대비 이점 |
|---|---|---|
| 알림 트리거 | Cron 폴링 (5분) | webhook 권한 없이 가능 |
| 인증 | 개인 Auth Token | 관리자 권한 불필요 |
| 중복 방지 | Sentry API 쿼리 | KV 인프라 0개, 환경 변수 0개 |
| 속도 제한 | limit=10 | 외부 rate limiter 불필요 |
| 모델 | Claude Haiku 4.5 | 속도+가격 균형 (Sonnet 대비 1/12 비용) |
| 알림 채널 | Telegram | 무료, HTML 포맷 지원, 봇 생성 5분 |
| 의존성 | @anthropic-ai/sdk 1개 | cold start 빠름, 보안 표면 좁음 |
| 이슈 쿼리 | lastSeen:> | 신규 이슈뿐 아니라 재발도 감지 |
한계와 다음 아이디어
현재 구조는 의도적으로 단순하지만, 이런 한계가 있다.
- Cron 경계 중복: 5분 윈도우가 겹치면 같은 이슈가 두 번 알림될 수 있음. 실용상 거의 안 보이지만 거슬리면 Vercel KV 1줄 추가로 해결 가능.
- 분석 결과 보관 X: 매번 일회성. 트렌드 분석을 하려면 별도 저장 필요.
- 인터랙션 X: Telegram에서 “Resolve” 같은 액션 버튼 없음.
다음 아이디어로 가져갈 만한 것들: - 주간 에러 트렌드 요약 발송 - Telegram 인라인 키보드로 Sentry 이슈 resolve - 에러 유형별 프롬프트 분기 (네트워크/렌더링/상태관리 등) - 다중 프로젝트 지원
마치며
실제 서비스단까지는 연결하지 못했지만, 이런식으로 활용할 수 있음을 알게 됐다.
AI를 활용하다보면, 기존 코드를 작성하던 개발자의 모습이 아닌 기획하고, 오류를 찾고, 더 나은 설계와 자동화할 수 있는 부분을 자동화하는 것으로 옮겨가고 있음을 느낀다.
그러면서도 “아는 만큼 보인다”는 말이 있듯, 내가 아는만큼 어디가 더 개선될 수 있고 더 나은 설계가 될 수 있을지도 생각하게 된다. (그래서 당연하게도 AI의 활용 뿐 아니라 기본도 중요함을 새삼 인지한다)
단순히 코드를 구현하는것을 넘어, 모두가 더 단단한 프로덕트를 만들기 위한 여러가지 방식들이 있는 것 같아 그걸 배우고 구현해보는 재미가 또 다른 재미로 다가온다.