@bacano/sdk 1.0.0 → 1.1.0

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 (68) hide show
  1. package/README.md +342 -7
  2. package/STOREFRONT_CLARITY.md +140 -0
  3. package/STOREFRONT_CONSTRUCTION.md +92 -0
  4. package/STOREFRONT_LEADS.md +130 -0
  5. package/STOREFRONT_LEGAL.md +66 -0
  6. package/STOREFRONT_MARKETING.md +286 -0
  7. package/STOREFRONT_PERFORMANCE.md +265 -0
  8. package/STOREFRONT_PREVIEW.md +474 -0
  9. package/client.d.ts +7 -1
  10. package/errors.d.ts +9 -1
  11. package/graphql.cjs +63 -0
  12. package/graphql.d.ts +2 -2
  13. package/graphql.js +63 -0
  14. package/index.cjs +1542 -192
  15. package/index.d.ts +8 -6
  16. package/index.js +1542 -192
  17. package/marketing/attribution.d.ts +51 -0
  18. package/marketing/clarity.d.ts +3 -0
  19. package/marketing/domain.d.ts +1 -0
  20. package/marketing/ga4.d.ts +2 -0
  21. package/marketing/index.d.ts +5 -0
  22. package/marketing/runtime.d.ts +2 -0
  23. package/marketing/sanitize.d.ts +2 -0
  24. package/marketing/types.d.ts +109 -0
  25. package/media.d.ts +45 -2
  26. package/modules/auth.d.ts +3 -2
  27. package/modules/availability.d.ts +2 -2
  28. package/modules/cart-session.d.ts +2 -2
  29. package/modules/cart.d.ts +3 -3
  30. package/modules/catalog.d.ts +15 -3
  31. package/modules/content.d.ts +2 -2
  32. package/modules/marketing.d.ts +5 -0
  33. package/modules/orders.d.ts +2 -2
  34. package/modules/profile.d.ts +1 -1
  35. package/modules/static-catalog.d.ts +13 -0
  36. package/modules/website.d.ts +7 -1
  37. package/package.json +14 -3
  38. package/react/context-registry.d.ts +9 -0
  39. package/react/context.d.ts +1 -1
  40. package/react/hooks.d.ts +23 -2
  41. package/react/marketing-context.d.ts +3 -0
  42. package/react/marketing-provider.d.ts +10 -0
  43. package/react/preview/cache.d.ts +23 -0
  44. package/react/preview/context.d.ts +5 -0
  45. package/react/preview/navigation.d.ts +9 -0
  46. package/react/preview/provider.d.ts +3 -0
  47. package/react/preview/shell.d.ts +3 -0
  48. package/react/preview/toolbar.d.ts +3 -0
  49. package/react/preview/types.d.ts +60 -0
  50. package/react/preview.cjs +742 -0
  51. package/react/preview.d.ts +14 -0
  52. package/react/preview.js +730 -0
  53. package/react/website-gate.d.ts +22 -0
  54. package/react.cjs +3432 -1780
  55. package/react.d.ts +8 -4
  56. package/react.js +3428 -1772
  57. package/skills/bacano-web/SKILL.md +109 -0
  58. package/skills/bacano-web/agents/openai.yaml +4 -0
  59. package/skills/bacano-web/references/common.md +91 -0
  60. package/skills/bacano-web/references/informational.md +39 -0
  61. package/skills/bacano-web/references/report.md +52 -0
  62. package/skills/bacano-web/references/storefront.md +116 -0
  63. package/skills/bacano-web/scripts/audit-http.mjs +247 -0
  64. package/skills/bacano-web/scripts/cli.mjs +222 -0
  65. package/skills/bacano-web/scripts/postinstall.mjs +8 -0
  66. package/transport/documents.d.ts +8 -6
  67. package/transport/graphql.d.ts +13 -1
  68. package/types.d.ts +202 -0
