2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASの「今日あと何分トリガーの残り起動数」がわかるChrome拡張機能を作った

2
Last updated at Posted at 2026-11-05

はじめに

Google Apps Script(GAS)でトリガーを回していると、ある日突然こんなエラーが出て処理が止まることがあります。

Service using too much computer time for one day

トリガーの合計実行時間が1日の割り当て(無料アカウントで90分、Google Workspaceで6時間)を超えたというエラーです。困るのは、今どれだけ使っているのかを確認する画面が GAS にはないことです。

そこで、Apps Script エディタの右下に「今日の使用量」を出す Chrome 拡張機能 GAS Quota Meter を作りました。

image.png

できること

  • script.google.com の右下に「⏱ Quota xx%」ボタンを表示。クリックでパネルが開く
  • トリガーの合計実行時間を、上限に対する割合のバーで表示
  • 実行回数・失敗・タイムアウト・実行中の数・最長実行時間
  • プロジェクト別の内訳と、最近の失敗(最新5件)
  • 枠が空き始める時刻(最も古いトリガー実行が24時間枠から外れる時刻)
  • メール・URL Fetch など各サービスの1日の上限値の一覧
  • 無料 / Workspace の上限切り替え、ダークモード、2分ごとの自動更新

使用率が80%を超えるとボタンがオレンジに、100%以上またはタイムアウトが発生すると赤になるので、パネルを開かなくても状況がわかります。

GAS の割り当てのおさらい

拡張機能の作りを説明する前に、前提となる仕様を2つ押さえておきます。

1. 「日付が変わったらリセット」ではない

GAS の割り当ては、暦日の0時にリセットされるのではなく、最初の実行から24時間で回復する仕組みです。そのため本拡張機能では、暦日ではなく直近24時間のローリングウィンドウで集計しています。

2. 使用量を取れるのは「実行履歴」だけ

メール送信数や URL Fetch の回数などの使用量は、Google が API で公開していません。取得できるのは Apps Script API の processes.list で返ってくる実行履歴だけです。

そこで、実行履歴から次の値を計算しています。

表示項目 計算方法
トリガーの合計実行時間 processType がトリガー系のものの duration を合計
失敗・タイムアウト processStatus が FAILED / TIMED_OUT
実行中(同時実行数) processStatus が RUNNING
最長実行時間 duration の最大値(1回あたりの上限は6分)

メールの残り送信数は、スクリプト内で MailApp.getRemainingDailyQuota() を呼ぶと確認できます。

構成

ビルドツールは使わず、素の JavaScript だけで作っています(Manifest V3)。

ファイル 役割
manifest.json 拡張機能の設定
background.js OAuth トークン取得、API 呼び出し、集計、60秒キャッシュ
quotas.js 公式ドキュメントの上限値の一覧
ui.js パネルの描画(エディタ内パネルとポップアップで共用)
content.js script.google.com にボタンとパネルを追加
popup.html / popup.js ツールバーのポップアップ

エディタ内パネルとポップアップは同じ ui.js を使い、API へのアクセスはすべて Service Worker にまとめています。

実装のポイント

manifest.json:権限は最小限に

manifest.json
{
  "manifest_version": 3,
  "permissions": ["identity", "storage"],
  "host_permissions": ["https://script.googleapis.com/*"],
  "background": { "service_worker": "background.js" },
  "content_scripts": [
    {
      "matches": ["https://script.google.com/*"],
      "js": ["quotas.js", "ui.js", "content.js"],
      "run_at": "document_idle"
    }
  ],
  "oauth2": {
    "client_id": "XXXXXXXX.apps.googleusercontent.com",
    "scopes": ["https://www.googleapis.com/auth/script.processes"]
  }
}

OAuth スコープは script.processes(実行履歴の読み取り)だけです。スクリプトの中身を読んだり書き換えたりする権限は要求しません。

