Back to skills
SkillHub ClubAnalyze Data & AIFull StackBackendData / AI

google-analytics

Query Google Analytics 4 (GA4) data directly via the Analytics Data API. Use when you need website analytics like top pages, traffic sources, sessions, users, conversions, bounce rate, or any GA4 metrics and dimensions. Supports custom date ranges, filtering, and multi-metric queries. Calls analyticsdata.googleapis.com directly with no third-party proxy.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,135
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B75.1

Install command

npx @skill-hub/cli install openclaw-skills-native-google-analytics

Repository

openclaw/skills

Skill path: skills/codeninja23/native-google-analytics

Query Google Analytics 4 (GA4) data directly via the Analytics Data API. Use when you need website analytics like top pages, traffic sources, sessions, users, conversions, bounce rate, or any GA4 metrics and dimensions. Supports custom date ranges, filtering, and multi-metric queries. Calls analyticsdata.googleapis.com directly with no third-party proxy.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Backend, Data / AI.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install google-analytics into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding google-analytics to shared team environments
  • Use google-analytics for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: google-analytics
description: "Query Google Analytics 4 (GA4) data directly via the Analytics Data API. Use when you need website analytics like top pages, traffic sources, sessions, users, conversions, bounce rate, or any GA4 metrics and dimensions. Supports custom date ranges, filtering, and multi-metric queries. Calls analyticsdata.googleapis.com directly with no third-party proxy."
metadata:
  openclaw:
    requires:
      env:
        - GA4_PROPERTY_ID
        - GOOGLE_CLIENT_ID
        - GOOGLE_CLIENT_SECRET
        - GOOGLE_REFRESH_TOKEN
      bins:
        - python3
    primaryEnv: GA4_PROPERTY_ID
    files:
      - "scripts/*"
---

# Google Analytics 4

Query GA4 properties directly via the Google Analytics Data API (`analyticsdata.googleapis.com`).

## Setup (one-time)

### 1. Create a Google Cloud project (or use an existing one)

Go to https://console.cloud.google.com and create or select a project.

### 2. Set the OAuth consent screen to Internal

Go to **APIs & Credentials > OAuth consent screen > Audience** and set:
- **User type**: Internal

This avoids Google's app verification process (which requires a demo video for sensitive scopes like Analytics). Internal is fine for personal/team use. Note: this requires a Google Workspace account (not a personal @gmail.com).

If you must use External (e.g. you have a personal Gmail), set publishing status to "In production" and add the `analytics.readonly` scope under **Data Access / Scopes**.

### 3. Add the Analytics scope

Go to **OAuth consent screen > Data Access** (or Scopes) and add:
```
https://www.googleapis.com/auth/analytics.readonly
```
This is listed as a "sensitive scope" by Google. If your app is Internal, no verification is needed.

### 4. Enable the Analytics Data API

Go to: https://console.cloud.google.com/apis/library/analyticsdata.googleapis.com

Click **Enable**.

### 5. Create OAuth 2.0 credentials

Go to **APIs & Credentials > Credentials > Create Credentials > OAuth client ID**
- Application type: **Desktop app**
- Name: anything you want

Save the **Client ID** and **Client Secret**.

### 6. Get your GA4 Property ID

Go to https://analytics.google.com > **Admin** (gear icon) > **Property Settings**. The Property ID is the numeric value at the top.

### 7. Generate a refresh token

Run this on your local machine (needs a browser for the Google login flow):

```bash
pip install google-auth-oauthlib
```

```bash
python3 -c "from google_auth_oauthlib.flow import InstalledAppFlow; flow = InstalledAppFlow.from_client_config({'installed': {'client_id': 'YOUR_CLIENT_ID', 'client_secret': 'YOUR_CLIENT_SECRET', 'auth_uri': 'https://accounts.google.com/o/oauth2/auth', 'token_uri': 'https://oauth2.googleapis.com/token'}}, scopes=['https://www.googleapis.com/auth/analytics.readonly']); creds = flow.run_local_server(port=0); print('REFRESH TOKEN:', creds.refresh_token)"
```

