2026/04/27

Google Analytics 4のデータをPDFレポートに自動変換するAPI連携ガイド

レポートGoogle Analytics自動化Node.js

GA4のダッシュボードはリアルタイム監視には優れていますが、ステークホルダーはオフラインで読めるPDF、共有ドライブに保存できるスナップショット、週次メールに添付できるレポートを求めることが多いです。GA4のUIには組み込みのPDFエクスポートがなく、データをスライドに手動で貼り付けるのはスケールしません。

このガイドでは、GA4 Data APIからメトリクスを取得し、Chart.jsでグラフを描画し、FUNBREW PDF APIでブランド付きPDFを生成するパイプライン全体をNode.jsとPythonで実装する方法を解説します。

アーキテクチャ概要

GA4 Data API  →  Node.js/Python  →  HTMLテンプレート  →  FUNBREW PDF API  →  メール / S3
  1. 取得: googleapis(Node.js)または google-analytics-data(Python)クライアントでGA4メトリクスを取得
  2. レンダリング: メトリクスとグラフをHTMLテンプレートに描画
  3. 変換: FUNBREW PDF REST APIでHTMLをPDFに変換
  4. 配信: メール送信またはクラウドストレージにアップロード

このパターンの詳細はPDFレポート自動生成ガイドレポート自動化ユースケースでも解説しています。

Step 1 — GA4 Data APIへのアクセス設定

Google Cloud ConsoleでサービスアカウントをGoogleアカウントに紐付けて作成し、GA4プロパティに閲覧者権限で追加して、JSON認証情報ファイルをダウンロードします。

npm install @google-analytics/data googleapis nodemailer

Step 2 — GA4メトリクスの取得(Node.js)

const { BetaAnalyticsDataClient } = require('@google-analytics/data');
const path = require('path');

const analyticsClient = new BetaAnalyticsDataClient({
  keyFilename: path.join(__dirname, 'ga4-credentials.json'),
});

const GA4_PROPERTY_ID = 'properties/XXXXXXXX'; // GA4プロパティIDに変更

async function fetchMonthlyMetrics(startDate, endDate) {
  const [response] = await analyticsClient.runReport({
    property: GA4_PROPERTY_ID,
    dateRanges: [{ startDate, endDate }],
    dimensions: [{ name: 'date' }],
    metrics: [
      { name: 'sessions' },
      { name: 'activeUsers' },
      { name: 'screenPageViews' },
      { name: 'bounceRate' },
    ],
    orderBys: [{ dimension: { dimensionName: 'date' } }],
  });

  return response.rows.map((row) => ({
    date: row.dimensionValues[0].value,
    sessions: parseInt(row.metricValues[0].value, 10),
    users: parseInt(row.metricValues[1].value, 10),
    pageviews: parseInt(row.metricValues[2].value, 10),
    bounceRate: parseFloat(row.metricValues[3].value),
  }));
}

Step 3 — HTMLレポートテンプレートの作成

テンプレートはKPIサマリーテーブルとChart.jsの折れ線グラフを組み合わせます。FUNBREW PDFはヘッドレスChromiumでレンダリングするため、Chart.jsはそのまま動作します。

