April 27, 2026

Export Google Analytics Data to PDF Reports via API

reportsGoogle AnalyticsautomationNode.js

Google Analytics 4 dashboards are great for real-time monitoring, but stakeholders often need a formatted PDF they can read offline, store in a shared drive, or attach to a weekly email digest. The GA4 UI has no built-in PDF export, and copying data into slide decks manually is not scalable.

This guide shows you how to automate the full pipeline: fetch metrics from the GA4 Data API, render them with Chart.js, and generate a branded PDF using the FUNBREW PDF API — in Node.js and Python.

Architecture Overview

GA4 Data API  →  Node.js/Python  →  HTML template  →  FUNBREW PDF API  →  Email / S3
  1. Fetch GA4 metrics via the googleapis (Node.js) or google-analytics-data (Python) client
  2. Render metrics and charts into an HTML template
  3. Convert HTML to PDF using the FUNBREW PDF REST API
  4. Deliver by email or upload to cloud storage

This pattern is covered in more detail in the PDF report generation guide and in the automation use cases.

Step 1 — Set Up GA4 Data API Access

Create a service account in Google Cloud Console, grant it Viewer access to your GA4 property, and download the JSON credentials file.

npm install googleapis @google-analytics/data nodemailer

Step 2 — Fetch GA4 Metrics (Node.js)

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

// Point to your service account credentials
const analyticsClient = new BetaAnalyticsDataClient({
  keyFilename: path.join(__dirname, 'ga4-credentials.json'),
});

const GA4_PROPERTY_ID = 'properties/XXXXXXXX'; // replace with your property 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 — Build the HTML Report Template

The template combines a KPI summary table with a Chart.js line chart. Because FUNBREW PDF renders with headless Chromium, Chart.js works out of the box.

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="en">
<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: -apple-system, 'Segoe UI', sans-serif; font-size: 13px;
           color: #1e293b; background: #fff; padding: 32px; }
    h1 { font-size: 22px; 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: 26px; font-weight: 700; color: #0f172a; }
    .chart-wrap { margin-bottom: 24px; }
    h2 { font-size: 15px; font-weight: 600; margin-bottom: 12px; }
    canvas { max-height: 220px; }
    @page { size: A4; margin: 15mm; }
    @media print { body { padding: 0; } }
  </style>
</head>
<body>
  <h1>Monthly Analytics Report</h1>
  <div class="period">${reportPeriod}</div>

  <div class="kpi-grid">
    <div class="kpi"><div class="kpi-label">Sessions</div>
      <div class="kpi-value">${totals.sessions.toLocaleString()}</div></div>
    <div class="kpi"><div class="kpi-label">Users</div>
      <div class="kpi-value">${totals.users.toLocaleString()}</div></div>
    <div class="kpi"><div class="kpi-label">Pageviews</div>
      <div class="kpi-value">${totals.pageviews.toLocaleString()}</div></div>
    <div class="kpi"><div class="kpi-label">Avg Bounce Rate</div>
      <div class="kpi-value">${avgBounce}%</div></div>
  </div>

  <div class="chart-wrap">
    <h2>Sessions &amp; Users — Daily Trend</h2>
    <canvas id="trendChart"></canvas>
  </div>

  <script>
    new Chart(document.getElementById('trendChart'), {
      type: 'line',
      data: {
        labels: ${JSON.stringify(labels)},
        datasets: [
          { label: 'Sessions', data: ${JSON.stringify(sessionData)},
            borderColor: '#6366f1', backgroundColor: 'rgba(99,102,241,.1)',
            tension: 0.3, fill: true },
          { label: 'Users', 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 — Convert HTML to PDF with FUNBREW 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;">' +
        'Page <span class="pageNumber"></span> of <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 error ${res.statusCode}`));
        });
      }
    );
    req.on('error', reject);
    req.write(payload);
    req.end();
  });
}

See the FUNBREW PDF documentation for a full list of options, including watermarks, custom headers, and password protection.

Step 5 — Wire It Together and Email the PDF

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

async function generateAndSendReport() {
  // Last calendar month
  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(`Fetching GA4 data: ${period}`);
  const metrics = await fetchMonthlyMetrics(startDate, endDate);

  console.log('Rendering HTML template…');
  const html = buildReportHTML(metrics, period);

  console.log('Generating PDF…');
  const pdfBuffer = await htmlToPdf(html);

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

  // Email
  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, // comma-separated
    subject: `Analytics Report — ${period}`,
    text: `Please find the monthly analytics report attached.`,
    attachments: [{ filename, content: pdfBuffer }],
  });

  console.log(`Report sent: ${filename}`);
}

generateAndSendReport().catch(console.error);

Python Version

import os
import json
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders
from datetime import date, timedelta
from calendar import monthrange
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;">'
                    'Page <span class="pageNumber"></span> of '
                    '<span class="totalPages"></span></div>'
                ),
                "marginTop": "15mm",
                "marginBottom": "20mm",
            },
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.content

Schedule the Report (Cron)

Add this to your server's crontab (crontab -e) to run on the 1st of every month at 8 AM:

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

For cloud environments, use AWS EventBridge Scheduler or Google Cloud Scheduler to trigger an HTTP endpoint that runs the same logic. For multi-client agencies generating one report per client, see the bulk PDF generation guide.

Common Issues

Problem Cause Fix
Chart missing in PDF Chart.js animation not disabled Add animation: false to Chart.js config
403 Forbidden from GA4 API Service account not added to property Add service account email as Viewer in GA4 Admin → Property Access
PDF is blank HTML rendering error (JS exception) Check browser console in the playground by pasting the HTML
Cron not firing Wrong timezone in crontab Add TZ=UTC before the cron expression or use crontab with explicit timezone

For more rendering issues, see the HTML to PDF troubleshooting guide.

Next Steps

  • Try the template in the FUNBREW PDF playground
  • Add per-client branding (logo, colors) by parameterizing the HTML template
  • Extend with top-page or channel breakdowns using additional GA4 dimensions
  • Explore batch PDF generation to generate 50+ client reports in parallel
Powered by FUNBREW PDF