0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「正常終了」なのに13週間成果物ゼロ — GAS × Search Console API バッチで学んだ監視設計

0
Posted at

環境情報

項目 内容
実行基盤 Google Apps Script(V8 ランタイム)
外部API Google Search Console API v3(searchAnalytics.query
起動方法 時間主導トリガー(毎週月曜 6:00)
出力先 Google スプレッドシート → Drive に CSV を退避
対象サイト規模 月間表示回数 約12,000、週あたりクエリ行 約3,000
通知 なし(これが最大の温床)
監視していたもの 実行ログの「完了」表記のみ

結論を先に書きます。exitコード(=例外なく終わったか)は「動いた」しか保証しません。「正しく動いた」は一切保証しません。 そして私はこれに13週間気づきませんでした。

何が起きていたか

やっていたことはシンプルです。Search Console API から直近7日分のクエリを取得し、スコアリングして閾値以上のキーワードだけを「発掘候補」としてスプレッドシートに書き出す。それを週次トリガーで回していました。

毎週月曜、私は実行ログを開いて「実行完了」の行を確認し、安心して閉じていました。赤いエラーは一度も出ていません。

ある日、ふと出力ファイルを直接開きました。ヘッダ行だけでした。しかも13週連続で0件。シートには1行もデータが積まれていませんでした。

なぜ13週間気づけなかったのか

振り返ると、原因は3つに分解できます。

  1. ログの見方が「成功したか」だけだった(出力の中身を見ていない)
  2. 自動化した達成感で、成果物を開かなくなった
  3. 通知が一切なかった(0件でも誰も教えてくれない)

特に2番目が厄介です。自動化とは「見なくてよくなる」ことではなく「見る場所を変える」ことなのに、私は前者だと錯覚していました。

誤診:「トリガーが設定されていないのでは」

最初に疑ったのは、トリガー未設定でした。過去に別のスクリプトで設定を忘れた経験があったので、真っ先にその可能性を考え、設定画面を開く前に「たぶんそうだろう」と結論しかけました。

ここで踏みとどまって、一次データを見に行きました。GAS の「実行数」画面と Cloud Logging の履歴です。結果、13回分の実行がすべて記録され、すべて正常終了していました。

つまり、動いていた。正しく動いていなかっただけ。 推測(二次情報)とログ(一次情報)が真逆だったわけです。原因調査で一番危ないのは、自分の記憶を証拠として扱うことだと痛感しました。

真因:閾値が構造的に到達不能だった

スコアリングのコードはこうなっていました。

function scoreKeyword(row) {
  const impressions = row.impressions; // 表示回数
  const clicks = row.clicks;
  const position = row.position;

  // 別サイトでの経験値からコピーした固定値
  if (impressions < 1000) return 0;
  if (position > 10) return 0;

  return (clicks / impressions) * (11 - position);
}

閾値 impressions >= 1000 は、以前運用していた大規模サイトでは妥当な数字でした。しかしこのサイトはロングテール中心で、大半のクエリは表示回数が二桁です。実測した分布はこうでした。

分位点 表示回数 旧閾値での通過
p50(中央値) 12 0%
p90 118 0%
p99 640 0%
max 2,180 通過するが position > 10 で落ちる

impressions >= 1000 を満たすのは全体の0.3%未満、さらにその大半が position > 10。積集合はほぼ空集合でした。バグではなく、設計のミスマッチです。例外は飛ばないので、どこにもエラーは出ません。

手順:実測値から閾値を逆算して再校正する

やったことを手順として残します。閾値は「経験値」ではなく「実測値」から決めます。

  1. 閾値なしで全行を書き出す(サンプリング用の一時シート)
  2. 分位点を計算する(p50 / p90 / p95 / p99)
  3. 目標通過件数を決める(例:週150件=上位5%)
  4. その件数になる閾値を逆算する
  5. 閾値をコードから外部化するPropertiesService に保存)
  6. 再実行し、出力件数を目視で確認する

手順2〜4は GAS 内で完結します。

function calcThresholds(rows) {
  const imps = rows.map(r => r.impressions).sort((a, b) => a - b);
  const q = p => imps[Math.min(imps.length - 1, Math.floor(imps.length * p))];

  return {
    minImpressions: Math.max(1, q(0.50)), // 中央値あたりまで許容
    maxPosition: 30,                      // 10 は絞りすぎだった
    minScore: 0.02,
    minExpectedCount: 30,                 // これ未満なら異常とみなす
    calibratedAt: new Date().toISOString(),
    sourceRows: rows.length
  };
}

ポイントは minExpectedCount を持たせたことです。「何件出るはず」を閾値とセットで定義しておくと、監視条件が自然に決まります。

