목차
오류가 발생했을 때 빠르게 인지하고 수정할 수 있도록 센트리에 디스코드 훅을 도입하고자 했다.
센트리에서 제공해주는 것을 쓰면 유료기 때문에 직접 세팅해서 사용하는 방법을 공유한다.
웹훅 url 복사하기
먼저 디스코드에 들어가서 서버 연동을 들어가 보자
이때 이 버튼이 뜨지 않으면 권한이 없는 것이니 권한을 설정해주면 된다.


그 후 웹훅을 만들어주고 웹훅 url을 복사해주자
nextjs 세팅 (엔드포인트 만들기)
이때 nextjs의 api 엔드포인트를 사용했다.
다음과 같이 app 폴더의 api 폴더에 sentry-webhook 폴더에 라우터를 만들어 주고 코드를 복붙해주자

webhook을 위한 post 엔드포인트 코드이다.
// app/api/sentry-webhook/route.ts
export const runtime = 'nodejs';
const { DISCORD_WEBHOOK_URL } = process.env;
function ellipsis(s: unknown, max = 300) {
const str = String(s ?? '');
return str.length > max ? `${str.slice(0, max - 1)}…` : str;
}
function colorByLevel(level?: string) {
switch ((level || '').toLowerCase()) {
case 'fatal':
return 0x8e44ad; // 보라
case 'error':
return 0xe74c3c; // 빨강
case 'warning':
return 0xf39c12; // 주황
case 'info':
return 0x3498db; // 파랑
case 'debug':
return 0x95a5a6; // 회색
default:
return 0x2ecc71; // 초록
}
}
function emojiByLevel(level?: string) {
switch ((level || '').toLowerCase()) {
case 'fatal':
return '🟪';
case 'error':
return '🟥';
case 'warning':
return '🟧';
case 'info':
return '🟦';
case 'debug':
return '⬜';
default:
return '🟩';
}
}
function compactDate(d?: string) {
return d
? new Date(d).toISOString().replace('T', ' ').replace('Z', 'Z')
: undefined;
}
function safeTagList(event: any, limit = 6) {
const tags: Array<[string, string]> = event?.tags ?? [];
const arr = Array.isArray(tags) ? tags : Object.entries(tags || {});
return arr
.slice(0, limit)
.map(([k, v]) => `\`${k}:${v}\``)
.join(' · ');
}
function pickTopFrame(event: any) {
const entry = (event?.entries || []).find(
(e: any) => e?.type === 'exception',
);
const values = entry?.data?.values || event?.exception?.values || [];
const ex = values[values.length - 1] || values[0];
const frames = ex?.stacktrace?.frames || [];
if (!frames.length) return undefined;
const frame =
[...frames].reverse().find((f) => f?.in_app) || frames[frames.length - 1];
const location = [
frame?.filename || frame?.abs_path || '?',
frame?.lineno ?? '?',
]
.filter(Boolean)
.join(':');
const func = frame?.function || '(anonymous)';
return { location, func };
}
async function postToDiscord(payload: any) {
if (!DISCORD_WEBHOOK_URL) throw new Error('DISCORD_WEBHOOK_URL not set');
const res = await fetch(DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`Discord webhook failed: ${res.status} ${text}`);
}
}
function extract(body: any) {
const issue = body?.data?.issue ?? body?.issue;
const event = body?.data?.event ?? body?.event;
const project =
issue?.project?.name ?? event?.project ?? body?.project ?? 'Unknown';
const level = (event?.level ?? issue?.level ?? 'error').toLowerCase();
const environment =
event?.environment ??
issue?.environment ??
body?.data?.environment ??
'unknown';
const release =
event?.release ??
event?.contexts?.app?.version ??
issue?.firstRelease?.version ??
undefined;
const user =
event?.user?.email || event?.user?.username || event?.user?.id || undefined;
const shortId = issue?.shortId ?? undefined;
const issueUrl = issue?.url ?? body?.url ?? undefined;
const culprit = event?.culprit ?? issue?.culprit ?? undefined;
const message = event?.message ?? issue?.title ?? 'Unknown error';
const counts = {
count: issue?.count ? String(issue.count) : undefined,
users: issue?.userCount ? String(issue.userCount) : undefined,
firstSeen: compactDate(issue?.firstSeen),
lastSeen: compactDate(issue?.lastSeen),
};
const top = pickTopFrame(event);
const tags = safeTagList(event, 8);
return {
project,
level,
environment,
release,
user,
shortId,
issueUrl,
culprit,
message,
counts,
top,
tags,
};
}
export async function POST(req: Request) {
try {
const body = await req.json();
const {
project,
level,
environment,
release,
user,
shortId,
issueUrl,
culprit,
message,
counts,
top,
tags,
} = extract(body);
const levelEmoji = emojiByLevel(level);
const title = shortId
? `${levelEmoji} [${project}] ${ellipsis(message, 190)} • ${shortId}`
: `${levelEmoji} [${project}] ${ellipsis(message, 220)}`;
const lines: string[] = [];
if (culprit) lines.push(`**Culprit**: \`${ellipsis(culprit, 180)}\``);
if (top)
lines.push(
`**Top Frame**: \`${ellipsis(`${top.location} · ${top.func}`, 220)}\``,
);
if (tags) lines.push(`**Tags**: ${ellipsis(tags, 900)}`);
const description = lines.join('\n');
const fields = [
environment && {
name: 'Environment',
value: `\`${environment}\``,
inline: true,
},
release && {
name: 'Release',
value: `\`${ellipsis(release, 60)}\``,
inline: true,
},
user && {
name: 'User',
value: `\`${ellipsis(user, 60)}\``,
inline: true,
},
counts.count && {
name: 'Events',
value: `\`${counts.count}\``,
inline: true,
},
counts.users && {
name: 'Users',
value: `\`${counts.users}\``,
inline: true,
},
counts.firstSeen && {
name: 'First Seen',
value: counts.firstSeen,
inline: true,
},
counts.lastSeen && {
name: 'Last Seen',
value: counts.lastSeen,
inline: true,
},
issueUrl && {
name: 'Issue',
value: `[Open in Sentry](${issueUrl})`,
inline: false,
},
].filter(Boolean) as Array<{
name: string;
value: string;
inline?: boolean;
}>;
const embed = {
title,
url: issueUrl,
description: ellipsis(description, 3900),
color: colorByLevel(level),
timestamp: new Date().toISOString(),
footer: { text: `Sentry → Discord • ${project}` },
fields,
author: {
name: 'Sentry Alert',
url: issueUrl,
icon_url:
'https://raw.githubusercontent.com/getsentry/sentry-docs/main/src/images/favicon.png',
},
};
await postToDiscord({ username: 'Sentry Bot', embeds: [embed] });
return Response.json({ ok: true });
} catch (err) {
console.error('❌ Sentry Webhook 처리 실패:', err);
return new Response('fail', { status: 200 });
}
}
env파일을 그 후 만들어 주어 디스코드 웹훅을 연결해주자