function buildReportHTML(metrics, reportPeriod) {
  const labels = metrics.map((m) => m.date);
  const sessionData = metrics.map((m) => m.sessions);
  const userDataArr = metrics.map((m) => m.users);

  const totals = metrics.reduce(
    (acc, m) => {
      acc.sessions += m.sessions;
      acc.users += m.users;
      acc.pageviews += m.pageviews;
      return acc;
    },
    { sessions: 0, users: 0, pageviews: 0 }
  );

  const avgBounce = (
    metrics.reduce((s, m) => s + m.bounceRate, 0) / metrics.length
  ).toFixed(1);

  return `<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: 'Noto Sans JP', -apple-system, sans-serif; font-size: 13px;
           color: #1e293b; background: #fff; padding: 32px; }
    h1 { font-size: 20px; font-weight: 700; color: #0f172a; margin-bottom: 4px; }
    .period { font-size: 13px; color: #64748b; margin-bottom: 24px; }
    .kpi-grid { display: flex; gap: 16px; margin-bottom: 28px; }
    .kpi { flex: 1; background: #f8fafc; border: 1px solid #e2e8f0;
           border-radius: 8px; padding: 16px; }
    .kpi-label { font-size: 11px; color: #64748b; text-transform: uppercase;
                  letter-spacing: .5px; margin-bottom: 6px; }
    .kpi-value { font-size: 24px; font-weight: 700; color: #0f172a; }
    .chart-wrap { margin-bottom: 24px; }
    h2 { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
    canvas { max-height: 220px; }
    @page { size: A4; margin: 15mm; }
    @media print { body { padding: 0; } }
  </style>
</head>
<body>
  <h1>月次アナリティクスレポート</h1>
  <div class="period">${reportPeriod}</div>

  <div class="kpi-grid">
    <div class="kpi"><div class="kpi-label">セッション数</div>
      <div class="kpi-value">${totals.sessions.toLocaleString()}</div></div>
    <div class="kpi"><div class="kpi-label">ユーザー数</div>
      <div class="kpi-value">${totals.users.toLocaleString()}</div></div>
    <div class="kpi"><div class="kpi-label">ページビュー</div>
      <div class="kpi-value">${totals.pageviews.toLocaleString()}</div></div>
    <div class="kpi"><div class="kpi-label">平均直帰率</div>
      <div class="kpi-value">${avgBounce}%</div></div>
  </div>

  <div class="chart-wrap">
    <h2>セッション数・ユーザー数 — 日別推移</h2>
    <canvas id="trendChart"></canvas>
  </div>

  <script>
    new Chart(document.getElementById('trendChart'), {
      type: 'line',
      data: {
        labels: ${JSON.stringify(labels)},
        datasets: [
          { label: 'セッション数', data: ${JSON.stringify(sessionData)},
            borderColor: '#6366f1', backgroundColor: 'rgba(99,102,241,.1)',
            tension: 0.3, fill: true },
          { label: 'ユーザー数', data: ${JSON.stringify(userDataArr)},
            borderColor: '#10b981', backgroundColor: 'rgba(16,185,129,.1)',
            tension: 0.3, fill: true },
        ],
      },
      options: {
        animation: false,
        plugins: { legend: { position: 'bottom' } },
        scales: { y: { beginAtZero: true } },
      },
    });
  </script>
</body>
</html>`;
}

Step 4 — FUNBREW PDF APIでPDF変換

const https = require('https');

async function htmlToPdf(html) {
  const payload = JSON.stringify({
    html,
    options: {
      format: 'A4',
      printBackground: true,
      displayHeaderFooter: true,
      footerTemplate:
        '<div style="font-size:9pt;color:#64748b;width:100%;text-align:center;">' +
        '<span class="pageNumber"></span> / <span class="totalPages"></span> ページ' +
        '</div>',
      marginTop: '15mm',
      marginBottom: '20mm',
    },
  });

  return new Promise((resolve, reject) => {
    const req = https.request(
      {
        hostname: 'pdf.funbrew.cloud',
        path: '/api/v1/pdf',
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
        },
      },
      (res) => {
        const chunks = [];
        res.on('data', (c) => chunks.push(c));
        res.on('end', () => {
          if (res.statusCode === 200) resolve(Buffer.concat(chunks));
          else reject(new Error(`PDF APIエラー: ${res.statusCode}`));
        });
      }
    );
    req.on('error', reject);
    req.write(payload);
    req.end();
  });
}

FUNBREW PDFドキュメントでウォーターマーク・カスタムヘッダー・パスワード保護などのオプションを確認できます。

Step 5 — 全体を結合してPDFをメール送信

const nodemailer = require('nodemailer');
const fs = require('fs');

