ステータス | 有用度 | 日時 | 備考 |
---|---|---|---|
公開 | ★★★ | 2022/08/31 | |
更新 | ★★★ | 2022/11/20 | Looker Studio に対応 |
こんにちは 田村 です。
Google Looker Studio (旧データポータル) は、Google が提供するサービスを中心に様々なデータソースを利用して、ダッシュボードの構築ができます。
データソースが用意されていないものでも自分でコネクタを作成して、 Google Looker Studio にデータを取り込めます。コネクタは Google Apps Script で記述し、 Web API、 CSV 、 JSON 、 XML 、Apps Script Services 、 JDBC API などからデータを取得できます。
今回は、 REST API からデータを取り込むコネクタを作りたいと思います。 REST API のサービスとして、 Redmine をベースに説明していきます。 基本的にどんな REST API でも構築の仕方は同様にできると思いますので、適宜ご自身のご利用したいサービスに置き換えてお読みください。
はじめに Looker Studio の完成形のイメージをお見せすると、このような形になります。
WESEEK の開発手法
弊社では、アジャイル開発の管理ツールとして、この Redmine を使用しています。 sprint ごとにどれくらい速度(ベロシティ)が出せているかや、プロジェクトによってはある期間においてどれくらいストーリーポイントを消化しているかは気になるところです。 Redmine の REST API を利用して実績情報を取得し、 Looker Studio で閲覧できるようになったら便利だなというのが、このコネクター作成のきっかけです。
Redmine の API を使う
API の有効化
管理
> 認証
> RESTによるWebサービスを有効にする
にチェックを入れて、 Redmine で REST API を利用できるようにします。
API アクセスキーの確認
Redmine REST API を呼び出すときは、 API アクセスキーが必要です。 個人設定
> APIアクセスキー
から確認しておきます。
チケット一覧の取得
試しに REST API を利用して、チケット一覧を取得してみます。
HTTP Header に X-Redmine-API-Key
を追加し、 API アクセスキーをセットします。
curl で呼び出してみると、下記のようにチケット一覧が取得できました。
$ curl -s --request GET 'https://[REDMINE HOSTNAME]/issues.json?project_id=1&limit=2' --header 'X-Redmine-API-Key: [REDMINE API TOKEN]' | jq .
{
"issues": [
{
"id": 3503,
"project": {
"id": 1,
"name": "[FILTERED]"
},
"tracker": {
"id": 5,
"name": "タスク"
},
"status": {
"id": 1,
"name": "新規"
},
"priority": {
"id": 2,
"name": "通常"
},
"author": {
"id": 141,
"name": "[FILTERED]"
},
"fixed_version": {
"id": 943,
"name": "sprint-25"
},
"parent": {
"id": 103421
},
"subject": "動作確認",
"description": "",
"start_date": "2022-05-19",
"done_ratio": 0,
"created_on": "2022-08-29T07:36:06Z",
"updated_on": "2022-08-29T07:36:06Z"
},
{
"id": 3502,
"project": {
"id": 1,
"name": "[FILTERED]"
},
"tracker": {
"id": 5,
"name": "タスク"
},
"status": {
"id": 1,
"name": "新規"
},
"priority": {
"id": 2,
"name": "通常"
},
"author": {
"id": 141,
"name": "[FILTERED]"
},
"fixed_version": {
"id": 943,
"name": "sprint-25"
},
"parent": {
"id": 103421
},
"subject": "PR & Merge #3240",
"description": "PR\r\nタスク\r\n#3240",
"start_date": "2022-05-19",
"done_ratio": 0,
"created_on": "2022-08-29T07:35:58Z",
"updated_on": "2022-08-29T07:35:58Z"
}
],
"total_count": 82,
"offset": 0,
"limit": 2
}
Redmine REST API の詳細は こちら を参照してください。
Looker Studio コミュニティ コネクタを作る
Google Looker Studio の Codelab を参考に構築していきます。
Apps Script のプロジェクトを作り、下記の4つの関数を定義して、コネクタを作成していきます。
getAuthType()
getConfig()
getSchema()
getData()
Apps Script プロジェクトを作る
Google Apps Script にアクセスします。
新しいプロジェクト
をクリックします。
最上部 無題のプロジェクト名
をクリックして、このプロジェクトに名前をつけます。 今回は RedmineDataStudioConnector
としました。
これから、 コード.gs
の中に機能を実装していきます。
getAuthType() の定義
コードを先に示します。
var cc = DataStudioApp.createCommunityConnector();
function getAuthType() {
var AuthTypes = cc.AuthType;
return cc
.newAuthTypeResponse()
.setAuthType(AuthTypes.NONE)
.build();
}
getAuthType()
は、 Looker Studio がコネクタが使用する認証方法を知る必要があるときに呼び出されます。
認証方法は下記があります。
列挙値 | 説明 |
---|---|
NONE | コネクタに認証が必要ないことを示します。 |
OAUTH2 | コネクタが認証に OAuth 2.0 を使用することを示します。 |
KEY | コネクタが認証に API キーを使用することを示します。 |
USER_PASS | コネクタが認証にユーザー名とパスワードを使用することを示します。 |
USER_TOKEN | コネクタが認証にユーザー名とトークンを使用することを示します。 |
作成したコネクタを一般にも公開する場合には、適切な認証方法を選択することが必要です。今回は Codelab でも説明している NONE
を利用します。
認証方法の詳細は こちら を参照してください。
getConfig() の定義
Looker Studio で作成したコネクタをデータソースとして追加する際に、コネクタに任意のパラメータを渡すようにできます。
このようなものです。
コードを下記に示します。 getAuthType()
の後に続けて記載してください。
function getConfig(request) {
var config = cc.getConfig();
config.newInfo()
.setId('instructions')
.setText('Enter the Redmine project ID to get a list of Issues for the project.');
config.newTextInput()
.setId('project')
.setName('Enter a project id');
config.setDateRangeRequired(true);
return config.build();
}
config.newInfo()
では、ユーザーに指示や情報を提供するためのテキストを定義しています。
config.newTextInput()
では、 1 行のテキストボックスを定義しています。今回は、 Redmine の プロジェクト ID の入力を促しています。
使用できる ConfigType
の一覧は こちら を参照してください。
getSchema() の定義
getSchema()
は、 Looker Studio にデータを読み込む際のスキーマを定義します。
この関数が返した結果は、 Looker Studio のデータソースの編集で表示されるフィールド一覧に対応します。
コードを示します。
function getFields(request) {
var cc = DataStudioApp.createCommunityConnector();
var fields = cc.getFields();
var types = cc.FieldType;
var aggregations = cc.AggregationType;
// Looker Studio 上で `id` と一意に設定し、表示名を `ID` に、そのデータ型を `NUMBER` 型に設定しています
fields.newDimension()
.setId('id')
.setName('ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('projectId')
.setName('プロジェクト ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('project')
.setName('プロジェクト')
.setType(types.TEXT);
fields.newDimension()
.setId('trackerId')
.setName('トラッカー ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('tracker')
.setName('トラッカー')
.setType(types.TEXT);
fields.newDimension()
.setId('statusId')
.setName('ステータス ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('status')
.setName('ステータス')
.setType(types.TEXT);
fields.newDimension()
.setId('priorityId')
.setName('優先度 ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('priority')
.setName('優先度')
.setType(types.TEXT);
fields.newDimension()
.setId('authorId')
.setName('作成者 ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('author')
.setName('作成者')
.setType(types.TEXT);
fields.newDimension()
.setId('assignedToId')
.setName('担当者 ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('assignedTo')
.setName('担当者')
.setType(types.TEXT);
fields.newDimension()
.setId('fixedVersionId')
.setName('対象バージョン ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('fixedVersion')
.setName('対象バージョン')
.setType(types.TEXT);
fields.newDimension()
.setId('parent')
.setName('親チケット ID')
.setType(types.NUMBER);
fields.newDimension()
.setId('subject')
.setName('題名')
.setType(types.TEXT);
fields.newDimension()
.setId('description')
.setName('説明')
.setType(types.TEXT);
fields.newDimension()
.setId('startDate')
.setName('開始日')
.setType(types.YEAR_MONTH_DAY);
fields.newDimension()
.setId('createdOn')
.setName('作成日')
.setType(types.YEAR_MONTH_DAY_SECOND);
fields.newMetric()
.setId('doneRatio')
.setName('進捗率')
.setType(types.PERCENT)
.setAggregation(aggregations.SUM);
fields.newDimension()
.setId('updatedOn')
.setName('更新日')
.setType(types.YEAR_MONTH_DAY_SECOND);
fields.newMetric()
.setId('storyPoints')
.setName('ストーリーポイント')
.setType(types.NUMBER)
.setAggregation(aggregations.SUM);
return fields;
}
function getSchema(request) {
var fields = getFields(request).build();
return { schema: fields };
}
チケット一覧の取得
で取得した内容をもとに、スキーマを定義しています。
Looker Studio 上で、ディメンションとして定義したい場合は newDimension()
を、 指標として定義したい場合は newMetric()
を定義します。setId()
は、一意のキーを定義します。これは後述する getData()
で、 REST API から取得した結果をパースするときに必要となります。getName()
はフィールド一覧に表示されるフィールド名を定義します。setType()
はフィールドのデータ型を指定します。 使用できるデータ型は こちら を参照してください。
getData() の定義
Looker Studio 上で下記のイベントが発生すると、コネクタの getData()
が呼び出されます。
- ユーザーがダッシュボードにグラフを追加するとき
- ユーザーがグラフを編集するとき
- ユーザーがダッシュボードを表示するとき
- ユーザーが関連付けられたフィルタまたはデータ コントロールを編集するとき
- Looker Studio がデータサンプルを必要としたとき
getData()
はその名前の通り、対象とするデータベース(今回は Redmine REST API)から実際にデータを取得し、定義されたスキーマにパースして Looker Studio に返す処理を行います。
先にコードを示します。
function getData(request) {
// 後述の「要求されたフィールドのスキーマを作成する」で詳しく説明します
var requestedFieldIds = request.fields.map(function(field) {
return field.name;
});
var requestedFields = getFields().forIds(requestedFieldIds);
// 後述の 「API からデータを取得して解析する」で詳しく説明します
// Fetch and parse data from API
var url = [
'https://[REDMINE HOSTNAME]/issues.json?',
'project_id=',
request.configParams.project,
'&limit=100'
];
var headers = {
'X-Redmine-API-Key': PropertiesService.getScriptProperties().getProperty("REDMINE_API_KEY")
};
var options = {
"headers": headers
};
var response = UrlFetchApp.fetch(url.join(''), options);
var parsedResponse = JSON.parse(response).issues;
var rows = responseToRows(requestedFields, parsedResponse);
return {
schema: requestedFields.build(),
rows: rows
};
}
// 後述の「解析されたデータを変換し、要求されたフィールドをフィルタする」で詳しく説明します
function responseToRows(requestedFields, response) {
// Transform parsed data and filter for requested fields
return response.map(function(issue) {
var row = [];
requestedFields.asArray().forEach(function (field) {
switch (field.getId()) {
case 'id':
return row.push(issue.id);
case 'projectId':
return row.push(issue.project.id);
case 'project':
return row.push(issue.project.name);
case 'trackerId':
return row.push(issue.tracker.id);
case 'tracker':
return row.push(issue.tracker.name);
case 'statusId':
return row.push(issue.status.id);
case 'status':
return row.push(issue.status.name);
case 'priorityId':
return row.push(issue.priority.id);
case 'priority':
return row.push(issue.priority.name);
case 'authorId':
return row.push(issue.author.id);
case 'author':
return row.push(issue.author.name);
case 'assignedToId':
// レスポンスの issue.assigned_to のキーがなければ null を、そうでなければ id を返します
return row.push(issue.assigned_to && issue.assigned_to.id);
case 'assignedTo':
return row.push(issue.assigned_to && issue.assigned_to.name);
case 'fixedVersionId':
return row.push(issue.fixed_version && issue.fixed_version.id);
case 'fixedVersion':
return row.push(issue.fixed_version && issue.fixed_version.name);
case 'parentId':
return row.push(issue.parent && issue.parent.id);
case 'subject':
return row.push(issue.subject);
case 'description':
return row.push(issue.description);
case 'startDate':
// レスポンスの issue.start_date のキーがなければ null を、そうでなければ 2022-08-29 という形式の日付を 20220829 に変換して値を返します
return row.push(issue.start_date && issue.start_date.replace(/-/g, ''));
case 'doneRatio':
return row.push(issue.done_ratio/100);
case 'createdOn':
return row.push(issue.created_on && issue.created_on.replace(/-|:|T|Z/g, ''));
case 'updatedOn':
return row.push(issue.updated_on && issue.updated_on.replace(/-|:|T|Z/g, ''));
case 'storyPoints':
return row.push(issue.story_points);
default:
return row.push('');
}
});
return { values: row };
});
}
request オブジェクト
getData()
で参照されている request
オブジェクトについて説明します。
Looker Studio からコネクタの getData()
が呼び出されると、この request
オブジェクトが渡されます。
request
オブジェクトは下記のような構造になっています。
{
configParams: object,
scriptParams: object,
dateRange: {
startDate: string,
endDate: string
},
fields: [
{
name: Field.name
}
]
}
例えば、 configParams
は、 getConfig() の定義
で説明したフィールドに入力した内容が格納されます。このような構造です。
{
configParams: {
project: '1'
},
...
}
コネクタの getData()
を実装する際は、 request
オブジェクトの中身を適切に処理してデータを返します。 request
オブジェクトの詳細は こちら を参照してください。
要求されたフィールドのスキーマを作成する
request
オブジェクトの fields
から、 Looker Studio からリクエストされたフィールドのスキーマを作成します。コネクタはAPIに問い合わせたデータのうち、これらのフィールドにフィルタして、結果を Looker Studio に返すようにします。
var requestedFieldIds = request.fields.map(function(field) {
return field.name;
});
var requestedFields = getFields().forIds(requestedFieldIds);
API からデータを取得して解析する
実際に API に問い合わせてデータを取得します。 チケット一覧の取得
で Redmine REST API を呼び出したのと同様のエンドポイントを url
に定義します。
project_id
には、 request
オブジェクトの中にある configParams.project
を渡しています。これは Looker Studio のデータソースの設定時に入力されたプロジェクト ID の値です。
Redmine REST API は、 1 回のリクエストで 100 件までのデータしか取得できません。 100 件を超えるデータに対応するためには、ページを再帰的に追いかける実装が必要です。 今回は、シンプルにするためにこの実装を割愛しています。
// Fetch and parse data from API
var url = [
'https://[REDMINE HOSTNAME]/issues.json?',
'project_id=',
request.configParams.project,
'&limit=100'
];
Redmine REST API を呼び出す際には、 X-Redmine-API-Key
ヘッダーの指定が必要でした。これは秘匿情報なため、コード上に直接記載するのは望ましくありません。 Google Apps Script のプロパティ サービスを使用して、秘匿情報を管理します。
プロジェクトの設定
を開き、 スクリプト プロパティを追加
をクリックします。
プロパティ
には REDMINE_API_KEY
を、 値
には Redmine API アクセスキー を入力し、 スクリプト プロパティ を保存
をクリックします。
下記のように、 PropertiesService.getScriptProperties().getProperty()
を使用して、定義したプロパティを参照できます。
var headers = {
'X-Redmine-API-Key': PropertiesService.getScriptProperties().getProperty("REDMINE_API_KEY")
};
var options = {
"headers": headers
};
var response = UrlFetchApp.fetch(url.join(''), options);
var parsedResponse = JSON.parse(response).issues;
解析されたデータを変換し、要求されたフィールドをフィルタする
switch case
文を使い、リクエストされたフィールドの結果を返すようにパースしていきます。 case
に記載しているキーは、 getSchema()
で setId()
した値を指定します。
function responseToRows(requestedFields, response) {
// Transform parsed data and filter for requested fields
return response.map(function(issue) {
var row = [];
requestedFields.asArray().forEach(function (field) {
switch (field.getId()) {
case 'id':
return row.push(issue.id);
case 'projectId':
return row.push(issue.project.id);
...
default:
return row.push('');
}
});
return { values: row };
});
}
REST API の結果にキーが含まれない場合がある場合は、下記のようにして null safe にします。
case 'assignedToId':
return row.push(issue.assigned_to && issue.assigned_to.id);
REST API の結果を Looker Studio のデータ型に変換することが必要になる場合があります。
createdOn
は、 2022-08-29T07:35:58Z
のような形で REST API から返されますが、 Looker Studio 上では 20220829073558
のような形式のデータが必要です。
下記のようにして、データの変換を行います。
case 'createdOn':
return row.push(issue.created_on && issue.created_on.replace(/-|:|T|Z/g, ''));
マニフェストの作成
マニフェストを作成して、 Looker Studio からコネクタを追加する際のコネクタの情報を定義します。
プロジェクトの設定
を開き、 「appsscript.json」マニフェスト ファイルをエディタで表示する
をクリックします。
ファイルに application.json
が表示されるようになりました。
application.json
を下記のように書き変えます。
{
"timeZone": "Asia/Tokyo",
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"dataStudio": {
"name": "Redmine",
"logoUrl": "https://www.redmine.org/attachments/download/3462/redmine_fluid_icon.png",
"company": "WESEEK, Inc.",
"companyUrl": "https://weseek.co.jp/",
"addonUrl": "https://weseek.co.jp/",
"supportUrl": "https://weseek.co.jp/",
"description": "Get a list of Issues in a Redmine project.",
"sources": ["redmine"]
}
}
dataStudio
object について説明します。これらは、 Looker Studio でデータソースを追加する際に表示される情報です。 name
は、コネクタの名前です。 logoUrl
はアイコン画像で、ここでは Redmine のロゴを設定しました。 company
companyUrl
はこのコネクタを製作した組織の情報を入力します。 addonUrl
は、このコネクタの専用の詳細ページの URL を記載します。今回は作成していないため、 companyUrl
と同じにしています。 supportUrl
は、このコネクタのサポートページの URL を記載します。今回は作成していないため、 companyUrl
と同じにしています。 description
には、コネクタの説明を記載します。 sources
には、このコネクタが利用できるデータソースのリストを列挙します。今回は、 redmine
とのみ記載しています。
マニフェストの詳細は こちら を参照してください。
コネクタをデプロイする(テスト)
デプロイ
> デプロイをテスト
をクリックします。
ヘッドデプロイ ID
をコピーしておきます。
Looker Studio からコネクタを利用する
作成したコネクタに接続する
Looker Studio を開き、 作成
> レポート
をクリックして、新しいレポートを作成します。
データのレポートへの追加
で、 独自に作成
をクリックします。
Deployment ID
に、先ほどコピーした Deployment ID
を入力し、検証をクリックします。
下部にこのように表示されたら、 Redmine の枠をクリックします。
承認をクリックします。
Google hasn’t verified this app
と表示されたら、 Advanced
をクリックし、 Go to RedmineDataStudioConnector (unsafe)
をクリックします。その後、 Allow
をクリックします。
Looker Studio に表示したい Redmine プロジェクトの ID を入力し、追加をクリックします。
グラフを追加してデータを表示してみる
ここでは、例として Redmine から取得したデータを元に表を描画してみます。
グラフを追加
> 表
をクリックし、任意の場所に配置します。
それぞれ下記のように選択します。
- データソース
- Redmine
- ディメンション
- 題名
- 対象バージョン
- ステータス
- 作成日
- 指標
- SUM: ストーリーポイント
このような表が構築できます。
その他に棒グラフや、ウォーターフォールグラフなどを使用して、このようなダッシュボードを構築できます。
最後に
今回作成したコネクタは、テスト(開発)用としてデプロイを行いました。 作成したコネクタを一般に広く公開する場合は、Publish a Community Connector を実施します。