どのデプロイで遅くなったのか?
6月のLCPは2.1秒でした。現在は2.9秒です。この8週間で40回リリースしましたが、その40回のうちどれが原因なのか、モニタリングツールは教えてくれません。
これはデータのせいではありません。CoreDashはすでに実際の全訪問者の全ページビューを測定しています。CoreDash単体で分からないのは、火曜日の14時32分にそちらで何が変更されたかです。その情報がないと、リグレッションは「v2.4.1が原因」ではなく「先月何かが遅くなった」という認識に留まります。結局、記憶を頼りに原因を探り、リリース内容をチームに確認し、大抵は諦めて無関係な箇所を最適化することになります。
CI/CD連携は、2つの方向からこのギャップを埋めます。デプロイ前には、Core/Dashがプレビュービルドをスキャンし、ページが予算を超過した場合はパイプラインを停止します。デプロイ後は、すべてのページビューに配信時のバージョンがタグ付けされるため、リリースごとに固有のトラフィックとして測定されます。
どちらも任意で、独立して機能します。片方だけ実行することも、両方実行することも、どちらも実行しないことも可能です。フロントエンドのコードを1行変更したり、ページに2つ目のスクリプトを追加したりする必要は一切ありません。
1つのデプロイに対する2つのチェック
この2つはそれぞれ異なる問題を検知します。連携を設定する前に、この点を理解しておくことが重要です。
デプロイ前チェックはシンセティックテストです。プレビュー環境のデプロイをLighthouseで読み込み、ビルド自体の実態を確認します。ページが送信するバイト数、スクリプトの量、画像のサイズ、DOMノード数などです。これらは、人間のレビューでは見落とされがちなリグレッションです。新しいヒーロー画像が4MBのPNGであることや、マーケティングタグが300KBの余分なJavaScriptをバンドルに引き込んだことに、diffを見て気づく人はいません。
リリース比較はfield dataです。実際のデバイス、実際のネットワーク、実際のユーザー環境で、コードが本番に反映された後に測定されます。LCP、INP、CLSが実際に存在するのはそこだけです。Googleのインフラでシンセティックテストを実行しても、インドネシアのAndroidトラフィックが900ミリ秒遅くなったことは分かりません。アイオワのデータセンターは、4G接続のAndroidスマートフォンではないからです。
どちらも互いの代わりにはならないため、両方必要です。シンセティックテストは、明白なミスを早期かつ低コストで検知します。field dataはユーザーが実際に体験した結果を示しており、Googleが検索順位の評価に使用する唯一の指標です。
デプロイ前:デプロイ前チェック
パイプラインからプレビューURLに対してPOSTリクエストを1回送信します。Core/Dashは、そのプレビューオリジン上の監視対象ページに対してLighthouseを実行し、設定済みのLighthouse予算とスコアを照らし合わせ、合格(pass)か違反(breach)かを返します。所要時間は約30〜45秒です。
チェックの対象
Core/Dashは、スコア化可能な予算が設定されている監視対象のLighthouseページのうち、最初の4つを対象とします。各ページ固有のパス、クエリ文字列、デバイス設定はそのまま保持し、ホストのみを指定されたプレビューオリジンに置き換えます。つまり、チェック対象となるのは、毎晩のスキャンで確認しているのと同じルートの、まだリリースされていないビルドです。
上限が4ページなのは意図的なものです。この4つのスキャンはPageSpeed Insights APIに対して並行して実行されます。1つの遅いページによってビルドが滞るのを防ぐため、各スキャンの実行時間は75秒に制限されています。
スコア化されるものと、されないもの
同一ビルドを2回実行しても変動しない測定値のみが対象となります。
| スコア化される | スコア化されない |
|---|---|
| ページの総容量 | Total Blocking Time |
| サードパーティの容量 | Bootup Time |
| スクリプトの容量 | Main Thread Work |
| 画像の容量 | Lighthouseパフォーマンススコア |
| CSSの容量 | |
| フォントの容量 | |
| 未使用のJavaScript | |
| DOMサイズ |
CIでのLighthouseパフォーマンススコアは、乱数発生器のようなものです。変更のない同じページを5回実行すると、スコアが10ポイントも変動することがあります。これは、スコアがCPUに依存する時間に大きく左右され、テストを実行するマシンが毎回異なるためです。これをビルドのゲートとして設定すると、意味のないパイプラインの失敗が頻発し、チームは緑色になるまでジョブを再実行するようになります。これは、チェックを全くしないよりも悪い状態です。
バイト数ならそのようなことは起こりません。今日のビルドのスクリプトが1.4MBで、明日のビルドが1.9MBになったとしたら、それはGoogleのハードウェアのせいではなく、あなたのコミットによるものです。そのため、チェックではこれらの項目のみをスコア化します。時間の指標は、本来あるべき場所である実際のユーザーデータ(つまりCPUが訪問者の実際のスマートフォンである環境)で測定します。
このフィルタリングは予算の項目ごとに機能します。両方のタイプが混在する予算の場合、スコア化可能な項目のみがテストされ、レスポンスには常にスキップされた項目とその理由がリストされます。
パイプラインからの呼び出し
まずプロジェクトのAPIキーを作成し(アプリ内で、プロジェクト > AI Insights > Connect Your AI の順に選択)、CIのシークレットにCOREDASH_API_KEYとして保存します。キーはcdk_で始まり、1つのプロジェクトにスコープされています。マスターエージェンシーキーは、リリースを紐付けるプロジェクトが指定されていないため、ここでは拒否されます。
STATUS=$(curl -sS -o gate.json -w '%{http_code}' -X POST "https://app.coredash.app/api/project/releases/check" \
-H "Authorization: Bearer $COREDASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tag":"v1.2.3","origin":"https://preview.example.app"}')
if [ "$STATUS" != "200" ]; then
echo "Gate call failed with HTTP $STATUS: $(cat gate.json)" >&2
exit 1
fi
jq -e '.data.check.status != "breach"' gate.json > /dev/null curlを直接jqにパイプして、このスニペットを短くしないでください。キーが不正な場合は401、タグが不正な場合は400が返され、これらのレスポンスにはdataオブジェクト自体が含まれません。dataがない状態で.data.check.statusを読み取ると、jqではnullになります。nullは"breach"ではないため、パイプは正常終了(exit 0)してしまいます。つまり、期限切れのAPIキーがパスしたかのように振る舞い、ゲートが密かに機能しなくなるのです。HTTPステータスをチェックすることで、この問題を確実に防げます。
パラメータ
| フィールド | 必須 | 説明 |
|---|---|---|
tag | はい | バージョン文字列。英数字、. _ / -を使用可能。最大64文字。 |
origin | はい | プレビューオリジン。例:https://pr-42.example.app。スキームとホストのみ。 |
sha, branch, actor, repo, prNumber, runUrl | いいえ | Gitコンテキスト。アプリ内のディープリンク用に保存されます。これによって処理が分岐することはありません。 |
オリジンはシンプル(bare)でなければなりません。URLにパス、クエリ文字列、フラグメント、またはBasic認証の認証情報が含まれていると、すべて400エラーになります。これは意図的な仕様です。スキャナーはスキームとホストのみを各監視対象ページのURLにコピーするため、/pr-42/配下で提供されるプレビューは、誤ったURLでサイレントにスキャンされ、存在しないページに対して合格を報告してしまうからです。
レスポンスの内容
有効なリクエストには常にHTTP 200が返されます。判定結果は、レスポンスボディのdata.check.statusに含まれます。
| ステータス | 意味 | ビルドの処理 |
|---|---|---|
pass | スコア化されたすべての項目が予算内 | 続行 |
breach | 少なくとも1つの項目が予算を超過 | 停止 |
error | ページを全くスキャンできなかった | 続行 |
no-budgets | 監視対象のどのページにも、スコア化可能な予算が設定されていない | 続行 |
タグまたはオリジンが不正な場合は400、キーが不正または存在しない場合は401が返されます。これらのレスポンスボディには、statusとmessageのみが含まれます。
チェックはページごとにフェイルオープン(障害時は処理を続行)で動作します。4ページのうち1ページでPageSpeed Insightsがtimeoutした場合、そのページはエラーとして報告され、残りの3ページは通常通りスコア化されます。Google側の数分間の不調のせいで、ビルドがブロックされることはありません。エラーは握りつぶされることなく報告されるため、どのページが測定されなかったかをレスポンスで確認できます。
さらに2つ知っておくべきことがあります。1つ目は、PageSpeed InsightsはGoogle側からアクセスするため、プレビュー環境はパブリックインターネットから到達可能である必要があります。パスワードで保護されていたり、IP制限がかけられていたりするプレビュー環境はスキャンできず、これに対する回避策はありません(プレビュー環境が認証下にある場合は、代わりに公開されているステージング環境に対してチェックを実行してください)。2つ目は、このチェック結果が本番環境の予算ボードに書き込まれることは絶対にないという点です。プレビュー環境の数値は、本番サイトを評価するための数値からは完全に除外されます。
デプロイ後:リリースの記録
デプロイが成功したら、新しいバージョンが本番環境に反映されたことをCore/Dashに伝えます。
curl -sS -X POST "https://app.coredash.app/api/project/releases/ingest" \
-H "Authorization: Bearer $COREDASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tag":"v1.2.3"}' GitHub Actionsでgitのrefをバージョンとして使用する場合:
- name: Report release to CoreDash
if: success()
env:
COREDASH_API_KEY: ${{ secrets.COREDASH_API_KEY }}
run: |
curl -sS -f -X POST "https://app.coredash.app/api/project/releases/ingest" \
-H "Authorization: Bearer ${COREDASH_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"tag\":\"${{ github.ref_name }}\"}" curlを実行できるCI環境なら何でも機能します。GitLab、Jenkins、CircleCI、あるいはどこかのサーバー上にあるbashのデプロイスクリプトでも構いません。
| フィールド | 必須 | 説明 |
|---|---|---|
tag | はい | チェック時と同じルールです。チェック時に送信したのと同じ文字列を使用してください。 |
deployedAt | いいえ | ISOタイムスタンプ。デフォルトは現在時刻です。過去の時刻を設定して、すでに完了したデプロイを遡って記録することも可能です。 |
notes | いいえ | リリース内容。最大2000文字。 |
sha, branch, actor, repo, prNumber, runUrl | いいえ | Gitコンテキスト。チェック時と同じです。 |
リリースはプロジェクトとタグごとに一意となります。同じタグを再度送信した場合は、2つ目の行が作成されるのではなく既存の行が更新されます。そのため、同じバージョンを再利用するロールバックは、元のリリースに統合されます。
パイプラインを使用していない場合は、ReleasesページにTag deploymentフォームがあります。バージョン、リリース時刻、任意のメモを入力できます。これは同じデータ行ですが、送信元がciではなくsiteになります。すでにリリース済みのデプロイを後から登録するために使用できます。ただし、誰かが忘れずに入力したデプロイのみが記録され、デプロイ時刻の正確性も記憶に依存するため、これは信頼性の低い選択肢であることは認識しておくべきです。
トラフィックとリリースの紐付け
リリースを記録すると、プロジェクトの現在のバージョンとしてスタンプされます。トラッキングスクリプトを配信するホストがそのスタンプを読み取り、配信するスニペットにタグを注入します。そのため、それ以降のすべてのページビューにはrelディメンションとしてバージョンが付与されます。スタンプが変更された瞬間に、Cloudflareからエッジのスクリプトがパージされるため、CDNキャッシュの更新を待つ必要はありません。
ページ自体を変更する必要はありません。ビルドステップの追加も、テンプレートへのデータ属性の追加も、2つ目のSDKも不要です。
もし自分自身で値を制御したい場合は、トラッカーが読み込まれる前にwindow.__CWVRELを設定してください。こちらが注入されたタグよりも優先されます。これは、CIではなくアプリケーション自身がビルドIDを把握しているセットアップに適した方法です。設定する値は、必ず実際のバージョン文字列にしてください。リクエストごとのビルドハッシュを紐付けると、ページビューごとに新しいディメンション値が生成され、後から実行するすべてのグループ化クエリの使い勝手が悪くなります。
指標は、そのタグのみを介してリリースと紐付きます。Core/Dashが「この2つのタイムスタンプ間のトラフィックは、おそらくこのリリースのものである」と推測するような、時間枠を使ったfallbackは一切ありません。タグ付けされたトラフィックがないリリースの場合、その行にはダッシュ(-)が表示されます。前のバージョンのユーザーをこっそり新しいバージョンに帰属させた数値を見せるよりも、これが誠実な回答だからです。
結果を判断する前に、約200ページビューの蓄積を待ってください。それ以下の数値では、個々のユーザーによってp75が大きく変動するため、合否の判定はノイズになってしまいます。
判定結果は、アラートと通知側ですでに運用しているRUMの予算に基づいています。リリース用に2つ目の閾値を設定する必要はなく、両者の判定が食い違うこともありません。
リリースのステータス
両方のステップが同じリリースの行に書き込むため、バージョンにはチェックからリリースまでのライフサイクルが存在します。
| ステータス | 設定元 | 意味 |
|---|---|---|
blocked | チェックでの予算超過時 | このタグでは何もリリースされていない |
pending | チェックでのそれ以外の場合 | 合格したが、デプロイはまだ報告されていない |
released | Ingest、またはアプリ内のフォーム | 本番に反映済み。実際のデプロイ時刻が記録されている |
releasedに到達して初めて、プロジェクトにスタンプが押され、トラッカースクリプトがパージされます。まだリリースされていない行も、「Blocked」または「Awaiting deploy」というラベル付きでタイムラインに表示されますが、デプロイ時刻や実際のユーザーの指標は存在しません。ブロックされた行には超過した指標が保持されるため、1週間後でも、なぜそのバージョンがリリースされなかったのかを確認できます。
すでにリリースされたタグに対してチェックを再実行した場合、ステータスやデプロイ時刻は変更されず、新しい結果のみが記録されます。この機能が提供される前に記録したリリースにはステータスが設定されておらず、すべてリリース済みとして扱われます。
設定全体の流れ
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... build and publish your preview here ...
- name: Check the preview against budgets
env:
COREDASH_API_KEY: ${{ secrets.COREDASH_API_KEY }}
run: |
STATUS=$(curl -sS -o gate.json -w '%{http_code}' -X POST \
"https://app.coredash.app/api/project/releases/check" \
-H "Authorization: Bearer $COREDASH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tag":"${{ github.ref_name }}","origin":"https://preview.example.app","sha":"${{ github.sha }}","runUrl":"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}')
if [ "$STATUS" != "200" ]; then
echo "Gate call failed with HTTP $STATUS: $(cat gate.json)" >&2
exit 1
fi
jq -e '.data.check.status != "breach"' gate.json > /dev/null
# ... deploy to production here ...
- name: Report release to CoreDash
if: success()
env:
COREDASH_API_KEY: ${{ secrets.COREDASH_API_KEY }}
run: |
curl -sS -f -X POST "https://app.coredash.app/api/project/releases/ingest" \
-H "Authorization: Bearer ${COREDASH_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"tag\":\"${{ github.ref_name }}\",\"sha\":\"${{ github.sha }}\"}" 両方の呼び出しで同じtagを使用してください。これにより、チェック結果と実際のユーザーの指標が2つに分かれることなく、1つのリリースとして統合されます。
Core/Dashでの見え方
Releasesページには、過去30日間の履歴が新しい順にリストされます。各リリースについて、バージョン、CIとアプリのどちらから送信されたか、デプロイ時刻、各Core Web Vitalsのp75値、前のリリースとの差分、そして現在の予算ステータスを確認できます。未リリースのバージョンも、ステータスを示すチップと一緒に同じリストに表示されます。
リリース詳細では、同じフィルターを適用した上で、サイト全体のデータとリリースのデータを比較できます。画面上部にはデプロイ前チェックの結果が表示され、スキャンされたページと予算項目ごとの行、およびスキップされた指標が確認できます。その下には、ミュートしていないすべてのRUMの予算状況を示すライブボードが表示されます。
Performance Snapshotsにはリリースのトグルがあり、チャート上にデプロイのマーカーを描画できます。マーカーは実際にリリースされたバージョンにのみ表示されるため、ブロックされたビルドが、トラフィックを一度も処理していないのにチャート上に線として表示されることはありません。
Lighthouseの予算提案に関する1つの変更点
Lighthouseの提案される予算は、測定値に対して従来の15%から30%の余裕(ヘッドルーム)を持つようになりました。自動セットアップとクロールによる提案のどちらにも、この新しい数値が使用されます。すでに設定済みの予算に変更はありません。
この変更の理由はデプロイ前チェックです。予算作成時の測定値から15%上乗せした程度の予算では、最初の真っ当な変更ですぐに超過してしまいます。通常の作業で頻繁に引っかかるようなゲートは、1週間もしないうちにオフにされてしまうでしょう。30%であれば、実際の機能追加のための余裕を残しつつ、4MBの画像のような明白な問題は確実に捕捉できます。
リリースのデータが表示されない場合
リリースの行にダッシュが表示されている場合、タグ付けされたトラフィックがまだ存在しないことを意味します。その理由はいくつかしかありません。コードが実際に反映される前にデプロイが報告されたため、流入してきたページビューがまだ古いバージョンで処理されていた。または、リリースが数分前に記録されたばかりで、まだ200ページビューに達していない。あるいは、ページ上でwindow.__CWVRELに別の値が設定されてそちらが優先されたため、CIが送信したタグとビーコンのタグが一致していない、といったケースです。
データテーブルをReleaseディメンションでグループ化すれば、これを直接確認できます。そのリストに自分のタグがあり、ページビューが発生していれば、紐付けは正常に機能しており、単純にトラフィック数の蓄積を待っている状態です。
以下もご参照ください。スクリプトやAIエージェントからこれらのデータをクエリする場合はCore/Dash API、判定に使用される予算についてはアラートと通知、サイトにトラッカーをまだ設置していない場合はインストールをご覧ください。