AsanaのタスクをGoogleスプレッドシートに連携して不具合管理をしてみた:その2

みなさんこんにちは!HRソリューション本部でQAエンジニアをしている筑後です。
私は2021年9月にマネーフォワードに入社し、品質の可視化に取り組んでいます。

前回は、チームごとに異なるツールを使って不具合の管理をしている現状と、AsanaのAPIリクエストが想定通りになっているかのテスト方法を書きました。

今回はいよいよGoogleスプレッドシートに出力します。

目次

  • まずはGAS全体を大公開!
  • GASの中身の説明
    • 定数を用意する
    • 出力先のGoogleスプレッドシートを取得する
    • URLを取得する
    • タスクを取得する
    • Googleスプレッドシートに出力できた!
  • トリガーを追加する
  • さいごに

まずはGAS全体を大公開!

const ENV = {
  // ASANA_APIのAccess Token
  ASANA_TOKEN: "***************************************************",
  // チームのgid
  ASANA_TEAM_GID: "****************",
  // タグのgid
  ASANA_TAG_GID: "****************",
  // このスクリプトでタスクを書き込むスプレッドシートのid
  SPREADSHEET_ID: "********************************************",
  // このスクリプトでタスクを書き込むシートの名前
  TARGET_SHEET_NAME: "****"
}

function main() {
  const sheet = SpreadsheetApp.openById(ENV.SPREADSHEET_ID).getSheetByName(ENV.TARGET_SHEET_NAME)

  const options = {
    method: "get",
    contentType: "application/json",
    headers: { 
      Authorization: "Bearer " + ENV.ASANA_TOKEN
    }
  }
  const tasks = getTasks(options)
  const users = getTeamUsers(options)
  const projects = getTeamProjects(options)

  tasks.forEach((task) => {
    let taskRowNumber = findSavedTaskRow(task.gid)
    taskRowNumber = taskRowNumber !== -1 ? taskRowNumber : getLastRow() + 1

    /**
     * このスクリプトで書き込んでいる列
     * A: gid
     * B: url
     * C: 作成日時
     * D: 完了日時
     * E: タスク名
     * F: 説明
     * G: 担当者
     * H: プロジェクト
     */
    writeSpreadSheet('A', taskRowNumber, task.gid)
    writeSpreadSheet('B', taskRowNumber, getTaskUrl(task))
    writeSpreadSheet('C', taskRowNumber, task.created_at.substring(0,10))
    writeSpreadSheet('D', taskRowNumber, task.completed_at ? task.completed_at.substring(0,10) : "")
    writeSpreadSheet('E', taskRowNumber, task.name)
    writeSpreadSheet('F', taskRowNumber, task.notes)
    writeSpreadSheet('G', taskRowNumber, getUserName(task, users))
    writeSpreadSheet('H', taskRowNumber, getProjectsName(task, projects))
  })

  /********** 以下 function **********/

  // タグづけされたタスクを全て取得する
  // @params [hash] options リクエストオプション
  // @return [array[hash,hash,hash...]] タスク
  function getTasks(options) {
    const getTasksPath = "https://app.asana.com/api/1.0/tags/" + ENV.ASANA_TAG_GID + "/tasks?opt_fields=assignee,created_at,completed_at,name,notes,projects&limit=100"
    return getAllData(getTasksPath, options)
  }

  // チームに属するユーザーを全て取得する、タスクの担当者名を取得するために使う
  // @params [hash] options リクエストオプション
  // @return [array[hash,hash,hash...]] ユーザー
  function getTeamUsers(options) {
    const getTeamUsersPath = "https://app.asana.com/api/1.0/teams/" + ENV.ASANA_TEAM_GID + "/users?opt_fields=gid,name&limit=100"
    return getAllData(getTeamUsersPath, options)
  }

  // チームに属するプロジェクトを全て取得する、タスクのプロジェクト名を取得するために使う
  // @params [hash] options リクエストオプション
  // @return [array[hash,hash,hash...]] プロジェクト
  function getTeamProjects(options) {
    const getTeamProjectsPath = "https://app.asana.com/api/1.0/teams/" + ENV.ASANA_TEAM_GID + "/projects?opt_fields=gid,name&limit=100"
    return getAllData(getTeamProjectsPath, options)
  }

  // 一覧取得APIでページングしつつ全てのデータを取得する
  // @params [string] path APIのpath
  // @params [hash] options リクエストオプション
  // @return [array[hash,hash,hash...]] データ
  function getAllData(path, options) {
    let response = JSON.parse(UrlFetchApp.fetch(path, options))
    let data = response.data
    while (response.next_page) {
      response = JSON.parse(UrlFetchApp.fetch(response.next_page.uri, options))
      data = data.concat(response.data)
    }
    return data
  }

  // 渡されたtaskのgidと一致する行番号(または `-1`) を返す
  // @params [string] gid taskのgid
  // @return [number] gidがあればその行番号、なければ `-1`
  function findSavedTaskRow(gid) {
    const gidCells = sheet.getRange(2, 1, getLastRow()).getValues().flat().map(String)
    const result = gidCells.indexOf(String(gid))
    return result === -1 ? result : result + 2
  }

  // 値が入っている最後のセルの行数を返す
  // @return [number]
  function getLastRow() {
    return sheet.getDataRange().getLastRow()
  }

  // タスクのURLを取得する
  // @params [hash] task タスク
  // @return [string] タスクのURL
  function getTaskUrl(task) {
    return "https://app.asana.com/0/" + ENV.ASANA_TAG_GID + "/" + task.gid + "/f"
  }

  // タスクの担当者名を取得する
  // @params [hash] task タスク
  // @params [array[hash,hash,hash...]] users ユーザーの配列
  // @return [string] ユーザー名、ユーザーが見つからなければ空文
  function getUserName(task, users) {
    if (task.assignee) {
      let user = users.find((user) => user.gid === task.assignee.gid)
      return user ? user.name : ""
    } else {
      return ""
    }
  }

  // タスクのプロジェクト名を取得する
  // @params [hash] task タスク
  // @params [array[hash,hash,hash...]] projects プロジェクトの配列
  // @return [string] プロジェクト名、プロジェクトが見つからなければ空文
  function getProjectsName(task, projects) {
    if (task.projects) {
      return task.projects.map(taskProject => {
        let project = projects.find((p) => p.gid === taskProject.gid)
        return project ? project.name : ""
      }).join(", ")
    } else {
      return ""
    }
  }

  // 指定した列と行にテキストを入力する。既に値がある場合は上書きする。
  // @params [string] column 列(A,B,C...で指定する)
  // @params [number] line 行
  // @params [string] text 書き込むテキスト
  function writeSpreadSheet(column, line, text) {
    sheet.getRange(column + line).setValue(text)
  }
}

