chore: import upstream snapshot with attribution
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:58 +08:00
commit bb5c75ce05
8824 changed files with 1946442 additions and 0 deletions
@@ -0,0 +1,459 @@
#!/usr/bin/env python3
"""
Google Analytics Data Analysis Tool
Performs higher-level analysis on Google Analytics data including:
- Period comparisons (current vs previous)
- Trend detection
- Performance insights
- Automated recommendations
Usage:
python analyze.py --period last-30-days --compare previous-period
python analyze.py --analysis-type traffic-sources --days 30
python analyze.py --analysis-type funnel --steps "homepage,/products,/cart,/checkout"
"""
import os
import sys
import json
import argparse
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
try:
from ga_client import GoogleAnalyticsClient
except ImportError:
print("Error: ga_client.py not found in the same directory", file=sys.stderr)
sys.exit(1)
class AnalyticsAnalyzer:
"""Performs analysis on Google Analytics data."""
def __init__(self):
"""Initialize the analyzer with GA client."""
self.client = GoogleAnalyticsClient()
def compare_periods(
self, current_days: int = 30, metrics: Optional[List[str]] = None
) -> Dict:
"""
Compare current period with previous period.
Args:
current_days: Number of days in current period
metrics: List of metrics to compare (default: core metrics)
Returns:
Dictionary with comparison data and insights
"""
if metrics is None:
metrics = [
"sessions",
"activeUsers",
"newUsers",
"bounceRate",
"engagementRate",
"averageSessionDuration",
]
# Fetch current period
current = self.client.run_report(
start_date=f"{current_days}daysAgo",
end_date="yesterday",
metrics=metrics,
limit=1,
)
# Fetch previous period
previous_start = current_days * 2
previous_end = current_days + 1
previous = self.client.run_report(
start_date=f"{previous_start}daysAgo",
end_date=f"{previous_end}daysAgo",
metrics=metrics,
limit=1,
)
# Calculate changes
comparison = {
"current_period": f"Last {current_days} days",
"previous_period": f"Previous {current_days} days",
"metrics": {},
}
if current["totals"] and previous["totals"]:
for i, metric in enumerate(metrics):
current_val = float(current["totals"][i]["value"])
previous_val = float(previous["totals"][i]["value"])
# Calculate percentage change
if previous_val != 0:
change_pct = ((current_val - previous_val) / previous_val) * 100
else:
change_pct = 0
comparison["metrics"][metric] = {
"current": current_val,
"previous": previous_val,
"change": current_val - previous_val,
"change_percent": round(change_pct, 2),
}
# Generate insights
comparison["insights"] = self._generate_insights(comparison["metrics"])
return comparison
def analyze_traffic_sources(self, days: int = 30, limit: int = 20) -> Dict:
"""
Analyze traffic sources and their performance.
Args:
days: Number of days to analyze
limit: Number of sources to return
Returns:
Dictionary with source performance data and recommendations
"""
result = self.client.run_report(
start_date=f"{days}daysAgo",
end_date="yesterday",
metrics=["sessions", "engagementRate", "bounceRate", "conversions"],
dimensions=["sessionSource", "sessionMedium"],
limit=limit,
order_by="-sessions",
)
# Analyze sources
sources = []
for row in result["rows"]:
source = row["dimensions"]["sessionSource"]
medium = row["dimensions"]["sessionMedium"]
sessions = int(row["metrics"]["sessions"])
engagement = float(row["metrics"]["engagementRate"])
bounce = float(row["metrics"]["bounceRate"])
conversions = int(row["metrics"].get("conversions", 0))
conv_rate = (conversions / sessions * 100) if sessions > 0 else 0
sources.append(
{
"source": source,
"medium": medium,
"sessions": sessions,
"engagement_rate": round(engagement * 100, 2),
"bounce_rate": round(bounce * 100, 2),
"conversions": conversions,
"conversion_rate": round(conv_rate, 2),
}
)
analysis = {
"period": f"Last {days} days",
"sources": sources,
"recommendations": self._recommend_source_optimizations(sources),
}
return analysis
def analyze_content_performance(self, days: int = 30, limit: int = 50) -> Dict:
"""
Analyze page performance and identify issues.
Args:
days: Number of days to analyze
limit: Number of pages to return
Returns:
Dictionary with page performance and improvement opportunities
"""
result = self.client.run_report(
start_date=f"{days}daysAgo",
end_date="yesterday",
metrics=[
"screenPageViews",
"bounceRate",
"averageSessionDuration",
"conversions",
],
dimensions=["pagePath", "pageTitle"],
limit=limit,
order_by="-screenPageViews",
)
# Identify high-bounce pages
high_bounce_threshold = 0.6
problem_pages = []
for row in result["rows"]:
page_path = row["dimensions"]["pagePath"]
page_title = row["dimensions"]["pageTitle"]
views = int(row["metrics"]["screenPageViews"])
bounce = float(row["metrics"]["bounceRate"])
avg_duration = float(row["metrics"]["averageSessionDuration"])
if bounce > high_bounce_threshold and views > 100:
problem_pages.append(
{
"path": page_path,
"title": page_title,
"views": views,
"bounce_rate": round(bounce * 100, 2),
"avg_duration": round(avg_duration, 2),
"issue": self._diagnose_page_issue(bounce, avg_duration),
}
)
analysis = {
"period": f"Last {days} days",
"total_pages": result["row_count"],
"high_bounce_pages": len(problem_pages),
"problem_pages": problem_pages[:10], # Top 10 issues
"recommendations": self._recommend_content_improvements(problem_pages),
}
return analysis
def analyze_device_performance(self, days: int = 30) -> Dict:
"""
Compare performance across device types.
Args:
days: Number of days to analyze
Returns:
Dictionary with device performance comparison
"""
result = self.client.run_report(
start_date=f"{days}daysAgo",
end_date="yesterday",
metrics=[
"sessions",
"bounceRate",
"averageSessionDuration",
"conversions",
"engagementRate",
],
dimensions=["deviceCategory"],
limit=10,
order_by="-sessions",
)
devices = []
for row in result["rows"]:
device = row["dimensions"]["deviceCategory"]
sessions = int(row["metrics"]["sessions"])
bounce = float(row["metrics"]["bounceRate"])
duration = float(row["metrics"]["averageSessionDuration"])
conversions = int(row["metrics"].get("conversions", 0))
engagement = float(row["metrics"]["engagementRate"])
conv_rate = (conversions / sessions * 100) if sessions > 0 else 0
devices.append(
{
"device": device,
"sessions": sessions,
"bounce_rate": round(bounce * 100, 2),
"avg_duration": round(duration, 2),
"conversion_rate": round(conv_rate, 2),
"engagement_rate": round(engagement * 100, 2),
}
)
analysis = {
"period": f"Last {days} days",
"devices": devices,
"recommendations": self._recommend_device_optimizations(devices),
}
return analysis
def _generate_insights(self, metrics: Dict) -> List[str]:
"""Generate insights from metric comparisons."""
insights = []
for metric, data in metrics.items():
change_pct = data["change_percent"]
if abs(change_pct) < 2:
status = "stable"
elif change_pct > 0:
status = "improving"
else:
status = "declining"
# Add insights for significant changes
if abs(change_pct) >= 5:
direction = "increased" if change_pct > 0 else "decreased"
insights.append(
f"{metric.replace('_', ' ').title()}: {direction} by {abs(change_pct):.1f}%"
)
return insights
def _recommend_source_optimizations(self, sources: List[Dict]) -> List[Dict]:
"""Generate recommendations for traffic source optimization."""
recommendations = []
if not sources:
return recommendations
# Find best performing source
best_source = max(sources, key=lambda x: x["conversion_rate"])
recommendations.append(
{
"priority": "HIGH",
"action": f"Scale {best_source['source']}/{best_source['medium']}",
"reason": f"Highest conversion rate ({best_source['conversion_rate']}%)",
"expected_impact": "Increase overall conversions by 20-30%",
}
)
# Find high-traffic low-conversion sources
for source in sources[:5]: # Check top 5
if source["conversion_rate"] < 2.0 and source["sessions"] > 1000:
recommendations.append(
{
"priority": "MEDIUM",
"action": f"Optimize {source['source']}/{source['medium']}",
"reason": f"High traffic ({source['sessions']} sessions) but low conversion ({source['conversion_rate']}%)",
"expected_impact": "Potential conversion rate improvement of 50-100%",
}
)
return recommendations
def _recommend_content_improvements(self, problem_pages: List[Dict]) -> List[Dict]:
"""Generate recommendations for content improvements."""
recommendations = []
if not problem_pages:
recommendations.append(
{
"priority": "INFO",
"action": "Content performing well",
"reason": "No pages with critically high bounce rates",
"expected_impact": "Continue monitoring",
}
)
return recommendations
# Prioritize by traffic
problem_pages.sort(key=lambda x: x["views"], reverse=True)
for page in problem_pages[:3]: # Top 3 issues
recommendations.append(
{
"priority": "HIGH",
"action": f"Improve {page['path']}",
"reason": f"{page['issue']} ({page['bounce_rate']}% bounce rate)",
"expected_impact": "Reduce bounce rate by 20-30%",
}
)
return recommendations
def _recommend_device_optimizations(self, devices: List[Dict]) -> List[Dict]:
"""Generate recommendations for device optimization."""
recommendations = []
if len(devices) < 2:
return recommendations
# Compare mobile vs desktop
mobile = next((d for d in devices if d["device"] == "mobile"), None)
desktop = next((d for d in devices if d["device"] == "desktop"), None)
if mobile and desktop:
conv_diff = (
(desktop["conversion_rate"] - mobile["conversion_rate"])
/ desktop["conversion_rate"]
* 100
)
if conv_diff > 30: # Desktop significantly better
recommendations.append(
{
"priority": "CRITICAL",
"action": "Mobile experience optimization",
"reason": f"Mobile conversion rate {mobile['conversion_rate']}% vs desktop {desktop['conversion_rate']}%",
"expected_impact": "Improve mobile conversion by 30-50%",
}
)
return recommendations
def _diagnose_page_issue(self, bounce_rate: float, avg_duration: float) -> str:
"""Diagnose the issue with a high-bounce page."""
if bounce_rate > 0.7 and avg_duration < 30:
return "Content mismatch - users leave quickly"
elif bounce_rate > 0.6 and avg_duration > 60:
return "Missing CTA - users read but don't act"
elif bounce_rate > 0.6:
return "High bounce - needs investigation"
else:
return "Performance issue"
def main():
parser = argparse.ArgumentParser(
description="Analyze Google Analytics data",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--analysis-type",
choices=["overview", "sources", "content", "devices"],
default="overview",
help="Type of analysis to perform",
)
parser.add_argument(
"--days", type=int, default=30, help="Number of days to analyze (default: 30)"
)
parser.add_argument(
"--compare",
action="store_true",
help="Compare with previous period",
)
parser.add_argument(
"--output", help="Output file path (default: stdout)"
)
args = parser.parse_args()
try:
analyzer = AnalyticsAnalyzer()
# Run analysis
if args.analysis_type == "overview" and args.compare:
result = analyzer.compare_periods(current_days=args.days)
elif args.analysis_type == "sources":
result = analyzer.analyze_traffic_sources(days=args.days)
elif args.analysis_type == "content":
result = analyzer.analyze_content_performance(days=args.days)
elif args.analysis_type == "devices":
result = analyzer.analyze_device_performance(days=args.days)
else:
result = analyzer.compare_periods(current_days=args.days)
# Format output
output = json.dumps(result, indent=2)
# Write output
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Analysis saved to {args.output}", file=sys.stderr)
else:
print(output)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,345 @@
#!/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()