GASからSlackにWebhookで通知メッセージを送信する方法

GAS

※本記事はプロモーションを含みます

目次

Google スプレッドシートをベースに業務自動化を組んでいると、深夜の定期バッチ処理の完了確認や、データ不整合が起きた際のエラー検知をどのように受け取るかが必ず問題になります。メールで受け取ると他の通知に埋もれて見落とされやすく、わざわざスプレッドシートのログ履歴を開きに行くのは非現実的です。なので一番手軽な選択肢となるのが、Slackの指定チャンネルへの自動投稿です。Google Apps Script(GAS)からSlackへ通知を送信する場合、Slack AppのOAuth認証を使った本格的なBot開発を行う道もあります。ただし単純な通知目的であればIncoming Webhookを利用するのが最も手離れが良く堅牢です。というのは、GASの通信ツールであるUrlFetchAppを叩く段階で、パラメーターの渡し方やヘッダー指定に少しでもズレがあると、エラーも吐かずにSlack側で弾かれたり、想定外のHTTPステータスコードでスクリプトが止まったりするからです。本稿では、GASからSlackにWebhookで通知メッセージを送信する方法について、現場でそのまま運用に乗せられる組み立て方と落とし穴を整理しましょう。

UrlFetchApp.fetchでPOST送信を組む際にハマりやすい仕様の罠

GASから外部APIに対してHTTPリクエストを送る際、標準で用意されているのがUrlFetchAppツールのfetchメソッドです。このメソッドは非常に直感的に使える反面、HTTPメソッドのデフォルトがGETである点や、オブジェクトをそのまま送信しようとして失敗するケースが後を絶ちません。SlackのIncoming WebhookのエンドポイントはPOSTリクエストかつJSON形式のペイロードのみを受け付けます。なので単にURLを指定してfetchを呼ぶだけでは当然エラーになり、オプションオブジェクトの中で適切な設定を明示的に渡す必要があります。

最も頻出するミスは、送信したいメッセージを格納したJavaScriptオブジェクトを、JSON形式の文字列に変換せずにそのままpayloadプロパティへ渡してしまうケースです。GASのUrlFetchApp.fetchは、payloadにオブジェクトが直接渡された場合、自動的に「application/x-www-form-urlencoded」形式としてキーと値を解釈しようとします。ただしSlackのIncoming Webhookサーバーは「application/json」のフォーマットを要求しているため、送信されたデータが不正なフォーマットとみなされ、Slack側から「missing_text_or_fallback_or_attachments」や「invalid_payload」といったエラーレスポンスが返ってくる結果になります。スクリプト側でオブジェクトを定義して渡しているつもりでも、通信のレイヤーで形式が狂ってしまうと、メッセージはSlackには届きません。

また、もう1つの落とし穴が「contentType」プロパティの指定漏れです。payloadをJSON.stringifyで文字列化したとしても、リクエストヘッダーのContent-Typeが明示的に「application/json」になっていなければ、受信側のサーバーによっては正しくボディがパースされません。なのでGASのUrlFetchAppでは、optionsの第2引数に「contentType: ‘application/json’」を必ず含める必要があります。というのは、デフォルトのUrlFetchApp.fetchは、HTTPステータスコードが400番台や500番台のエラーレスポンスを受け取った際に、GAS全体の実行を止めて例外を投げる仕様になっているからです。なのでSlack側で一時的な接続障害が発生していたり、URLの指定が誤っていたりすると、スプレッドシートの重要な後続処理まで巻き添えでクラッシュしてしまいます。通知が失敗したからといって基幹のデータ書き込み処理まで中断させないためには、通信オプションの制御と例外処理の分離が不可欠になります。

Incoming Webhookを活用して即座に通知メッセージを送信する基本構文

Incoming Webhookの最大の強みは、Slack APIのBotトークン(xoxbから始まる文字列)やOAuth2の権限認可プロセスを完全にスキップできる点にあります。URLそのものが認可情報を含んでいるため、GAS側でトークンをヘッダーに載せて認証をクリアする必要がなく、指定のURLに対してJSONをPOSTするだけでメッセージが届きます。なのでこれは、スプレッドシートのトリガーで定期実行されるバッチ処理の完了報告や、try-catchブロックで拾った例外のアラート通知において、保守の手間を最小限に抑える上で決定打となります。

以下に、スプレッドシート業務の現場で安定して動作する最小構成の送信関数を提示しましょう。Webhook URLに対して、必要なヘッダーとシリアライズされたJSONペイロードを確実に送信するパターンです。

function sendSlackAlert() {
  const webhookUrl = "https://hooks.slack.com/services/xxx/yyy/zzz";
  const payload = {
    text: "🚨 エラーが発生しました。ログを確認してください。"
  };

  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(webhookUrl, options);
  const responseCode = response.getResponseCode();
  const responseText = response.getContentText();

  if (responseCode !== 200) {
    console.error(`Slack通知送信失敗: HTTP ${responseCode} - ${responseText}`);
  } else {
    console.log("Slack通知送信成功");
  }
}

このコードを使うタイミングは、スプレッドシートに記録された受発注データの更新バッチが終了した時や、外部システムから取得したCSVの取り込み処理で予期しないバリデーションエラーが発生した瞬間です。前述した「muteHttpExceptions: true」というオプションを付与している点に注目しましょう。というのは、この設定を入れておくことで、万が一Slack側でアクセス過多による一時的なエラーやURLの打ち間違いがあっても、UrlFetchApp自体がスクリプトの実行を強制終了させることを防げるからです。レスポンスコードを取得して200番以外であればGASの実行ログにconsole.errorで記録を残す設計にしておけば、業務処理を止めずに通知の成否だけを安全に監視できます。

