Skill: whosc/google-analytics-ga4 (version 1.0.0)

# Google Analytics skill Query GA4 properties using the [Google Analytics Data API v1](https://developers.google.com/analytics/devguides/reporting/data/v1). ## Capabilities - **Realtime metrics** — e.g. active users in the last N minutes - **Historical reports** — custom date ranges, metrics, dimensions, paging - **Metadata** — discover valid dimension and metric API names - **Property list hint** — Data API alone cannot enumerate properties; doc explains where to find the numeric ID ## Setup ### 1. Create a service account 1. Open [Google Cloud Console](https://console.cloud.google.com/). 2. Create or select a project. 3. Enable **Google Analytics Data API**. 4. Create a service account: **IAM & Admin** → **Service Accounts** → **Create service account**. - Optional GCP role: **BigQuery Job User** (only if you also use BigQuery). 5. Finish the wizard. ### 2. Create a JSON key 1. Open the service account → **Keys**. 2. **Add key** → **Create new key** → **JSON**. 3. Download the file and save it as `ga-credentials.json` (or any path you pass via `--credentials` / `GOOGLE_APPLICATION_CREDENTIALS`). ### 3. Grant GA4 access 1. Open [Google Analytics](https://analytics.google.com/). 2. Select the property. 3. **Admin** (gear) → **Property access management**. 4. **Add users** → enter the service account email (`…@….iam.gserviceaccount.com`). 5. Role: at least **Viewer**. ### 4. Credentials location Either: - **A.** Place `ga-credentials.json` in this skill directory, or - **B.** Set `GOOGLE_APPLICATION_CREDENTIALS` to the absolute path of the JSON key. Never commit real keys. `.gitignore` excludes `ga-credentials.json` and `config.json`. ## Examples ### Property list guidance ```bash python ga_query.py --action list-properties ``` ### Realtime (active users) ```bash python ga_query.py --action realtime \ --property-id YOUR-GA4-PROPERTY-ID ``` ### Historical ```bash python ga_query.py --action historical \ --property-id YOUR-GA4-PROPERTY-ID \ --start-date 7daysAgo \ --end-date yesterday \ --metrics activeUsers,sessions,eventCount \ --dimensions country,deviceCategory ``` ### Metadata ```bash python ga_query.py --action metadata \ --property-id YOUR-GA4-PROPERTY-ID ``` ## Arguments ### Common | Argument | Description | Default | |----------|-------------|---------| | `--property-id` | Numeric GA4 property ID | Required (except `list-properties`) | | `--credentials` | Service account JSON path | `ga-credentials.json` | ### Realtime | Argument | Description | Default | |----------|-------------|---------| | `--metrics` | Comma-separated metrics | `activeUsers` | | `--dimensions` | Comma-separated dimensions | (none) | | `--minute-range` | Minutes ago window, e.g. `0-30` | `0-30` | ### Historical | Argument | Description | Default | |----------|-------------|---------| | `--start-date` | Start (`YYYY-MM-DD` or relative) | Required | | `--end-date` | End | Required | | `--metrics` | Comma-separated metrics | `activeUsers` | | `--dimensions` | Comma-separated dimensions | (none) | | `--limit` | Max rows | `10000` | | `--offset` | Paging offset | `0` | ## Common metrics | Name | Meaning | |------|------------------------------------------------------| | `activeUsers` | Active users | | `sessions` | Sessions | | `eventCount` | Event count | | `engagementRate` | Engagement rate | | `averageSessionDuration` | Avg session duration (seconds) | | `screenPageViews` | Page / screen views | | `conversions` | Conversions | | `totalRevenue` | Revenue | ## Common dimensions | Name | Meaning | |------|---------| | `country` | Country | | `city` | City | | `deviceCategory` | desktop / mobile / tablet | | `eventName` | Event name | | `pagePath` | Page path | | `source` | Traffic source | | `medium` | Medium | | `campaign` | Campaign | | `date` | Date | ## Date expressions - Absolute: `2024-01-15` - Relative: `today`, `yesterday`, `7daysAgo`, `30daysAgo` ## Output Default: Markdown tables. Use `--output json` for machine-readable output. ## Dependencies ```bash pip install google-analytics-data ``` Optional (traffic source report + DingTalk): `pip install requests` and set `DINGTALK_WEBHOOK` / `DINGTALK_SECRET`. ## References - [Data API docs](https://developers.google.com/analytics/devguides/reporting/data/v1) - [Python client](https://github.com/googleapis/google-analytics-data-python) - [Dimensions & metrics schema](https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema)

