意外と簡単！Google Data Studio のコネクタの作り方（Qiitaのデータを表示する）

過去の記事で Google Data Studio を使って Googleアナリティクスのデータをビジュアル化しました。

:link:[無料でテクニカル レポートを作ろう！Google Data Studio＋Google Analyticsガイド](https://qiita.com/prograti/items/933280f821b4c2c1190c)

この時は、標準で用意されているコネクタを使ってGoogleアナリティクスに接続しました。しかし、Data Studioが標準でサポートしていないデータソースと接続する場合には、一旦GoogleスプレッドシートやBigQueryなどのサポートされているデータソースに記録して接続する必要があり、やや汎用性に欠ける部分があります。

いろんな用途で幅広く使いたいという場合はコネクタを自作した方が何かと都合が良いでしょう。そこで今回は、基本的なコネクタの作り方としてQiitaと接続するためのコネクタを作ってみたいと思います。

※英語のリファレンスはこちら:point_down:
https://developers.google.com/datastudio/connector/overview

##:beginner: 基本的な処理フロー
はじめに基本的な処理フローを見てみましょう。簡単に図示すると以下のような感じになります。

![Data Studioとコネクタの流れ](https://qiita-image-store.s3.amazonaws.com/0/184074/679e1d71-9823-803c-d230-72fa6a498708.gif)

コネクタはGoogle Apps Scriptのプロジェクトとして作成し、定義されたAPI（Data Studioからコネクタに対してコールされる関数）仕様に従って認証・認可やデータの取得、フォーマットなどの処理を実装する形になります。

##:gear: API一覧
Data Studioからコールされる関数の一覧は以下の通りです。必ずコールされる関数が4つ、OAuthを利用する場合にコールされる関数が4つの計8つの関数があります。 [^1]

###必ずコールされる関数
- **getAuthType**： Data Studioは、この関数から返される認証方式をもとに外部データソースとの接続に認可が必要かどうか判断する。認可が必要でまだ認可を取得していない場合、ユーザーはData Studioの画面上で認証・認可を行う。 [^2]
- **getConfig**： ユーザーが設定できるオプション情報を取得する。Data Studioは、この情報をもとにユーザーにオプション情報入力画面を表示し、コネクタはそこで入力された値を使って外部データソース接続などの処理を行うことができる。
- **getSchema**： コネクタが提供するデータの構造（フィールドの名前や型など）をData Studioに返す。Data Studioはこのデータ構造をもとにユーザーにフィールド情報を表示し、ユーザーがグラフを作成できるようにする。
- **getData**： Data Studioからリクエストが来た時に外部データソースなどに接続してデータを取得し、定義したデータ構造に合わせた形に整形して返す。

###OAuthを利用する場合にコールされる関数
- **isAuthValid**： 有効な資格を持っている場合はtrue、もっていない場合はfalseを返す。
- **get3PAuthorizationUrls**： 外部データソースへのアクセスを OAuthクライアントに認可するための認可エンドポイントを返す。
- **authCallback**： 認可サーバで認可を行った後にコールバックされる関数。
- **resetAuth**： 保存している資格情報を削除する。 [^3]

##  ![image.png](https://qiita-image-store.s3.amazonaws.com/0/184074/31c1e5f2-8792-3d06-4304-3efe9cc8e7b6.png) Qiitaコネクタを作ってみる
###事前準備１．OAuthライブラリを追加する
Qiitaと接続するためにOAuth認証が必要になりますが、OAuthクライアントを一から実装するのは大変です。今回は既にあるOAuthライブラリを利用します。Google Apps Scriptでプロジェクトを作成したら、メニューの「リソース」⇒「ライブラリ」からライブラリ画面を開き、以下のライブラリIDを入力してOAuthライブラリを追加してください。
ライブラリID：```1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF```
via https://github.com/googlesamples/apps-script-oauth2

![OAuthライブラリ](https://qiita-image-store.s3.amazonaws.com/0/184074/b4c5e6c2-9efa-63e3-e491-3c8fee862863.gif)

###事前準備２．マニフェスト ファイルにData Studioの情報を追記する
メニューの「表示」⇒「マニフェスト ファイルを表示」からマニフェスト ファイル（appsscript.json）を開き、Data Studioに関する情報を追記してください。

```appsscript.json
{
  "dataStudio": {
    "name": "Qiita Connector",
    "logoUrl": "https://ロゴのURL",
    "company": "コネクタの作者",
    "companyUrl": "https://コネクタの作者のURL",
    "addonUrl": "https://コネクタの詳細に関するURL",
    "supportUrl": "https://コネクタのサポート窓口のURL",
    "description": "Connect to your Qiita account"
  }
}
```
マニフェスト ファイルに記載した内容が以下のようにData Studioのコネクタの画面に反映されます。
![マニフェスト ファイル](https://qiita-image-store.s3.amazonaws.com/0/184074/bdfd72d2-a915-9bdd-5a09-3a17fba15daf.gif)

###事前準備３．Client IDとClient Secretを発行する
OAuthを利用するためにはClient IDとClient Secretが必要です。Qiitaの「設定」⇒「アプリケーション」⇒「アプリケーションを登録する」から必要な情報を入力し、Client IDとClient Secretを発行してください。なお、「リダイレクト先のURL」は以下のようになります。
リダイレクト先のURL：```https://script.google.com/macros/d/（スクリプト ID）/usercallback```
※スクリプトIDはプロジェクトのプロパティから参照できます。

プログラムの中にClient IDとClient Secretをハードコーディングしても良いのですが、ここではスクリプトのプロパティに設定して、プログラムからはプロパティ名で参照するようにしたいので、プロジェクトのプロパティからスクリプトのプロパティを選んで適当な名前で登録してください。

![report.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/b3d6244f-5dcc-61f0-5d49-856de2c747e9.gif)

###サンプルコード
では、実際にAPI一覧でご紹介した関数8つを実装してみたいと思います。

:one: **getAuthType**

```javascript
function getAuthType() {
  var response = {'type': 'OAUTH2'}; // 認可不要の場合は'NONE'を設定
  return response;
}
```

:two: **getConfig**

```javascript
function getConfig(request) {
  var config = {
    configParams: [
      {
        name: 'qiitaDataType',
        type: 'SELECT_SINGLE',
        displayName: 'Qiitaデータタイプ',
        helpText: '取得したいデータのタイプを選択してください。',
        options: [
          {
            label: '投稿',
            value: 'posts'
          },
          {
            label: 'いいね',
            value: 'likes'
          }
        ]
      }
    ]
  };
  return config;
}
```
サンプルでは投稿データと いいね データを分けて取得するため、ユーザーにどちらのデータを取得するのか選択させるようにオプション情報を設定しています。こうすると、Data Studioのコネクタの画面で下図のような画面を表示させることができます。

![getConfig](https://qiita-image-store.s3.amazonaws.com/0/184074/74ba00f0-bf54-657e-924d-daeba6e12f1e.gif)

選択リスト以外にもテキスト入力欄やチェックボックスなどが利用可能です。

:three: **getSchema**

```javascript
var connector = connector || {}; // namespace
connector.schema = {
  posts: [
    {
      name: 'title',
      label: 'タイトル',
      description: '投稿のタイトル',
      dataType: 'STRING',
      semantics: {
        semanticType: 'TEXT',
        conceptType: 'DIMENSION'
      }
    },
    {
      name: 'likes_count',
      label: 'いいね数',
      description: '投稿に対する いいね の総数',
      dataType: 'NUMBER',
      semantics: {
        semanticType: 'NUMBER',
        semanticGroup: 'NUMERIC',
        conceptType: 'METRIC'
      }
    },
    {
      name: 'comments_count',
      label: 'コメント数',
      description: '投稿に対するコメントの総数',
      dataType: 'NUMBER',
      semantics: {
        semanticType: 'NUMBER',
        semanticGroup: 'NUMERIC',
        conceptType: 'METRIC'
      }
    },
    {
      name: 'created_at',
      label: '投稿日',
      description: '投稿した日',
      dataType: 'STRING',
      semantics: {
        semanticType: 'YEAR_MONTH_DAY',
        semanticGroup: 'DATE_TIME',
        conceptType: 'DIMENSION'
      }
    }
  ],
  likes: [
    {
      name: 'id',
      label: 'ID',
      description: '投稿のID',
      dataType: 'STRING',
      semantics: {
        semanticType: 'TEXT',
        conceptType: 'DIMENSION'
      }
    },
    {
      name: 'title',
      label: 'タイトル',
      description: '投稿のタイトル',
      dataType: 'STRING',
      semantics: {
        semanticType: 'TEXT',
        conceptType: 'DIMENSION'
      }
    },
    {
      name: 'created_at',
      label: 'いいね作成日',
      description: 'ユーザーが いいね した日',
      dataType: 'STRING',
      semantics: {
        semanticType: 'YEAR_MONTH_DAY',
        semanticGroup: 'DATE_TIME',
        conceptType: 'DIMENSION'
      }
    },
    {
      name: 'likes_count',
      label: 'いいね数',
      description: 'いいね数',
      dataType: 'NUMBER',
      defaultAggregationType: 'COUNT',
      semantics: {
        semanticType: 'NUMBER',
        semanticGroup: 'NUMERIC',
        conceptType: 'METRIC'
      }
    }
  ]
};

function getSchema(request) {
  return {schema: connector.schema[request.configParams.qiitaDataType || 'posts']};
}
```

投稿データと いいね データを分けているためスキーマも2つ用意し、ユーザーが選択したQiitaデータタイプに合わせてスキーマを返すようにしています。スキーマには各フィールドの名前やラベル、説明、データ型、セマンティック情報などを定義します。セマンティック情報とは文字通りフィールドに意味的な情報を付加するもので、Data Studioのフィールドエディタで確認出来ますのでGoogleアナリティクスなどのフィールドを参考にしてみてください。

![セマンティック情報](https://qiita-image-store.s3.amazonaws.com/0/184074/12a34959-d874-86b9-ee9b-a70f12eb8415.gif)

:four: **getData**

```javascript
function getData() {
  var dataType = request.configParams.qiitaDataType || 'posts';
  var dataFunc = connector.dataFuncs[dataType];
  return dataFunc(request);
}

connector.dataFuncs = {};

connector.dataFuncs.posts = function(request) {
  // リクエストされたフィールドに対応するスキーマを作成
  var dataSchema = request.fields.map(function(field) {
    for (var i = 0; i < connector.schema.posts.length; i++) {
      if (connector.schema.posts[i].name == field.name) {
        return connector.schema.posts[i];
      }
    }
  });

  var posts = [];
  var access_token = connector.getOAuthService().getToken().token;
  // Qiitaに投稿データの1ページ目をリクエスト
  var response = UrlFetchApp.fetch(
      'https://qiita.com/api/v2/authenticated_user/items?page=1&per_page=100', 
      {headers: {'Authorization': 'Bearer ' + access_token}}
    );
  
  // レスポンスヘッダーから総ページ数を取得
  var headers = response.getAllHeaders();
  if ('Link' in headers) {
    var link = headers['Link'];
    var lastPageRegEx = /\?page=([0-9]+)&per_page=[0-9]+>; rel="last"/;
    var matches = link.match(lastPageRegEx);
    var lastPageStr = matches[1];
    var totalPages = parseInt(lastPageStr, 10);
  } else {
    var totalPages = 1;
  }
  
  // 全ページのデータを取得してマージ
  for (var i = 1; i <= totalPages; i++) {
    if (i > 1) {
      response = UrlFetchApp.fetch(
          'https://qiita.com/api/v2/authenticated_user/items?page=' + i + '&per_page=100', 
          {headers: {'Authorization': 'Bearer ' + access_token}}
        );
    }
    try {
      var currentPostData = JSON.parse(response);
    } catch (e) {
      throw new Error('取得したデータの解析に失敗しました。');
    }
    posts = posts.concat(currentPostData);
  }

  // スキーマに合わせて行データを作成
  var data = posts.map(function(post) {
    var values = [];
    dataSchema.forEach(function(field) {
      switch (field.name) {
        case 'title':
          values.push(post.title);
          break;
        case 'likes_count':
          values.push(post.likes_count);
          break;
        case 'comments_count':
          values.push(post.comments_count);
          break;
        case 'created_at':
          values.push(post.created_at.slice(0, 10).replace(/-/g, ''));
          break;
        default:
          values.push('');
      }
    });
    return { values: values };
  });

  return {
    schema: dataSchema,
    rows: data
  };
}
```

基本的にはこの関数でコネクタの大部分の処理を行うことになります。今回は投稿データと いいね データを分けて取得するため、便宜上関数を分けています（いいね に関する関数はページが長くなるので割愛させていただきます）。

Data Studioはデフォルトでキャッシュする仕組みを持っており、レポートを表示するたびに毎回```getData```を呼び出すわけではなく、極力キャッシュからデータを取得しようと動作します（レポートの左下にある稲妻アイコンがキャッシュから取得していることを表す）。

![キャッシュアイコン](https://qiita-image-store.s3.amazonaws.com/0/184074/413bcfc3-3218-504d-b017-a9c3a2c5ad4c.gif)

キャッシュには以下の2種類があり、キャッシュにヒットしない場合に```getData```が呼び出されます（キャッシュは定期的に自動更新される）。

- **クエリ キャッシュ**：レポートのコンポーネントによって発行されたクエリ（データのリクエスト）を記憶し、レポートを表示するユーザーが過去のクエリと同一のクエリをリクエストした場合、キャッシュからデータを取得する。 [^4]
- **プリフェッチ キャッシュ**：レポートのディメンション、指標、フィルタ、期間のプロパティなどを分析することで、コンポーネントがリクエストするデータを予測し、使用できるデータを可能な限り保存する。クエリ キャッシュにヒットしない場合に、さらにこのキャッシュを使ってデータの取得を試みる。

レポート編集権限者は、ページ上部の「データを更新」ボタンを使っていつでもキャッシュを更新できます。また、キャッシュを無効にしたい場合はメニューの「ファイル」⇒「レポート設定」から「キャッシュを有効化」のチェックを外してください。

![データを更新](https://qiita-image-store.s3.amazonaws.com/0/184074/94c6b194-ab91-08ff-d410-753394b12ebc.gif)

:closed_lock_with_key: OAuthクライアントを実装する

OAuth関連の関数を実装する前にOAuthクライアントの実装が必要ですが、OAuthライブラリが必要な処理を全て行ってくれるためパラメータを設定するだけで大丈夫です。

```javascript
connector.getOAuthService = function() {
  // スクリプトのプロパティに設定したClient IDとClient Secretを取得
  var scriptProps = PropertiesService.getScriptProperties();
  var clientId = scriptProps.getProperty('OAUTH_CLIENT_ID');
  var clientSecret = scriptProps.getProperty('OAUTH_CLIENT_SECRET');
  
  return OAuth2.createService('qiita')
    .setAuthorizationBaseUrl('https://qiita.com/api/v2/oauth/authorize')
    .setTokenUrl('https://qiita.com/api/v2/access_tokens')
    .setClientId(clientId)
    .setClientSecret(clientSecret)
    .setTokenHeaders({
      'Content-Type': 'application/json'
    })
    .setPropertyStore(PropertiesService.getUserProperties()) // アクセストークンの保存先
    .setScope('read_qiita')
    .setCallbackFunction('authCallback')
    .setTokenPayloadHandler(connector.tokenHandler);
}

// アクセストークンを要求する際のペイロードをJSON文字列に変換
connector.tokenHandler = function(payload) {
  return JSON.stringify(payload);
}
```

Qiita APIはデータの送受信にJSONしか受け付けないため```setTokenHeaders```と```setTokenPayloadHandler```でJSONに関する処理を施しています（ライブラリのデフォルトが```application/x-www-form-urlencoded```のため）。

:five: **isAuthValid**

```javascript
function isAuthValid() {
  var service = connector.getOAuthService();
  if (service == null) {
    return false;
  }
  return service.hasAccess();
}
```

:six: **get3PAuthorizationUrls**

```javascript
function get3PAuthorizationUrls() {
  var service = connector.getOAuthService();
  if (service == null) {
    return '';
  }
  return service.getAuthorizationUrl();
}
```

:seven: **authCallback**

```javascript
function authCallback(request) {
  var authorized = connector.getOAuthService().handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('認証に成功しました。');
  } else {
    return HtmlService.createHtmlOutput('認証が拒否されました。');
  }
}
```

:eight: **resetAuth**

```javascript
function resetAuth() {
  var service = connector.getOAuthService();
  service.reset();
} 
```

以上で実装は終了です。

###コネクタのテスト
では、実際に作ったコネクタを使ってみたいと思います。コネクタを利用するためにはDeployment IDが必要になります。Google Apps Scriptのメニューの「公開」⇒「マニフェストから配置」⇒「Get ID」でDeployment IDを取得してください。

![Deployment ID](https://qiita-image-store.s3.amazonaws.com/0/184074/ac97bacd-40bd-6118-4018-8e09cec1f940.gif)

Deployment IDを取得したら、Data Studioの画面で左メニューの「ユーザー設定」⇒「開発者向けオプション」から、コネクタのオプションを有効にしてください。

![開発者向けオプション](https://qiita-image-store.s3.amazonaws.com/0/184074/0122528f-8272-81d0-ee13-1ee9bdeccb35.gif)

これでコネクタを追加できるようになります。左メニューの「データソース」からデータソースの新規作成を行い（画面右下の:heavy_plus_sign:ボタン）、コネクタの一覧が表示されているエリアの一番下の:wrench:デベロッパーをクリックしてください。そうするとコネクタを追加する画面が開きますので先ほど取得したDeployment IDを入力して検証ボタンをクリックしてください。

マニフェストが有効な場合、コネクタが表示されますので「コネクタを追加」をクリックしてください。

![コネクタの追加](https://qiita-image-store.s3.amazonaws.com/0/184074/e4dc4aff-cfb6-9b16-8215-61cbf9073f29.gif)

コミュニティ コネクタを利用するためにはGoogleアカウントでの承認が必要になりますので、承認ボタンをクリックしてください。承認が完了すると今度はQiitaと接続するための承認ボタンが表示されますので、承認ボタンをクリックしてOAuth認証を行ってください。

![OAuth認証](https://qiita-image-store.s3.amazonaws.com/0/184074/aa0d9269-24d6-a3d4-cb6b-5c6de786b40f.gif)

以上で準備は完了です。あとは投稿データか いいね データのどちらかを選択して接続すればレポートを作成できます。もし両方のデータを使いたい場合は、投稿データ用のデータソースと いいね データ用のデータソースの2つを作って、レポート内でそれぞれのデータソースを使えばOKです。

実際に表やグラフを表示してみると下のような感じになります。データがしょぼくてお恥ずかしいのですが。。。

![report.gif](https://qiita-image-store.s3.amazonaws.com/0/184074/0f433005-37c6-9214-94d7-7cbaf6091fe3.gif)

##まとめ
最近だと[Metabase](https://www.metabase.com/)に関する記事がトレンドに入ったりデータ可視化ツールが注目を集めているようです。Google Data Studioは痒い所に手が届かなかったり、まだまだこれからのツールかなという感じですが、継続的に改良が加えられて使い勝手も良くなっています。なにより、手軽に始められ配布しやすいという点は大きなメリットかなと思います。

既にいろいろなコネクタが公開されていますが、有料だったりするので欲しいものがなければ自作してみては如何でしょうか？
https://developers.google.com/datastudio/connector/gallery/
https://developers.google.com/datastudio/connector/data-sources

今回作ったQiitaコネクタのソースは下記リンクからご参照いただけます。
https://github.com/prograti/datastudio-community-connectors/tree/master/qiita



[^1]: 8つ以外に任意実装の関数が1つあります。詳しくは[APIリファレンス](https://developers.google.com/datastudio/connector/reference)をご確認ください。
[^2]: 2018年1月時点ではOAuthのみですが、今後他の方式も追加する予定とのことです。
[^3]: 今後Data Studioの画面上からリセットを実行可能にする予定とのことです。
[^4]: 試した感じだと、表の列の昇順・降順を変更したり、結果をフィルタリングした場合は、既に取得したデータセットに対して処理を行うのではなく、新たなクエリを発行しているようです。