GASの中身の説明

定数を用意する

const ENV = {
  // ASANA_APIのAccess Token
  ASANA_TOKEN: "***************************************************",
  // チームのgid
  ASANA_TEAM_GID: "****************",
  // タグのgid
  ASANA_TAG_GID: "****************",
  // このスクリプトでタスクを書き込むスプレッドシートのid
  SPREADSHEET_ID: "********************************************",
  // このスクリプトでタスクを書き込むシートの名前
  TARGET_SHEET_NAME: "****"
}

GASで使用する定数を用意します。

  • ASANA_TOKEN: 前回取得したAccess Token
  • ASANA_TEAM_GID: Asanaのチームのgid
  • ASANA_TAG_GID: Asanaのタグのgid
  • SPREADSHEET_ID: このスクリプトでタスクを書き込むスプレッドシートのid
  • TARGET_SHEET_NAME: このスクリプトでタスクを書き込むシートの名前

Asanaのチームのgidは、チームを選択した時に表示される画面のURLの、この部分です。

Asanaのタグのgidは、タグをクリックすると表示される、タグのタスク一覧画面のURLの、この部分です。

出力先のGoogleスプレッドシートを取得する

出力先のスプレッドシートを取得します。

  const sheet = SpreadsheetApp.openById(ENV.SPREADSHEET_ID).getSheetByName(ENV.TARGET_SHEET_NAME)

リクエストオプションを作成します。
Authorizationに、前回取得したAccess Tokenを入れます。

  const options = {
    method: "get",
    contentType: "application/json",
    headers: { 
      Authorization: "Bearer " + ENV.ASANA_TOKEN
    }
  }

リクエストオプションを使って、タスク・ユーザー・プロジェクトの一覧を取得します。
ユーザーとプロジェクトの一覧は、それぞれの名前をスプレッドシートに表示させるために使用しています。

  const tasks = getTagTasks(options)
  const users = getTeamUsers(options)
  const projects = getTeamProjects(options)

取得したタスク一覧をスプレッドシートに出力します。
APIで取得したタスクがすでにスプレッドシートに存在する場合はその行に上書き、存在しない場合は一番下の行に追加しています。

タスクがすでに出力されているかどうかは、A列のタスクのgidで確認しています。

URLを取得する

