CLI for the Google Analytics 4 Data API — pull aggregate traffic reports from any GA4 property. Read-only, aggregate-only (no PII). An AFK plugin.
Wraps the GA Admin API (property discovery) and Data API (runReport / getMetadata) behind a Typer CLI. JSON by default (pipe into jq); --pretty for Rich tables. No hardcoded property — works with any GA4 property the authenticated Google account can see.
afk plugin install griffinwork40/ga-cliThis registers the ga skill, available in every AFK session. Install the Python dependencies once from the bundled requirements file:
pip install -r ~/.afk/plugins/ga-cli/skills/ga/scripts/requirements.txtRequires Python 3.11+.
git clone https://github.com/griffinwork40/ga-cli.git
cd ga-cli/skills/ga/scripts
pip install -r requirements.txt
# Optional: put `ga` on your PATH
ln -s "$(pwd)/ga" /usr/local/bin/ga
ga --help- Read-only: scope
analytics.readonlyonly — no write operations. - Aggregate-only: standard GA4 reports; no user-level / User-Explorer / PII.
- Property discovery:
ga propertieslists every GA4 property the authenticated account can see, with numeric IDs for use with--property. - Canned reports: acquisition channels, referrers (source/medium), top pages, geo breakdown, daily trend.
- Generic report:
ga report --metrics … --dimensions …runs any runReport query. - Metadata:
ga metadatadumps all available dimensions and metrics for a property. - Machine-friendly: compact JSON to stdout by default; errors are JSON on stderr with a nonzero exit code; stdout stays clean and pipeable even on failure.
--prettyrenders a Rich table for any command.
ga uses user OAuth (a Desktop OAuth client), read-only scope only. Full setup walkthrough: skills/ga/references/auth-setup.md.
In short:
- In Google Cloud Console, enable both the Google Analytics Admin API and the Google Analytics Data API.
- Create an OAuth client (Desktop app), download the JSON, and save it to:
~/.config/ga-cli/client_secret.json - Run the first-time consent flow:
A browser window opens. On the "Google hasn't verified this app" screen: Advanced → Go to <app> (unsafe) → allow read-only Analytics.
ga auth --reauth
- The refresh token is cached at
~/.config/ga-cli/token.json(chmod 0600). Subsequent commands refresh silently.
Token paths and overrides:
| Purpose | Default | Env override |
|---|---|---|
| OAuth client secret | ~/.config/ga-cli/client_secret.json |
GA_CLIENT_SECRET |
| Cached refresh token | ~/.config/ga-cli/token.json |
GA_TOKEN |
⚠️ Testing-mode tokens expire in ~7 days. If you plan to rungain a scheduled/headless context, either publish the OAuth consent screen (Testing → In production) or switch to a service account. Seeauth-setup.mdfor details.
ga properties List accessible GA4 properties (account + property name + numeric ID)
ga auth [--reauth] Load/refresh OAuth creds; --reauth forces fresh browser consent
ga report --metrics … Generic runReport (any metrics × dimensions)
ga acquisition Sessions + users by default channel group
ga referrers Sessions + users by source / medium (top referrers)
ga top-pages Page views + sessions by page path + title, top N
ga geo Sessions + users by country
ga trend Sessions + users by date (daily), ascending
ga metadata Dump all dimensions & metrics for the property
All commands accept --property <ID> (or set GA_PROPERTY_ID in the environment). Run ga properties to find the numeric ID for your property. All commands support --pretty for a Rich table.
No property ID is hardcoded. Before running data commands:
# Discover your property IDs:
ga properties | jq -r '.rows[] | [.property_id, .account, .property] | @tsv'
# Set for the session:
export GA_PROPERTY_ID=123456789
# Or pass per-command:
ga acquisition --property 123456789 --days 28# Acquisition channels — where traffic comes from:
ga acquisition --days 28 --pretty
# Total sessions + users across all channels:
ga acquisition --days 28 \
| jq '{sessions: ([.rows[].sessions|tonumber]|add), users: ([.rows[].totalUsers|tonumber]|add)}'
# Top referrers (source / medium):
ga referrers --days 28 --limit 10 \
| jq -r '.rows[] | [.sessions, .sessionSourceMedium] | @tsv'
# Top pages by view count:
ga top-pages --days 28 --limit 10 \
| jq -r '.rows[] | [.screenPageViews, .pagePath] | @tsv'
# Daily trend, last 14 days:
ga trend --days 14 | jq -r '.rows[] | [.date, .sessions, .totalUsers] | @tsv'
# Arbitrary report — any metric × dimension:
ga report --metrics sessions,totalUsers \
--dimensions sessionDefaultChannelGroup \
--days 28 --order-by -sessions --pretty
# Discover available metrics + dimensions:
ga metadata | jq '{dims: .dimension_count, metrics: .metric_count}'
ga metadata | jq -r '.metrics[] | select(.api_name|test("user";"i")) | .api_name'{
"property_id": "123456789",
"date_range": {"start": "28daysAgo", "end": "today"},
"dimensions": ["sessionDefaultChannelGroup"],
"metrics": ["sessions", "totalUsers"],
"row_count": 8,
"rows": [
{"sessionDefaultChannelGroup": "Organic Social", "sessions": "4921", "totalUsers": "4739"}
]
}Metric values are strings (GA4 API returns them as strings) — use tonumber in jq before arithmetic.
ga-cli/
├── .claude-plugin/plugin.json # AFK plugin manifest
├── skills/ga/
│ ├── SKILL.md # skill manifest (agent-facing usage)
│ ├── scripts/
│ │ ├── ga # bash entrypoint (SCRIPT_DIR-relative)
│ │ ├── requirements.txt # pinned Python deps
│ │ └── ga_cli/ # Python package
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── app.py # Typer root + error handling
│ │ ├── client.py # auth, property resolution, output helpers
│ │ ├── auth.py # `ga auth`, `ga properties`
│ │ └── report.py # `ga report` + canned reports
│ └── references/
│ └── auth-setup.md # GCP OAuth setup + 7-day token caveat
├── README.md
└── LICENSE
MIT © 2026 Griffin Long