path: ga_query.py
content: #!/usr/bin/env python3
"""
Google Analytics Data API v1 CLI — realtime, historical, and metadata reports.
"""

import argparse
import json
import os
import sys
from typing import List, Optional, Dict, Any

try:
    from google.analytics.data_v1beta import BetaAnalyticsDataClient
    from google.analytics.data_v1beta.types import (
        DateRange,
        Dimension,
        Metric,
        RunReportRequest,
        RunRealtimeReportRequest,
        MinuteRange,
        MetadataRequest,
    )
except ImportError:
    print("Error: install google-analytics-data")
    print("  pip install google-analytics-data")
    sys.exit(1)


class GoogleAnalyticsQuery:
    """Thin wrapper around BetaAnalyticsDataClient."""

    def __init__(self, credentials_path: Optional[str] = None):
        """
        Args:
            credentials_path: Path to the service account JSON key (optional if ADC is set).
        """
        if credentials_path:
            os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_path
        
        self.client = BetaAnalyticsDataClient()

    def list_properties(self) -> List[Dict[str, str]]:
        """
        The Data API does not list properties; return guidance for locating a GA4 property ID.
        """
        return [{
            "note": "Property listing is not available via the Data API alone.",
            "instruction": "In GA4: Admin > Property settings — copy the numeric Property ID.",
            "format": "Property ID is numeric only, e.g. 123456789",
        }]

    def get_metadata(self, property_id: str) -> Dict[str, Any]:
        """
        Args:
            property_id: Numeric GA4 property ID.

        Returns:
            Dict of dimensions and metrics metadata.
        """
        request = MetadataRequest(property=f"properties/{property_id}")
        response = self.client.get_metadata(request)
        
        dimensions = []
        for dim in response.dimensions:
            dimensions.append({
                "name": dim.api_name,
                "description": dim.description,
                "category": dim.category
            })
        
        metrics = []
        for metric in response.metrics:
            metrics.append({
                "name": metric.api_name,
                "description": metric.description,
                "type": metric.type_.name,
                "category": metric.category
            })
        
        return {
            "dimensions": dimensions,
            "metrics": metrics
        }

    def query_realtime(
        self,
        property_id: str,
        metrics: List[str] = None,
        dimensions: List[str] = None,
        minute_range: str = "0-30"
    ) -> Dict[str, Any]:
        """
        Run a realtime report.
        """
        if metrics is None:
            metrics = ["activeUsers"]
        
        start_minutes, end_minutes = map(int, minute_range.split("-"))
        
        request = RunRealtimeReportRequest(
            property=f"properties/{property_id}",
            metrics=[Metric(name=m) for m in metrics],
            dimensions=[Dimension(name=d) for d in dimensions] if dimensions else [],
            minute_ranges=[MinuteRange(
                start_minutes_ago=start_minutes,
                end_minutes_ago=end_minutes
            )]
        )
        
        response = self.client.run_realtime_report(request)
        
        result = self._parse_response(response, is_realtime=True)
        result["query_type"] = "realtime"
        result["minute_range"] = f"{start_minutes}-{end_minutes} minutes ago"
        
        return result

    def query_historical(
        self,
        property_id: str,
        start_date: str,
        end_date: str,
        metrics: List[str] = None,
        dimensions: List[str] = None,
        limit: int = 10000,
        offset: int = 0
    ) -> Dict[str, Any]:
        """Run a standard (historical) report."""
        if metrics is None:
            metrics = ["activeUsers"]
        
        request = RunReportRequest(
            property=f"properties/{property_id}",
            date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
            metrics=[Metric(name=m) for m in metrics],
            dimensions=[Dimension(name=d) for d in dimensions] if dimensions else [],
            limit=limit,
            offset=offset
        )
        
        response = self.client.run_report(request)
        
        result = self._parse_response(response, is_realtime=False)
        result["query_type"] = "historical"
        result["date_range"] = f"{start_date} to {end_date}"
        
        return result

    def _parse_response(self, response, is_realtime: bool = False) -> Dict[str, Any]:
        """Normalize API response rows and totals."""

        # Headers
        dimension_headers = [h.name for h in response.dimension_headers]
        metric_headers = [h.name for h in response.metric_headers]
        
        # Rows
        rows = []
        for row in response.rows:
            row_data = {}
            for i, dim_value in enumerate(row.dimension_values):
                if i < len(dimension_headers):
                    row_data[dimension_headers[i]] = dim_value.value
            for i, metric_value in enumerate(row.metric_values):
                if i < len(metric_headers):
                    row_data[metric_headers[i]] = metric_value.value
            rows.append(row_data)
        
        # Totals
        totals = {}
        if response.totals:
            for total_row in response.totals:
                for i, metric_value in enumerate(total_row.metric_values):
                    if i < len(metric_headers):
                        totals[metric_headers[i]] = metric_value.value
        
        return {
            "success": True,
            "dimension_headers": dimension_headers,
            "metric_headers": metric_headers,
            "rows": rows,
            "row_count": response.row_count,
            "totals": totals,
            "metadata": {
                "currency_code": getattr(response.metadata, 'currency_code', None) if response.metadata else None,
                "time_zone": getattr(response.metadata, 'time_zone', None) if response.metadata else None
            }
        }


