「契約による設計」を応用し、エラー通知のノイズを激減させる — KARTE Messageにおけるエラーハンドリング改善

はじめに

こんにちは、プレイドのKARTE Messageチーム エンジニアの土谷です。

今回はKARTE Messageの管理画面の開発で行った、契約による設計を参考にしたエラー通知の切り分け方法を紹介します。

KARTE Messageについて

KARTE Messageはマルチチャネル(Mail/アプリPush/ LINE[1] )での大量配信を行えるMAツールです。

最大2000rps近くの速度で配信しており、大量かつ高速な配信ができるMAツールとなっています。

詳しくは過去のブログで紹介しているので、そちらをご覧ください。

KARTE Messageにおけるエラー通知の課題

多様なユーザー入力とエラーの関係

KARTE Messageでは、自由度の高いユーザー入力を取り扱っています。

  • 配信対象を抽出するためのクエリ
  • 相対日付(「n日前」など)による複雑なフィルタ条件
  • 配信スケジュール(開始日とタイミングの組み合わせ)

これらの入力値の中には、構文エラーや論理的に無効な設定が含まれる場合があります。これらは「ユーザーが修正すべきもの」であり、必ずしも「システム的に問題がある状態」ではありません。

従来の通知の仕組みと課題

KARTE Messageでは、独自実装したlog.error()を呼び出すと、Datadogへの記録と同時にSentry(およびSlack)へ通知が飛ぶ仕組みになっています。 しかし、ユーザー入力に起因するエラーまで全てSentryに通知されていたため、以下の問題が発生していました。

  • オオカミ少年化: 通知が多すぎて、誰も即座には反応しなくなる。
  • 管理コストの増大: SentryのIssueが膨れ上がり、トリアージが困難になる。

「契約による設計」をベースにしたエラーの再定義

ユーザー起因のエラーとシステム起因のエラーを明確に分けるため、契約による設計の概念を取り入れました。

1. 事前条件(PreCondition)

メソッド実行時に呼び出し側が満たすべき条件です。

  • : キャンペーンの公開時に開始時刻が現在時刻よりも前(過去) に設定されている。
  • 対応: ユーザーに修正を促す(400系エラー)。エンジニアの即時対応は不要。

2. 事後条件(Postcondition)

メソッド終了時に保証されるべき条件です。

  • : キャンペーン公開時にネットワークエラーでDBへのデータの書き込みに失敗する。
  • 対応: システムの異常、外部サービスのダウンなど。エンジニアが調査・対応すべき(500系エラー)。

実装例

エラークラスの定義

エラーハンドリングをしやすくするため、それぞれ型を実装しています。

export class PreConditionError extends Error {
  constructor(public readonly message: string) {
    super(message);
    this.name = 'PreConditionError';
  }
}

export class PostConditionError extends Error {
  constructor(
    public readonly message: string, 
    public readonly originalError?: unknown
  ) {
    super(message);
    this.name = 'PostConditionError';
  }
}

handlerでのハンドリング

アプリケーションの処理が完了して、レスポンスを返す処理のあるhandlerで以下のようなエラーハンドリングを行っています。

export const errorHandler = (_request, res, error: unknown) => {
  if (error instanceof PreConditionError) {
    // ユーザー起因なので Info ログ(通知しない)
    logger.info('PreConditionError occurred', { error });
    return res.status(400).send({ error_message: error.message });
  } 
  
  if (error instanceof PostConditionError) {
    // システム起因なので Error ログ(Sentry通知)
    logger.error(error.message, { 
      isUnknownError: false, 
      originalError: error.originalError 
    });
    return res.status(500).send({ error_message: "Internal Server Error" });
  }

  // 想定外のエラーも通知対象とする
  logger.error("Unknown Error occurred", { error });
  return res.status(500).send({ error_message: "An unexpected error occurred" });
};

導入後の変化とこれからの課題

良かったこと

Slack通知が「対応が必要なもの」だけに絞られたことで、チーム内のアラートに対する意識が改善しました。「通知が鳴った=すぐに確認」という文化が徐々に醸成されてきています。

開発時も全てエラーとしてしまうのではなく、ユースケースから考えてどのようにエラーハンドリングすべきかを考えるきっかけにもなっています。

残っている課題

ライブラリが内部で発生させるランタイムエラーなど、本来は事前条件違反だが実装時には判断できないものがあり、事後条件違反として誤アラートされるケースがあります。これらをどう分類し直すかは、今後の改善ポイントです。

最後に

KARTE Messageは大規模な配信をしているので、そちらの配信基盤に目が向きがちですがシステムの安定稼働のためwebアプリケーションの開発・改善にも力を入れています!

興味がある方は採用ページをご覧ください。お待ちしております!


[1]: LINEとの連携機能は現状β版として提供しています。