Google Analytics 4のデータをPDFレポートに自動変換するAPI連携ガイド
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
- 取得:
googleapis(Node.js)またはgoogle-analytics-data(Python)クライアントでGA4メトリクスを取得 - レンダリング: メトリクスとグラフをHTMLテンプレートに描画
- 変換: FUNBREW PDF REST APIでHTMLをPDFに変換
- 配信: メール送信またはクラウドストレージにアップロード
このパターンの詳細は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件以上のクライアントレポートを並列生成する