なお、コードの「text」プロパティに渡している文字列は、最もシンプルなプレーンテキストですが、Slack特有のマークアップ記法もそのまま反映されます。たとえば文字を太字にしたい場合はアスタリスクで囲み、URLにハイパーリンクを貼りたい場合は山括弧を使って「」のように記述しましょう。これで通知の見やすさを大きく向上させることが可能です。

通知内容が崩れる・届かない事象を引き起こす特殊文字とペイロードの扱い

前述のコードを本番運用に組み込んだ際、テスト送信では問題なかったのに実際の運用データを含めた途端に送信エラーになるというトラブルが頻発します。というのは、この現象の主犯となるのが、スプレッドシート上のセルから取得した生データに含まれる特殊文字の処理不足だからです。Slackの通知メッセージには、Slack独自のテキストフォーマット仕様が存在し、通常のHTMLタグや一部の記号が衝突を起こします。

特に危険なのは、セル内の文章に「&」「<」「>」が含まれているケースです。Slackの内部レンダラーは、山括弧をメンション(ユーザーIDやチャンネルIDの指定)やURLリンクの特殊制御タグとして認識します。なのでセルの値として「A < B の条件で不整合」といった文字列をそのままtextフィールドに流し込むと、構文解析エラーを引き起こしてメッセージ全体が表示されなくなったり、以降のテキストが消し飛んだりします。Slack側へ送信する動的なデータについては、HTMLエスケープと同様の要領で「&」を「&」に、「<」を「<」に、「>」を「>」へと置換しておく処理を挟みましょう。これが実務上の防衛策となります。

もう1つ見落としがちなのが、スプレッドシートの改行コードです。セル内で「Alt + Enter」によって入力された改行コードは通常「\n」ですが、Windows環境から貼り付けられたCSVなどを経由している場合、キャリッジリターン(\r)が含まれた「\r\n」になっていることがあります。JavaScriptのオブジェクトをJSON.stringifyする過程で適切にエスケープはされます。ただし文字列の結合処理を自前で複雑に行っていると、意図しない場所でエスケープシーケンスが二重化され、Slack側で「\n」という文字そのものがそのまま画面に表示されてしまう格好悪い状態に陥ります。なのでSlack通知を自動化する際は、送信直前の文字列をプレビューログで確認し、意図通りの改行と記号処理が保たれているかを検証しましょう。

運用停止を防ぐためのURL保護とレートリミット対策

GASとIncoming Webhookの組み合わせは手軽である反面、運用のフェーズに進むと「セキュリティ」と「APIレート制限」という2つの現実的な壁に直面します。なのでこれらを軽視してスクリプトを放置すると、突然Slackチャンネルが荒らされたり、大事な通知が届かなくなったりする重大なインシデントに発展します。

まず絶対に避けなければならないのは、Webhook URLをGASのコード内にベタ書きしたまま、スプレッドシートを複数人で共有したり、GitHubなどの外部リポジトリへ連携したりすることです。Incoming WebhookのURLは、それ単体で指定されたSlackチャンネルへ誰でもメッセージを投げ込める強力な権限を持っています。というのは、URLが漏洩した時点で、チャンネルに対する書き込み権限を世界中に公開したのと同じ状態になるからです。社内の他メンバーが意図せず閲覧できる状態を防ぐためにも、URLはコードに直接記述せず、GASの「PropertiesService(スクリプトプロパティ)」に隔離して管理しましょう。スクリプトプロパティに「SLACK_WEBHOOK_URL」というキーで値を保存しておき、コード上からは「PropertiesService.getScriptProperties().getProperty(‘SLACK_WEBHOOK_URL’)」として動的に呼び出す設計にすることで、誤ってシートを複製された際にも認証情報の流出を食い止めることができます。

次に警戒すべきは、Slack APIのレート制限(送信頻度制限)です。Incoming Webhookには、基本的に「1秒間に約1回(1リクエスト/秒)」という厳格な送信レートの上限が課されています。なのでスプレッドシートを1行ずつループ処理で読み込み、条件に合致した行ごとにSlackへWebhookを叩くようなコードを書くと、数件〜数十件の通知が瞬時に走り、あっという間にレート制限に激突します。レート制限を超過すると、Slackサーバーは「HTTP 429 Too Many Requests」を返し、以降の送信を強制遮断します。

この事態を回避するための手段は2つあります。1つは、ループ内で送信する必要がある場合に「Utilities.sleep(1000)」を挟んで確実に1秒以上のインターバルを設けるアプローチです。ただしこの方法はGASの「6分間の最大実行時間制限」を圧迫するため、大量の通知には向きません。もう1つにしてより優れた現場の解決策は、通知内容を1つのメッセージにまとめる手法です。なのでループ処理の中でエラーや報告事項を配列にストックしておき、ループが完了した後に「\n」で結合して、1回のリクエストとしてまとめてSlackへ送信しましょう。この一括送信のアプローチを採ることで、API通信の回数を劇的に減らし、レート制限の回避とGASの実行時間短縮を同時に達成できます。

タイトルとURLをコピーしました