인터페이스는 현지화되어 있으며, Skill 소개와 설명 문서는 선택한 언어로 AI가 자동 번역합니다. 코드·명령어·경로·전문 용어는 원문을 유지하며 원작자 문서가 우선합니다.
Skills 둘러보기
데이터 및 연구 · SKILL.md

ga4-data-api-query

ga4-data-api-query

Build a runReport request against the GA4 Data API v1 — pick valid metric/dimension combinations, set date ranges that respect data-freshness limits, apply filters, paginate large result sets, handle sampling thresholds. Trigger with "query GA4", "GA4 Data API", "runReport", "fetch GA4 metrics", "GA4 pageviews", "GA4 sessions".

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill 문서

GitHub

name: ga4-data-api-query description: | Build a runReport request against the GA4 Data API v1 — pick valid metric/dimension combinations, set date ranges that respect data-freshness limits, apply filters, paginate large result sets, handle sampling thresholds. Trigger with "query GA4", "GA4 Data API", "runReport", "fetch GA4 metrics", "GA4 pageviews", "GA4 sessions". allowed-tools: Bash(python3:), Bash(curl:) version: 1.3.0 license: MIT author: Jeremy Longshore jeremy@intentsolutions.io tags: [saas, analytics, google-analytics, ga4, data-api] compatibility: Designed for Claude Code

GA4 Data API v1 — runReport

Overview

The Data API v1 is the canonical read path for GA4. One endpoint (runReport) covers most use cases. Two paths matter for picking the right query: dimensions describe rows (date, page, source), metrics describe values (sessions, users, events). Not every combination is valid — see "Compatibility" below.

Prerequisite: auth working (see ga4-auth-setup).

Prerequisites

  • An authenticated GA4 Data API client with access to the target property; follow ga4-auth-setup first.
  • Python and the google-analytics-data package.
  • A numeric GA4 property ID and a bounded date range appropriate to the metric's freshness.

Instructions

Examples

The minimum viable query

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

client = BetaAnalyticsDataClient()

req = RunReportRequest(
    property="properties/123456789",     # YOUR property ID (digits only)
    date_ranges=[
        DateRange(start_date="30daysAgo", end_date="today"),
    ],
    metrics=[Metric(name="activeUsers")],
    dimensions=[Dimension(name="date")],
)
resp = client.run_report(req)

for row in resp.rows:
    date = row.dimension_values[0].value     # YYYYMMDD string
    users = row.metric_values[0].value       # numeric string
    print(f"{date}: {users}")

That's the full skeleton. Everything below extends this shape.

The 12 metrics worth knowing

MetricWhat it countsNotes
activeUsersUnique users with engagement in the windowThe "users" people mean by default
newUsersFirst-seen users in the window
totalUsersAll users (engaged or not) — superset of activeUsers
sessionsSessions started in the windowRe-engages after 30min inactivity
engagedSessionsSessions ≥10s OR ≥2 pageviews OR ≥1 conversionThe "good" sessions
screenPageViewsPageviews + app screenviews combinedWhat people mean by "pageviews"
eventCountTotal event count (every event, not just page_view)Often misleadingly large
bounceRate(sessions - engagedSessions) / sessionsLower is better
averageSessionDurationAvg seconds per sessionAcross sessions, not engagedSessions
eventsPerSessioneventCount / sessions
conversionsEvents flagged as conversions in the property setupProperty-specific
totalRevenueSum of purchase event revenueCurrency = property default

bounceRate and averageSessionDuration are ratios — don't SUM them across rows; they're already aggregated within each row's group.

The 12 dimensions worth knowing

DimensionCardinalityWhen to use
dateLow (1/day)Time series
dateHourMedIntra-day patterns
pagePathHighTop-pages reports
pageTitleHighWhen path is opaque (e.g. SPA hash routes)
sessionSource / sessionMediumMedAttribution
sessionDefaultChannelGroupingLow (~12 channels)High-level traffic source breakdown
country / region / cityMed / Med / HighGeo
deviceCategoryLow (desktop/mobile/tablet)
browser / operatingSystemMedTech audit
landingPageHighEntry-page reports
eventNameMedEvent-level breakdowns
customEvent:<name>Property-specificIf you defined custom dimensions in the property setup

Compatibility — not every (dim, metric) combo is valid

GA4 enforces a compatibility matrix at the API level. If you ask for sessions + customEvent:purchaseId together you may get an empty result or a 400 INVALID_ARGUMENT. Two rules cover ~90% of cases:

  1. User-scoped vs session-scoped vs event-scoped dimensions don't always mix with each other's metrics. Stick to dimensions in the same scope as your headline metric where possible.
  2. High-cardinality custom dimensions can trigger sampling. GA4 will silently sample if a single query touches more than the property's data-quota threshold; the response includes metadata.dataLossFromOtherRow=true. Check it.

