Skip to content

Commit e815b65

Browse files
committed
地図の依存サービスの応答を毎日確認し、異常時に Slack へ通知する
過去2回、サイト自体は HTTP 200 を返しているのに地図が表示されない状態が 発生し、気づくのが遅れた。自サイトの死活監視では検知できないため、 地図の表示に必要な依存先の応答を毎日確認する。 2026/06: スプライト サーバが 502 → マーカーが1件も描画されなかった 2026/07: 埋め込み v1 が参照する旧インフラの不具合 → 世界地図が描画されなかった ## 設計 依存先の HTTP 応答を確認する。過去2件はどちらも HTTP 層で検知できる。 ヘッドレス Chrome での描画検証は採用しない。世界地図 (zoom 0) はヘッドレスでは 描画が完了せず(実験で確認)、検知したい状態をそもそも検知できないうえ、 偽陽性がアラートの信頼を損なうため。 独立ワークフローは作らず scheduler_daily.yml に相乗りする(YAGNI)。 1日1回でも現状からは大きな改善。分離は必要になってから。 ## 2つのガードレール 1. デプロイの「後」に置き、continue-on-error でジョブを失敗させない。 依存先に異常がある時こそ marker: default への切替をデプロイしたいので、 外部要因でデプロイを止めてはならない。 2. 監視専用の Slack 通知を用意し、本文に復旧手順を書く。既存の通知は 「Failed to build DojoMap」だけで、自分のビルド失敗と区別できないため。 ## sprite_status_test.rb の修正(誤報を出す状態だった) - リダイレクトを追うようにした。api.geolonia.com は cdn.geolonia.com へ 302 を返す。 追わないと 302 を異常と誤検知し、毎日誤報が飛ぶ状態だった。 - 検査対象を basic-v1 から basic-v2 に変更(埋め込み v5 が使うスタイル)。 - 「スプライトに coderdojo シンボルが含まれるか」の検査を削除した。 実測したところ basic-v1 / basic-v2 のいずれにも(API キーの有無にかかわらず) coderdojo は存在しないが、マーカーは正常に描画されている(スクショで確認)。 前提が現実と食い違っており、残すと毎日誤報が飛ぶ。ロゴの読み込み経路は未解明。 ## 文言について Slack 通知やコード コメントは第三者の目にも触れうる。提供元への敬意を欠く 表現は使わず、自分たちの地図の状態を主語にした中立な言い回しに統一した (❌「Geolonia の障害を検知」→ ✅「依存サービスから正常な応答が得られていません」)。 障害を模擬して実際にテストが失敗すること (RED → GREEN) を確認済み。
1 parent 45af6f1 commit e815b65

2 files changed

Lines changed: 97 additions & 24 deletions

File tree

.github/workflows/scheduler_daily.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,57 @@ jobs:
108108
personal_token: ${{ secrets.GITHUB_TOKEN }}
109109
publish_dir: ./_site
110110