監視対象を exitコード から「出力件数」へ変える

比較表にすると、何を見落としていたかがはっきりします。

観点 exitコード監視(Before) 出力件数監視(After)
検知できる障害 例外、タイムアウト、認証切れ 左記に加えて沈黙障害全般
検知できない障害 フィルタ後0件、閾値ミス、キー名変更
一次情報 実行ログの文字列 成果物そのもの
通知の根拠 なし 件数の閾値・前週比
気づくまでの時間 13週間 翌週の通知時点

実装はこうなりました。「入力0件」と「フィルタ後0件」を明確に区別するのがミソです。

function main() {
  const rows = fetchSearchAnalytics();            // 入力

  if (rows.length === 0) {
    throw new Error('入力0件: APIレスポンス自体が空。期間指定か権限を疑う');
  }

  const th = JSON.parse(
    PropertiesService.getScriptProperties().getProperty('THRESHOLDS')
  );
  const picked = rows.filter(r =>
    r.impressions >= th.minImpressions &&
    r.position <= th.maxPosition &&
    (r.clicks / Math.max(1, r.impressions)) * (31 - r.position) >= th.minScore
  );

  if (picked.length < th.minExpectedCount) {
    throw new Error(
      `出力${picked.length}件 (期待${th.minExpectedCount}件以上) / 入力${rows.length}件`
    );
  }

  writeSheet(picked);
  Logger.log(`OK: 入力${rows.length}件 → 出力${picked.length}件`);
}

例外を投げる設計にしたので、GAS のトリガー失敗通知(「失敗時の通知」設定)にそのまま乗ります。「出力件数が期待未満なら例外にする」——これだけで、通知基盤を新規に作らずに沈黙障害を検知できます。

なお picked.length === 0 ではなく < minExpectedCount にしているのは、0件だけを狙い撃ちすると「1件だけ通った」状態を見逃すからです。

ハマりポイント

  • GAS の実行数画面は「失敗」しか目立たない。 成功は緑で流れていくので、ログを眺める運用は成功の確認にならない
  • Logger.log は長期保存に向かない。 後から追うならスプレッドシートか Cloud Logging に落とす
  • トリガーの実行時間は6分上限。 rowLimit: 25000 を素直に取ると重い。dimensions を増やすとクォータも食う
  • 「データ未確定」と「0件」は区別できない。 Search Console は直近2〜3日が確定しないため、期間指定をずらすと件数が変わる。件数監視には「確定済み期間のみ対象」の条件を入れる
  • 自分で出した「完了」ログは自己申告。 成果物と突き合わせて初めて証拠になる

FAQ

Q1. トリガーが動いたかどうかは、どこで確認すればいいですか?
GAS の「実行数」画面(または Cloud Logging)が一次情報です。「設定したはず」という記憶は証拠になりません。

Q2. 出力件数の閾値はどう決めますか?
過去の実測分布から分位点を出し、「上位何%を拾いたいか」から逆算します。経験値からのコピーは今回の再現に直結します。

Q3. 0件が正常なケースはどう扱いますか?
新規サイトや閑散期は0件も正常です。その場合は minExpectedCount: 0 にしたうえで、代わりに「入力0件の検知」だけを残します。監視を消すのではなく、期待値を下げて維持します。

Q4. 既存バッチに後付けする場合、優先順位は?
①入力件数と出力件数のログ出力 → ②期待件数未満で例外 → ③前週比±50%で警告、の順が費用対効果が高いです。①だけでも今回の事故は翌週に発見できました。

チェックリスト

  • exitコード以外の「成果物KPI」を1つ以上監視している
  • 「入力0件」と「フィルタ後0件」をログで区別している
  • 閾値をコードにハードコードせず、実測から逆算して外部化している
  • 期待件数を下回ったら例外を投げ、トリガー失敗通知に乗せている
  • 前週比の逸脱で警告を出している
  • 出力ファイルを月1回は人間が開いて実査している

まとめ

正常終了は「動いた」だけで、「正しく動いた」は保証しません。今回の13週間は、バグではなく監視設計の穴が生んだ静かな損失でした。そして厄介なのは、この種の障害がログに何も残らないことです。エラーが出ないから気づけない、気づけないから直らない、直らないから成果物が増えない——そのループに入ると、人間はむしろ安心してしまいます。

対策は地味です。成果物の件数を数える。期待値を決めて、下回ったら例外にする。たったそれだけです。あなたのバッチは今週、何件出力しましたか? 即答できないなら、それが最初のシグナルかもしれません。


この記事を書いた人

BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。

GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。

👉 業務自動化サービス — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?