first commit
This commit is contained in:
803
scripts/reports/fetch_monthly_report_data.py
Normal file
803
scripts/reports/fetch_monthly_report_data.py
Normal file
@@ -0,0 +1,803 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pobiera dane z Google Ads API + GA4 za wskazany miesiąc i zapisuje jako JSON.
|
||||
|
||||
Użycie:
|
||||
python scripts/reports/fetch_monthly_report_data.py --customer studio-zoe.pl --month 2026-02
|
||||
python scripts/reports/fetch_monthly_report_data.py --customer 3871661050 --month 2026-02 --output output/report.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import calendar
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import io
|
||||
import tomllib
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from lib.gads_client import get_client, get_customer_id, run_query
|
||||
import requests
|
||||
|
||||
ROOT = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from src.gads_v2.config import load_env
|
||||
|
||||
load_env(ROOT / ".env")
|
||||
|
||||
|
||||
def load_client_report_config(domain):
|
||||
"""Load scalar report settings for a client from config/clients.toml."""
|
||||
config_path = ROOT / "config" / "clients.toml"
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||||
return data.get("clients", {}).get(domain, {})
|
||||
|
||||
|
||||
def parse_month(month_str):
|
||||
"""Parse YYYY-MM to (year, month) and calculate date range."""
|
||||
year, month = map(int, month_str.split("-"))
|
||||
last_day = calendar.monthrange(year, month)[1]
|
||||
start = f"{year}-{month:02d}-01"
|
||||
end = f"{year}-{month:02d}-{last_day:02d}"
|
||||
return year, month, start, end
|
||||
|
||||
|
||||
def prev_month(year, month):
|
||||
"""Calculate previous month's date range."""
|
||||
if month == 1:
|
||||
py, pm = year - 1, 12
|
||||
else:
|
||||
py, pm = year, month - 1
|
||||
last_day = calendar.monthrange(py, pm)[1]
|
||||
start = f"{py}-{pm:02d}-01"
|
||||
end = f"{py}-{pm:02d}-{last_day:02d}"
|
||||
return py, pm, start, end
|
||||
|
||||
|
||||
def pct_change(current, previous):
|
||||
"""Calculate percentage change, handling zero division."""
|
||||
if previous == 0:
|
||||
return 100.0 if current > 0 else 0.0
|
||||
return round(((current - previous) / previous) * 100, 1)
|
||||
|
||||
|
||||
def normalize_header(value):
|
||||
value = (value or "").strip().lower()
|
||||
replacements = {
|
||||
"ą": "a",
|
||||
"ć": "c",
|
||||
"ę": "e",
|
||||
"ł": "l",
|
||||
"ń": "n",
|
||||
"ó": "o",
|
||||
"ś": "s",
|
||||
"ź": "z",
|
||||
"ż": "z",
|
||||
}
|
||||
for src, dst in replacements.items():
|
||||
value = value.replace(src, dst)
|
||||
return re.sub(r"[^a-z0-9]+", "", value)
|
||||
|
||||
|
||||
def parse_money(value):
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return 0.0
|
||||
text = text.replace("PLN", "").replace("zl", "").replace("zł", "")
|
||||
text = text.replace("\u00a0", " ").replace(" ", "")
|
||||
if "," in text and "." in text:
|
||||
text = text.replace(".", "").replace(",", ".")
|
||||
elif "," in text:
|
||||
text = text.replace(",", ".")
|
||||
text = re.sub(r"[^0-9.\-]", "", text)
|
||||
return round(float(text), 2) if text else 0.0
|
||||
|
||||
|
||||
def parse_int_value(value):
|
||||
return int(round(parse_money(value)))
|
||||
|
||||
|
||||
def parse_history_month(value):
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
if re.fullmatch(r"\d{4}-\d{2}", text):
|
||||
return text
|
||||
if re.fullmatch(r"\d{2}[.-]\d{4}", text):
|
||||
month, year = re.split(r"[.-]", text)
|
||||
return f"{int(year):04d}-{int(month):02d}"
|
||||
if re.fullmatch(r"\d{4}[./-]\d{1,2}[./-]\d{1,2}", text):
|
||||
year, month, _day = re.split(r"[./-]", text)
|
||||
return f"{int(year):04d}-{int(month):02d}"
|
||||
if re.fullmatch(r"\d{1,2}[./-]\d{1,2}[./-]\d{4}", text):
|
||||
_day, month, year = re.split(r"[./-]", text)
|
||||
return f"{int(year):04d}-{int(month):02d}"
|
||||
return text
|
||||
|
||||
|
||||
def parse_sheet_config(sheet_config):
|
||||
value = str(sheet_config or "").strip()
|
||||
if not value:
|
||||
return "", "0"
|
||||
if value.startswith("http"):
|
||||
parsed = urlparse(value)
|
||||
match = re.search(r"/spreadsheets/d/([^/]+)", parsed.path)
|
||||
spreadsheet_id = match.group(1) if match else value
|
||||
query_gid = parse_qs(parsed.query).get("gid", [None])[0]
|
||||
fragment_gid = parse_qs(parsed.fragment).get("gid", [None])[0]
|
||||
return spreadsheet_id, query_gid or fragment_gid or ""
|
||||
if ":" in value:
|
||||
return value.split(":", 1)
|
||||
return value, ""
|
||||
|
||||
|
||||
def fetch_sales_history_from_sheet(domain, sheet_config):
|
||||
"""Fetch monthly sales history from a public Google Sheet CSV export."""
|
||||
spreadsheet_id, gid = parse_sheet_config(sheet_config)
|
||||
export_url = f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}/gviz/tq?tqx=out:csv"
|
||||
if gid:
|
||||
export_url += f"&gid={gid}"
|
||||
response = requests.get(export_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
response.encoding = "utf-8"
|
||||
|
||||
reader = csv.DictReader(io.StringIO(response.text))
|
||||
history = []
|
||||
for row in reader:
|
||||
normalized = {normalize_header(key): value for key, value in row.items()}
|
||||
month = parse_history_month(
|
||||
normalized.get("month")
|
||||
or normalized.get("miesiac")
|
||||
or normalized.get("data")
|
||||
or normalized.get("date")
|
||||
)
|
||||
revenue = parse_money(
|
||||
normalized.get("revenue")
|
||||
or normalized.get("przychod")
|
||||
or normalized.get("przychody")
|
||||
or normalized.get("sprzedaz")
|
||||
or normalized.get("wartosc")
|
||||
)
|
||||
transactions = parse_int_value(
|
||||
normalized.get("transactions")
|
||||
or normalized.get("transakcje")
|
||||
or normalized.get("zamowienia")
|
||||
or normalized.get("orders")
|
||||
)
|
||||
if not month or not revenue:
|
||||
continue
|
||||
aov = parse_money(
|
||||
normalized.get("aov")
|
||||
or normalized.get("sredniakoszyka")
|
||||
or normalized.get("sredniawartosckoszyka")
|
||||
or normalized.get("sredniawartosczamowienia")
|
||||
)
|
||||
if not aov and transactions:
|
||||
aov = round(revenue / transactions, 2)
|
||||
history.append({
|
||||
"month": month,
|
||||
"transactions": transactions,
|
||||
"revenue": revenue,
|
||||
"aov": aov,
|
||||
"source": "google_sheet",
|
||||
})
|
||||
return sorted(history, key=lambda item: item["month"])
|
||||
|
||||
|
||||
def apply_sheet_ecommerce(report, sales_history, month, previous_month):
|
||||
"""Use Google Sheet sales data for e-commerce KPI cards."""
|
||||
by_month = {row["month"]: row for row in sales_history}
|
||||
current = by_month.get(month)
|
||||
if not current:
|
||||
return False
|
||||
previous = by_month.get(previous_month, {"transactions": 0, "revenue": 0.0, "aov": 0.0})
|
||||
has_previous = previous_month in by_month
|
||||
ecommerce = (report.get("ga4") or {}).get("ecommerce") or {}
|
||||
ecommerce["current"] = {
|
||||
"transactions": current.get("transactions", 0),
|
||||
"revenue": current.get("revenue", 0.0),
|
||||
"aov": current.get("aov", 0.0),
|
||||
}
|
||||
ecommerce["previous"] = {
|
||||
"transactions": previous.get("transactions", 0),
|
||||
"revenue": previous.get("revenue", 0.0),
|
||||
"aov": previous.get("aov", 0.0),
|
||||
}
|
||||
ecommerce["mom_change"] = {
|
||||
"transactions_pct": pct_change(ecommerce["current"]["transactions"], ecommerce["previous"]["transactions"]) if has_previous else None,
|
||||
"revenue_pct": pct_change(ecommerce["current"]["revenue"], ecommerce["previous"]["revenue"]) if has_previous else None,
|
||||
"aov_pct": pct_change(ecommerce["current"]["aov"], ecommerce["previous"]["aov"]) if has_previous else None,
|
||||
}
|
||||
ecommerce["source"] = "google_sheet"
|
||||
if report.get("ga4") is None:
|
||||
report["ga4"] = {}
|
||||
report["ga4"]["ecommerce"] = ecommerce
|
||||
return True
|
||||
|
||||
|
||||
def fetch_google_ads_data(client, customer_id, start_date, end_date):
|
||||
"""Fetch campaign metrics for a date range."""
|
||||
query = f"""
|
||||
SELECT campaign.id, campaign.name, campaign.status,
|
||||
campaign.advertising_channel_type,
|
||||
metrics.impressions, metrics.clicks,
|
||||
metrics.cost_micros, metrics.conversions,
|
||||
metrics.conversions_value,
|
||||
metrics.ctr, metrics.average_cpc
|
||||
FROM campaign
|
||||
WHERE segments.date BETWEEN '{start_date}' AND '{end_date}'
|
||||
AND campaign.status != 'REMOVED'
|
||||
"""
|
||||
rows = run_query(client, customer_id, query)
|
||||
|
||||
campaigns = {}
|
||||
for r in rows:
|
||||
cid = str(r.campaign.id)
|
||||
if cid not in campaigns:
|
||||
campaigns[cid] = {
|
||||
"id": cid,
|
||||
"name": r.campaign.name,
|
||||
"status": r.campaign.status.name,
|
||||
"type": r.campaign.advertising_channel_type.name,
|
||||
"impressions": 0, "clicks": 0, "cost": 0.0,
|
||||
"conversions": 0.0, "conversion_value": 0.0,
|
||||
"ctr": 0.0, "cpc": 0.0,
|
||||
}
|
||||
c = campaigns[cid]
|
||||
c["impressions"] += r.metrics.impressions
|
||||
c["clicks"] += r.metrics.clicks
|
||||
c["cost"] += r.metrics.cost_micros / 1_000_000
|
||||
c["conversions"] += r.metrics.conversions
|
||||
c["conversion_value"] += r.metrics.conversions_value
|
||||
|
||||
# Calculate derived metrics
|
||||
for c in campaigns.values():
|
||||
c["cost"] = round(c["cost"], 2)
|
||||
c["conversions"] = round(c["conversions"], 1)
|
||||
c["conversion_value"] = round(c["conversion_value"], 2)
|
||||
c["ctr"] = round((c["clicks"] / c["impressions"] * 100) if c["impressions"] else 0, 2)
|
||||
c["cpc"] = round((c["cost"] / c["clicks"]) if c["clicks"] else 0, 2)
|
||||
c["cpa"] = round((c["cost"] / c["conversions"]) if c["conversions"] else 0, 2)
|
||||
c["roas"] = round((c["conversion_value"] / c["cost"]) if c["cost"] else 0, 2)
|
||||
|
||||
return list(campaigns.values())
|
||||
|
||||
|
||||
def calc_totals(campaigns):
|
||||
"""Sum up totals across campaigns."""
|
||||
t = {"impressions": 0, "clicks": 0, "cost": 0.0, "conversions": 0.0, "conversion_value": 0.0}
|
||||
for c in campaigns:
|
||||
t["impressions"] += c["impressions"]
|
||||
t["clicks"] += c["clicks"]
|
||||
t["cost"] += c["cost"]
|
||||
t["conversions"] += c["conversions"]
|
||||
t["conversion_value"] += c.get("conversion_value", 0.0)
|
||||
|
||||
t["cost"] = round(t["cost"], 2)
|
||||
t["conversions"] = round(t["conversions"], 1)
|
||||
t["conversion_value"] = round(t["conversion_value"], 2)
|
||||
t["ctr"] = round((t["clicks"] / t["impressions"] * 100) if t["impressions"] else 0, 2)
|
||||
t["cpc"] = round((t["cost"] / t["clicks"]) if t["clicks"] else 0, 2)
|
||||
t["cpa"] = round((t["cost"] / t["conversions"]) if t["conversions"] else 0, 2)
|
||||
t["roas"] = round((t["conversion_value"] / t["cost"]) if t["cost"] else 0, 2)
|
||||
return t
|
||||
|
||||
|
||||
def fetch_daily_data(client, customer_id, start_date, end_date):
|
||||
"""Fetch daily breakdown for charts."""
|
||||
query = f"""
|
||||
SELECT segments.date,
|
||||
metrics.impressions, metrics.clicks, metrics.cost_micros
|
||||
FROM campaign
|
||||
WHERE segments.date BETWEEN '{start_date}' AND '{end_date}'
|
||||
AND campaign.status != 'REMOVED'
|
||||
"""
|
||||
rows = run_query(client, customer_id, query)
|
||||
|
||||
daily = {}
|
||||
for r in rows:
|
||||
d = r.segments.date
|
||||
if d not in daily:
|
||||
daily[d] = {"date": d, "impressions": 0, "clicks": 0, "cost": 0.0}
|
||||
daily[d]["impressions"] += r.metrics.impressions
|
||||
daily[d]["clicks"] += r.metrics.clicks
|
||||
daily[d]["cost"] += r.metrics.cost_micros / 1_000_000
|
||||
|
||||
result = sorted(daily.values(), key=lambda x: x["date"])
|
||||
for d in result:
|
||||
d["cost"] = round(d["cost"], 2)
|
||||
return result
|
||||
|
||||
|
||||
def fetch_search_terms(client, customer_id, start_date, end_date, limit=15):
|
||||
"""Fetch top search terms by clicks."""
|
||||
query = f"""
|
||||
SELECT search_term_view.search_term,
|
||||
metrics.impressions, metrics.clicks,
|
||||
metrics.cost_micros, metrics.conversions
|
||||
FROM search_term_view
|
||||
WHERE segments.date BETWEEN '{start_date}' AND '{end_date}'
|
||||
ORDER BY metrics.clicks DESC
|
||||
LIMIT {limit}
|
||||
"""
|
||||
rows = run_query(client, customer_id, query)
|
||||
|
||||
terms = []
|
||||
for r in rows:
|
||||
clicks = r.metrics.clicks
|
||||
impressions = r.metrics.impressions
|
||||
terms.append({
|
||||
"term": r.search_term_view.search_term,
|
||||
"impressions": impressions,
|
||||
"clicks": clicks,
|
||||
"cost": round(r.metrics.cost_micros / 1_000_000, 2),
|
||||
"conversions": round(r.metrics.conversions, 1),
|
||||
"ctr": round((clicks / impressions * 100) if impressions else 0, 2),
|
||||
})
|
||||
return terms
|
||||
|
||||
|
||||
def fetch_ga4_data(property_id, start_date, end_date, prev_start, prev_end):
|
||||
"""Fetch GA4 data: sessions, users, traffic sources, devices."""
|
||||
from google.oauth2.credentials import Credentials
|
||||
from google.analytics.data_v1beta import BetaAnalyticsDataClient
|
||||
from google.analytics.data_v1beta.types import (
|
||||
RunReportRequest, DateRange, Metric, Dimension, OrderBy,
|
||||
)
|
||||
|
||||
credentials = Credentials(
|
||||
token=None,
|
||||
refresh_token=os.environ["GA4_REFRESH_TOKEN"],
|
||||
client_id=os.environ["GOOGLE_ADS_OAUTH2_CLIENT_ID"],
|
||||
client_secret=os.environ["GOOGLE_ADS_OAUTH2_CLIENT_SECRET"],
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
)
|
||||
client = BetaAnalyticsDataClient(credentials=credentials)
|
||||
prop = f"properties/{property_id}"
|
||||
|
||||
# 1. Sessions & Users (current + previous month)
|
||||
def get_totals(sd, ed):
|
||||
resp = client.run_report(RunReportRequest(
|
||||
property=prop,
|
||||
date_ranges=[DateRange(start_date=sd, end_date=ed)],
|
||||
metrics=[
|
||||
Metric(name="sessions"),
|
||||
Metric(name="totalUsers"),
|
||||
Metric(name="newUsers"),
|
||||
Metric(name="screenPageViews"),
|
||||
Metric(name="averageSessionDuration"),
|
||||
Metric(name="bounceRate"),
|
||||
],
|
||||
))
|
||||
row = resp.rows[0] if resp.rows else None
|
||||
if not row:
|
||||
return {"sessions": 0, "users": 0, "new_users": 0, "pageviews": 0, "avg_duration": 0, "bounce_rate": 0}
|
||||
return {
|
||||
"sessions": int(row.metric_values[0].value),
|
||||
"users": int(row.metric_values[1].value),
|
||||
"new_users": int(row.metric_values[2].value),
|
||||
"pageviews": int(row.metric_values[3].value),
|
||||
"avg_duration": round(float(row.metric_values[4].value), 1),
|
||||
"bounce_rate": round(float(row.metric_values[5].value) * 100, 1),
|
||||
}
|
||||
|
||||
current = get_totals(start_date, end_date)
|
||||
previous = get_totals(prev_start, prev_end)
|
||||
|
||||
# 2. Traffic sources
|
||||
resp = client.run_report(RunReportRequest(
|
||||
property=prop,
|
||||
date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
|
||||
dimensions=[Dimension(name="sessionSourceMedium")],
|
||||
metrics=[Metric(name="sessions")],
|
||||
order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="sessions"), desc=True)],
|
||||
limit=10,
|
||||
))
|
||||
sources = []
|
||||
for row in resp.rows:
|
||||
sources.append({
|
||||
"source_medium": row.dimension_values[0].value,
|
||||
"sessions": int(row.metric_values[0].value),
|
||||
})
|
||||
|
||||
# 3. Devices
|
||||
resp = client.run_report(RunReportRequest(
|
||||
property=prop,
|
||||
date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
|
||||
dimensions=[Dimension(name="deviceCategory")],
|
||||
metrics=[Metric(name="sessions")],
|
||||
order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="sessions"), desc=True)],
|
||||
))
|
||||
devices = []
|
||||
for row in resp.rows:
|
||||
devices.append({
|
||||
"device": row.dimension_values[0].value,
|
||||
"sessions": int(row.metric_values[0].value),
|
||||
})
|
||||
|
||||
# 4. Daily sessions (for chart)
|
||||
resp = client.run_report(RunReportRequest(
|
||||
property=prop,
|
||||
date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
|
||||
dimensions=[Dimension(name="date")],
|
||||
metrics=[Metric(name="sessions"), Metric(name="totalUsers")],
|
||||
order_bys=[OrderBy(dimension=OrderBy.DimensionOrderBy(dimension_name="date"))],
|
||||
))
|
||||
daily_sessions = []
|
||||
for row in resp.rows:
|
||||
raw = row.dimension_values[0].value
|
||||
formatted = f"{raw[:4]}-{raw[4:6]}-{raw[6:]}"
|
||||
daily_sessions.append({
|
||||
"date": formatted,
|
||||
"sessions": int(row.metric_values[0].value),
|
||||
"users": int(row.metric_values[1].value),
|
||||
})
|
||||
|
||||
return {
|
||||
"current": current,
|
||||
"previous": previous,
|
||||
"mom_change": {
|
||||
"sessions_pct": pct_change(current["sessions"], previous["sessions"]),
|
||||
"users_pct": pct_change(current["users"], previous["users"]),
|
||||
"new_users_pct": pct_change(current["new_users"], previous["new_users"]),
|
||||
"pageviews_pct": pct_change(current["pageviews"], previous["pageviews"]),
|
||||
"avg_duration_pct": pct_change(current["avg_duration"], previous["avg_duration"]),
|
||||
"bounce_rate_pct": pct_change(current["bounce_rate"], previous["bounce_rate"]),
|
||||
},
|
||||
"sources": sources,
|
||||
"devices": devices,
|
||||
"daily": daily_sessions,
|
||||
}
|
||||
|
||||
|
||||
def fetch_ga4_ecommerce(property_id, start_date, end_date, prev_start, prev_end):
|
||||
"""Fetch GA4 e-commerce data: transactions, revenue, AOV."""
|
||||
from google.oauth2.credentials import Credentials
|
||||
from google.analytics.data_v1beta import BetaAnalyticsDataClient
|
||||
from google.analytics.data_v1beta.types import (
|
||||
RunReportRequest, DateRange, Metric, Dimension, OrderBy,
|
||||
)
|
||||
|
||||
credentials = Credentials(
|
||||
token=None,
|
||||
refresh_token=os.environ["GA4_REFRESH_TOKEN"],
|
||||
client_id=os.environ["GOOGLE_ADS_OAUTH2_CLIENT_ID"],
|
||||
client_secret=os.environ["GOOGLE_ADS_OAUTH2_CLIENT_SECRET"],
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
)
|
||||
client = BetaAnalyticsDataClient(credentials=credentials)
|
||||
prop = f"properties/{property_id}"
|
||||
|
||||
def get_ecom(sd, ed):
|
||||
resp = client.run_report(RunReportRequest(
|
||||
property=prop,
|
||||
date_ranges=[DateRange(start_date=sd, end_date=ed)],
|
||||
metrics=[
|
||||
Metric(name="transactions"),
|
||||
Metric(name="purchaseRevenue"),
|
||||
Metric(name="averagePurchaseRevenue"),
|
||||
],
|
||||
))
|
||||
row = resp.rows[0] if resp.rows else None
|
||||
if not row:
|
||||
return {"transactions": 0, "revenue": 0.0, "aov": 0.0}
|
||||
return {
|
||||
"transactions": int(row.metric_values[0].value),
|
||||
"revenue": round(float(row.metric_values[1].value), 2),
|
||||
"aov": round(float(row.metric_values[2].value), 2),
|
||||
}
|
||||
|
||||
current = get_ecom(start_date, end_date)
|
||||
previous = get_ecom(prev_start, prev_end)
|
||||
|
||||
# Daily revenue chart
|
||||
resp = client.run_report(RunReportRequest(
|
||||
property=prop,
|
||||
date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
|
||||
dimensions=[Dimension(name="date")],
|
||||
metrics=[Metric(name="purchaseRevenue"), Metric(name="transactions")],
|
||||
order_bys=[OrderBy(dimension=OrderBy.DimensionOrderBy(dimension_name="date"))],
|
||||
))
|
||||
daily_revenue = []
|
||||
for row in resp.rows:
|
||||
raw = row.dimension_values[0].value
|
||||
formatted = f"{raw[:4]}-{raw[4:6]}-{raw[6:]}"
|
||||
daily_revenue.append({
|
||||
"date": formatted,
|
||||
"revenue": round(float(row.metric_values[0].value), 2),
|
||||
"transactions": int(row.metric_values[1].value),
|
||||
})
|
||||
|
||||
# Revenue by source
|
||||
resp = client.run_report(RunReportRequest(
|
||||
property=prop,
|
||||
date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
|
||||
dimensions=[Dimension(name="sessionSourceMedium")],
|
||||
metrics=[Metric(name="purchaseRevenue"), Metric(name="transactions")],
|
||||
order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="purchaseRevenue"), desc=True)],
|
||||
limit=10,
|
||||
))
|
||||
revenue_by_source = []
|
||||
for row in resp.rows:
|
||||
revenue_by_source.append({
|
||||
"source_medium": row.dimension_values[0].value,
|
||||
"revenue": round(float(row.metric_values[0].value), 2),
|
||||
"transactions": int(row.metric_values[1].value),
|
||||
})
|
||||
|
||||
# Top products by revenue
|
||||
resp = client.run_report(RunReportRequest(
|
||||
property=prop,
|
||||
date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
|
||||
dimensions=[Dimension(name="itemName")],
|
||||
metrics=[
|
||||
Metric(name="itemRevenue"),
|
||||
Metric(name="itemsPurchased"),
|
||||
],
|
||||
order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="itemRevenue"), desc=True)],
|
||||
limit=10,
|
||||
))
|
||||
top_products = []
|
||||
for row in resp.rows:
|
||||
top_products.append({
|
||||
"name": row.dimension_values[0].value,
|
||||
"revenue": round(float(row.metric_values[0].value), 2),
|
||||
"quantity": int(row.metric_values[1].value),
|
||||
})
|
||||
|
||||
return {
|
||||
"current": current,
|
||||
"previous": previous,
|
||||
"mom_change": {
|
||||
"transactions_pct": pct_change(current["transactions"], previous["transactions"]),
|
||||
"revenue_pct": pct_change(current["revenue"], previous["revenue"]),
|
||||
"aov_pct": pct_change(current["aov"], previous["aov"]),
|
||||
},
|
||||
"daily": daily_revenue,
|
||||
"revenue_by_source": revenue_by_source,
|
||||
"top_products": top_products,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Pobierz dane do raportu miesięcznego")
|
||||
parser.add_argument("--customer", required=True, help="Domena lub Google Ads customer ID")
|
||||
parser.add_argument("--month", required=True, help="Miesiąc raportu (YYYY-MM)")
|
||||
parser.add_argument("--output", help="Ścieżka do pliku JSON")
|
||||
parser.add_argument("--ga4-property", help="GA4 Property ID (domyślnie z .env)")
|
||||
parser.add_argument("--skip-ga4", action="store_true", help="Pomiń dane GA4")
|
||||
args = parser.parse_args()
|
||||
|
||||
customer_id = get_customer_id(args.customer)
|
||||
client = get_client(use_proto_plus=True)
|
||||
|
||||
year, month, start_date, end_date = parse_month(args.month)
|
||||
py, pm, prev_start, prev_end = prev_month(year, month)
|
||||
month_names_pl = {
|
||||
1: "Styczeń", 2: "Luty", 3: "Marzec", 4: "Kwiecień",
|
||||
5: "Maj", 6: "Czerwiec", 7: "Lipiec", 8: "Sierpień",
|
||||
9: "Wrzesień", 10: "Październik", 11: "Listopad", 12: "Grudzień",
|
||||
}
|
||||
|
||||
# Resolve domain name for output
|
||||
domain = args.customer if not args.customer.replace("-", "").isdigit() else args.customer
|
||||
|
||||
print(f"Pobieram dane Google Ads: {domain} za {args.month}...")
|
||||
|
||||
# Google Ads data
|
||||
campaigns = fetch_google_ads_data(client, customer_id, start_date, end_date)
|
||||
prev_campaigns = fetch_google_ads_data(client, customer_id, prev_start, prev_end)
|
||||
totals = calc_totals(campaigns)
|
||||
prev_totals = calc_totals(prev_campaigns)
|
||||
daily = fetch_daily_data(client, customer_id, start_date, end_date)
|
||||
search_terms = fetch_search_terms(client, customer_id, start_date, end_date)
|
||||
|
||||
mom_change = {
|
||||
"impressions_pct": pct_change(totals["impressions"], prev_totals["impressions"]),
|
||||
"clicks_pct": pct_change(totals["clicks"], prev_totals["clicks"]),
|
||||
"cost_pct": pct_change(totals["cost"], prev_totals["cost"]),
|
||||
"conversions_pct": pct_change(totals["conversions"], prev_totals["conversions"]),
|
||||
"conversion_value_pct": pct_change(totals["conversion_value"], prev_totals["conversion_value"]),
|
||||
"ctr_pct": pct_change(totals["ctr"], prev_totals["ctr"]),
|
||||
"cpc_pct": pct_change(totals["cpc"], prev_totals["cpc"]),
|
||||
"cpa_pct": pct_change(totals["cpa"], prev_totals["cpa"]),
|
||||
"roas_pct": pct_change(totals["roas"], prev_totals["roas"]),
|
||||
}
|
||||
|
||||
report = {
|
||||
"client": domain,
|
||||
"month": args.month,
|
||||
"month_name": month_names_pl[month],
|
||||
"year": year,
|
||||
"prev_month": f"{py}-{pm:02d}",
|
||||
"prev_month_name": month_names_pl[pm],
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"google_ads": {
|
||||
"campaigns": campaigns,
|
||||
"totals": totals,
|
||||
"prev_totals": prev_totals,
|
||||
"mom_change": mom_change,
|
||||
"daily": daily,
|
||||
"search_terms": search_terms,
|
||||
},
|
||||
}
|
||||
|
||||
# GA4 data
|
||||
if not args.skip_ga4:
|
||||
ga4_property = args.ga4_property
|
||||
if not ga4_property:
|
||||
# Try to find GA4 property in .env
|
||||
env_key = f"GA4_PROPERTY_ID_{domain}"
|
||||
ga4_property = os.environ.get(env_key)
|
||||
|
||||
if ga4_property:
|
||||
print(f"Pobieram dane GA4 (property: {ga4_property})...")
|
||||
try:
|
||||
ga4 = fetch_ga4_data(ga4_property, start_date, end_date, prev_start, prev_end)
|
||||
report["ga4"] = ga4
|
||||
print(f" GA4: {ga4['current']['sessions']} sesji, {ga4['current']['users']} uzytkownikow")
|
||||
except Exception as e:
|
||||
print(f" UWAGA: Blad GA4: {e}")
|
||||
report["ga4"] = None
|
||||
else:
|
||||
print(f" Brak GA4 Property ID w .env ({env_key}) - pomijam GA4")
|
||||
report["ga4"] = None
|
||||
else:
|
||||
report["ga4"] = None
|
||||
|
||||
# Semstorm SEO data
|
||||
semstorm_login = os.environ.get("SEMSTORM_LOGIN", "")
|
||||
if semstorm_login:
|
||||
print(f"Pobieram dane Semstorm...")
|
||||
try:
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from fetch_semstorm_data import fetch_domain_stats
|
||||
semstorm = fetch_domain_stats(domain, args.month)
|
||||
report["semstorm"] = semstorm
|
||||
if semstorm and semstorm.get("current"):
|
||||
cur = semstorm["current"]
|
||||
print(f" Semstorm: TOP3={cur['top3']}, TOP10={cur['top10']}, TOP50={cur['top50']}, traffic={cur['traffic']}")
|
||||
except Exception as e:
|
||||
print(f" UWAGA: Blad Semstorm: {e}")
|
||||
report["semstorm"] = None
|
||||
else:
|
||||
report["semstorm"] = None
|
||||
|
||||
# E-commerce data: Shoper (primary) or GA4 (fallback)
|
||||
shoper_key = f"SHOPER_API_URL_{domain}"
|
||||
if os.environ.get(shoper_key):
|
||||
print(f"Pobieram dane e-commerce ze Shoper...")
|
||||
try:
|
||||
from fetch_shoper_data import fetch_shoper_ecommerce
|
||||
shoper_ecom = fetch_shoper_ecommerce(domain, args.month, f"{py}-{pm:02d}")
|
||||
if shoper_ecom and shoper_ecom["current"]["transactions"] > 0:
|
||||
# Get revenue_by_source and top_products from GA4
|
||||
if report.get("ga4") and ga4_property:
|
||||
try:
|
||||
ga4_ecom = fetch_ga4_ecommerce(ga4_property, start_date, end_date, prev_start, prev_end)
|
||||
if ga4_ecom:
|
||||
shoper_ecom["revenue_by_source"] = ga4_ecom.get("revenue_by_source", [])
|
||||
shoper_ecom["top_products"] = ga4_ecom.get("top_products", [])
|
||||
except Exception as e:
|
||||
print(f" UWAGA: GA4 revenue_by_source/top_products: {e}")
|
||||
shoper_ecom["revenue_by_source"] = []
|
||||
shoper_ecom["top_products"] = []
|
||||
if report.get("ga4") is None:
|
||||
report["ga4"] = {}
|
||||
report["ga4"]["ecommerce"] = shoper_ecom
|
||||
cur = shoper_ecom["current"]
|
||||
print(f" Shoper: {cur['transactions']} zamówień, {cur['revenue']:.2f} PLN, AOV {cur['aov']:.2f} PLN")
|
||||
else:
|
||||
if report.get("ga4"):
|
||||
report["ga4"]["ecommerce"] = None
|
||||
except Exception as e:
|
||||
print(f" UWAGA: Blad Shoper: {e}")
|
||||
if report.get("ga4"):
|
||||
report["ga4"]["ecommerce"] = None
|
||||
elif report.get("ga4") and ga4_property:
|
||||
print(f"Pobieram dane GA4 e-commerce...")
|
||||
try:
|
||||
ecom = fetch_ga4_ecommerce(ga4_property, start_date, end_date, prev_start, prev_end)
|
||||
if ecom and ecom["current"]["transactions"] > 0:
|
||||
report["ga4"]["ecommerce"] = ecom
|
||||
cur = ecom["current"]
|
||||
print(f" E-commerce (GA4): {cur['transactions']} transakcji, {cur['revenue']:.2f} PLN przychodu")
|
||||
else:
|
||||
report["ga4"]["ecommerce"] = None
|
||||
except Exception as e:
|
||||
print(f" UWAGA: Blad GA4 e-commerce: {e}")
|
||||
if report.get("ga4"):
|
||||
report["ga4"]["ecommerce"] = None
|
||||
|
||||
# Monthly sales history for chart. Prefer client Google Sheet when configured.
|
||||
client_report_config = load_client_report_config(domain)
|
||||
sales_history_sheet = client_report_config.get("sales_history_sheet") or os.environ.get(f"GSHEET_SALES_HISTORY_{domain}")
|
||||
report_start = os.environ.get(f"REPORT_START_DATE_{domain}")
|
||||
if sales_history_sheet:
|
||||
try:
|
||||
sales_history = fetch_sales_history_from_sheet(domain, sales_history_sheet)
|
||||
if apply_sheet_ecommerce(report, sales_history, args.month, f"{py}-{pm:02d}"):
|
||||
current_sheet = report["ga4"]["ecommerce"]["current"]
|
||||
print(
|
||||
f" E-commerce (Google Sheet): {current_sheet['transactions']} transakcji, "
|
||||
f"{current_sheet['revenue']:.2f} PLN przychodu"
|
||||
)
|
||||
filtered = [e for e in sales_history if not report_start or e["month"] >= report_start]
|
||||
report["sales_history"] = filtered
|
||||
print(f" Historia sprzedaży z Google Sheet: {len(filtered)} miesięcy")
|
||||
except Exception as e:
|
||||
report["sales_history"] = []
|
||||
print(f" UWAGA: Nie udalo sie pobrac historii sprzedazy z Google Sheet: {e}")
|
||||
else:
|
||||
ecom_data = report.get("ga4", {}).get("ecommerce") if report.get("ga4") else None
|
||||
if ecom_data and ecom_data.get("current", {}).get("transactions", 0) > 0:
|
||||
history_path = ROOT / "clients" / domain / "sales_history.json"
|
||||
history_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
sales_history = []
|
||||
if history_path.exists():
|
||||
with open(history_path, "r", encoding="utf-8") as f:
|
||||
sales_history = json.load(f)
|
||||
cur_entry = {
|
||||
"month": args.month,
|
||||
"transactions": ecom_data["current"]["transactions"],
|
||||
"revenue": ecom_data["current"]["revenue"],
|
||||
"aov": ecom_data["current"]["aov"],
|
||||
"source": ecom_data.get("source", "ga4"),
|
||||
}
|
||||
by_month = {e["month"]: e for e in sales_history}
|
||||
by_month[args.month] = cur_entry
|
||||
sales_history = sorted(by_month.values(), key=lambda x: x["month"])
|
||||
with open(history_path, "w", encoding="utf-8") as f:
|
||||
json.dump(sales_history, f, indent=2, ensure_ascii=False)
|
||||
filtered = [e for e in sales_history if not report_start or e["month"] >= report_start]
|
||||
report["sales_history"] = filtered
|
||||
print(f" Historia sprzedaży: {len(filtered)} miesięcy zapisanych")
|
||||
else:
|
||||
report["sales_history"] = []
|
||||
|
||||
# SEO links from Google Sheets
|
||||
seo_links_key = f"GSHEET_SEO_LINKS_{domain}"
|
||||
if os.environ.get(seo_links_key):
|
||||
print(f"Pobieram linki SEO...")
|
||||
try:
|
||||
from fetch_seo_links import fetch_seo_links, fetch_seo_activities
|
||||
seo_links = fetch_seo_links(domain, args.month)
|
||||
report["seo_links"] = seo_links or []
|
||||
print(f" Linki SEO: {len(report['seo_links'])} w {args.month}")
|
||||
# SEO activities (text box)
|
||||
seo_act_key = f"GSHEET_SEO_ACTIVITIES_{domain}"
|
||||
if os.environ.get(seo_act_key):
|
||||
seo_activities = fetch_seo_activities(domain, args.month)
|
||||
report["seo_activities"] = seo_activities
|
||||
if seo_activities:
|
||||
print(f" Działania SEO: {len(seo_activities)} znaków")
|
||||
except Exception as e:
|
||||
print(f" UWAGA: Blad SEO links: {e}")
|
||||
report["seo_links"] = []
|
||||
else:
|
||||
report["seo_links"] = []
|
||||
|
||||
# Output
|
||||
if args.output:
|
||||
output_path = Path(args.output)
|
||||
else:
|
||||
output_path = ROOT / "scripts" / "reports" / "output" / f"{domain}_{args.month}.json"
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(report, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"\nZapisano: {output_path}")
|
||||
print(f"Google Ads: {totals['clicks']} klikniec, {totals['conversions']} konwersji, {totals['cost']:.2f} PLN")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user