Cap. 23 · Código
Dashboard único: coleta e alertas
Execução
Rodar em 3 comandos
Requer credencial: apenas collectors.py precisa de credenciais (GOOGLE_APPLICATION_CREDENTIALS, GSC_SITE_URL, GA4_PROPERTY_ID, CUSTOM_API_BASE/TOKEN) para buscar dados reais. Todo o resto — storage, alertas e o demo — roda sem nenhum segredo.
$ python3 demo_data.py # dashboard completo com dados fictícios$ python3 alerts.py # insere dados de exemplo e mostra alertas$ python3 storage.py # self-check da camada de dadosSaída esperada (python3 demo_data.py)
[demo] 35 linhas fictícias inseridas / fictional rows inserted============================================================ DASHBOARD ÚNICO — dados fictícios (sem credenciais) Unified dashboard — fictional data (no credentials)============================================================ Snapshot (valor mais recente / latest value) ---------------------------------------------- source metric value date custom signups 45 2026-07-11 ga4 lcp 2.7 2026-07-11 ga4 sessions 3090 2026-07-11 gsc clicks 890 2026-07-11 gsc impressions 46000 2026-07-11 Tendência 7 dias (7-day trend) ---------------------------------------------- gsc/clicks ▆▆▇▇▇█▁ (1180 → 890) gsc/impressions ▁▃▅▇▅█▃ (45000 → 46000) ga4/sessions ▁▃▅▇▄█▆ (2900 → 3090) ga4/lcp ▁▂▁▃▃▄█ (2 → 2.7) custom/signups ▁▂▄▆▄█▅ (38 → 45) Alertas (alerts) ---------------------------------------------- [ALERTA] clicks dropped 30.2% (1275.0 -> 890.0) exceeding 20.0% (source: gsc) [ALERTA] lcp = 2.7 exceeds max 2.5 (source: ga4)============================================================Código-fonte
Código do módulo
#!/usr/bin/env python3"""Cap. 23 - End-to-end dashboard demo with FICTIONAL data (zero credentials).Runs the whole pipeline offline: 1. Creates an in-memory SQLite DB via ``storage.py``. 2. Inserts several days of FICTIONAL metrics (no API, no credentials). 3. Evaluates the alert rules from ``alerts.py``. 4. Prints a small text dashboard: current snapshot, 7-day trend and alerts.Run it standalone:: python3 demo_data.pyNothing here touches Google Search Console, GA4 or any external service - thenumbers are made up so you can see the dashboard working before wiring realcredentials into ``collectors.py``.Part of the "living code" companion repository for the book"Aparecer ou Sumir" / "Be Seen or Be Forgotten"."""from __future__ import annotationsfrom datetime import date, timedeltafrom typing import Listimport alertsimport storage# ---------------------------------------------------------------------------# Fictional 7-day series. Engineered so the last day trips two alert rules:# * gsc clicks fall sharply on the final day (drop_pct > 20%).# * ga4 lcp creeps above the 2.5s ceiling on the final day (max).# ---------------------------------------------------------------------------CLICKS = [1180, 1205, 1240, 1260, 1230, 1275, 890] # last day: big dropIMPRESSIONS = [45000, 46200, 47100, 47800, 47200, 48100, 46000]SESSIONS = [2900, 2980, 3050, 3110, 3020, 3140, 3090] # stable -> no alertLCP = [2.0, 2.1, 2.0, 2.2, 2.3, 2.4, 2.7] # last day: over ceilingSIGNUPS = [38, 41, 44, 47, 43, 49, 45]def _dates(n: int) -> List[str]: """Return the last ``n`` dates ending today, oldest first, as 'YYYY-MM-DD'.""" today = date(2026, 7, 11) # fixed for reproducible demo output return [(today - timedelta(days=n - 1 - i)).isoformat() for i in range(n)]def seed_fictional_data(conn: storage.sqlite3.Connection) -> int: """Insert the fictional multi-day series. Returns rows written.""" days = _dates(7) rows = [] series = [ ("gsc", "clicks", CLICKS), ("gsc", "impressions", IMPRESSIONS), ("ga4", "sessions", SESSIONS), ("ga4", "lcp", LCP), ("custom", "signups", SIGNUPS), ] for source, metric, values in series: for day, value in zip(days, values): rows.append({"source": source, "metric": metric, "value": value, "date": day}) return storage.upsert_metrics(conn, rows)def _spark(values: List[float]) -> str: """Tiny inline sparkline from a list of numbers (Unicode block bars).""" bars = "▁▂▃▄▅▆▇█" lo, hi = min(values), max(values) span = (hi - lo) or 1.0 return "".join(bars[int((v - lo) / span * (len(bars) - 1))] for v in values)def print_dashboard(conn: storage.sqlite3.Connection) -> None: """Render the small text dashboard from whatever is in the DB.""" print("=" * 60) print(" DASHBOARD ÚNICO — dados fictícios (sem credenciais)") print(" Unified dashboard — fictional data (no credentials)") print("=" * 60) snapshot = storage.latest_per_metric(conn) print("\n Snapshot (valor mais recente / latest value)") print(" " + "-" * 46) print(f" {'source':<8}{'metric':<14}{'value':>12} {'date':>12}") for row in snapshot: value = row["value"] shown = f"{value:.1f}" if value != int(value) else f"{int(value)}" print(f" {row['source']:<8}{row['metric']:<14}{shown:>12} {row['date']:>12}") print("\n Tendência 7 dias (7-day trend)") print(" " + "-" * 46) for source, metric in [ ("gsc", "clicks"), ("gsc", "impressions"), ("ga4", "sessions"), ("ga4", "lcp"), ("custom", "signups"), ]: rows = storage.query_recent(conn, metric, source=source, limit=7) values = [r["value"] for r in reversed(rows)] # oldest -> newest label = f"{source}/{metric}" print(f" {label:<20} {_spark(values)} ({values[0]:g} → {values[-1]:g})") fired = list(alerts.evaluate(conn)) print("\n Alertas (alerts)") print(" " + "-" * 46) if not fired: print(" OK — nenhuma métrica fora do limite / no metrics off threshold.") else: for alert in fired: print(f" [ALERTA] {alert.message}") print("=" * 60)def main() -> None: conn = storage.connect(":memory:") storage.init_db(conn) written = seed_fictional_data(conn) print(f"[demo] {written} linhas fictícias inseridas / fictional rows inserted\n") print_dashboard(conn) conn.close()if __name__ == "__main__": main()#!/usr/bin/env python3"""Cap. 23 - Metric collectors for the unified dashboard - STRUCTURE.Defines collectors that pull data from three sources into a common shape: * Google Search Console (Search Analytics API) * Google Analytics 4 (GA4 Data API) * A "custom" source (your own internal API)Each collector returns a list of metric dicts shaped like:: {"source": "gsc", "metric": "clicks", "value": 1234, "date": "2026-07-10"}so they can be fed straight into ``storage.upsert_metrics``.============================================================================ REQUER CREDENCIAL / REQUIRES CREDENTIALS --------------------------------------------------------------------------- The three real collectors below (GSC, GA4, Custom) need API credentials to return live data. Credentials are read ONLY from ``os.environ`` - no secret is ever hardcoded. Until they are set, the collectors raise ``NotImplementedError`` with setup guidance, but the module stays importable and ``py_compile``-clean. Use ``sample_rows()`` / ``collect_all(..., use_sample=True)`` to run the whole pipeline offline with fictional data.============================================================================Part of the "living code" companion repository for the book"Aparecer ou Sumir" / "Be Seen or Be Forgotten"."""from __future__ import annotationsimport osfrom typing import Dict, List# ---------------------------------------------------------------------------# TODO: credential — all values come from the environment. Nothing is hardcoded.# Set these before switching use_sample to False.# ---------------------------------------------------------------------------# TODO: credential — path to a Google service-account key JSON with GSC + GA4 access.GOOGLE_APPLICATION_CREDENTIALS = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", "")# TODO: credential — the verified Search Console site URL, e.g. "https://www.example.com/".GSC_SITE_URL = os.environ.get("GSC_SITE_URL", "")# TODO: credential — the GA4 numeric property id, e.g. "properties/123456789".GA4_PROPERTY_ID = os.environ.get("GA4_PROPERTY_ID", "")# TODO: credential — base URL + token for your own custom metrics API.CUSTOM_API_BASE = os.environ.get("CUSTOM_API_BASE", "")CUSTOM_API_TOKEN = os.environ.get("CUSTOM_API_TOKEN", "")MetricRow = Dict[str, object]class Collector: """Base collector. Subclasses implement :meth:`collect`.""" source = "base" def collect(self, date: str) -> List[MetricRow]: """Return metric rows for the given 'YYYY-MM-DD' date.""" raise NotImplementedErrorclass SearchConsoleCollector(Collector): """Collector for the Google Search Console Search Analytics API. REQUIRES CREDENTIALS: GOOGLE_APPLICATION_CREDENTIALS + GSC_SITE_URL. """ source = "gsc" def collect(self, date: str) -> List[MetricRow]: """Pull clicks/impressions/CTR/position for ``date`` from GSC. TODO: credential — steps once you have a service account: 1. pip install google-api-python-client google-auth 2. Build the service with GOOGLE_APPLICATION_CREDENTIALS:: from googleapiclient.discovery import build from google.oauth2.service_account import Credentials creds = Credentials.from_service_account_file( GOOGLE_APPLICATION_CREDENTIALS, scopes=["https://www.googleapis.com/auth/webmasters.readonly"]) service = build("searchconsole", "v1", credentials=creds) 3. Call ``searchanalytics.query`` on GSC_SITE_URL for the date range. """ if not (GOOGLE_APPLICATION_CREDENTIALS and GSC_SITE_URL): raise NotImplementedError( "SearchConsoleCollector needs GOOGLE_APPLICATION_CREDENTIALS and " "GSC_SITE_URL. See the docstring for the setup steps." ) # TODO: credential — replace with the real searchanalytics.query call. raise NotImplementedError("GSC API call not implemented yet.")class GA4Collector(Collector): """Collector for the Google Analytics 4 Data API. REQUIRES CREDENTIALS: GOOGLE_APPLICATION_CREDENTIALS + GA4_PROPERTY_ID. """ source = "ga4" def collect(self, date: str) -> List[MetricRow]: """Pull sessions/engagement/conversions for ``date`` from GA4. TODO: credential — steps once you have a service account: 1. pip install google-analytics-data 2. Authenticate with GOOGLE_APPLICATION_CREDENTIALS and run a ``runReport`` request against GA4_PROPERTY_ID with the metrics and the date range you need. """ if not (GOOGLE_APPLICATION_CREDENTIALS and GA4_PROPERTY_ID): raise NotImplementedError( "GA4Collector needs GOOGLE_APPLICATION_CREDENTIALS and " "GA4_PROPERTY_ID. See the docstring for the setup steps." ) # TODO: credential — replace with the real runReport call. raise NotImplementedError("GA4 API call not implemented yet.")class CustomCollector(Collector): """Collector for your own internal metrics API. REQUIRES CREDENTIALS: CUSTOM_API_BASE + CUSTOM_API_TOKEN. """ source = "custom" def collect(self, date: str) -> List[MetricRow]: """Pull custom metrics for ``date`` from your own API. TODO: credential — perform an authenticated HTTP request to CUSTOM_API_BASE using CUSTOM_API_TOKEN and map the response into MetricRow dicts. """ if not (CUSTOM_API_BASE and CUSTOM_API_TOKEN): raise NotImplementedError( "CustomCollector needs CUSTOM_API_BASE and CUSTOM_API_TOKEN." ) # TODO: credential — replace with the real HTTP request. raise NotImplementedError("Custom API call not implemented yet.")def sample_rows(date: str) -> List[MetricRow]: """Return sample-shaped rows so the pipeline can be exercised offline. Useful for local development, tests and demos - it mimics exactly what the real collectors would return, without touching any external service or needing any credential. """ return [ {"source": "gsc", "metric": "clicks", "value": 1250, "date": date}, {"source": "gsc", "metric": "impressions", "value": 48000, "date": date}, {"source": "ga4", "metric": "sessions", "value": 3100, "date": date}, {"source": "ga4", "metric": "lcp", "value": 2.1, "date": date}, {"source": "custom", "metric": "signups", "value": 42, "date": date}, ]def collect_all(date: str, use_sample: bool = True) -> List[MetricRow]: """Collect from every source for ``date``. When ``use_sample`` is True (default) it returns offline sample data so the dashboard can run without credentials. Set it to False once the collectors above are implemented AND their credentials are configured to hit the real APIs. """ if use_sample: return sample_rows(date) rows: List[MetricRow] = [] for collector in (SearchConsoleCollector(), GA4Collector(), CustomCollector()): rows.extend(collector.collect(date)) return rowsif __name__ == "__main__": # Demo without credentials: print the sample rows. for r in collect_all("2026-07-10"): print(r)#!/usr/bin/env python3"""Cap. 23 - SQLite storage layer for the unified dashboard.A tiny, fully functional persistence layer built on the stdlib ``sqlite3``.Every metric collected from Google Search Console, GA4 or a custom source isstored in a single ``metrics`` table so the dashboard and alert rules can querythem uniformly. No external dependencies, no credentials.Part of the "living code" companion repository for the book"Aparecer ou Sumir" / "Be Seen or Be Forgotten"."""from __future__ import annotationsimport sqlite3from pathlib import Pathfrom typing import Iterable, List, Mapping, Optional, Union# Default on-disk database file for the dashboard.DEFAULT_DB_PATH = "dashboard.db"def connect(db_path: Union[str, Path] = DEFAULT_DB_PATH) -> sqlite3.Connection: """Open a SQLite connection with row access by column name.""" conn = sqlite3.connect(str(db_path)) conn.row_factory = sqlite3.Row return conndef init_db(conn: sqlite3.Connection) -> None: """Create the metrics table (and a helpful unique index) if missing. Schema: source - where the metric came from (e.g. 'gsc', 'ga4', 'custom') metric - the metric name (e.g. 'clicks', 'lcp', 'signups') value - the numeric value date - the day the metric refers to, as 'YYYY-MM-DD' The UNIQUE(source, metric, date) index makes upserts idempotent: re-running a collector for the same day updates the value instead of duplicating rows. """ conn.execute( """ CREATE TABLE IF NOT EXISTS metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, metric TEXT NOT NULL, value REAL NOT NULL, date TEXT NOT NULL ) """ ) conn.execute( """ CREATE UNIQUE INDEX IF NOT EXISTS ux_metrics_source_metric_date ON metrics (source, metric, date) """ ) conn.commit()def upsert_metric( conn: sqlite3.Connection, source: str, metric: str, value: float, date: str,) -> None: """Insert or update a single metric row keyed by (source, metric, date).""" conn.execute( """ INSERT INTO metrics (source, metric, value, date) VALUES (?, ?, ?, ?) ON CONFLICT(source, metric, date) DO UPDATE SET value = excluded.value """, (source, metric, value, date), ) conn.commit()def upsert_metrics(conn: sqlite3.Connection, rows: Iterable[Mapping[str, object]]) -> int: """Bulk upsert. Each row is a mapping with source/metric/value/date keys. Returns the number of rows processed. """ prepared = [ (r["source"], r["metric"], float(r["value"]), r["date"]) for r in rows ] conn.executemany( """ INSERT INTO metrics (source, metric, value, date) VALUES (?, ?, ?, ?) ON CONFLICT(source, metric, date) DO UPDATE SET value = excluded.value """, prepared, ) conn.commit() return len(prepared)def query_recent( conn: sqlite3.Connection, metric: str, source: Optional[str] = None, limit: int = 30,) -> List[sqlite3.Row]: """Return the most recent rows for a metric, newest first. Args: metric: metric name to filter on (e.g. 'clicks'). source: optional source filter (e.g. 'gsc'). limit: maximum number of rows to return. """ if source is None: cur = conn.execute( "SELECT source, metric, value, date FROM metrics " "WHERE metric = ? ORDER BY date DESC LIMIT ?", (metric, limit), ) else: cur = conn.execute( "SELECT source, metric, value, date FROM metrics " "WHERE metric = ? AND source = ? ORDER BY date DESC LIMIT ?", (metric, source, limit), ) return cur.fetchall()def latest_per_metric(conn: sqlite3.Connection) -> List[sqlite3.Row]: """Return the single newest row for every (source, metric) pair. Handy for rendering a "current state" dashboard snapshot. """ cur = conn.execute( """ SELECT m.source, m.metric, m.value, m.date FROM metrics AS m JOIN ( SELECT source, metric, MAX(date) AS max_date FROM metrics GROUP BY source, metric ) AS latest ON m.source = latest.source AND m.metric = latest.metric AND m.date = latest.max_date ORDER BY m.source, m.metric """ ) return cur.fetchall()if __name__ == "__main__": # Tiny self-check that runs without any external credentials. connection = connect(":memory:") init_db(connection) upsert_metric(connection, "gsc", "clicks", 1200, "2026-07-10") upsert_metric(connection, "gsc", "clicks", 1300, "2026-07-11") for row in query_recent(connection, "clicks"): print(dict(row)) connection.close()#!/usr/bin/env python3"""Cap. 23 - Threshold-based alerts for the unified dashboard.Reads recent metrics from the SQLite storage layer and yields alerts when athreshold is crossed. Two kinds of rules are demonstrated: * DROP rules: fire when a metric drops more than X% versus the previous value (e.g. organic clicks fall > 20% day over day). * MAX rules: fire when a metric exceeds an absolute ceiling (e.g. Largest Contentful Paint > 2.5s).Pure Python, no external services, no credentials. The ``__main__`` demo insertssample data via ``storage.py`` so it runs standalone.Part of the "living code" companion repository for the book"Aparecer ou Sumir" / "Be Seen or Be Forgotten"."""from __future__ import annotationsfrom dataclasses import dataclassfrom typing import Iterator, List, Optionalimport storage@dataclassclass Threshold: """A single alert rule. kind: 'drop_pct' - fire when value falls more than ``limit`` percent vs previous. 'max' - fire when the latest value is greater than ``limit``. """ metric: str kind: str limit: float source: Optional[str] = None # optional source filter# Example threshold configuration. Tune these to your own baselines.EXAMPLE_THRESHOLDS: List[Threshold] = [ Threshold(metric="clicks", kind="drop_pct", limit=20.0, source="gsc"), Threshold(metric="sessions", kind="drop_pct", limit=25.0, source="ga4"), Threshold(metric="lcp", kind="max", limit=2.5, source="ga4"),]@dataclassclass Alert: """A fired alert, ready to be logged, emailed or posted to Slack.""" metric: str source: str message: str value: floatdef evaluate( conn: "storage.sqlite3.Connection", thresholds: Optional[List[Threshold]] = None,) -> Iterator[Alert]: """Yield an Alert for every threshold that is currently crossed. For each rule we pull the two most recent stored values for the metric and compare them (drop rules) or check the latest against a ceiling (max rules). """ rules = thresholds if thresholds is not None else EXAMPLE_THRESHOLDS for rule in rules: rows = storage.query_recent(conn, rule.metric, source=rule.source, limit=2) if not rows: continue latest = rows[0]["value"] src = rows[0]["source"] if rule.kind == "max": if latest > rule.limit: yield Alert( metric=rule.metric, source=src, value=latest, message=( f"{rule.metric} = {latest} exceeds max {rule.limit} " f"(source: {src})" ), ) elif rule.kind == "drop_pct": if len(rows) < 2: continue # need a previous value to compute a drop previous = rows[1]["value"] if previous <= 0: continue drop_pct = (previous - latest) / previous * 100.0 if drop_pct > rule.limit: yield Alert( metric=rule.metric, source=src, value=latest, message=( f"{rule.metric} dropped {drop_pct:.1f}% " f"({previous} -> {latest}) exceeding {rule.limit}% " f"(source: {src})" ), )def _demo() -> None: """Standalone demo: build an in-memory DB, insert sample data, run alerts.""" conn = storage.connect(":memory:") storage.init_db(conn) # Sample data engineered to trigger both a drop alert and a max alert. storage.upsert_metrics( conn, [ # clicks fell from 1500 -> 1000 (~33% drop) -> triggers drop_pct. {"source": "gsc", "metric": "clicks", "value": 1500, "date": "2026-07-10"}, {"source": "gsc", "metric": "clicks", "value": 1000, "date": "2026-07-11"}, # sessions roughly stable -> no alert. {"source": "ga4", "metric": "sessions", "value": 3000, "date": "2026-07-10"}, {"source": "ga4", "metric": "sessions", "value": 2950, "date": "2026-07-11"}, # lcp = 3.2s > 2.5s ceiling -> triggers max. {"source": "ga4", "metric": "lcp", "value": 3.2, "date": "2026-07-11"}, ], ) alerts = list(evaluate(conn)) if not alerts: print("No alerts. All metrics within thresholds.") for alert in alerts: print(f"[ALERT] {alert.message}") conn.close()if __name__ == "__main__": _demo()Dúvidas
Perguntas frequentes
Preciso de credenciais para rodar o dashboard?
Não. O demo_data.py roda ponta a ponta com dados fictícios, sem tocar em nenhuma API. Só o collectors.py precisa de credenciais (GSC/GA4/custom) para buscar dados reais — e essas seções estão claramente marcadas.
Onde os dados ficam guardados?
No storage.py, uma camada SQLite da biblioteca padrão. Todas as métricas vão para uma única tabela metrics(source, metric, value, date), com upsert idempotente por (source, metric, date).
Como os alertas funcionam?
O alerts.py lê as métricas recentes do storage e aplica regras de limite: quedas percentuais (ex.: cliques caem > 20% de um dia para o outro) e tetos absolutos (ex.: LCP > 2,5s). Cada regra cruzada vira um alerta.
Implementação
Prefere que eu implemente?
Conte o seu caso numa conversa e a gente define o escopo juntos.
Agendar conversa