This note documents how the Site Analytics MicroSim (https://dmccreary.github.io/intelligent-textbooks/sims/site-analytics/) is refreshed and republished automatically once a month using the Claude Code Routines (scheduled-tasks) feature.
The MicroSim renders a ranked bar chart of Google Analytics (GA4) page views over the last 90 days for every published intelligent textbook. The chart is driven by a CSV; this routine regenerates that CSV and redeploys the site so the numbers — and the "Last Update" date shown at the top of the chart — stay current without anyone remembering to do it by hand.
| Routine ID | refresh-site-analytics-report |
| Schedule | First day of every month at 9:00 AM local time |
| Cron | 0 9 1 * * (5-field cron, evaluated in local time, not UTC) |
| Where it's stored | ~/.claude/scheduled-tasks/refresh-site-analytics-report/SKILL.md |
| Where to manage it | the Scheduled section in the Claude Code sidebar |
| Repo it operates on | ~/Documents/ws/intelligent-textbooks |
| Runs | fully autonomously — report → verify → commit → push → gh-deploy |
A "Routine" is a saved prompt that Claude Code runs on a cron schedule, locally
on this Mac (not in a remote cloud sandbox). That locality matters here: the
job needs the local service-account key, the local conda environments, and read
access to every ~/Documents/ws/*/mkdocs.yml — none of which a cloud runner
would have.
Key behaviors:
- Each run starts fresh with no memory of the conversation that created it, so the routine's prompt is fully self-contained.
- It runs while the Claude Code app is open. If the app is closed at 9:00 AM on the 1st, the run fires the next time the app is launched — so it is never skipped, only (occasionally) delayed.
- A small deterministic dispatch delay (a few minutes) is applied to recurring tasks to balance load.
- Tool approvals are remembered. The first time the routine runs
git pushandmkdocs gh-deploy, Claude asks for permission. Approving once (e.g. via Run now) stores the approval on the task so future unattended runs don't pause on a prompt. - Manage it from the Scheduled sidebar: Run now, pause/resume, edit the schedule or prompt, or delete.
-
Run the report with the
google-analyticsconda env's Python and--refresh(so newly-registered textbooks get mapped):/usr/local/Caskroom/miniforge/base/envs/google-analytics/bin/python \ src/site-analytics/ga4-pageviews-report.py --refresh --csvThe script scans every
~/Documents/ws/*/mkdocs.ymlfor aproperty: G-XXXXXmeasurement ID, maps each to its numeric GA4 property via the Analytics Admin API (cached inga4-property-map.json), then queries the Analytics Data API forscreenPageViewsover the last 90 days. It writessrc/site-analytics/ga4-pageviews-results.csv(and refreshes the property-map cache). -
Sanity-check the output before publishing — the CSV must exist, its header must contain
page_viewsandgenerated, it must have > 10 data rows, and the page-view total must be > 0. If the report fails or the CSV looks wrong, the run stops without committing or deploying and reports the error. -
Copy the refreshed CSV into the MicroSim directory:
cp src/site-analytics/ga4-pageviews-results.csv \ docs/sims/site-analytics/ga4-pageviews-results.csv
-
Publish — stage only the three data files (never a blanket
git add -A), commit, push, and deploy with themkdocsconda env:git add docs/sims/site-analytics/ga4-pageviews-results.csv \ src/site-analytics/ga4-pageviews-results.csv \ src/site-analytics/ga4-property-map.json git commit -m "Refresh site-analytics GA4 page-views data (automated monthly run)" git push conda activate mkdocs && mkdocs gh-deploy -m "Refresh site-analytics data [skip ci]"If nothing is staged (data unchanged), it skips the commit/deploy.
-
Self-heal on divergence. If
git pushis rejected as non-fast-forward, it runsgit pull --rebaseand retries. Ifmkdocs gh-deployis rejected pushing togh-pages, it syncs the local branch (git fetch origin gh-pages && git branch -f gh-pages origin/gh-pages) and retries once. -
Report a short summary: number of textbooks, total page views, and whether the MicroSim was published.
These must exist on the machine for the routine to succeed (they are already set up):
- Two conda environments:
google-analytics— runs the report. Hasgoogle-analytics-dataandgoogle-analytics-admininstalled.mkdocs— runsmkdocs gh-deploy. Hasmkdocs+mkdocs-material.
- A GA service account key at
~/.config/gcloud/textbook-analytics-key.json(chmod600). The script auto-detects it at that path, or honorsGOOGLE_APPLICATION_CREDENTIALS. - The service account
textbook-analytics-reader@textbook-analytics.iam.gserviceaccount.commust be a Viewer at the GA account level so it can see every property under the account in one grant. - Both the Analytics Admin API and Analytics Data API enabled in the
textbook-analyticsGoogle Cloud project. gitpush credentials forgithub.com/dmccreary/*.
⚠️ Never commit the service-account key. It is gitignored (*-key.json) and the repos are public. If it is ever exposed, rotate it in the Cloud Console.
Path (relative to intelligent-textbooks/) |
Role |
|---|---|
src/site-analytics/ga4-pageviews-report.py |
The report generator (scan → map → query → CSV). |
src/site-analytics/ga4-property-map.json |
Cache of G-XXXXX → numeric property ID. |
src/site-analytics/ga4-pageviews-results.csv |
Report output. Columns: rank, slug, measurement_id, numeric_id, page_views, generated. |
src/site-analytics/README.md |
Full setup + troubleshooting reference. |
docs/sims/site-analytics/ga4-pageviews-results.csv |
The MicroSim's copy of the data (what the chart fetches). |
docs/sims/site-analytics/site-analytics.js |
Chart.js sketch; reads the CSV and shows the Last Update date from the generated column. |
docs/sims/site-analytics/index.md |
Lesson page that embeds the chart in an iframe. |
The "Last Update: DD/MM/YYYY" line on the chart comes from the CSV's
generated column, which the report stamps with its run date — so it always
reflects when the report last ran, not when the site last deployed.
Stored verbatim at
~/.claude/scheduled-tasks/refresh-site-analytics-report/SKILL.md:
---
name: refresh-site-analytics-report
description: Automated monthly run: refresh the GA4 site-analytics page-views report and publish the MicroSim (report, commit, push, gh-deploy)
---
Automated MONTHLY job — run fully autonomously, end to end. Objective: refresh the
Google Analytics page-views data that powers the "Site Analytics" MicroSim ...
(STEP 1 run report --refresh --csv; STEP 2 sanity-check; STEP 3 copy CSV;
STEP 4 commit only the 3 data files, push, mkdocs gh-deploy; STEP 5 handle
non-fast-forward divergence; STEP 6 report a summary)(The full prompt lives in that file — edit it there or via Scheduled → edit.)
- Run it now / pre-approve permissions: Scheduled sidebar → the task →
Run now. Do this once so
git push/gh-deployapprovals are saved. - Change the schedule or prompt: edit it in the Scheduled sidebar (or ask Claude: "update my refresh-site-analytics-report routine to …").
- Pause / resume: toggle it in the Scheduled sidebar.
- Cron cheatsheet:
0 9 1 * *= 09:00 on day-of-month 1.0 9 * * 1= 09:00 every Monday.0 9 1 */3 *= 09:00 on the 1st every 3rd month.
From ~/Documents/ws/intelligent-textbooks:
conda activate google-analytics
python src/site-analytics/ga4-pageviews-report.py --csv # add --refresh after adding new books
cp src/site-analytics/ga4-pageviews-results.csv docs/sims/site-analytics/
conda activate mkdocs
git add docs/sims/site-analytics/ga4-pageviews-results.csv src/site-analytics/ga4-pageviews-results.csv
git commit -m "Refresh site-analytics page-views data"
git push
mkdocs gh-deploy| Symptom | Cause / fix |
|---|---|
| Routine never fires | Claude Code app was closed at the scheduled time — it runs on next launch. |
| Run pauses asking permission | First-time git push / gh-deploy approval — click Run now once to store it. |
DefaultCredentialsError |
Key missing at ~/.config/gcloud/textbook-analytics-key.json or GOOGLE_APPLICATION_CREDENTIALS unset. |
403 SERVICE_DISABLED |
Admin/Data API not enabled in the textbook-analytics Cloud project. |
| A book is skipped ("no numeric ID found") | Its property is on a GA account the service account can't see — grant Viewer there, then run with --refresh. |
push / gh-deploy rejected (non-fast-forward) |
Remote moved ahead — git pull --rebase, or for gh-pages git fetch origin gh-pages && git branch -f gh-pages origin/gh-pages, then retry. |
All bars render as 0 |
The CSV header lost the page_views column name (e.g. a stray carriage return). Regenerate via the Python report, which writes a clean CSV. |
See src/site-analytics/README.md in the repo for the full setup guide.