def format_as_markdown_table(result: Dict[str, Any]) -> str:
    """Render a result dict as a Markdown table."""
    if not result.get("success"):
        return "❌ Query failed"
    
    lines = []
    
    query_type = result.get("query_type", "unknown")
    if query_type == "realtime":
        lines.append(f"### 📊 Realtime ({result.get('minute_range', '')})")
    elif query_type == "historical":
        lines.append(f"### 📈 Historical ({result.get('date_range', '')})")
    
    lines.append("")
    
    if result.get("totals"):
        lines.append("**Totals:**")
        for metric, value in result["totals"].items():
            lines.append(f"- {metric}: {value}")
        lines.append("")
    
    rows = result.get("rows", [])
    if not rows:
        lines.append("⚠️ No rows")
        return "\n".join(lines)

    headers = result.get("dimension_headers", []) + result.get("metric_headers", [])
    if not headers:
        lines.append("⚠️ No columns returned")
        return "\n".join(lines)
    lines.append("| " + " | ".join(headers) + " |")
    lines.append("| " + " | ".join(["---"] * len(headers)) + " |")
    
    for row in rows[:50]:
        values = [str(row.get(h, "")) for h in headers]
        lines.append("| " + " | ".join(values) + " |")
    
    if len(rows) > 50:
        lines.append(f"\n*... {len(rows) - 50} more rows*")
    
    lines.append(f"\n*{result.get('row_count', 0)} rows total*")
    
    return "\n".join(lines)