If you're unsure, query the compatibility metadata endpoint:

from google.analytics.data_v1beta.types import CheckCompatibilityRequest
compat = client.check_compatibility(CheckCompatibilityRequest(
    property="properties/123456789",
    dimensions=[Dimension(name="pagePath"), Dimension(name="sessionSource")],
    metrics=[Metric(name="screenPageViews"), Metric(name="sessions")],
))
print(compat)

Filters

Filters are nested expressions. The common case: filter rows by a dimension value.

from google.analytics.data_v1beta.types import (
    FilterExpression, Filter, FilterExpressionList,
)

# Just pages under /docs/
docs_only = FilterExpression(
    filter=Filter(
        field_name="pagePath",
        string_filter=Filter.StringFilter(
            match_type=Filter.StringFilter.MatchType.BEGINS_WITH,
            value="/docs/",
            case_sensitive=False,
        ),
    ),
)

# AND combine: organic search AND not from referrer "spam.com"
combined = FilterExpression(
    and_group=FilterExpressionList(expressions=[
        FilterExpression(filter=Filter(
            field_name="sessionMedium",
            string_filter=Filter.StringFilter(
                match_type=Filter.StringFilter.MatchType.EXACT,
                value="organic",
            ),
        )),
        FilterExpression(not_expression=FilterExpression(filter=Filter(
            field_name="sessionSource",
            string_filter=Filter.StringFilter(
                match_type=Filter.StringFilter.MatchType.EXACT,
                value="spam.com",
            ),
        ))),
    ]),
)

req = RunReportRequest(
    property="properties/123456789",
    date_ranges=[DateRange(start_date="30daysAgo", end_date="today")],
    metrics=[Metric(name="sessions")],
    dimensions=[Dimension(name="pagePath")],
    dimension_filter=docs_only,
)

Use metric_filter for filtering by metric (e.g. only rows where sessions > 100). Same shape.

Date ranges

FormMeaning
"2026-05-01"Absolute (ISO date)
"30daysAgo"Relative — N days before today
"yesterday", "today"Named relative
"NdaysAgo" to "today"Standard rolling window

GA4 has 48-hour data freshness — today's numbers fluctuate; yesterday's settle ~24h after midnight in the property's timezone; numbers older than 48h are stable. Don't draw conclusions from "today" alone.

Multiple date_ranges in one request gives you a comparison report:

DateRange(start_date="30daysAgo", end_date="yesterday", name="current"),
DateRange(start_date="60daysAgo", end_date="31daysAgo", name="prior"),

The response will have dateRange as an extra dimension on each row.

Pagination

req = RunReportRequest(
    # ... as above
    limit=10_000,    # max 250_000 per request
    offset=0,
)
resp = client.run_report(req)
# resp.row_count is the TOTAL matching rows; resp.rows is the current page
while resp.row_count > req.offset + len(resp.rows):
    req.offset += len(resp.rows)
    resp = client.run_report(req)
    # process resp.rows

For result sets over ~1M rows, use ga4-bigquery-export instead.

Sampling — always check

resp = client.run_report(req)
if resp.metadata.data_loss_from_other_row:
    print("WARNING: data was sampled. Tighten date range, drop high-cardinality dimensions, or use BigQuery export for unsampled data.")

If sampled, results are statistically valid but not exact. For exact counts, BigQuery export is the only path.

Output

A successful runReport response contains dimension and metric values in rows, plus response metadata for pagination and data-quality signals. The examples print those rows; production callers should retain the metadata and handle empty result sets explicitly.

Error Handling

ErrorCauseFix
400 INVALID_ARGUMENT: dimension X is incompatible with metric YCompatibility matrix violationUse check_compatibility to find a valid combination
400 The request must contain at least one valid dimensionAll dimensions in the list are invalid (typo, deprecated name)Check the Dimensions & metrics explorer
503 RESOURCE_EXHAUSTEDPer-property quota hitWait 1h or raise quota; batch fewer queries
Empty rows despite valid queryDate range outside data window OR property has no data for that periodSanity-check with a known-good query (e.g. activeUsers over today)

Resources

  • ga4-auth-setup — prerequisite
  • ga4-realtime-api — for "right now" data instead of runReport's ~24h lag
  • ga4-common-reports — copy-paste recipes for the canonical 6-7 reports
  • ga4-bigquery-export — when you've outgrown the Data API
저장한 Skills