[CODE]

Automating GA4 Weekly Reports with AI for Multiple Clients

I built a multi-site engine that pulls GA4 data every Monday, runs it through Claude, and commits a Markdown report to the repo. Each site gets context-aware analysis without me touching anything.

6 min read
ga4 claude typescript automation

Most of my clients are small family-run B&Bs in Taiwan. They compete for the same pool of tourist bookings, but they’re fighting that battle without an IT team, without a data analyst, and in many cases, without ever opening GA4.

That last point isn’t ignorance — it’s a rational response to complexity. GA4 is genuinely confusing if no one has explained to you what “session,” “conversion,” or “channel” means in the context of your specific business. So the data sits there, collecting dust, while booking decisions get made on gut feel and word of mouth.

The problem I kept running into: I’d set up GA4 for a client, connect their Google Ads, configure the right conversion events — and then six weeks later, they’d have no idea what their own data was telling them. They weren’t using it.

So I built them a different interface to it.

What the System Does

Every Monday morning, an automated agent pulls GA4 data for each B&B, sends it through Claude for analysis, and produces a plain-language Markdown report. The report lands in the repo on a schedule. My clients don’t log in to anything. They don’t need to know what a “bounce rate” is. They get a weekly summary in Chinese, framed around the questions they actually care about: Did more people look at the booking page this week? Did the Qingming holiday affect traffic? Is our Google Ads spend doing anything?

The change in practice: before this system, most of them weren’t looking at their data at all. After it, data review became something that happened to them every Monday, not something they had to remember to do.

The Engine: cometrue-bnb_analysis_agent

The engine lives in a repo called cometrue-bnb_analysis_agent. The name is a relic — it started as one site, then I generalized it. The structure is simple:

sites/
  cometrue-bnb/
    business-context.md
    .env
  other-site/
    business-context.md
    .env
out/
  cometrue-bnb/
    data.json
    insights.json

Each site folder holds two things: a business-context.md that describes what the site actually is (GA4 property ID, conversion events, KPIs, seasonal patterns, key questions the owner wants answered), and a .env with API credentials.

The engine has three stages: fetch → analyze → render.

Fetch

The fetch stage calls GA4 Data API v1alpha plus Search Console and Ads where connected. Output goes to out/<site>/data.json. Nothing special here, but the important thing is that I’m not dumping raw GA4 JSON at Claude. I normalize it into a structure that includes week-over-week and month-over-month diffs already computed.

interface WeeklyMetrics {
  sessionCount: { current: number; wowDelta: number; momDelta: number };
  conversionRate: { current: number; wowDelta: number; momDelta: number };
  topChannels: Array<{ channel: string; sessions: number; conversions: number }>;
  // ...
}

The rationale: Claude shouldn’t be doing arithmetic on raw numbers. It should be interpreting trends. If I hand it { current: 142, wowDelta: -18 } instead of two separate numbers, the prompt stays shorter and the model spends tokens on reasoning rather than subtraction.

Analyze

This is where Claude comes in. The analyze stage loads business-context.md for the site, appends the normalized JSON, and sends it through the API.

The prompt structure matters a lot. Early on I was getting generic “traffic declined 12% WoW” type outputs that anyone could generate by reading the numbers. Not useful. What I actually wanted was something like: “The drop is concentrated in organic search, which is consistent with the Qingming holiday — paid traffic held steady, so this isn’t a campaign issue.”

To get there I had to:

  • Pass the holiday calendar for Taiwan (embedded in the system prompt)
  • Include sample size warnings when conversion event counts were below a threshold
  • Tell it about the specific conversion events for each site and what they mean for that business
const systemPrompt = `
You are analyzing weekly performance for ${site.name}.
Business context: ${businessContext}

Rules:
- If conversion event count < 30, note sample size before drawing conclusions
- Taiwan public holidays this week: ${holidaysThisWeek.join(', ') || 'none'}
- Do not attribute channel changes to seasonality without checking holiday context first
- WoW and MoM deltas are pre-computed — use them directly
`;

The holiday awareness came after one report confidently called a traffic drop “a sign of weakening SEO performance” when it was just Lunar New Year. After that I started treating Claude less like an oracle and more like a junior analyst who needs context to do good work — the same as any analyst you’d actually hire.

The business-context.md file is where the per-client knowledge lives. One B&B cares about booking form submissions; another cares about phone call clicks. One has strong repeat customers from Instagram; another depends almost entirely on Google search. These differences matter for interpreting the same underlying numbers, and they can’t be generic.

Render and Commit

The render stage takes insights.json and formats it into a Markdown file at sites/<site>/reports/YYYY-Www.md. Then the GitHub Action commits it.

- name: Commit reports
  run: |
    git config user.email "actions@github.com"
    git config user.name "GA4 Bot"
    git add sites/*/reports/
    git diff --staged --quiet || git commit -m "chore(reports): weekly GA4 analysis $(date +%Y-W%V)"
    git push

I like having the reports in the repo rather than email or Slack. It means the history is diffs — you can see exactly what the AI said week over week, and if a report was weirdly off you can look back at the input data.

That said, the next step for some clients is routing the Markdown to a WeChat message or LINE notification — something they’d actually see without me having to explain what a GitHub repository is.

Adding a New Site

The part I’m most happy about is the onboarding time. To add a new site, you copy an existing site folder, edit business-context.md with the new property ID and KPIs, drop in a .env, and you’re done in about five minutes. The engine itself never changes.

This matters operationally. I’m not just building tools for myself — I’m building the infrastructure that lets me take on another B&B client without proportionally adding to my own workload. The business context file is the entire cost of adding a new client to the system.

Where It Stands

I’ve been running this for a few months now. The reports aren’t perfect — sometimes Claude hedges when I want a clear read, and the formatting is still something I tweak occasionally. But the core thing works: data that was being ignored is now being seen, in a format that makes sense to the people who own that data.

That’s a different kind of problem from the engineering one, and in some ways it’s the harder one to solve. The engineering took a few days. Getting a B&B owner to trust a weekly report enough to act on it takes longer.