@bacano/sdk 1.2.1 → 1.2.2

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/README.md CHANGED
@@ -945,6 +945,10 @@ contains only browser-safe media metadata and a public Storage URL.
945
945
 
946
946
  GA4, Meta, and Microsoft Clarity are configured per website in Bacano and are
947
947
  disabled by default.
948
+ An optional Google Tag Manager web container is also supported via
949
+ `marketing.destinations.gtm.containerId`. It adds `bacano_*` browser events to
950
+ the data layer without replacing native destinations or server conversions.
951
+ See [GTM setup, limitations and duplication safeguards](./STOREFRONT_MARKETING.md#google-tag-manager-contenedor-web-opcional).
948
952
  The existing public-config request returns only active public destination IDs;
949
953
  provider secrets never reach the SDK. The runtime remains a no-op outside the
950
954
  website's canonical production domain.
@@ -6,9 +6,65 @@ identificadores, adaptadores ni llamadas directas a proveedores.
6
6
 
7
7
  ## Regla principal
8
8
 
9
+ ### Google Tag Manager (contenedor web opcional)
10
+
11
+ En Bacano, la tarjeta Google Tag Manager permite guardar un ID `GTM-…`, validar
12
+ que el script publicado sea accesible y activar o pausar la integración por sitio.
13
+ Validar no comprueba propiedad del contenedor, etiquetas, consentimiento ni
14
+ recepción de eventos: comprobar esto en Tag Assistant con el administrador del
15
+ contenedor. No se solicitan secretos, HTML ni URLs de scripts personalizados.
16
+
17
+ Requiere desplegar API/migración y actualizar `@bacano/sdk` en el storefront.
18
+ El contrato público añade `marketing.destinations.gtm.containerId`; SDKs anteriores
19
+ lo ignoran. Un sitio con configuración estática debe reconstruirse para incorporar
20
+ cambios. No se modifica ni se publica automáticamente ningún contenedor de Google.
21
+
22
+ El mismo `BacanoMarketingProvider` carga `gtm.js` una sola vez, conserva la cola
23
+ `window.dataLayer` existente y respeta las barreras de dominio, preview y
24
+ `measurementAllowed`, además de la carga diferida. No instalar otro snippet.
25
+ Esta versión requiere JavaScript; no incorpora un iframe `noscript` ni GTM server-side.
26
+ Pausar evita nuevas cargas con la configuración actualizada; no puede deshacer
27
+ etiquetas de terceros ya ejecutadas en pestañas abiertas. Recargar para comprobar.
28
+
29
+ Los eventos oficiales de navegador se publican con prefijo `bacano_`:
30
+
31
+ ```js
32
+ { event: "bacano_add_to_cart", bacano: {
33
+ schema_version: 1, event_name: "add_to_cart", event_id: "…",
34
+ currency: "COP", value: 50000,
35
+ items: [{ item_id: "product-variant-id", item_name: "Producto", quantity: 1 }]
36
+ } }
37
+ ```
38
+
39
+ Configurar activadores de evento personalizado `bacano_*` y variables de capa de
40
+ datos como `bacano.event_name`, `bacano.value` y `bacano.items`. El objeto `bacano`
41
+ se limpia antes de cada evento para no arrastrar artículos ni parámetros anteriores.
42
+ Incluye URL saneada, parámetros de comercio e identificadores técnicos de
43
+ interacciones; no copia datos de clientes, valores de formularios, atribución,
44
+ títulos de página ni texto libre de búsqueda. Los eventos `search` no llevan el término.
45
+ No emitir desde GTM `purchase`, `refund` u `order_placed` basándose en la página de
46
+ gracias: siguen siendo autoritativos del servidor y no se publican en este puente.
47
+
48
+ GA4, Meta y Clarity nativos permanecen sin cambios. **No repetir esos destinos o
49
+ conversiones dentro del contenedor**. Esta fase no es un modo alternativo completo
50
+ de medición ni realiza deduplicación entre proveedores y etiquetas configuradas
51
+ externamente. El administrador de GTM puede ejecutar scripts con acceso a la página:
52
+ otorgar acceso solo a personas de confianza y revisar cada publicación.
53
+ El aviso de cookies informativo de Bacano no es un CMP y no autoriza las etiquetas;
54
+ el responsable del contenedor debe configurar el consentimiento aplicable.
55
+
56
+ CSP: permitir `https://www.googletagmanager.com` en `script-src`. Los destinos
57
+ adicionales y Tag Assistant requieren revisar sus propios permisos de CSP; no
58
+ habilitar comodines ni `unsafe-eval` globalmente para acomodar un contenedor.
59
+
60
+ Referencias: [configuración de GTM](https://support.google.com/tagmanager/answer/14842164)
61
+ y [contrato dataLayer](https://developers.google.com/tag-platform/tag-manager/datalayer).
62
+
63
+ ### Eventos comunes
64
+
9
65
  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
66
+ si la medición está activa, sanea la URL y adapta el evento a GA4, Meta,
67
+ Microsoft Clarity y, si se configuró, GTM. Nunca
12
68
  se debe llamar directamente a `gtag`, `dataLayer`, `fbq`, Measurement Protocol
13
69
  o Conversions API.
14
70
  Clarity también se carga desde este runtime; no se instala su snippet de forma
package/index.cjs CHANGED
@@ -394,7 +394,8 @@ async function getWebsitePublicConfig(options) {
394
394
  pixelId: marketing.destinations.meta.pixel_id,
395
395
  diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
396
396
  } : null,
397
- clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
397
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null,
398
+ ...marketing.destinations.gtm ? { gtm: { containerId: marketing.destinations.gtm.container_id } } : {}
398
399
  }
399
400
  },
400
401
  legal: {
@@ -6066,6 +6067,61 @@ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
6066
6067
  }
6067
6068
  }
