@bacano/sdk 1.1.20 → 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/react.cjs CHANGED
@@ -33,6 +33,7 @@ __export(react_exports, {
33
33
  useAttributeList: () => useAttributeList,
34
34
  useAvailability: () => useAvailability,
35
35
  useBacano: () => useBacano,
36
+ useBacanoCta: () => useBacanoCta,
36
37
  useBacanoState: () => useBacanoState,
37
38
  useBrands: () => useBrands,
38
39
  useCart: () => useCart,
@@ -4149,7 +4150,7 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
4149
4150
  };
4150
4151
  }
4151
4152
 
4152
- // libs/sdk/src/react/hooks.ts
4153
+ // libs/sdk/src/react/cta-tracking.ts
4153
4154
  var import_react2 = require("react");
4154
4155
 
4155
4156
  // libs/sdk/src/react/context-registry.ts
@@ -4178,13 +4179,6 @@ function getOrCreateReactContext(key, defaultValue) {
4178
4179
  return context;
4179
4180
  }
4180
4181
 
4181
- // libs/sdk/src/react/context.ts
4182
- var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
4183
- client: null,
4184
- loading: true,
4185
- error: null
4186
- });
4187
-
4188
4182
  // libs/sdk/src/react/marketing-context.ts
4189
4183
  var NOOP_MARKETING_RUNTIME = {
4190
4184
  enabled: false,
@@ -4198,26 +4192,55 @@ var BacanoMarketingContext = getOrCreateReactContext(
4198
4192
  NOOP_MARKETING_RUNTIME
4199
4193
  );
4200
4194
 
4195
+ // libs/sdk/src/react/cta-tracking.ts
4196
+ function useBacanoCta(context) {
4197
+ const runtime = (0, import_react2.useContext)(BacanoMarketingContext);
4198
+ const lastClick = (0, import_react2.useRef)({ identity: "", at: -Infinity });
4199
+ const { ctaId, formKey, placement } = context;
4200
+ return (0, import_react2.useCallback)(() => {
4201
+ if (!runtime.enabled) return;
4202
+ const identity = `${ctaId}:${formKey ?? ""}:${placement ?? ""}`;
4203
+ const at = Date.now();
4204
+ if (identity === lastClick.current.identity && at - lastClick.current.at < 400)
4205
+ return;
4206
+ lastClick.current = { identity, at };
4207
+ try {
4208
+ runtime.track({ eventName: "cta_click", ctaId, formKey, placement });
4209
+ } catch {
4210
+ }
4211
+ }, [runtime, ctaId, formKey, placement]);
4212
+ }
4213
+
4214
+ // libs/sdk/src/react/hooks.ts
4215
+ var import_react3 = require("react");
4216
+
4217
+ // libs/sdk/src/react/context.ts
4218
+ var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
4219
+ client: null,
4220
+ loading: true,
4221
+ error: null
4222
+ });
4223
+
4201
4224
  // libs/sdk/src/react/hooks.ts
4202
4225
  function useClient() {
4203
- const { client, loading, error } = (0, import_react2.useContext)(BacanoReactContext);
4226
+ const { client, loading, error } = (0, import_react3.useContext)(BacanoReactContext);
4204
4227
  if (error) throw error;
4205
4228
  if (loading || !client) throw new NotInitializedError();
4206
4229
  return client;
4207
4230
  }
4208
4231
  function useSafeClient() {
4209
- const { client } = (0, import_react2.useContext)(BacanoReactContext);
4232
+ const { client } = (0, import_react3.useContext)(BacanoReactContext);
4210
4233
  return client;
4211
4234
  }
