@bacano/sdk 1.1.20 → 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/react.js CHANGED
@@ -4093,8 +4093,8 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
4093
4093
  };
4094
4094
  }
4095
4095
 
4096
- // libs/sdk/src/react/hooks.ts
4097
- import { useCallback, useContext, useEffect, useRef, useState } from "react";
4096
+ // libs/sdk/src/react/cta-tracking.ts
4097
+ import { useCallback, useContext, useRef } from "react";
4098
4098
 
4099
4099
  // libs/sdk/src/react/context-registry.ts
4100
4100
  import { createContext } from "react";
@@ -4122,13 +4122,6 @@ function getOrCreateReactContext(key, defaultValue) {
4122
4122
  return context;
4123
4123
  }
4124
4124
 
4125
- // libs/sdk/src/react/context.ts
4126
- var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
4127
- client: null,
4128
- loading: true,
4129
- error: null
4130
- });
4131
-
4132
4125
  // libs/sdk/src/react/marketing-context.ts
4133
4126
  var NOOP_MARKETING_RUNTIME = {
4134
4127
  enabled: false,
@@ -4142,15 +4135,44 @@ var BacanoMarketingContext = getOrCreateReactContext(
4142
4135
  NOOP_MARKETING_RUNTIME
4143
4136
  );
4144
4137
 
4138
+ // libs/sdk/src/react/cta-tracking.ts
4139
+ function useBacanoCta(context) {
4140
+ const runtime = useContext(BacanoMarketingContext);
4141
+ const lastClick = useRef({ identity: "", at: -Infinity });
4142
+ const { ctaId, formKey, placement } = context;
4143
+ return useCallback(() => {
4144
+ if (!runtime.enabled) return;
4145
+ const identity = `${ctaId}:${formKey ?? ""}:${placement ?? ""}`;
4146
+ const at = Date.now();
4147
+ if (identity === lastClick.current.identity && at - lastClick.current.at < 400)
4148
+ return;
4149
+ lastClick.current = { identity, at };
4150
+ try {
4151
+ runtime.track({ eventName: "cta_click", ctaId, formKey, placement });
4152
+ } catch {
4153
+ }
4154
+ }, [runtime, ctaId, formKey, placement]);
4155
+ }
4156
+
4157
+ // libs/sdk/src/react/hooks.ts
4158
+ import { useCallback as useCallback2, useContext as useContext2, useEffect, useRef as useRef2, useState } from "react";
4159
+
4160
+ // libs/sdk/src/react/context.ts
4161
+ var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
4162
+ client: null,
4163
+ loading: true,
4164
+ error: null
4165
+ });
4166
+
4145
4167
  // libs/sdk/src/react/hooks.ts
4146
4168
  function useClient() {
4147
- const { client, loading, error } = useContext(BacanoReactContext);
4169
+ const { client, loading, error } = useContext2(BacanoReactContext);
4148
4170
  if (error) throw error;
4149
4171
  if (loading || !client) throw new NotInitializedError();
4150
4172
  return client;
4151
4173
  }
4152
4174
  function useSafeClient() {
4153
- const { client } = useContext(BacanoReactContext);
4175
+ const { client } = useContext2(BacanoReactContext);
4154
4176
  return client;
4155
4177
  }
4156
4178
  function useAsync(fetcher, deps = []) {
@@ -4160,8 +4182,8 @@ function useAsync(fetcher, deps = []) {
4160
4182
  loading: true,
4161
4183
  error: null
4162
4184
  });