6068
6069
 
6070
+ // libs/sdk/src/marketing/gtm.ts
6071
+ var CONTAINER_ID = /^GTM-[A-Z0-9]{4,32}$/;
6072
+ var preparedQueues = /* @__PURE__ */ new WeakMap();
6073
+ function prepareGtm(containerId) {
6074
+ const target = marketingWindow();
6075
+ if (!target || !CONTAINER_ID.test(containerId)) return;
6076
+ target.dataLayer ??= [];
6077
+ const containers = preparedQueues.get(target.dataLayer) ?? /* @__PURE__ */ new Set();
6078
+ if (containers.has(containerId)) return;
6079
+ target.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
6080
+ containers.add(containerId);
6081
+ preparedQueues.set(target.dataLayer, containers);
6082
+ }
6083
+ function loadGtmScript(containerId, onError) {
6084
+ const target = marketingWindow();
6085
+ if (!target || typeof document === "undefined" || !CONTAINER_ID.test(containerId))
6086
+ return;
6087
+ const id = `bacano-gtm-${containerId}`;
6088
+ if (document.getElementById(id)) return;
6089
+ prepareGtm(containerId);
6090
+ const script = document.createElement("script");
6091
+ script.id = id;
6092
+ script.async = true;
6093
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
6094
+ script.referrerPolicy = "strict-origin-when-cross-origin";
6095
+ script.onerror = () => onError();
6096
+ document.head.appendChild(script);
6097
+ }
6098
+ function sendGtm(event) {
6099
+ const target = marketingWindow();
6100
+ if (!target?.dataLayer) return;
6101
+ target.dataLayer.push({ bacano: null });
6102
+ target.dataLayer.push({
6103
+ event: `bacano_${event.eventName}`,
6104
+ bacano: {
6105
+ schema_version: event.schemaVersion,
6106
+ event_name: event.eventName,
6107
+ event_id: event.eventId,
6108
+ ...interactionParameters(event),
6109
+ page_location: event.pageLocation,
6110
+ currency: event.currency,
6111
+ value: event.value,
6112
+ items: mapGa4Items(event.items),
6113
+ item_list_id: event.itemListId,
6114
+ item_list_name: event.itemListName,
6115
+ shipping_tier: event.shippingTier,
6116
+ payment_type: event.paymentType,
6117
+ method: event.contactMethod,
6118
+ contact_placement: event.eventName === "contact" && WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
6119
+ (value) => value === event.contactPlacement
6120
+ ) ? event.contactPlacement : void 0
6121
+ }
6122
+ });
6123
+ }
6124
+
6069
6125
  // libs/sdk/src/marketing/runtime.ts