Replace `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` with your values. A browser window will open for you to log in with Google. Copy the refresh token from the output.

### 8. Set environment variables

```
GA4_PROPERTY_ID=123456789
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REFRESH_TOKEN=your-refresh-token
```

## Troubleshooting

- **403 HTML error page**: The `analytics.readonly` scope is probably not added to your OAuth consent screen. Go to Data Access/Scopes and add it, then regenerate your refresh token.
- **403 JSON error "caller does not have permission"**: Your Google account doesn't have access to the GA4 property. Check Admin > Property Access Management in Google Analytics.
- **Token refresh fails**: Your refresh token may be expired. Regenerate it using step 7.

## Queries

### Top pages by pageviews
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics screenPageViews \
  --dimension pagePath \
  --limit 20
```

### Top pages with sessions and users
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics screenPageViews,sessions,totalUsers \
  --dimension pagePath \
  --limit 20
```

### Traffic sources
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics sessions \
  --dimension sessionSource \
  --limit 20
```

### Traffic by source and medium
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics sessions,totalUsers,conversions \
  --dimensions sessionSource,sessionMedium \
  --limit 20
```

### Landing pages
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics sessions,bounceRate \
  --dimension landingPage \
  --limit 30
```

### Custom date range
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics screenPageViews,sessions \
  --dimension pagePath \
  --start 2026-01-01 \
  --end 2026-01-31 \
  --limit 20
```

### Filter by path prefix
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics screenPageViews,sessions \
  --dimension pagePath \
  --filter "pagePath=~/blog/" \
  --limit 20
```

### Conversions by campaign
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics conversions,sessions \
  --dimensions sessionCampaignName,sessionSource \
  --limit 20
```

### Device breakdown
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics sessions,totalUsers \
  --dimension deviceCategory \
  --limit 10
```

### Country breakdown
```bash
python3 /mnt/skills/user/google-analytics/scripts/ga4_query.py \
  --metrics sessions,totalUsers \
  --dimension country \
  --limit 20
```

## Common metrics
`screenPageViews`, `sessions`, `totalUsers`, `newUsers`, `activeUsers`, `bounceRate`, `averageSessionDuration`, `conversions`, `eventCount`, `engagementRate`, `userEngagementDuration`

## Common dimensions
`pagePath`, `pageTitle`, `landingPage`, `sessionSource`, `sessionMedium`, `sessionCampaignName`, `country`, `city`, `deviceCategory`, `browser`, `date`, `week`, `month`

## Output
Results are printed as a formatted table to stdout. Pipe to `| python3 -m json.tool` if you need raw JSON.


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# native-google-analytics

An OpenClaw skill that queries Google Analytics 4 (GA4) directly via the [Analytics Data API](https://developers.google.com/analytics/devguides/reporting/data/v1). No third-party proxies, no external APIs, no SDK wrappers. Just your credentials talking to `analyticsdata.googleapis.com`.

## What it does

Ask OpenClaw for analytics data in plain English and it runs the appropriate GA4 query. Top pages, traffic sources, device breakdowns, custom date ranges, filters, etc.

## What you need

- A Google Cloud project with the Analytics Data API enabled
- OAuth 2.0 credentials (Desktop app)
- A GA4 property you have access to

## Setup

### 1. Google Cloud project

Go to [console.cloud.google.com](https://console.cloud.google.com) and create or select a project.

### 2. OAuth consent screen

Go to **APIs & Credentials > OAuth consent screen > Audience**.

Set **User type** to **Internal**. This skips Google's app verification process (which requires a demo video for sensitive scopes). Internal works for personal and team use but requires a Google Workspace account.

If you're on a personal @gmail.com, set it to External with publishing status "In production" instead.

### 3. Add the Analytics scope

Go to **OAuth consent screen > Data Access** and add:

```
https://www.googleapis.com/auth/analytics.readonly
```

### 4. Enable the API

Go to [Analytics Data API](https://console.cloud.google.com/apis/library/analyticsdata.googleapis.com) and click **Enable**.

### 5. Create OAuth credentials

Go to **Credentials > Create Credentials > OAuth client ID**. Select **Desktop app**.

Save the Client ID and Client Secret.

### 6. Get your GA4 Property ID

Go to [analytics.google.com](https://analytics.google.com) > **Admin** > **Property Settings**. Copy the numeric Property ID.

### 7. Generate a refresh token

```bash
pip install google-auth-oauthlib
```

```bash
python3 -c "
from google_auth_oauthlib.flow import InstalledAppFlow
flow = InstalledAppFlow.from_client_config(
    {'installed': {
        'client_id': 'YOUR_CLIENT_ID',
        'client_secret': 'YOUR_CLIENT_SECRET',
        'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
        'token_uri': 'https://oauth2.googleapis.com/token'}},
    scopes=['https://www.googleapis.com/auth/analytics.readonly'])
