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¶
- Completed Connect Google Analytics to Your AI Agent
- Node scripting and basic scheduling
π οΈ System Requirements¶
- Node.js 18+, the
ghCLI 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(orcrontab -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.