6070
6126
  function createWebsiteMarketingRuntime(options) {
6071
6127
  let destroyed = false;
@@ -6086,7 +6142,7 @@ function createWebsiteMarketingRuntime(options) {
6086
6142
  target.location.hostname
6087
6143
  )
6088
6144
  ) && Boolean(
6089
- options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
6145
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity || options.config.destinations.gtm
6090
6146
  );
6091
6147
  if (!enabled)
6092
6148
  diagnostic({
@@ -6127,6 +6183,10 @@ function createWebsiteMarketingRuntime(options) {
6127
6183
  const { projectId } = options.config.destinations.clarity;
6128
6184
  guard("Clarity", () => prepareClarity(projectId));
6129
6185
  }
6186
+ if (options.config.destinations.gtm) {
6187
+ const { containerId } = options.config.destinations.gtm;
6188
+ guard("GTM", () => prepareGtm(containerId));
6189
+ }
6130
6190
  cancelScheduledLoad = scheduleScriptLoad(() => {
6131
6191
  if (destroyed) return;
6132
6192
  if (options.config.destinations.ga4) {
@@ -6142,6 +6202,10 @@ function createWebsiteMarketingRuntime(options) {
6142
6202
  () => loadClarityScript(projectId, scriptError("Clarity"))
6143
6203
  );
6144
6204
  }
6205
+ if (options.config.destinations.gtm) {
6206
+ const { containerId } = options.config.destinations.gtm;
6207
+ guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
6208
+ }
6145
6209
  }, options.scriptLoadStrategy);
6146
6210
  captureWebsiteMarketingAttribution(
6147
6211
  options.websiteId,
@@ -6261,6 +6325,9 @@ function createWebsiteMarketingRuntime(options) {
6261
6325
  if (options.config.destinations.clarity) {
6262
6326
  dispatch("Clarity", () => sendClarityEvent(event.eventName));
6263
6327
  }
6328
+ if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
6329
+ dispatch("GTM", () => sendGtm(event));
6330
+ }
6264
6331
  },
6265
6332
  captureAttribution() {
6266
6333
  if (!enabled || destroyed) return null;
package/index.js CHANGED
@@ -337,7 +337,8 @@ async function getWebsitePublicConfig(options) {
337
337
  pixelId: marketing.destinations.meta.pixel_id,
338
338
  diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
339
339
  } : null,
340
- clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
340
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null,
341
+ ...marketing.destinations.gtm ? { gtm: { containerId: marketing.destinations.gtm.container_id } } : {}
341
342
  }
342
343
  },
343
344
  legal: {
@@ -6009,6 +6010,61 @@ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
6009
6010
  }
6010
6011
  }
6011
6012
 