URLはgetTaskUrlでタグのgidとタスクのgidを組み合わせて作成しています。
タスク一覧のAPIでは、ユーザーとプロジェクトのgidしか取得できないため、別途取得したユーザーとプロジェクトの一覧とgidで突合し、名前を取得しています。

  tasks.forEach((task) => {
    let taskRowNumber = findSavedTaskRow(task.gid)
    taskRowNumber = taskRowNumber !== -1 ? taskRowNumber : getLastRow() + 1

    /**
     * このスクリプトで書き込んでいる列
     * A: gid
     * B: url
     * C: 作成日時
     * D: 完了日時
     * E: タスク名
     * F: 説明
     * G: 担当者
     * H: プロジェクト
     */
    writeSpreadSheet('A', taskRowNumber, task.gid)
    writeSpreadSheet('B', taskRowNumber, getTaskUrl(task))
    writeSpreadSheet('C', taskRowNumber, task.created_at.substring(0,10))
    writeSpreadSheet('D', taskRowNumber, task.completed_at ? task.completed_at.substring(0,10) : "")
    writeSpreadSheet('E', taskRowNumber, task.name)
    writeSpreadSheet('F', taskRowNumber, task.notes)
    writeSpreadSheet('G', taskRowNumber, getUserName(task, users))
    writeSpreadSheet('H', taskRowNumber, getProjectsName(task, projects))
  })

タスクを取得する

タグづけされたタスクを全て取得します。
getTasksPathを変更すれば、全てのタスク一覧や、プロジェクトに紐づくタスク一覧を取得することもできます。
また、opt_fieldsに取得したい項目を追加することもできます。

タスクは一度のAPIリクエストで100件まで取得できます。
100件以降は再度APIリクエストを送る必要があり、getAllDataで処理しています。

ユーザー・プロジェクト一覧も同様に上限が100件のため、getAllDataで処理してます。

  // タグづけされたタスクを全て取得する
  // @params [hash] options リクエストオプション
  // @return [array[hash,hash,hash...]] タスク
  function getTasks(options) {
    const getTasksPath = "https://app.asana.com/api/1.0/tags/" + ENV.ASANA_TAG_GID + "/tasks?opt_fields=assignee,created_at,completed_at,name,notes,projects&limit=100"
    return getAllData(getTasksPath, options)
  }

  // 複数データのAPIでページングしつつ全てのデータを取得する
  // @params [string] path APIのpath
  // @params [hash] options リクエストオプション
  // @return [array[hash,hash,hash...]] データ
  function getAllData(path, options) {
    let response = JSON.parse(UrlFetchApp.fetch(path, options))
    let data = response.data
    while (response.next_page) {
      response = JSON.parse(UrlFetchApp.fetch(response.next_page.uri, options))
      data = data.concat(response.data)
    }
    return data
  }

Googleスプレッドシートに出力できた!

無事、Googleスプレッドシートに出力できました!
『AsanaのタスクをGoogleスプレッドシートに連携して不具合管理をしてみた:その1』で指定したフィールド名に従って、「gid」「URL」「作成日時」「完了日時」「タスク名」「説明」「担当者」「プロジェクト」が取得できています。

また、Asanaで管理している項目以外にGoogleスプレッドシート上でいくつかの項目を追加しています。つまり、開発チームが管理したい項目はAsana上に、横断QAチームで管理したい項目はGoogleスプレッドシート上で追加しています。

今回紹介した方法は、Googleスプレッドシート上で追加した項目に影響なく、Asanaから取得したタスクを自動で更新・追加することができるので、同じ情報でも、開発チームとQAチームで管理したい項目が違う場合にとても便利です!

トリガーを追加する

今回は定期的にスクリプトを実行して更新したいので、自動更新スクリプトのトリガーを追加するのも忘れずに!

さいごに

これでAsanaを使って不具合を管理しているチームも、Googleスプレッドシートで不具合を可視化することができました!共通のツールで可視化することで、私たちQAのような横断組織だけではなく、リーダーやマネージャーも品質指標として確認したり、資料に取り入れたりが格段にしやすくなりました!

今回は、先にツールを使っているチームがあり、後からQAが可視化をしたいという構図でしたので、チームの運用やルールを変えずに可視化ができて良かったです。
(厳密にいうと、私だけではスクリプトがうまく書けず、ほぼほぼ全て作成してくださったエンジニアさんの工数を奪っておりますので、チームに影響は出ているとも言えるかも知れません…いつもありがとうございます!#超respect #最高!)

最後までお読みいただき、ありがとうございます。


マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【会社情報】
Wantedly
株式会社マネーフォワード
福岡開発拠点
関西開発拠点(大阪/京都)

【SNS】
マネーフォワード公式note
Twitter – 【公式】マネーフォワード
Twitter – Money Forward Developers
connpass – マネーフォワード
YouTube – Money Forward Developers

Pocket