111+
# 地図の表示は Geolonia のサービスに依存している。過去2回、サイト自体は HTTP 200 を
112+
# 返しているのに地図が表示されない状態が発生した。自サイトの死活監視では気づけないため、
113+
# 依存先の応答を毎日確認する。
114+
# 2026/06: スプライト サーバが 502 → マーカーが1件も描画されなかった
115+
# 2026/07: 埋め込み v1 が参照する旧インフラの不具合 → 世界地図が描画されなかった
116+
#
117+
# 意図的な設計:
118+
# - デプロイの「後」に置く。依存先に異常がある時こそ marker: default への切替を
119+
# デプロイしたいので、外部要因でデプロイを止めてはならない
120+
# - continue-on-error でジョブ自体は失敗させない(本体のビルド失敗と混同しないため)
121+
# - always() でビルドが失敗した時も実行する(依存先の状態は常に知りたい)
122+
- name: 🩺 Check map dependencies
123+
id: geolonia
124+
if: always()
125+
continue-on-error: true
126+
run: |
127+
set -e
128+
129+
# 1. 埋め込みスクリプト (_config.yml で一元管理している URL を読む)
130+
EMBED_URL=$(ruby -ryaml -e 'puts YAML.load_file("_config.yml")["geolonia_embed_url"]')
131+
echo "Checking embed script: $EMBED_URL"
132+
curl -sfL --retry 3 --retry-delay 30 --max-time 30 \
133+
"$EMBED_URL?geolonia-api-key=YOUR-API-KEY" -o /tmp/embed.js
134+
test -s /tmp/embed.js # 空でないこと(200 を返しても中身が空なら異常)
135+
136+
# 2. スプライト サーバ (マーカー描画に必須)
137+
echo "Checking sprite server..."
138+
ruby tests/sprite_status_test.rb # 標準ライブラリのみ。bundle install 不要
139+
env:
140+
GEOLONIA_API_KEY: ${{ secrets.GEOLONIA_API_KEY }}
141+
142+
- name: 🔔 Notify map dependency issue to Slack (if any)
143+
if: always() && steps.geolonia.outcome == 'failure'
144+
uses: slackapi/slack-github-action@v3.0.5
145+
with:
146+
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
147+
webhook-type: incoming-webhook
148+
payload: |
149+
{
150+
"text": "<@yasulab> 地図の依存サービスから正常な応答が得られていません。地図が表示されない可能性があります。",
151+
"blocks": [
152+
{
153+
"type": "section",
154+
"text": {
155+
"type": "mrkdwn",
156+
"text": "<@yasulab> *地図の依存サービス (Geolonia) から正常な応答が得られていません。* 地図が表示されない可能性があります。\n\n*暫定復旧の手順:* マーカーが表示されない場合、`_config.yml` の `marker: coderdojo` を `marker: default` に変更して再デプロイすると、スプライト サーバに依存しない circle マーカーに切り替わります。\n\n<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|» 詳細を見る (GitHub)>"
157+
}
158+
}
159+
]
160+
}
161+
111162
- name: 🔔 Notify error to Slack (if any)
112163
if: failure()
113164
id: slack

tests/sprite_status_test.rb

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,85 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4-
# Geolonia スプライト サーバの健全性を手動で確認するためのテスト
4+
# Geolonia スプライト サーバの健全性を確認するテスト
55
#
66
# 関連 PR: https://github.com/coderdojo-japan/map.coderdojo.jp/pull/28
77
#
88
# == 用途 ==
9-
# マーカーは Geolonia のスプライト サーバ
10-
# (https://api.geolonia.com/v1/sprites/...) からアイコン画像を取得して描画する。
11-
# このサーバが 502 等で落ちると marker-symbol ベースのマーカーが全滅する
12-
# (2026/06 の不具合の真因)。本テストはその状態を手動で検査する。
9+
# マーカーは Geolonia のスプライト サーバからアイコン画像を取得して描画する。
10+
# このサーバから正常な応答が得られないと marker-symbol ベースのマーカーが描画されない
11+
# (2026/06 の不具合の原因)。本テストはその状態を検査する。
1312
#
14-
# == CI には統合しない ==
15-
# 外部サービスの瞬間的な状態に CI/デプロイを依存させない(赤くしない)ため、
16-
# `rake test` には含めない。必要なときに手動で実行する:
17-
# GEOLONIA_API_KEY=xxxx bundle exec rake test_sprite
18-
# (または GEOLONIA_API_KEY=xxxx ruby tests/sprite_status_test.rb)
13+
# == 実行方法 ==
14+
# scheduler_daily.yml から毎日実行され、失敗すると Slack に通知される。
15+
# 手動実行する場合:
16+
# GEOLONIA_API_KEY=xxxx ruby tests/sprite_status_test.rb
17+
#
18+
# == デプロイは止めない ==
19+
# 外部サービスの状態でデプロイを止めてはならない(依存先に異常がある時こそ
20+
# marker: default への切替をデプロイしたい)。そのため `rake test` には含めず、
21+
# ワークフロー側で continue-on-error として実行する。
1922
#
2023
# == セキュリティ ==
2124
# Geolonia の埋め込みキーはクライアント用の公開キー(公開サイトの HTML に既に
2225
# 埋め込み済み)。ドメイン制限で保護される種類。とはいえ本テストは URL や
23-
# キーをログに出さず、HTTP ステータスと coderdojo の有無だけを出力する
26+
# キーをログに出さず、HTTP ステータスだけを出力する
2427

2528
require 'minitest/autorun'
2629
require 'json'
2730
require 'net/http'
2831
require 'uri'
2932

