---
name: ingest_gsc_search_analytics
kind: pipeline
lang: py
domain: pipelines
version: "1.0.0"
purity: impure
signature: "def ingest_gsc_search_analytics(site_url: str = '', duckdb_path: str = '', pg_dsn: str = '', start_date: str = '', end_date: str = '', lookback_days: int = 5, credentials_path: str = '') -> dict"
description: "Pipeline de ingesta diaria de Google Search Console (Search Analytics): GSC -> DuckDB -> PostgreSQL. Autentica con una service account (gsc_auth), extrae las filas de Search Analytics por las dimensiones date/query/page (pull_gsc_search_analytics), crea la tabla DuckDB si no existe con una restriccion UNIQUE (duckdb_execute), transforma cada fila renombrando 'date'->'data_date' y rellenando defaults estables (country='', device='', search_type='web') para las dimensiones no pedidas, hace upsert idempotente en DuckDB (duckdb_upsert) y espeja la tabla completa a PostgreSQL en modo replace para que Metabase la lea (duckdb_to_postgres). DuckDB es la verdad acumulada (historico append idempotente); PostgreSQL es un espejo regenerado por completo cada corrida. Resuelve defaults de site_url/pg_dsn/duckdb_path desde env (GSC_SITE_URL, SEO_DSN, SEO_DUCKDB con fallback ~/.fn_seo/seo.duckdb). Resuelve fechas teniendo en cuenta el lag de ~3 dias de la API: end=hoy-3, start=hoy-(3+lookback_days), re-pulleando los ultimos dias para que el upsert corrija lo que GSC ajusta a posteriori. Devuelve un dict sin lanzar: {status:'ok', site_url, start_date, end_date, rows_pulled, duckdb, postgres} en exito, {status:'error', error} en fallo."
tags: [seo, gsc, search-console, pipelines, duckdb]
uses_functions:
  - gsc_auth_py_infra
  - pull_gsc_search_analytics_py_datascience
  - duckdb_execute_py_infra
  - duckdb_upsert_py_infra
  - duckdb_to_postgres_py_pipelines
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [os, datetime]
params:
  - name: site_url
    desc: "propiedad de Search Console: 'sc-domain:ejemplo.com' (propiedad de dominio) o la URL de prefijo 'https://ejemplo.com/'. Si esta vacio se lee de la env var GSC_SITE_URL. Obligatorio: ValueError si falta."
  - name: duckdb_path
    desc: "ruta al archivo DuckDB de la fuente de verdad acumulada. Si esta vacio se lee de la env var SEO_DUCKDB y, en su defecto, ~/.fn_seo/seo.duckdb. El directorio padre se crea (os.makedirs exist_ok=True)."
  - name: pg_dsn
    desc: "cadena de conexion PostgreSQL del espejo BI, p.ej. 'postgresql://user:pass@host:5432/db'. Si esta vacio se lee de la env var SEO_DSN. Obligatorio: ValueError si falta."
  - name: start_date
    desc: "fecha inicial inclusiva 'YYYY-MM-DD'. Si esta vacia se calcula como hoy-(3+lookback_days)."
  - name: end_date
    desc: "fecha final inclusiva 'YYYY-MM-DD'. Si esta vacia se calcula como hoy-3 (lag de la API de GSC)."
  - name: lookback_days
    desc: "numero de dias extra hacia atras que se re-pullean para que el upsert idempotente corrija los datos que GSC ajusta a posteriori (hasta ~3 dias). Default 5."
  - name: credentials_path
    desc: "ruta al JSON de la service account. Se pasa tal cual a gsc_auth, que ya hace su propio fallback a la env var GSC_SA_JSON."
output: "dict. En exito: {status:'ok', site_url:str, start_date:str, end_date:str, rows_pulled:int, duckdb:dict (resultado de duckdb_upsert), postgres:dict (resultado de duckdb_to_postgres)}. En error (sin lanzar): {status:'error', error:str}."
tested: true
tests:
  - "test_renombra_date_a_data_date_y_persiste_en_duckdb"
  - "test_resolucion_fechas_por_defecto"
  - "test_upsert_idempotente_no_duplica"
  - "test_falta_site_url_da_value_error"
  - "test_falta_pg_dsn_da_value_error"
