@bacano/sdk 1.1.19 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,14 +5057,69 @@ 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;
5014
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/runtime.ts
5015
5109
  function createWebsiteMarketingRuntime(options) {
5016
5110
  let destroyed = false;
5111
+ const dispatched = /* @__PURE__ */ new Set();
5112
+ const observer = options.diagnostic;
5113
+ const diagnostic = (value) => {
5114
+ try {
5115
+ observer?.(value);
5116
+ } catch {
5117
+ }
5118
+ };
5119
+ options = { ...options, diagnostic };
5017
5120
  let cancelScheduledLoad = () => void 0;
5018
5121
  const target = marketingWindow();
5019
- const enabled = options.config.enabled && Boolean(
5122
+ const enabled = options.config.enabled && options.measurementAllowed !== false && Boolean(
5020
5123
  target && matchesCanonicalMarketingHost(
5021
5124
  options.canonicalDomain,
5022
5125
  target.location.hostname
@@ -5024,25 +5127,59 @@ function createWebsiteMarketingRuntime(options) {
5024
5127
  ) && Boolean(
5025
5128
  options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
5026
5129
  );
5130
+ if (!enabled)
5131
+ diagnostic({
5132
+ level: "debug",
5133
+ code: !options.config.enabled ? "MARKETING_DISABLED" : options.measurementAllowed === false ? "MEASUREMENT_NOT_ALLOWED" : !target ? "SERVER_RENDER" : !matchesCanonicalMarketingHost(
5134
+ options.canonicalDomain,
5135
+ target.location.hostname
5136
+ ) ? "DOMAIN_NOT_ALLOWED" : "PROVIDER_NOT_CONFIGURED",
5137
+ message: "Measurement is inactive; no events are emitted"
5138
+ });
5027
5139
  if (enabled) {
5140
+ const guard = (provider, action) => {
5141
+ try {
5142
+ action();
5143
+ } catch {
5144
+ diagnostic({
5145
+ level: "warning",
5146
+ code: "RUNTIME_INITIALIZATION_FAILED",
5147
+ message: `${provider}: initialization failed`
5148
+ });
5149
+ }
5150
+ };
5151
+ const scriptError = (provider) => () => diagnostic({
5152
+ level: "warning",
5153
+ code: "SCRIPT_LOAD_FAILED",
5154
+ message: `${provider}: script could not load (network, CSP or blocker); external reception is unverified`
5155
+ });
5028
5156
  try {
5029
5157
  if (options.config.destinations.ga4) {
5030
- prepareGa4(options.config.destinations.ga4.measurementId);
5158
+ const { measurementId } = options.config.destinations.ga4;
5159
+ guard("GA4", () => prepareGa4(measurementId));
5031
5160
  }
5032
5161
  if (options.config.destinations.meta) {
5033
- prepareMeta(options.config.destinations.meta.pixelId);
5162
+ const { pixelId } = options.config.destinations.meta;
5163
+ guard("Meta", () => prepareMeta(pixelId));
5034
5164
  }
5035
5165
  if (options.config.destinations.clarity) {
5036
- prepareClarity(options.config.destinations.clarity.projectId);
5166
+ const { projectId } = options.config.destinations.clarity;
5167
+ guard("Clarity", () => prepareClarity(projectId));
5037
5168
  }
5038
5169
  cancelScheduledLoad = scheduleScriptLoad(() => {
5039
5170
  if (destroyed) return;
5040
5171
  if (options.config.destinations.ga4) {
5041
- loadGa4Script(options.config.destinations.ga4.measurementId);
5172
+ const { measurementId } = options.config.destinations.ga4;
5173
+ guard("GA4", () => loadGa4Script(measurementId, scriptError("GA4")));
5042
5174
  }
5043
- if (options.config.destinations.meta) loadMetaScript();
5175
+ if (options.config.destinations.meta)
5176
+ guard("Meta", () => loadMetaScript(scriptError("Meta")));
5044
5177
  if (options.config.destinations.clarity) {
5045
- loadClarityScript(options.config.destinations.clarity.projectId);
5178
+ const { projectId } = options.config.destinations.clarity;
5179
+ guard(
5180
+ "Clarity",
5181
+ () => loadClarityScript(projectId, scriptError("Clarity"))
5182
+ );
5046
5183
  }
5047
5184
  }, options.scriptLoadStrategy);
5048
5185
  captureWebsiteMarketingAttribution(
@@ -5064,11 +5201,11 @@ function createWebsiteMarketingRuntime(options) {
5064
5201
  if (options.config.destinations.meta) {
5065
5202
  refreshWebsiteMarketingMetaContext(options.websiteId);
5066
5203
  }
5067
- } catch (error) {
5204
+ } catch {
5068
5205
  options.diagnostic?.({
5069
5206
  level: "warning",
5070
5207
  code: "RUNTIME_INITIALIZATION_FAILED",
5071
- message: error instanceof Error ? error.message : "Initialization failed"
5208
+ message: "Marketing initialization failed; external reception is unverified"
5072
5209
  });
5073
5210
  }
5074
5211
  }
@@ -5076,41 +5213,96 @@ function createWebsiteMarketingRuntime(options) {
5076
5213
  enabled,
5077
5214
  track(input) {
5078
5215
  if (!enabled || destroyed) return;
5216
+ if (!isBrowserMarketingEvent(input.eventName)) {
5217
+ diagnostic({
5218
+ level: "warning",
5219
+ code: "INVALID_EVENT",
5220
+ message: "Unknown or server-owned event rejected"
5221
+ });
5222
+ return;
5223
+ }
5224
+ try {
5225
+ captureWebsiteMarketingAttribution(
5226
+ options.websiteId,
5227
+ options.config.attributionWindowDays
5228
+ );
5229
+ } catch {
5230
+ }
5079
5231
  const event = {
5080
5232
  ...input,
5081
5233
  schemaVersion: 1,
5082
- eventId: input.eventId ?? newEventId(),
5234
+ eventId: input.eventId ?? newMarketingEventId(),
5083
5235
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5084
5236
  currency: input.currency ?? options.config.currencyCode,
5085
5237
  pageLocation: sanitizeMarketingUrl(
5086
5238
  input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
5087
5239
  )
5088
5240
  };
5089
- try {
5090
- if (options.config.destinations.ga4) {
5091
- sendGa4(event, options.config.destinations.ga4.diagnosticMode);
5241
+ if (isInteractionEvent(event.eventName)) {
5242
+ if (!interactionParameters(event) || !/^[a-zA-Z0-9_-]{1,100}$/.test(event.eventId)) {
5243
+ diagnostic({
5244
+ level: "warning",
5245
+ code: "INVALID_INTERACTION",
5246
+ message: "Interaction rejected by the public contract",
5247
+ eventName: event.eventName
5248
+ });
5249
+ return;
5092
5250
  }
5093
- if (options.config.destinations.meta) sendMeta(event);
5094
- if (options.config.destinations.clarity) {
5095
- sendClarityEvent(event.eventName);
5251
+ for (const key of [
5252
+ "currency",
5253
+ "value",
5254
+ "searchTerm",
5255
+ "itemListId",
5256
+ "itemListName",
5257
+ "shippingTier",
5258
+ "paymentType",
5259
+ "contactMethod",
5260
+ "contactPlacement",
5261
+ "items",
5262
+ "pageTitle"
5263
+ ])
5264
+ delete event[key];
5265
+ }
5266
+ const identity = `${event.eventName}:${event.eventId}`;
5267
+ if (dispatched.has(identity)) return;
5268
+ dispatched.add(identity);
5269
+ const oldest = dispatched.values().next().value;
5270
+ if (dispatched.size > 1e3 && oldest) dispatched.delete(oldest);
5271
+ const dispatch = (provider, send) => {
5272
+ try {
5273
+ send();
5274
+ diagnostic({
5275
+ level: "debug",
5276
+ code: "EVENT_DISPATCHED",
5277
+ message: `${provider}: passed to adapter; external reception is unverified`,
5278
+ eventName: event.eventName
5279
+ });
5280
+ } catch {
5281
+ diagnostic({
5282
+ level: "warning",
5283
+ code: "EVENT_DISPATCH_FAILED",
5284
+ message: `${provider}: adapter failed`,
5285
+ eventName: event.eventName
5286
+ });
5096
5287
  }
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
- });
5288
+ };
5289
+ if (options.config.destinations.ga4) {
5290
+ const ga4 = options.config.destinations.ga4;
5291
+ dispatch(
5292
+ "GA4",
5293
+ () => sendGa4(event, ga4.diagnosticMode, ga4.measurementId)
5294
+ );
5295
+ }
5296
+ if (options.config.destinations.meta) {
5297
+ const meta = options.config.destinations.meta;
5298
+ dispatch("Meta", () => sendMeta(event, meta.pixelId));
5299
+ }
5300
+ if (options.config.destinations.clarity) {
5301
+ dispatch("Clarity", () => sendClarityEvent(event.eventName));
5110
5302
  }
5111
5303
  },
5112
5304
  captureAttribution() {
5113
- if (!enabled) return null;
5305
+ if (!enabled || destroyed) return null;
5114
5306
  captureWebsiteMarketingAttribution(
5115
5307
  options.websiteId,
5116
5308
  options.config.attributionWindowDays
@@ -5121,7 +5313,7 @@ function createWebsiteMarketingRuntime(options) {
5121
5313
  return getWebsiteMarketingAttribution(options.websiteId);
5122
5314
  },
5123
5315
  getAttribution() {
5124
- if (!enabled) return null;
5316
+ if (!enabled || destroyed) return null;
5125
5317
  if (options.config.destinations.meta) {
5126
5318
  refreshWebsiteMarketingMetaContext(options.websiteId);
5127
5319
  }
@@ -5135,7 +5327,7 @@ function createWebsiteMarketingRuntime(options) {
5135
5327
  }
5136
5328
 
5137
5329
  // libs/sdk/src/react/preview/context.ts
5138
- var import_react3 = require("react");
5330
+ var import_react4 = require("react");
5139
5331
  var BacanoPreviewModeContext = getOrCreateReactContext(
5140
5332
  "bacano-preview-mode-v1",
5141
5333
  false
@@ -5151,15 +5343,24 @@ function BacanoMarketingProvider({
5151
5343
  publicConfig,
5152
5344
  children,
5153
5345
  scriptLoadStrategy,
5154
- diagnostic
5346
+ diagnostic,
5347
+ measurementAllowed
5155
5348
  }) {
5156
- const isPreview = (0, import_react4.useContext)(BacanoPreviewModeContext);
5157
- const [runtime, setRuntime] = (0, import_react4.useState)(
5349
+ const isPreview = (0, import_react5.useContext)(BacanoPreviewModeContext);
5350
+ const [runtime, setRuntime] = (0, import_react5.useState)(
5158
5351
  NOOP_MARKETING_RUNTIME
5159
5352
  );
5160
- (0, import_react4.useEffect)(() => {
5353
+ (0, import_react5.useEffect)(() => {
5161
5354
  if (isPreview || !publicConfig) {
5162
5355
  setRuntime(NOOP_MARKETING_RUNTIME);
5356
+ try {
5357
+ diagnostic?.({
5358
+ level: "debug",
5359
+ code: isPreview ? "PREVIEW_DISABLED" : "PUBLIC_CONFIG_UNAVAILABLE",
5360
+ message: "Measurement is inactive"
5361
+ });
5362
+ } catch {
5363
+ }
5163
5364
  return;
5164
5365
  }
5165
5366
  const next = createWebsiteMarketingRuntime({
@@ -5167,22 +5368,29 @@ function BacanoMarketingProvider({
5167
5368
  config: publicConfig.marketing,
5168
5369
  canonicalDomain: publicConfig.domain,
5169
5370
  scriptLoadStrategy,
5170
- diagnostic
5371
+ diagnostic,
5372
+ measurementAllowed
5171
5373
  });
5172
5374
  setRuntime(next);
5173
5375
  return () => next.destroy();
5174
- }, [diagnostic, isPreview, publicConfig, scriptLoadStrategy]);
5376
+ }, [
5377
+ diagnostic,
5378
+ isPreview,
5379
+ publicConfig,
5380
+ scriptLoadStrategy,
5381
+ measurementAllowed
5382
+ ]);
5175
5383
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
5176
5384
  BacanoMarketingContext.Provider,
5177
5385
  {
5178
- value: isPreview ? NOOP_MARKETING_RUNTIME : runtime,
5386
+ value: isPreview || measurementAllowed === false ? NOOP_MARKETING_RUNTIME : runtime,
5179
5387
  children
5180
5388
  }
5181
5389
  );
5182
5390
  }
5183
5391
 
5184
5392
  // libs/sdk/src/react/provider.tsx
5185
- var import_react5 = require("react");
5393
+ var import_react6 = require("react");
5186
5394
 
5187
5395
  // libs/sdk/src/modules/auth.ts
5188
5396
  function createAuthHeaders(accessToken) {
@@ -7293,14 +7501,14 @@ function BacanoProvider({
7293
7501
  config,
7294
7502
  children
7295
7503
  }) {
7296
- const getAccessTokenRef = (0, import_react5.useRef)(config.getAccessToken);
7504
+ const getAccessTokenRef = (0, import_react6.useRef)(config.getAccessToken);
7297
7505
  getAccessTokenRef.current = config.getAccessToken;
7298
- const [state, setState] = (0, import_react5.useState)({
7506
+ const [state, setState] = (0, import_react6.useState)({
7299
7507
  client: null,
7300
7508
  loading: true,
7301
7509
  error: null
7302
7510
  });
7303
- (0, import_react5.useEffect)(() => {
7511
+ (0, import_react6.useEffect)(() => {
7304
7512
  let cancelled = false;
7305
7513
  const client = createBacanoClient({
7306
7514
  ...config,
@@ -7334,7 +7542,7 @@ function BacanoProvider({
7334
7542
  }
7335
7543
 
7336
7544
  // libs/sdk/src/react/website-gate.tsx
7337
- var import_react6 = require("react");
7545
+ var import_react7 = require("react");
7338
7546
  var import_jsx_runtime3 = require("react/jsx-runtime");
7339
7547
  var PREVIEW_HASH_KEY = "bacano-preview";
7340
7548
  function previewStorageKey(websiteSlug) {
@@ -7453,11 +7661,11 @@ function BacanoWebsiteGate({
7453
7661
  loadingFallback = null,
7454
7662
  constructionFallback
7455
7663
  }) {
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);
7664
+ const [publicConfig, setPublicConfig] = (0, import_react7.useState)(initialConfig);
7665
+ const [isLoading, setIsLoading] = (0, import_react7.useState)(true);
7666
+ const [failedOpen, setFailedOpen] = (0, import_react7.useState)(false);
7459
7667
  const isConstructionEnforced = Boolean(publicConfig?.construction.enforced);
7460
- (0, import_react6.useEffect)(() => {
7668
+ (0, import_react7.useEffect)(() => {
7461
7669
  let active = true;
7462
7670
  const previewToken = readPreviewToken(config.websiteSlug);
7463
7671
  setIsLoading(true);
@@ -7477,7 +7685,7 @@ function BacanoWebsiteGate({
7477
7685
  active = false;
7478
7686
  };
7479
7687
  }, [config.apiUrl, config.origin, config.websiteSlug]);
7480
- (0, import_react6.useEffect)(() => {
7688
+ (0, import_react7.useEffect)(() => {
7481
7689
  if (typeof document === "undefined" || !isConstructionEnforced) return;
7482
7690
  const robots = document.createElement("meta");
7483
7691
  robots.name = "robots";