🦔

Notion × Jira を Google Apps Script + Google Data Studio で自動同期&ダッシュボード化

に公開

前提・準備
1. Google アカウント
2. Notion の Integration(トークン取得)
3. Jira API トークン/ユーザー情報
4. Google スプレッドシート ✔️
5. Data Studio(Looker Studio) アクセス権 ✔️

  1. Notion 連携設定
    • Notion の開発者ポータルで Integration を作成
    • NOTION_TOKEN(秘密)と、対象のデータベース ID (NOTION_DATABASE_ID) を控えておく
    • 対象 DB の共有設定で Integration に読み書き権限を付与

  2. Jira 連携設定
    • Jira Cloud 管理画面で API トークンを作成
    • ベース URL(例 https://your-domain.atlassian.net)、ユーザー名(メールアドレス)、トークンを控える

  3. Google Apps Script プロジェクト作成

    1. Google ドライブで 新規 > その他 > Google Apps Script
    2. 「プロジェクトの設定」からスクリプト プロパティに以下を登録
      • NOTION_TOKEN
      • NOTION_DATABASE_ID
      • JIRA_BASE_URL
      • JIRA_USER
      • JIRA_API_TOKEN

実装ステップ

ステップ1:Notion API 取得

const NOTION_TOKEN       = PropertiesService.getScriptProperties().getProperty('NOTION_TOKEN');
const NOTION_DATABASE_ID = PropertiesService.getScriptProperties().getProperty('NOTION_DATABASE_ID');

function fetchNotionTasks(cursor = null) {
  const url = `https://api.notion.com/v1/databases/${NOTION_DATABASE_ID}/query`;
  const headers = {
    "Authorization": `Bearer ${NOTION_TOKEN}`,
    "Notion-Version": "2022-06-28",
    "Content-Type": "application/json"
  };
  const payload = { page_size: 100 };
  if (cursor) payload.start_cursor = cursor;

  const res  = UrlFetchApp.fetch(url, {
    method: 'post',
    headers,
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });
  const json = JSON.parse(res.getContentText());
  // 結果を統一フォーマットにマッピング
  const items = json.results.map(page => ({
    source    : 'notion',
    id        : page.id,
    title     : page.properties.Name.title[0]?.plain_text || '',
    status    : page.properties.Status.select?.name || '',
    assignee  : page.properties.Assignee.people[0]?.name || '',
    dueDate   : page.properties.Due.date?.start || '',
    url       : page.url
  }));
  return { items, nextCursor: json.has_more ? json.next_cursor : null };
}

ステップ2:Jira API 取得

const JIRA_BASE_URL = PropertiesService.getScriptProperties().getProperty('JIRA_BASE_URL');
const JIRA_USER     = PropertiesService.getScriptProperties().getProperty('JIRA_USER');
const JIRA_API_TOKEN= PropertiesService.getScriptProperties().getProperty('JIRA_API_TOKEN');

function fetchJiraIssues(startAt = 0) {
  const jql  = encodeURIComponent('project = ABC AND status in ("To Do","In Progress","Done")');
  const url  = `${JIRA_BASE_URL}/rest/api/2/search?jql=${jql}&startAt=${startAt}&maxResults=50`;
  const auth = Utilities.base64Encode(`${JIRA_USER}:${JIRA_API_TOKEN}`);
  const res  = UrlFetchApp.fetch(url, {
    headers: {
      Authorization: `Basic ${auth}`,
      Accept       : 'application/json'
    },
    muteHttpExceptions: true
  });
  const data = JSON.parse(res.getContentText());
  const items = data.issues.map(issue => ({
    source    : 'jira',
    id        : issue.id,
    title     : issue.fields.summary,
    status    : issue.fields.status.name,
    assignee  : issue.fields.assignee?.displayName || '',
    dueDate   : issue.fields.duedate || '',
    url       : `${JIRA_BASE_URL}/browse/${issue.key}`
  }));
  return { items, nextStart: data.startAt + data.maxResults < data.total ? data.startAt + data.maxResults : null };
}

ステップ3:Google スプレッドシートへアップサート

function upsertToSheet(allItems) {
  const ss    = SpreadsheetApp.openById('YOUR_SHEET_ID');
  const sheet = ss.getSheetByName('Tasks');
  const data  = sheet.getDataRange().getValues();

  // ヘッダー行を読む
  const headers = data.shift();
  const keyCol = headers.indexOf('Source') + 1;
  const idCol  = headers.indexOf('ID') + 1;

  allItems.forEach(item => {
    // キー「Source+ID」で既存行を検索
    const finder = sheet.createTextFinder(item.source + item.id).findNext();
    const rowIdx = finder ? finder.getRow() : sheet.getLastRow() + 1;
    const rowValues = [
      item.source + item.id,
      item.source,
      item.id,
      item.title,
      item.status,
      item.assignee,
      item.dueDate,
      item.url
    ];
    sheet.getRange(rowIdx, 1, 1, rowValues.length).setValues([rowValues]);
  });
}

ステップ4:全体をまとめて定期実行

function syncTasks() {
  // 1. Notion 全件取得
  let notionAll = [], cursor = null;
  do {
    const res = fetchNotionTasks(cursor);
    notionAll = notionAll.concat(res.items);
    cursor = res.nextCursor;
  } while (cursor);

  // 2. Jira 全件取得
  let jiraAll = [], startAt = 0;
  do {
    const res = fetchJiraIssues(startAt);
    jiraAll = jiraAll.concat(res.items);
    startAt = res.nextStart;
  } while (startAt);

  // 3. シートにアップサート
  upsertToSheet(notionAll.concat(jiraAll));
}
•	GAS の「トリガー」メニューから 時間主導型トリガー(例:毎時)を設定してください。

Google Data Studio ダッシュボード
1. データソース:対象スプレッドシートを追加
2. レポート作成
• ステータス別件数:棒グラフ
• 担当者別残件数:円グラフ
• 期日超過タスク:フィルタ付きテーブル
• 日次タスク消化数推移:折れ線グラフ(シートに日付列を追加推奨)
3. 自動更新
• Data Studio の「データの更新」設定で、GAS 側同期とのタイミングを調整

まとめ
• 完全カスタムなので、必要なプロパティだけを取捨選択できる
• スクリプトとシート構成を一度作れば、ほぼメンテナンスフリー
• Data Studio でビジュアルを自由に拡張可能

まずは上記コードをコピペで動かし、PoC を進めてみてください。
ご質問や改善アイデアはぜひコメントでお知らせください!

Discussion