@@ -0,0 +1,130 @@
1
+ # Captura de leads en storefronts Bacano
2
+
3
+ Este es el contrato canónico para formularios comerciales construidos por un
4
+ storefront. Bacano no impone un constructor visual: cada tienda decide sus
5
+ campos y diseño, pero toda captura debe entrar por `@bacano/sdk`.
6
+
7
+ ## Responsabilidades
8
+
9
+ El storefront:
10
+
11
+ - Renderiza el formulario y valida su experiencia básica.
12
+ - Muestra exactamente `publicConfig.legal.directMarketingConsent.statement`.
13
+ - Mantiene la aceptación opcional desmarcada hasta una acción explícita.
14
+ - Genera una clave de idempotencia por envío y la conserva durante reintentos.
15
+ - Envía únicamente respuestas necesarias para el propósito informado.
16
+
17
+ Bacano:
18
+
19
+ - Deriva y valida el website y su dominio.
20
+ - Resuelve la campaña por su clave pública.
21
+ - Conserva la versión y huella del texto aceptado.
22
+ - Reutiliza la identidad, el lead o el cliente existente sin duplicarlos.
23
+ - Completa un correo o teléfono faltante únicamente cuando la coincidencia de
24
+ identidad es inequívoca; nunca reemplaza un dato existente.
25
+ - Conserva el contacto enviado dentro de la evidencia de autorización ligada a
26
+ la captura, incluso cuando el participante ya era cliente.
27
+ - Conserva la captura, atribución y respuestas como evidencia inmutable.
28
+ - Aplica límites de frecuencia, tamaño e idempotencia.
29
+
30
+ ## Configuración previa
31
+
32
+ 1. Configura el texto de **Autorización de comunicaciones comerciales** en la
33
+ pestaña Legal del website.
34
+ 2. Crea una campaña en Bacano y actívala.
35
+ 3. Usa su clave pública estable en el formulario.
36
+
37
+ Una campaña en borrador, pausada, finalizada, fuera de vigencia o asociada a
38
+ otro website no acepta capturas. Si el texto legal no existe, el storefront no
39
+ debe mostrar ni habilitar el formulario comercial.
40
+
41
+ ## Implementación
42
+
43
+ ```tsx
44
+ const consent = publicConfig.legal.directMarketingConsent;
45
+
46
+ if (!consent) return null;
47
+
48
+ // Renderiza consent.statement junto a un checkbox desmarcado.
49
+ // Ejecuta esta llamada únicamente después de validar el formulario y aceptar.
50
+ // Conserva esta clave en el estado del formulario mientras reintentas el envío.
51
+ const idempotencyKey = crypto.randomUUID();
52
+ const result = await client.marketing.captureLead({
53
+ campaignKey: "newsletter-baby-rex",
54
+ idempotencyKey,
55
+ commercialConsent: true,
56
+ contact: {
57
+ partyKind: "PERSON",
58
+ firstName: "Ana",
59
+ email: "ana@example.com",
60
+ },
61
+ answers: {
62
+ interest: "bebe-nina",
63
+ },
64
+ });
65
+ ```
66
+
67
+ Debe existir al menos correo o teléfono. Los nombres son opcionales. Para una
68
+ empresa pueden usarse `partyKind: "COMPANY"`, `legalName` y `commercialName`.
69
+
70
+ `source` se omite para formularios normales y será `WEBSITE`. Usa `QR`
71
+ únicamente cuando el formulario se abrió mediante una entrada QR controlada por
72
+ el storefront. UTM, referidor y click IDs se toman automáticamente de la
73
+ atribución segura del SDK.
74
+
75
+ El storefront no debe construir ni enviar manualmente el payload HTTP. El SDK
76
+ serializa este contrato como JSON y la ruta pública de Bacano lo procesa antes
77
+ de validar la campaña. Un SDK `1.0.14` o posterior conserva esta forma del
78
+ payload; no se requieren adaptadores específicos por tienda.
79
+
80
+ ## Respuestas personalizadas
81
+
82
+ `answers` es un snapshot del formulario, no un constructor de campos del CRM.
83
+
84
+ - Máximo 50 claves, cada una de hasta 80 caracteres.
85
+ - Payload total aproximado máximo: 30 KB.
86
+ - No incluyas contraseñas, tokens, datos de pago, documentos adjuntos ni HTML.
87
+ - No dupliques correo o teléfono dentro de `answers`; usa `contact`.
88
+ - Un cambio futuro del formulario no modifica capturas anteriores.
89
+
90
+ ## Idempotencia y privacidad
91
+
92
+ - La misma interacción debe reutilizar el mismo `idempotencyKey` hasta obtener
93
+ respuesta. Un nuevo envío voluntario usa una clave nueva.
94
+ - La respuesta es deliberadamente genérica y nunca revela si la persona ya era
95
+ lead o cliente.
96
+ - Si correo y teléfono pertenecen a identidades activas diferentes, Bacano no
97
+ sobrescribe ninguna de ellas y conserva el caso para revisión manual.
98
+ - No llames GraphQL, Hasura ni endpoints internos directamente.
99
+ - No captures un lead si el visitante no aceptó el texto comercial vigente.
100
+ - Una revocación posterior conserva el historial, pero impide comunicaciones
101
+ futuras hasta una nueva aceptación expresa.
102
+
103
+ ## Beneficio de primera compra
104
+
105
+ Cuando la campaña activa tiene objetivo `FIRST_PURCHASE_COUPON` y referencia un
106
+ cupón online vigente, Bacano puede encolar el correo de bienvenida después de
107
+ aceptar la captura. Esta operación es completamente del servidor:
108
+
109
+ - el storefront no envía correos ni consulta Resend;
110
+ - no debe afirmar que el cupón ya fue enviado;
111
+ - repetir la misma captura no duplica el beneficio;
112
+ - una falla de correo nunca convierte la captura en error;
113
+ - Bacano revalida autorización, campaña, website, cupón y compras anteriores
114
+ inmediatamente antes del envío.
115
+
116
+ El contrato de `captureLead()` no cambia y no expone estados internos del
117
+ correo. La evidencia operativa queda en `outbound_emails` dentro de Bacano. El
118
+ newsletter masivo y las automatizaciones visuales continúan fuera de alcance.
119
+
120
+ ## Checklist para agentes
121
+
122
+ - [ ] Leí este contrato antes de crear el formulario.
123
+ - [ ] Uso un único `BacanoClient` inicializado.
124
+ - [ ] Leo el texto desde `publicConfig.legal.directMarketingConsent`.
125
+ - [ ] La aceptación empieza desmarcada y es explícita.
126
+ - [ ] Uso `client.marketing.captureLead()` y no GraphQL directo.
127
+ - [ ] Conservo la clave de idempotencia durante reintentos.
128
+ - [ ] No envío secretos ni información innecesaria en `answers`.
129
+ - [ ] No afirmo que el correo o el cupón fueron entregados a partir de la
130
+ respuesta genérica de `captureLead()`.
@@ -0,0 +1,66 @@
1
+ # Storefront legal settings
2
+
3
+ This document is the canonical contract for legal content exposed by the
4
+ Bacano SDK. Storefronts must use this configuration instead of maintaining
5
+ parallel legal documents in Content or querying GraphQL directly.
6
+
7
+ ## Public contract
8
+
9
+ `getWebsitePublicConfig()` returns the current configuration for the resolved
10
+ website:
11
+
12
+ ```ts
13
+ const config = await getWebsitePublicConfig(options);
14
+
15
+ config.legal = {
16
+ schemaVersion: 1,
17
+ version: 3,
18
+ contentHash: "...",
19
+ documents: {
20
+ terms: { type: "doc", content: [] },
21
+ shipping: { type: "doc", content: [] },
22
+ returns: { type: "doc", content: [] },
23
+ privacy: { type: "doc", content: [] },
24
+ cookies: { type: "doc", content: [] },
25
+ },
26
+ inquiriesChannel: "privacidad@example.com",
27
+ cookieNotice: {
28
+ enabled: true,
29
+ message: "Usamos cookies propias y de terceros...",
30
+ buttonLabel: "Entendido",
31
+ },
32
+ };
33
+ ```
34
+
35
+ All documents and the inquiries channel are optional. Missing documents are
36
+ returned as `null`. Documents use Bacano's rich-text JSON format and must be
37
+ rendered safely; do not inject untrusted HTML.
38
+
39
+ The configuration is included in the existing public-config request. Do not
40
+ add a second request, direct GraphQL access, or local fallback copies of legal
41
+ text.
42
+
43
+ ## Informational cookie notice
44
+
45
+ Render the notice only when `config.legal.cookieNotice.enabled` is `true`.
46
+ Display the message and button label supplied by Bacano, allow the visitor to
47
+ close it, and store that dismissal locally if the storefront should avoid
48
+ showing it again in the same browser.
49
+
50
+ The notice is informational. It is not a consent manager and does not gate,
51
+ delay, or reconfigure the GA4, Meta, or Microsoft Clarity runtime. Marketing remains governed by
52
+ `config.marketing`.
53
+
54
+ ## Storefront checklist
55
+
56
+ - Preserve existing public legal routes and their SEO behavior.
57
+ - Source terms, shipping, returns, privacy, cookies, and the contact channel only from
58
+ `config.legal`.
59
+ - Handle every nullable document without crashing or inventing content.
60
+ - Remove only the obsolete legal Content keys; keep unrelated Content data.
61
+ - Do not add direct GraphQL calls or duplicate public-config requests.
62
+ - Verify the notice in enabled, disabled, and dismissed states.
63
+ - Verify GA4, Meta, and Clarity continue operating in every notice state.
64
+ - Do not present the informational notice as consent for Clarity. Review the
65
+ privacy rules in `STOREFRONT_CLARITY.md` before activating it.
66
+ - Run typecheck, lint, unit tests, E2E tests, and the production build.
@@ -0,0 +1,286 @@
1
+ # Marketing y medición en storefronts Bacano
2
+
3
+ Este documento es el contrato canónico para agentes y equipos que construyen un
4
+ storefront con `@bacano/sdk`. Una tienda no debe inventar nombres de eventos,
5
+ identificadores, adaptadores ni llamadas directas a proveedores.
6
+
7
+ ## Regla principal
8
+
9
+ El storefront expresa hechos mediante `useMarketing().track()`. El SDK decide
10
+ si la medición está activa, sanea la URL y adapta el evento a GA4, Meta y
11
+ Microsoft Clarity. Nunca
12
+ se debe llamar directamente a `gtag`, `dataLayer`, `fbq`, Measurement Protocol
13
+ o Conversions API.
14
+ Clarity también se carga desde este runtime; no se instala su snippet de forma
15
+ paralela.
16
+
17
+ ```tsx
18
+ import { useMarketing } from "@bacano/sdk/react";
19
+
20
+ const marketing = useMarketing();
21
+
22
+ marketing.track({
23
+ eventName: "contact",
24
+ contactMethod: "whatsapp",
25
+ contactPlacement: "product_detail",
26
+ });
27
+ ```
28
+
29
+ Un contacto general usa el mismo contrato sin inventar un producto:
30
+
31
+ ```tsx
32
+ marketing.track({
33
+ eventName: "contact",
34
+ contactMethod: "email",
35
+ contactPlacement: "contact_page",
36
+ });
37
+ ```
38
+
39
+ La caída o ausencia de marketing nunca puede bloquear catálogo, carrito,
40
+ checkout, pago, autenticación o navegación. En localhost, preview y orígenes no
41
+ canónicos el runtime es un `no-op` seguro.
42
+
43
+ ## Identidades estables
44
+
45
+ - `WebsiteMarketingItem.itemId` siempre es `product_variant_id` de Bacano.
46
+ - SKU es información complementaria; nunca reemplaza `itemId`.
47
+ - Un evento de producto debe usar la variante realmente visible o elegida.
48
+ - `order_id` es el `transaction_id` de conversiones autoritativas.
49
+ - Los nombres, precios, cantidades y atributos deben corresponder al mismo
50
+ instante y variante.
51
+
52
+ Estas reglas permiten que eventos, feeds de catálogo, públicos y conversiones
53
+ se relacionen sin depender del slug o de la ruta del storefront.
54
+
55
+ ## Cuándo emitir cada evento
56
+
57
+ | Evento | Emitir después de | No emitir por |
58
+ | ------------------- | -------------------------------------- | ---------------------------------- |
59
+ | `page_view` | Navegación resuelta y renderizada | Cada render de React |
60
+ | `search` | Resultados válidos para una búsqueda | Cada tecla |
61
+ | `view_item_list` | Lista visible con items reales | Skeleton o respuesta fallida |
62
+ | `select_item` | Selección que inicia navegación válida | Hover |
63
+ | `view_item` | Producto y variante cargados | Cambio irrelevante de estado |
64
+ | `add_to_cart` | Carrito actualizado | Clic antes de conocer el resultado |
65
+ | `remove_from_cart` | Carrito actualizado | Intento fallido |
66
+ | `view_cart` | Carrito abierto con su contenido | Montaje global de providers |
67
+ | `begin_checkout` | Carrito sincronizado con Bacano | Abrir un modal sin sincronizar |
68
+ | `add_shipping_info` | Entrega elegida o confirmada | Valores predeterminados invisibles |
69
+ | `add_payment_info` | Método elegido o confirmado | Método aún inválido |
70
+ | `contact` | Apertura intencional del canal | Render del botón |
71
+
72
+ `purchase`, `order_placed` y `refund` son eventos autoritativos del backend. El
73
+ storefront nunca los emite desde una página de éxito o respuesta de pago.
74
+
75
+ ## Payload de productos
76
+
77
+ ```tsx
78
+ marketing.track({
79
+ eventName: "add_to_cart",
80
+ value: unitPrice * quantity,
81
+ items: [
82
+ {
83
+ itemId: selectedVariant.id,
84
+ itemName: product.name,
85
+ sku: selectedVariant.sku,
86
+ price: unitPrice,
87
+ quantity,
88
+ itemBrand: product.brand?.name,
89
+ itemCategory: product.category,
90
+ itemVariant: selectedVariant.attributes
91
+ .map((attribute) => attribute.value)
92
+ .join(" / "),
93
+ },
94
+ ],
95
+ });
96
+ ```
97
+
98
+ - `value` representa el valor total de los items del evento.
99
+ - `price` es unitario y `quantity` es positiva.
100
+ - No se deben enviar productos ocultos, precios aproximados ni la primera
101
+ variante por conveniencia si el comprador eligió otra.
102
+ - El storefront no envía PII dentro de eventos de navegador.
103
+
104
+ ## Contactos y WhatsApp
105
+
106
+ Todo enlace de WhatsApp, teléfono o correo debe emitir `contact`. Esto mide la
107
+ apertura del canal; no demuestra que el comprador haya enviado un mensaje o que
108
+ el comercio lo haya atendido.
109
+
110
+ Valores de `contactMethod`:
111
+
112
+ - `whatsapp`
113
+ - `phone`
114
+ - `email`
115
+
116
+ Valores de `contactPlacement`:
117
+
118
+ - `product_detail`
119
+ - `floating`
120
+ - `header`
121
+ - `footer`
122
+ - `contact_page`
123
+ - `checkout`
124
+ - `other` únicamente cuando ninguna ubicación canónica describe el contacto
125
+
126
+ El campo es opcional, no acepta textos visibles ni valores libres y el runtime
127
+ lo ignora cuando el evento no es `contact`. Una ubicación nueva debe añadirse
128
+ primero al contrato oficial del SDK.
129
+
130
+ Un contacto asociado a un producto debe incluir la variante, cantidad y valor:
131
+
132
+ ```tsx
133
+ marketing.track({
134
+ eventName: "contact",
135
+ contactMethod: "whatsapp",
136
+ contactPlacement: "product_detail",
137
+ value: unitPrice * quantity,
138
+ items: [
139
+ {
140
+ itemId: selectedVariant.id,
141
+ itemName: product.name,
142
+ sku: selectedVariant.sku,
143
+ price: unitPrice,
144
+ quantity,
145
+ itemBrand: product.brand?.name,
146
+ itemCategory: product.category,
147
+ itemVariant: variantLabel,
148
+ },
149
+ ],
150
+ });
151
+ ```
152
+
153
+ Un botón flotante o enlace general no debe atribuirse artificialmente a un
154
+ producto. GA4 recibe `method`, `contact_placement`, `items` y `value`; Meta
155
+ recibe el evento estándar `Contact`, `contact_method`, `contact_placement`,
156
+ `content_ids` y `contents` cuando existen items.
157
+
158
+ Clarity registra automáticamente el comportamiento visual de la sesión y
159
+ recibe `contact` como evento personalizado para filtrar grabaciones. No recibe
160
+ PII, items ni etiquetas construidas por el storefront. La guía específica está
161
+ en [`STOREFRONT_CLARITY.md`](./STOREFRONT_CLARITY.md).
162
+
163
+ ## Fuente única y deduplicación
164
+
165
+ - Cada interacción tiene un único propietario de instrumentación.
166
+ - No se combinan handlers locales con seguimiento automático duplicado.
167
+ - `page_view` tiene una sola fuente para navegación inicial y SPA.
168
+ - Un cambio de cantidad puede expresarse como diferencia real de carrito; no se
169
+ vuelve a emitir todo el carrito como nuevas altas.
170
+ - Los efectos de React deben depender de identidades primitivas estables o
171
+ contar con una guarda explícita para evitar duplicados.
172
+
173
+ ## URLs, atribución y privacidad
174
+
175
+ - No enviar URLs construidas manualmente; el runtime usa y sanea la ubicación
176
+ actual cuando `pageLocation` se omite.
177
+ - Nunca incluir email, teléfono, dirección, documento, tokens de pedido,
178
+ autenticación, OAuth o pago en nombres, URLs o parámetros.
179
+ - La atribución se obtiene con `marketing.getAttribution()` y se entrega al
180
+ contrato oficial de carrito/checkout.
181
+ - La autorización publicitaria del checkout es opcional, separada y nunca
182
+ bloquea la compra.
183
+ - Los datos de contacto solo pueden normalizarse y hashearse en el backend bajo
184
+ las reglas de autorización de Bacano.
185
+
186
+ ## Componentes que todo storefront debe auditar
187
+
188
+ - Provider raíz y una única fuente de `page_view`.
189
+ - Buscador, resultados, categorías, colecciones y tarjetas de producto.
190
+ - Detalle, selección de variante y recomendaciones si están dentro del alcance.
191
+ - Carrito: alta, baja, cantidad y vista.
192
+ - Checkout: inicio, entrega, pago y consentimiento.
193
+ - Todos los enlaces de WhatsApp, teléfono y correo.
194
+ - Preview: marketing desactivado en todo su árbol.
195
+
196
+ ## Content Security Policy
197
+
198
+ La CSP pertenece al storefront y se genera durante el build o en el hosting.
199
+ El runtime no puede ampliar una cabecera que el navegador ya recibió. Se deben
200
+ incluir únicamente los proveedores activos del website y conservar los demás
201
+ orígenes que realmente use la aplicación.
202
+
203
+ Para GA4 y destinos de Google Ads vinculados al mismo Google tag:
204
+
205
+ ```text
206
+ script-src https://www.googletagmanager.com
207
+ connect-src https://www.googletagmanager.com
208
+ https://www.google-analytics.com
209
+ https://*.google-analytics.com
210
+ https://analytics.google.com
211
+ https://*.analytics.google.com
212
+ https://stats.g.doubleclick.net
213
+ https://googleads.g.doubleclick.net
214
+ https://www.googleadservices.com
215
+ img-src https://www.google-analytics.com
216
+ https://googleads.g.doubleclick.net
217
+ https://www.googleadservices.com
218
+ ```
219
+
220
+ `https://www.googletagmanager.com` debe estar tanto en `script-src` como en
221
+ `connect-src`: Google tag puede usar `/td` mediante `fetch`, aunque el script
222
+ principal ya haya cargado correctamente.
223
+
224
+ Para Meta Pixel:
225
+
226
+ ```text
227
+ script-src https://connect.facebook.net
228
+ connect-src https://www.facebook.com https://graph.facebook.com
229
+ img-src https://www.facebook.com
230
+ frame-src https://www.facebook.com
231
+ form-action 'self' https://www.facebook.com
232
+ ```
233
+
234
+ Meta puede enviar `/tr/` mediante un formulario POST dirigido a un iframe
235
+ oculto. Permitir `connect-src` o `img-src` no sustituye `form-action` y
236
+ `frame-src`. Usar el origen exacto; no abrir `*.facebook.com` ni comodines de
237
+ infraestructura para silenciar errores.
238
+
239
+ Para Microsoft Clarity:
240
+
241
+ ```text
242
+ script-src https://*.clarity.ms
243
+ connect-src https://*.clarity.ms https://c.bing.com
244
+ img-src https://*.clarity.ms https://c.bing.com
245
+ ```
246
+
247
+ Clerk, Wompi, videos y cualquier otro proveedor conservan su propia lista de
248
+ orígenes. La CSP final es la unión mínima de las capacidades efectivamente
249
+ montadas. Probarla en consola, Network y, cuando sea posible, primero mediante
250
+ `Content-Security-Policy-Report-Only`.
251
+
252
+ ## Validación obligatoria
253
+
254
+ Antes de declarar un storefront listo:
255
+
256
+ 1. Ejecutar typecheck, lint, unit tests y build.
257
+ 2. Probar que acciones fallidas no emitan eventos exitosos.
258
+ 3. Confirmar que una acción produzca un solo evento.
259
+ 4. Verificar IDs, variantes, cantidades, valores y moneda.
260
+ 5. Revisar URLs para confirmar que no contienen tokens ni PII.
261
+ 6. Validar GA4 en DebugView o tiempo real.
262
+ 7. Validar Meta en Test Events y revisar `content_ids`.
263
+ 8. Confirmar que preview y localhost no envíen eventos reales.
264
+ 9. Si existe catálogo publicitario, confirmar que cada `itemId` coincide con el
265
+ `id` de la misma variante en la fuente de Meta o Google.
266
+ 10. Si Clarity está activo, validar tag, recolección, sesiones y eventos
267
+ personalizados en el dominio canónico.
268
+ 11. Confirmar que la CSP no bloquea los transportes reales de GA4, Meta o
269
+ Clarity, incluidos Google tag `/td` y Meta `/tr/` por POST/iframe.
270
+
271
+ ## Checklist corto para agentes
272
+
273
+ - [ ] Leí este contrato antes de instrumentar.
274
+ - [ ] Uso únicamente `useMarketing().track()`.
275
+ - [ ] Uso `product_variant_id` como `itemId`.
276
+ - [ ] Emito eventos después del éxito, no antes.
277
+ - [ ] Instrumenté todos los contactos con método y ubicación.
278
+ - [ ] Los contactos de producto incluyen item, cantidad y valor.
279
+ - [ ] No emito conversiones autoritativas desde el navegador.
280
+ - [ ] No envío PII, tokens o secretos.
281
+ - [ ] No duplico `page_view` ni eventos por renders.
282
+ - [ ] Validé GA4, Meta, Clarity, preview y degradación segura.
283
+
284
+ La guía de gobierno, persistencia y eventos autoritativos vive en
285
+ `docs/websites/MARKETING_MEASUREMENT_STOREFRONTS_CHECKLIST.md` dentro del
286
+ repositorio Bacano. Este archivo es el contrato distribuible para storefronts.