← Back to Plugins
Tools

Plugin Ga4

anatoli-iliev By anatoli-iliev 👁 31 views ▲ 0 votes

Read-only, privacy-respecting Google Analytics 4 access for OpenClaw agents. One runtime dependency, three allowed hosts, one OAuth scope.

Homepage GitHub

Install

npm install
npm

Configuration Example

{
  "plugins": {
    "entries": {
      "ga4": {
        "enabled": true,
        "config": {
          "credentials": "~/.openclaw/credentials/ga4.json",
          "propertyId": "123456789"
        }
      }
    }
  }
}

README

# openclaw-plugin-ga4

Read-only Google Analytics 4 reporting for an OpenClaw agent, running on your own machine.

**GA4 Analytics** adds six tools to OpenClaw so an agent can answer questions about a GA4
property: what the top pages were last month, where traffic came from, what changed since
last year, who is on the site right now. It calls Google's REST API directly — no gateway,
no vendor account, no Python. It is for people who run OpenClaw locally, and who would
rather read a plugin's whole network surface than trust a description of it. That surface
is one runtime dependency, three allowed hosts, and one OAuth scope.

## What it does

| Tool | What it returns |
| --- | --- |
| `ga4_report` | One preset report — top pages, traffic sources, channels, countries, devices, events, ecommerce and more — as a markdown table. |
| `ga4_compare` | Two named periods side by side, with the change between them. |
| `ga4_realtime` | Active users in the last 30 minutes. The only tool that sees today. |
| `ga4_fields` | Searches the property's live dimension and metric catalog and returns exact API names. |
| `ga4_query` | Full control: your own dimensions, metrics, filters and sort order. |
| `ga4_diagnose` | Runs the setup checks in dependency order, names the first failure and its fix, and lists the properties this credential can read. |

Things you can ask in plain English:

- "What were my top pages last month?"
- "How did the last 28 days compare with the 28 days before that?"
- "Which channels drove the most key events this year?"
- "Is anyone on the site right now?"

Presets carry dimension and metric names that are known to work together, so the model
names an intent instead of guessing that "pageviews" is spelled `screenPageViews` and that
"conversions" is now `keyEvents`. When no preset fits, `ga4_query` takes the fields directly.

## Install

No release has been published to npm yet, so there is nothing to install from the registry
today. Once the first release is out, this is the command:

```bash
openclaw plugins install openclaw-plugin-ga4
```

Until then, clone this repository and run `npm install`, then `npm run build`. The entry
point is the built `dist/index.js`, which `package.json` declares under
`openclaw.extensions`.

Requires OpenClaw `>=2026.7.1`. Then add a config entry:

```json
{
  "plugins": {
    "entries": {
      "ga4": {
        "enabled": true,
        "config": {
          "credentials": "~/.openclaw/credentials/ga4.json",
          "propertyId": "123456789"
        }
      }
    }
  }
}
```

`credentials` is optional. Without it the plugin looks at `GOOGLE_APPLICATION_CREDENTIALS`,
then at `~/.config/gcloud/application_default_credentials.json`, in that order — so if you
already authenticated for another Google tool, you are already set up.

## Setup

Five steps here, eight in the click-by-click version — about ten minutes.
[SETUP.md](SETUP.md) has it with console links.

1. **Create a Google Cloud project** and enable the Google Analytics Data API. Enable the
   Admin API too if you want `ga4_diagnose` to list your properties by name; reports work
   without it.
2. **Create a service account.** Skip the "grant this service account access to project"
   step — a Cloud IAM role does nothing for GA4 access.
3. **Download a JSON key**, move it somewhere private, and `chmod 600` it. That file is a
   password.
4. **Grant the service account read access in Google Analytics.** This is the step
   everyone gets wrong. Copy the `client_email` from the key file — it looks like
   `[email protected]` — then open Google Analytics, go to
   **Admin > Property access management**, click **+ > Add users**, paste that address,
   untick "Notify new users by email", and give it **Viewer** and nothing else. Access is
   granted here, inside Analytics, not in Google Cloud. Skipping this produces a 403 that
   says nothing useful; `ga4_diagnose` translates it.
5. **Point the plugin at the key and the property id**, then run `ga4_diagnose`. The
   property id is the 9–10 digit number under **Admin > Property details** — not the
   `G-XXXXXXXXXX` measurement ID from your site's tag. Paste the measurement ID and the
   plugin will tell you which number you actually need.

## Why this one

- **One runtime dependency.** `typebox`, which OpenClaw already depends on and which has no
  dependencies of its own. OAuth is a signed JWT built with `node:crypto`; the API is called
  with `fetch`. No gRPC stack, no generated protos, no `google-auth-library`, no Python.
  You can read the whole client in one sitting.