creds = flow.run_local_server(port=0)
print('REFRESH TOKEN:', creds.refresh_token)
"
```

A browser window will open for Google login. Copy the refresh token from the output.

### 8. Set environment variables

```
GA4_PROPERTY_ID=123456789
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REFRESH_TOKEN=your-refresh-token
```

## Example queries

```bash
# Top pages
python3 scripts/ga4_query.py --metrics screenPageViews,sessions,totalUsers --dimension pagePath --limit 20

# Traffic sources
python3 scripts/ga4_query.py --metrics sessions --dimension sessionSource --limit 20

# Landing pages with bounce rate
python3 scripts/ga4_query.py --metrics sessions,bounceRate --dimension landingPage --limit 30

# Custom date range
python3 scripts/ga4_query.py --metrics screenPageViews,sessions --dimension pagePath --start 2026-01-01 --end 2026-01-31 --limit 20

# Filter by path
python3 scripts/ga4_query.py --metrics screenPageViews,sessions --dimension pagePath --filter "pagePath=~/blog/" --limit 20
```

## Available metrics

`screenPageViews`, `sessions`, `totalUsers`, `newUsers`, `activeUsers`, `bounceRate`, `averageSessionDuration`, `conversions`, `eventCount`, `engagementRate`, `userEngagementDuration`

## Available dimensions

`pagePath`, `pageTitle`, `landingPage`, `sessionSource`, `sessionMedium`, `sessionCampaignName`, `country`, `city`, `deviceCategory`, `browser`, `date`, `week`, `month`

## Troubleshooting

**403 HTML error page**: The `analytics.readonly` scope isn't added to your OAuth consent screen. Add it, then regenerate your refresh token.

**403 "caller does not have permission"**: Your Google account doesn't have access to the GA4 property. Check Admin > Property Access Management in Google Analytics.

**Token refresh fails**: Refresh token expired. Regenerate it using step 7.

```

### _meta.json

```json
{
  "owner": "codeninja23",
  "slug": "native-google-analytics",
  "displayName": "Native Google Analytics",
  "latest": {
    "version": "0.1.1",
    "publishedAt": 1771791027190,
    "commit": "https://github.com/openclaw/skills/commit/06cfe53ec1eace851e501edfdc0122ce15ea8327"
  },
  "history": []
}

```

### scripts/ga4_auth.py

