# Google Analytics

> Google Analytics 4 Data API

- Skill: `utxo-ag/google-analytics` (Agent Skill)
- Install (CLI): `npx skillmds@latest add utxo-ag/google-analytics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/utxo-ag/google-analytics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: utxo-AG (https://skillmd.com/u/utxo-ag)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/utxo-ag/google-analytics

---

# Google Analytics 4 Data API

Query GA4 properties for arbitrary reporting data via curl.

## Prerequisites

- Google OAuth skill must be set up (`.claude/skills/google-oauth/`)
- Scope: `analytics.readonly`
- Token retrieval: `TOKEN=$(node ~/.claude/skills/google-oauth/scripts/token-store.js get analytics.readonly)`
- If token retrieval fails (non-zero exit), trigger the OAuth flow per the google-oauth skill

## Workflow

### Step 1: Discover Property IDs (mandatory before any report)

```bash
TOKEN=$(node ~/.claude/skills/google-oauth/scripts/token-store.js get analytics.readonly)
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://analyticsadmin.googleapis.com/v1beta/accountSummaries"
```

Response:
```json
{
  "accountSummaries": [{
    "displayName": "My Account",
    "account": "accounts/123456",
    "propertySummaries": [{
      "property": "properties/987654321",
      "displayName": "My Website"
    }]
  }]
}
```

Extract the `property` value (e.g. `properties/987654321`). The numeric part (`987654321`) is used in Data API URL paths. Supports pagination via `pageSize` and `pageToken` query params.

### Step 2: Use the property ID in report calls

## API Endpoints

Base URL: `https://analyticsdata.googleapis.com/v1beta/`

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `properties/ID:runReport` | Standard report |
| POST | `properties/ID:batchRunReports` | Multiple reports in batch |
| POST | `properties/ID:runRealtimeReport` | Realtime data (last 30min) |
| POST | `properties/ID:runPivotReport` | Pivot table report |
| POST | `properties/ID:batchRunPivotReports` | Batch pivot reports |
| POST | `properties/ID:checkCompatibility` | Check dimension/metric compatibility |
| GET | `properties/ID/metadata` | Available dimensions & metrics metadata |

## runReport Request Body

```json
{
  "dimensions": [{"name": "date"}],
  "metrics": [{"name": "sessions"}],
  "dateRanges": [{"startDate": "28daysAgo", "endDate": "today"}],
  "dimensionFilter": "<FilterExpression>",
  "metricFilter": "<FilterExpression>",
  "orderBys": ["<OrderBy>"],
  "limit": "10000",
  "offset": "0",
  "currencyCode": "USD",
  "keepEmptyRows": false,
  "returnPropertyQuota": false,
  "metricAggregations": ["TOTAL", "MINIMUM", "MAXIMUM"],
  "comparisons": ["<Comparison>"]
}
```

- `limit`: string, default 10000, max 250000
- Date formats: relative (`today`, `yesterday`, `NdaysAgo`) or absolute `YYYY-MM-DD`
- Multiple dateRanges supported for comparison

## FilterExpression

Union type — exactly one of these four fields:

- `andGroup`: `{"expressions": [FilterExpression, ...]}`
- `orGroup`: `{"expressions": [FilterExpression, ...]}`
- `notExpression`: `FilterExpression`
- `filter`: primitive filter (see below)

**Primitive filter** (alongside `fieldName`):

| Type | Fields | Notes |
|------|--------|-------|
| `stringFilter` | `{matchType, value, caseSensitive}` | matchType: EXACT, BEGINS_WITH, ENDS_WITH, CONTAINS, FULL_REGEXP, PARTIAL_REGEXP |
| `inListFilter` | `{values[], caseSensitive}` | Match any in list |
| `numericFilter` | `{operation, value}` | operation: EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL; value: `{int64Value}` or `{doubleValue}` |
| `betweenFilter` | `{fromValue, toValue}` | NumericValue objects |
| `emptyFilter` | `{}` | Matches "(not set)" values |

## OrderBy

```json
{"desc": true, "metric": {"metricName": "sessions"}}
{"desc": false, "dimension": {"dimensionName": "date", "orderType": "NUMERIC"}}
```

orderType enum: ALPHANUMERIC, CASE_INSENSITIVE_ALPHANUMERIC, NUMERIC

