0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

今回は、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)を使用しています。

手順

  1. Google Analytics測定idの取得
  2. GitHub環境変数へ登録
  3. GitHub Actionワークフローの編集
  4. nuxt.config.tsの編集
  5. 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ワークフローの「ビルドステップ」に、以下のようなコードを追記します。

.github/workflows/deploy-frontend.yml
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の全体像の例
(詳しい実装背景に関しては過去の記事をご参照ください)

.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.tsruntimeConfig.publicgaId: ''を追加します。
NUXT_PUBLIC_GA_IDというenvが存在すればその値で上書きされ、無ければ空のまま、という挙動になります。

nuxt.config.ts(該当箇所抜粋)
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      gaId: '',
    },
  },
})

5. gtag.client.tsの実装

アクセスユーザーの統計データ取得ロジックファイルgtag.client.tsを以下のように実装します。

app/plugins/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-gtagvue-gtagといったライブラリもあるのでぜひ使ってみてください。

今回は以上になります!
最後までお読みいただきありがとうございました。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?