Amazon QuickSight の分析結果を Google Apps Script (GAS) でメッセージを整形して Slack へ通知する方法

QuickSightからSlackへリッチなテキストを送ろうと思うとひと手間必要でした。
2024.03.26

Amazon QuickSight はデータの可視化と分析を簡単に行えるサービスです。本記事では QuickSight で可視化したデータを CSV ファイルにしてメール送信し、Google Apps Script (GAS) を使ってメッセージを整形してから Slack へ通知する方法を紹介します。

「QuickSight から AWS Chatbot 経由で Slack に通知送れないですか?」

良い質問ですね。今のところそのようなインテーグレーションはないです。Slack へ通知するためにメール通知からどうにかしてみた記録です。

QuickSight の設定

以下のブログを参考にして QuickSight で分析したデータを CSV ファイルにして所定のメールアドレスへ定期送信する設定をします。CSV ファイル送信はオプションの有償機能を有効にしないと利用できません。

QuickSight のテーブル(表形式)の表示内容が CSV ファイルに出力されます。そのため、テーブルには Slack 通知時に載せたい情報を表示しておく必要があります。

Gmail の設定

QuickSight から届くメールに対して特定のラベルを付与するフィルタルールを作成します。このラベルが付いたメールを検索して CSV ファイルを取得する処理を GAS で実行します。

今回はBlogTestラベルが自動的に付与されるルールを作成しました。

Slack の設定

以下の記事などを参考にして Incoming Webhook のアプリを有効化します。

通知先のチャンネルを設定し、Webhook URL を控えておきます。後ほど GAS の設定で必要になります。

GAS の設定

  1. ローカル環境で GAS プロジェクトを作成
  2. Slack の Webhook URL をスクリプトプロパティに登録
  3. QuickSight から送信された CSV ファイルを取得し、内容を整形して Slack に通知するコードを作成
  4. GAS をトリガーで定期実行するよう設定

GAS 準備

ローカルで GAS を書くための初期セットアップを行います。

mkdir quicksight-sent-to-slack
cd quicksight-sent-to-slack
clasp create --type standalone --title "DevIO QS to Slack"
clasp open

ローカルでコードを書いて Web の GAS に反映させるには以下のコマンドを使用しました。

clasp push // 手動プッシュ
clasp push --watch // コードに変更があれば自動プッシュ

スクリプトプロパティの設定

GAS の Web UI でプロジェクトの設定から Webhook URL をスクリプトプロパティに登録します。秘密情報のハードコードを避けました。

CSV ファイルの内容

QuickSight から送信されたメールに添付されている CSV ファイルは以下です。CSV ファイルを取得し、必要な情報を取り出し Slack へ通知します。

組織名,先週の利用費,今週の利用費,差額,上昇率,担当者
ABC株式会社,25000,25300,300,0.012,山田太郎
DEF企業,18500,19800,1300,0.070,鈴木花子
GHI組合,32200,34500,2300,0.072,佐藤一郎
JKL会社,42800,45600,2800,0.065,木村春香
MNO団体,16700,21200,4500,0.269,高橋洋介
PQR協会,29400,30100,700,0.024,田中博之
STU財団,21300,22000,700,0.033,渡辺美咲
VWX社団,38900,39400,500,0.013,伊藤健太
YZA組織,17600,18100,500,0.028,中村優子
BCD連合,44200,48900,4700,0.106,小林直樹

Slack 通知コード全文

1 つのファイル(devio.gs)を作成しました。最新のメールに添付された CSV ファイルを取得したいため、常に特定のラベルが付いたメールは 1 件のみという条件下で動作します。

  • Gmail の特定のラベル(BlogTest)が付いたスレッドから CSV ファイルを取得
  • CSV ファイルの内容を整形して Slack に通知
  • CSV ファイル内の差額データに応じて、Slack 通知時のサイドバーの色を変更
  • Slack 通知後、メールからラベルを削除

ラベルを削除しないと、過去に受信した同じラベルが付与されているメールも処理されてしまいます。最新のメールに添付された CSV ファイルが欲しいため、過去のメールに付与されたラベルを処理後に削除するようにしました。 メール検索方法は他に良い方法があるのかもしれませんが、私の要件は満たせたので以下のコードに落ち着きました。

// 色の定数を定義
const COLOR_RED = "#A30100";
const COLOR_YELLOW = "#DAA038";
const COLOR_GREEN = "#2EB886";
const COLOR_ERROR = "#A30100";