30-
SPRITE_BASE = 'https://api.geolonia.com/v1/sprites/basic-v1@2x'
33+
# 埋め込み v5 が使うスタイル (basic-v2) のスプライト。
34+
# 2026/07 時点で api.geolonia.com は cdn.geolonia.com へ 302 リダイレクトするため、
35+
# リダイレクトを追う必要がある(追わないと 302 を異常と誤検知し、毎日誤報が飛ぶ)。
36+
SPRITE_URL = 'https://api.geolonia.com/v1/sprites/basic-v2.json'
3137

3238
class SpriteStatusTest < Minitest::Test
3339
def setup
3440
@key = ENV['GEOLONIA_API_KEY'].to_s
35-
skip 'GEOLONIA_API_KEY が未設定です(手動実行時に環境変数で渡してください)' if @key.empty?
41+
skip 'GEOLONIA_API_KEY が未設定です' if @key.empty?
3642

37-
uri = URI("#{SPRITE_BASE}.json?key=#{@key}")
38-
@response = Net::HTTP.get_response(uri)
43+
@response = fetch_following_redirects(URI("#{SPRITE_URL}?key=#{@key}"))
3944
rescue StandardError => e
4045
@error = e
4146
end
4247

43-
# スプライト JSON が正常(200)に返るか
48+
# スプライト サーバから正常な応答が得られるか。
49+
# 応答が得られないと marker-symbol ベースのマーカーが描画されない。
4450
def test_sprite_endpoint_is_healthy
4551
flunk "スプライト取得で通信エラー: #{@error.class}: #{@error.message}" if @error
4652

4753
assert_equal '200', @response.code,
48-
"Geolonia スプライト サーバが正常応答していません (HTTP #{@response.code})。" \
49-
'marker-symbol ベースのマーカーは描画されません。' \
50-
'_config.yml は marker: default(スプライト非依存)を推奨します。'
54+
"スプライト サーバから正常な応答が得られていません (HTTP #{@response.code})。" \
55+
'マーカーが描画されない可能性があります。' \
56+
'暫定復旧: _config.yml の marker: coderdojo を marker: default に変更して' \
57+
'再デプロイすると、スプライト非依存の circle マーカーに切り替わります。'
5158
end
5259

53-
# スプライトに 'coderdojo' シンボルが含まれるか(coderdojo モードが使えるか)
54-
def test_sprite_contains_coderdojo_symbol
60+
# 200 を返しても中身が壊れていることはある(今日の教訓: ステータスではなく中身を見る)。
61+
def test_sprite_json_is_parsable
5562
flunk "スプライト取得で通信エラー: #{@error.class}: #{@error.message}" if @error
5663
skip "スプライトが 200 を返していません (HTTP #{@response.code})" unless @response.code == '200'
5764

5865
symbols = JSON.parse(@response.body)
59-
assert symbols.key?('coderdojo'),
60-
"スプライトに 'coderdojo' シンボルがありません(含まれるシンボル数: #{symbols.size})。" \
61-
'marker: coderdojo に切り替えてもロゴ マーカーは描画されません。'
66+
refute_empty symbols, 'スプライト JSON が空です。マーカーが描画されない可能性があります。'
67+
end
68+
69+
# NOTE: 以前ここに「スプライトに coderdojo シンボルが含まれるか」を検査するテストがあったが、
70+
# 2026/07 に削除した。実測したところ basic-v1 / basic-v2 のいずれにも(API キーの有無に
71+
# かかわらず)coderdojo シンボルは存在しないが、マーカーは正常に CoderDojo ロゴで描画されて
72+
# いた(スクリーンショットで確認済み)。前提が現実と食い違っており、残すと毎日誤報が飛ぶ。
73+
# ロゴがどの経路で読み込まれているかは未解明。判明したら適切な監視を足すこと。
74+
75+
private
76+
77+
def fetch_following_redirects(uri, limit = 3)
78+
raise 'リダイレクトが多すぎます' if limit.zero?
79+
80+
res = Net::HTTP.get_response(uri)
81+
return res unless res.is_a?(Net::HTTPRedirection)
82+
83+
fetch_following_redirects(URI(res['location']), limit - 1)
6284
end
6385
end

0 commit comments

Comments
 (0)