@saulwade/swl-ses 2.4.3 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +194 -241
- package/README.md +600 -597
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/agentes/abogado-diablo-swl.md +145 -0
- package/agentes/accesibilidad-wcag-swl.md +690 -690
- package/agentes/arquitecto-swl.md +267 -267
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/backend-api-swl.md +1 -1
- package/agentes/backend-csharp-swl.md +420 -420
- package/agentes/backend-go-swl.md +390 -390
- package/agentes/backend-java-swl.md +281 -281
- package/agentes/backend-node-swl.md +1 -1
- package/agentes/backend-python-swl.md +1 -1
- package/agentes/backend-rust-swl.md +364 -364
- package/agentes/backend-workers-swl.md +482 -482
- package/agentes/cloud-infra-swl.md +509 -509
- package/agentes/consolidador-swl.md +541 -541
- package/agentes/datos-swl.md +1 -1
- package/agentes/depurador-swl.md +352 -352
- package/agentes/devops-ci-swl.md +400 -400
- package/agentes/disenador-ui-swl.md +569 -569
- package/agentes/documentador-swl.md +345 -345
- package/agentes/frontend-angular-swl.md +621 -621
- package/agentes/frontend-css-swl.md +716 -716
- package/agentes/frontend-react-swl.md +692 -692
- package/agentes/frontend-swl.md +496 -496
- package/agentes/frontend-tailwind-swl.md +826 -826
- package/agentes/gh-fix-ci-swl.md +6 -1
- package/agentes/implementador-swl.md +1 -1
- package/agentes/investigador-swl.md +432 -432
- package/agentes/investigador-ux-swl.md +505 -505
- package/agentes/llm-apps-swl.md +1 -1
- package/agentes/migrador-swl.md +442 -442
- package/agentes/mobile-android-swl.md +511 -511
- package/agentes/mobile-cross-swl.md +541 -541
- package/agentes/mobile-ios-swl.md +502 -502
- package/agentes/mobile-testing-swl.md +302 -302
- package/agentes/nemesis-auditor-swl.md +285 -285
- package/agentes/notificador-swl.md +1 -1
- package/agentes/observabilidad-swl.md +438 -438
- package/agentes/pagos-swl.md +310 -310
- package/agentes/perfilador-usuario-swl.md +321 -321
- package/agentes/planificador-swl.md +399 -399
- package/agentes/producto-prd-swl.md +589 -589
- package/agentes/red-team-swl.md +218 -218
- package/agentes/release-manager-swl.md +590 -590
- package/agentes/rendimiento-swl.md +713 -713
- package/agentes/resolutor-build-swl.md +10 -1
- package/agentes/revisor-angular-swl.md +278 -278
- package/agentes/revisor-codigo-swl.md +1 -1
- package/agentes/revisor-csharp-swl.md +264 -264
- package/agentes/revisor-go-swl.md +259 -259
- package/agentes/revisor-java-swl.md +257 -257
- package/agentes/revisor-kotlin-swl.md +273 -273
- package/agentes/revisor-nextjs-swl.md +281 -281
- package/agentes/revisor-php-swl.md +271 -271
- package/agentes/revisor-react-swl.md +278 -278
- package/agentes/revisor-rust-swl.md +346 -346
- package/agentes/revisor-seguridad-swl.md +399 -399
- package/agentes/revisor-swift-swl.md +268 -268
- package/agentes/revisor-typescript-swl.md +346 -346
- package/agentes/sre-swl.md +1 -1
- package/agentes/tdd-qa-swl.md +393 -393
- package/bin/lib/bot-comandos.js +1 -1
- package/bin/swl-ses.js +6 -0
- package/comandos/swl/adoptar-proyecto.md +14 -2
- package/comandos/swl/configurar-ci.md +8 -1
- package/comandos/swl/deuda-codigo.md +97 -97
- package/comandos/swl/discutir-fase.md +22 -118
- package/comandos/swl/fix.md +118 -0
- package/comandos/swl/nuevo-proyecto.md +54 -3
- package/comandos/swl/predecir.md +32 -2
- package/comandos/swl/seguridad.md +189 -0
- package/comandos/swl/status.md +5 -3
- package/habilidades/aprendizaje-continuo/SKILL.md +3 -1
- package/habilidades/discutir-fase/SKILL.md +84 -81
- package/habilidades/discutir-fase/recursos/plantilla-contexto.md +136 -0
- package/habilidades/doc-sync/SKILL.md +3 -1
- package/habilidades/doubt-driven-review/SKILL.md +15 -1
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/estructura-proyecto-claude/SKILL.md +11 -2
- package/habilidades/harness-claude-code/SKILL.md +3 -1
- package/habilidades/instalar-sistema/SKILL.md +3 -1
- package/habilidades/meta-reglas-extendido/SKILL.md +92 -0
- package/habilidades/meta-reglas-extendido/recursos/analisis-previo-tareas-grandes.md +186 -0
- package/habilidades/meta-reglas-extendido/recursos/analizar-directorios-antes-de-escribir.md +235 -0
- package/habilidades/meta-reglas-extendido/recursos/api-diseno.md +413 -0
- package/habilidades/meta-reglas-extendido/recursos/arquitectura.md +491 -0
- package/habilidades/meta-reglas-extendido/recursos/arreglar-al-detectar.md +264 -0
- package/habilidades/meta-reglas-extendido/recursos/debatir-antes-de-aceptar.md +152 -0
- package/habilidades/meta-reglas-extendido/recursos/git-workflow.md +259 -0
- package/habilidades/meta-reglas-extendido/recursos/gobernanza.md +291 -0
- package/habilidades/meta-reglas-extendido/recursos/memoria-consolidada.md +263 -0
- package/habilidades/meta-reglas-extendido/recursos/seguridad-agentes.md +443 -0
- package/habilidades/meta-reglas-extendido/recursos/sesiones-paralelas.md +190 -0
- package/habilidades/meta-reglas-extendido/recursos/sin-duplicacion-reglas-globales.md +179 -0
- package/habilidades/meta-reglas-extendido/recursos/skills-estandar.md +394 -0
- package/habilidades/meta-reglas-extendido/recursos/usar-code-review-graph.md +156 -0
- package/habilidades/meta-reglas-extendido/recursos/usar-context7.md +236 -0
- package/habilidades/meta-reglas-extendido/recursos/usar-sistema-swl.md +253 -0
- package/habilidades/meta-reglas-extendido/recursos/verificar-citas-normativas.md +527 -0
- package/habilidades/meta-skills-estandar/SKILL.md +3 -1
- package/habilidades/nuevo-proyecto/SKILL.md +20 -3
- package/habilidades/php-experto/SKILL.md +10 -3
- package/habilidades/{filament-admin/SKILL.md → php-experto/recursos/filament-admin.md} +23 -39
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +5 -4
- package/habilidades/proceso-ingenieria-requerimientos/SKILL.md +147 -0
- package/hooks/check-update.js +19 -10
- package/hooks/contexto-subagente.js +68 -68
- package/hooks/degradacion-instintos.js +1 -1
- package/hooks/extraccion-aprendizajes.js +2 -2
- package/hooks/lib/briefing.js +3 -3
- package/hooks/lib/nudge-tracker.js +1 -1
- package/hooks/lib/otlp-exporter.js +1 -1
- package/hooks/lib/webhook-dedup.js +1 -1
- package/hooks/session-briefing.js +1 -1
- package/llms.txt +6 -6
- package/manifiestos/canonical-hashes.json +713 -52
- package/manifiestos/hooks-config.json +469 -469
- package/manifiestos/invariantes-criticos.json +30 -30
- package/manifiestos/modulos.json +168 -135
- package/manifiestos/perfiles.json +0 -2
- package/manifiestos/skills-lock.json +49 -56
- package/package.json +7 -5
- package/plantillas/github-workflows/README.md +15 -1
- package/plantillas/github-workflows/swl-devsecops.yml +70 -0
- package/plugin.json +5 -5
- package/reglas/analisis-previo-tareas-grandes.md +30 -156
- package/reglas/analizar-directorios-antes-de-escribir.md +30 -211
- package/reglas/api-diseno.md +28 -398
- package/reglas/arquitectura.md +35 -456
- package/reglas/arreglar-al-detectar.md +30 -230
- package/reglas/debatir-antes-de-aceptar.md +30 -143
- package/reglas/docs.md +7 -0
- package/reglas/estilo-codigo.md +9 -0
- package/reglas/fragmentos-compartidos.md +6 -0
- package/reglas/git-workflow.md +44 -240
- package/reglas/gobernanza.md +23 -262
- package/reglas/memoria-consolidada.md +34 -228
- package/reglas/performance.md +8 -0
- package/reglas/pruebas.md +12 -0
- package/reglas/seguridad-agentes.md +37 -418
- package/reglas/seguridad.md +12 -0
- package/reglas/sesiones-paralelas.md +29 -162
- package/reglas/sin-duplicacion-reglas-globales.md +25 -166
- package/reglas/skills-estandar.md +23 -373
- package/reglas/usar-code-review-graph.md +31 -140
- package/reglas/usar-context7.md +30 -208
- package/reglas/usar-sistema-swl.md +47 -242
- package/reglas/verificar-citas-normativas.md +47 -537
- package/scripts/actualizar.js +253 -253
- package/scripts/audit-tools/auditar-relleno-inventario.js +145 -0
- package/scripts/auditar-clases-conocidas.js +106 -0
- package/scripts/bootstrap-instintos.js +2 -2
- package/scripts/canario-hooks.js +166 -0
- package/scripts/cli/configurar-ci.js +2 -1
- package/scripts/evidencia-valor.js +93 -0
- package/scripts/field-report.js +1 -1
- package/scripts/generar-comandos.js +143 -0
- package/scripts/generar-inventario.js +236 -23
- package/scripts/generar-matriz-lenguajes.js +1 -1
- package/scripts/instalador.js +15 -1
- package/scripts/lib/configurar-ci.js +10 -3
- package/scripts/lib/diary-entry.js +3 -1
- package/scripts/lib/drift-detector.js +1 -1
- package/scripts/lib/evidencia-valor.js +189 -0
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/frontmatter-md.js +63 -0
- package/scripts/lib/parsear-opciones.js +2 -0
- package/scripts/lib/prune-componentes.js +180 -0
- package/scripts/lib/reglas-globales-conocidas.json +16 -2
- package/scripts/lib/scoring-instintos.js +2 -2
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/lib/transformadores/claude.js +1 -1
- package/scripts/lib/transformadores/codex.js +1 -1
- package/scripts/lib/transformadores/copilot.js +1 -1
- package/scripts/lib/transformadores/cursor.js +1 -1
- package/scripts/lib/transformadores/gemini.js +22 -2
- package/scripts/lib/transformadores/opencode.js +1 -1
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/prune.js +102 -0
- package/scripts/publicar.js +18 -2
- package/scripts/tui/pantallas/inspect.js +175 -175
- package/scripts/tui/pantallas/uninstall-wizard.js +210 -210
- package/scripts/tui/pantallas/update-wizard.js +234 -234
- package/scripts/tui/pantallas/welcome.js +189 -189
- package/habilidades/paid-media-tracking/SKILL.md +0 -269
- package/habilidades/paid-media-tracking/recursos/auditoria-tracking.md +0 -220
- package/habilidades/paid-media-tracking/recursos/google-ads-api.md +0 -215
- package/habilidades/tracking-measurement/SKILL.md +0 -239
- package/habilidades/tracking-measurement/recursos/consent-mode.md +0 -231
- package/habilidades/tracking-measurement/recursos/gtm-datalayer.md +0 -216
- package/habilidades/tracking-measurement/recursos/meta-capi.md +0 -262
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
# Auditoría técnica de cuentas Google Ads
|
|
2
|
-
|
|
3
|
-
## Checklist técnico de auditoría
|
|
4
|
-
|
|
5
|
-
Una auditoría técnica verifica que la infraestructura de medición está
|
|
6
|
-
correctamente configurada — no evalúa la estrategia de campañas.
|
|
7
|
-
|
|
8
|
-
### Conversiones
|
|
9
|
-
|
|
10
|
-
- [ ] **Conversiones primarias configuradas**: al menos una acción de conversión
|
|
11
|
-
marcada como "Primaria" en la cuenta. Las conversiones secundarias no se usan
|
|
12
|
-
en las estrategias de puja automática.
|
|
13
|
-
- [ ] **Enhanced Conversions activadas**: en Configuración de conversiones,
|
|
14
|
-
"Enhanced conversions for web" debe estar habilitado con hash SHA-256.
|
|
15
|
-
- [ ] **Auto-tagging habilitado**: en Configuración de la cuenta, permite que
|
|
16
|
-
Google Ads agregue `gclid` a las URLs de destino para importación a GA4.
|
|
17
|
-
- [ ] **Sin conversiones duplicadas**: verificar que el mismo evento de compra
|
|
18
|
-
no esté configurado dos veces (una en GA4-importada y otra en etiqueta directa).
|
|
19
|
-
- [ ] **Ventana de conversión correcta**: campañas de búsqueda usan 30 días;
|
|
20
|
-
campañas de display o video pueden requerir ventanas más largas (90 días).
|
|
21
|
-
|
|
22
|
-
### Audiencias y remarketing
|
|
23
|
-
|
|
24
|
-
- [ ] **Etiqueta de remarketing activa**: Google Ads Tag o GA4 Audience deben
|
|
25
|
-
estar recibiendo datos (verificar en Audience Manager > Fuentes de audiencia).
|
|
26
|
-
- [ ] **Audiencias de remarketing creadas**: al menos "Todos los visitantes",
|
|
27
|
-
"Visitantes de producto" y "Compradores anteriores" para campañas RLSA.
|
|
28
|
-
- [ ] **Customer Match habilitado**: si se usa lista de clientes, la cuenta debe
|
|
29
|
-
tener historial suficiente y cumplimiento de políticas.
|
|
30
|
-
|
|
31
|
-
### Shopping (si aplica)
|
|
32
|
-
|
|
33
|
-
- [ ] **Feed de productos aprobado** en Merchant Center: sin errores de
|
|
34
|
-
disponibilidad, precio o imagen. Los items rechazados no aparecen en anuncios.
|
|
35
|
-
- [ ] **Vinculación Merchant Center — Google Ads**: confirmada en ambas cuentas.
|
|
36
|
-
- [ ] **Etiquetas personalizadas configuradas**: para segmentación por margen,
|
|
37
|
-
temporada o prioridad de inventario.
|
|
38
|
-
|
|
39
|
-
### Estructura técnica de la cuenta
|
|
40
|
-
|
|
41
|
-
- [ ] **Naming convention consistente**: campañas, ad groups y keywords siguen
|
|
42
|
-
una nomenclatura que permite filtrar y reportar programáticamente.
|
|
43
|
-
- [ ] **UTM parameters en URLs finales**: `utm_source=google&utm_medium=cpc&utm_campaign={campaignid}`
|
|
44
|
-
para atribución correcta en GA4 sin depender solo del auto-tagging.
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## Métricas de calidad de cuenta
|
|
49
|
-
|
|
50
|
-
Estas métricas indican la salud técnica de la implementación, no el
|
|
51
|
-
desempeño de negocio:
|
|
52
|
-
|
|
53
|
-
| Métrica | Objetivo | Señal de problema |
|
|
54
|
-
|---------|----------|------------------|
|
|
55
|
-
| Quality Score promedio (búsqueda) | ≥ 7/10 | < 5 indica relevancia baja o landing page deficiente |
|
|
56
|
-
| Impression Share de marca | ≥ 85% | < 70% indica competencia capturando tráfico de marca |
|
|
57
|
-
| Invalid click rate | < 2% | > 5% indica fraude de clics o tráfico de baja calidad |
|
|
58
|
-
| Discrepancia GA4 vs Google Ads | < 15% | > 15% indica problema de tracking o atribución |
|
|
59
|
-
| Conversiones con valor cero | 0% | Cualquier conversión primaria sin valor = ROAS incalculable |
|
|
60
|
-
|
|
61
|
-
### Query para Quality Score promedio
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
def obtener_quality_score_promedio(customer_id: str) -> dict:
|
|
65
|
-
"""Obtiene Quality Score promedio de keywords activas."""
|
|
66
|
-
ga_service = client.get_service("GoogleAdsService")
|
|
67
|
-
|
|
68
|
-
query = """
|
|
69
|
-
SELECT
|
|
70
|
-
ad_group_criterion.keyword.text,
|
|
71
|
-
ad_group_criterion.quality_info.quality_score,
|
|
72
|
-
ad_group_criterion.quality_info.creative_quality_score,
|
|
73
|
-
ad_group_criterion.quality_info.post_click_quality_score,
|
|
74
|
-
ad_group_criterion.quality_info.search_predicted_ctr,
|
|
75
|
-
metrics.impressions
|
|
76
|
-
FROM keyword_view
|
|
77
|
-
WHERE campaign.status = 'ENABLED'
|
|
78
|
-
AND ad_group.status = 'ENABLED'
|
|
79
|
-
AND ad_group_criterion.status = 'ENABLED'
|
|
80
|
-
AND ad_group_criterion.quality_info.quality_score > 0
|
|
81
|
-
AND segments.date DURING LAST_30_DAYS
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
response = ga_service.search(customer_id=customer_id, query=query)
|
|
85
|
-
|
|
86
|
-
scores = []
|
|
87
|
-
impresiones_total = 0
|
|
88
|
-
suma_ponderada = 0
|
|
89
|
-
|
|
90
|
-
for row in response:
|
|
91
|
-
qs = row.ad_group_criterion.quality_info.quality_score
|
|
92
|
-
imp = row.metrics.impressions
|
|
93
|
-
scores.append(qs)
|
|
94
|
-
suma_ponderada += qs * imp
|
|
95
|
-
impresiones_total += imp
|
|
96
|
-
|
|
97
|
-
if not scores:
|
|
98
|
-
return {"error": "sin_keywords_con_quality_score"}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
"promedio_simple": round(sum(scores) / len(scores), 1),
|
|
102
|
-
"promedio_ponderado_impresiones": round(
|
|
103
|
-
suma_ponderada / impresiones_total, 1
|
|
104
|
-
) if impresiones_total > 0 else 0,
|
|
105
|
-
"total_keywords": len(scores),
|
|
106
|
-
"distribucion": {
|
|
107
|
-
"excelente_8_10": sum(1 for s in scores if s >= 8),
|
|
108
|
-
"bueno_6_7": sum(1 for s in scores if 6 <= s <= 7),
|
|
109
|
-
"bajo_1_5": sum(1 for s in scores if s <= 5),
|
|
110
|
-
},
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## Herramientas de diagnóstico
|
|
117
|
-
|
|
118
|
-
### Google Tag Assistant
|
|
119
|
-
|
|
120
|
-
Extensión de Chrome para verificar que la etiqueta de Google Ads está
|
|
121
|
-
disparando correctamente en el sitio:
|
|
122
|
-
|
|
123
|
-
1. Activar grabación en la página objetivo
|
|
124
|
-
2. Completar el flujo de conversión (compra, registro, etc.)
|
|
125
|
-
3. Verificar que el evento aparece en el historial con los parámetros correctos
|
|
126
|
-
4. Confirmar que no hay errores de "Tag not found" o "Tag fired multiple times"
|
|
127
|
-
|
|
128
|
-
### Google Ads Diagnostics Tool
|
|
129
|
-
|
|
130
|
-
En la consola de Google Ads: Herramientas > Diagnóstico y vista previa
|
|
131
|
-
de anuncios. Permite verificar por qué un anuncio no aparece para una
|
|
132
|
-
búsqueda específica (presupuesto agotado, keyword pausada, policy violation).
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## Reporte automático de auditoría con Python
|
|
137
|
-
|
|
138
|
-
```python
|
|
139
|
-
import json
|
|
140
|
-
from dataclasses import dataclass, field
|
|
141
|
-
|
|
142
|
-
@dataclass
|
|
143
|
-
class ResultadoAuditoria:
|
|
144
|
-
customer_id: str
|
|
145
|
-
quality_score: dict = field(default_factory=dict)
|
|
146
|
-
discrepancia_conversiones: dict = field(default_factory=dict)
|
|
147
|
-
candidatos_negative: list = field(default_factory=list)
|
|
148
|
-
alertas: list[str] = field(default_factory=list)
|
|
149
|
-
|
|
150
|
-
def ejecutar_auditoria_completa(
|
|
151
|
-
customer_id: str,
|
|
152
|
-
ga4_conversiones: float,
|
|
153
|
-
campaign_ids: list[str],
|
|
154
|
-
) -> ResultadoAuditoria:
|
|
155
|
-
"""
|
|
156
|
-
Ejecuta auditoría técnica completa de una cuenta Google Ads.
|
|
157
|
-
|
|
158
|
-
Retorna un ResultadoAuditoria con métricas de calidad,
|
|
159
|
-
discrepancias de conversiones y candidatos a negative keywords.
|
|
160
|
-
"""
|
|
161
|
-
resultado = ResultadoAuditoria(customer_id=customer_id)
|
|
162
|
-
|
|
163
|
-
# 1. Quality Score
|
|
164
|
-
resultado.quality_score = obtener_quality_score_promedio(customer_id)
|
|
165
|
-
qs_prom = resultado.quality_score.get("promedio_simple", 0)
|
|
166
|
-
if qs_prom < 7:
|
|
167
|
-
resultado.alertas.append(
|
|
168
|
-
f"Quality Score promedio bajo: {qs_prom}/10 (objetivo ≥ 7)"
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
# 2. Rendimiento de campañas y discrepancia de conversiones
|
|
172
|
-
campanas = obtener_rendimiento_campanas(customer_id)
|
|
173
|
-
google_ads_conv = sum(c["conversiones"] for c in campanas)
|
|
174
|
-
resultado.discrepancia_conversiones = detectar_discrepancia_conversiones(
|
|
175
|
-
google_ads_conv, ga4_conversiones
|
|
176
|
-
) # Nota: esta función es async en SKILL.md, adaptar según contexto
|
|
177
|
-
|
|
178
|
-
if resultado.discrepancia_conversiones.get("estado") != "ok":
|
|
179
|
-
pct = resultado.discrepancia_conversiones.get("discrepancia_porcentual", 0)
|
|
180
|
-
resultado.alertas.append(
|
|
181
|
-
f"Discrepancia de conversiones: {pct}% (umbral: 15%)"
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
# 3. Search terms con desperdicio
|
|
185
|
-
analisis = analizar_search_terms(customer_id, campaign_ids)
|
|
186
|
-
resultado.candidatos_negative = analisis["candidatos_negative"][:10]
|
|
187
|
-
if resultado.candidatos_negative:
|
|
188
|
-
total_desperdicio = sum(
|
|
189
|
-
t["costo_desperdiciado"] for t in resultado.candidatos_negative
|
|
190
|
-
)
|
|
191
|
-
resultado.alertas.append(
|
|
192
|
-
f"{len(resultado.candidatos_negative)} términos candidatos a negative, "
|
|
193
|
-
f"costo desperdiciado estimado: ${total_desperdicio:,.2f}"
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
return resultado
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def exportar_reporte_json(resultado: ResultadoAuditoria, ruta: str) -> None:
|
|
200
|
-
"""Exporta el resultado de la auditoría como JSON."""
|
|
201
|
-
import dataclasses
|
|
202
|
-
with open(ruta, "w", encoding="utf-8") as f:
|
|
203
|
-
json.dump(dataclasses.asdict(resultado), f, ensure_ascii=False, indent=2)
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### Ejemplo de uso
|
|
207
|
-
|
|
208
|
-
```python
|
|
209
|
-
resultado = ejecutar_auditoria_completa(
|
|
210
|
-
customer_id="1234567890",
|
|
211
|
-
ga4_conversiones=142.0,
|
|
212
|
-
campaign_ids=["111222333", "444555666"],
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
print(f"Alertas encontradas: {len(resultado.alertas)}")
|
|
216
|
-
for alerta in resultado.alertas:
|
|
217
|
-
print(f" - {alerta}")
|
|
218
|
-
|
|
219
|
-
exportar_reporte_json(resultado, "auditoria_2026_04.json")
|
|
220
|
-
```
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
# Google Ads API — Referencia técnica
|
|
2
|
-
|
|
3
|
-
## Autenticación
|
|
4
|
-
|
|
5
|
-
### Tipos de credenciales
|
|
6
|
-
|
|
7
|
-
| Tipo | Cuándo usar | Configuración requerida |
|
|
8
|
-
|------|-------------|------------------------|
|
|
9
|
-
| OAuth2 con refresh token | Acceso en nombre de un usuario | `client_id`, `client_secret`, `refresh_token` |
|
|
10
|
-
| Service Account | Procesos server-side sin interacción humana | Archivo JSON de service account + acceso delegado en MCC |
|
|
11
|
-
| Developer Token | Obligatorio en todos los casos | Aprobado por Google en la cuenta MCC principal |
|
|
12
|
-
|
|
13
|
-
### Niveles del Developer Token
|
|
14
|
-
|
|
15
|
-
| Nivel | Quota diaria | Cuentas accesibles |
|
|
16
|
-
|-------|-------------|-------------------|
|
|
17
|
-
| Test | 15,000 ops/día | Solo cuentas de prueba |
|
|
18
|
-
| Basic | 1,000 ops/día | Todas las cuentas vinculadas al MCC |
|
|
19
|
-
| Standard | 10,000 ops/día | Requiere solicitud y aprobación de Google |
|
|
20
|
-
|
|
21
|
-
En producción con reportes diarios, **Standard es necesario** si se tienen
|
|
22
|
-
más de 50 cuentas o consultas de más de 30 días de datos.
|
|
23
|
-
|
|
24
|
-
### Configuración con diccionario Python
|
|
25
|
-
|
|
26
|
-
```python
|
|
27
|
-
from google.ads.googleads.client import GoogleAdsClient
|
|
28
|
-
|
|
29
|
-
config = {
|
|
30
|
-
"developer_token": settings.GOOGLE_ADS_DEVELOPER_TOKEN,
|
|
31
|
-
"client_id": settings.GOOGLE_ADS_CLIENT_ID,
|
|
32
|
-
"client_secret": settings.GOOGLE_ADS_CLIENT_SECRET,
|
|
33
|
-
"refresh_token": settings.GOOGLE_ADS_REFRESH_TOKEN,
|
|
34
|
-
"login_customer_id": settings.GOOGLE_ADS_MCC_ID, # ID del MCC, sin guiones
|
|
35
|
-
"use_proto_plus": True, # Objetos Python nativos en lugar de protobuf raw
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
client = GoogleAdsClient.load_from_dict(config)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Configuración con archivo YAML (alternativa)
|
|
42
|
-
|
|
43
|
-
```yaml
|
|
44
|
-
# google-ads.yaml (NO subir a repositorio)
|
|
45
|
-
developer_token: "Tu_Developer_Token"
|
|
46
|
-
client_id: "client_id.apps.googleusercontent.com"
|
|
47
|
-
client_secret: "client_secret"
|
|
48
|
-
refresh_token: "1//0g..."
|
|
49
|
-
login_customer_id: "1234567890"
|
|
50
|
-
use_proto_plus: true
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
```python
|
|
54
|
-
client = GoogleAdsClient.load_from_storage("google-ads.yaml")
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## GAQL — Google Ads Query Language
|
|
60
|
-
|
|
61
|
-
### Estructura básica
|
|
62
|
-
|
|
63
|
-
```sql
|
|
64
|
-
SELECT campo1, campo2, metrica1
|
|
65
|
-
FROM recurso_principal
|
|
66
|
-
WHERE condicion1 AND condicion2
|
|
67
|
-
ORDER BY metrica1 DESC
|
|
68
|
-
LIMIT 100
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Recursos principales
|
|
72
|
-
|
|
73
|
-
| Recurso | Qué contiene |
|
|
74
|
-
|---------|-------------|
|
|
75
|
-
| `campaign` | Campañas: nombre, status, tipo, presupuesto |
|
|
76
|
-
| `ad_group` | Grupos de anuncios dentro de campañas |
|
|
77
|
-
| `ad_group_ad` | Anuncios individuales con sus métricas |
|
|
78
|
-
| `keyword_view` | Keywords configuradas con Quality Score |
|
|
79
|
-
| `search_term_view` | Términos exactos que activaron anuncios |
|
|
80
|
-
| `geographic_view` | Rendimiento por ubicación geográfica |
|
|
81
|
-
| `audience_view` | Rendimiento por audiencia configurada |
|
|
82
|
-
| `shopping_performance_view` | Rendimiento de campañas Shopping |
|
|
83
|
-
|
|
84
|
-
### Segmentos comunes
|
|
85
|
-
|
|
86
|
-
Los segmentos dividen las métricas por dimensión. No todos son compatibles
|
|
87
|
-
con todos los recursos — la API retorna error si se combina incorrectamente.
|
|
88
|
-
|
|
89
|
-
```sql
|
|
90
|
-
-- Segmentar por fecha
|
|
91
|
-
SELECT campaign.name, metrics.clicks, segments.date
|
|
92
|
-
FROM campaign
|
|
93
|
-
WHERE segments.date DURING LAST_30_DAYS
|
|
94
|
-
|
|
95
|
-
-- Segmentar por dispositivo
|
|
96
|
-
SELECT campaign.name, metrics.clicks, segments.device
|
|
97
|
-
FROM campaign
|
|
98
|
-
WHERE segments.date DURING LAST_7_DAYS
|
|
99
|
-
|
|
100
|
-
-- Rangos de fecha explícitos
|
|
101
|
-
WHERE segments.date BETWEEN '2026-01-01' AND '2026-03-31'
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Períodos predefinidos útiles
|
|
105
|
-
|
|
106
|
-
| Constante GAQL | Equivalente |
|
|
107
|
-
|----------------|-------------|
|
|
108
|
-
| `LAST_7_DAYS` | Últimos 7 días |
|
|
109
|
-
| `LAST_30_DAYS` | Últimos 30 días |
|
|
110
|
-
| `LAST_MONTH` | Mes calendario anterior completo |
|
|
111
|
-
| `THIS_MONTH` | Mes calendario actual hasta hoy |
|
|
112
|
-
| `LAST_YEAR` | Año anterior completo |
|
|
113
|
-
|
|
114
|
-
### Filtros de status importantes
|
|
115
|
-
|
|
116
|
-
```sql
|
|
117
|
-
-- Solo campañas activas (ENABLED)
|
|
118
|
-
WHERE campaign.status = 'ENABLED'
|
|
119
|
-
|
|
120
|
-
-- Campañas activas y pausadas (excluye eliminadas)
|
|
121
|
-
WHERE campaign.status IN ('ENABLED', 'PAUSED')
|
|
122
|
-
|
|
123
|
-
-- Search terms de campañas activas únicamente
|
|
124
|
-
WHERE campaign.status = 'ENABLED'
|
|
125
|
-
AND ad_group.status = 'ENABLED'
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## Paginación y streaming
|
|
131
|
-
|
|
132
|
-
### `search` vs `search_stream`
|
|
133
|
-
|
|
134
|
-
| Método | Cuándo usar | Comportamiento |
|
|
135
|
-
|--------|-------------|----------------|
|
|
136
|
-
| `search` | Resultados esperados < 10,000 filas | Devuelve páginas de 10,000 resultados |
|
|
137
|
-
| `search_stream` | Resultados esperados > 10,000 filas | Stream de chunks, más eficiente en memoria |
|
|
138
|
-
|
|
139
|
-
```python
|
|
140
|
-
# Para datasets grandes (search terms de cuentas con alto volumen)
|
|
141
|
-
stream = ga_service.search_stream(customer_id=customer_id, query=query)
|
|
142
|
-
resultados = []
|
|
143
|
-
for batch in stream:
|
|
144
|
-
for row in batch.results:
|
|
145
|
-
resultados.append(procesar_fila(row))
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## Batch operations con `MutateGoogleAdsRequest`
|
|
151
|
-
|
|
152
|
-
Para operaciones de escritura (agregar negative keywords, pausar campañas),
|
|
153
|
-
agrupar en un solo request reduce el consumo de quota.
|
|
154
|
-
|
|
155
|
-
```python
|
|
156
|
-
from google.ads.googleads.client import GoogleAdsClient
|
|
157
|
-
|
|
158
|
-
def agregar_negative_keywords(
|
|
159
|
-
customer_id: str,
|
|
160
|
-
campaign_id: str,
|
|
161
|
-
terminos: list[str],
|
|
162
|
-
) -> None:
|
|
163
|
-
"""Agrega lista de negative keywords a una campaña en un solo request."""
|
|
164
|
-
campaign_criterion_service = client.get_service("CampaignCriterionService")
|
|
165
|
-
|
|
166
|
-
operaciones = []
|
|
167
|
-
for termino in terminos:
|
|
168
|
-
operacion = client.get_type("CampaignCriterionOperation")
|
|
169
|
-
criterio = operacion.create
|
|
170
|
-
criterio.campaign = f"customers/{customer_id}/campaigns/{campaign_id}"
|
|
171
|
-
criterio.negative = True
|
|
172
|
-
criterio.keyword.text = termino
|
|
173
|
-
criterio.keyword.match_type = client.enums.KeywordMatchTypeEnum.BROAD
|
|
174
|
-
operaciones.append(operacion)
|
|
175
|
-
|
|
176
|
-
campaign_criterion_service.mutate_campaign_criteria(
|
|
177
|
-
customer_id=customer_id,
|
|
178
|
-
operations=operaciones,
|
|
179
|
-
)
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## Errores comunes y solución
|
|
185
|
-
|
|
186
|
-
| Error | Causa probable | Solución |
|
|
187
|
-
|-------|----------------|----------|
|
|
188
|
-
| `PERMISSION_DENIED` | `login_customer_id` incorrecto o sin acceso | Verificar que el MCC tiene acceso a la sub-cuenta |
|
|
189
|
-
| `QUOTA_ERROR` | Quota diaria agotada | Implementar caché, solicitar nivel Standard |
|
|
190
|
-
| `NOT_FOUND` | `customer_id` o `campaign_id` no existe | Verificar IDs sin guiones |
|
|
191
|
-
| `INVALID_CUSTOMER_ID` | `customer_id` con guiones | `customer_id.replace("-", "")` |
|
|
192
|
-
| `AUTHENTICATION_ERROR` | `refresh_token` vencido o revocado | Regenerar refresh token en Google OAuth Playground |
|
|
193
|
-
| `RESOURCE_EXHAUSTED` | Demasiados requests en poco tiempo | Implementar retry con backoff exponencial |
|
|
194
|
-
|
|
195
|
-
### Retry con backoff exponencial
|
|
196
|
-
|
|
197
|
-
```python
|
|
198
|
-
import time
|
|
199
|
-
from google.ads.googleads.errors import GoogleAdsException
|
|
200
|
-
|
|
201
|
-
def ejecutar_con_retry(fn, max_reintentos: int = 3):
|
|
202
|
-
"""Ejecuta una llamada a la API con retry y backoff exponencial."""
|
|
203
|
-
for intento in range(max_reintentos):
|
|
204
|
-
try:
|
|
205
|
-
return fn()
|
|
206
|
-
except GoogleAdsException as ex:
|
|
207
|
-
es_quota = any(
|
|
208
|
-
"QUOTA" in str(e.error_code) for e in ex.failure.errors
|
|
209
|
-
)
|
|
210
|
-
if es_quota and intento < max_reintentos - 1:
|
|
211
|
-
espera = 2 ** intento # 1s, 2s, 4s
|
|
212
|
-
time.sleep(espera)
|
|
213
|
-
continue
|
|
214
|
-
raise
|
|
215
|
-
```
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: tracking-measurement
|
|
3
|
-
description: >
|
|
4
|
-
Implementación técnica de sistemas de medición web: GTM (dataLayer, triggers,
|
|
5
|
-
variables, tags), GA4 (eventos, e-commerce, dimensiones personalizadas), Meta
|
|
6
|
-
CAPI server-side con deduplicación, Enhanced Conversions y Consent Mode v2.
|
|
7
|
-
Cargar cuando se implemente conversion tracking, se configure GTM, se integre
|
|
8
|
-
GA4 o Meta Pixel, se construya un endpoint server-side para CAPI, o se
|
|
9
|
-
implemente cumplimiento de privacidad GDPR/CCPA en tracking.
|
|
10
|
-
version: "1.0.0"
|
|
11
|
-
herramientasPermitidas: [Read, Write]
|
|
12
|
-
evolvable: true # default para skill estandar
|
|
13
|
-
exclusiones:
|
|
14
|
-
- "No cargar para análisis de datos de conversiones ya capturados (reportes de ROAS, atribución multi-touch) — para análisis de datos de paid media cargar `paid-media-tracking`."
|
|
15
|
-
- "No cargar para instrumentación de aplicaciones backend con OpenTelemetry o métricas de negocio en Prometheus — para observabilidad de sistemas cargar `monitoring-alertas`."
|
|
16
|
-
- "No cargar para implementar sistemas de analytics propietarios o data warehouses (BigQuery, Snowflake) sin componente de tracking web — para pipelines de datos cargar el skill de cloud correspondiente."
|
|
17
|
-
- "No cargar para scraping o extracción de datos de plataformas publicitarias externas — para acceso a APIs de Google Ads o Meta Business cargar `paid-media-tracking`."
|
|
18
|
-
---
|
|
19
|
-
# tracking-measurement — Sistemas de medición web
|
|
20
|
-
|
|
21
|
-
## Cuándo NO cargar
|
|
22
|
-
|
|
23
|
-
- La tarea es analizar datos de conversiones ya capturados (ROAS, atribución): cargar `paid-media-tracking`.
|
|
24
|
-
- La tarea es instrumentación de backend con OpenTelemetry o Prometheus: cargar `monitoring-alertas`.
|
|
25
|
-
- La tarea es construir un data warehouse o pipeline de datos sin componente de tracking web: cargar el skill de cloud correspondiente.
|
|
26
|
-
- La tarea es acceder a APIs de Google Ads o Meta Business para reportes: cargar `paid-media-tracking`.
|
|
27
|
-
|
|
28
|
-
## Cuándo cargar este skill
|
|
29
|
-
|
|
30
|
-
- Configurar GTM: workspace, triggers, variables, tags
|
|
31
|
-
- Implementar eventos GA4 (e-commerce, custom dimensions, Measurement Protocol)
|
|
32
|
-
- Construir endpoint server-side para Meta Conversions API (CAPI)
|
|
33
|
-
- Implementar Enhanced Conversions de Google Ads
|
|
34
|
-
- Añadir Consent Mode v2 (GDPR/CCPA) a una aplicación web
|
|
35
|
-
- Integrar CMP (Cookiebot, OneTrust, Usercentrics) con GTM
|
|
36
|
-
- Depurar discrepancias entre datos de plataformas y datos reales
|
|
37
|
-
|
|
38
|
-
## Arquitectura de medición moderna
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
Navegador Servidor Plataformas
|
|
42
|
-
───────── ──────── ──────────
|
|
43
|
-
GTM Container Server-side GTM Google Analytics 4
|
|
44
|
-
└─ dataLayer.push() ──► (Cloud Run / App Engine) Meta Ads Manager
|
|
45
|
-
└─ GA4 tag ──► └─ GA4 Measurement API Google Ads
|
|
46
|
-
└─ Meta Pixel tag ──► └─ Meta CAPI Microsoft Ads
|
|
47
|
-
└─ Consent Mode ──► └─ Hash PII antes de enviar
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
El tracking browser-only pierde 40-60% de eventos en Safari/iOS por ITP.
|
|
51
|
-
El server-side tagging recupera esos eventos usando datos first-party.
|
|
52
|
-
|
|
53
|
-
## Reglas obligatorias
|
|
54
|
-
|
|
55
|
-
### Regla 1 — Limpiar ecommerce antes de cada evento
|
|
56
|
-
|
|
57
|
-
Siempre ejecutar `dataLayer.push({ ecommerce: null })` antes de cualquier
|
|
58
|
-
evento e-commerce. Sin esto, GTM mezcla datos de eventos anteriores.
|
|
59
|
-
|
|
60
|
-
**Verificación**: En Tag Assistant, cada evento e-commerce debe tener
|
|
61
|
-
`ecommerce: null` inmediatamente antes en el historial del dataLayer.
|
|
62
|
-
|
|
63
|
-
### Regla 2 — Hashear PII antes de enviar a plataformas externas
|
|
64
|
-
|
|
65
|
-
Email y teléfono NUNCA se envían en texto plano. Siempre SHA-256 normalizado:
|
|
66
|
-
email en minúsculas sin espacios, teléfono en formato E.164.
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
import hashlib
|
|
70
|
-
|
|
71
|
-
def hash_pii(valor: str) -> str:
|
|
72
|
-
"""Normaliza y hashea PII para plataformas externas (SHA-256)."""
|
|
73
|
-
return hashlib.sha256(valor.strip().lower().encode()).hexdigest()
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
**Verificación**: En los payloads de red (DevTools > Network), los campos
|
|
77
|
-
`em`, `ph`, `hashed_email` deben ser cadenas de 64 caracteres hexadecimales.
|
|
78
|
-
|
|
79
|
-
### Regla 3 — Deduplicación con event_id único
|
|
80
|
-
|
|
81
|
-
Cuando el mismo evento se envía desde browser (pixel) Y desde servidor (CAPI
|
|
82
|
-
o Measurement Protocol), AMBOS deben incluir el mismo `event_id`. Sin esto,
|
|
83
|
-
GA4 y Meta cuentan el evento dos veces.
|
|
84
|
-
|
|
85
|
-
```javascript
|
|
86
|
-
// Generar event_id antes de disparar el evento browser
|
|
87
|
-
const eventId = `purchase_${transactionId}_${Date.now()}`;
|
|
88
|
-
// Este mismo eventId se pasa al servidor para el envío CAPI
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**Verificación**: En Meta Events Manager > Test Events, el campo "Match
|
|
92
|
-
type" debe mostrar "Deduplicated" para eventos con browser + server.
|
|
93
|
-
|
|
94
|
-
### Regla 4 — Consent Mode: default denied, actualizar al consentir
|
|
95
|
-
|
|
96
|
-
Inicializar Consent Mode con todo denegado ANTES de cargar GTM. Actualizar
|
|
97
|
-
solo después de que el usuario otorgue consentimiento explícito.
|
|
98
|
-
|
|
99
|
-
```javascript
|
|
100
|
-
// Debe ejecutarse ANTES del snippet de GTM
|
|
101
|
-
window.dataLayer = window.dataLayer || [];
|
|
102
|
-
function gtag(){dataLayer.push(arguments);}
|
|
103
|
-
gtag('consent', 'default', {
|
|
104
|
-
analytics_storage: 'denied',
|
|
105
|
-
ad_storage: 'denied',
|
|
106
|
-
ad_user_data: 'denied',
|
|
107
|
-
ad_personalization: 'denied',
|
|
108
|
-
wait_for_update: 500,
|
|
109
|
-
});
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
**Verificación**: En Chrome DevTools > Application > Cookies, no deben
|
|
113
|
-
existir cookies `_ga`, `_fbp` antes de que el usuario acepte.
|
|
114
|
-
|
|
115
|
-
### Regla 5 — transaction_id único por compra
|
|
116
|
-
|
|
117
|
-
Un `transaction_id` duplicado en GA4 causa que la compra no se cuente
|
|
118
|
-
(deduplicación silenciosa). Usar el ID de orden del sistema, nunca
|
|
119
|
-
generado en frontend.
|
|
120
|
-
|
|
121
|
-
**Verificación**: En GA4 DebugView, el evento `purchase` debe aparecer
|
|
122
|
-
con el mismo `transaction_id` que la orden en la base de datos.
|
|
123
|
-
|
|
124
|
-
## dataLayer schema para e-commerce (GA4 Enhanced Ecommerce)
|
|
125
|
-
|
|
126
|
-
```javascript
|
|
127
|
-
window.dataLayer = window.dataLayer || [];
|
|
128
|
-
|
|
129
|
-
// Vista de producto
|
|
130
|
-
dataLayer.push({ ecommerce: null });
|
|
131
|
-
dataLayer.push({
|
|
132
|
-
event: 'view_item',
|
|
133
|
-
ecommerce: {
|
|
134
|
-
currency: 'MXN',
|
|
135
|
-
value: 299.00,
|
|
136
|
-
items: [{
|
|
137
|
-
item_id: 'SKU-001',
|
|
138
|
-
item_name: 'Producto Premium',
|
|
139
|
-
item_category: 'Software',
|
|
140
|
-
price: 299.00,
|
|
141
|
-
quantity: 1,
|
|
142
|
-
}]
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Compra completada
|
|
147
|
-
const eventId = `purchase_${transactionId}_${Date.now()}`;
|
|
148
|
-
dataLayer.push({ ecommerce: null });
|
|
149
|
-
dataLayer.push({
|
|
150
|
-
event: 'purchase',
|
|
151
|
-
event_id: eventId, // Para deduplicación con Meta CAPI
|
|
152
|
-
ecommerce: {
|
|
153
|
-
transaction_id: 'ORD-2026-001', // Único — del sistema backend
|
|
154
|
-
value: 299.00,
|
|
155
|
-
tax: 47.84,
|
|
156
|
-
currency: 'MXN',
|
|
157
|
-
items: [{
|
|
158
|
-
item_id: 'SKU-001',
|
|
159
|
-
item_name: 'Producto Premium',
|
|
160
|
-
price: 299.00,
|
|
161
|
-
quantity: 1,
|
|
162
|
-
}]
|
|
163
|
-
},
|
|
164
|
-
user_data: { // Para Enhanced Conversions (GTM hashea esto)
|
|
165
|
-
email_address: 'usuario@ejemplo.com',
|
|
166
|
-
phone_number: '+525512345678',
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
## GA4 via Measurement Protocol (server-side)
|
|
172
|
-
|
|
173
|
-
```python
|
|
174
|
-
import httpx
|
|
175
|
-
from app.core.config import settings
|
|
176
|
-
|
|
177
|
-
async def enviar_evento_ga4(
|
|
178
|
-
client_id: str,
|
|
179
|
-
evento: str,
|
|
180
|
-
parametros: dict,
|
|
181
|
-
session_id: str | None = None,
|
|
182
|
-
) -> None:
|
|
183
|
-
"""Envía evento a GA4 desde el servidor usando Measurement Protocol."""
|
|
184
|
-
payload = {
|
|
185
|
-
"client_id": client_id,
|
|
186
|
-
"events": [{
|
|
187
|
-
"name": evento,
|
|
188
|
-
"params": {
|
|
189
|
-
"session_id": session_id,
|
|
190
|
-
"engagement_time_msec": "100",
|
|
191
|
-
**parametros,
|
|
192
|
-
}
|
|
193
|
-
}]
|
|
194
|
-
}
|
|
195
|
-
async with httpx.AsyncClient() as client:
|
|
196
|
-
await client.post(
|
|
197
|
-
"https://www.google-analytics.com/mp/collect",
|
|
198
|
-
params={
|
|
199
|
-
"measurement_id": settings.GA4_MEASUREMENT_ID,
|
|
200
|
-
"api_secret": settings.GA4_API_SECRET, # Solo en servidor
|
|
201
|
-
},
|
|
202
|
-
json=payload,
|
|
203
|
-
timeout=5.0,
|
|
204
|
-
)
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
El `client_id` viene de la cookie `_ga`: formato `GA1.1.XXXXXXXXXX.XXXXXXXXXX`,
|
|
208
|
-
los dos últimos segmentos separados por punto.
|
|
209
|
-
|
|
210
|
-
## Anti-patrones críticos
|
|
211
|
-
|
|
212
|
-
| Anti-patrón | Problema | Solución |
|
|
213
|
-
|-------------|----------|----------|
|
|
214
|
-
| Email en texto plano en CAPI | Violación GDPR/Meta ToS | SHA-256 normalizado |
|
|
215
|
-
| `transaction_id` generado en frontend | Duplicados por recargas | Usar ID de orden del backend |
|
|
216
|
-
| Sin `ecommerce: null` previo | Datos de evento anterior contaminan | Siempre limpiar antes |
|
|
217
|
-
| `api_secret` de GA4 en frontend | Exposición de credenciales | Solo en variables de servidor |
|
|
218
|
-
| Pixel browser como única fuente | 40-60% pérdida en iOS | Agregar server-side CAPI |
|
|
219
|
-
| Consent Mode sin inicializar | Cookies antes del banner | Default denied antes de GTM |
|
|
220
|
-
|
|
221
|
-
## Referencias a recursos
|
|
222
|
-
|
|
223
|
-
| Tema | Archivo |
|
|
224
|
-
|------|---------|
|
|
225
|
-
| GTM: dataLayer avanzado, triggers, variables, workspace, server-side | [recursos/gtm-datalayer.md](recursos/gtm-datalayer.md) |
|
|
226
|
-
| Meta CAPI: endpoint completo, deduplicación, hashing PII, EMQ | [recursos/meta-capi.md](recursos/meta-capi.md) |
|
|
227
|
-
| Consent Mode v2, GDPR, CCPA, integración con CMPs | [recursos/consent-mode.md](recursos/consent-mode.md) |
|
|
228
|
-
|
|
229
|
-
---
|
|
230
|
-
|
|
231
|
-
## Gotchas / Errores comunes no obvios
|
|
232
|
-
|
|
233
|
-
**GTM dispara el tag de `purchase` dos veces en SPAs (React, Angular) porque el trigger "All Pages" se activa tanto en la carga inicial como en la navegación history-pushState, y el dataLayer push de compra queda en memoria entre rutas**: en una tienda Angular, el usuario completa la compra en `/checkout/success`, el evento `purchase` se envía correctamente. Pero si el usuario regresa a `/` usando el botón atrás y luego navega de nuevo a `/checkout/success` sin recargar, GTM vuelve a disparar el tag porque el dataLayer aún contiene el objeto `ecommerce` de la compra anterior. Causa: GTM persiste el dataLayer en memoria durante toda la sesión de la SPA; sin limpiar `{ ecommerce: null }` al navegar, el estado de eventos anteriores contamina rutas subsecuentes. Fix: en el router de la SPA, ejecutar `dataLayer.push({ ecommerce: null })` en cada cambio de ruta (Angular Router: `NavigationEnd` event), y usar triggers de GTM basados en evento personalizado (`purchase_completed`) en lugar de "All Pages" para eventos de conversión.
|
|
234
|
-
|
|
235
|
-
**Consent Mode v2 configurado con `wait_for_update: 500` bloquea el primer pageview de GA4 en dispositivos lentos donde el CMP tarda más de 500ms en renderizar, causando pérdida del 5-15% de pageviews en usuarios móviles con conexión 3G**: el banner de consentimiento en redes lentas tarda 800-1200ms en cargar el script del CMP. Durante ese tiempo, `wait_for_update: 500` expiró y GA4 disparó el pageview sin el estado de consentimiento correcto, lo que puede generar datos sin la cookie `_ga` y pageviews no atribuibles. Causa: `wait_for_update` es una espera máxima en milisegundos; si el CMP no llama a `gtag('consent', 'update', ...)` antes de ese límite, GA4 procede con el estado default. Fix: aumentar `wait_for_update` a 1500ms para móviles o implementar el CMP de forma que el script cargue de forma crítica (no diferida) para garantizar que actualiza el consentimiento antes del timeout.
|
|
236
|
-
|
|
237
|
-
**La deduplicación de Meta CAPI falla silenciosamente cuando el `event_id` del browser pixel y del server-side tienen formatos diferentes aunque semánticamente sean el mismo ID**: `event_id: "purchase_ORD-2026-001_1714500000000"` en el browser pixel y `event_id: "purchase_ord-2026-001_1714500000000"` (lowercase) en CAPI son tratados como eventos diferentes por Meta. Meta Events Manager muestra los dos como eventos separados sin deduplicar, duplicando las conversiones reportadas. Causa: Meta compara `event_id` como string exacto, case-sensitive. Fix: definir la generación del `event_id` en una sola función compartida (backend), enviarla al frontend como parte de la respuesta de creación de orden, y usar exactamente ese string en ambos lados. Documentar el formato como contrato: `purchase_{ORDER_ID_UPPERCASE}_{UNIX_TIMESTAMP_MS}`.
|
|
238
|
-
|
|
239
|
-
**El hashing SHA-256 de email para Meta CAPI falla en la deduplicación cuando el email viene con caracteres de espacio en blanco invisible (NBSP, U+00A0) que el usuario copió de un email de confirmación**: `hashlib.sha256("usuario@ejemplo.com".encode()).hexdigest()` y `hashlib.sha256("usuario@ejemplo.com\u00a0".encode()).hexdigest()` producen hashes completamente diferentes, y Meta no puede hacer match con el PII del usuario registrado. Causa: el método `.strip()` de Python elimina espacios ASCII (U+0020) pero no todos los caracteres de espacio Unicode como NBSP (U+00A0) o ZWSP (U+200B). Fix: normalizar el email con `unicodedata.normalize('NFKC', email).strip().lower()` antes del hash. La normalización NFKC convierte variantes Unicode de caracteres de espacio a sus equivalentes ASCII, garantizando consistencia del hash.
|