4212
4235
  function useAsync(fetcher, deps = []) {
4213
4236
  const client = useSafeClient();
4214
- const [state, setState] = (0, import_react2.useState)({
4237
+ const [state, setState] = (0, import_react3.useState)({
4215
4238
  data: null,
4216
4239
  loading: true,
4217
4240
  error: null
4218
4241
  });
4219
- const mountedRef = (0, import_react2.useRef)(true);
4220
- const fetchData = (0, import_react2.useCallback)(async () => {
4242
+ const mountedRef = (0, import_react3.useRef)(true);
4243
+ const fetchData = (0, import_react3.useCallback)(async () => {
4221
4244
  if (!client) return;
4222
4245
  setState((prev) => ({ ...prev, loading: true, error: null }));
4223
4246
  try {
@@ -4235,7 +4258,7 @@ function useAsync(fetcher, deps = []) {
4235
4258
  }
4236
4259
  }
4237
4260
  }, [client, ...deps]);
4238
- (0, import_react2.useEffect)(() => {
4261
+ (0, import_react3.useEffect)(() => {
4239
4262
  mountedRef.current = true;
4240
4263
  fetchData();
4241
4264
  return () => {
@@ -4248,10 +4271,10 @@ function useBacano() {
4248
4271
  return useClient();
4249
4272
  }
4250
4273
  function useBacanoState() {
4251
- return (0, import_react2.useContext)(BacanoReactContext);
4274
+ return (0, import_react3.useContext)(BacanoReactContext);
4252
4275
  }
4253
4276
  function useMarketing() {
4254
- return (0, import_react2.useContext)(BacanoMarketingContext);
4277
+ return (0, import_react3.useContext)(BacanoMarketingContext);
4255
4278
  }
4256
4279
  function useProducts(opts) {
4257
4280
  return useAsync(
@@ -4342,10 +4365,10 @@ function useContentByTag(tag, opts) {
4342
4365
  }
4343
4366
  function useCart() {
4344
4367
  const client = useSafeClient();
4345
- const [cart, setCart] = (0, import_react2.useState)(null);
4346
- const [loading, setLoading] = (0, import_react2.useState)(false);
4347
- const [error, setError] = (0, import_react2.useState)(null);
4348
- const refreshCart = (0, import_react2.useCallback)(async () => {
4368
+ const [cart, setCart] = (0, import_react3.useState)(null);
4369
+ const [loading, setLoading] = (0, import_react3.useState)(false);
4370
+ const [error, setError] = (0, import_react3.useState)(null);
4371
+ const refreshCart = (0, import_react3.useCallback)(async () => {
4349
4372
  if (!client) return;
4350
4373
  setLoading(true);
4351
4374
  try {
@@ -4358,10 +4381,10 @@ function useCart() {
4358
4381
  setLoading(false);
4359
4382
  }
4360
4383
  }, [client]);
4361
- (0, import_react2.useEffect)(() => {
4384
+ (0, import_react3.useEffect)(() => {
4362
4385
  refreshCart();
4363
4386
  }, [refreshCart]);
4364
- const addItem = (0, import_react2.useCallback)(
4387
+ const addItem = (0, import_react3.useCallback)(
4365
4388
  async (productVariantId, quantity) => {
4366
4389
  if (!client) throw new NotInitializedError();
4367
4390
  const item = await client.cart.addItem(productVariantId, quantity);
@@ -4370,7 +4393,7 @@ function useCart() {
4370
4393
  },
4371
4394
  [client, refreshCart]
4372
4395
  );
4373
- const updateItem = (0, import_react2.useCallback)(
4396
+ const updateItem = (0, import_react3.useCallback)(
4374
4397
  async (cartItemId, quantity) => {
4375
4398
  if (!client) throw new NotInitializedError();
4376
4399
  const result = await client.cart.updateItem(cartItemId, quantity);
@@ -4379,7 +4402,7 @@ function useCart() {
4379
4402
  },
4380
4403
  [client, refreshCart]
4381
4404
  );
4382
- const removeItem = (0, import_react2.useCallback)(
4405
+ const removeItem = (0, import_react3.useCallback)(
4383
4406
  async (cartItemId) => {
4384
4407
  if (!client) throw new NotInitializedError();
4385
4408
  await client.cart.removeItem(cartItemId);
@@ -4387,7 +4410,7 @@ function useCart() {
4387
4410
  },
4388
4411
  [client, refreshCart]
4389
4412
  );
4390
- const clear = (0, import_react2.useCallback)(async () => {
4413
+ const clear = (0, import_react3.useCallback)(async () => {
4391
4414
  if (!client) throw new NotInitializedError();
4392
4415
  setLoading(true);
4393
4416
  try {
@@ -4402,7 +4425,7 @@ function useCart() {
4402
4425
  setLoading(false);
4403
4426
  }
4404
4427
  }, [client]);
4405
- const getOrCreate = (0, import_react2.useCallback)(
4428
+ const getOrCreate = (0, import_react3.useCallback)(
4406
4429
  async (contact, fulfillmentFamily) => {
4407
4430
  if (!client) throw new NotInitializedError();
4408
4431
  const c = await client.cart.getOrCreate(contact, fulfillmentFamily);
@@ -4411,7 +4434,7 @@ function useCart() {
4411
4434
  },
4412
4435
  [client]
4413
4436
  );
4414
- const startCheckout = (0, import_react2.useCallback)(
4437
+ const startCheckout = (0, import_react3.useCallback)(
4415
4438
  async (input) => {
4416
4439
  if (!client) throw new NotInitializedError();
4417
4440
  const c = await client.cart.startCheckout(input);
@@ -4435,9 +4458,9 @@ function useCart() {
4435
4458
  }
4436
4459
  function useCheckout() {
4437
4460
  const client = useSafeClient();
4438
- const [loading, setLoading] = (0, import_react2.useState)(false);
4439
- const [error, setError] = (0, import_react2.useState)(null);
4440
- const checkout = (0, import_react2.useCallback)(
4461
+ const [loading, setLoading] = (0, import_react3.useState)(false);
4462
+ const [error, setError] = (0, import_react3.useState)(null);
4463
+ const checkout = (0, import_react3.useCallback)(
4441
4464
  async (input) => {
4442
4465
  if (!client) throw new NotInitializedError();
4443
4466
  setLoading(true);
@@ -4477,7 +4500,7 @@ function useProfile() {
4477
4500
  error,
4478
4501
  refetch
4479
4502
  } = useAsync((client2) => client2.profile.get());
4480
- const updateProfile = (0, import_react2.useCallback)(
4503
+ const updateProfile = (0, import_react3.useCallback)(
4481
4504
  async (data) => {
4482
4505
  if (!client) throw new NotInitializedError();
4483
4506
  const updated = await client.profile.update(data);
@@ -4490,7 +4513,7 @@ function useProfile() {
4490
4513
  }
4491
4514
 
4492
4515
  // libs/sdk/src/react/marketing-provider.tsx
4493
- var import_react4 = require("react");
4516
+ var import_react5 = require("react");
4494
4517
 
4495
4518
  // libs/sdk/src/marketing/sanitize.ts
4496
4519
  var SENSITIVE_PARAMETER_PATTERNS = [
@@ -4530,6 +4553,9 @@ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
4530
4553
  function sanitizeMarketingUrl(input, baseUrl) {
4531
4554
  try {
4532
4555
  const url = new URL(input, baseUrl);
4556
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "";
4557
+ url.username = "";
4558
+ url.password = "";
4533
4559
  const safeParameters = new URLSearchParams();
4534
4560
  for (const [key, value] of url.searchParams.entries()) {
4535
4561
  if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
@@ -4749,50 +4775,65 @@ function serializeWebsiteMarketingAttribution(attribution) {
4749
4775
  };
4750
4776
  }
4751
4777
 
4752
- // libs/sdk/src/marketing/clarity.ts
4753
- var initializedDestinations = /* @__PURE__ */ new Set();
4754
- function clarityWindow() {
4755
- return typeof window === "undefined" ? null : window;
4756
- }
4757
- function prepareClarity(projectId) {
4758
- const target = clarityWindow();
4759
- if (!target || initializedDestinations.has(projectId)) return;
4760
- if (!target.clarity) {
4761
- const clarity = function() {
4762
- clarity.q ??= [];
4763
- clarity.q.push(arguments);
4764
- };
4765
- target.clarity = clarity;
4778
+ // libs/sdk/src/marketing/interactions.ts
4779
+ var WEBSITE_INTERACTION_EVENTS = [
4780
+ "cta_click",
4781
+ "form_open",
4782
+ "form_start",
4783
+ "form_step_complete",
4784
+ "form_submit_attempt",
4785
+ "form_submit_error",
4786
+ "form_submit_success"
4787
+ ];
4788
+ function isInteractionEvent(name) {
4789
+ return WEBSITE_INTERACTION_EVENTS.some((candidate) => candidate === name);
4790
+ }
4791
+ function isInteractionIdentifier(value) {
4792
+ return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value);
4793
+ }
4794
+ function isBrowserMarketingEvent(name) {
4795
+ return isInteractionEvent(name) || [
4796
+ "page_view",
4797
+ "search",
4798
+ "view_item_list",
4799
+ "select_item",
4800
+ "view_item",
4801
+ "add_to_cart",
4802
+ "remove_from_cart",
4803
+ "view_cart",
4804
+ "begin_checkout",
4805
+ "add_shipping_info",
4806
+ "add_payment_info",
4807
+ "contact"
4808
+ ].includes(name);
4809
+ }
4810
+ function interactionParameters(event) {
4811
+ if (!isInteractionEvent(event.eventName)) return {};
4812
+ if (event.eventName === "cta_click" ? !isInteractionIdentifier(event.ctaId) : !isInteractionIdentifier(event.formKey))
4813
+ return null;
4814
+ if (event.eventName === "form_step_complete" && !isInteractionIdentifier(event.stepKey))
4815
+ return null;
4816
+ if (event.eventName === "form_submit_error" && !["validation", "network", "rejected", "unavailable"].includes(
4817
+ event.errorCategory ?? ""
4818
+ ))
4819
+ return null;
4820
+ const params = {};
4821
+ for (const [key, value] of Object.entries({
4822
+ form_key: event.formKey,
4823
+ cta_id: event.ctaId,
4824
+ placement: event.placement,
4825
+ step_key: event.stepKey
4826
+ })) {
4827
+ if (value === void 0) continue;
4828
+ if (!isInteractionIdentifier(value)) return null;
4829
+ params[key] = value;
4766
4830
  }
4767
- initializedDestinations.add(projectId);
4768
- }
4769
- function loadClarityScript(projectId) {
4770
- if (typeof document === "undefined") return;
4771
- const id = `bacano-clarity-${projectId}`;
4772
- if (document.getElementById(id)) return;
4773
- const script = document.createElement("script");
4774
- script.id = id;
4775
- script.async = true;
4776
- script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
4777
- script.referrerPolicy = "strict-origin-when-cross-origin";
4778
- document.head.appendChild(script);
4779
- }
4780
- function sendClarityEvent(eventName) {
4781
- clarityWindow()?.clarity?.("event", eventName);
4831
+ if (event.eventName === "form_submit_error" && event.errorCategory)
4832
+ params.error_category = event.errorCategory;
4833
+ return params;
4782
4834
  }
4783
-
4784
- // libs/sdk/src/marketing/domain.ts
4785
- function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
4786
- if (canonicalDomain === void 0) return true;
4787
- if (!canonicalDomain?.trim()) return false;
4788
- try {
4789
- const configuredHost = new URL(
4790
- canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
4791
- ).hostname.toLowerCase();
4792
- return currentHostname.toLowerCase() === configuredHost;
4793
- } catch {
4794
- return false;
4795
- }
4835
+ function newMarketingEventId() {
4836
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
4796
4837
  }
4797
4838
 
4798
4839
  // libs/sdk/src/marketing/ga4.ts
@@ -4819,19 +4860,20 @@ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
4819
4860
  "contact_page"
4820
4861
  ];
4821
4862
 
4822
- // libs/sdk/src/marketing/runtime.ts
4863
+ // libs/sdk/src/marketing/browser-adapters.ts
4823
4864
  var initializedGa4Destinations = /* @__PURE__ */ new Set();
4824
4865
  var initializedMetaDestinations = /* @__PURE__ */ new Set();
4825
4866
  function marketingWindow() {
4826
4867
  return typeof window === "undefined" ? null : window;
4827
4868
  }
4828
- function loadScriptOnce(id, source) {
4869
+ function loadScriptOnce(id, source, onError) {
4829
4870
  if (typeof document === "undefined" || document.getElementById(id)) return;
4830
4871
  const script = document.createElement("script");
4831
4872
  script.id = id;
4832
4873
  script.async = true;
4833
4874
  script.src = source;
4834
4875
  script.referrerPolicy = "strict-origin-when-cross-origin";
4876
+ script.onerror = () => onError?.();
4835
4877
  document.head.appendChild(script);
4836
4878
  }
4837
4879
  function prepareGa4(measurementId) {
@@ -4846,10 +4888,11 @@ function prepareGa4(measurementId) {
4846
4888
  target.gtag("config", measurementId, { send_page_view: false });
4847
4889
  initializedGa4Destinations.add(measurementId);
4848
4890
  }
4849
- function loadGa4Script(measurementId) {
4891
+ function loadGa4Script(measurementId, onError) {
4850
4892
  loadScriptOnce(
4851
4893
  `bacano-ga4-${measurementId}`,
4852
- `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`
4894
+ `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`,
4895
+ onError
4853
4896
  );
4854
4897
  }
4855
4898
  function captureGa4ClientId(measurementId, websiteId, diagnostic) {
@@ -4897,10 +4940,11 @@ function prepareMeta(pixelId) {
4897
4940
  target.fbq("init", pixelId);
4898
4941
  initializedMetaDestinations.add(pixelId);
4899
4942
  }
4900
- function loadMetaScript() {
4943
+ function loadMetaScript(onError) {
4901
4944
  loadScriptOnce(
4902
4945
  "bacano-meta-pixel",
4903
- "https://connect.facebook.net/en_US/fbevents.js"
4946
+ "https://connect.facebook.net/en_US/fbevents.js",
4947
+ onError
4904
4948
  );
4905
4949
  }
4906
4950
  function scheduleScriptLoad(load, strategy) {
@@ -4964,10 +5008,12 @@ function contactPlacementFor(event) {
4964
5008
  }
4965
5009
  return placement;
4966
5010
  }
4967
- function sendGa4(event, diagnosticMode) {
5011
+ function sendGa4(event, diagnosticMode, measurementId) {
4968
5012
  const target = marketingWindow();
4969
5013
  const contactPlacement = contactPlacementFor(event);
4970
5014
  target?.gtag?.("event", event.eventName, {
5015
+ send_to: measurementId,
5016
+ ...interactionParameters(event),
4971
5017
  currency: event.currency,
4972
5018
  value: event.value,
4973
5019
  search_term: event.searchTerm,
@@ -4984,15 +5030,17 @@ function sendGa4(event, diagnosticMode) {
4984
5030
  debug_mode: diagnosticMode ? true : void 0
4985
5031
  });
4986
5032
  }
4987
- function sendMeta(event) {
5033
+ function sendMeta(event, pixelId) {
4988
5034
  const target = marketingWindow();
4989
5035
  const eventName = metaEventName(event.eventName);
4990
5036
  const isStandard = eventName !== event.eventName;
4991
5037
  const contactPlacement = contactPlacementFor(event);
4992
5038
  target?.fbq?.(
4993
- isStandard ? "track" : "trackCustom",
5039
+ isStandard ? "trackSingle" : "trackSingleCustom",
5040
+ pixelId,
4994
5041
  eventName,
4995
5042
  {
5043
+ ...interactionParameters(event),
4996
5044
  currency: event.currency,
4997
5045
  value: event.value,
4998
5046
  search_string: event.searchTerm,
@@ -5009,40 +5057,192 @@ function sendMeta(event) {
5009
5057
  { eventID: event.eventId }
5010
5058
  );
5011
5059
  }
5012
- function newEventId() {
5013
- return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5060
+
5061
+ // libs/sdk/src/marketing/clarity.ts
5062
+ var initializedDestinations = /* @__PURE__ */ new Set();
5063
+ function clarityWindow() {
5064
+ return typeof window === "undefined" ? null : window;
5065
+ }
5066
+ function prepareClarity(projectId) {
5067
+ const target = clarityWindow();
5068
+ if (!target || initializedDestinations.has(projectId)) return;
5069
+ if (!target.clarity) {
5070
+ const clarity = function() {
5071
+ clarity.q ??= [];
5072
+ clarity.q.push(arguments);
5073
+ };
5074
+ target.clarity = clarity;
5075
+ }
5076
+ initializedDestinations.add(projectId);
5077
+ }
5078
+ function loadClarityScript(projectId, onError) {
5079
+ if (typeof document === "undefined") return;
5080
+ const id = `bacano-clarity-${projectId}`;
5081
+ if (document.getElementById(id)) return;
5082
+ const script = document.createElement("script");
5083
+ script.id = id;
5084
+ script.async = true;
5085
+ script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
5086
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5087
+ script.onerror = () => onError?.();
5088
+ document.head.appendChild(script);
5089
+ }
5090
+ function sendClarityEvent(eventName) {
5091
+ clarityWindow()?.clarity?.("event", eventName);
5092
+ }
5093
+
5094
+ // libs/sdk/src/marketing/domain.ts
5095
+ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
5096
+ if (canonicalDomain === void 0) return true;
5097
+ if (!canonicalDomain?.trim()) return false;
5098
+ try {
5099
+ const configuredHost = new URL(
5100
+ canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
5101
+ ).hostname.toLowerCase();
5102
+ return currentHostname.toLowerCase() === configuredHost;
5103
+ } catch {
5104
+ return false;
5105
+ }
5106
+ }
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);
5014
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
+
5163
+ // libs/sdk/src/marketing/runtime.ts
5015
5164
  function createWebsiteMarketingRuntime(options) {
5016
5165
  let destroyed = false;
5166
+ const dispatched = /* @__PURE__ */ new Set();
5167
+ const observer = options.diagnostic;
5168
+ const diagnostic = (value) => {
5169
+ try {
5170
+ observer?.(value);
5171
+ } catch {
5172
+ }
5173
+ };
5174
+ options = { ...options, diagnostic };
5017
5175
  let cancelScheduledLoad = () => void 0;
5018
5176
  const target = marketingWindow();
5019
- const enabled = options.config.enabled && Boolean(
5177
+ const enabled = options.config.enabled && options.measurementAllowed !== false && Boolean(
5020
5178
  target && matchesCanonicalMarketingHost(
5021
5179
  options.canonicalDomain,
5022
5180
  target.location.hostname
5023
5181
  )
5024
5182
  ) && Boolean(
5025
- 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
5026
5184
  );
5185
+ if (!enabled)
5186
+ diagnostic({
5187
+ level: "debug",
5188
+ code: !options.config.enabled ? "MARKETING_DISABLED" : options.measurementAllowed === false ? "MEASUREMENT_NOT_ALLOWED" : !target ? "SERVER_RENDER" : !matchesCanonicalMarketingHost(
5189
+ options.canonicalDomain,
5190
+ target.location.hostname
5191
+ ) ? "DOMAIN_NOT_ALLOWED" : "PROVIDER_NOT_CONFIGURED",
5192
+ message: "Measurement is inactive; no events are emitted"
5193
+ });
5027
5194
  if (enabled) {
5195
+ const guard = (provider, action) => {
5196
+ try {
5197
+ action();
5198
+ } catch {
5199
+ diagnostic({
5200
+ level: "warning",
5201
+ code: "RUNTIME_INITIALIZATION_FAILED",
5202
+ message: `${provider}: initialization failed`
5203
+ });
5204
+ }
5205
+ };
5206
+ const scriptError = (provider) => () => diagnostic({
5207
+ level: "warning",
5208
+ code: "SCRIPT_LOAD_FAILED",
5209
+ message: `${provider}: script could not load (network, CSP or blocker); external reception is unverified`
5210
+ });
5028
5211
  try {
5029
5212
  if (options.config.destinations.ga4) {
5030
- prepareGa4(options.config.destinations.ga4.measurementId);
5213
+ const { measurementId } = options.config.destinations.ga4;
5214
+ guard("GA4", () => prepareGa4(measurementId));
5031
5215
  }
5032
5216
  if (options.config.destinations.meta) {
5033
- prepareMeta(options.config.destinations.meta.pixelId);
5217
+ const { pixelId } = options.config.destinations.meta;
5218
+ guard("Meta", () => prepareMeta(pixelId));
5034
5219
  }
5035
5220
  if (options.config.destinations.clarity) {
5036
- prepareClarity(options.config.destinations.clarity.projectId);
5221
+ const { projectId } = options.config.destinations.clarity;
5222
+ guard("Clarity", () => prepareClarity(projectId));
5223
+ }
5224
+ if (options.config.destinations.gtm) {
5225
+ const { containerId } = options.config.destinations.gtm;
5226
+ guard("GTM", () => prepareGtm(containerId));
5037
5227
  }
5038
5228
  cancelScheduledLoad = scheduleScriptLoad(() => {
5039
5229
  if (destroyed) return;
5040
5230
  if (options.config.destinations.ga4) {
5041
- loadGa4Script(options.config.destinations.ga4.measurementId);
5231
+ const { measurementId } = options.config.destinations.ga4;
5232
+ guard("GA4", () => loadGa4Script(measurementId, scriptError("GA4")));
5042
5233
  }
5043
- if (options.config.destinations.meta) loadMetaScript();
5234
+ if (options.config.destinations.meta)
5235
+ guard("Meta", () => loadMetaScript(scriptError("Meta")));
5044
5236
  if (options.config.destinations.clarity) {
5045
- loadClarityScript(options.config.destinations.clarity.projectId);
5237
+ const { projectId } = options.config.destinations.clarity;
5238
+ guard(
5239
+ "Clarity",
5240
+ () => loadClarityScript(projectId, scriptError("Clarity"))
5241
+ );
5242
+ }
5243
+ if (options.config.destinations.gtm) {
5244
+ const { containerId } = options.config.destinations.gtm;
5245
+ guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
5046
5246
  }
5047
5247
  }, options.scriptLoadStrategy);
5048
5248
  captureWebsiteMarketingAttribution(
@@ -5064,11 +5264,11 @@ function createWebsiteMarketingRuntime(options) {
5064
5264
  if (options.config.destinations.meta) {
5065
5265
  refreshWebsiteMarketingMetaContext(options.websiteId);
5066
5266
  }
5067
- } catch (error) {
5267
+ } catch {
5068
5268
  options.diagnostic?.({
5069
5269
  level: "warning",
5070
5270
  code: "RUNTIME_INITIALIZATION_FAILED",
5071
- message: error instanceof Error ? error.message : "Initialization failed"
5271
+ message: "Marketing initialization failed; external reception is unverified"
5072
5272
  });
5073
5273
  }
5074
5274
  }
@@ -5076,41 +5276,99 @@ function createWebsiteMarketingRuntime(options) {
5076
5276
  enabled,
5077
5277
  track(input) {
5078
5278
  if (!enabled || destroyed) return;
5279
+ if (!isBrowserMarketingEvent(input.eventName)) {
5280
+ diagnostic({
5281
+ level: "warning",
5282
+ code: "INVALID_EVENT",
5283
+ message: "Unknown or server-owned event rejected"
5284
+ });
5285
+ return;
5286
+ }
5287
+ try {
5288
+ captureWebsiteMarketingAttribution(
5289
+ options.websiteId,
5290
+ options.config.attributionWindowDays
5291
+ );
5292
+ } catch {
5293
+ }
5079
5294
  const event = {
5080
5295
  ...input,
5081
5296
  schemaVersion: 1,
5082
- eventId: input.eventId ?? newEventId(),
5297
+ eventId: input.eventId ?? newMarketingEventId(),
5083
5298
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5084
5299
  currency: input.currency ?? options.config.currencyCode,
5085
5300
  pageLocation: sanitizeMarketingUrl(
5086
5301
  input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
5087
5302
  )
5088
5303
  };
5089
- try {
5090
- if (options.config.destinations.ga4) {
5091
- sendGa4(event, options.config.destinations.ga4.diagnosticMode);
5304
+ if (isInteractionEvent(event.eventName)) {
5305
+ if (!interactionParameters(event) || !/^[a-zA-Z0-9_-]{1,100}$/.test(event.eventId)) {
5306
+ diagnostic({
5307
+ level: "warning",
5308
+ code: "INVALID_INTERACTION",
5309
+ message: "Interaction rejected by the public contract",
5310
+ eventName: event.eventName
5311
+ });
5312
+ return;
5092
5313
  }
5093
- if (options.config.destinations.meta) sendMeta(event);
5094
- if (options.config.destinations.clarity) {
5095
- sendClarityEvent(event.eventName);
5314
+ for (const key of [
5315
+ "currency",
5316
+ "value",
5317
+ "searchTerm",
5318
+ "itemListId",
5319
+ "itemListName",
5320
+ "shippingTier",
5321
+ "paymentType",
5322
+ "contactMethod",
5323
+ "contactPlacement",
5324
+ "items",
5325
+ "pageTitle"
5326
+ ])
5327
+ delete event[key];
5328
+ }
5329
+ const identity = `${event.eventName}:${event.eventId}`;
5330
+ if (dispatched.has(identity)) return;
5331
+ dispatched.add(identity);
5332
+ const oldest = dispatched.values().next().value;
5333
+ if (dispatched.size > 1e3 && oldest) dispatched.delete(oldest);
5334
+ const dispatch = (provider, send) => {
5335
+ try {
5336
+ send();
5337
+ diagnostic({
5338
+ level: "debug",
5339
+ code: "EVENT_DISPATCHED",
5340
+ message: `${provider}: passed to adapter; external reception is unverified`,
5341
+ eventName: event.eventName
5342
+ });
5343
+ } catch {
5344
+ diagnostic({
5345
+ level: "warning",
5346
+ code: "EVENT_DISPATCH_FAILED",
5347
+ message: `${provider}: adapter failed`,
5348
+ eventName: event.eventName
5349
+ });
5096
5350
  }
5097
- options.diagnostic?.({
5098
- level: "debug",
5099
- code: "EVENT_DISPATCHED",
5100
- message: "Marketing event dispatched",
5101
- eventName: event.eventName
5102
- });
5103
- } catch (error) {
5104
- options.diagnostic?.({
5105
- level: "warning",
5106
- code: "EVENT_DISPATCH_FAILED",
5107
- message: error instanceof Error ? error.message : "Dispatch failed",
5108
- eventName: event.eventName
5109
- });
5351
+ };
5352
+ if (options.config.destinations.ga4) {
5353
+ const ga4 = options.config.destinations.ga4;
5354
+ dispatch(
5355
+ "GA4",
5356
+ () => sendGa4(event, ga4.diagnosticMode, ga4.measurementId)
5357
+ );
5358
+ }
5359
+ if (options.config.destinations.meta) {
5360
+ const meta = options.config.destinations.meta;
5361
+ dispatch("Meta", () => sendMeta(event, meta.pixelId));
5362
+ }
5363
+ if (options.config.destinations.clarity) {
5364
+ dispatch("Clarity", () => sendClarityEvent(event.eventName));
5365
+ }
5366
+ if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
5367
+ dispatch("GTM", () => sendGtm(event));
5110
5368
  }
5111
5369
  },
5112
5370
  captureAttribution() {
5113
- if (!enabled) return null;
5371
+ if (!enabled || destroyed) return null;
5114
5372
  captureWebsiteMarketingAttribution(
5115
5373
  options.websiteId,
5116
5374
  options.config.attributionWindowDays
@@ -5121,7 +5379,7 @@ function createWebsiteMarketingRuntime(options) {
5121
5379
  return getWebsiteMarketingAttribution(options.websiteId);
5122
5380
  },
5123
5381
  getAttribution() {
5124
- if (!enabled) return null;
5382
+ if (!enabled || destroyed) return null;
5125
5383
  if (options.config.destinations.meta) {
5126
5384
  refreshWebsiteMarketingMetaContext(options.websiteId);
5127
5385
  }
@@ -5135,7 +5393,7 @@ function createWebsiteMarketingRuntime(options) {
5135
5393
  }
5136
5394
 
5137
5395
  // libs/sdk/src/react/preview/context.ts
5138
- var import_react3 = require("react");
5396
+ var import_react4 = require("react");
5139
5397
  var BacanoPreviewModeContext = getOrCreateReactContext(
5140
5398
  "bacano-preview-mode-v1",
5141
5399
  false
@@ -5151,15 +5409,24 @@ function BacanoMarketingProvider({
5151
5409
  publicConfig,
5152
5410
  children,
5153
5411
  scriptLoadStrategy,
5154
- diagnostic
5412
+ diagnostic,
5413
+ measurementAllowed
5155
5414
  }) {
5156
- const isPreview = (0, import_react4.useContext)(BacanoPreviewModeContext);
5157
- const [runtime, setRuntime] = (0, import_react4.useState)(
5415
+ const isPreview = (0, import_react5.useContext)(BacanoPreviewModeContext);
5416
+ const [runtime, setRuntime] = (0, import_react5.useState)(
5158
5417
  NOOP_MARKETING_RUNTIME
5159
5418
  );
5160
- (0, import_react4.useEffect)(() => {
5419
+ (0, import_react5.useEffect)(() => {
5161
5420
  if (isPreview || !publicConfig) {
5162
5421
  setRuntime(NOOP_MARKETING_RUNTIME);
5422
+ try {
5423
+ diagnostic?.({
5424
+ level: "debug",
5425
+ code: isPreview ? "PREVIEW_DISABLED" : "PUBLIC_CONFIG_UNAVAILABLE",
5426
+ message: "Measurement is inactive"
5427
+ });
5428
+ } catch {
5429
+ }
5163
5430
  return;
5164
5431
  }
5165
5432
  const next = createWebsiteMarketingRuntime({
@@ -5167,22 +5434,29 @@ function BacanoMarketingProvider({
5167
5434
  config: publicConfig.marketing,
5168
5435
  canonicalDomain: publicConfig.domain,
5169
5436
  scriptLoadStrategy,
5170
- diagnostic
5437
+ diagnostic,
5438
+ measurementAllowed
5171
5439
  });
5172
5440
  setRuntime(next);
5173
5441
  return () => next.destroy();
5174
- }, [diagnostic, isPreview, publicConfig, scriptLoadStrategy]);
5442
+ }, [
5443
+ diagnostic,
5444
+ isPreview,
5445
+ publicConfig,
5446
+ scriptLoadStrategy,
5447
+ measurementAllowed
5448
+ ]);
5175
5449
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5176
5450
  BacanoMarketingContext.Provider,
5177
5451
  {
5178
- value: isPreview ? NOOP_MARKETING_RUNTIME : runtime,
5452
+ value: isPreview || measurementAllowed === false ? NOOP_MARKETING_RUNTIME : runtime,
5179
5453
  children
5180
5454
  }
5181
5455
  );
5182
5456
  }
5183
5457
 
5184
5458
  // libs/sdk/src/react/provider.tsx
5185
- var import_react5 = require("react");
5459
+ var import_react6 = require("react");
5186
5460
 
5187
5461
  // libs/sdk/src/modules/auth.ts
5188
5462
  function createAuthHeaders(accessToken) {
@@ -5366,7 +5640,8 @@ async function getWebsitePublicConfig(options) {
5366
5640
  pixelId: marketing.destinations.meta.pixel_id,
5367
5641
  diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
5368
5642
  } : null,
5369
- 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 } } : {}
5370
5645
  }
5371
5646
  },
5372
5647
  legal: {
@@ -7293,14 +7568,14 @@ function BacanoProvider({
7293
7568
  config,
7294
7569
  children
7295
7570
  }) {
7296
- const getAccessTokenRef = (0, import_react5.useRef)(config.getAccessToken);
7571
+ const getAccessTokenRef = (0, import_react6.useRef)(config.getAccessToken);
7297
7572
  getAccessTokenRef.current = config.getAccessToken;
7298
- const [state, setState] = (0, import_react5.useState)({
7573
+ const [state, setState] = (0, import_react6.useState)({
7299
7574
  client: null,
7300
7575
  loading: true,
7301
7576
  error: null
7302
7577
  });
7303
- (0, import_react5.useEffect)(() => {
7578
+ (0, import_react6.useEffect)(() => {
7304
7579
  let cancelled = false;
7305
7580
  const client = createBacanoClient({
7306
7581
  ...config,
@@ -7334,7 +7609,7 @@ function BacanoProvider({
7334
7609
  }
7335
7610
 
7336
7611
  // libs/sdk/src/react/website-gate.tsx
7337
- var import_react6 = require("react");
7612
+ var import_react7 = require("react");
7338
7613
  var import_jsx_runtime3 = require("react/jsx-runtime");
7339
7614
  var PREVIEW_HASH_KEY = "bacano-preview";
7340
7615
  function previewStorageKey(websiteSlug) {
@@ -7453,11 +7728,11 @@ function BacanoWebsiteGate({
7453
7728
  loadingFallback = null,
7454
7729
  constructionFallback
7455
7730
  }) {
7456
- const [publicConfig, setPublicConfig] = (0, import_react6.useState)(initialConfig);
7457
- const [isLoading, setIsLoading] = (0, import_react6.useState)(true);
7458
- const [failedOpen, setFailedOpen] = (0, import_react6.useState)(false);
7731
+ const [publicConfig, setPublicConfig] = (0, import_react7.useState)(initialConfig);
7732
+ const [isLoading, setIsLoading] = (0, import_react7.useState)(true);
7733
+ const [failedOpen, setFailedOpen] = (0, import_react7.useState)(false);
7459
7734
  const isConstructionEnforced = Boolean(publicConfig?.construction.enforced);
7460
- (0, import_react6.useEffect)(() => {
7735
+ (0, import_react7.useEffect)(() => {
7461
7736
  let active = true;
7462
7737
  const previewToken = readPreviewToken(config.websiteSlug);
7463
7738
  setIsLoading(true);
@@ -7477,7 +7752,7 @@ function BacanoWebsiteGate({
7477
7752
  active = false;
7478
7753
  };
7479
7754
  }, [config.apiUrl, config.origin, config.websiteSlug]);
7480
- (0, import_react6.useEffect)(() => {
7755
+ (0, import_react7.useEffect)(() => {
7481
7756
  if (typeof document === "undefined" || !isConstructionEnforced) return;
7482
7757
  const robots = document.createElement("meta");
7483
7758
  robots.name = "robots";