Files
InvestmentTrackerApp/Scripts/analyze_ga4.py
T
alexandrev-tibco 7a18dd8360 1.4.1 (build 42): contribución por snapshot, fixes Period vs Period y navegación iPad
- 151: contribución editable por snapshot en cualquier modo (quitado gate detailed)
  con diálogo de propagación al editar: solo este / adelante / atrás / todos
  (SnapshotRepository.propagateContribution, SnapshotFormViewModel.contributionChanged)
- 148: Period vs Period mostraba mal el último mes del period B — la agrupación usaba
  chartMonth(for:) que aplicaba el grace-period del check-in a snapshots históricos;
  ahora agrupa por mes calendario crudo
- 152: iPad Sources no saltaba entre fuentes — añadido .id() al SourceDetailView para
  recrear el StateObject al cambiar de selección
- 146/147: Year vs Year con forecast del año en curso (asterisco de estimado) y KPIs
  arriba / detalle debajo
- 145: card de contribución mensual en SourceDetailView
- Nuevas claves snapshot_contribution_propagate_* en los 7 idiomas

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
2026-06-30 17:01:50 +02:00

186 lines
7.6 KiB
Python

"""
GA4 Analytics report for Portfolio Journal
Property: 300195945
"""
import os
from datetime import date, timedelta
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
RunReportRequest, Dimension, Metric, DateRange, OrderBy, FilterExpression, Filter
)
PROPERTY_ID = "519342587"
import subprocess, json, tempfile
def _fix_multiline_json(raw):
result, in_string, escape_next = [], False, False
for c in raw:
if escape_next:
result.append(c); escape_next = False
elif c == '\\':
result.append(c); escape_next = True
elif c == '"':
in_string = not in_string; result.append(c)
elif in_string and c == '\n':
result.append('\\n')
elif in_string and c == '\r':
pass
else:
result.append(c)
return ''.join(result)
key_raw = subprocess.check_output(["pass", "show", "google/sirius-service-account"]).decode()
key_json = json.dumps(json.loads(_fix_multiline_json(key_raw)))
_tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
_tmp.write(key_json); _tmp.flush()
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = _tmp.name
client = BetaAnalyticsDataClient()
TODAY = date.today().isoformat()
D7 = (date.today() - timedelta(days=7)).isoformat()
D28 = (date.today() - timedelta(days=28)).isoformat()
D90 = (date.today() - timedelta(days=90)).isoformat()
def run(dimensions, metrics, date_from, date_to=TODAY, order_by=None, limit=20):
req = RunReportRequest(
property=f"properties/{PROPERTY_ID}",
dimensions=[Dimension(name=d) for d in dimensions],
metrics=[Metric(name=m) for m in metrics],
date_ranges=[DateRange(start_date=date_from, end_date=date_to)],
limit=limit,
)
if order_by:
req.order_bys = order_by
return client.run_report(req)
def run_no_dim(metrics, date_from, date_to=TODAY):
req = RunReportRequest(
property=f"properties/{PROPERTY_ID}",
metrics=[Metric(name=m) for m in metrics],
date_ranges=[DateRange(start_date=date_from, end_date=date_to)],
)
return client.run_report(req)
def header(title):
print(f"\n{'='*60}")
print(f" {title}")
print('='*60)
def fmt_row(row, widths):
parts = [str(v.value).ljust(w) for v, w in zip(list(row.dimension_values) + list(row.metric_values), widths)]
print(" " + " ".join(parts))
# ── 1. Overview ────────────────────────────────────────────────
header("OVERVIEW — últimos 28 días vs 7 días")
r28 = run_no_dim(["activeUsers", "sessions", "screenPageViews", "averageSessionDuration"], D28)
r7 = run_no_dim(["activeUsers", "sessions", "screenPageViews", "averageSessionDuration"], D7)
def ovw(r):
row = r.rows[0]
vals = [v.value for v in row.metric_values]
return vals
v28 = ovw(r28)
v7 = ovw(r7)
labels = ["Usuarios activos", "Sesiones", "Pantallas vistas", "Duración media (s)"]
print(f"\n {'Métrica':<30} {'28d':>10} {'7d':>10}")
print(" " + "-"*52)
for l, a, b in zip(labels, v28, v7):
print(f" {l:<30} {a:>10} {b:>10}")
# ── 2. Pantallas más visitadas ──────────────────────────────────
header("PANTALLAS MÁS VISITADAS — últimos 28d")
r = run(["unifiedScreenName"], ["screenPageViews", "activeUsers"], D28,
order_by=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="screenPageViews"), desc=True)])
print(f"\n {'Pantalla':<40} {'Vistas':>8} {'Usuarios':>10}")
print(" " + "-"*60)
for row in r.rows:
fmt_row(row, [40, 8, 10])
# ── 3. Eventos clave ────────────────────────────────────────────
header("EVENTOS CLAVE — últimos 28d")
r = run(["eventName"], ["eventCount", "totalUsers"], D28,
order_by=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="eventCount"), desc=True)],
limit=30)
print(f"\n {'Evento':<45} {'Count':>8} {'Usuarios':>10}")
print(" " + "-"*65)
for row in r.rows:
fmt_row(row, [45, 8, 10])
# ── 4. Funnel paywall ───────────────────────────────────────────
header("FUNNEL PAYWALL — últimos 90d")
paywall_events = [
"paywall_shown", "purchase_attempt", "purchase_success", "purchase_failure",
"restore_purchases"
]
r = run(["eventName"], ["eventCount", "totalUsers"], D90,
order_by=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="eventCount"), desc=True)],
limit=50)
print(f"\n {'Evento paywall':<40} {'Count':>8} {'Usuarios':>10}")
print(" " + "-"*60)
for row in r.rows:
name = row.dimension_values[0].value
if any(p in name for p in ["paywall", "purchase", "restore", "premium"]):
fmt_row(row, [40, 8, 10])
# ── 5. Triggers del paywall ─────────────────────────────────────
header("TRIGGERS DEL PAYWALL — últimos 90d (qué feature lo dispara)")
try:
r = run(["eventName", "customEvent:paywall_trigger"], ["eventCount"], D90,
order_by=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="eventCount"), desc=True)],
limit=30)
rows = [row for row in r.rows if "paywall" in row.dimension_values[0].value]
if rows:
print(f"\n {'Evento':<30} {'Trigger':<25} {'Count':>8}")
print(" " + "-"*65)
for row in rows:
fmt_row(row, [30, 25, 8])
else:
print("\n (sin eventos paywall con trigger en este periodo — los datos aparecerán desde hoy)")
except Exception as e:
print(f"\n Error: {e}")
# ── 6. Retención — nuevos vs recurrentes ───────────────────────
header("NUEVOS vs RECURRENTES — últimos 28d")
r = run(["newVsReturning"], ["activeUsers", "sessions", "screenPageViews"], D28,
order_by=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="activeUsers"), desc=True)])
print(f"\n {'Tipo':<20} {'Usuarios':>10} {'Sesiones':>10} {'Pantallas':>10}")
print(" " + "-"*54)
for row in r.rows:
fmt_row(row, [20, 10, 10, 10])
# ── 7. Versiones de app ─────────────────────────────────────────
header("VERSIONES EN USO — últimos 28d")
r = run(["appVersion"], ["activeUsers", "sessions"], D28,
order_by=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="activeUsers"), desc=True)],
limit=10)
print(f"\n {'Versión':<20} {'Usuarios':>10} {'Sesiones':>10}")
print(" " + "-"*44)
for row in r.rows:
fmt_row(row, [20, 10, 10])
# ── 8. Dispositivos ─────────────────────────────────────────────
header("DISPOSITIVOS — últimos 28d")
r = run(["deviceModel"], ["activeUsers"], D28,
order_by=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="activeUsers"), desc=True)],
limit=10)
print(f"\n {'Dispositivo':<30} {'Usuarios':>10}")
print(" " + "-"*44)
for row in r.rows:
fmt_row(row, [30, 10])
# ── 9. País / región ────────────────────────────────────────────
header("PAÍSES — últimos 28d")
r = run(["country"], ["activeUsers"], D28,
order_by=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="activeUsers"), desc=True)],
limit=10)
print(f"\n {'País':<30} {'Usuarios':>10}")
print(" " + "-"*44)
for row in r.rows:
fmt_row(row, [30, 10])
print("\n")