def format_metadata(metadata: Dict[str, Any]) -> str:
    """Pretty-print metadata for CLI markdown output."""
    lines = ["### 📋 Available dimensions and metrics\n"]

    lines.append("**Dimensions:**")
    dimensions = metadata.get("dimensions", [])[:30]
    for dim in dimensions:
        lines.append(f"- `{dim['name']}` - {dim['description'][:80]}...")
    if len(metadata.get("dimensions", [])) > 30:
        lines.append(f"\n*... {len(metadata['dimensions'])} dimensions total*")
    
    lines.append("")
    
    lines.append("**Metrics:**")
    metrics = metadata.get("metrics", [])[:30]
    for metric in metrics:
        lines.append(f"- `{metric['name']}` ({metric['type']}) - {metric['description'][:80]}...")
    if len(metadata.get("metrics", [])) > 30:
        lines.append(f"\n*... {len(metadata['metrics'])} metrics total*")
    
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(
        description="Google Analytics Data API CLI",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python ga_query.py --action realtime --property-id 123456789

  python ga_query.py --action historical \\
    --property-id 123456789 \\
    --start-date 7daysAgo \\
    --end-date yesterday \\
    --metrics activeUsers,sessions \\
    --dimensions country

  python ga_query.py --action metadata --property-id 123456789
        """,
    )
    
    parser.add_argument(
        "--action",
        choices=["realtime", "historical", "metadata", "list-properties"],
        required=True,
        help="Action to run",
    )
    
    parser.add_argument(
        "--property-id",
        help="Numeric GA4 property ID",
    )
    
    parser.add_argument(
        "--credentials",
        default="ga-credentials.json",
        help="Service account JSON path (default: ga-credentials.json)",
    )

    parser.add_argument(
        "--metrics",
        default="activeUsers",
        help="Comma-separated metrics (default: activeUsers)",
    )
    
    parser.add_argument(
        "--dimensions",
        help="Comma-separated dimensions",
    )
    
    parser.add_argument(
        "--minute-range",
        default="0-30",
        help="Realtime window: start-end minutes ago (default: 0-30)",
    )

    parser.add_argument(
        "--start-date",
        help="Historical start date (YYYY-MM-DD or relative)",
    )
    
    parser.add_argument(
        "--end-date",
        help="Historical end date",
    )
    
    parser.add_argument(
        "--limit",
        type=int,
        default=10000,
        help="Row limit (default: 10000)",
    )
    
    parser.add_argument(
        "--offset",
        type=int,
        default=0,
        help="Row offset (default: 0)",
    )
    
    parser.add_argument(
        "--output",
        choices=["markdown", "json"],
        default="markdown",
        help="Output format (default: markdown)",
    )
    
    args = parser.parse_args()
    
    if args.action != "list-properties" and not args.property_id:
        print("Error: --property-id is required for this action")
        sys.exit(1)
    
    if args.action == "historical":
        if not args.start_date or not args.end_date:
            print("Error: --start-date and --end-date are required for historical reports")
            sys.exit(1)

    credentials_path = args.credentials
    if not os.path.isabs(credentials_path):
        script_dir = os.path.dirname(os.path.abspath(__file__))
        local_path = os.path.join(script_dir, credentials_path)
        if os.path.exists(local_path):
            credentials_path = local_path
    
    if not os.path.exists(credentials_path) and not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"):
        print(f"⚠️  Credentials file not found: {credentials_path}")
        print("Set GOOGLE_APPLICATION_CREDENTIALS or place ga-credentials.json next to this script.")
        print("")
    
    try:
        ga = GoogleAnalyticsQuery(credentials_path if os.path.exists(credentials_path) else None)
    except Exception as e:
        print(f"❌ Failed to initialize client: {e}")
        sys.exit(1)

    try:
        if args.action == "list-properties":
            result = ga.list_properties()
            print(json.dumps(result, indent=2, ensure_ascii=False))
            
        elif args.action == "metadata":
            metadata = ga.get_metadata(args.property_id)
            if args.output == "json":
                print(json.dumps(metadata, indent=2, ensure_ascii=False))
            else:
                print(format_metadata(metadata))
                
        elif args.action == "realtime":
            metrics = [m.strip() for m in args.metrics.split(",")]
            dimensions = [d.strip() for d in args.dimensions.split(",")] if args.dimensions else None
            
            result = ga.query_realtime(
                property_id=args.property_id,
                metrics=metrics,
                dimensions=dimensions,
                minute_range=args.minute_range
            )
            
            if args.output == "json":
                print(json.dumps(result, indent=2, ensure_ascii=False))
            else:
                print(format_as_markdown_table(result))
                
        elif args.action == "historical":
            metrics = [m.strip() for m in args.metrics.split(",")]
            dimensions = [d.strip() for d in args.dimensions.split(",")] if args.dimensions else None
            
            result = ga.query_historical(
                property_id=args.property_id,
                start_date=args.start_date,
                end_date=args.end_date,
                metrics=metrics,
                dimensions=dimensions,
                limit=args.limit,
                offset=args.offset
            )
            
            if args.output == "json":
                print(json.dumps(result, indent=2, ensure_ascii=False))
            else:
                print(format_as_markdown_table(result))
    
    except Exception as e:
        print(f"❌ Query failed: {e}")
        if os.environ.get("DEBUG"):
            import traceback
            traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()


path: EXAMPLES.md
content: # Google Analytics skill — examples

How to run the skill from OpenClaw or any shell.

## Prerequisites

1. `pip install google-analytics-data`  
2. Service account JSON configured (see [SKILL.md](SKILL.md))  
3. Numeric GA4 property ID  

## Invoking from OpenClaw

### Option A — `ga_query.py`

```bash
python skills/google-analytics/ga_query.py \
  --action realtime \
  --property-id YOUR-PROPERTY-ID

python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id YOUR-PROPERTY-ID \
  --start-date 7daysAgo \
  --end-date yesterday \
  --metrics activeUsers,sessions,eventCount \
  --dimensions country,deviceCategory
```

### Option B — `openclaw_ga.py`

```bash
python skills/google-analytics/openclaw_ga.py realtime \
  --property-id YOUR-PROPERTY-ID

python skills/google-analytics/openclaw_ga.py historical \
  --property-id YOUR-PROPERTY-ID \
  --start-date 7daysAgo \
  --end-date yesterday \
  --metrics activeUsers,sessions
```

## Scenarios

### Site overview (yesterday)

```bash
python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date yesterday \
  --end-date yesterday \
  --metrics activeUsers,sessions,eventCount,screenPageViews,engagementRate
```

**Sample output:**

```
### 📈 Historical (yesterday to yesterday)

**Totals:**
- activeUsers: 1234
- sessions: 2345
...

| activeUsers | sessions | eventCount | screenPageViews | engagementRate |
|-------------|----------|------------|-----------------|----------------|
| 1234        | 2345     | 12345      | 5678            | 0.65           |

*1 rows total*
```

### By country (7 days)

```bash
python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date 7daysAgo \
  --end-date yesterday \
  --metrics activeUsers,sessions \
  --dimensions country \
  --limit 20
```

### Device category

```bash
python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date 30daysAgo \
  --end-date yesterday \
  --metrics activeUsers,sessions,engagementRate \
  --dimensions deviceCategory
```

### Realtime active users

```bash
python skills/google-analytics/ga_query.py \
  --action realtime \
  --property-id 123456789 \
  --metrics activeUsers
```

### Realtime by country

```bash
python skills/google-analytics/ga_query.py \
  --action realtime \
  --property-id 123456789 \
  --metrics activeUsers \
  --dimensions country
```

### Daily trend

```bash
python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date 30daysAgo \
  --end-date yesterday \
  --metrics activeUsers \
  --dimensions date
```

### Source / medium

```bash
python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date 7daysAgo \
  --end-date yesterday \
  --metrics sessions,conversions,totalRevenue \
  --dimensions source,medium
```

### Top pages

```bash
python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date 7daysAgo \
  --end-date yesterday \
  --metrics screenPageViews \
  --dimensions pagePath \
  --limit 20
```

### Events

```bash
python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date 7daysAgo \
  --end-date yesterday \
  --metrics eventCount \
  --dimensions eventName \
  --limit 30
```

## Advanced

### Pagination

```bash
python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date 90daysAgo \
  --end-date yesterday \
  --metrics activeUsers \
  --dimensions date \
  --limit 100 \
  --offset 0

python skills/google-analytics/ga_query.py \
  --action historical \
  --property-id 123456789 \
  --start-date 90daysAgo \
  --end-date yesterday \
  --metrics activeUsers \
  --dimensions date \
  --limit 100 \
  --offset 100
```

### JSON output

```bash
python skills/google-analytics/ga_query.py \
  --action realtime \
  --property-id 123456789 \
  --output json
```

### `GOOGLE_APPLICATION_CREDENTIALS`

```bash
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
python skills/google-analytics/ga_query.py \
  --action realtime \
  --property-id 123456789
```

## Troubleshooting

### Verbose errors

```bash
export DEBUG=1
python skills/google-analytics/ga_query.py \
  --action realtime \
  --property-id 123456789
```

### Smoke test

```bash
python skills/google-analytics/ga_query.py \
  --action metadata \
  --property-id 123456789
```

## Tips

1. Property ID is numeric only (e.g. `123456789`).  
2. Dates: `YYYY-MM-DD` or relative (`7daysAgo`, `yesterday`).  
3. Up to **10 metrics** and **9 dimensions** per request (API limits).  
4. Large exports may require paging with `--limit` / `--offset`.  
5. Realtime covers roughly the last **30 minutes** (60 for GA360).  


path: openclaw_ga.py
content: #!/usr/bin/env python3
"""
OpenClaw-friendly wrapper: shells out to ga_query.py with normalized flags.
"""

import subprocess
import sys
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
GA_QUERY_SCRIPT = os.path.join(SCRIPT_DIR, "ga_query.py")


def run_ga_query(action: str, **kwargs) -> str:
    """
    Run GA CLI and return stdout text.

    Args:
        action: realtime | historical | metadata
        **kwargs: forwarded as --kebab-case flags
    """
    cmd = [sys.executable, GA_QUERY_SCRIPT, "--action", action]

    credentials = kwargs.get("credentials")
    if credentials:
        cmd.extend(["--credentials", credentials])
    else:
        default_creds = os.path.join(SCRIPT_DIR, "ga-credentials.json")
        if os.path.exists(default_creds):
            cmd.extend(["--credentials", default_creds])

    property_id = kwargs.get("property_id")
    if property_id:
        cmd.extend(["--property-id", str(property_id)])

    for key, value in kwargs.items():
        if key in ["credentials", "property_id"]:
            continue
        if value is not None:
            key_formatted = f"--{key.replace('_', '-')}"
            if isinstance(value, bool):
                if value:
                    cmd.append(key_formatted)
            elif isinstance(value, list):
                cmd.extend([key_formatted, ",".join(map(str, value))])
            else:
                cmd.extend([key_formatted, str(value)])

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            cwd=SCRIPT_DIR,
            timeout=60,
        )

        if result.returncode != 0:
            return f"❌ Query failed: {result.stderr}"

        return result.stdout

    except subprocess.TimeoutExpired:
        return "❌ Timeout (> 60s)"
    except Exception as e:
        return f"❌ Error: {e}"


def main():
    import argparse

    parser = argparse.ArgumentParser(
        description="OpenClaw helper for ga_query.py",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python openclaw_ga.py realtime --property-id 123456789

  python openclaw_ga.py historical \\
    --property-id 123456789 \\
    --start-date 7daysAgo \\
    --end-date yesterday \\
    --metrics activeUsers,sessions \\
    --dimensions country
        """,
    )

    subparsers = parser.add_subparsers(dest="action", help="Action")

    realtime_parser = subparsers.add_parser("realtime", help="Realtime report")
    realtime_parser.add_argument("--property-id", required=True, help="GA4 property ID")
    realtime_parser.add_argument("--metrics", default="activeUsers", help="Metrics list")
    realtime_parser.add_argument("--dimensions", help="Dimensions list")
    realtime_parser.add_argument("--minute-range", default="0-30", help="Minutes ago window")
    realtime_parser.add_argument("--credentials", help="Service account JSON path")

    historical_parser = subparsers.add_parser("historical", help="Historical report")
    historical_parser.add_argument("--property-id", required=True, help="GA4 property ID")
    historical_parser.add_argument("--start-date", required=True, help="Start date")
    historical_parser.add_argument("--end-date", required=True, help="End date")
    historical_parser.add_argument("--metrics", default="activeUsers", help="Metrics list")
    historical_parser.add_argument("--dimensions", help="Dimensions list")
    historical_parser.add_argument("--limit", type=int, default=10000, help="Row limit")
    historical_parser.add_argument("--credentials", help="Service account JSON path")

    metadata_parser = subparsers.add_parser("metadata", help="Metadata listing")
    metadata_parser.add_argument("--property-id", required=True, help="GA4 property ID")
    metadata_parser.add_argument("--credentials", help="Service account JSON path")

    args = parser.parse_args()

    if not args.action:
        parser.print_help()
        sys.exit(1)

    kwargs = vars(args)
    action = kwargs.pop("action")

    result = run_ga_query(action, **kwargs)
    print(result)


