はじめに
今回は、Google Analytics(統計データ収集)を本番(prod)環境にのみ適用させる仕組みを、GitHubの環境変数(Environments)を使用してシンプルに実装してみました。
フロントエンドに測定idを固定値で埋め込むことも可能ですが、それだと複数の環境(dev, stg)でテストする際にも統計データが収集されてしまうとう問題が発生したため、以下のように実装してみました。
[GitHubの環境変数(Environments): prod]にのみ[測定id]を登録
↓
GitHub Actionのワークフローで、フロントエンドのビルド時にGitHubの環境変数(Environments)から[測定id]を取得
↓
結果、prodのenvにのみ登録されているため、devとstgには適用されない(測定されない)
前提:
・フロントエンドのデプロイ(更新)にGitHub Actionを使用していること
※今回の例では、フロントエンドのフレームワークにNuxt(TypeScript)を使用しています。
手順
- Google Analytics測定idの取得
- GitHub環境変数へ登録
- GitHub Actionワークフローの編集
- nuxt.config.tsの編集
- gtag.client.tsの実装
1. Google Analytics測定idの取得
今回はGoogle Analyticsアカウントの詳しい作成手順は割愛いたしますが、以下の記事がとてもわかりやすいため、ぜひご参照ください。
アカウントが作成されると、G-1A2B3C4DのようなG-から始まる測定idが発行されます。
2. GitHub環境変数へ登録
本番(prod)環境用のGitHub環境変数にのみ、新たに以下のように環境変数を追加します。
Name: GA_ID
Value: G-1A2B3C4D
GitHub環境変数への登録手順については以下の記事の「2. GitHub Environmentsを3つ用意」セクションをご参照ください。
3. GitHub Actionワークフローの編集
フロントエンド更新用のGitHub Actionワークフローの「ビルドステップ」に、以下のようなコードを追記します。
steps:
- name: Build
env:
# Google Analytics 4 測定 ID。GitHub Environments の prod 環境にだけ
# Variable `GA_ID` を登録する運用(dev/stg は未登録)。未登録の環境では
# 空文字が入り、plugins/gtag.client.ts(後述)が GA を注入しないため、
# prod 以外には計測が一切載らない。
NUXT_PUBLIC_GA_ID: ${{ vars.GA_ID }}
run: pnpm build
以下、.github/workflows/deploy-frontend.ymlの全体像の例
(詳しい実装背景に関しては過去の記事をご参照ください)
name: Deploy LIFF Frontend
run-name: Deploy Frontend 【${{ inputs.target_env }}】
# ============================================================
# フロントエンド(Nuxt SPA を S3 + CloudFront に配信)専用ワークフロー。
# バックエンド(Lambda + API Gateway)は別ワークフロー
# `.github/workflows/deploy-backend.yml` で更新する。
#
# 同一リポジトリ内で関心事を分離する理由:
# - フロント更新(HTML/CSS/JS の差し替え)と
# バックエンド更新(Nitro bundle 差し替え)は更新頻度・影響範囲・
# ロールバック単位が大きく異なる。
# - 1 つにまとめると、デザイン微調整のたびに Lambda の冷起動が走る等の
# 不必要な副作用が生じる。
# - ワークフロー単位で並列実行制御・環境保護を独立に設定できる。
# ============================================================
on:
workflow_dispatch:
inputs:
target_env:
description: 'デプロイ先環境'
required: true
type: choice
options:
- dev
- stg
- prod
# 同一環境への並行デプロイを防止
# 実行中deployは止めず、後続deployはqueueする
concurrency:
group: deploy-frontend-${{ github.event.inputs.target_env }}
cancel-in-progress: false
permissions:
contents: read
id-token: write # OIDC AssumeRole に必須
jobs:
deploy:
name: Deploy Frontend to ${{ inputs.target_env }}
runs-on: ubuntu-latest
timeout-minutes: 15
environment:
name: ${{ inputs.target_env }}
steps:
- name: Validate deploy ref
env:
TARGET_ENV: ${{ inputs.target_env }}
DEPLOY_REF: ${{ github.ref_name }}
run: |
if [ "$TARGET_ENV" = "prod" ] && [ "$DEPLOY_REF" != "main" ]; then
echo "::error::prod deploys must use main (got: $DEPLOY_REF)"
exit 1
fi
- name: Checkout (${{ github.ref_name }})
uses: actions/checkout@v4
with:
# 履歴は不要なので浅く
fetch-depth: 1
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
# package.json の packageManager に従う
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Type check
run: pnpm typecheck
- name: Lint
run: pnpm lint
- name: Build (nuxt build, prerender all routes)
env:
NUXT_PUBLIC_LIFF_ID: ${{ vars.LIFF_ID }}
# Google Analytics 4 測定 ID。GitHub Environments の prod 環境にだけ
# Variable `GA_ID` を登録する運用(dev/stg は未登録)。未登録の環境では
# 空文字が入り、plugins/gtag.client.ts が GA を注入しないため、
# prod 以外には計測が一切載らない。
NUXT_PUBLIC_GA_ID: ${{ vars.GA_ID }}
run: pnpm build
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
role-session-name: gha-frontend-${{ inputs.target_env }}-${{ github.run_id }}
# S3 へ一括アップロード
# 全ファイルを短期キャッシュで統一(ハッシュ付きアセットも含む)
- name: Upload to S3
run: |
aws s3 sync ./.output/public/ s3://${{ vars.S3_BUCKET_NAME }}/ \
--delete \
--cache-control "no-cache, must-revalidate" \
--metadata-directive REPLACE \
--only-show-errors
# CloudFront 無効化(静的アセットのみ。/api/* は CachingDisabled なので
# invalidation 不要)。
- name: Create CloudFront invalidation
id: invalidate
run: |
INV_ID=$(aws cloudfront create-invalidation \
--distribution-id ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*" \
--query 'Invalidation.Id' \
--output text)
echo "invalidation_id=$INV_ID" >> $GITHUB_OUTPUT
echo "Invalidation: $INV_ID"
- name: Wait for invalidation to complete
run: |
aws cloudfront wait invalidation-completed \
--distribution-id ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} \
--id ${{ steps.invalidate.outputs.invalidation_id }}
- name: Summary
env:
DEPLOY_ENV: ${{ inputs.target_env }}
run: |
printf '## ✅ Frontend deploy completed (`%s`)\n' "$DEPLOY_ENV" >> "$GITHUB_STEP_SUMMARY"
ビルド時にvars.GA_IDを注入しますが、prod環境以外の場合は空文字になります。
4. nuxt.config.tsの編集
外部から注入された環境変数をフロントエンドで使用できるようにするため、nuxt.config.tsのruntimeConfig.publicにgaId: ''を追加します。
NUXT_PUBLIC_GA_IDというenvが存在すればその値で上書きされ、無ければ空のまま、という挙動になります。
export default defineNuxtConfig({
runtimeConfig: {
public: {
gaId: '',
},
},
})
5. gtag.client.tsの実装
アクセスユーザーの統計データ取得ロジックファイルgtag.client.tsを以下のように実装します。
// ============================================================
// Google Analytics 4 (gtag.js) ローダー
// ------------------------------------------------------------
// .client.ts のためクライアント側でのみ実行される(gtag.js は
// window/document を前提とするため SSR 不可。本アプリは元々 ssr:false)。
//
// 【prod だけに効かせる設計】
// 測定 ID は runtimeConfig.public.gaId(← NUXT_PUBLIC_GA_ID)から読む。
// この env はビルド時注入で、GitHub Environments の prod 環境にだけ
// Variable `GA_ID` を登録する運用(dev/stg は未登録=空)。
// よって gaId が空の環境では下の早期 return で GA を一切注入しない。
// 詳細は nuxt.config.ts の runtimeConfig.public.gaId コメント参照。
//
// 【SPA でのページビュー計測】
// ssr:false の SPA では gtag('config') の自動 page_view は初回ロードの
// 1 回しか飛ばず、<NuxtLink> によるルート遷移を拾えない。そのため
// send_page_view:false で自動送信を止め、router.afterEach で遷移ごと
// (初回ナビゲーション含む)に page_view を手動送信する。
// ============================================================
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const gaId = config.public.gaId
// ==========================================================
// 測定 ID 未設定(dev/stg やローカル)なら何もしない
// ==========================================================
// GA のスクリプトも dataLayer も注入しないので、prod 以外の環境では
// GA は完全に無効(ネットワークリクエストすら発生しない)。
if (!gaId) {
return
}
// ==========================================================
// dataLayer / gtag の初期化
// ==========================================================
// gtag.js の定番ブートストラップ。
//
// 【重要】必ず `arguments` オブジェクトを push すること。
// gtag.js の dataLayer 処理は「arguments オブジェクト([object Arguments])」
// だけをコマンドとして解釈する。`function gtag(...args){ push(args) }` のように
// rest パラメータの「配列([object Array])」を push すると、gtag.js はそれを
// コマンドと認識せず黙って無視する。その結果 js / config / page_view が
// 一切実行されず、/g/collect へのヒットが 0 件になり GA が計測されない。
// (Google 公式スニペットが `function gtag(){dataLayer.push(arguments)}` と
// 書いているのはこのため。)
window.dataLayer = window.dataLayer || []
function gtag(..._args: unknown[]) {
// eslint-disable-next-line prefer-rest-params -- gtag.js は arguments のみ解釈するため rest 配列は不可
window.dataLayer.push(arguments)
}
gtag('js', new Date())
// ==========================================================
// gtag.js 本体をロード(useHead で <head> に注入)
// ==========================================================
useHead({
script: [
{
src: `https://www.googletagmanager.com/gtag/js?id=${gaId}`,
async: true,
},
],
})
// ==========================================================
// 設定(自動 page_view は止める)
// ==========================================================
gtag('config', gaId, { send_page_view: false })
// ==========================================================
// SPA ルート遷移ごとに page_view を手動送信
// ==========================================================
// プラグインは初回ナビゲーション解決前に走るため、afterEach は初回遷移も
// 拾う。これで初回ロード + 以降の <NuxtLink> 遷移をすべて計測できる。
const router = useRouter()
router.afterEach((to) => {
gtag('event', 'page_view', {
page_path: to.fullPath,
page_location: window.location.href,
page_title: document.title,
})
})
})
// ============================================================
// TypeScript 型拡張: window.dataLayer
// ------------------------------------------------------------
// gtag.js が使うグローバル配列。未宣言だと window.dataLayer 参照で
// 型エラーになるため最小限の型を足す。
// ============================================================
declare global {
interface Window {
dataLayer: unknown[]
}
}
export {}
config.public.gaIdが空文字の場合(prod環境以外の場合)は早期リターンで計測を実施しません。
今回はSPAのページビュー計測を自前制御したい&&軽微な実装のためにgtagを自前実装していますが、制限がある代わりに導入が楽なnuxt-gtagやvue-gtagといったライブラリもあるのでぜひ使ってみてください。
今回は以上になります!
最後までお読みいただきありがとうございました。