6013
+ // libs/sdk/src/marketing/gtm.ts
6014
+ var CONTAINER_ID = /^GTM-[A-Z0-9]{4,32}$/;
6015
+ var preparedQueues = /* @__PURE__ */ new WeakMap();
6016
+ function prepareGtm(containerId) {
6017
+ const target = marketingWindow();
6018
+ if (!target || !CONTAINER_ID.test(containerId)) return;
6019
+ target.dataLayer ??= [];
6020
+ const containers = preparedQueues.get(target.dataLayer) ?? /* @__PURE__ */ new Set();
6021
+ if (containers.has(containerId)) return;
6022
+ target.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
6023
+ containers.add(containerId);
6024
+ preparedQueues.set(target.dataLayer, containers);
6025
+ }
6026
+ function loadGtmScript(containerId, onError) {
6027
+ const target = marketingWindow();
6028
+ if (!target || typeof document === "undefined" || !CONTAINER_ID.test(containerId))
6029
+ return;
6030
+ const id = `bacano-gtm-${containerId}`;
6031
+ if (document.getElementById(id)) return;
6032
+ prepareGtm(containerId);
6033
+ const script = document.createElement("script");
6034
+ script.id = id;
6035
+ script.async = true;
6036
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
6037
+ script.referrerPolicy = "strict-origin-when-cross-origin";
6038
+ script.onerror = () => onError();
6039
+ document.head.appendChild(script);
6040
+ }
6041
+ function sendGtm(event) {
6042
+ const target = marketingWindow();
6043
+ if (!target?.dataLayer) return;
6044
+ target.dataLayer.push({ bacano: null });
6045
+ target.dataLayer.push({
6046
+ event: `bacano_${event.eventName}`,
6047
+ bacano: {
6048
+ schema_version: event.schemaVersion,
6049
+ event_name: event.eventName,
6050
+ event_id: event.eventId,
6051
+ ...interactionParameters(event),
6052
+ page_location: event.pageLocation,
6053
+ currency: event.currency,
6054
+ value: event.value,
6055
+ items: mapGa4Items(event.items),
6056
+ item_list_id: event.itemListId,
6057
+ item_list_name: event.itemListName,
6058
+ shipping_tier: event.shippingTier,
6059
+ payment_type: event.paymentType,
6060
+ method: event.contactMethod,
6061
+ contact_placement: event.eventName === "contact" && WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
6062
+ (value) => value === event.contactPlacement
6063
+ ) ? event.contactPlacement : void 0
6064
+ }
6065
+ });
6066
+ }
6067
+
6012
6068
  // libs/sdk/src/marketing/runtime.ts
6013
6069
  function createWebsiteMarketingRuntime(options) {
6014
6070
  let destroyed = false;
@@ -6029,7 +6085,7 @@ function createWebsiteMarketingRuntime(options) {
6029
6085
  target.location.hostname
6030
6086
  )
6031
6087
  ) && Boolean(
6032
- options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
6088
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity || options.config.destinations.gtm
6033
6089
  );
6034
6090
  if (!enabled)
