

【実装】Cloudflare Workers + Next.js でエラーをDiscord Webhookに自動通知する仕組みを作った
はじめに
個人開発のプロダクトを Cloudflare Workers にデプロイして運用していたところ、本番でエラーが起きても気づけないという問題がありました。console.error でログは出しているものの、Cloudflare のダッシュボードを能動的に見に行かないと気づけません。
そこで、Discord Webhook を使ってエラーを自動通知する仕組みを作りました。
この記事でわかること
-
Discord Webhook でエラー通知を送る
notifyError関数の実装 -
critical / warning / notice の3段階分類の設計
-
fire-and-forget でレスポンス遅延を防ぐ方法
-
Cloudflare Worker エントリポイント(worker.ts)と Next.js APIルートでのコード共有
-
レートリミット通知のスパム防止とPIIマスク
対象読者
-
Cloudflare Workers / Next.js でプロダクトを運用している方
-
個人開発・小規模チームでエラー監視を導入したい方
-
Sentry 等の外部サービスを使わずにシンプルに通知したい方
前提条件
-
Next.js(App Router)
-
Cloudflare Workers(OpenNext でデプロイ)
-
Discord サーバーとWebhook URL
1. 全体設計
アーキテクチャ
APIルート catch → notifyError() → Discord Webhook POST
worker.ts catch → notifyError(env, ...) → Discord Webhook POST
↓
buildDiscordPayload() ← 共通関数
ポイントは2つあります。
-
Next.js APIルートでは
process.envから環境変数を取得 -
worker.ts(Cloudflare Worker エントリポイント)では
envオブジェクトから取得
この違いがあるため、notifyError 関数は2箇所に定義しつつ、Discord embed のペイロード構築は buildDiscordPayload() として共通化しています。
3段階の重要度
| レベル | アイコン | 色 | 基準 |
|---|---|---|---|
| critical | 🚨 | 赤 | プロダクトが機能しなくなる |
| warning | ⚠️ | 黄 | 一部機能に影響するが動く |
| notice | 💬 | 紫 | 情報として把握 |
2. notifyError の実装
コア:ペイロード構築(共通関数)
// src/lib/error-notify.ts
export function buildDiscordPayload(
options: ErrorNotifyOptions,
appEnv: string
): object {
const config = SEVERITY_CONFIG[options.severity];
const envEmoji = ENV_EMOJI[appEnv] ?? "⚪";
const fields = options.metadata
? Object.entries(options.metadata).map(([name, value]) => ({
name: `🔹 ${name}`,
value: `\`${value}\``,
inline: true,
}))
: [];
return {
embeds: [{
author: { name: `${config.emoji} ${config.label}` },
title: options.title,
description: `\`\`\`\n${options.description}\n\`\`\``,
color: config.color,
fields,
footer: {
text: `${envEmoji} ${appEnv.toUpperCase()} • anoni`,
},
timestamp: new Date().toISOString(),
}],
};
}
この関数は webhook URL や環境変数の取得方法に依存しないため、どこからでも使えます。
Next.js APIルート用
// src/lib/error-notify.ts
export async function notifyError(options: ErrorNotifyOptions): Promise<void> {
console.error(
`[${options.severity.toUpperCase()}] ${options.title}:`,
options.description
);
const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
if (!webhookUrl) return;
const env = process.env.APP_ENV
|| (process.env.NODE_ENV === "production" ? "prod" : "local");
await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(buildDiscordPayload(options, env)),
}).catch(() => {});
}
console.error を内蔵しているため、呼び出し側は void notifyError(...) だけで済みます。
worker.ts 用(Cloudflare Worker)
// worker.ts
import { buildDiscordPayload, type ErrorNotifyOptions } from "./src/lib/error-notify";
async function notifyError(
env: CloudflareEnv,
options: ErrorNotifyOptions
): Promise<void> {
console.error(...);
const webhookUrl = env.DISCORD_WEBHOOK_URL;
if (!webhookUrl) return;
await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(buildDiscordPayload(options, env.APP_ENV || "local")),
}).catch(() => {});
}
worker.ts では process.env が使えないため、env オブジェクトを引数で受け取る薄いラッパーになっています。
3. 呼び出し側のパターン
fire-and-forget で呼ぶ
} catch (error) {
void notifyError({
severity: "critical",
title: "Stripe Webhook 処理エラー",
description: `イベント処理に失敗: ${error instanceof Error ? error.message : String(error)}`,
metadata: { eventType: event.type, eventId: event.id },
});
return NextResponse.json({ error: "Webhook processing failed" }, { status: 500 });
}
void をつけることで Promise を await せず、Discord への POST(100-500ms)をブロックしません。notifyError 内部の .catch(() => {}) で通知失敗も握りつぶすため、エラーレスポンスの遅延もなく安全です。
4. レートリミット通知の工夫
レートリミット超過は notice レベルですが、2つの工夫が必要でした。
スパム防止
同じユーザーが100回叩いたら100回通知が飛ぶのは困ります。Set で同一キーの通知を1回に制限しています。
const notifiedNamespaces = new Set<string>();
if (isOverLimit && !notifiedNamespaces.has(key)) {
notifiedNamespaces.add(key);
void notifyError({ ... });
}
PIIマスク
identifier にはメールアドレスが入ることがあるため、マスクしてからDiscordに送ります。
function maskIdentifier(identifier: string): string {
if (identifier.includes("@")) {
const [local, domain] = identifier.split("@");
return `${local[0]}***@${domain}`;
}
return identifier.length > 4
? `${identifier.slice(0, 2)}***${identifier.slice(-2)}`
: "***";
}
5. 環境変数と CI/CD
GitHub environment secrets で管理
dev / prod の2環境があるため、GitHub の environment secrets で分けて管理しています。
| 変数名 | dev | prod | local |
|---|---|---|---|
| DISCORD_WEBHOOK_URL | 設定済み | 設定済み | .env に設定 |
| APP_ENV | dev | prod | 未設定→local |
デプロイ時に wrangler secret bulk で Worker secrets に自動同期されます。
通知の見分け方
Discord embed の footer に環境アイコンが表示されるため、一目で判別できます。
-
🔴 PROD — 本番エラー、即対応
-
🟡 DEV — 開発環境、確認用
-
🟢 LOCAL — ローカル開発、テスト用
Tips
Discord Webhook は完全無料
Webhook は何本作っても、何回送っても無料です。レート制限(30リクエスト/分)はありますが、エラー通知程度なら問題ありません。
バッククォートのエスケープ
エラーメッセージにバッククォート()が含まれると Discord の fenced code block が壊れます。replaceAll` でエスケープしています。
description: `\`\`\`\n${options.description.replaceAll("\`", "ˋ")}\n\`\`\``,
テストの書き方
vi.spyOn(global, "fetch") で fetch をモックし、process.env を操作してテストできます。
it("DISCORD_WEBHOOK_URL 設定時に fetch を呼ぶ", async () => {
process.env.DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/test";
await notifyError({ severity: "warning", title: "テスト", description: "テスト" });
expect(fetchSpy).toHaveBeenCalledOnce();
});
まとめ
-
Discord Webhook でエラー通知する
notifyErrorを全APIルート(38ファイル、66箇所)に導入 -
buildDiscordPayloadを共通関数として切り出し、worker.ts と APIルートで共有 -
fire-and-forget(
void notifyError(...))でレスポンス遅延を防止 -
console.errorを内蔵し、呼び出し側のコードを最小化 -
レートリミット通知はスパム防止 + PIIマスク付き
-
追加ライブラリ不要、Cloudflare Workers 完全互換


