@saulwade/swl-ses 2.4.2 → 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.
Files changed (198) hide show
  1. package/CLAUDE.md +194 -241
  2. package/README.md +600 -597
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/abogado-diablo-swl.md +145 -0
  6. package/agentes/accesibilidad-wcag-swl.md +690 -690
  7. package/agentes/arquitecto-swl.md +267 -267
  8. package/agentes/auto-evolucion-swl.md +908 -908
  9. package/agentes/backend-api-swl.md +1 -1
  10. package/agentes/backend-csharp-swl.md +420 -420
  11. package/agentes/backend-go-swl.md +390 -390
  12. package/agentes/backend-java-swl.md +281 -281
  13. package/agentes/backend-node-swl.md +1 -1
  14. package/agentes/backend-python-swl.md +1 -1
  15. package/agentes/backend-rust-swl.md +364 -364
  16. package/agentes/backend-workers-swl.md +482 -482
  17. package/agentes/cloud-infra-swl.md +509 -509
  18. package/agentes/consolidador-swl.md +541 -541
  19. package/agentes/datos-swl.md +1 -1
  20. package/agentes/depurador-swl.md +352 -352
  21. package/agentes/devops-ci-swl.md +400 -400
  22. package/agentes/disenador-ui-swl.md +569 -569
  23. package/agentes/documentador-swl.md +345 -345
  24. package/agentes/frontend-angular-swl.md +621 -621
  25. package/agentes/frontend-css-swl.md +716 -716
  26. package/agentes/frontend-react-swl.md +692 -692
  27. package/agentes/frontend-swl.md +496 -496
  28. package/agentes/frontend-tailwind-swl.md +826 -826
  29. package/agentes/gh-fix-ci-swl.md +6 -1
  30. package/agentes/implementador-swl.md +1 -1
  31. package/agentes/investigador-swl.md +432 -432
  32. package/agentes/investigador-ux-swl.md +505 -505
  33. package/agentes/llm-apps-swl.md +1 -1
  34. package/agentes/migrador-swl.md +442 -442
  35. package/agentes/mobile-android-swl.md +511 -511
  36. package/agentes/mobile-cross-swl.md +541 -541
  37. package/agentes/mobile-ios-swl.md +502 -502
  38. package/agentes/mobile-testing-swl.md +302 -302
  39. package/agentes/nemesis-auditor-swl.md +285 -285
  40. package/agentes/notificador-swl.md +1 -1
  41. package/agentes/observabilidad-swl.md +438 -438
  42. package/agentes/pagos-swl.md +310 -310
  43. package/agentes/perfilador-usuario-swl.md +321 -321
  44. package/agentes/planificador-swl.md +399 -399
  45. package/agentes/producto-prd-swl.md +589 -589
  46. package/agentes/red-team-swl.md +218 -218
  47. package/agentes/release-manager-swl.md +590 -590
  48. package/agentes/rendimiento-swl.md +713 -713
  49. package/agentes/resolutor-build-swl.md +10 -1
  50. package/agentes/revisor-angular-swl.md +278 -278
  51. package/agentes/revisor-codigo-swl.md +1 -1
  52. package/agentes/revisor-csharp-swl.md +264 -264
  53. package/agentes/revisor-go-swl.md +259 -259
  54. package/agentes/revisor-java-swl.md +257 -257
  55. package/agentes/revisor-kotlin-swl.md +273 -273
  56. package/agentes/revisor-nextjs-swl.md +281 -281
  57. package/agentes/revisor-php-swl.md +271 -271
  58. package/agentes/revisor-react-swl.md +278 -278
  59. package/agentes/revisor-rust-swl.md +346 -346
  60. package/agentes/revisor-seguridad-swl.md +399 -399
  61. package/agentes/revisor-swift-swl.md +268 -268
  62. package/agentes/revisor-typescript-swl.md +346 -346
  63. package/agentes/sre-swl.md +1 -1
  64. package/agentes/tdd-qa-swl.md +393 -393
  65. package/bin/lib/bot-comandos.js +1 -1
  66. package/bin/swl-ses.js +6 -0
  67. package/comandos/swl/adoptar-proyecto.md +14 -2
  68. package/comandos/swl/configurar-ci.md +8 -1
  69. package/comandos/swl/deuda-codigo.md +97 -97
  70. package/comandos/swl/discutir-fase.md +22 -118
  71. package/comandos/swl/fix.md +118 -0
  72. package/comandos/swl/nuevo-proyecto.md +54 -3
  73. package/comandos/swl/predecir.md +32 -2
  74. package/comandos/swl/seguridad.md +189 -0
  75. package/comandos/swl/status.md +5 -3
  76. package/habilidades/aprendizaje-continuo/SKILL.md +3 -1
  77. package/habilidades/discutir-fase/SKILL.md +84 -81
  78. package/habilidades/discutir-fase/recursos/plantilla-contexto.md +136 -0
  79. package/habilidades/doc-sync/SKILL.md +3 -1
  80. package/habilidades/doubt-driven-review/SKILL.md +15 -1
  81. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  82. package/habilidades/estructura-proyecto-claude/SKILL.md +11 -2
  83. package/habilidades/harness-claude-code/SKILL.md +3 -1
  84. package/habilidades/instalar-sistema/SKILL.md +3 -1
  85. package/habilidades/meta-reglas-extendido/SKILL.md +92 -0
  86. package/habilidades/meta-reglas-extendido/recursos/analisis-previo-tareas-grandes.md +186 -0
  87. package/habilidades/meta-reglas-extendido/recursos/analizar-directorios-antes-de-escribir.md +235 -0
  88. package/habilidades/meta-reglas-extendido/recursos/api-diseno.md +413 -0
  89. package/habilidades/meta-reglas-extendido/recursos/arquitectura.md +491 -0
  90. package/habilidades/meta-reglas-extendido/recursos/arreglar-al-detectar.md +264 -0
  91. package/habilidades/meta-reglas-extendido/recursos/debatir-antes-de-aceptar.md +152 -0
  92. package/habilidades/meta-reglas-extendido/recursos/git-workflow.md +259 -0
  93. package/habilidades/meta-reglas-extendido/recursos/gobernanza.md +291 -0
  94. package/habilidades/meta-reglas-extendido/recursos/memoria-consolidada.md +263 -0
  95. package/habilidades/meta-reglas-extendido/recursos/seguridad-agentes.md +443 -0
  96. package/habilidades/meta-reglas-extendido/recursos/sesiones-paralelas.md +190 -0
  97. package/habilidades/meta-reglas-extendido/recursos/sin-duplicacion-reglas-globales.md +179 -0
  98. package/habilidades/meta-reglas-extendido/recursos/skills-estandar.md +394 -0
  99. package/habilidades/meta-reglas-extendido/recursos/usar-code-review-graph.md +156 -0
  100. package/habilidades/meta-reglas-extendido/recursos/usar-context7.md +236 -0
  101. package/habilidades/meta-reglas-extendido/recursos/usar-sistema-swl.md +253 -0
  102. package/habilidades/meta-reglas-extendido/recursos/verificar-citas-normativas.md +527 -0
  103. package/habilidades/meta-skills-estandar/SKILL.md +3 -1
  104. package/habilidades/nuevo-proyecto/SKILL.md +20 -3
  105. package/habilidades/php-experto/SKILL.md +10 -3
  106. package/habilidades/{filament-admin/SKILL.md → php-experto/recursos/filament-admin.md} +23 -39
  107. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
  108. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
  109. package/habilidades/proceso-debate-adversarial/recursos/personas.md +5 -4
  110. package/habilidades/proceso-ingenieria-requerimientos/SKILL.md +147 -0
  111. package/hooks/check-update.js +19 -10
  112. package/hooks/contexto-subagente.js +68 -68
  113. package/hooks/degradacion-instintos.js +1 -1
  114. package/hooks/extraccion-aprendizajes.js +2 -2
  115. package/hooks/lib/briefing.js +3 -3
  116. package/hooks/lib/nudge-tracker.js +1 -1
  117. package/hooks/lib/otlp-exporter.js +1 -1
  118. package/hooks/lib/webhook-dedup.js +1 -1
  119. package/hooks/session-briefing.js +1 -1
  120. package/llms.txt +6 -6
  121. package/manifiestos/canonical-hashes.json +989 -0
  122. package/manifiestos/hooks-config.json +469 -469
  123. package/manifiestos/invariantes-criticos.json +30 -30
  124. package/manifiestos/modulos.json +168 -135
  125. package/manifiestos/perfiles.json +0 -2
  126. package/manifiestos/skills-lock.json +52 -59
  127. package/package.json +7 -5
  128. package/plantillas/github-workflows/README.md +15 -1
  129. package/plantillas/github-workflows/swl-devsecops.yml +70 -0
  130. package/plugin.json +5 -5
  131. package/reglas/analisis-previo-tareas-grandes.md +30 -156
  132. package/reglas/analizar-directorios-antes-de-escribir.md +30 -211
  133. package/reglas/api-diseno.md +28 -398
  134. package/reglas/arquitectura.md +35 -456
  135. package/reglas/arreglar-al-detectar.md +30 -230
  136. package/reglas/debatir-antes-de-aceptar.md +30 -143
  137. package/reglas/docs.md +7 -0
  138. package/reglas/estilo-codigo.md +9 -0
  139. package/reglas/fragmentos-compartidos.md +6 -0
  140. package/reglas/git-workflow.md +44 -240
  141. package/reglas/gobernanza.md +23 -262
  142. package/reglas/memoria-consolidada.md +34 -228
  143. package/reglas/performance.md +8 -0
  144. package/reglas/pruebas.md +12 -0
  145. package/reglas/seguridad-agentes.md +37 -418
  146. package/reglas/seguridad.md +12 -0
  147. package/reglas/sesiones-paralelas.md +29 -162
  148. package/reglas/sin-duplicacion-reglas-globales.md +25 -166
  149. package/reglas/skills-estandar.md +23 -373
  150. package/reglas/usar-code-review-graph.md +31 -140
  151. package/reglas/usar-context7.md +30 -208
  152. package/reglas/usar-sistema-swl.md +47 -242
  153. package/reglas/verificar-citas-normativas.md +47 -537
  154. package/scripts/actualizar.js +253 -253
  155. package/scripts/audit-tools/auditar-relleno-inventario.js +145 -0
  156. package/scripts/auditar-clases-conocidas.js +106 -0
  157. package/scripts/bootstrap-instintos.js +2 -2
  158. package/scripts/canario-hooks.js +166 -0
  159. package/scripts/cli/configurar-ci.js +2 -1
  160. package/scripts/evidencia-valor.js +93 -0
  161. package/scripts/field-report.js +1 -1
  162. package/scripts/generar-comandos.js +143 -0
  163. package/scripts/generar-inventario.js +236 -23
  164. package/scripts/generar-matriz-lenguajes.js +1 -1
  165. package/scripts/instalador.js +15 -1
  166. package/scripts/lib/configurar-ci.js +10 -3
  167. package/scripts/lib/detectar-runtime.js +12 -3
  168. package/scripts/lib/diary-entry.js +3 -1
  169. package/scripts/lib/drift-detector.js +1 -1
  170. package/scripts/lib/evidencia-valor.js +189 -0
  171. package/scripts/lib/expandir-targets.js +71 -71
  172. package/scripts/lib/frontmatter-md.js +63 -0
  173. package/scripts/lib/parsear-opciones.js +2 -0
  174. package/scripts/lib/prune-componentes.js +180 -0
  175. package/scripts/lib/reglas-globales-conocidas.json +16 -2
  176. package/scripts/lib/scoring-instintos.js +2 -2
  177. package/scripts/lib/toml-merge.js +204 -204
  178. package/scripts/lib/transformadores/claude.js +1 -1
  179. package/scripts/lib/transformadores/codex.js +1 -1
  180. package/scripts/lib/transformadores/copilot.js +1 -1
  181. package/scripts/lib/transformadores/cursor.js +1 -1
  182. package/scripts/lib/transformadores/gemini.js +22 -2
  183. package/scripts/lib/transformadores/opencode.js +1 -1
  184. package/scripts/mcp-server/auth.js +105 -105
  185. package/scripts/mcp-server/cache.js +106 -106
  186. package/scripts/prune.js +102 -0
  187. package/scripts/publicar.js +18 -2
  188. package/scripts/tui/pantallas/inspect.js +175 -175
  189. package/scripts/tui/pantallas/uninstall-wizard.js +210 -210
  190. package/scripts/tui/pantallas/update-wizard.js +234 -234
  191. package/scripts/tui/pantallas/welcome.js +189 -189
  192. package/habilidades/paid-media-tracking/SKILL.md +0 -269
  193. package/habilidades/paid-media-tracking/recursos/auditoria-tracking.md +0 -220
  194. package/habilidades/paid-media-tracking/recursos/google-ads-api.md +0 -215
  195. package/habilidades/tracking-measurement/SKILL.md +0 -239
  196. package/habilidades/tracking-measurement/recursos/consent-mode.md +0 -231
  197. package/habilidades/tracking-measurement/recursos/gtm-datalayer.md +0 -216
  198. 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.