test_file_path: "python/functions/pipelines/ingest_gsc_search_analytics_test.py"
file_path: "python/functions/pipelines/ingest_gsc_search_analytics.py"
---

## Ejemplo

```bash
# Con las 3 env seteadas, una sola corrida hace el snapshot diario completo:
export GSC_SITE_URL="sc-domain:ejemplo.com"
export SEO_DSN="postgresql://seo:****@127.0.0.1:5432/seo"
export GSC_SA_JSON="$HOME/.fn_seo/service_account.json"
# (SEO_DUCKDB opcional; por defecto ~/.fn_seo/seo.duckdb)
./fn run ingest_gsc_search_analytics
# -> {"status": "ok", "site_url": "sc-domain:ejemplo.com",
#     "start_date": "2026-06-09", "end_date": "2026-06-17",
#     "rows_pulled": 1280, "duckdb": {...}, "postgres": {...}}
```

```python
import sys
sys.path.insert(0, "python/functions")
from pipelines.ingest_gsc_search_analytics import ingest_gsc_search_analytics

# Variante explicita: rango de fechas fijo y rutas pasadas como args.
res = ingest_gsc_search_analytics(
    site_url="sc-domain:ejemplo.com",
    duckdb_path="/home/me/.fn_seo/seo.duckdb",
    pg_dsn="postgresql://seo:****@127.0.0.1:5432/seo",
    start_date="2026-06-01",
    end_date="2026-06-17",
    credentials_path="/home/me/.fn_seo/service_account.json",
)
print(res["rows_pulled"], res["status"])  # 4210 ok
```

## Cuando usarla

Cuando quieras un snapshot diario de Google Search Console acumulado y consultable
desde Metabase: cada corrida añade/actualiza los datos del rango en DuckDB y
regenera el espejo PostgreSQL. La invoca el DAG `seo-gsc-daily` de dag_engine una
vez al dia (no uses cron ni systemd timers: usa dag_engine). Para un re-pull manual
puntual de un rango concreto, pásale `start_date`/`end_date` a mano.

## Gotchas

- **Lag de ~3 dias**: la API de GSC no consolida datos hasta ~3 dias despues. Por
  eso `end_date` por defecto es hoy-3 y `start_date` retrocede `lookback_days` extra.
  Pedir hasta hoy devolveria filas vacias o incompletas.
- **Re-pull idempotente**: se re-piden a proposito los ultimos `lookback_days` dias.
  La restriccion `UNIQUE (site_url, data_date, query, page, country, device,
  search_type)` + `duckdb_upsert` actualizan esas filas sin duplicarlas, recogiendo
  las correcciones que GSC aplica a posteriori. El `snapshot_date` se sobrescribe al
  valor de la ultima corrida.
- **DuckDB es la verdad; PostgreSQL es un espejo**: la ingesta acumula histórico solo
  en DuckDB. El espejo a Postgres usa `mode='replace'` -> hace DROP + CREATE + INSERT
  de la tabla completa cada vez. NO escribas en la tabla Postgres ni esperes acumular
  alli: se borra y reescribe en cada corrida. Si quieres histórico, leelo de DuckDB.
- **Dimensiones**: este pull pide solo `date`/`query`/`page`. `country` y `device`
  quedan vacios y `search_type='web'` como defaults estables para que la tupla UNIQUE
  sea consistente. Si necesitas desglose por pais/dispositivo, es otro pull/tabla.
- **Requisitos de entorno**: necesita las 3 env (`GSC_SITE_URL`, `SEO_DSN`,
  `GSC_SA_JSON`) o sus args equivalentes, y la service account debe estar añadida como
  usuario con permiso sobre la propiedad en Search Console. Faltar `site_url` o
  `pg_dsn` devuelve `{status:'error'}` (ValueError capturado, no crash).