6035
6091
  diagnostic({
@@ -6070,6 +6126,10 @@ function createWebsiteMarketingRuntime(options) {
6070
6126
  const { projectId } = options.config.destinations.clarity;
6071
6127
  guard("Clarity", () => prepareClarity(projectId));
6072
6128
  }
6129
+ if (options.config.destinations.gtm) {
6130
+ const { containerId } = options.config.destinations.gtm;
6131
+ guard("GTM", () => prepareGtm(containerId));
6132
+ }
6073
6133
  cancelScheduledLoad = scheduleScriptLoad(() => {
6074
6134
  if (destroyed) return;
6075
6135
  if (options.config.destinations.ga4) {
@@ -6085,6 +6145,10 @@ function createWebsiteMarketingRuntime(options) {
6085
6145
  () => loadClarityScript(projectId, scriptError("Clarity"))
6086
6146
  );
6087
6147
  }
6148
+ if (options.config.destinations.gtm) {
6149
+ const { containerId } = options.config.destinations.gtm;
6150
+ guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
6151
+ }
6088
6152
  }, options.scriptLoadStrategy);
6089
6153
  captureWebsiteMarketingAttribution(
6090
6154
  options.websiteId,
@@ -6204,6 +6268,9 @@ function createWebsiteMarketingRuntime(options) {
6204
6268
  if (options.config.destinations.clarity) {
6205
6269
  dispatch("Clarity", () => sendClarityEvent(event.eventName));
6206
6270
  }
6271
+ if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
6272
+ dispatch("GTM", () => sendGtm(event));
6273
+ }
6207
6274
  },
6208
6275
  captureAttribution() {
6209
6276
  if (!enabled || destroyed) return null;
@@ -0,0 +1,6 @@
1
+ import type { WebsiteMarketingBrowserEvent } from "./types";
2
+ /** Preserve the shared Google queue, including an existing native gtag.js. */
3
+ export declare function prepareGtm(containerId: string): void;
4
+ export declare function loadGtmScript(containerId: string, onError: () => void): void;
5
+ /** Explicit allowlist: never spread caller input, customer data or attribution. */
6
+ export declare function sendGtm(event: WebsiteMarketingBrowserEvent): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacano/sdk",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "TypeScript SDK for building websites on the Bacano ERP",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
package/react.cjs CHANGED
@@ -5105,6 +5105,61 @@ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
5105
5105
  }
5106
5106
  }
5107
5107
 
5108
+ // libs/sdk/src/marketing/gtm.ts
5109
+ var CONTAINER_ID = /^GTM-[A-Z0-9]{4,32}$/;
5110
+ var preparedQueues = /* @__PURE__ */ new WeakMap();
5111
+ function prepareGtm(containerId) {
5112
+ const target = marketingWindow();
5113
+ if (!target || !CONTAINER_ID.test(containerId)) return;
5114
+ target.dataLayer ??= [];
5115
+ const containers = preparedQueues.get(target.dataLayer) ?? /* @__PURE__ */ new Set();
5116
+ if (containers.has(containerId)) return;
5117
+ target.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
5118
+ containers.add(containerId);
5119
+ preparedQueues.set(target.dataLayer, containers);
5120
+ }
5121
+ function loadGtmScript(containerId, onError) {
5122
+ const target = marketingWindow();
5123
+ if (!target || typeof document === "undefined" || !CONTAINER_ID.test(containerId))
5124
+ return;
5125
+ const id = `bacano-gtm-${containerId}`;
5126
+ if (document.getElementById(id)) return;
5127
+ prepareGtm(containerId);
5128
+ const script = document.createElement("script");
5129
+ script.id = id;
5130
+ script.async = true;
5131
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
5132
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5133
+ script.onerror = () => onError();
5134
+ document.head.appendChild(script);
5135
+ }
5136
+ function sendGtm(event) {
5137
+ const target = marketingWindow();
5138
+ if (!target?.dataLayer) return;
5139
+ target.dataLayer.push({ bacano: null });
5140
+ target.dataLayer.push({
5141
+ event: `bacano_${event.eventName}`,
5142
+ bacano: {
5143
+ schema_version: event.schemaVersion,
5144
+ event_name: event.eventName,
5145
+ event_id: event.eventId,
5146
+ ...interactionParameters(event),
5147
+ page_location: event.pageLocation,
5148
+ currency: event.currency,
5149
+ value: event.value,
5150
+ items: mapGa4Items(event.items),
5151
+ item_list_id: event.itemListId,
5152
+ item_list_name: event.itemListName,
5153
+ shipping_tier: event.shippingTier,
5154
+ payment_type: event.paymentType,
5155
+ method: event.contactMethod,
5156
+ contact_placement: event.eventName === "contact" && WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
5157
+ (value) => value === event.contactPlacement
5158
+ ) ? event.contactPlacement : void 0
5159
+ }
5160
+ });
5161
+ }
5162
+
5108
5163
  // libs/sdk/src/marketing/runtime.ts
5109
5164
  function createWebsiteMarketingRuntime(options) {
5110
5165
  let destroyed = false;
@@ -5125,7 +5180,7 @@ function createWebsiteMarketingRuntime(options) {
5125
5180
  target.location.hostname
5126
5181
  )
5127
5182
  ) && Boolean(
5128
- options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
5183
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity || options.config.destinations.gtm
5129
5184
  );
5130
5185
  if (!enabled)