```python
#!/usr/bin/env python3
"""
One-time OAuth flow to get a refresh token for GA4.
Run once, then store the printed refresh token as GOOGLE_REFRESH_TOKEN.
"""
import json
import os
import sys
import urllib.parse
import urllib.request

CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID")
CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET")

if not CLIENT_ID or not CLIENT_SECRET:
    print("Error: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set", file=sys.stderr)
    sys.exit(1)

SCOPE = "https://www.googleapis.com/auth/analytics.readonly"
REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"

auth_url = (
    "https://accounts.google.com/o/oauth2/auth?"
    + urllib.parse.urlencode({
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "scope": SCOPE,
        "response_type": "code",
        "access_type": "offline",
    })
)

print("\n1. Open this URL in your browser:\n")
print(auth_url)
print("\n2. Authorize and paste the code below:")
code = input("Authorization code: ").strip()

data = urllib.parse.urlencode({
    "code": code,
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
    "redirect_uri": REDIRECT_URI,
    "grant_type": "authorization_code",
}).encode()

req = urllib.request.Request(
    "https://oauth2.googleapis.com/token",
    data=data,
    headers={"Content-Type": "application/x-www-form-urlencoded"},
)

try:
    with urllib.request.urlopen(req) as resp:
        tokens = json.loads(resp.read())
except urllib.error.HTTPError as e:
    print(f"Error: {e.read().decode()}", file=sys.stderr)
    sys.exit(1)

if "refresh_token" not in tokens:
    print("Error: No refresh token returned. Make sure you revoked previous access first.", file=sys.stderr)
    sys.exit(1)

print("\n✓ Success! Add this to your environment:\n")
print(f"export GOOGLE_REFRESH_TOKEN={tokens['refresh_token']}")

```

### scripts/ga4_query.py