if __name__ == "__main__":
    main()


path: requirements.txt
content: # Google Analytics Data API
google-analytics-data>=0.19.0

# Google Auth (pulled in transitively)
google-auth>=2.0.0

# Optional: DingTalk outbound webhook in traffic_source_report.py
requests>=2.28.0


path: config.json
content: property_id: YOUR_NUMERIC_GA4_PROPERTY_ID
property_name: My product or site name

path: README.md
content: # Google Analytics OpenClaw skill

Call the Google Analytics **Data API v1** from the command line to read **GA4** data (not Universal Analytics).

## Quick start

### 1. Install

```bash
pip install -r requirements.txt
```

### 2. Credentials

Follow [SKILL.md](SKILL.md) to create a service account, download JSON, and grant the account **Viewer** on your GA4 property.

### 3. Run a query

```bash
# Realtime active users
python ga_query.py --action realtime --property-id YOUR-PROPERTY-ID

# Historical
python ga_query.py --action historical \
  --property-id YOUR-PROPERTY-ID \
  --start-date 7daysAgo \
  --end-date yesterday \
  --metrics activeUsers,sessions \
  --dimensions country
```

## Layout

| File | Purpose |
|------|---------|
| [SKILL.md](SKILL.md) | Full setup and reference |
| [QUICK_REFERENCE.md](QUICK_REFERENCE.md) | Cheat sheet |
| [EXAMPLES.md](EXAMPLES.md) | Example queries |
| `ga_query.py` | Main CLI |
| `openclaw_ga.py` | Thin wrapper for OpenClaw-style invocation |
| `test_connection.py` | Validate credentials and API access |
| `traffic_source_report.py` | Optional period-over-period source report (needs `config.json` or env) |
| `ga-credentials.json` | **Local only** — create from GCP (not in git) |