content script はバンドラーを使わず、quotas.js → ui.js → content.js の順に読み込んで、グローバル変数(GQM_QUOTAS / GQM_UI)で受け渡しています。

OAuth は chrome.identity に任せる

chrome.identity.getAuthToken を使うと、トークンの取得・保存・更新を Chrome がやってくれます。

background.js
function getToken(interactive) {
  return new Promise((resolve, reject) => {
    chrome.identity.getAuthToken({ interactive }, (result) => {
      const err = chrome.runtime.lastError;
      const token = typeof result === 'string' ? result : result && result.token;
      if (err || !token) {
        const msg = (err && err.message) || 'アクセストークンを取得できませんでした';
        const badClient = /client id|oauth2|bad client/i.test(msg);
        reject(new QuotaError(
          badClient ? 'OAuth クライアントIDが正しく設定されていません' : 'Google アカウントへのログインが必要です',
          badClient ? 'SETUP' : 'AUTH',
          badClient ? 'manifest.json の oauth2.client_id を README の手順で設定してください。' : msg
        ));
      } else {
        resolve(token);
      }
    });
  });
}

getAuthToken の戻り値は Chrome のバージョンによって「文字列」か「{ token } オブジェクト」なので、両方に対応しています。

また、エラーメッセージの内容から「クライアント ID の設定ミス」なのか「単にログインしていないだけ」なのかを判別し、画面に出す案内を変えています。セットアップでつまずいたときにどこを直せばいいかがすぐわかるようにするためです。

processes.list を24時間分ページング

background.js
const WINDOW_MS = 24 * 60 * 60 * 1000;
const MAX_PAGES = 40; // 50件 x 40ページ = 最大2000件

async function fetchProcesses(token, since) {
  const all = [];
  let pageToken = '';
  for (let i = 0; i < MAX_PAGES; i++) {
    const url = new URL('https://script.googleapis.com/v1/processes');
    url.searchParams.set('pageSize', '50');
    url.searchParams.set('userProcessFilter.startTime', since.toISOString());
    if (pageToken) url.searchParams.set('pageToken', pageToken);

    const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
    if (!res.ok) throw await toApiError(res);
    const body = await res.json();
    all.push(...(body.processes || []));
    pageToken = body.nextPageToken || '';
    if (!pageToken) return { processes: all, truncated: false };
  }
  return { processes: all, truncated: true };
}

userProcessFilter.startTime に「24時間前」を指定して、nextPageToken がなくなるまで取得します。無限に取りに行かないよう、2000件で打ち切ってその旨を画面に表示しています。

processes.list はユーザー単位の実行履歴を返すので、1回呼ぶだけで自分の全プロジェクトの実行がまとめて取れます。プロジェクトごとに API を呼び分ける必要はありません。

集計:トリガー実行だけを数える

割り当ての対象は「トリガーの合計実行時間」なので、エディタから手動で実行したものなどは除外します。

background.js
const TRIGGER_TYPES = new Set(['TIME_DRIVEN', 'TRIGGER', 'SIMPLE_TRIGGER']);

const seconds = (p) => {
  if (p.duration) return parseFloat(p.duration) || 0; // "12.345s"
  if (p.processStatus === 'RUNNING' && p.startTime) {
    return Math.max(0, (now - Date.parse(p.startTime)) / 1000);
  }
  return 0;
};

duration は "12.345s" という文字列で返ってくるので、parseFloat で数値にしています。実行中のプロセスには duration がないため、「現在時刻 − 開始時刻」で経過時間を計算します。

さらに、トリガー実行のうち最も古い開始時刻に24時間を足すと、枠が空き始める時刻がわかります。上限に近づいたときに「いつから再開できるか」の目安になります。

background.js
nextReleaseAt: oldestTriggerStart === null ? null : oldestTriggerStart + WINDOW_MS,

トークン失効時は1回だけリトライ