4163
- const mountedRef = useRef(true);
4164
- const fetchData = useCallback(async () => {
4185
+ const mountedRef = useRef2(true);
4186
+ const fetchData = useCallback2(async () => {
4165
4187
  if (!client) return;
4166
4188
  setState((prev) => ({ ...prev, loading: true, error: null }));
4167
4189
  try {
@@ -4192,10 +4214,10 @@ function useBacano() {
4192
4214
  return useClient();
4193
4215
  }
4194
4216
  function useBacanoState() {
4195
- return useContext(BacanoReactContext);
4217
+ return useContext2(BacanoReactContext);
4196
4218
  }
4197
4219
  function useMarketing() {
4198
- return useContext(BacanoMarketingContext);
4220
+ return useContext2(BacanoMarketingContext);
4199
4221
  }
4200
4222
  function useProducts(opts) {
4201
4223
  return useAsync(
@@ -4289,7 +4311,7 @@ function useCart() {
4289
4311
  const [cart, setCart] = useState(null);
4290
4312
  const [loading, setLoading] = useState(false);
4291
4313
  const [error, setError] = useState(null);
4292
- const refreshCart = useCallback(async () => {
4314
+ const refreshCart = useCallback2(async () => {
4293
4315
  if (!client) return;
4294
4316
  setLoading(true);
4295
4317
  try {
@@ -4305,7 +4327,7 @@ function useCart() {
4305
4327
  useEffect(() => {
4306
4328
  refreshCart();
4307
4329
  }, [refreshCart]);
4308
- const addItem = useCallback(
4330
+ const addItem = useCallback2(
4309
4331
  async (productVariantId, quantity) => {
4310
4332
  if (!client) throw new NotInitializedError();
4311
4333
  const item = await client.cart.addItem(productVariantId, quantity);
@@ -4314,7 +4336,7 @@ function useCart() {
4314
4336
  },
4315
4337
  [client, refreshCart]
4316
4338
  );
4317
- const updateItem = useCallback(
4339
+ const updateItem = useCallback2(
4318
4340
  async (cartItemId, quantity) => {
4319
4341
  if (!client) throw new NotInitializedError();
4320
4342
  const result = await client.cart.updateItem(cartItemId, quantity);
@@ -4323,7 +4345,7 @@ function useCart() {
4323
4345
  },
4324
4346
  [client, refreshCart]
4325
4347
  );
4326
- const removeItem = useCallback(
4348
+ const removeItem = useCallback2(
4327
4349
  async (cartItemId) => {
4328
4350
  if (!client) throw new NotInitializedError();
4329
4351
  await client.cart.removeItem(cartItemId);
@@ -4331,7 +4353,7 @@ function useCart() {
4331
4353
  },
4332
4354
  [client, refreshCart]
4333
4355
  );
4334
- const clear = useCallback(async () => {
4356
+ const clear = useCallback2(async () => {
4335
4357
  if (!client) throw new NotInitializedError();
4336
4358
  setLoading(true);
4337
4359
  try {
@@ -4346,7 +4368,7 @@ function useCart() {
4346
4368
  setLoading(false);
4347
4369
  }
4348
4370
  }, [client]);
4349
- const getOrCreate = useCallback(
4371
+ const getOrCreate = useCallback2(
4350
4372
  async (contact, fulfillmentFamily) => {
4351
4373
  if (!client) throw new NotInitializedError();
4352
4374
  const c = await client.cart.getOrCreate(contact, fulfillmentFamily);
@@ -4355,7 +4377,7 @@ function useCart() {
4355
4377
  },
4356
4378
  [client]
4357
4379
  );
4358
- const startCheckout = useCallback(
4380
+ const startCheckout = useCallback2(
4359
4381
  async (input) => {
4360
4382
  if (!client) throw new NotInitializedError();
4361
4383
  const c = await client.cart.startCheckout(input);
@@ -4381,7 +4403,7 @@ function useCheckout() {
4381
4403
  const client = useSafeClient();
4382
4404
  const [loading, setLoading] = useState(false);
4383
4405
  const [error, setError] = useState(null);
4384
- const checkout = useCallback(
4406
+ const checkout = useCallback2(
4385
4407
  async (input) => {
4386
4408
  if (!client) throw new NotInitializedError();
4387
4409
  setLoading(true);
@@ -4421,7 +4443,7 @@ function useProfile() {
4421
4443
  error,
4422
4444
  refetch
4423
4445
  } = useAsync((client2) => client2.profile.get());
4424
- const updateProfile = useCallback(
4446
+ const updateProfile = useCallback2(
4425
4447
  async (data) => {
4426
4448
  if (!client) throw new NotInitializedError();
4427
4449
  const updated = await client.profile.update(data);
@@ -4435,7 +4457,7 @@ function useProfile() {
4435
4457
 
4436
4458
  // libs/sdk/src/react/marketing-provider.tsx
4437
4459
  import {
4438
- useContext as useContext3,
4460
+ useContext as useContext4,
4439
4461
  useEffect as useEffect2,
4440
4462
  useState as useState2
4441
4463
  } from "react";
@@ -4478,6 +4500,9 @@ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
4478
4500
  function sanitizeMarketingUrl(input, baseUrl) {
4479
4501
  try {
4480
4502
  const url = new URL(input, baseUrl);
4503
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "";
4504
+ url.username = "";
4505
+ url.password = "";
4481
4506
  const safeParameters = new URLSearchParams();
4482
4507
  for (const [key, value] of url.searchParams.entries()) {
4483
4508
  if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
@@ -4697,50 +4722,65 @@ function serializeWebsiteMarketingAttribution(attribution) {
4697
4722
  };
4698
4723
  }
4699
4724
 
4700
- // libs/sdk/src/marketing/clarity.ts
4701
- var initializedDestinations = /* @__PURE__ */ new Set();
4702
- function clarityWindow() {
4703
- return typeof window === "undefined" ? null : window;
4704
- }
4705
- function prepareClarity(projectId) {
4706
- const target = clarityWindow();
4707
- if (!target || initializedDestinations.has(projectId)) return;
4708
- if (!target.clarity) {
4709
- const clarity = function() {
4710
- clarity.q ??= [];
4711
- clarity.q.push(arguments);
4712
- };
4713
- target.clarity = clarity;
4725
+ // libs/sdk/src/marketing/interactions.ts
4726
+ var WEBSITE_INTERACTION_EVENTS = [
4727
+ "cta_click",
4728
+ "form_open",
4729
+ "form_start",
4730
+ "form_step_complete",
4731
+ "form_submit_attempt",
4732
+ "form_submit_error",
4733
+ "form_submit_success"
4734
+ ];
4735
+ function isInteractionEvent(name) {
4736
+ return WEBSITE_INTERACTION_EVENTS.some((candidate) => candidate === name);
4737
+ }
4738
+ function isInteractionIdentifier(value) {
4739
+ return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value);
4740
+ }
4741
+ function isBrowserMarketingEvent(name) {
4742
+ return isInteractionEvent(name) || [
4743
+ "page_view",
4744
+ "search",
4745
+ "view_item_list",
4746
+ "select_item",
4747
+ "view_item",
4748
+ "add_to_cart",
4749
+ "remove_from_cart",
4750
+ "view_cart",
4751
+ "begin_checkout",
4752
+ "add_shipping_info",
4753
+ "add_payment_info",
4754
+ "contact"
4755
+ ].includes(name);
4756
+ }
4757
+ function interactionParameters(event) {
4758
+ if (!isInteractionEvent(event.eventName)) return {};
4759
+ if (event.eventName === "cta_click" ? !isInteractionIdentifier(event.ctaId) : !isInteractionIdentifier(event.formKey))
4760
+ return null;
4761
+ if (event.eventName === "form_step_complete" && !isInteractionIdentifier(event.stepKey))
4762
+ return null;
4763
+ if (event.eventName === "form_submit_error" && !["validation", "network", "rejected", "unavailable"].includes(
4764
+ event.errorCategory ?? ""
4765
+ ))
4766
+ return null;
4767
+ const params = {};
4768
+ for (const [key, value] of Object.entries({
4769
+ form_key: event.formKey,
4770
+ cta_id: event.ctaId,
4771
+ placement: event.placement,
4772
+ step_key: event.stepKey
4773
+ })) {
4774
+ if (value === void 0) continue;
4775
+ if (!isInteractionIdentifier(value)) return null;
4776
+ params[key] = value;
4714
4777
  }
4715
- initializedDestinations.add(projectId);
4778
+ if (event.eventName === "form_submit_error" && event.errorCategory)
4779
+ params.error_category = event.errorCategory;
4780
+ return params;
4716
4781
  }
4717
- function loadClarityScript(projectId) {
4718
- if (typeof document === "undefined") return;
4719
- const id = `bacano-clarity-${projectId}`;
4720
- if (document.getElementById(id)) return;
4721
- const script = document.createElement("script");
4722
- script.id = id;
4723
- script.async = true;
4724
- script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
4725
- script.referrerPolicy = "strict-origin-when-cross-origin";
4726
- document.head.appendChild(script);
4727
- }
4728
- function sendClarityEvent(eventName) {
4729
- clarityWindow()?.clarity?.("event", eventName);
4730
- }
4731
-
4732
- // libs/sdk/src/marketing/domain.ts
4733
- function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
4734
- if (canonicalDomain === void 0) return true;
4735
- if (!canonicalDomain?.trim()) return false;
4736
- try {
4737
- const configuredHost = new URL(
4738
- canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
4739
- ).hostname.toLowerCase();
4740
- return currentHostname.toLowerCase() === configuredHost;
4741
- } catch {
4742
- return false;
4743
- }
4782
+ function newMarketingEventId() {
4783
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
4744
4784
  }
4745
4785
 
4746
4786
  // libs/sdk/src/marketing/ga4.ts
@@ -4767,19 +4807,20 @@ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
4767
4807
  "contact_page"
4768
4808
  ];
4769
4809
 
4770
- // libs/sdk/src/marketing/runtime.ts
4810
+ // libs/sdk/src/marketing/browser-adapters.ts
4771
4811
  var initializedGa4Destinations = /* @__PURE__ */ new Set();
4772
4812
  var initializedMetaDestinations = /* @__PURE__ */ new Set();
4773
4813
  function marketingWindow() {
4774
4814
  return typeof window === "undefined" ? null : window;
4775
4815
  }
4776
- function loadScriptOnce(id, source) {
4816
+ function loadScriptOnce(id, source, onError) {
4777
4817
  if (typeof document === "undefined" || document.getElementById(id)) return;
4778
4818
  const script = document.createElement("script");
4779
4819
  script.id = id;
4780
4820
  script.async = true;
4781
4821
  script.src = source;
4782
4822
  script.referrerPolicy = "strict-origin-when-cross-origin";
4823
+ script.onerror = () => onError?.();
4783
4824
  document.head.appendChild(script);
4784
4825
  }
4785
4826
  function prepareGa4(measurementId) {
@@ -4794,10 +4835,11 @@ function prepareGa4(measurementId) {
4794
4835
  target.gtag("config", measurementId, { send_page_view: false });
4795
4836
  initializedGa4Destinations.add(measurementId);
4796
4837
  }
4797
- function loadGa4Script(measurementId) {
4838
+ function loadGa4Script(measurementId, onError) {
4798
4839
  loadScriptOnce(
4799
4840
  `bacano-ga4-${measurementId}`,
4800
- `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`
4841
+ `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`,
4842
+ onError
4801
4843
  );
4802
4844
  }
4803
4845
  function captureGa4ClientId(measurementId, websiteId, diagnostic) {
@@ -4845,10 +4887,11 @@ function prepareMeta(pixelId) {
4845
4887
  target.fbq("init", pixelId);
4846
4888
  initializedMetaDestinations.add(pixelId);
4847
4889
  }
4848
- function loadMetaScript() {
4890
+ function loadMetaScript(onError) {
4849
4891
  loadScriptOnce(
4850
4892
  "bacano-meta-pixel",
4851
- "https://connect.facebook.net/en_US/fbevents.js"
4893
+ "https://connect.facebook.net/en_US/fbevents.js",
4894
+ onError
4852
4895
  );
4853
4896
  }
4854
4897
  function scheduleScriptLoad(load, strategy) {
@@ -4912,10 +4955,12 @@ function contactPlacementFor(event) {
4912
4955
  }
4913
4956
  return placement;
4914
4957
  }
4915
- function sendGa4(event, diagnosticMode) {
4958
+ function sendGa4(event, diagnosticMode, measurementId) {
4916
4959
  const target = marketingWindow();
4917
4960
  const contactPlacement = contactPlacementFor(event);
4918
4961
  target?.gtag?.("event", event.eventName, {
4962
+ send_to: measurementId,
4963
+ ...interactionParameters(event),
4919
4964
  currency: event.currency,
4920
4965
  value: event.value,
4921
4966
  search_term: event.searchTerm,
@@ -4932,15 +4977,17 @@ function sendGa4(event, diagnosticMode) {
4932
4977
  debug_mode: diagnosticMode ? true : void 0
4933
4978
  });
4934
4979
  }
4935
- function sendMeta(event) {
4980
+ function sendMeta(event, pixelId) {
4936
4981
  const target = marketingWindow();
4937
4982
  const eventName = metaEventName(event.eventName);
4938
4983
  const isStandard = eventName !== event.eventName;
4939
4984
  const contactPlacement = contactPlacementFor(event);
4940
4985
  target?.fbq?.(
4941
- isStandard ? "track" : "trackCustom",
4986
+ isStandard ? "trackSingle" : "trackSingleCustom",
4987
+ pixelId,
4942
4988
  eventName,
4943
4989
  {
4990
+ ...interactionParameters(event),
4944
4991
  currency: event.currency,
4945
4992
  value: event.value,
4946
4993
  search_string: event.searchTerm,
@@ -4957,14 +5004,69 @@ function sendMeta(event) {
4957
5004
  { eventID: event.eventId }
4958
5005
  );
4959
5006
  }
4960
- function newEventId() {
4961
- return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5007
+
5008
+ // libs/sdk/src/marketing/clarity.ts
5009
+ var initializedDestinations = /* @__PURE__ */ new Set();
5010
+ function clarityWindow() {
5011
+ return typeof window === "undefined" ? null : window;
5012
+ }
5013
+ function prepareClarity(projectId) {
5014
+ const target = clarityWindow();
5015
+ if (!target || initializedDestinations.has(projectId)) return;
5016
+ if (!target.clarity) {
5017
+ const clarity = function() {
5018
+ clarity.q ??= [];
5019
+ clarity.q.push(arguments);
5020
+ };
5021
+ target.clarity = clarity;
5022
+ }
5023
+ initializedDestinations.add(projectId);
5024
+ }
5025
+ function loadClarityScript(projectId, onError) {
5026
+ if (typeof document === "undefined") return;
5027
+ const id = `bacano-clarity-${projectId}`;
5028
+ if (document.getElementById(id)) return;
5029
+ const script = document.createElement("script");
5030
+ script.id = id;
5031
+ script.async = true;
5032
+ script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
5033
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5034
+ script.onerror = () => onError?.();
5035
+ document.head.appendChild(script);
5036
+ }
5037
+ function sendClarityEvent(eventName) {
5038
+ clarityWindow()?.clarity?.("event", eventName);
4962
5039
  }
5040
+
5041
+ // libs/sdk/src/marketing/domain.ts
5042
+ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
5043
+ if (canonicalDomain === void 0) return true;
5044
+ if (!canonicalDomain?.trim()) return false;
5045
+ try {
5046
+ const configuredHost = new URL(
5047
+ canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
5048
+ ).hostname.toLowerCase();
5049
+ return currentHostname.toLowerCase() === configuredHost;
5050
+ } catch {
5051
+ return false;
5052
+ }
5053
+ }
5054
+
5055
+ // libs/sdk/src/marketing/runtime.ts
4963
5056
  function createWebsiteMarketingRuntime(options) {
4964
5057
  let destroyed = false;
5058
+ const dispatched = /* @__PURE__ */ new Set();
5059
+ const observer = options.diagnostic;
5060
+ const diagnostic = (value) => {
5061
+ try {
5062
+ observer?.(value);
5063
+ } catch {
5064
+ }
5065
+ };
5066
+ options = { ...options, diagnostic };
4965
5067
  let cancelScheduledLoad = () => void 0;
4966
5068
  const target = marketingWindow();
4967
- const enabled = options.config.enabled && Boolean(
5069
+ const enabled = options.config.enabled && options.measurementAllowed !== false && Boolean(
4968
5070
  target && matchesCanonicalMarketingHost(
4969
5071
  options.canonicalDomain,
4970
5072
  target.location.hostname
@@ -4972,25 +5074,59 @@ function createWebsiteMarketingRuntime(options) {
4972
5074
  ) && Boolean(
4973
5075
  options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
4974
5076
  );
5077
+ if (!enabled)
5078
+ diagnostic({
5079
+ level: "debug",
5080
+ code: !options.config.enabled ? "MARKETING_DISABLED" : options.measurementAllowed === false ? "MEASUREMENT_NOT_ALLOWED" : !target ? "SERVER_RENDER" : !matchesCanonicalMarketingHost(
5081
+ options.canonicalDomain,
5082
+ target.location.hostname
5083
+ ) ? "DOMAIN_NOT_ALLOWED" : "PROVIDER_NOT_CONFIGURED",
5084
+ message: "Measurement is inactive; no events are emitted"
5085
+ });
4975
5086
  if (enabled) {
5087
+ const guard = (provider, action) => {
5088
+ try {
5089
+ action();
5090
+ } catch {
5091
+ diagnostic({
5092
+ level: "warning",
5093
+ code: "RUNTIME_INITIALIZATION_FAILED",
5094
+ message: `${provider}: initialization failed`
5095
+ });
5096
+ }
5097
+ };
5098
+ const scriptError = (provider) => () => diagnostic({
5099
+ level: "warning",
5100
+ code: "SCRIPT_LOAD_FAILED",
5101
+ message: `${provider}: script could not load (network, CSP or blocker); external reception is unverified`
5102
+ });
4976
5103
  try {
4977
5104
  if (options.config.destinations.ga4) {
4978
- prepareGa4(options.config.destinations.ga4.measurementId);
5105
+ const { measurementId } = options.config.destinations.ga4;
5106
+ guard("GA4", () => prepareGa4(measurementId));
4979
5107
  }
4980
5108
  if (options.config.destinations.meta) {
4981
- prepareMeta(options.config.destinations.meta.pixelId);
5109
+ const { pixelId } = options.config.destinations.meta;
5110
+ guard("Meta", () => prepareMeta(pixelId));
4982
5111
  }
4983
5112
  if (options.config.destinations.clarity) {
4984
- prepareClarity(options.config.destinations.clarity.projectId);
5113
+ const { projectId } = options.config.destinations.clarity;
5114
+ guard("Clarity", () => prepareClarity(projectId));
4985
5115
  }
4986
5116
  cancelScheduledLoad = scheduleScriptLoad(() => {
4987
5117
  if (destroyed) return;
4988
5118
  if (options.config.destinations.ga4) {
4989
- loadGa4Script(options.config.destinations.ga4.measurementId);
5119
+ const { measurementId } = options.config.destinations.ga4;
5120
+ guard("GA4", () => loadGa4Script(measurementId, scriptError("GA4")));
4990
5121
  }
4991
- if (options.config.destinations.meta) loadMetaScript();
5122
+ if (options.config.destinations.meta)
5123
+ guard("Meta", () => loadMetaScript(scriptError("Meta")));
4992
5124
  if (options.config.destinations.clarity) {
4993
- loadClarityScript(options.config.destinations.clarity.projectId);
5125
+ const { projectId } = options.config.destinations.clarity;
5126
+ guard(
5127
+ "Clarity",
5128
+ () => loadClarityScript(projectId, scriptError("Clarity"))
5129
+ );
4994
5130
  }
4995
5131
  }, options.scriptLoadStrategy);
4996
5132
  captureWebsiteMarketingAttribution(
@@ -5012,11 +5148,11 @@ function createWebsiteMarketingRuntime(options) {
5012
5148
  if (options.config.destinations.meta) {
5013
5149
  refreshWebsiteMarketingMetaContext(options.websiteId);
5014
5150
  }
5015
- } catch (error) {
5151
+ } catch {
5016
5152
  options.diagnostic?.({
5017
5153
  level: "warning",
5018
5154
  code: "RUNTIME_INITIALIZATION_FAILED",
5019
- message: error instanceof Error ? error.message : "Initialization failed"
5155
+ message: "Marketing initialization failed; external reception is unverified"
5020
5156
  });
5021
5157
  }
5022
5158
  }
@@ -5024,41 +5160,96 @@ function createWebsiteMarketingRuntime(options) {
5024
5160
  enabled,
5025
5161
  track(input) {
5026
5162
  if (!enabled || destroyed) return;
5163
+ if (!isBrowserMarketingEvent(input.eventName)) {
5164
+ diagnostic({
5165
+ level: "warning",
5166
+ code: "INVALID_EVENT",
5167
+ message: "Unknown or server-owned event rejected"
5168
+ });
5169
+ return;
5170
+ }
5171
+ try {
5172
+ captureWebsiteMarketingAttribution(
5173
+ options.websiteId,
5174
+ options.config.attributionWindowDays
5175
+ );
5176
+ } catch {
5177
+ }
5027
5178
  const event = {
5028
5179
  ...input,
5029
5180
  schemaVersion: 1,
5030
- eventId: input.eventId ?? newEventId(),
5181
+ eventId: input.eventId ?? newMarketingEventId(),
5031
5182
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5032
5183
  currency: input.currency ?? options.config.currencyCode,
5033
5184
  pageLocation: sanitizeMarketingUrl(
5034
5185
  input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
5035
5186
  )
5036
5187
  };
5037
- try {
5038
- if (options.config.destinations.ga4) {
5039
- sendGa4(event, options.config.destinations.ga4.diagnosticMode);
5188
+ if (isInteractionEvent(event.eventName)) {
5189
+ if (!interactionParameters(event) || !/^[a-zA-Z0-9_-]{1,100}$/.test(event.eventId)) {
5190
+ diagnostic({
5191
+ level: "warning",
5192
+ code: "INVALID_INTERACTION",
5193
+ message: "Interaction rejected by the public contract",
5194
+ eventName: event.eventName
5195
+ });
5196
+ return;
5040
5197
  }
5041
- if (options.config.destinations.meta) sendMeta(event);
5042
- if (options.config.destinations.clarity) {
5043
- sendClarityEvent(event.eventName);
5198
+ for (const key of [
5199
+ "currency",
5200
+ "value",
5201
+ "searchTerm",
5202
+ "itemListId",
5203
+ "itemListName",
5204
+ "shippingTier",
5205
+ "paymentType",
5206
+ "contactMethod",
5207
+ "contactPlacement",
5208
+ "items",
5209
+ "pageTitle"
5210
+ ])
5211
+ delete event[key];
5212
+ }
5213
+ const identity = `${event.eventName}:${event.eventId}`;
5214
+ if (dispatched.has(identity)) return;
5215
+ dispatched.add(identity);
5216
+ const oldest = dispatched.values().next().value;
5217
+ if (dispatched.size > 1e3 && oldest) dispatched.delete(oldest);
5218
+ const dispatch = (provider, send) => {
5219
+ try {
5220
+ send();
5221
+ diagnostic({
5222
+ level: "debug",
5223
+ code: "EVENT_DISPATCHED",
5224
+ message: `${provider}: passed to adapter; external reception is unverified`,
5225
+ eventName: event.eventName
5226
+ });
5227
+ } catch {
5228
+ diagnostic({
5229
+ level: "warning",
5230
+ code: "EVENT_DISPATCH_FAILED",
5231
+ message: `${provider}: adapter failed`,
5232
+ eventName: event.eventName
5233
+ });
5044
5234
  }
5045
- options.diagnostic?.({
5046
- level: "debug",
5047
- code: "EVENT_DISPATCHED",
5048
- message: "Marketing event dispatched",
5049
- eventName: event.eventName
5050
- });
5051
- } catch (error) {
5052
- options.diagnostic?.({
5053
- level: "warning",
5054
- code: "EVENT_DISPATCH_FAILED",
5055
- message: error instanceof Error ? error.message : "Dispatch failed",
5056
- eventName: event.eventName
5057
- });
5235
+ };
5236
+ if (options.config.destinations.ga4) {
5237
+ const ga4 = options.config.destinations.ga4;
5238
+ dispatch(
5239
+ "GA4",
5240
+ () => sendGa4(event, ga4.diagnosticMode, ga4.measurementId)
5241
+ );
5242
+ }
5243
+ if (options.config.destinations.meta) {
5244
+ const meta = options.config.destinations.meta;
5245
+ dispatch("Meta", () => sendMeta(event, meta.pixelId));
5246
+ }
5247
+ if (options.config.destinations.clarity) {
5248
+ dispatch("Clarity", () => sendClarityEvent(event.eventName));
5058
5249
  }
5059
5250
  },
5060
5251
  captureAttribution() {
5061
- if (!enabled) return null;
5252
+ if (!enabled || destroyed) return null;
5062
5253
  captureWebsiteMarketingAttribution(
5063
5254
  options.websiteId,
5064
5255
  options.config.attributionWindowDays
@@ -5069,7 +5260,7 @@ function createWebsiteMarketingRuntime(options) {
5069
5260
  return getWebsiteMarketingAttribution(options.websiteId);
5070
5261
  },
5071
5262
  getAttribution() {
5072
- if (!enabled) return null;
5263
+ if (!enabled || destroyed) return null;
5073
5264
  if (options.config.destinations.meta) {
5074
5265
  refreshWebsiteMarketingMetaContext(options.websiteId);
5075
5266
  }
@@ -5083,7 +5274,7 @@ function createWebsiteMarketingRuntime(options) {
5083
5274
  }
5084
5275
 
5085
5276
  // libs/sdk/src/react/preview/context.ts
5086
- import { useContext as useContext2 } from "react";
5277
+ import { useContext as useContext3 } from "react";
5087
5278
  var BacanoPreviewModeContext = getOrCreateReactContext(
5088
5279
  "bacano-preview-mode-v1",
5089
5280
  false
@@ -5099,15 +5290,24 @@ function BacanoMarketingProvider({
5099
5290
  publicConfig,
5100
5291
  children,
5101
5292
  scriptLoadStrategy,
5102
- diagnostic
5293
+ diagnostic,
5294
+ measurementAllowed
5103
5295
  }) {
5104
- const isPreview = useContext3(BacanoPreviewModeContext);
5296
+ const isPreview = useContext4(BacanoPreviewModeContext);
5105
5297
  const [runtime, setRuntime] = useState2(
5106
5298
  NOOP_MARKETING_RUNTIME
5107
5299
  );
5108
5300
  useEffect2(() => {
5109
5301
  if (isPreview || !publicConfig) {
5110
5302
  setRuntime(NOOP_MARKETING_RUNTIME);
5303
+ try {
5304
+ diagnostic?.({
5305
+ level: "debug",
5306
+ code: isPreview ? "PREVIEW_DISABLED" : "PUBLIC_CONFIG_UNAVAILABLE",
5307
+ message: "Measurement is inactive"
5308
+ });
5309
+ } catch {
5310
+ }
5111
5311
  return;
5112
5312
  }
5113
5313
  const next = createWebsiteMarketingRuntime({
@@ -5115,22 +5315,29 @@ function BacanoMarketingProvider({
5115
5315
  config: publicConfig.marketing,
5116
5316
  canonicalDomain: publicConfig.domain,
5117
5317
  scriptLoadStrategy,
5118
- diagnostic
5318
+ diagnostic,
5319
+ measurementAllowed
5119
5320
  });
5120
5321
  setRuntime(next);
5121
5322
  return () => next.destroy();
5122
- }, [diagnostic, isPreview, publicConfig, scriptLoadStrategy]);
5323
+ }, [
5324
+ diagnostic,
5325
+ isPreview,
5326
+ publicConfig,
5327
+ scriptLoadStrategy,
5328
+ measurementAllowed
5329
+ ]);
5123
5330
  return /* @__PURE__ */ jsx(
5124
5331
  BacanoMarketingContext.Provider,
5125
5332
  {
5126
- value: isPreview ? NOOP_MARKETING_RUNTIME : runtime,
5333
+ value: isPreview || measurementAllowed === false ? NOOP_MARKETING_RUNTIME : runtime,
5127
5334
  children
5128
5335
  }
5129
5336
  );
5130
5337
  }
5131
5338
 
5132
5339
  // libs/sdk/src/react/provider.tsx
5133
- import { useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
5340
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
5134
5341
 
5135
5342
  // libs/sdk/src/modules/auth.ts
5136
5343
  function createAuthHeaders(accessToken) {
@@ -7241,7 +7448,7 @@ function BacanoProvider({
7241
7448
  config,
7242
7449
  children
7243
7450
  }) {
7244
- const getAccessTokenRef = useRef2(config.getAccessToken);
7451
+ const getAccessTokenRef = useRef3(config.getAccessToken);
7245
7452
  getAccessTokenRef.current = config.getAccessToken;
7246
7453
  const [state, setState] = useState3({
7247
7454
  client: null,
@@ -7460,6 +7667,7 @@ export {
7460
7667
  useAttributeList,
7461
7668
  useAvailability,
7462
7669
  useBacano,
7670
+ useBacanoCta,
7463
7671
  useBacanoState,
7464
7672
  useBrands,
7465
7673
  useCart,