- **An egress allowlist enforced in code.** Every request goes through one guard that
  rejects any host other than `oauth2.googleapis.com`, `analyticsdata.googleapis.com` and
  `analyticsadmin.googleapis.com`, before the request is made, and redirects are an error
  rather than something to follow, so a 302 cannot reach a host that was never checked. A
  test scans the built bundle for `https://` URLs and fails on any host that is neither one
  of those three nor on a short reviewed list of addresses that only ever appear as text:
  `console.cloud.google.com` and `console.developers.google.com`, which are the "enable the
  API here" link printed in an error message, and `www.googleapis.com`, which appears
  inside the OAuth scope identifier. Your credentials go to Google and to no third-party
  service. The allowlist constrains which host the plugin asks for, not what your system
  does with the request: if OpenClaw or your machine is configured with an HTTP proxy or a
  TLS-intercepting middlebox, this traffic traverses it exactly as all other OpenClaw
  traffic does.
- **Read-only by scope.** The only scope requested is
  `https://www.googleapis.com/auth/analytics.readonly`, and a test asserts no other
  `googleapis.com/auth/` string exists in the shipped bundle.
- **No telemetry, and no report data on disk.** No phone-home, no update check, no usage
  counter. Tokens are held in memory only — a cached Google access token in a file is a
  credential at rest that you did not agree to.
- **Errors that name the fix.** A missing property grant becomes "add this address in
  Admin > Property access management with the Viewer role". A disabled API becomes the
  console link that enables it. A machine whose clock has drifted becomes "turn on network
  time sync" instead of `invalid_grant`. Diagnosis reads Google's machine-readable status
  and reason fields, never its error prose.
- **Reports say what they do not mean.** Sampling, Google's minimum-aggregation
  thresholding, `(other)`-row rollups, restricted metrics and the property's own time zone
  each produce one plain sentence, and only when the flag is actually set.

Verified comparisons, each backed by a fetched artifact:

| Project | Verified fact | Here instead |
| --- | --- | --- |
| [`adamkristopher/ga4-api-toolkit`](https://github.com/adamkristopher/ga4-api-toolkit) | `src/api/reports.ts` `runReport()` accepts `filters` and `orderBy`, then never uses them when building the request — a filtered question returns whole-site numbers with no error. It also defaults `save=true`, writing results to disk. | A parameter this plugin cannot honour raises an error naming why, rather than being dropped — asking `overview` for a filter says that report has no dimension to filter on. Ranked presets carry an explicit sort; single-row summaries do not, because sorting one row means nothing. Nothing writes report data to disk. |
| [`jdrhyne/agent-skills`](https://github.com/jdrhyne/agent-skills) GA4 skill | `skills/ga4/SKILL.md` declares `requires: {"bins": ["python3"]}` and every example runs `python3 scripts/ga4_query.py`. Those scripts tell you to `pip install google-analytics-data google-auth-oauthlib` on the host. | A TypeScript plugin that runs in the OpenClaw process. No Python, no `pip`, no shell tool. |

## Analytics data is untrusted input

GA4 dimension values are not written by you. They are written by whoever visited your site.
Anyone can open `yoursite.com/?<any text they like>` and that text lands in `pagePath` and
`pagePathPlusQueryString` in tomorrow's report. The same goes for `pageTitle`,
`pageReferrer`, `landingPage`, `sessionCampaignName` and every other visitor-derived or
marketer-written field. Referrer spam has been doing this to Google Analytics for a decade
for SEO reasons. An agent that reads those values turns it into a prompt-injection channel.

This plugin does two things about it:

1. Every tool registers with `resultContentSource: "network"`, OpenClaw's marker for
   externally controlled content. The field landed in OpenClaw 2026.8; the newest released
   host today is 2026.7.1-2, which does not read it, so there the field is emitted and
   ignored. It is set unconditionally so the labelling takes effect when you upgrade. Until
   then the framing below is what separates visitor-authored text from trusted output.
2. Report rows are rendered inside a fenced block introduced as data supplied by site
   visitors, rather than interpolated into prose. Every table cell of every tool has `|`
   escaped and newlines collapsed, so a value cannot forge table structure or start a line
   of its own. `ga4_fields` and `ga4_diagnose` list field, property and account names in a
   plain table without that fence.

This reduces risk. It does not eliminate prompt injection, and nothing does. If you wire an
agent to act on GA4 data — send email, file tickets, change bids — keep a human in that
loop. The numbers are trustworthy. The strings are not.

## Privacy

Full detail in [PRIVACY.md](PRIVACY.md). The short version:

- Every dimension value in a report passes through redaction before the model sees it,
  unless you set `privacy.redact` to `false`: emails, phone numbers, UUIDs, JWTs, card
  numbers confirmed by Luhn, long opaque tokens, and the values of query parameters outside
  a keep-list. The report says how many values

... (truncated)
tools

Comments

Sign in to leave a comment

Loading comments...