Sentry 세팅
다음과 같이 sentry에서 integration에 들어가자

아래와 같이 배포한 사이트 url에 연결해주자

권한은 issue & event만 read 가능하게 만들어 주면 된다.

그 후 issue에서 alert에 들어가주자

아래와 같이 new-issue 새로운 에러 발생 시와
해결된 에러가 재발생 시를 체크해 준다.
(필요하면 프로젝트에 맞게 더 커스텀하자)
then에서 내가 만든 integration을 꼭 연결해주자

저기에 있는 send test notification을 눌렀을 때 디스코드에 메세지가 오면 성공적으로 연결된 것이다!

실제 오류 테스트 방법
만약 실제 배포 사이트에서 오류 발생 시 오류가 오는지 확인하고 싶다면 다음과 같이 세팅하고 실험해보면 된다.


'코딩 정보 > NextJs' 카테고리의 다른 글
| [NextJs] 개발 환경 효율적인 로깅 시스템 도입하기 (0) | 2025.09.26 |
|---|---|
| [to-do-pin] npm 개선 일지 (0) | 2025.09.12 |
| next-dev-pin npm 라이브러리 (0) | 2025.09.10 |
| 번들링에 대해 알아보자 (2) | 2025.09.03 |
| [개념부터 nextjs] 페이지 라우터의 한계, 서버 컴포넌트 도입 (1) | 2025.08.19 |