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
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user