This commit is contained in:
2026-05-15 23:19:26 +02:00
parent def1fae0fc
commit 75b9434de5
113 changed files with 50906 additions and 1305 deletions

View 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("", "")
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()

View File

@@ -0,0 +1,716 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Raport Kwiecień 2026 &mdash; Aruba Rzeszow</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
:root {
--primary: #0d8b8b;
--primary-dark: #065a5a;
--primary-light: #e8f5f5;
--green: #27ae60;
--red: #e74c3c;
--gray: #6c757d;
--light-gray: #f8f9fa;
--border: #e9ecef;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
color: #333;
background: #f5f5f5;
line-height: 1.6;
}
.hero {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 60%, #10a5a5 100%);
color: white;
padding: 60px 40px;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: 0; right: 0;
width: 50%;
height: 100%;
background: url("data:image/svg+xml,%3Csvg width='400' height='400' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='grid' width='40' height='40' patternUnits='userSpaceOnUse'%3E%3Ccircle cx='20' cy='20' r='1.5' fill='rgba(255,255,255,0.15)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='400' height='400' fill='url(%23grid)'/%3E%3Cline x1='20' y1='60' x2='100' y2='20' stroke='rgba(255,255,255,0.08)' stroke-width='1'/%3E%3Cline x1='100' y1='20' x2='180' y2='80' stroke='rgba(255,255,255,0.08)' stroke-width='1'/%3E%3Cline x1='180' y1='80' x2='260' y2='40' stroke='rgba(255,255,255,0.08)' stroke-width='1'/%3E%3Cline x1='260' y1='40' x2='340' y2='100' stroke='rgba(255,255,255,0.08)' stroke-width='1'/%3E%3Cline x1='60' y1='140' x2='140' y2='120' stroke='rgba(255,255,255,0.06)' stroke-width='1'/%3E%3Cline x1='140' y1='120' x2='220' y2='180' stroke='rgba(255,255,255,0.06)' stroke-width='1'/%3E%3Cline x1='220' y1='180' x2='300' y2='140' stroke='rgba(255,255,255,0.06)' stroke-width='1'/%3E%3Cline x1='300' y1='140' x2='380' y2='200' stroke='rgba(255,255,255,0.06)' stroke-width='1'/%3E%3C/svg%3E") repeat;
opacity: 0.7;
}
.hero-content {
max-width: 1400px;
margin: 0 auto;
position: relative;
z-index: 1;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 50px;
}
.logo svg {
height: 40px;
}
.logo-text {
font-size: 22px;
font-weight: 700;
letter-spacing: 3px;
text-transform: uppercase;
}
.logo-subtitle {
font-size: 11px;
letter-spacing: 2px;
opacity: 0.85;
text-transform: lowercase;
}
.hero h1 {
font-size: 42px;
font-weight: 700;
line-height: 1.2;
margin-bottom: 16px;
}
.hero .meta {
font-size: 18px;
opacity: 0.9;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
}
.report-section {
background: white;
border-radius: 12px;
padding: 32px;
margin: 24px auto;
max-width: 1400px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.section-title {
color: var(--primary-dark);
font-size: 24px;
font-weight: 700;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 3px solid var(--primary);
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.kpi-card {
background: var(--light-gray);
border-radius: 10px;
padding: 20px;
text-align: center;
border: 1px solid var(--border);
}
.kpi-label {
font-size: 12px;
color: var(--primary);
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.kpi-value {
font-size: 28px;
font-weight: 700;
color: #2c3e50;
white-space: nowrap;
}
.kpi-unit {
font-size: 14px;
font-weight: 400;
color: var(--gray);
}
.kpi-change {
font-size: 13px;
margin-top: 6px;
font-weight: 500;
}
.chart-container {
margin: 24px 0;
}
.chart-container h3 {
color: var(--primary-dark);
margin-bottom: 12px;
font-size: 16px;
}
.chart-container canvas {
max-height: 300px;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.data-table thead {
background: var(--primary);
color: white;
}
.data-table th {
padding: 10px 12px;
text-align: left;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.data-table td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
}
.data-table tbody tr:hover {
background: var(--primary-light);
}
.data-table .num {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.badge-search { background: #dbeafe; color: #1e40af; }
.badge-performance_max { background: #fef3c7; color: #92400e; }
.badge-shopping { background: #d1fae5; color: #065f46; }
.badge-display { background: #ede9fe; color: #5b21b6; }
.badge-demand_gen { background: #fce7f3; color: #9d174d; }
.summary-box {
background: var(--primary-light);
border-left: 4px solid var(--primary);
padding: 24px;
border-radius: 0 10px 10px 0;
font-size: 15px;
line-height: 1.8;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-top: 24px;
}
.bar {
height: 8px;
background: var(--primary);
border-radius: 4px;
min-width: 4px;
}
.recommendations-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.rec-item {
display: flex;
gap: 16px;
padding: 16px;
background: var(--light-gray);
border-radius: 8px;
border-left: 4px solid var(--primary);
}
.rec-icon {
font-size: 20px;
flex-shrink: 0;
margin-top: 2px;
}
.rec-item p {
margin-top: 4px;
color: var(--gray);
font-size: 14px;
}
.questions-box {
background: linear-gradient(135deg, #fff8e1 0%, #fff3cd 100%);
border-left: 4px solid #f59e0b;
border-radius: 10px;
padding: 24px 28px;
margin-top: 16px;
}
.questions-box h3 {
margin: 0 0 8px 0;
color: #92400e;
font-size: 18px;
}
.questions-box .intro {
color: #78350f;
font-size: 14px;
margin-bottom: 16px;
}
.questions-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.q-item {
display: flex;
gap: 14px;
padding: 14px 16px;
background: rgba(255,255,255,0.7);
border-radius: 8px;
}
.q-num {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: #f59e0b;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
}
.q-item strong { color: #92400e; display: block; margin-bottom: 4px; }
.q-item p { margin: 0; color: #57534e; font-size: 14px; line-height: 1.6; }
.footer {
text-align: center;
padding: 32px;
color: var(--gray);
font-size: 13px;
}
.footer a {
color: var(--primary);
text-decoration: none;
}
@media (max-width: 768px) {
.hero { padding: 40px 20px; }
.hero h1 { font-size: 28px; }
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
.two-col { grid-template-columns: 1fr; }
.report-section { padding: 20px; margin: 12px; }
}
@media print {
body { background: white; }
.report-section { box-shadow: none; page-break-inside: avoid; }
.hero { padding: 30px; }
}
</style>
</head>
<body>
<!-- HERO / TITLE -->
<header class="hero">
<div class="hero-content">
<div class="logo">
<img src="https://www.project-pro.pl/upload/filemanager/Project-Design/logos/project-pro/logo-white.svg" alt="Project-Pro" style="height: 40px;">
</div>
<h1>Raport z działań marketingowych</h1>
<div class="meta">Aruba Rzeszow &mdash; Kwiecień 2026</div>
</div>
</header>
<!-- RECOMMENDATIONS -->
<section class="report-section" id="recommendations">
<h2 class="section-title">Wnioski i rekomendacje</h2>
<div class="recommendations-list">
<div class="rec-item"><span class="rec-icon">&#9888;</span><div><strong>Spadek konwersji do obserwacji</strong><p>Liczba konwersji spadla o 8.2% miesiac do miesiaca. Rekomendujemy sprawdzenie kampanii o najwiekszym spadku wolumenu.</p></div></div><div class="rec-item"><span class="rec-icon">&#128200;</span><div><strong>ROAS liczony z Google Ads</strong><p>ROAS z Google Ads wyniosl 8.47. Ten wskaznik liczymy z wartosci konwersji Google Ads, nie z przychodow sklepu.</p></div></div><div class="rec-item"><span class="rec-icon">&#128269;</span><div><strong>Kontrola wzrostu kosztu</strong><p>Koszt reklam wzrosl o 12.2% miesiac do miesiaca. Warto porownac wzrost kosztu ze wzrostem konwersji i wartosci konwersji.</p></div></div>
</div>
</section>
<!-- GA4 SECTION -->
<!-- E-COMMERCE -->
<!-- PRODUCT OPTIMIZATIONS -->
<!-- TOP ADS PRODUCTS -->
<!-- YEAR OVER YEAR -->
<!-- GOOGLE ADS KPIs -->
<section class="report-section" id="ads-kpi">
<h2 class="section-title">Google Ads &mdash; Podsumowanie</h2>
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">Wyświetlenia</div>
<div class="kpi-value">172 277<span class="kpi-unit"></span></div>
<div class="kpi-change" style="color: #e74c3c">
&#9660; -0.6% vs Marzec
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Kliknięcia</div>
<div class="kpi-value">3 826<span class="kpi-unit"></span></div>
<div class="kpi-change" style="color: #27ae60">
&#9650; +2.5% vs Marzec
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">CTR</div>
<div class="kpi-value">2.2<span class="kpi-unit">%</span></div>
<div class="kpi-change" style="color: #27ae60">
&#9650; +3.3% vs Marzec
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Konwersje</div>
<div class="kpi-value">199<span class="kpi-unit"></span></div>
<div class="kpi-change" style="color: #e74c3c">
&#9660; -8.2% vs Marzec
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Koszt</div>
<div class="kpi-value">4880.74<span class="kpi-unit"> PLN</span></div>
<div class="kpi-change" style="color: #e74c3c">
&#9650; +12.2% vs Marzec
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">CPA</div>
<div class="kpi-value">24.46<span class="kpi-unit"> PLN</span></div>
<div class="kpi-change" style="color: #e74c3c">
&#9650; +22.2% vs Marzec
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">ROAS</div>
<div class="kpi-value">8.47<span class="kpi-unit">x</span></div>
<div class="kpi-change" style="color: #e74c3c">
&#9660; -1.5% vs Marzec
</div>
</div></div>
</section>
<!-- DAILY CHART -->
<section class="report-section" id="ads-chart">
<h2 class="section-title">Google Ads &mdash; Aktywność dzienna</h2>
<div class="chart-container">
<canvas id="dailyClicksChart"></canvas>
</div>
</section>
<!-- CAMPAIGNS TABLE -->
<section class="report-section" id="campaigns">
<h2 class="section-title">Kampanie</h2>
<table class="data-table">
<thead>
<tr>
<th>Kampania</th>
<th>Typ</th>
<th>Wyświetlenia</th>
<th>Kliknięcia</th>
<th>CTR</th>
<th>Konwersje</th>
<th>Koszt</th>
<th>CPA</th>
</tr>
</thead>
<tbody>
<tr>
<td>[Search] brand</td>
<td><span class="badge badge-search">SEARCH</span></td>
<td class="num">1 614</td>
<td class="num">483</td>
<td class="num">29.9%</td>
<td class="num">27</td>
<td class="num">337.96 PLN</td>
<td class="num">12.52 PLN</td>
</tr>
<tr>
<td>[DSA] produkty</td>
<td><span class="badge badge-search">SEARCH</span></td>
<td class="num">9 984</td>
<td class="num">694</td>
<td class="num">7.0%</td>
<td class="num">23</td>
<td class="num">1098.15 PLN</td>
<td class="num">47.75 PLN</td>
</tr>
<tr>
<td>[PMax] products (catch-all)</td>
<td><span class="badge badge-performance_max">PERFORMANCE_MAX</span></td>
<td class="num">138 921</td>
<td class="num">2 164</td>
<td class="num">1.6%</td>
<td class="num">106</td>
<td class="num">2762.99 PLN</td>
<td class="num">25.94 PLN</td>
</tr>
<tr>
<td>[PLA] produkty (bestsellers)</td>
<td><span class="badge badge-shopping">SHOPPING</span></td>
<td class="num">21 758</td>
<td class="num">485</td>
<td class="num">2.2%</td>
<td class="num">43</td>
<td class="num">681.64 PLN</td>
<td class="num">15.85 PLN</td>
</tr></tbody>
</table>
</section>
<!-- SEARCH TERMS -->
<section class="report-section" id="search-terms">
<h2 class="section-title">Najpopularniejsze frazy wyszukiwania</h2>
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>Fraza</th>
<th>Wyświetlenia</th>
<th>Kliknięcia</th>
<th>CTR</th>
<th>Konwersje</th>
</tr>
</thead>
<tbody>
<tr>
<td class="num">1</td>
<td>aruba rzeszów</td>
<td class="num">836</td>
<td class="num">246</td>
<td class="num">29.4%</td>
<td class="num">16</td>
</tr>
<tr>
<td class="num">2</td>
<td>gen factor</td>
<td class="num">858</td>
<td class="num">59</td>
<td class="num">6.9%</td>
<td class="num">1</td>
</tr>
<tr>
<td class="num">3</td>
<td>aruba hurtownia</td>
<td class="num">122</td>
<td class="num">48</td>
<td class="num">39.3%</td>
<td class="num">4</td>
</tr>
<tr>
<td class="num">4</td>
<td>aruba rzeszow</td>
<td class="num">127</td>
<td class="num">39</td>
<td class="num">30.7%</td>
<td class="num">0</td>
</tr>
<tr>
<td class="num">5</td>
<td>gen factor green</td>
<td class="num">207</td>
<td class="num">21</td>
<td class="num">10.1%</td>
<td class="num">2</td>
</tr>
<tr>
<td class="num">6</td>
<td>gen factor</td>
<td class="num">604</td>
<td class="num">21</td>
<td class="num">3.5%</td>
<td class="num">1</td>
</tr>
<tr>
<td class="num">7</td>
<td>verru immuno</td>
<td class="num">495</td>
<td class="num">19</td>
<td class="num">3.8%</td>
<td class="num">2</td>
</tr>
<tr>
<td class="num">8</td>
<td>aruba sklep</td>
<td class="num">48</td>
<td class="num">17</td>
<td class="num">35.4%</td>
<td class="num">1</td>
</tr>
<tr>
<td class="num">9</td>
<td>aurumaris</td>
<td class="num">113</td>
<td class="num">13</td>
<td class="num">11.5%</td>
<td class="num">0</td>
</tr>
<tr>
<td class="num">10</td>
<td>aruba hurtownia kosmetyczna</td>
<td class="num">25</td>
<td class="num">12</td>
<td class="num">48.0%</td>
<td class="num">1</td>
</tr>
<tr>
<td class="num">11</td>
<td>aruba kosmetyki</td>
<td class="num">33</td>
<td class="num">12</td>
<td class="num">36.4%</td>
<td class="num">1</td>
</tr>
<tr>
<td class="num">12</td>
<td>gen factor 09</td>
<td class="num">47</td>
<td class="num">11</td>
<td class="num">23.4%</td>
<td class="num">0</td>
</tr>
<tr>
<td class="num">13</td>
<td>genfactor</td>
<td class="num">111</td>
<td class="num">11</td>
<td class="num">9.9%</td>
<td class="num">2</td>
</tr>
<tr>
<td class="num">14</td>
<td>podopharm verru immuno</td>
<td class="num">230</td>
<td class="num">11</td>
<td class="num">4.8%</td>
<td class="num">2</td>
</tr>
<tr>
<td class="num">15</td>
<td>hurtownia aruba</td>
<td class="num">32</td>
<td class="num">10</td>
<td class="num">31.2%</td>
<td class="num">0</td>
</tr></tbody>
</table>
</section>
<!-- NEGATIVE KEYWORDS ADDED -->
<!-- SEMSTORM SEO -->
<!-- SEO ACTIVITIES -->
<!-- SEO LINKS -->
<!-- SUMMARY -->
<section class="report-section" id="summary">
<h2 class="section-title">Podsumowanie miesiąca</h2>
<div class="summary-box">
Odnotowano 199 konwersji w tym miesiącu. Ruch z reklam wzrósł o 2.5% (3826 kliknięć).
</div>
</section>
<!-- RECOMMENDATIONS moved to top -->
<!-- FOOTER -->
<div class="footer">
Raport wygenerowany przez <a href="https://www.project-pro.pl">Project-Pro</a> &mdash; marketing w wersji PRO
</div>
<script>
// Daily clicks chart
var ctx1 = document.getElementById('dailyClicksChart').getContext('2d');
new Chart(ctx1, {
type: 'line',
data: {
labels: ["04-01", "04-02", "04-03", "04-04", "04-05", "04-06", "04-07", "04-08", "04-09", "04-10", "04-11", "04-12", "04-13", "04-14", "04-15", "04-16", "04-17", "04-18", "04-19", "04-20", "04-21", "04-22", "04-23", "04-24", "04-25", "04-26", "04-27", "04-28", "04-29", "04-30"],
datasets: [{
label: 'Kliknięcia',
data: [102, 108, 72, 54, 39, 96, 130, 166, 137, 112, 95, 114, 185, 176, 164, 149, 107, 101, 114, 196, 163, 210, 170, 116, 112, 131, 144, 132, 135, 96],
borderColor: '#0d8b8b',
backgroundColor: 'rgba(13,139,139,0.1)',
fill: true,
tension: 0.3,
pointRadius: 3,
pointBackgroundColor: '#0d8b8b',
}, {
label: 'Wyświetlenia',
data: [6909, 5632, 4210, 3045, 2088, 3976, 5853, 7519, 6605, 4340, 3177, 4104, 7332, 7941, 7296, 6191, 4557, 3621, 5409, 7762, 7615, 9246, 9234, 5931, 5078, 5786, 6014, 6078, 5629, 4099],
borderColor: '#95a5a6',
backgroundColor: 'transparent',
borderDash: [5, 5],
tension: 0.3,
pointRadius: 0,
yAxisID: 'y1',
}]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
scales: {
y: { beginAtZero: true, position: 'left', grid: { color: '#f0f0f0' } },
y1: { beginAtZero: true, position: 'right', grid: { display: false } },
x: { grid: { display: false }, ticks: { maxTicksLimit: 10 } }
}
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,429 @@
{
"client": "aruba.rzeszow.pl",
"month": "2026-04",
"month_name": "Kwiecień",
"year": 2026,
"prev_month": "2026-03",
"prev_month_name": "Marzec",
"generated_at": "2026-05-14T23:23:53.496703",
"google_ads": {
"campaigns": [
{
"id": "19591441631",
"name": "[Search] brand",
"status": "ENABLED",
"type": "SEARCH",
"impressions": 1614,
"clicks": 483,
"cost": 337.96,
"conversions": 27.0,
"conversion_value": 7967.63,
"ctr": 29.93,
"cpc": 0.7,
"cpa": 12.52,
"roas": 23.58
},
{
"id": "20561423980",
"name": "[DSA] produkty",
"status": "ENABLED",
"type": "SEARCH",
"impressions": 9984,
"clicks": 694,
"cost": 1098.15,
"conversions": 23.0,
"conversion_value": 6600.7,
"ctr": 6.95,
"cpc": 1.58,
"cpa": 47.75,
"roas": 6.01
},
{
"id": "21260050298",
"name": "[PMax] products (catch-all)",
"status": "ENABLED",
"type": "PERFORMANCE_MAX",
"impressions": 138921,
"clicks": 2164,
"cost": 2762.99,
"conversions": 106.5,
"conversion_value": 19390.88,
"ctr": 1.56,
"cpc": 1.28,
"cpa": 25.94,
"roas": 7.02
},
{
"id": "22926581178",
"name": "[PLA] produkty (bestsellers)",
"status": "ENABLED",
"type": "SHOPPING",
"impressions": 21758,
"clicks": 485,
"cost": 681.64,
"conversions": 43.0,
"conversion_value": 7367.07,
"ctr": 2.23,
"cpc": 1.41,
"cpa": 15.85,
"roas": 10.81
}
],
"totals": {
"impressions": 172277,
"clicks": 3826,
"cost": 4880.74,
"conversions": 199.5,
"conversion_value": 41326.28,
"ctr": 2.22,
"cpc": 1.28,
"cpa": 24.46,
"roas": 8.47
},
"prev_totals": {
"impressions": 173273,
"clicks": 3733,
"cost": 4351.39,
"conversions": 217.4,
"conversion_value": 37429.84,
"ctr": 2.15,
"cpc": 1.17,
"cpa": 20.02,
"roas": 8.6
},
"mom_change": {
"impressions_pct": -0.6,
"clicks_pct": 2.5,
"cost_pct": 12.2,
"conversions_pct": -8.2,
"ctr_pct": 3.3,
"cpc_pct": 9.4,
"cpa_pct": 22.2
},
"daily": [
{
"date": "2026-04-01",
"impressions": 6909,
"clicks": 102,
"cost": 120.77
},
{
"date": "2026-04-02",
"impressions": 5632,
"clicks": 108,
"cost": 167.66
},
{
"date": "2026-04-03",
"impressions": 4210,
"clicks": 72,
"cost": 95.19
},
{
"date": "2026-04-04",
"impressions": 3045,
"clicks": 54,
"cost": 101.11
},
{
"date": "2026-04-05",
"impressions": 2088,
"clicks": 39,
"cost": 49.2
},
{
"date": "2026-04-06",
"impressions": 3976,
"clicks": 96,
"cost": 112.82
},
{
"date": "2026-04-07",
"impressions": 5853,
"clicks": 130,
"cost": 138.02
},
{
"date": "2026-04-08",
"impressions": 7519,
"clicks": 166,
"cost": 225.05
},
{
"date": "2026-04-09",
"impressions": 6605,
"clicks": 137,
"cost": 165.58
},
{
"date": "2026-04-10",
"impressions": 4340,
"clicks": 112,
"cost": 130.39
},
{
"date": "2026-04-11",
"impressions": 3177,
"clicks": 95,
"cost": 92.74
},
{
"date": "2026-04-12",
"impressions": 4104,
"clicks": 114,
"cost": 116.26
},
{
"date": "2026-04-13",
"impressions": 7332,
"clicks": 185,
"cost": 201.76
},
{
"date": "2026-04-14",
"impressions": 7941,
"clicks": 176,
"cost": 232.59
},
{
"date": "2026-04-15",
"impressions": 7296,
"clicks": 164,
"cost": 186.57
},
{
"date": "2026-04-16",
"impressions": 6191,
"clicks": 149,
"cost": 165.26
},
{
"date": "2026-04-17",
"impressions": 4557,
"clicks": 107,
"cost": 95.56
},
{
"date": "2026-04-18",
"impressions": 3621,
"clicks": 101,
"cost": 118.02
},
{
"date": "2026-04-19",
"impressions": 5409,
"clicks": 114,
"cost": 175.25
},
{
"date": "2026-04-20",
"impressions": 7762,
"clicks": 196,
"cost": 239.2
},
{
"date": "2026-04-21",
"impressions": 7615,
"clicks": 163,
"cost": 262.91
},
{
"date": "2026-04-22",
"impressions": 9246,
"clicks": 210,
"cost": 265.25
},
{
"date": "2026-04-23",
"impressions": 9234,
"clicks": 170,
"cost": 222.45
},
{
"date": "2026-04-24",
"impressions": 5931,
"clicks": 116,
"cost": 202.37
},
{
"date": "2026-04-25",
"impressions": 5078,
"clicks": 112,
"cost": 174.69
},
{
"date": "2026-04-26",
"impressions": 5786,
"clicks": 131,
"cost": 162.94
},
{
"date": "2026-04-27",
"impressions": 6014,
"clicks": 144,
"cost": 191.42
},
{
"date": "2026-04-28",
"impressions": 6078,
"clicks": 132,
"cost": 181.99
},
{
"date": "2026-04-29",
"impressions": 5629,
"clicks": 135,
"cost": 166.02
},
{
"date": "2026-04-30",
"impressions": 4099,
"clicks": 96,
"cost": 121.72
}
],
"search_terms": [
{
"term": "aruba rzeszów",
"impressions": 836,
"clicks": 246,
"cost": 131.67,
"conversions": 16.0,
"ctr": 29.43
},
{
"term": "gen factor",
"impressions": 858,
"clicks": 59,
"cost": 134.33,
"conversions": 1.0,
"ctr": 6.88
},
{
"term": "aruba hurtownia",
"impressions": 122,
"clicks": 48,
"cost": 26.45,
"conversions": 4.0,
"ctr": 39.34
},
{
"term": "aruba rzeszow",
"impressions": 127,
"clicks": 39,
"cost": 24.46,
"conversions": 0.0,
"ctr": 30.71
},
{
"term": "gen factor green",
"impressions": 207,
"clicks": 21,
"cost": 46.65,
"conversions": 2.0,
"ctr": 10.14
},
{
"term": "gen factor",
"impressions": 604,
"clicks": 21,
"cost": 25.05,
"conversions": 1.0,
"ctr": 3.48
},
{
"term": "verru immuno",
"impressions": 495,
"clicks": 19,
"cost": 27.24,
"conversions": 2.0,
"ctr": 3.84
},
{
"term": "aruba sklep",
"impressions": 48,
"clicks": 17,
"cost": 3.4,
"conversions": 1.0,
"ctr": 35.42
},
{
"term": "aurumaris",
"impressions": 113,
"clicks": 13,
"cost": 14.52,
"conversions": 0.0,
"ctr": 11.5
},
{
"term": "aruba hurtownia kosmetyczna",
"impressions": 25,
"clicks": 12,
"cost": 7.85,
"conversions": 1.0,
"ctr": 48.0
},
{
"term": "aruba kosmetyki",
"impressions": 33,
"clicks": 12,
"cost": 3.27,
"conversions": 1.0,
"ctr": 36.36
},
{
"term": "gen factor 09",
"impressions": 47,
"clicks": 11,
"cost": 15.43,
"conversions": 0.0,
"ctr": 23.4
},
{
"term": "genfactor",
"impressions": 111,
"clicks": 11,
"cost": 27.84,
"conversions": 2.0,
"ctr": 9.91
},
{
"term": "podopharm verru immuno",
"impressions": 230,
"clicks": 11,
"cost": 15.75,
"conversions": 2.0,
"ctr": 4.78
},
{
"term": "hurtownia aruba",
"impressions": 32,
"clicks": 10,
"cost": 7.31,
"conversions": 0.0,
"ctr": 31.25
}
]
},
"ga4": null,
"semstorm": null,
"sales_history": [],
"seo_links": [],
"recommendations": [
{
"icon": "&#9888;",
"title": "Spadek konwersji do obserwacji",
"text": "Liczba konwersji spadla o 8.2% miesiac do miesiaca. Rekomendujemy sprawdzenie kampanii o najwiekszym spadku wolumenu."
},
{
"icon": "&#128200;",
"title": "ROAS liczony z Google Ads",
"text": "ROAS z Google Ads wyniosl 8.47. Ten wskaznik liczymy z wartosci konwersji Google Ads, nie z przychodow sklepu."
},
{
"icon": "&#128269;",
"title": "Kontrola wzrostu kosztu",
"text": "Koszt reklam wzrosl o 12.2% miesiac do miesiaca. Warto porownac wzrost kosztu ze wzrostem konwersji i wartosci konwersji."
}
]
}