## Realtime Reports

Uses `minuteRanges` instead of `dateRanges`:

```json
{"minuteRanges": [{"startMinutesAgo": 29, "endMinutesAgo": 0}]}
```

- Max 2 minute ranges
- Standard properties: up to 29 min ago; GA 360: up to 59 min
- Same filter/orderBy/limit structure as runReport

## Response Shape

```json
{
  "dimensionHeaders": [{"name": "date"}],
  "metricHeaders": [{"name": "sessions", "type": "TYPE_INTEGER"}],
  "rows": [{"dimensionValues": [{"value": "20260301"}], "metricValues": [{"value": "1234"}]}],
  "rowCount": 100,
  "metadata": {}
}
```

## Common Dimensions

| Dimension | Description |
|-----------|-------------|
| date | YYYYMMDD format |
| dateHour | YYYYMMDDHH |
| country, city, region, continent | Geographic |
| deviceCategory | desktop, mobile, tablet |
| browser, operatingSystem | Tech |
| pagePath, pageTitle, landingPage | Content |
| sessionSource, sessionMedium | Traffic source |
| sessionDefaultChannelGrouping | Channel |
| sessionCampaignName | Campaign |
| eventName | Event name |
| hostname | Site hostname |
| newVsReturning | new or returning |
| platform | web, iOS, Android |
| language | User language |

## Common Metrics

| Metric | Description |
|--------|-------------|
| sessions | Total sessions |
| totalUsers, newUsers, activeUsers | User counts |
| screenPageViews | Page views |
| engagedSessions | Sessions with engagement |
| bounceRate | Float 0-1 |
| averageSessionDuration | Seconds (float) |
| engagementRate | Float 0-1 |
| sessionsPerUser | Float |
| eventCount | Total events |
| conversions | Key events |
| totalRevenue | Revenue (currency) |
| ecommercePurchases | Purchase count |
| userEngagementDuration | Total engagement seconds |

## Curl Examples

### 1. List all properties

```bash
TOKEN=$(node ~/.claude/skills/google-oauth/scripts/token-store.js get analytics.readonly)
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://analyticsadmin.googleapis.com/v1beta/accountSummaries"
```

### 2. Traffic overview by date (last 7 days)

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dimensions": [{"name": "date"}],
    "metrics": [{"name": "sessions"}, {"name": "totalUsers"}, {"name": "screenPageViews"}],
    "dateRanges": [{"startDate": "7daysAgo", "endDate": "today"}],
    "orderBys": [{"dimension": {"dimensionName": "date"}, "desc": false}]
  }' \
  "https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runReport"
```

### 3. Top pages

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dimensions": [{"name": "pagePath"}],
    "metrics": [{"name": "screenPageViews"}],
    "dateRanges": [{"startDate": "28daysAgo", "endDate": "today"}],
    "orderBys": [{"metric": {"metricName": "screenPageViews"}, "desc": true}],
    "limit": "20"
  }' \
  "https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runReport"
```

### 4. Traffic by source filtered to US

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dimensions": [{"name": "sessionSource"}, {"name": "sessionMedium"}],
    "metrics": [{"name": "sessions"}, {"name": "totalUsers"}],
    "dateRanges": [{"startDate": "28daysAgo", "endDate": "today"}],
    "dimensionFilter": {
      "filter": {
        "fieldName": "country",
        "stringFilter": {"matchType": "EXACT", "value": "United States"}
      }
    }
  }' \
  "https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runReport"
```

### 5. Realtime active users by country

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dimensions": [{"name": "country"}],
    "metrics": [{"name": "activeUsers"}],
    "minuteRanges": [{"startMinutesAgo": 29, "endMinutesAgo": 0}]
  }' \
  "https://analyticsdata.googleapis.com/v1beta/properties/PROPERTY_ID:runRealtimeReport"
```

## Tips & Gotchas

- Property ID is always `properties/XXXXXXXX` in the URL path
- The `date` dimension returns `YYYYMMDD` (no dashes)
- `bounceRate` is 0-1, not a percentage
- Max 9 dimensions and 10 metrics per report
- `limit` is a string, not an integer
- Empty reports: `rows` key will be absent from the response
- Use the `properties/ID/metadata` GET endpoint to discover all available dimensions/metrics for a property
- API quota: 10,000 requests/day/property by default

