@bacano/sdk 1.1.19 → 1.2.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/STOREFRONT_FORMS.md +58 -0
- package/STOREFRONT_MARKETING.md +103 -4
- package/index.cjs +253 -84
- package/index.js +253 -84
- package/marketing/browser-adapters.d.ts +30 -0
- package/marketing/clarity.d.ts +1 -1
- package/marketing/form-interactions.d.ts +12 -0
- package/marketing/index.d.ts +2 -0
- package/marketing/interactions.d.ts +19 -0
- package/marketing/types.d.ts +5 -2
- package/package.json +1 -1
- package/react/cta-tracking.d.ts +5 -0
- package/react/form-sections.d.ts +16 -0
- package/react/form-tracking.d.ts +11 -0
- package/react/forms.cjs +240 -94
- package/react/forms.d.ts +10 -2
- package/react/forms.js +233 -87
- package/react/marketing-provider.d.ts +2 -1
- package/react.cjs +342 -134
- package/react.d.ts +1 -0
- package/react.js +325 -117
- package/skills/bacano-web/SKILL.md +5 -0
- package/skills/bacano-web/references/forms.md +5 -0
- package/skills/bacano-web/references/interactions.md +147 -0
- package/skills/bacano-web/references/report.md +5 -0
package/STOREFRONT_FORMS.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Formularios de websites
|
|
2
2
|
|
|
3
|
+
Desde SDK 1.2.0, `BacanoForm` instrumenta interacciones mediante el provider
|
|
4
|
+
global de marketing. Para modales montados ocultos, pasar `visible={open}`.
|
|
5
|
+
Renderers personalizados usan `useBacanoFormTracking`. Leer la sección de
|
|
6
|
+
interacciones en [STOREFRONT_MARKETING.md](./STOREFRONT_MARKETING.md) y los
|
|
7
|
+
[ejemplos de migración](./skills/bacano-web/references/interactions.md).
|
|
8
|
+
No añadir un segundo listener ni convertir aperturas en leads.
|
|
9
|
+
|
|
3
10
|
Los formularios públicos de Bacano son definiciones tipadas y versionadas. El
|
|
4
11
|
storefront decide dónde se muestran y cómo lucen; Bacano controla su estructura,
|
|
5
12
|
estado y validaciones. La definición nunca contiene HTML ni JavaScript arbitrario.
|
|
@@ -35,6 +42,57 @@ idempotente estable y llama `client.forms.submitResponse(...)`. Un formulario
|
|
|
35
42
|
pausado muestra su mensaje de indisponibilidad y deja de operar sin reconstruir
|
|
36
43
|
el storefront.
|
|
37
44
|
|
|
45
|
+
## Preseleccionar desde un botón externo
|
|
46
|
+
|
|
47
|
+
Usa `values` y `onValuesChange` juntos para controlar el estado. El botón mezcla
|
|
48
|
+
solo el campo que cambia: no reemplaces todo el objeto ni desmontes el formulario
|
|
49
|
+
para preseleccionar. El visitante puede seguir editando cualquier campo.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { useRef, useState } from "react";
|
|
53
|
+
import { BacanoForm, type BacanoFormValues } from "@bacano/sdk/react/forms";
|
|
54
|
+
|
|
55
|
+
export function ProjectInquiry() {
|
|
56
|
+
const [values, setValues] = useState<BacanoFormValues>({});
|
|
57
|
+
const sectionRef = useRef<HTMLElement>(null);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={() => {
|
|
64
|
+
setValues((current) => ({ ...current, producto: "gaia" }));
|
|
65
|
+
sectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
Me interesa Gaia
|
|
69
|
+
</button>
|
|
70
|
+
<section ref={sectionRef}>
|
|
71
|
+
<BacanoForm
|
|
72
|
+
formKey="contacto"
|
|
73
|
+
values={values}
|
|
74
|
+
onValuesChange={setValues}
|
|
75
|
+
/>
|
|
76
|
+
</section>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `producto` debe ser la key real del campo publicado y `gaia` el `value`
|
|
83
|
+
real de la opción, no su label. Usa `string[]` para `multi_select` y booleanos
|
|
84
|
+
para `checkbox`. Las validaciones publicadas siguen aplicándose.
|
|
85
|
+
- Puedes inicializar `useState` con la preselección, incluso antes de cargar
|
|
86
|
+
la definición. Un cambio externo no reinicia los pasos ni los otros campos.
|
|
87
|
+
- `onValuesChange` entrega el objeto completo tras cada edición del visitante;
|
|
88
|
+
los cambios externos se hacen sobre el estado del componente padre.
|
|
89
|
+
- Las keys ajenas a la definición publicada no se envían. Si compartes un
|
|
90
|
+
componente entre distintos sitios o formularios, reinicia su estado al cambiar
|
|
91
|
+
de identidad; no reutilices los datos de un visitante entre formularios.
|
|
92
|
+
- Sin `onSubmit` se mantiene el envío oficial, incluyendo la selección. No
|
|
93
|
+
necesitas otro endpoint, eventos DOM ni un formulario paralelo.
|
|
94
|
+
- Omitir ambas props conserva el funcionamiento anterior, con estado interno.
|
|
95
|
+
|
|
38
96
|
## Estructura del formulario
|
|
39
97
|
|
|
40
98
|
La definición tiene tres niveles explícitos:
|
package/STOREFRONT_MARKETING.md
CHANGED
|
@@ -120,8 +120,6 @@ Valores de `contactPlacement`:
|
|
|
120
120
|
- `header`
|
|
121
121
|
- `footer`
|
|
122
122
|
- `contact_page`
|
|
123
|
-
- `checkout`
|
|
124
|
-
- `other` únicamente cuando ninguna ubicación canónica describe el contacto
|
|
125
123
|
|
|
126
124
|
El campo es opcional, no acepta textos visibles ni valores libres y el runtime
|
|
127
125
|
lo ignora cuando el evento no es `contact`. Una ubicación nueva debe añadirse
|
|
@@ -276,9 +274,9 @@ Antes de declarar un storefront listo:
|
|
|
276
274
|
## Checklist corto para agentes
|
|
277
275
|
|
|
278
276
|
- [ ] Leí este contrato antes de instrumentar.
|
|
279
|
-
- [ ] Uso
|
|
277
|
+
- [ ] Uso el runtime oficial mediante `track`, `useBacanoCta` y los formularios oficiales.
|
|
280
278
|
- [ ] Uso `product_variant_id` como `itemId`.
|
|
281
|
-
- [ ]
|
|
279
|
+
- [ ] Cada evento representa su hecho: un intento no se presenta como éxito.
|
|
282
280
|
- [ ] Instrumenté todos los contactos con método y ubicación.
|
|
283
281
|
- [ ] Los contactos de producto incluyen item, cantidad y valor.
|
|
284
282
|
- [ ] No emito conversiones autoritativas desde el navegador.
|
|
@@ -289,3 +287,104 @@ Antes de declarar un storefront listo:
|
|
|
289
287
|
La guía de gobierno, persistencia y eventos autoritativos vive en
|
|
290
288
|
`docs/websites/MARKETING_MEASUREMENT_STOREFRONTS_CHECKLIST.md` dentro del
|
|
291
289
|
repositorio Bacano. Este archivo es el contrato distribuible para storefronts.
|
|
290
|
+
|
|
291
|
+
## Interacciones y formularios — SDK 1.2.0
|
|
292
|
+
|
|
293
|
+
Aplicable también a webs informativas y de servicios. No exige checkout ni
|
|
294
|
+
configurar un proveedor ausente. Esta versión agrega instrumentación; no crea
|
|
295
|
+
un dashboard de embudos dentro de Bacano ni publica configuraciones remotas.
|
|
296
|
+
|
|
297
|
+
| Evento Bacano | GA4 | Meta | Clarity | Propietario y condición |
|
|
298
|
+
| --- | --- | --- | --- | --- |
|
|
299
|
+
| `cta_click` | `cta_click` | personalizado `cta_click` | `cta_click` | CTA pulsado, no hover |
|
|
300
|
+
| `form_open` | `form_open` | personalizado `form_open` | `form_open` | Formulario pasa a estar presentado; no implica inicio |
|
|
301
|
+
| `form_start` | `form_start` | personalizado `form_start` | `form_start` | Primera respuesta introducida/seleccionada por la persona |
|
|
302
|
+
| `form_step_complete` | `form_step_complete` | personalizado `form_step_complete` | `form_step_complete` | Paso validado y avance, una vez por paso/diligenciamiento |
|
|
303
|
+
| `form_submit_attempt` | `form_submit_attempt` | personalizado `form_submit_attempt` | `form_submit_attempt` | Intento de envío final, incluso si la validación lo rechaza |
|
|
304
|
+
| `form_submit_error` | `form_submit_error` | personalizado `form_submit_error` | `form_submit_error` | Fallo de ese intento; categoría segura |
|
|
305
|
+
| `form_submit_success` | `form_submit_success` | personalizado `form_submit_success` | `form_submit_success` | Resultado oficial `accepted: true` y UUID de respuesta |
|
|
306
|
+
|
|
307
|
+
GA4 y Meta reciben solamente `form_key`, `cta_id`, `placement`, `step_key`
|
|
308
|
+
cuando corresponden y `error_category` para errores. Los identificadores son
|
|
309
|
+
técnicos, estables, 1–64 caracteres, patrón `[a-z][a-z0-9_-]*`; no labels,
|
|
310
|
+
respuestas, emails ni teléfonos. `cta_click` exige `ctaId`; los eventos de
|
|
311
|
+
formulario exigen `formKey`; completar un paso exige `stepKey`. Categorías:
|
|
312
|
+
`validation`, `network`, `rejected`, `unavailable`. Parámetros inválidos
|
|
313
|
+
rechazan el evento completo. Clarity recibe solo el nombre, no esos parámetros.
|
|
314
|
+
GA4 recibe además URL saneada, `event_id`, destino explícito y debug según
|
|
315
|
+
configuración. Meta recibe la misma identidad mediante `eventID`, dirigida
|
|
316
|
+
solo al píxel configurado, no a todos los píxeles de la página.
|
|
317
|
+
|
|
318
|
+
`BacanoForm` instrumenta apertura, primera edición, pasos, intento y resultado.
|
|
319
|
+
Un formulario montado dentro de un contenedor oculto necesita `visible={open}`;
|
|
320
|
+
no se intenta adivinar CSS o extraer textos del DOM. Siempre visible significa
|
|
321
|
+
presentado en la página, no una impresión garantizada dentro del viewport.
|
|
322
|
+
Los cambios programáticos de `values` no cuentan como primera edición.
|
|
323
|
+
Un renderer personalizado usa `useBacanoFormTracking` y conserva su propia UI.
|
|
324
|
+
Los CTA usan `useBacanoCta`; no instrumentar el mismo clic también con `track`.
|
|
325
|
+
Ver [ejemplos y migración](./skills/bacano-web/references/interactions.md).
|
|
326
|
+
|
|
327
|
+
### Persistencia, antispam y conversiones
|
|
328
|
+
|
|
329
|
+
`accepted: true, responseId: null` conserva el mensaje genérico de aceptación,
|
|
330
|
+
pero **no** emite éxito. No revelar el motivo al visitante. Un `onSubmit`
|
|
331
|
+
personalizado que devuelve `void` conserva su comportamiento visual, pero no
|
|
332
|
+
demuestra persistencia para medición; devolver el resultado de
|
|
333
|
+
`client.forms.submitResponse()` cuando se usa ese flujo.
|
|
334
|
+
|
|
335
|
+
Una respuesta guardada no prueba que se haya creado un lead nuevo. Esta
|
|
336
|
+
integración **no emite `generate_lead` ni `Lead`**, tampoco para `CAPTURE_LEAD`:
|
|
337
|
+
la resolución de identidad pertenece al backend y su resultado no forma parte
|
|
338
|
+
del contrato público. Una futura conversión autoritativa debe definirse ahí,
|
|
339
|
+
con identidad compartida si se envía también por navegador; no inferirla de
|
|
340
|
+
este evento de interacción. No duplicar la captura del lead por campaña.
|
|
341
|
+
|
|
342
|
+
### Deduplicación y atribución
|
|
343
|
+
|
|
344
|
+
- El runtime descarta repeticiones de `(eventName, eventId)` entre sus últimas
|
|
345
|
+
1.000 identidades. No es una garantía entre recargas ni un acuse del proveedor.
|
|
346
|
+
- CTA: el helper agrupa doble clic del mismo botón dentro de 400 ms, sin
|
|
347
|
+
impedir su acción. Un clic posterior es legítimo y recibe otra identidad.
|
|
348
|
+
- Cerrar/reabrir genera otra apertura; no otro inicio mientras el formulario
|
|
349
|
+
permanezca montado. Desmontar/montar inicia otro diligenciamiento. Un render
|
|
350
|
+
o la repetición de efectos de Strict Mode no equivalen a reabrir.
|
|
351
|
+
- Un envío pendiente no acepta otro simultáneo. Un reintento tras error genera
|
|
352
|
+
otro intento analítico, pero conserva la `idempotencyKey` de persistencia.
|
|
353
|
+
Éxito usa el UUID de respuesta para no duplicarlo dentro del runtime.
|
|
354
|
+
- Se reutiliza la atribución existente: `utm_content` se conserva como
|
|
355
|
+
`content`, junto a las demás UTM. Cada interacción recaptura la ubicación
|
|
356
|
+
usando ese mismo almacenamiento; navegación sin UTM no borra el origen.
|
|
357
|
+
No se copian click IDs de un proveedor a parámetros personalizados de otro.
|
|
358
|
+
- Las respuestas conservan atribución en Bacano; CTA/aperturas se envían a los
|
|
359
|
+
proveedores, no se guardan como una secuencia propia en Bacano. No prometer
|
|
360
|
+
seguimiento completo entre dispositivos, navegadores o bloqueadores.
|
|
361
|
+
|
|
362
|
+
GA4 puede medir formularios automáticamente. Al adoptar este contrato, revisar
|
|
363
|
+
y desactivar **Form interactions** de Enhanced Measurement para que `form_start`
|
|
364
|
+
no tenga dos propietarios. El SDK no cambia esa configuración de la cuenta.
|
|
365
|
+
No tratar el `form_submit` automático como evidencia de guardado.
|
|
366
|
+
[Referencia oficial de GA4](https://support.google.com/analytics/answer/9216061?hl=en).
|
|
367
|
+
|
|
368
|
+
### Privacidad y diagnóstico
|
|
369
|
+
|
|
370
|
+
El único `BacanoMarketingProvider` acepta `measurementAllowed={false}` cuando
|
|
371
|
+
el gestor de consentimiento del sitio todavía no permite medir. No carga
|
|
372
|
+
scripts, no captura atribución nueva ni emite eventos en ese estado. Este
|
|
373
|
+
interruptor no sustituye el gestor de consentimiento, no borra almacenamiento
|
|
374
|
+
previo ni descarga scripts de terceros que ya estaban cargados. Tampoco
|
|
375
|
+
autoriza publicidad por el hecho de aceptar términos de compra.
|
|
376
|
+
|
|
377
|
+
Config desactivada, dominio distinto, preview o proveedores ausentes son no-op.
|
|
378
|
+
Diagnósticos: `MARKETING_DISABLED`, `MEASUREMENT_NOT_ALLOWED`,
|
|
379
|
+
`DOMAIN_NOT_ALLOWED`, `PROVIDER_NOT_CONFIGURED`, `PREVIEW_DISABLED`,
|
|
380
|
+
`PUBLIC_CONFIG_UNAVAILABLE`, `INVALID_INTERACTION`, `INVALID_EVENT`,
|
|
381
|
+
`SCRIPT_LOAD_FAILED`, `EVENT_DISPATCH_FAILED`, `EVENT_DISPATCHED`.
|
|
382
|
+
Un fallo de descarga no permite distinguir por sí solo bloqueador, CSP o red.
|
|
383
|
+
`EVENT_DISPATCHED` significa únicamente pasado al adaptador; no recepción
|
|
384
|
+
externa. Ni un callback de diagnóstico que falle puede bloquear el formulario.
|
|
385
|
+
|
|
386
|
+
No incluir PII en rutas ni en valores UTM: sanear una URL no identifica toda la
|
|
387
|
+
información personal que el sitio pudiera introducir en su ruta. No evadir
|
|
388
|
+
Core Setup/Protected Data Mode o consentimiento mediante campos alternativos.
|
|
389
|
+
La coherencia entre píxel y CAPI se revisa del lado Bacano; no exponer tokens
|
|
390
|
+
ni inventar un segundo destino público para intentar corregirla en el sitio.
|
package/index.cjs
CHANGED
|
@@ -2957,6 +2957,9 @@ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
|
|
|
2957
2957
|
function sanitizeMarketingUrl(input, baseUrl) {
|
|
2958
2958
|
try {
|
|
2959
2959
|
const url = new URL(input, baseUrl);
|
|
2960
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
|
|
2961
|
+
url.username = "";
|
|
2962
|
+
url.password = "";
|
|
2960
2963
|
const safeParameters = new URLSearchParams();
|
|
2961
2964
|
for (const [key, value] of url.searchParams.entries()) {
|
|
2962
2965
|
if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
|
|
@@ -5732,50 +5735,65 @@ function createContentModule(graphqlUrl, getHeaders, getContext, storageUrl) {
|
|
|
5732
5735
|
};
|
|
5733
5736
|
}
|
|
5734
5737
|
|
|
5735
|
-
// libs/sdk/src/marketing/
|
|
5736
|
-
var
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5738
|
+
// libs/sdk/src/marketing/interactions.ts
|
|
5739
|
+
var WEBSITE_INTERACTION_EVENTS = [
|
|
5740
|
+
"cta_click",
|
|
5741
|
+
"form_open",
|
|
5742
|
+
"form_start",
|
|
5743
|
+
"form_step_complete",
|
|
5744
|
+
"form_submit_attempt",
|
|
5745
|
+
"form_submit_error",
|
|
5746
|
+
"form_submit_success"
|
|
5747
|
+
];
|
|
5748
|
+
function isInteractionEvent(name) {
|
|
5749
|
+
return WEBSITE_INTERACTION_EVENTS.some((candidate) => candidate === name);
|
|
5750
|
+
}
|
|
5751
|
+
function isInteractionIdentifier(value) {
|
|
5752
|
+
return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value);
|
|
5753
|
+
}
|
|
5754
|
+
function isBrowserMarketingEvent(name) {
|
|
5755
|
+
return isInteractionEvent(name) || [
|
|
5756
|
+
"page_view",
|
|
5757
|
+
"search",
|
|
5758
|
+
"view_item_list",
|
|
5759
|
+
"select_item",
|
|
5760
|
+
"view_item",
|
|
5761
|
+
"add_to_cart",
|
|
5762
|
+
"remove_from_cart",
|
|
5763
|
+
"view_cart",
|
|
5764
|
+
"begin_checkout",
|
|
5765
|
+
"add_shipping_info",
|
|
5766
|
+
"add_payment_info",
|
|
5767
|
+
"contact"
|
|
5768
|
+
].includes(name);
|
|
5769
|
+
}
|
|
5770
|
+
function interactionParameters(event) {
|
|
5771
|
+
if (!isInteractionEvent(event.eventName)) return {};
|
|
5772
|
+
if (event.eventName === "cta_click" ? !isInteractionIdentifier(event.ctaId) : !isInteractionIdentifier(event.formKey))
|
|
5773
|
+
return null;
|
|
5774
|
+
if (event.eventName === "form_step_complete" && !isInteractionIdentifier(event.stepKey))
|
|
5775
|
+
return null;
|
|
5776
|
+
if (event.eventName === "form_submit_error" && !["validation", "network", "rejected", "unavailable"].includes(
|
|
5777
|
+
event.errorCategory ?? ""
|
|
5778
|
+
))
|
|
5779
|
+
return null;
|
|
5780
|
+
const params = {};
|
|
5781
|
+
for (const [key, value] of Object.entries({
|
|
5782
|
+
form_key: event.formKey,
|
|
5783
|
+
cta_id: event.ctaId,
|
|
5784
|
+
placement: event.placement,
|
|
5785
|
+
step_key: event.stepKey
|
|
5786
|
+
})) {
|
|
5787
|
+
if (value === void 0) continue;
|
|
5788
|
+
if (!isInteractionIdentifier(value)) return null;
|
|
5789
|
+
params[key] = value;
|
|
5749
5790
|
}
|
|
5750
|
-
|
|
5791
|
+
if (event.eventName === "form_submit_error" && event.errorCategory)
|
|
5792
|
+
params.error_category = event.errorCategory;
|
|
5793
|
+
return params;
|
|
5751
5794
|
}
|
|
5752
|
-
function
|
|
5753
|
-
|
|
5754
|
-
const id = `bacano-clarity-${projectId}`;
|
|
5755
|
-
if (document.getElementById(id)) return;
|
|
5756
|
-
const script = document.createElement("script");
|
|
5757
|
-
script.id = id;
|
|
5758
|
-
script.async = true;
|
|
5759
|
-
script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
|
|
5760
|
-
script.referrerPolicy = "strict-origin-when-cross-origin";
|
|
5761
|
-
document.head.appendChild(script);
|
|
5762
|
-
}
|
|
5763
|
-
function sendClarityEvent(eventName) {
|
|
5764
|
-
clarityWindow()?.clarity?.("event", eventName);
|
|
5765
|
-
}
|
|
5766
|
-
|
|
5767
|
-
// libs/sdk/src/marketing/domain.ts
|
|
5768
|
-
function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
|
|
5769
|
-
if (canonicalDomain === void 0) return true;
|
|
5770
|
-
if (!canonicalDomain?.trim()) return false;
|
|
5771
|
-
try {
|
|
5772
|
-
const configuredHost = new URL(
|
|
5773
|
-
canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
|
|
5774
|
-
).hostname.toLowerCase();
|
|
5775
|
-
return currentHostname.toLowerCase() === configuredHost;
|
|
5776
|
-
} catch {
|
|
5777
|
-
return false;
|
|
5778
|
-
}
|
|
5795
|
+
function newMarketingEventId() {
|
|
5796
|
+
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
5779
5797
|
}
|
|
5780
5798
|
|
|
5781
5799
|
// libs/sdk/src/marketing/ga4.ts
|
|
@@ -5803,19 +5821,20 @@ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
|
|
|
5803
5821
|
"contact_page"
|
|
5804
5822
|
];
|
|
5805
5823
|
|
|
5806
|
-
// libs/sdk/src/marketing/
|
|
5824
|
+
// libs/sdk/src/marketing/browser-adapters.ts
|
|
5807
5825
|
var initializedGa4Destinations = /* @__PURE__ */ new Set();
|
|
5808
5826
|
var initializedMetaDestinations = /* @__PURE__ */ new Set();
|
|
5809
5827
|
function marketingWindow() {
|
|
5810
5828
|
return typeof window === "undefined" ? null : window;
|
|
5811
5829
|
}
|
|
5812
|
-
function loadScriptOnce(id, source) {
|
|
5830
|
+
function loadScriptOnce(id, source, onError) {
|
|
5813
5831
|
if (typeof document === "undefined" || document.getElementById(id)) return;
|
|
5814
5832
|
const script = document.createElement("script");
|
|
5815
5833
|
script.id = id;
|
|
5816
5834
|
script.async = true;
|
|
5817
5835
|
script.src = source;
|
|
5818
5836
|
script.referrerPolicy = "strict-origin-when-cross-origin";
|
|
5837
|
+
script.onerror = () => onError?.();
|
|
5819
5838
|
document.head.appendChild(script);
|
|
5820
5839
|
}
|
|
5821
5840
|
function prepareGa4(measurementId) {
|
|
@@ -5830,10 +5849,11 @@ function prepareGa4(measurementId) {
|
|
|
5830
5849
|
target.gtag("config", measurementId, { send_page_view: false });
|
|
5831
5850
|
initializedGa4Destinations.add(measurementId);
|
|
5832
5851
|
}
|
|
5833
|
-
function loadGa4Script(measurementId) {
|
|
5852
|
+
function loadGa4Script(measurementId, onError) {
|
|
5834
5853
|
loadScriptOnce(
|
|
5835
5854
|
`bacano-ga4-${measurementId}`,
|
|
5836
|
-
`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}
|
|
5855
|
+
`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`,
|
|
5856
|
+
onError
|
|
5837
5857
|
);
|
|
5838
5858
|
}
|
|
5839
5859
|
function captureGa4ClientId(measurementId, websiteId, diagnostic) {
|
|
@@ -5881,10 +5901,11 @@ function prepareMeta(pixelId) {
|
|
|
5881
5901
|
target.fbq("init", pixelId);
|
|
5882
5902
|
initializedMetaDestinations.add(pixelId);
|
|
5883
5903
|
}
|
|
5884
|
-
function loadMetaScript() {
|
|
5904
|
+
function loadMetaScript(onError) {
|
|
5885
5905
|
loadScriptOnce(
|
|
5886
5906
|
"bacano-meta-pixel",
|
|
5887
|
-
"https://connect.facebook.net/en_US/fbevents.js"
|
|
5907
|
+
"https://connect.facebook.net/en_US/fbevents.js",
|
|
5908
|
+
onError
|
|
5888
5909
|
);
|
|
5889
5910
|
}
|
|
5890
5911
|
function scheduleScriptLoad(load, strategy) {
|
|
@@ -5948,10 +5969,12 @@ function contactPlacementFor(event) {
|
|
|
5948
5969
|
}
|
|
5949
5970
|
return placement;
|
|
5950
5971
|
}
|
|
5951
|
-
function sendGa4(event, diagnosticMode) {
|
|
5972
|
+
function sendGa4(event, diagnosticMode, measurementId) {
|
|
5952
5973
|
const target = marketingWindow();
|
|
5953
5974
|
const contactPlacement = contactPlacementFor(event);
|
|
5954
5975
|
target?.gtag?.("event", event.eventName, {
|
|
5976
|
+
send_to: measurementId,
|
|
5977
|
+
...interactionParameters(event),
|
|
5955
5978
|
currency: event.currency,
|
|
5956
5979
|
value: event.value,
|
|
5957
5980
|
search_term: event.searchTerm,
|
|
@@ -5968,15 +5991,17 @@ function sendGa4(event, diagnosticMode) {
|
|
|
5968
5991
|
debug_mode: diagnosticMode ? true : void 0
|
|
5969
5992
|
});
|
|
5970
5993
|
}
|
|
5971
|
-
function sendMeta(event) {
|
|
5994
|
+
function sendMeta(event, pixelId) {
|
|
5972
5995
|
const target = marketingWindow();
|
|
5973
5996
|
const eventName = metaEventName(event.eventName);
|
|
5974
5997
|
const isStandard = eventName !== event.eventName;
|
|
5975
5998
|
const contactPlacement = contactPlacementFor(event);
|
|
5976
5999
|
target?.fbq?.(
|
|
5977
|
-
isStandard ? "
|
|
6000
|
+
isStandard ? "trackSingle" : "trackSingleCustom",
|
|
6001
|
+
pixelId,
|
|
5978
6002
|
eventName,
|
|
5979
6003
|
{
|
|
6004
|
+
...interactionParameters(event),
|
|
5980
6005
|
currency: event.currency,
|
|
5981
6006
|
value: event.value,
|
|
5982
6007
|
search_string: event.searchTerm,
|
|
@@ -5993,14 +6018,69 @@ function sendMeta(event) {
|
|
|
5993
6018
|
{ eventID: event.eventId }
|
|
5994
6019
|
);
|
|
5995
6020
|
}
|
|
5996
|
-
|
|
5997
|
-
|
|
6021
|
+
|
|
6022
|
+
// libs/sdk/src/marketing/clarity.ts
|
|
6023
|
+
var initializedDestinations = /* @__PURE__ */ new Set();
|
|
6024
|
+
function clarityWindow() {
|
|
6025
|
+
return typeof window === "undefined" ? null : window;
|
|
5998
6026
|
}
|
|
6027
|
+
function prepareClarity(projectId) {
|
|
6028
|
+
const target = clarityWindow();
|
|
6029
|
+
if (!target || initializedDestinations.has(projectId)) return;
|
|
6030
|
+
if (!target.clarity) {
|
|
6031
|
+
const clarity = function() {
|
|
6032
|
+
clarity.q ??= [];
|
|
6033
|
+
clarity.q.push(arguments);
|
|
6034
|
+
};
|
|
6035
|
+
target.clarity = clarity;
|
|
6036
|
+
}
|
|
6037
|
+
initializedDestinations.add(projectId);
|
|
6038
|
+
}
|
|
6039
|
+
function loadClarityScript(projectId, onError) {
|
|
6040
|
+
if (typeof document === "undefined") return;
|
|
6041
|
+
const id = `bacano-clarity-${projectId}`;
|
|
6042
|
+
if (document.getElementById(id)) return;
|
|
6043
|
+
const script = document.createElement("script");
|
|
6044
|
+
script.id = id;
|
|
6045
|
+
script.async = true;
|
|
6046
|
+
script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
|
|
6047
|
+
script.referrerPolicy = "strict-origin-when-cross-origin";
|
|
6048
|
+
script.onerror = () => onError?.();
|
|
6049
|
+
document.head.appendChild(script);
|
|
6050
|
+
}
|
|
6051
|
+
function sendClarityEvent(eventName) {
|
|
6052
|
+
clarityWindow()?.clarity?.("event", eventName);
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
// libs/sdk/src/marketing/domain.ts
|
|
6056
|
+
function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
|
|
6057
|
+
if (canonicalDomain === void 0) return true;
|
|
6058
|
+
if (!canonicalDomain?.trim()) return false;
|
|
6059
|
+
try {
|
|
6060
|
+
const configuredHost = new URL(
|
|
6061
|
+
canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
|
|
6062
|
+
).hostname.toLowerCase();
|
|
6063
|
+
return currentHostname.toLowerCase() === configuredHost;
|
|
6064
|
+
} catch {
|
|
6065
|
+
return false;
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
|
|
6069
|
+
// libs/sdk/src/marketing/runtime.ts
|
|
5999
6070
|
function createWebsiteMarketingRuntime(options) {
|
|
6000
6071
|
let destroyed = false;
|
|
6072
|
+
const dispatched = /* @__PURE__ */ new Set();
|
|
6073
|
+
const observer = options.diagnostic;
|
|
6074
|
+
const diagnostic = (value) => {
|
|
6075
|
+
try {
|
|
6076
|
+
observer?.(value);
|
|
6077
|
+
} catch {
|
|
6078
|
+
}
|
|
6079
|
+
};
|
|
6080
|
+
options = { ...options, diagnostic };
|
|
6001
6081
|
let cancelScheduledLoad = () => void 0;
|
|
6002
6082
|
const target = marketingWindow();
|
|
6003
|
-
const enabled = options.config.enabled && Boolean(
|
|
6083
|
+
const enabled = options.config.enabled && options.measurementAllowed !== false && Boolean(
|
|
6004
6084
|
target && matchesCanonicalMarketingHost(
|
|
6005
6085
|
options.canonicalDomain,
|
|
6006
6086
|
target.location.hostname
|
|
@@ -6008,25 +6088,59 @@ function createWebsiteMarketingRuntime(options) {
|
|
|
6008
6088
|
) && Boolean(
|
|
6009
6089
|
options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
|
|
6010
6090
|
);
|
|
6091
|
+
if (!enabled)
|
|
6092
|
+
diagnostic({
|
|
6093
|
+
level: "debug",
|
|
6094
|
+
code: !options.config.enabled ? "MARKETING_DISABLED" : options.measurementAllowed === false ? "MEASUREMENT_NOT_ALLOWED" : !target ? "SERVER_RENDER" : !matchesCanonicalMarketingHost(
|
|
6095
|
+
options.canonicalDomain,
|
|
6096
|
+
target.location.hostname
|
|
6097
|
+
) ? "DOMAIN_NOT_ALLOWED" : "PROVIDER_NOT_CONFIGURED",
|
|
6098
|
+
message: "Measurement is inactive; no events are emitted"
|
|
6099
|
+
});
|
|
6011
6100
|
if (enabled) {
|
|
6101
|
+
const guard = (provider, action) => {
|
|
6102
|
+
try {
|
|
6103
|
+
action();
|
|
6104
|
+
} catch {
|
|
6105
|
+
diagnostic({
|
|
6106
|
+
level: "warning",
|
|
6107
|
+
code: "RUNTIME_INITIALIZATION_FAILED",
|
|
6108
|
+
message: `${provider}: initialization failed`
|
|
6109
|
+
});
|
|
6110
|
+
}
|
|
6111
|
+
};
|
|
6112
|
+
const scriptError = (provider) => () => diagnostic({
|
|
6113
|
+
level: "warning",
|
|
6114
|
+
code: "SCRIPT_LOAD_FAILED",
|
|
6115
|
+
message: `${provider}: script could not load (network, CSP or blocker); external reception is unverified`
|
|
6116
|
+
});
|
|
6012
6117
|
try {
|
|
6013
6118
|
if (options.config.destinations.ga4) {
|
|
6014
|
-
|
|
6119
|
+
const { measurementId } = options.config.destinations.ga4;
|
|
6120
|
+
guard("GA4", () => prepareGa4(measurementId));
|
|
6015
6121
|
}
|
|
6016
6122
|
if (options.config.destinations.meta) {
|
|
6017
|
-
|
|
6123
|
+
const { pixelId } = options.config.destinations.meta;
|
|
6124
|
+
guard("Meta", () => prepareMeta(pixelId));
|
|
6018
6125
|
}
|
|
6019
6126
|
if (options.config.destinations.clarity) {
|
|
6020
|
-
|
|
6127
|
+
const { projectId } = options.config.destinations.clarity;
|
|
6128
|
+
guard("Clarity", () => prepareClarity(projectId));
|
|
6021
6129
|
}
|
|
6022
6130
|
cancelScheduledLoad = scheduleScriptLoad(() => {
|
|
6023
6131
|
if (destroyed) return;
|
|
6024
6132
|
if (options.config.destinations.ga4) {
|
|
6025
|
-
|
|
6133
|
+
const { measurementId } = options.config.destinations.ga4;
|
|
6134
|
+
guard("GA4", () => loadGa4Script(measurementId, scriptError("GA4")));
|
|
6026
6135
|
}
|
|
6027
|
-
if (options.config.destinations.meta)
|
|
6136
|
+
if (options.config.destinations.meta)
|
|
6137
|
+
guard("Meta", () => loadMetaScript(scriptError("Meta")));
|
|
6028
6138
|
if (options.config.destinations.clarity) {
|
|
6029
|
-
|
|
6139
|
+
const { projectId } = options.config.destinations.clarity;
|
|
6140
|
+
guard(
|
|
6141
|
+
"Clarity",
|
|
6142
|
+
() => loadClarityScript(projectId, scriptError("Clarity"))
|
|
6143
|
+
);
|
|
6030
6144
|
}
|
|
6031
6145
|
}, options.scriptLoadStrategy);
|
|
6032
6146
|
captureWebsiteMarketingAttribution(
|
|
@@ -6048,11 +6162,11 @@ function createWebsiteMarketingRuntime(options) {
|
|
|
6048
6162
|
if (options.config.destinations.meta) {
|
|
6049
6163
|
refreshWebsiteMarketingMetaContext(options.websiteId);
|
|
6050
6164
|
}
|
|
6051
|
-
} catch
|
|
6165
|
+
} catch {
|
|
6052
6166
|
options.diagnostic?.({
|
|
6053
6167
|
level: "warning",
|
|
6054
6168
|
code: "RUNTIME_INITIALIZATION_FAILED",
|
|
6055
|
-
message:
|
|
6169
|
+
message: "Marketing initialization failed; external reception is unverified"
|
|
6056
6170
|
});
|
|
6057
6171
|
}
|
|
6058
6172
|
}
|
|
@@ -6060,41 +6174,96 @@ function createWebsiteMarketingRuntime(options) {
|
|
|
6060
6174
|
enabled,
|
|
6061
6175
|
track(input) {
|
|
6062
6176
|
if (!enabled || destroyed) return;
|
|
6177
|
+
if (!isBrowserMarketingEvent(input.eventName)) {
|
|
6178
|
+
diagnostic({
|
|
6179
|
+
level: "warning",
|
|
6180
|
+
code: "INVALID_EVENT",
|
|
6181
|
+
message: "Unknown or server-owned event rejected"
|
|
6182
|
+
});
|
|
6183
|
+
return;
|
|
6184
|
+
}
|
|
6185
|
+
try {
|
|
6186
|
+
captureWebsiteMarketingAttribution(
|
|
6187
|
+
options.websiteId,
|
|
6188
|
+
options.config.attributionWindowDays
|
|
6189
|
+
);
|
|
6190
|
+
} catch {
|
|
6191
|
+
}
|
|
6063
6192
|
const event = {
|
|
6064
6193
|
...input,
|
|
6065
6194
|
schemaVersion: 1,
|
|
6066
|
-
eventId: input.eventId ??
|
|
6195
|
+
eventId: input.eventId ?? newMarketingEventId(),
|
|
6067
6196
|
occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
6068
6197
|
currency: input.currency ?? options.config.currencyCode,
|
|
6069
6198
|
pageLocation: sanitizeMarketingUrl(
|
|
6070
6199
|
input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
|
|
6071
6200
|
)
|
|
6072
6201
|
};
|
|
6073
|
-
|
|
6074
|
-
if (
|
|
6075
|
-
|
|
6202
|
+
if (isInteractionEvent(event.eventName)) {
|
|
6203
|
+
if (!interactionParameters(event) || !/^[a-zA-Z0-9_-]{1,100}$/.test(event.eventId)) {
|
|
6204
|
+
diagnostic({
|
|
6205
|
+
level: "warning",
|
|
6206
|
+
code: "INVALID_INTERACTION",
|
|
6207
|
+
message: "Interaction rejected by the public contract",
|
|
6208
|
+
eventName: event.eventName
|
|
6209
|
+
});
|
|
6210
|
+
return;
|
|
6076
6211
|
}
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6212
|
+
for (const key of [
|
|
6213
|
+
"currency",
|
|
6214
|
+
"value",
|
|
6215
|
+
"searchTerm",
|
|
6216
|
+
"itemListId",
|
|
6217
|
+
"itemListName",
|
|
6218
|
+
"shippingTier",
|
|
6219
|
+
"paymentType",
|
|
6220
|
+
"contactMethod",
|
|
6221
|
+
"contactPlacement",
|
|
6222
|
+
"items",
|
|
6223
|
+
"pageTitle"
|
|
6224
|
+
])
|
|
6225
|
+
delete event[key];
|
|
6226
|
+
}
|
|
6227
|
+
const identity = `${event.eventName}:${event.eventId}`;
|
|
6228
|
+
if (dispatched.has(identity)) return;
|
|
6229
|
+
dispatched.add(identity);
|
|
6230
|
+
const oldest = dispatched.values().next().value;
|
|
6231
|
+
if (dispatched.size > 1e3 && oldest) dispatched.delete(oldest);
|
|
6232
|
+
const dispatch = (provider, send) => {
|
|
6233
|
+
try {
|
|
6234
|
+
send();
|
|
6235
|
+
diagnostic({
|
|
6236
|
+
level: "debug",
|
|
6237
|
+
code: "EVENT_DISPATCHED",
|
|
6238
|
+
message: `${provider}: passed to adapter; external reception is unverified`,
|
|
6239
|
+
eventName: event.eventName
|
|
6240
|
+
});
|
|
6241
|
+
} catch {
|
|
6242
|
+
diagnostic({
|
|
6243
|
+
level: "warning",
|
|
6244
|
+
code: "EVENT_DISPATCH_FAILED",
|
|
6245
|
+
message: `${provider}: adapter failed`,
|
|
6246
|
+
eventName: event.eventName
|
|
6247
|
+
});
|
|
6080
6248
|
}
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6249
|
+
};
|
|
6250
|
+
if (options.config.destinations.ga4) {
|
|
6251
|
+
const ga4 = options.config.destinations.ga4;
|
|
6252
|
+
dispatch(
|
|
6253
|
+
"GA4",
|
|
6254
|
+
() => sendGa4(event, ga4.diagnosticMode, ga4.measurementId)
|
|
6255
|
+
);
|
|
6256
|
+
}
|
|
6257
|
+
if (options.config.destinations.meta) {
|
|
6258
|
+
const meta = options.config.destinations.meta;
|
|
6259
|
+
dispatch("Meta", () => sendMeta(event, meta.pixelId));
|
|
6260
|
+
}
|
|
6261
|
+
if (options.config.destinations.clarity) {
|
|
6262
|
+
dispatch("Clarity", () => sendClarityEvent(event.eventName));
|
|
6094
6263
|
}
|
|
6095
6264
|
},
|
|
6096
6265
|
captureAttribution() {
|
|
6097
|
-
if (!enabled) return null;
|
|
6266
|
+
if (!enabled || destroyed) return null;
|
|
6098
6267
|
captureWebsiteMarketingAttribution(
|
|
6099
6268
|
options.websiteId,
|
|
6100
6269
|
options.config.attributionWindowDays
|
|
@@ -6105,7 +6274,7 @@ function createWebsiteMarketingRuntime(options) {
|
|
|
6105
6274
|
return getWebsiteMarketingAttribution(options.websiteId);
|
|
6106
6275
|
},
|
|
6107
6276
|
getAttribution() {
|
|
6108
|
-
if (!enabled) return null;
|
|
6277
|
+
if (!enabled || destroyed) return null;
|
|
6109
6278
|
if (options.config.destinations.meta) {
|
|
6110
6279
|
refreshWebsiteMarketingMetaContext(options.websiteId);
|
|
6111
6280
|
}
|