## Output

Markdown tables by default; add `--output json` for JSON.

## Troubleshooting

### Missing credentials

```
Error: Your default credentials were not found.
```

1. Add `ga-credentials.json` beside the scripts, or  
2. `export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json`

### Permission errors

Add the service account email under **Property access management** with at least **Viewer**.

### API disabled

In Google Cloud: **APIs & Services** → **Library** → enable **Google Analytics Data API**.

## References

- [SKILL.md](SKILL.md)  
- [GA4 Data API](https://developers.google.com/analytics/devguides/reporting/data/v1)  
- [Schema (dimensions/metrics)](https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema)  


path: temp_test.py
content: #!/usr/bin/env python3
"""Minimal one-shot report (set GA4_PROPERTY_ID)."""
import os
import sys
import io

if sys.platform == "win32":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
creds_path = os.path.join(SCRIPT_DIR, "ga-credentials.json")
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = creds_path

print(f"Credentials path: {creds_path}")
print(f"File exists: {os.path.exists(creds_path)}")

ga4_property_id = os.environ.get("GA4_PROPERTY_ID")
if not ga4_property_id:
    print("Set GA4_PROPERTY_ID to your numeric GA4 property ID.")
    sys.exit(1)

try:
    from google.analytics.data_v1beta import BetaAnalyticsDataClient
    from google.analytics.data_v1beta.types import (
        RunReportRequest,
        DateRange,
        Metric,
    )

    client = BetaAnalyticsDataClient()
    property_id = f"properties/{ga4_property_id}"
    print(f"Querying {property_id}")

    request = RunReportRequest(
        property=property_id,
        date_ranges=[DateRange(start_date="7daysAgo", end_date="yesterday")],
        metrics=[Metric(name="activeUsers")],
    )

    response = client.run_report(request)
    print("OK — activeUsers (aggregated window):")
    for row in response.rows:
        print(f"  {row.metric_values[0].value}")

except Exception as e:
    print(f"Error: {e}")
    import traceback

    traceback.print_exc()
    sys.exit(1)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)