```python
#!/usr/bin/env python3
"""
GA4 query script — calls analyticsdata.googleapis.com directly.
No third-party proxy or managed OAuth.
"""
import argparse
import json
import os
import sys
import urllib.parse
import urllib.request

# ── Auth ─────────────────────────────────────────────────────────────────────

def get_access_token():
    client_id = os.environ.get("GOOGLE_CLIENT_ID")
    client_secret = os.environ.get("GOOGLE_CLIENT_SECRET")
    refresh_token = os.environ.get("GOOGLE_REFRESH_TOKEN")

    missing = [k for k, v in {
        "GOOGLE_CLIENT_ID": client_id,
        "GOOGLE_CLIENT_SECRET": client_secret,
        "GOOGLE_REFRESH_TOKEN": refresh_token,
    }.items() if not v]

    if missing:
        print(f"Error: missing env vars: {', '.join(missing)}", file=sys.stderr)
        print("Run ga4_auth.py to complete setup.", file=sys.stderr)
        sys.exit(1)

    data = urllib.parse.urlencode({
        "client_id": client_id,
        "client_secret": client_secret,
        "refresh_token": refresh_token,
        "grant_type": "refresh_token",
    }).encode()

    req = urllib.request.Request(
        "https://oauth2.googleapis.com/token",
        data=data,
        headers={"Content-Type": "application/x-www-form-urlencoded"},
    )
    try:
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read())["access_token"]
    except urllib.error.HTTPError as e:
        print(f"Auth error: {e.read().decode()}", file=sys.stderr)
        sys.exit(1)


# ── API ───────────────────────────────────────────────────────────────────────

def run_report(property_id, metrics, dimensions, start, end, limit, filter_expr, access_token):
    url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport"

    body = {
        "dateRanges": [{"startDate": start, "endDate": end}],
        "metrics": [{"name": m.strip()} for m in metrics],
        "dimensions": [{"name": d.strip()} for d in dimensions],
        "limit": limit,
    }

    if filter_expr:
        # Simple dimension filter: "pagePath=~/blog/"
        # Supports: = (exact), =~ (regex), != (not exact), !~ (not regex)
        for op_str, op_name in [("=~", "PARTIAL_REGEXP"), ("!~", "NOT_PARTIAL_REGEXP"), ("!=", "EXACT"), ("=", "EXACT")]:
            if op_str in filter_expr:
                parts = filter_expr.split(op_str, 1)
                negate = op_name.startswith("NOT_")
                body["dimensionFilter"] = {
                    "filter": {
                        "fieldName": parts[0].strip(),
                        "stringFilter": {
                            "matchType": "PARTIAL_REGEXP" if "REGEXP" in op_name else "EXACT",
                            "value": parts[1].strip(),
                            "caseSensitive": False,
                        },
                    }
                }
                if negate:
                    body["dimensionFilter"] = {"notExpression": body["dimensionFilter"]}
                break

    payload = json.dumps(body).encode()
    req = urllib.request.Request(
        url,
        data=payload,
        headers={
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        },
    )

    try:
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as e:
        error = e.read().decode()
        print(f"API error {e.code}: {error}", file=sys.stderr)
        sys.exit(1)


# ── Output ────────────────────────────────────────────────────────────────────

def print_table(response, dimensions, metrics):
    if not response.get("rows"):
        print("No data returned.")
        return

    headers = dimensions + metrics
    rows = []
    for row in response["rows"]:
        dim_vals = [v["value"] for v in row.get("dimensionValues", [])]
        metric_vals = [v["value"] for v in row.get("metricValues", [])]
        rows.append(dim_vals + metric_vals)

    # Column widths
    widths = [max(len(h), max((len(r[i]) for r in rows), default=0)) for i, h in enumerate(headers)]

    sep = "+-" + "-+-".join("-" * w for w in widths) + "-+"
    header_row = "| " + " | ".join(h.ljust(widths[i]) for i, h in enumerate(headers)) + " |"

    print(sep)
    print(header_row)
    print(sep)
    for row in rows:
        print("| " + " | ".join(str(v).ljust(widths[i]) for i, v in enumerate(row)) + " |")
    print(sep)

    total = response.get("rowCount", len(rows))
    print(f"\n{len(rows)} rows shown / {total} total")


# ── CLI ───────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="Query GA4 via Analytics Data API")
    parser.add_argument("--metrics", required=True, help="Comma-separated metrics, e.g. screenPageViews,sessions")
    parser.add_argument("--metric", help="Single metric (alias for --metrics)")
    parser.add_argument("--dimensions", help="Comma-separated dimensions, e.g. pagePath,deviceCategory")
    parser.add_argument("--dimension", help="Single dimension (alias for --dimensions)")
    parser.add_argument("--start", default="30daysAgo", help="Start date (YYYY-MM-DD or NdaysAgo)")
    parser.add_argument("--end", default="today", help="End date (YYYY-MM-DD or today)")
    parser.add_argument("--limit", type=int, default=20, help="Max rows to return")
    parser.add_argument("--filter", dest="filter_expr", help='Dimension filter, e.g. "pagePath=~/blog/"')
    parser.add_argument("--property", help="GA4 property ID (overrides GA4_PROPERTY_ID env var)")
    parser.add_argument("--json", action="store_true", help="Output raw JSON instead of table")
    args = parser.parse_args()

    property_id = args.property or os.environ.get("GA4_PROPERTY_ID")
    if not property_id:
        print("Error: GA4_PROPERTY_ID env var not set (or use --property)", file=sys.stderr)
        sys.exit(1)

    # Normalize metric/dimension args
    metrics_str = args.metrics or args.metric
    dimensions_str = args.dimensions or args.dimension or ""
    metrics = [m.strip() for m in metrics_str.split(",") if m.strip()]
    dimensions = [d.strip() for d in dimensions_str.split(",") if d.strip()]

    if not metrics:
        print("Error: at least one metric required", file=sys.stderr)
        sys.exit(1)

    access_token = get_access_token()
    response = run_report(property_id, metrics, dimensions, args.start, args.end, args.limit, args.filter_expr, access_token)

    if args.json:
        print(json.dumps(response, indent=2))
    else:
        print_table(response, dimensions, metrics)


if __name__ == "__main__":
    main()

```

google-analytics | SkillHub