#!/usr/bin/env python3 """ Google Analytics 4 Data API Client Fetches analytics data from Google Analytics 4 using the Data API. Authentication uses service account credentials from environment variables. Usage: python ga_client.py --days 30 --metrics sessions,users python ga_client.py --start 2026-01-01 --end 2026-01-31 --dimensions country python ga_client.py --days 7 --metrics sessions --dimensions pagePath --limit 10 Environment Variables: GOOGLE_ANALYTICS_PROPERTY_ID: GA4 property ID (required) GOOGLE_APPLICATION_CREDENTIALS: Path to service account JSON (required) """ import os import sys import json import argparse from datetime import datetime, timedelta from typing import List, Dict, Optional try: from google.analytics.data_v1beta import BetaAnalyticsDataClient from google.analytics.data_v1beta.types import ( DateRange, Dimension, Metric, RunReportRequest, OrderBy, FilterExpression, Filter, ) from dotenv import load_dotenv except ImportError as e: print(f"Error: Required package not installed: {e}", file=sys.stderr) print("Install with: pip install google-analytics-data python-dotenv", file=sys.stderr) sys.exit(1) class GoogleAnalyticsClient: """Client for interacting with Google Analytics 4 Data API.""" def __init__(self): """Initialize the client with credentials from environment.""" load_dotenv() # Load from .env file if present self.property_id = os.environ.get("GOOGLE_ANALYTICS_PROPERTY_ID") if not self.property_id: raise ValueError( "GOOGLE_ANALYTICS_PROPERTY_ID environment variable not set. " "Find your property ID in GA4: Admin > Property Settings" ) credentials_path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") if not credentials_path: raise ValueError( "GOOGLE_APPLICATION_CREDENTIALS environment variable not set. " "Set it to the path of your service account JSON file." ) if not os.path.exists(credentials_path): raise FileNotFoundError( f"Service account file not found: {credentials_path}" ) try: self.client = BetaAnalyticsDataClient() except Exception as e: raise RuntimeError( f"Failed to initialize Google Analytics client: {e}\n" "Verify your service account has access to the GA4 property." ) def run_report( self, start_date: str, end_date: str, metrics: List[str], dimensions: Optional[List[str]] = None, limit: int = 10, order_by: Optional[str] = None, filter_expression: Optional[str] = None, ) -> Dict: """ Run a report query against Google Analytics. Args: start_date: Start date (YYYY-MM-DD or 'NdaysAgo') end_date: End date (YYYY-MM-DD or 'today'/'yesterday') metrics: List of metric names (e.g., ['sessions', 'users']) dimensions: List of dimension names (e.g., ['country', 'city']) limit: Maximum number of rows to return order_by: Metric or dimension to sort by filter_expression: Filter to apply (dimension_name:value) Returns: Dictionary with report data and metadata """ # Build request request = RunReportRequest( property=f"properties/{self.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 or [])], limit=limit, ) # Add ordering if order_by: desc = True if order_by.startswith("+"): desc = False order_by = order_by[1:] elif order_by.startswith("-"): order_by = order_by[1:] # Check if it's a metric or dimension if order_by in metrics: request.order_bys = [ OrderBy(metric=OrderBy.MetricOrderBy(metric_name=order_by), desc=desc) ] elif dimensions and order_by in dimensions: request.order_bys = [ OrderBy( dimension=OrderBy.DimensionOrderBy(dimension_name=order_by), desc=desc, ) ] # Add filter if filter_expression and ":" in filter_expression: field_name, value = filter_expression.split(":", 1) request.dimension_filter = FilterExpression( filter=Filter( field_name=field_name, string_filter=Filter.StringFilter(value=value), ) ) try: response = self.client.run_report(request) except Exception as e: raise RuntimeError(f"Failed to run report: {e}") # Parse response return self._parse_response(response) def _parse_response(self, response) -> Dict: """Parse API response into a structured dictionary.""" result = { "dimension_headers": [h.name for h in response.dimension_headers], "metric_headers": [ {"name": h.name, "type": h.type_.name} for h in response.metric_headers ], "rows": [], "row_count": response.row_count, "metadata": {}, } # Add totals if present if response.totals: result["totals"] = [ {"value": v.value} for v in response.totals[0].metric_values ] # Parse rows for row in response.rows: parsed_row = { "dimensions": {}, "metrics": {}, } # Dimension values for i, value in enumerate(row.dimension_values): dim_name = result["dimension_headers"][i] parsed_row["dimensions"][dim_name] = value.value # Metric values for i, value in enumerate(row.metric_values): metric_info = result["metric_headers"][i] metric_name = metric_info["name"] parsed_row["metrics"][metric_name] = value.value result["rows"].append(parsed_row) return result def parse_date_range(days: Optional[int], start: Optional[str], end: Optional[str]): """Parse date range arguments into start and end dates.""" if days: return f"{days}daysAgo", "yesterday" elif start and end: return start, end else: # Default to last 7 days return "7daysAgo", "yesterday" def main(): parser = argparse.ArgumentParser( description="Fetch Google Analytics 4 data", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Last 30 days of sessions and users python ga_client.py --days 30 --metrics sessions,users # Specific date range with dimensions python ga_client.py --start 2026-01-01 --end 2026-01-31 \\ --metrics sessions,bounceRate --dimensions country,city # Top pages by views python ga_client.py --days 7 --metrics screenPageViews \\ --dimensions pagePath --order-by screenPageViews --limit 20 # Filter by country python ga_client.py --days 30 --metrics sessions \\ --dimensions country --filter "country:United States" """, ) # Date range arguments date_group = parser.add_mutually_exclusive_group() date_group.add_argument( "--days", type=int, help="Number of days to look back (e.g., 30)" ) date_group.add_argument( "--start", help="Start date (YYYY-MM-DD or 'NdaysAgo')" ) parser.add_argument("--end", help="End date (YYYY-MM-DD or 'today'/'yesterday')") # Query arguments parser.add_argument( "--metrics", required=True, help="Comma-separated list of metrics (e.g., sessions,users,bounceRate)", ) parser.add_argument( "--dimensions", help="Comma-separated list of dimensions (e.g., country,city,deviceCategory)", ) parser.add_argument( "--limit", type=int, default=10, help="Maximum rows to return (default: 10)" ) parser.add_argument( "--order-by", help="Metric or dimension to sort by (prefix with - for desc, + for asc)", ) parser.add_argument( "--filter", help="Filter expression (e.g., 'country:United States')" ) # Output arguments parser.add_argument( "--format", choices=["json", "table"], default="json", help="Output format (default: json)", ) parser.add_argument( "--output", help="Output file path (default: stdout)" ) args = parser.parse_args() try: # Initialize client client = GoogleAnalyticsClient() # Parse date range start_date, end_date = parse_date_range(args.days, args.start, args.end) # Parse metrics and dimensions metrics = [m.strip() for m in args.metrics.split(",")] dimensions = ( [d.strip() for d in args.dimensions.split(",")] if args.dimensions else None ) # Run report result = client.run_report( start_date=start_date, end_date=end_date, metrics=metrics, dimensions=dimensions, limit=args.limit, order_by=args.order_by, filter_expression=args.filter, ) # Format output if args.format == "json": output = json.dumps(result, indent=2) else: # table format output = format_as_table(result) # Write output if args.output: with open(args.output, "w") as f: f.write(output) print(f"Report saved to {args.output}", file=sys.stderr) else: print(output) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) def format_as_table(result: Dict) -> str: """Format result as a human-readable table.""" lines = [] # Header headers = result["dimension_headers"] + [m["name"] for m in result["metric_headers"]] lines.append(" | ".join(headers)) lines.append("-" * (len(" | ".join(headers)))) # Rows for row in result["rows"]: values = [] for dim in result["dimension_headers"]: values.append(row["dimensions"].get(dim, "")) for metric in result["metric_headers"]: values.append(row["metrics"].get(metric["name"], "")) lines.append(" | ".join(values)) # Totals if "totals" in result: lines.append("-" * (len(" | ".join(headers)))) total_values = ["TOTAL"] + [""] * (len(result["dimension_headers"]) - 1) total_values += [t["value"] for t in result["totals"]] lines.append(" | ".join(total_values)) lines.append("") lines.append(f"Total rows: {result['row_count']}") return "\n".join(lines) if __name__ == "__main__": main()