Qiitaの指標をGASで集計してNotion Chartsで可視化する

Qiitaの各指標（LGTM数, ストック数, はてブ数..）をGASを使って自動集計し、Notion Chartsで可視化してみました。指標が可視化されると記事執筆のモチベーションも上がるのでオススメです！
またQiitaの指標集計に興味がなくとも、Notion Chartsは本当に便利なので、「[Notion Chartsで可視化する](https://qiita.com/ryo2132/items/74a582aaa5b04d848654#notion-charts%E3%81%A7%E5%8F%AF%E8%A6%96%E5%8C%96%E3%81%99%E3%82%8B)」の章だけでも読んでもらえると嬉しいです。

# 何を作るの？

[こちらのNotionページ](https://www.notion.so/ryokawamata/Qiita-KPI-c3d712170a244065b2bb1c2549a8ad17)のようにQiitaの各指標のダッシュボードを作ります。チャートの値は日時で更新されます。
今回集計する指標は以下の通りです。

- Qiita 記事数
- Qiita LGTM数
- Qiita ストック数
- Qiita フォロワー数
- はてなブックマーク数

![ezgif.com-optimize (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/bbc652f3-8a96-f9b0-33d5-4ac5939c4edf.gif)


また、今回のclaspのプロジェクトは全て以下リポジトリにあります。もしサンプルコードが動かないなどあればこちらを参照してください。

https://github.com/kawamataryo/qiita-kpi


# GASで自動集計する
GAS(Google Apps Script)でQiita APIとはてなAPIを叩き、各指標をGoogleスプレッドシートに自動集計するまでを書きます。
GASをローカルで管理出来る様にするためCLIツールの[Clasp](https://github.com/google/clasp)を利用します。

## Claspプロジェクトの作成

まず、claspコマンドを使えるようにするために、claspをnpmでグローバルにインストールします。

```bash
$ npm i @google/clasp -g
```

次にclaspに自分のGoogleアカウントを紐づけます。

```bash
$ clasp login
```
loginコマンドでプラウザが開き、紐付けたいGoogleアカウントの選択と、権限の許可をしてください。
完了後コマンドラインに`Authorization successful.`と表示されれば紐付け完了です。

最後にcreateコマンドでclaspプロジェクトを作成します。
`--title qiita-kpi`でプロジェクト名を設定し`--type sheets`で、Google Spreadsheetが自動で作成されGASと紐づけています。また、`--rootDirt`で作業ディレクトリを指定しています。

```bash
# "qiita-kpi" の部分は任意のプロジェクト名を設定してください
$ clasp create --title qiita-kpi --type sheets --rootDir ./src
```

完了すると、`.clasp.json`とsrcディレクトリに`appscript.json`が生成されます。
`.clasp.json`にはGASスクリプトとの紐付き`appscript.json`にはプロジェクトの設定が保持されています。

createの段階だとGASのタイムゾーンが`America/New_York`となっているので、`Asia/Tokyo`に修正しましょう。

```diff:src/appscript.json
{
+ "timeZone": "Asia/Tokyo",
- "timeZone": "America/New_York",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}
```

この状態で`clasp open`コマンドを実行すると、プラウザでGASのエディタ画面が開かれるはずです。
これでClaspプロジェクトの環境構築は完了です。


## TypeScriptの環境構築と最初のスクリプト作成
TypeScriptで書きたいのでTypeScriptの環境を構築します。
Claspは標準でTypeScriptのトランスパイルに対応しているので、GASで使える関数の型定義をインストールするだけOKです。

```
$ npm init -y
$ npm install --save @types/google-apps-script
```

最初のスクリプトを書いてみます。
src配下に`index.ts`を作成し、以下を記述してみましょう。

```index.ts
function main() {
  const sheet = SpreadsheetApp.getActiveSheet();
  sheet.getRange(1,1).setValue("Hello Gas!");
}
```

これをGASにデプロイします。

```bash
# overrideを確認されるのでforce指定をしています。
$ clasp push -f
```

GASのエディタページを更新すると先ほどのコードが反映されているはずです。

![スクリーンショット 2020-07-04 15.11.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/a765430c-b36f-252a-9be5-a727244878c2.png)

main関数を選択し、▶️ でスクリプトを実行してみましょう。
ここでスプレッドシートへのアクセス権限を聞かれた場合は、以下記事参考に権限を許可してください。
https://tonari-it.com/gas-script-approval/

実行が完了すると、紐づくスプレッドシートに文字列が入力されているはずです。

[スプレッドシート一覧](https://docs.google.com/spreadsheets/u/0/?tgif=d)から新規に追加されているスプレッドシートを開き内容を確認してください。

![スクリーンショット 2020-07-04 15.20.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/1aefb744-022a-7e26-62af-7007443278d3.png)

書き込みが完了しましたね🎉

`Hello Clasp`の文字列は次項以降で不要なので削除し、代わりに以下のような表のヘッダー部分を作成してください。次項以降でA2以下に値を埋めていきます。

<img width="790" alt="スクリーンショット 2020-07-04 21.21.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/9d57931d-9ccf-0baf-f132-174cf4b4aebd.png">



## Qiita API、はてなAPIから指標を集計
Qiita API、はてなAPIから指標を取得するスクリプトを書きます。
APIの型情報があると便利なので最初にダウンロードしておきます。[こちらの記事](https://qiita.com/ryo2132/items/b5bf2d8b2aeea2838927
)で紹介している方法です。

```bash
# typesディレクトリの作成
$ mkdir src/types
# Qiita apiのschema.jsonからschema2typeを使って型情報を生成
$ curl https://qiita.com/api/v2/schema | npx json2ts > src/types/qiita-types.d.ts
```

そして`index.ts`を以下のように修正します。
コードの内容についてはコメントを記載しているので詳細な説明は割愛します。
ざっくりQiita API、はてなAPIから各指標を取得し集計、スプレッドシートの最終行に結果を挿入しています。

Qiita記事の総ブックマーク数の取得方法は、技術ブログの師匠[@kakakakakku](https://twitter.com/kakakakakku)さんに教えてもらいました。感謝 🙏

```src/index.ts
import { User, Item } from "./types/qiita-types";

const QIITA_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty(
  "qiitaAccessToken"
) as string;
const QIITA_USERNAME = PropertiesService.getScriptProperties().getProperty(
  "qiitaUsername"
) as string;

// -------------------------------------------------------------
// メイン処理 各指標のスプレッドシートへの書き込み
// -------------------------------------------------------------
function main() {
  const today = Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd");
  // Qiitaの指標取得
  const qiitaKpi = new QiitaClient(
    QIITA_ACCESS_TOKEN,
    QIITA_USERNAME
  ).fetchKpi();
  // はてなの指標取得
  const hatenaKpi = new HatenaClient(QIITA_USERNAME).fetchKpi();

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const insertLow = sheet.getLastRow() + 1;
  [
    today,
    qiitaKpi.postCount,
    qiitaKpi.lgtmCount,
    qiitaKpi.stockCount,
    qiitaKpi.followersCount,
    hatenaKpi.bookmarkCount
  ].forEach((data, i) => {
    sheet.getRange(insertLow, i + 1).setValue(data);
  });
}

// -------------------------------------------------------------
// Qiita API Client
// -------------------------------------------------------------
class QiitaClient {
  private readonly BASE_URL = "https://qiita.com/api/v2";
  private readonly PER_PAGE = 100;
  private readonly FETCH_OPTION = {
    headers: {
      Authorization: `Bearer ${this.accessToken}`,
    },
    method: "get" as const,
  };

  constructor(private accessToken: string, private username: string) {}

  // Qiita APIから指標取得
  fetchKpi() {
    const user = this.fetchUser();
    const items = this.fetchAllItems(user);
    const lgtmCount = this.tallyUpLgtmCount(items);
    const stockCount = this.tallyUpStockCount(items);

    return {
      lgtmCount,
      stockCount,
      followersCount: user.followers_count,
      postCount: user.items_count,
    };
  }

  // ユーザー情報の取得
  private fetchUser() {
    const response = UrlFetchApp.fetch(
      `${this.BASE_URL}/users/${this.username}`,
      this.FETCH_OPTION
    );
    return JSON.parse(response.getContentText()) as User;
  }

  // 全ての投稿記事の取得
  private fetchAllItems(user: User) {
    // 最大ページ数
    const maxPage = Math.ceil(user.items_count / this.PER_PAGE);
    // 投稿一覧の取得
    let allItems = [] as Item[];
    [...Array(maxPage)].forEach((_, i) => {
      const page = i + 1;
      const items = this.fetchItems(page, this.PER_PAGE);
      allItems = [...allItems, ...items];
    });
    return allItems;
  }

  // 投稿記事の取得
  private fetchItems(page: number, perPage: number) {
    const response = UrlFetchApp.fetch(
      `${this.BASE_URL}/authenticated_user/items?page=${page}&per_page=${perPage}`,
      this.FETCH_OPTION
    );
    return JSON.parse(response.getContentText()) as Item[];
  }

  // 記事をストックしたユーザーの取得
  private fetchStockers(itemId: string) {
    const response = UrlFetchApp.fetch(
      `${this.BASE_URL}/items/${itemId}/stockers`,
      this.FETCH_OPTION
    );
    return JSON.parse(response.getContentText()) as User[];
  }

  // LGTM数の集計
  private tallyUpLgtmCount(items: Item[]) {
    const lgtmCount = items.reduce(
      (result, item) => result + item.likes_count,
      0
    );
    return lgtmCount;
  }

  // ストック数の集計
  private tallyUpStockCount(items: Item[]) {
    const stockCount = items.reduce((result, item) => {
      const stockedUser = this.fetchStockers(item.id);
      return result + stockedUser.length;
    }, 0);
    return stockCount;
  }
}

// -------------------------------------------------------------
// Hatena API Client
// -------------------------------------------------------------
class HatenaClient {
  private readonly BASE_URL = "http://b.hatena.ne.jp";

  constructor(private qiitaUsername: string) {}

  // はてな APIから指標取得
  fetchKpi() {
    const bookmarkCount = this.fetchBookmarkCount();

    return {
      bookmarkCount,
    };
  }

  // ブックマークカウントの集計
  private fetchBookmarkCount() {
    const redirectUrl = this.getRedirectUrl(
      `${this.BASE_URL}/bc/https://qiita.com/${this.qiitaUsername}`
    );
    // `https://b.st-hatena.com/images/counter/default/00/00/0000653.gif` の形式で
    // ブクマ数が書かれたgif画像のURLを取得できるので、そこからブクマ数だけを抽出する
    const bookmarkCount = redirectUrl.match(
      /https:\/\/b.st-hatena\.com\/images\/counter\/default\/\d+\/\d+\/(\d+).gif/
    )![1];

    return Number(bookmarkCount);
  }

  // リダイレクトURLの取得
  private getRedirectUrl(url: string): string {
    const response = UrlFetchApp.fetch(url, {
      followRedirects: false,
      muteHttpExceptions: false,
    });
    const redirectUrl = (response.getHeaders() as any)["Location"] as string;
    if (redirectUrl) {
      const nextRedirectUrl = this.getRedirectUrl(redirectUrl);
      return nextRedirectUrl;
    } else {
      return url;
    }
  }
}
```

`indext.ts`を修正したら、内容をGASに反映します。

```bash
$ clasp push
```

次に、スクリプト上で参照している環境変数（スクリプトプロパティ）をGAS上で設定します。

上部メニューのファイル => プロジェクトのプロパティ => スクリプトのプロパティから以下2つの値を設定してください。

|プロパティ名|値|
|---|---|
|qiitaAccessToken|Qiita APIのアクセストークン。[こちら](https://qiita.com/api/v2/docs)を参考に取得してください。アクセストークンのスコープは`read_qiita`だけで大丈夫です。|
|qiitaUsername|Qiitaのアカウント名。アカウントページのプロフィール画像下部に表示されるIDの＠を除いた部分です。|

![スクリーンショット 2020-07-04 17.27.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/1ed2fb11-f6c4-eba4-e579-5841faa734cd.png)


設定できたらmain関数を選択し、▶️ でスクリプトを実行してみましょう。
※ ここで再度スプレッドシートへのアクセス権限を聞かれると思うので、再度許可してください。

スプレッドシートに各指標の値が出力されていれば成功です🎉

<img width="780" alt="スクリーンショット 2020-07-04 21.21.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/11ed6081-75dc-f178-7487-4ca5b352f03d.png">



## 定期実行の設定
これで集計処理の実装はできたので、あとはこのGASスクリプトを定期実行するようにします。
GASは標準で日時のトリガーを設定できるので、そちらを利用します。
まず、GASエディタ画面で実行ボタン横のタイマーマークを押します。

<img width="731" alt="スクリーンショット 2020-07-04 20.31.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/e5eb9626-acbe-28d5-3118-ac27a2e2dbb2.png">

GASのトリガー設定画面に遷移するので、新しいトリガーを作成します。
イベントのトリガーソースを「時間主導型」、時間ベースのトリガーのタイプを「日付ベースのトリガー」に設定し、あとは定期実行したい時刻を選択すれば完了です。

<img width="1099" alt="スクリーンショット 2020-07-04 20.32.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/ca9f2c44-d525-f291-4d68-5290bc164bf5.png">

この例だと、毎日午後11時〜12時にスクリプトが実行され、各指標がスプレッドシートに記録されます。


# Notion Chartsで可視化する
次に、GASで集計した指標をNotion Chartで可視化する手順を書きます。

## Notionとは？
まず最初にNotionの説明を簡単に。
Notionは、メモやタスク管理、カレンダー、スプレッドシート、Wikiなど、幅広い機能を備えているドキュメントアプリです。
簡単にページを外部公開できるのも特徴で、最近のノーコードの流れでますます注目を集めています。

https://www.notion.so/

<a href="https://www.notion.so/" target="blank"><img width="1452" alt="スクリーンショット 2020-07-04 21.05.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/0be7b821-8c41-0b61-45a9-c317c1aef210.png"></a>

## Notion Chartsとは？
Notion ChartsとはGoogleスレッドシートをデータソースに使い、Notionページに簡単にチャートを表示するツールです。

https://www.notion.vip/charts/

<a href="https://www.notion.vip/charts/" traget="_blank"><img width="1446" alt="スクリーンショット 2020-07-04 21.09.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/2cab29b5-d3ac-30f1-5f95-e31517655463.png"></a>

こちらのデモページのように様々なチャートを表示できます。
https://www.notion.so/Digital-Footprint-d4295448c72b4056af9b06fa8ece36d4


## 指標の可視化
まずNotion ChartsからGoogleスプレッドシートの情報が読めるように、スプレッドシートの共有設定を一般公開に変更します。

<img width="948" alt="スクリーンショット 2020-07-04 21.26.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/a9e03a6d-13c2-5d84-0e99-259f64554b1e.png">



次に、Notion Chartsのトップページ下部のConnect your Google Sheetに値を入力します。
`GOOGLE SHEETS DOCUMENT ID`はGoogleスプレッドシートのIDです。シートURLの`/d/`から`/edit`の間の文字列がIDとなります。
`SHEET NAME`はそのままシート名です。
`DATA RANGE`はチャート化したい値の範囲です。シートの行追加でチャートデータも更新したいため行範囲は多めに指定しましょう。（例だと`A1:B500`）


<img width="1018" alt="スクリーンショット 2020-07-04 21.27.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/86c01bb1-32cf-8a17-4c09-bdc7513f0062.png">

他チャートの色や種類なども指定できますが、一旦デフォルトで大丈夫です。
下部の`MAKE MAGIC`のボタンを押しましょう。
（どうでも良いのですが「MAKE MAGIC」って表現かっこいいですね）

<img width="770" alt="スクリーンショット 2020-07-04 21.25.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/c9e99dc8-a8ea-71e9-e98a-d314e5759ac1.png">

URLが表示されるので、ボタンクリックでコピーします。

 <img width="943" alt="スクリーンショット 2020-07-04 21.45.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/db3578b1-8638-f2a2-f5ac-4b9115b8194d.png">

そしてNotionページを開き`/embed`を入力し、表示されたフォームにコピーした値を貼り付けます。


このようにチャートが表示されればOKです。

<img width="1389" alt="スクリーンショット 2020-07-04 21.44.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/9f0fa16d-f673-1ae8-9ff3-f04bff34c74e.png">

あとは他の指標も同じ手順でチャート化しましょう。
そして最後にレイアウト整えれば完成です🎉

![ezgif.com-optimize (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/bbc652f3-8a96-f9b0-33d5-4ac5939c4edf.gif)



# 終わりに

以上「Qiitaの指標をGASで集計してNotion Chartsで可視化する」でした。
今回は、Qiita周りの指標とはてブ数のみでしたが、コードを追加すれば他にもGoogle Analyticsから週間PV、Twitter APIからフォロワー数なども集計することができます。以下リポジトリではそれらを集計しているので、もし興味あればコード見てみてください。
https://github.com/kawamataryo/blog-kpi

Notionは情報の一元化が簡単に出来てとても便利ですね。今後も使っていきたいです。