getAuthToken は Chrome がキャッシュしたトークンを返すため、失効していることがあります。401 が返ったらキャッシュを破棄して1回だけ取り直します。

background.js
try {
  result = await fetchProcesses(token, since);
} catch (e) {
  if (e.code !== 'AUTH') throw e;
  await removeToken(token); // chrome.identity.removeCachedAuthToken
  token = await getToken(!!interactive);
  result = await fetchProcesses(token, since);
}

結果は chrome.storage.session に60秒キャッシュしています。パネルの開閉のたびに API を呼ばないようにするためです。session 領域なのでブラウザを閉じると消えます。

Shadow DOM でエディタの CSS と分離

Apps Script エディタのページにボタンを差し込むと、エディタ側の CSS が干渉して表示が崩れがちです。そこで Shadow DOM の中に描画しています。

content.js
const host = document.createElement('div');
host.style.cssText = 'position:fixed;right:20px;bottom:20px;z-index:2147483000;';
document.documentElement.appendChild(host);

const shadow = host.attachShadow({ mode: 'open' });

さらに、UI 側の CSS の先頭で all: initial を指定し、継承されるスタイルもリセットしています。

UI は「状態 → HTML 文字列」のシンプルな構成

フレームワークは使わず、状態が変わるたびに HTML 文字列を作り直して innerHTML に入れる単純な作りです。クリックは data-action 属性を使ったイベント委譲で処理しています。

ui.js
root.addEventListener('click', (e) => {
  const el = e.target.closest('[data-action]');
  if (!el) return;
  const action = el.dataset.action;
  if (action === 'refresh') load(true, false);
  if (action === 'login') load(true, true);
  // ...
});

innerHTML を使うので、プロジェクト名や関数名など API から来た文字列はすべて esc() でエスケープしています。

セットアップ手順

自分の環境で動かすには、Google Cloud で OAuth クライアント ID を作る必要があります。

  1. chrome://extensions で「デベロッパー モード」をオンにし、「パッケージ化されていない拡張機能を読み込む」でフォルダを選ぶ
  2. 表示された拡張機能 ID をメモする
  3. Google Cloud Console で次の作業をする
    • Apps Script API を有効にする
    • OAuth 同意画面を設定し、スコープ https://www.googleapis.com/auth/script.processes を追加する
    • 「OAuth クライアント ID」を作成する(種類は Chrome 拡張機能、アイテム ID は手順2の ID)
  4. manifest.json の oauth2.client_id を書き換えて、拡張機能を再読み込みする
  5. https://script.google.com/home/usersettings で「Google Apps Script API」をオンにする

拡張機能 ID はフォルダの場所で決まります。フォルダを移動すると ID が変わり、OAuth クライアントと一致しなくなるので、置き場所を決めてから読み込んでください。

ハマりどころ

Apps Script API の「有効化」は2か所ある

processes.list を呼ぶには、次の両方がオンになっている必要があります。

どちらも 403 が返ってくるので、エラーメッセージに User has not enabled が含まれるかどうかで見分け、画面に出す案内を変えています。

Edge では動かない

chrome.identity.getAuthToken は Chrome 専用の API なので、Chromium 系でも Edge では動きません。Edge に対応するなら launchWebAuthFlow で OAuth のフローを自前で書く必要があります。

会社の Workspace では管理者設定に注意

Workspace の管理者が外部アプリや API のアクセスを制限していると、ログイン画面で拒否されます。社内で使う場合は、OAuth 同意画面の種類を「内部」にして管理者に相談しましょう。

おわりに

GAS の「今日あとどれだけ使えるか」が見えるようになり、トリガーの実行頻度を調整しやすくなりました。

取得できるのは実行時間系の値だけですが、GAS で一番ぶつかりやすい上限は「トリガーの合計実行時間」なので、それだけでも十分役に立ちます。

上限値は 公式ドキュメント に基づいていますが、予告なく変わることがあるため、quotas.js を定期的に見直す予定です。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?