5131
5186
  diagnostic({
@@ -5166,6 +5221,10 @@ function createWebsiteMarketingRuntime(options) {
5166
5221
  const { projectId } = options.config.destinations.clarity;
5167
5222
  guard("Clarity", () => prepareClarity(projectId));
5168
5223
  }
5224
+ if (options.config.destinations.gtm) {
5225
+ const { containerId } = options.config.destinations.gtm;
5226
+ guard("GTM", () => prepareGtm(containerId));
5227
+ }
5169
5228
  cancelScheduledLoad = scheduleScriptLoad(() => {
5170
5229
  if (destroyed) return;
5171
5230
  if (options.config.destinations.ga4) {
@@ -5181,6 +5240,10 @@ function createWebsiteMarketingRuntime(options) {
5181
5240
  () => loadClarityScript(projectId, scriptError("Clarity"))
5182
5241
  );
5183
5242
  }
5243
+ if (options.config.destinations.gtm) {
5244
+ const { containerId } = options.config.destinations.gtm;
5245
+ guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
5246
+ }
5184
5247
  }, options.scriptLoadStrategy);
5185
5248
  captureWebsiteMarketingAttribution(
5186
5249
  options.websiteId,
@@ -5300,6 +5363,9 @@ function createWebsiteMarketingRuntime(options) {
5300
5363
  if (options.config.destinations.clarity) {
5301
5364
  dispatch("Clarity", () => sendClarityEvent(event.eventName));
5302
5365
  }
5366
+ if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
5367
+ dispatch("GTM", () => sendGtm(event));
5368
+ }
5303
5369
  },
5304
5370
  captureAttribution() {
5305
5371
  if (!enabled || destroyed) return null;
@@ -5574,7 +5640,8 @@ async function getWebsitePublicConfig(options) {
5574
5640
  pixelId: marketing.destinations.meta.pixel_id,
5575
5641
  diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
5576
5642
  } : null,
5577
- clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
5643
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null,
5644
+ ...marketing.destinations.gtm ? { gtm: { containerId: marketing.destinations.gtm.container_id } } : {}
5578
5645
  }
5579
5646
  },
5580
5647
  legal: {
package/react.js CHANGED
@@ -5052,6 +5052,61 @@ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
5052
5052
  }
5053
5053
  }
5054
5054
 
5055
+ // libs/sdk/src/marketing/gtm.ts
5056
+ var CONTAINER_ID = /^GTM-[A-Z0-9]{4,32}$/;
5057
+ var preparedQueues = /* @__PURE__ */ new WeakMap();
5058
+ function prepareGtm(containerId) {
5059
+ const target = marketingWindow();
5060
+ if (!target || !CONTAINER_ID.test(containerId)) return;
5061
+ target.dataLayer ??= [];
5062
+ const containers = preparedQueues.get(target.dataLayer) ?? /* @__PURE__ */ new Set();
5063
+ if (containers.has(containerId)) return;
5064
+ target.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
5065
+ containers.add(containerId);
5066
+ preparedQueues.set(target.dataLayer, containers);
5067
+ }
5068
+ function loadGtmScript(containerId, onError) {
5069
+ const target = marketingWindow();
5070
+ if (!target || typeof document === "undefined" || !CONTAINER_ID.test(containerId))
5071
+ return;
5072
+ const id = `bacano-gtm-${containerId}`;
5073
+ if (document.getElementById(id)) return;
5074
+ prepareGtm(containerId);
5075
+ const script = document.createElement("script");
5076
+ script.id = id;
5077
+ script.async = true;
5078
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
5079
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5080
+ script.onerror = () => onError();
5081
+ document.head.appendChild(script);
5082
+ }
5083
+ function sendGtm(event) {
5084
+ const target = marketingWindow();
5085
+ if (!target?.dataLayer) return;
5086
+ target.dataLayer.push({ bacano: null });
5087
+ target.dataLayer.push({
5088
+ event: `bacano_${event.eventName}`,
5089
+ bacano: {
5090
+ schema_version: event.schemaVersion,
5091
+ event_name: event.eventName,
5092
+ event_id: event.eventId,
5093
+ ...interactionParameters(event),
5094
+ page_location: event.pageLocation,
5095
+ currency: event.currency,
5096
+ value: event.value,
5097
+ items: mapGa4Items(event.items),
5098
+ item_list_id: event.itemListId,
5099
+ item_list_name: event.itemListName,
5100
+ shipping_tier: event.shippingTier,
5101
+ payment_type: event.paymentType,
5102
+ method: event.contactMethod,
5103
+ contact_placement: event.eventName === "contact" && WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
5104
+ (value) => value === event.contactPlacement
5105
+ ) ? event.contactPlacement : void 0
5106
+ }
5107
+ });
5108
+ }
5109
+
5055
5110
  // libs/sdk/src/marketing/runtime.ts
