GASとSlackAPIを使って、非エンジニアでも簡単にSlack内のコミュニケーションを分析できるツールを作ってみた

リモートワークが進むこのご時世、直接顔を合わせることなく採用を決めて、そのままチームに受け入れをするということも当たり前になってきたのではないでしょうか。新しいメンバーは上手にチームに馴染めているのだろうか...そう心配になることも少なくないはず。そこで、SlackのコミュニケーションのログをGoogleスプレッドシートに自動集計するツールを作って、非エンジニアであってもピポットテーブルや[Googleデータポータル](https://support.google.com/datastudio/answer/6283323?hl=ja)を使って簡単にコミュニケーションの分析をできるようにしてみました。 ※動かすまでの設定はエンジニアでないと難しいです

## 作ったもの
[takuya0206 / slackMessageLogger](https://github.com/takuya0206/slackMessageLogger) に公開しています。インストールから実際に動かすまでの手順はREADMEに記載していますので、そちらをご参照ください。MITライセンスで公開していますのでご自由に改変して頂いても問題ありません（※READMEは英語です）。

### こんな感じで使えるよ
![message_sheet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/e86dd10b-dd5d-d423-5ef6-2ab637e0b866.png)
messageシート。Slackのオープンチャンネルの全ての投稿が出力されます。

![mention_sheet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/2a464e32-d082-b62e-028f-49dc50257b6a.png)
mentionシート。オープンチャンネルの内、メンションのついている投稿のみ出力されます。一つの投稿でメンションが複数人についていることもありますが、その場合は人ごとに複数行で出力されます。

![example_dataportal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/be66373e-87e3-260e-22be-692e32a7df5a.png)

これはデータポータルでの可視化の例です。この画像ではチャンネルごとの投稿数をグラフにしています。また、投稿者によってフィルタが出来るようにしているので、特定のメンバーがどのチャンネルにどれだけ投稿しているかを見ることができるようになっています。
このようにスプレッドシートにログがあることで様々な角度の分析が簡単に出来るようになります。例えば、オンボーディングが上手くいっているかを定量的に振り返るのであれば、特定のメンバーが誰にメンションをつけているか、誰からメンションをつけられているか、などを可視化するのが有効かなと思いますが、そうした分析もデータポータルの設定だけで簡単に行えます。

## 作っているときの話
ここからは開発についてです。開発時に考えていたことや気をつけたことを備忘録として残しておこうと思います。
### 設計
分析をする度にエンジニアがログを出力するということは避けたかったので、スプレッドシートに自動出力をするというのは必須と思っていました。
![design-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/149c1651-5a6f-4b81-cf1e-d2eff70e83b4.jpeg)
初めは上記のようにSlackから投稿の度にOutgoing WebHooksでGASへ情報を送り、それをスプレッドシートに出力していくのが一番シンプルかつリアルタイムで良いかなと思ったのですが、しかし、Google workspaceのセキュリティの設定によっては外部のドメインからWebHookが受け取れないケースもあると考えて断念。
![desgin-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/9f33ad4b-a3f6-af9f-48b9-0c678fc43da2.jpeg)
最終的にはGASのトリガーを毎日起動させて、24時間以内に投稿されてメッセージをSlack APIを通じて取得し、それをスプレッドシートに反映するようにしました。実装も運用も少し複雑になるので残念でしたがやむを得ない選択でした。

### 実装
[gas-clasp-starter](https://qiita.com/howdy39/items/0e799a9bfc1d3bccf6e5)というgoogle/clasp をベースにTypeScriptでローカル開発ができるテンプレートを利用させてもらいました。GASで型定義して開発できるとはありがたいですねー！

#### Slack APIで取得できる数には上限がある
設計上、Slack側から投稿の情報を通知できないため、GAS側からSlackのワークスペース上で投稿されたメッセージを探索しないといけません。その為にはSlackの全てのチャンネルを取得し、一つひとつのチャンネルごとの投稿を取得していく必要があります。会社の規模によってはSlack APIの上限に引っかかってしまう可能性があると考えて、全数を取得できるよう考慮しました。 → [Paginating through collections](Paginating through collections)

```slack.service.ts 
 async getAllSlackChannels(pagination = ''): Promise<slackChannelProp[]> {
    let isNextCursor = true;
    let result = [];
    try {
      while (isNextCursor) {
        const res = UrlFetchApp.fetch(
          `${this.slackAPIURL}/conversations.list?token=${this.slackToken}&limit=1000&exclude_archived=true&pretty=1${pagination}`
        );
        const resInParse = JSON.parse(res.getContentText());
        result = result.concat(resInParse.channels);
        if (resInParse.has_more) {
          pagination = `&cursor=${resInParse.response_metadata.next_cursor}`;
          Utilities.sleep(100);
        } else {
          isNextCursor = false;
        }
      }
      return result;
    } catch (e) {
      console.error(`Failed getting slack channels -> ${e}`);
      return result;
    }
  }
```

このようにAPIのレスポンスの`has_more`を見て、次のページがある限りはループを回すようにしています。

#### 並列処理でGAS全体の処理時間を短く
Google workspaceのプランによってはスクリプトが6分でタイムアウトになるので、なるべく処理時間が短くなるように工夫をしました。これはチャンネルごとにメッセージを取得していく部分のコードです。

```index.ts
  await Promise.all(channels.map( async (channel) => {
    Utilities.sleep(100)
    const messages = await slack.getSlackMessagesWithin24hours(channel.id)
    return { channel: channel.name, messages }
  })).then(async (channelMessages) => {
    await Promise.all(channelMessages.map( async (channelMessage) => {
      if(channelMessage.messages.length > 0) {
        channelMessage.messages.map(async (message) => {
          if(!message.subtype && !message.bot_id){
            const post_at = dayjs.dayjs(parseInt(message.ts) * 1000).format('YYYY-MM-DD')
            const post_by = await convertUserIdToName(users, message.user)
            const thread_ts = message.thread_ts ? message.thread_ts : ''
            loggingMessage.push({
              ts: message.ts,
              post_at,
              channel: channelMessage.channel,
              post_by,
              text: message.text,
              thread_ts,
            })
            // log messages with each user whom someone mentions
            const talkToWhoms = getTalkToWhom(message.text)
            talkToWhoms.map( async (talkToWhom) => {
              const toWhom = await convertUserIdToName(users, talkToWhom)
              if(!toWhom){
                console.log(`Error: ${talkToWhom} doesn't exist in our user list.`)
              } else {
                loggingMention.push({
                  ts: message.ts,
                  post_at,
                  channel: channelMessage.channel,
                  post_by,
                  text: message.text,
                  thread_ts,
                  toWhom,
                })
              }
            })
          }
        })
      }
    }))

```
APIを叩く間隔を取りつつ、チャンネルに対するメッセージの取得を並列処理で行っています。その後、取得したメッセージに対してスプレッドシートに貼る用の加工であったり、メンションの抽出をするのですが、そこも並列処理をするようにしました。


## 終わりに
というわけで、[slackMessageLogger](https://github.com/takuya0206/slackMessageLogger) でした。GoogleスプレッドシートとGoogleデータポータルの組み合わせ、癖があって慣れるまで少し大変なのですが、お手軽にデータ分析ができて大変良いので、自動化ツールとの連携を今後も色々試してみたいと思います。