// 差額のしきい値を定義
const COST_DIFFERENCE_THRESHOLD_HIGH = 1000;
const COST_DIFFERENCE_THRESHOLD_MEDIUM = 500;

function processTopMoversEmail() {
  const labelName = 'BlogTest';
  const targetLabel = GmailApp.getUserLabelByName(labelName);
  const threads = GmailApp.search(`label:${labelName}`, 0, 1);

  if (threads.length === 0) {
    notifySlackError('ラベルが付いたメールが見つかりませんでした。');
    return;
  }

  const thread = threads[0];
  const messages = thread.getMessages();

  if (messages.length === 0) {
    notifySlackError('メールにメッセージが含まれていませんでした。');
    return;
  }

  const attachments = messages[0].getAttachments();
  const csvFile = attachments.find(attachment => attachment.getContentType() === "text/csv");

  if (!csvFile) {
    notifySlackError('CSVファイルが添付されていませんでした。');
    return;
  }

  const csvContent = csvFile.getDataAsString();
  const csvData = Utilities.parseCsv(csvContent);

  let counter = 1;
  csvData.slice(1).forEach(csvRow => {
    const tableData = processCSVData(csvRow, counter);
    notifySlack(tableData);
    counter++;
  });

  // ラベルを削除する
  thread.removeLabel(targetLabel);

  // 不要ならメール削除する
  // messages.forEach(message => message.moveToTrash());
}

function notifySlackError(errorMessage) {
  const message = {
    "attachments": [
      {
        "color": COLOR_ERROR,
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": `*Error*\n${errorMessage}`
            }
          }
        ]
      }
    ]
  };

  notifySlack(message);
}

function processCSVData(csvRow, counter) {
  const orgName = csvRow[0];
  const cost = Number(csvRow[1]);
  const differenceCost = Math.ceil(Number(csvRow[3]));
  const riseRate = `${Math.ceil(csvRow[4] * 100)}%`;
  const salesPerson = csvRow[5];

  const sidebarColor = getSidebarColor(differenceCost);

  // 3桁ごとにカンマを入れる
  const formattedCost = cost.toLocaleString();
  const formattedDifferenceCost = differenceCost.toLocaleString();

  const tableData = {
    "attachments": [
      {
        "color": sidebarColor,
        "blocks": [
          {
            "type": "divider"
          },
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": `*${counter}. ${orgName}*`
            }
          },
          {
            "type": "section",
            "fields": [
              {
                "type": "mrkdwn",
                "text": `*利用費*\n¥${formattedCost}`
              },
              {
                "type": "mrkdwn",
                "text": `*上昇率*\n${riseRate}`
              },
              {
                "type": "mrkdwn",
                "text": `*差額*\n¥${formattedDifferenceCost}`
              },
              {
                "type": "mrkdwn",
                "text": `*営業担当*\n${salesPerson}`
              }
            ]
          },
        ]
      }
    ]
  };

  return tableData;
}

function getSidebarColor(differenceCost) {
  if (differenceCost >= COST_DIFFERENCE_THRESHOLD_HIGH) {
    return COLOR_RED;
  } else if (differenceCost >= COST_DIFFERENCE_THRESHOLD_MEDIUM) {
    return COLOR_YELLOW;
  } else {
    return COLOR_GREEN;
  }
}

function getSlackWebhookUrl() {
  return PropertiesService.getScriptProperties().getProperty('SLACK_WEBHOOK_URL');
}

function notifySlack(message) {
  const postUrl = getSlackWebhookUrl();
  const payload = JSON.stringify(message);
  const options = {
    "method": "post",
    "contentType": "application/json",
    "payload": payload
  };

  UrlFetchApp.fetch(postUrl, options);
}

トリガー設定

GAS 定期実行させるスケジュールを設定します。QuickSight で設定したメール送信タイミングに合わせて設定します。今回は毎週月曜日に定期実行する設定としました。

Slack への通知内容

以下のメッセージが通知されました。CSV ファイルの内容を整形してから Slack へ通知が実現できました。

通知内容に応じて見栄えを整えてあげれば充分に活用できそうですね。

まとめ

本記事では、QuickSight で可視化したデータを CSV ファイルをメールに添付し、GAS を使って Slack に通知する一連の流れを紹介しました。QuickSight の分析結果をリッチなフォーマットで Slack で共有できるようになりました。

おわりに

今回は比較的単純なユースケースの検証でしたが、QuickSight で分析した結果を手軽に Slack へ通知しようと考えたら GAS を使うことになるではないでしょうか。そう思ってはじめて GAS を触りました。他に良い方法あれば連絡いただけると幸いです。

参考