Skip to content

Mondays at 09:00 β€” note the absolute paths and env var

πŸ“– The Legend Behind This Quest

A report you have to remember to run is a report you never run. In this final quest of the path you'll turn the on-demand queries from the last quest into a hands-off weekly digest β€” a small script the scheduler runs for you, dropping a Markdown summary on disk and opening a GitHub issue so the numbers show up where you already work.

🎯 Quest Objectives

Primary Objectives (Required for Quest Completion)

  • Write a standalone Node script that queries GA and writes a Markdown digest
  • Deliver it as a dated file and a GitHub issue
  • Schedule it weekly with launchd (macOS) or cron (Linux)

Secondary Objectives (Bonus Achievements)

  • Filter the report to production traffic only
  • Add a week-over-week comparison

Mastery Indicators

  • You know why a cloud scheduler can't reach a local key
  • Your job uses absolute paths and a defined environment

πŸ—ΊοΈ Quest Prerequisites

πŸ“‹ Knowledge Requirements

πŸ› οΈ System Requirements

  • Node.js 18+, the gh CLI authenticated, and a scheduler

πŸ—οΈ Chapter 1: A Self-Contained Digest Script

The script reads the same service-account key from ~/.config/gcloud/ (never the repo), queries GA, and writes Markdown. Keep it standalone so it doesn't depend on your agent being online.

const fs = require("fs");
const os = require("os");
const path = require("path");
const { execFileSync } = require("child_process");
const { BetaAnalyticsDataClient } = require("@google-analytics/data");

const key = require(path.join(os.homedir(), ".config/gcloud/ga-reader.json"));
const client = new BetaAnalyticsDataClient({
  credentials: { client_email: key.client_email, private_key: key.private_key },
});
const property = "properties/" + process.env.GA_PROPERTY_ID;
const HOST = { filter: { fieldName: "hostName",
  stringFilter: { matchType: "EXACT", value: "your-site.dev" } } };

async function main() {
  const [r] = await client.runReport({
    property,
    dimensionFilter: HOST,
    dateRanges: [{ startDate: "7daysAgo", endDate: "today" }],
    metrics: [{ name: "activeUsers" }, { name: "sessions" }, { name: "screenPageViews" }],
  });
  const m = (r.rows?.[0]?.metricValues || []).map(v => v.value);
  const today = new Date().toISOString().slice(0, 10);
  const md = `# Weekly traffic β€” week ending ${today}\n\n` +
    `- Active users: ${m[0] ?? 0}\n- Sessions: ${m[1] ?? 0}\n- Page views: ${m[2] ?? 0}\n`;

  const dir = path.join(os.homedir(), "ga-reports");
  fs.mkdirSync(dir, { recursive: true });
  const file = path.join(dir, `report-${today}.md`);
  fs.writeFileSync(file, md);

  // Deliver as a GitHub issue (gh must be authenticated)
  execFileSync("/opt/homebrew/bin/gh", ["issue", "create", "--repo", "you/your-repo",
    "--title", `Weekly traffic β€” ${today}`, "--body-file", file], { stdio: "inherit" });
}
main().catch(e => { console.error(e.message); process.exit(1); });

Install the one dependency locally (outside the repo) and test it once:

npm install --prefix ~/.local/share/ga-report @google-analytics/data
GA_PROPERTY_ID=123456789 node ~/.local/share/ga-report/ga-weekly-report.js

⏰ Chapter 2: Schedule It (macOS β€” launchd)

A LaunchAgent runs the script on a calendar interval. Use absolute paths β€” scheduled jobs don't inherit your interactive shell's PATH.

<!-- ~/Library/LaunchAgents/com.you.ga-weekly-report.plist -->
<dict>
  <key>Label</key><string>com.you.ga-weekly-report</string>
  <key>ProgramArguments</key>
  <array>
    <string>/opt/homebrew/bin/node</string>
    <string>/Users/you/.local/share/ga-report/ga-weekly-report.js</string>
  </array>
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key><string>/opt/homebrew/bin:/usr/bin:/bin</string>
    <key>GA_PROPERTY_ID</key><string>123456789</string>
  </dict>
  <key>StartCalendarInterval</key>
  <dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer>9</integer></dict>
</dict>
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.you.ga-weekly-report.plist
launchctl print gui/$(id -u)/com.you.ga-weekly-report | grep -i "state\|calendar"

Linux β€” cron

# Mondays at 09:00 β€” note the absolute paths and env var
0 9 * * 1 GA_PROPERTY_ID=123456789 /usr/bin/node /home/you/.local/share/ga-report/ga-weekly-report.js

☁️ Chapter 3: Why Not a Cloud Scheduler?

It's tempting to run this as a hosted cron, but the script needs the local service-account key file. A cloud runner won't have ~/.config/gcloud/ga-reader.json, so the job belongs on a machine that holds the key β€” a local launchd/cron job, or a CI runner with the key injected as a secret (never committed). Pick whichever matches where your key safely lives.

βœ… Validation

  • Running the script by hand writes a dated digest and opens an issue
  • launchctl print (or crontab -l) shows the job registered
  • The report contains production-only numbers

πŸ† Rewards

  • πŸ† Automation Architect β€” reporting now runs itself
  • ⏰ Scheduler Sage β€” launchd/cron hold no mysteries

πŸŽ“ Path Complete

You've connected Google Analytics to your agent, learned to query it, and automated the reporting. Loop back to the series hub for more Monitoring & Observability quests β€” or read the full story in the post The first thing analytics told me was that the analytics were wrong.