5056
5111
  function createWebsiteMarketingRuntime(options) {
5057
5112
  let destroyed = false;
@@ -5072,7 +5127,7 @@ function createWebsiteMarketingRuntime(options) {
5072
5127
  target.location.hostname
5073
5128
  )
5074
5129
  ) && Boolean(
5075
- options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
5130
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity || options.config.destinations.gtm
5076
5131
  );
5077
5132
  if (!enabled)
5078
5133
  diagnostic({
@@ -5113,6 +5168,10 @@ function createWebsiteMarketingRuntime(options) {
5113
5168
  const { projectId } = options.config.destinations.clarity;
5114
5169
  guard("Clarity", () => prepareClarity(projectId));
5115
5170
  }
5171
+ if (options.config.destinations.gtm) {
5172
+ const { containerId } = options.config.destinations.gtm;
5173
+ guard("GTM", () => prepareGtm(containerId));
5174
+ }
5116
5175
  cancelScheduledLoad = scheduleScriptLoad(() => {
5117
5176
  if (destroyed) return;
5118
5177
  if (options.config.destinations.ga4) {
@@ -5128,6 +5187,10 @@ function createWebsiteMarketingRuntime(options) {
5128
5187
  () => loadClarityScript(projectId, scriptError("Clarity"))
5129
5188
  );
5130
5189
  }
5190
+ if (options.config.destinations.gtm) {
5191
+ const { containerId } = options.config.destinations.gtm;
5192
+ guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
5193
+ }
5131
5194
  }, options.scriptLoadStrategy);
5132
5195
  captureWebsiteMarketingAttribution(
5133
5196
  options.websiteId,
@@ -5247,6 +5310,9 @@ function createWebsiteMarketingRuntime(options) {
5247
5310
  if (options.config.destinations.clarity) {
5248
5311
  dispatch("Clarity", () => sendClarityEvent(event.eventName));
5249
5312
  }
5313
+ if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
5314
+ dispatch("GTM", () => sendGtm(event));
5315
+ }
5250
5316
  },
5251
5317
  captureAttribution() {
5252
5318
  if (!enabled || destroyed) return null;
@@ -5521,7 +5587,8 @@ async function getWebsitePublicConfig(options) {
5521
5587
  pixelId: marketing.destinations.meta.pixel_id,
5522
5588
  diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
5523
5589
  } : null,
5524
- clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
5590
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null,
5591
+ ...marketing.destinations.gtm ? { gtm: { containerId: marketing.destinations.gtm.container_id } } : {}
5525
5592
  }
5526
5593
  },
5527
5594
  legal: {
package/types.d.ts CHANGED
@@ -267,6 +267,10 @@ export interface WebsiteMarketingPublicConfig {
267
267
  clarity?: {
268
268
  projectId: string;
269
269
  } | null;
270
+ /** Optional web container. Does not replace native GA4/Meta destinations. */
271
+ gtm?: {
272
+ containerId: string;
273
+ } | null;
270
274
  };
271
275
  }
272
276
  export type WebsiteCustomerIdentifierType = "AUTO" | "EMAIL" | "PHONE" | "DOCUMENT";