メインコンテンツへスキップ
kt-tech.blog
【実装】Cloudflare Workers + Next.js でエラーをDiscord Webhookに自動通知する仕組みを作った
実装
(更新: 2026/3/22)· 約10分で読めます

【実装】Cloudflare Workers + Next.js でエラーをDiscord Webhookに自動通知する仕組みを作った

Share
💡
Cloudflare Workers + Next.js 環境で、全APIルートのエラーをDiscord Webhookに自動通知する仕組みを実装しました。3段階の重要度分類、fire-and-forget による遅延防止、PIIマスクなど、実運用を見据えた設計を解説します。

はじめに

個人開発のプロダクトを 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 完全互換

この記事が役に立ったら共有しよう

Share
Koki

Koki

フルスタックエンジニア / React, Next.js, TypeScript