async function generateAndSendReport() {
  // 先月の日付範囲を計算
  const now = new Date();
  const firstOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
  const lastOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0);
  const startDate = firstOfLastMonth.toISOString().slice(0, 10);
  const endDate = lastOfLastMonth.toISOString().slice(0, 10);
  const period = `${startDate} 〜 ${endDate}`;

  console.log(`GA4データ取得中: ${period}`);
  const metrics = await fetchMonthlyMetrics(startDate, endDate);

  console.log('HTMLテンプレートをレンダリング中…');
  const html = buildReportHTML(metrics, period);

  console.log('PDF生成中…');
  const pdfBuffer = await htmlToPdf(html);

  const filename = `analytics-report-${startDate.slice(0, 7)}.pdf`;
  fs.writeFileSync(filename, pdfBuffer);

  const transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    port: 587,
    auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
  });
  await transporter.sendMail({
    from: process.env.SMTP_USER,
    to: process.env.REPORT_RECIPIENTS,
    subject: `月次アナリティクスレポート — ${period}`,
    text: '月次アナリティクスレポートを添付します。',
    attachments: [{ filename, content: pdfBuffer }],
  });

  console.log(`レポート送信完了: ${filename}`);
}

generateAndSendReport().catch(console.error);

Pythonバージョン

import os
import requests
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    DateRange, Dimension, Metric, RunReportRequest, OrderBy
)

GA4_PROPERTY_ID = "properties/XXXXXXXX"

def fetch_monthly_metrics(start_date: str, end_date: str):
    client = BetaAnalyticsDataClient.from_service_account_file("ga4-credentials.json")
    request = RunReportRequest(
        property=GA4_PROPERTY_ID,
        date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
        dimensions=[Dimension(name="date")],
        metrics=[
            Metric(name="sessions"),
            Metric(name="activeUsers"),
            Metric(name="screenPageViews"),
            Metric(name="bounceRate"),
        ],
        order_bys=[OrderBy(dimension={"dimension_name": "date"})],
    )
    response = client.run_report(request)
    return [
        {
            "date": row.dimension_values[0].value,
            "sessions": int(row.metric_values[0].value),
            "users": int(row.metric_values[1].value),
            "pageviews": int(row.metric_values[2].value),
            "bounce_rate": float(row.metric_values[3].value),
        }
        for row in response.rows
    ]

def html_to_pdf(html: str) -> bytes:
    resp = requests.post(
        "https://pdf.funbrew.cloud/api/v1/pdf",
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {os.environ['FUNBREW_PDF_API_KEY']}",
        },
        json={
            "html": html,
            "options": {
                "format": "A4",
                "printBackground": True,
                "displayHeaderFooter": True,
                "footerTemplate": (
                    '<div style="font-size:9pt;color:#64748b;width:100%;text-align:center;">'
                    '<span class="pageNumber"></span> / '
                    '<span class="totalPages"></span> ページ</div>'
                ),
                "marginTop": "15mm",
                "marginBottom": "20mm",
            },
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.content

cron で自動スケジュール実行

毎月1日の午前8時に実行するcron設定(crontab -e で追加):

0 8 1 * * /usr/bin/node /app/generate-ga4-report.js >> /var/log/reports.log 2>&1

クラウド環境ではAWS EventBridge SchedulerまたはGoogle Cloud Schedulerを使ってHTTPエンドポイントをトリガーする方法も有効です。複数クライアント向けにレポートを一括生成する場合はバッチPDF生成ガイドも参照してください。

よくあるエラーと対処法

問題 原因 対処法
PDFにグラフが出ない Chart.jsのアニメーションが完了前に変換される animation: false をChart.js設定に追加
GA4 APIが 403 Forbidden サービスアカウントがプロパティに追加されていない GA4管理画面 → プロパティへのアクセス で閲覧者として追加
PDFが空白 HTML描画エラー(JSの例外) プレイグラウンドにHTMLを貼り付けてブラウザコンソールで確認
cronが実行されない crontabのタイムゾーン設定が違う cron式の前に TZ=Asia/Tokyo を追加

レンダリング問題の詳細はHTML→PDFトラブルシューティングガイドを参照してください。

次のステップ

  • FUNBREW PDFプレイグラウンドでHTMLテンプレートを試す
  • HTMLテンプレートをパラメータ化してクライアント別ロゴ・カラーを適用する
  • GA4の追加dimensionsでトップページ別・チャネル別の内訳レポートを追加する
  • バッチPDF生成で50件以上のクライアントレポートを並列生成する
Powered by FUNBREW PDF