@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/index.js CHANGED
@@ -2900,6 +2900,9 @@ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
2900
2900
  function sanitizeMarketingUrl(input, baseUrl) {
2901
2901
  try {
2902
2902
  const url = new URL(input, baseUrl);
2903
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "";
2904
+ url.username = "";
2905
+ url.password = "";
2903
2906
  const safeParameters = new URLSearchParams();
2904
2907
  for (const [key, value] of url.searchParams.entries()) {
2905
2908
  if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
@@ -5675,50 +5678,65 @@ function createContentModule(graphqlUrl, getHeaders, getContext, storageUrl) {
5675
5678
  };
5676
5679
  }
5677
5680
 
5678
- // libs/sdk/src/marketing/clarity.ts
5679
- var initializedDestinations = /* @__PURE__ */ new Set();
5680
- function clarityWindow() {
5681
- return typeof window === "undefined" ? null : window;
5682
- }
5683
- function prepareClarity(projectId) {
5684
- const target = clarityWindow();
5685
- if (!target || initializedDestinations.has(projectId)) return;
5686
- if (!target.clarity) {
5687
- const clarity = function() {
5688
- clarity.q ??= [];
5689
- clarity.q.push(arguments);
5690
- };
5691
- target.clarity = clarity;
5681
+ // libs/sdk/src/marketing/interactions.ts
5682
+ var WEBSITE_INTERACTION_EVENTS = [
5683
+ "cta_click",
5684
+ "form_open",
5685
+ "form_start",
5686
+ "form_step_complete",
5687
+ "form_submit_attempt",
5688
+ "form_submit_error",
5689
+ "form_submit_success"
5690
+ ];
5691
+ function isInteractionEvent(name) {
5692
+ return WEBSITE_INTERACTION_EVENTS.some((candidate) => candidate === name);
5693
+ }
5694
+ function isInteractionIdentifier(value) {
5695
+ return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value);
5696
+ }
5697
+ function isBrowserMarketingEvent(name) {
5698
+ return isInteractionEvent(name) || [
5699
+ "page_view",
5700
+ "search",
5701
+ "view_item_list",
5702
+ "select_item",
5703
+ "view_item",
5704
+ "add_to_cart",
5705
+ "remove_from_cart",
5706
+ "view_cart",
5707
+ "begin_checkout",
5708
+ "add_shipping_info",
5709
+ "add_payment_info",
5710
+ "contact"
5711
+ ].includes(name);
5712
+ }
5713
+ function interactionParameters(event) {
5714
+ if (!isInteractionEvent(event.eventName)) return {};
5715
+ if (event.eventName === "cta_click" ? !isInteractionIdentifier(event.ctaId) : !isInteractionIdentifier(event.formKey))
5716
+ return null;
5717
+ if (event.eventName === "form_step_complete" && !isInteractionIdentifier(event.stepKey))
5718
+ return null;
5719
+ if (event.eventName === "form_submit_error" && !["validation", "network", "rejected", "unavailable"].includes(
5720
+ event.errorCategory ?? ""
5721
+ ))
5722
+ return null;
5723
+ const params = {};
5724
+ for (const [key, value] of Object.entries({
5725
+ form_key: event.formKey,
5726
+ cta_id: event.ctaId,
5727
+ placement: event.placement,
5728
+ step_key: event.stepKey
5729
+ })) {
5730
+ if (value === void 0) continue;
5731
+ if (!isInteractionIdentifier(value)) return null;
5732
+ params[key] = value;
5692
5733
  }
5693
- initializedDestinations.add(projectId);
5734
+ if (event.eventName === "form_submit_error" && event.errorCategory)
5735
+ params.error_category = event.errorCategory;
5736
+ return params;
5694
5737
  }
5695
- function loadClarityScript(projectId) {
5696
- if (typeof document === "undefined") return;
5697
- const id = `bacano-clarity-${projectId}`;
5698
- if (document.getElementById(id)) return;
5699
- const script = document.createElement("script");
5700
- script.id = id;
5701
- script.async = true;
5702
- script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
5703
- script.referrerPolicy = "strict-origin-when-cross-origin";
5704
- document.head.appendChild(script);
5705
- }
5706
- function sendClarityEvent(eventName) {
5707
- clarityWindow()?.clarity?.("event", eventName);
5708
- }
5709
-
5710
- // libs/sdk/src/marketing/domain.ts
5711
- function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
5712
- if (canonicalDomain === void 0) return true;
5713
- if (!canonicalDomain?.trim()) return false;
5714
- try {
5715
- const configuredHost = new URL(
5716
- canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
5717
- ).hostname.toLowerCase();
5718
- return currentHostname.toLowerCase() === configuredHost;
5719
- } catch {
5720
- return false;
5721
- }
5738
+ function newMarketingEventId() {
5739
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5722
5740
  }
5723
5741
 
5724
5742
  // libs/sdk/src/marketing/ga4.ts
@@ -5746,19 +5764,20 @@ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
5746
5764
  "contact_page"
5747
5765
  ];
5748
5766
 
5749
- // libs/sdk/src/marketing/runtime.ts
5767
+ // libs/sdk/src/marketing/browser-adapters.ts
5750
5768
  var initializedGa4Destinations = /* @__PURE__ */ new Set();
5751
5769
  var initializedMetaDestinations = /* @__PURE__ */ new Set();
5752
5770
  function marketingWindow() {
5753
5771
  return typeof window === "undefined" ? null : window;
5754
5772
  }
5755
- function loadScriptOnce(id, source) {
5773
+ function loadScriptOnce(id, source, onError) {
5756
5774
  if (typeof document === "undefined" || document.getElementById(id)) return;
5757
5775
  const script = document.createElement("script");
5758
5776
  script.id = id;
5759
5777
  script.async = true;
5760
5778
  script.src = source;
5761
5779
  script.referrerPolicy = "strict-origin-when-cross-origin";
5780
+ script.onerror = () => onError?.();
5762
5781
  document.head.appendChild(script);
5763
5782
  }
5764
5783
  function prepareGa4(measurementId) {
@@ -5773,10 +5792,11 @@ function prepareGa4(measurementId) {
5773
5792
  target.gtag("config", measurementId, { send_page_view: false });
5774
5793
  initializedGa4Destinations.add(measurementId);
5775
5794
  }
5776
- function loadGa4Script(measurementId) {
5795
+ function loadGa4Script(measurementId, onError) {
5777
5796
  loadScriptOnce(
5778
5797
  `bacano-ga4-${measurementId}`,
5779
- `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`
5798
+ `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`,
5799
+ onError
5780
5800
  );
5781
5801
  }
5782
5802
  function captureGa4ClientId(measurementId, websiteId, diagnostic) {
@@ -5824,10 +5844,11 @@ function prepareMeta(pixelId) {
5824
5844
  target.fbq("init", pixelId);
5825
5845
  initializedMetaDestinations.add(pixelId);
5826
5846
  }
5827
- function loadMetaScript() {
5847
+ function loadMetaScript(onError) {
5828
5848
  loadScriptOnce(
5829
5849
  "bacano-meta-pixel",
5830
- "https://connect.facebook.net/en_US/fbevents.js"
5850
+ "https://connect.facebook.net/en_US/fbevents.js",
5851
+ onError
5831
5852
  );
5832
5853
  }
5833
5854
  function scheduleScriptLoad(load, strategy) {
@@ -5891,10 +5912,12 @@ function contactPlacementFor(event) {
5891
5912
  }
5892
5913
  return placement;
5893
5914
  }
5894
- function sendGa4(event, diagnosticMode) {
5915
+ function sendGa4(event, diagnosticMode, measurementId) {
5895
5916
  const target = marketingWindow();
5896
5917
  const contactPlacement = contactPlacementFor(event);
5897
5918
  target?.gtag?.("event", event.eventName, {
5919
+ send_to: measurementId,
5920
+ ...interactionParameters(event),
5898
5921
  currency: event.currency,
5899
5922
  value: event.value,
5900
5923
  search_term: event.searchTerm,
@@ -5911,15 +5934,17 @@ function sendGa4(event, diagnosticMode) {
5911
5934
  debug_mode: diagnosticMode ? true : void 0
5912
5935
  });
5913
5936
  }
5914
- function sendMeta(event) {
5937
+ function sendMeta(event, pixelId) {
5915
5938
  const target = marketingWindow();
5916
5939
  const eventName = metaEventName(event.eventName);
5917
5940
  const isStandard = eventName !== event.eventName;
5918
5941
  const contactPlacement = contactPlacementFor(event);
5919
5942
  target?.fbq?.(
5920
- isStandard ? "track" : "trackCustom",
5943
+ isStandard ? "trackSingle" : "trackSingleCustom",
5944
+ pixelId,
5921
5945
  eventName,
5922
5946
  {
5947
+ ...interactionParameters(event),
5923
5948
  currency: event.currency,
5924
5949
  value: event.value,
5925
5950
  search_string: event.searchTerm,
@@ -5936,14 +5961,69 @@ function sendMeta(event) {
5936
5961
  { eventID: event.eventId }
5937
5962
  );
5938
5963
  }
5939
- function newEventId() {
5940
- return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5964
+
5965
+ // libs/sdk/src/marketing/clarity.ts
5966
+ var initializedDestinations = /* @__PURE__ */ new Set();
5967
+ function clarityWindow() {
5968
+ return typeof window === "undefined" ? null : window;
5941
5969
  }
5970
+ function prepareClarity(projectId) {
5971
+ const target = clarityWindow();
5972
+ if (!target || initializedDestinations.has(projectId)) return;
5973
+ if (!target.clarity) {
5974
+ const clarity = function() {
5975
+ clarity.q ??= [];
5976
+ clarity.q.push(arguments);
5977
+ };
5978
+ target.clarity = clarity;
5979
+ }
5980
+ initializedDestinations.add(projectId);
5981
+ }
5982
+ function loadClarityScript(projectId, onError) {
5983
+ if (typeof document === "undefined") return;
5984
+ const id = `bacano-clarity-${projectId}`;
5985
+ if (document.getElementById(id)) return;
5986
+ const script = document.createElement("script");
5987
+ script.id = id;
5988
+ script.async = true;
5989
+ script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
5990
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5991
+ script.onerror = () => onError?.();
5992
+ document.head.appendChild(script);
5993
+ }
5994
+ function sendClarityEvent(eventName) {
5995
+ clarityWindow()?.clarity?.("event", eventName);
5996
+ }
5997
+
5998
+ // libs/sdk/src/marketing/domain.ts
5999
+ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
6000
+ if (canonicalDomain === void 0) return true;
6001
+ if (!canonicalDomain?.trim()) return false;
6002
+ try {
6003
+ const configuredHost = new URL(
6004
+ canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
6005
+ ).hostname.toLowerCase();
6006
+ return currentHostname.toLowerCase() === configuredHost;
6007
+ } catch {
6008
+ return false;
6009
+ }
6010
+ }
6011
+
6012
+ // libs/sdk/src/marketing/runtime.ts
5942
6013
  function createWebsiteMarketingRuntime(options) {
5943
6014
  let destroyed = false;
6015
+ const dispatched = /* @__PURE__ */ new Set();
6016
+ const observer = options.diagnostic;
6017
+ const diagnostic = (value) => {
6018
+ try {
6019
+ observer?.(value);
6020
+ } catch {
6021
+ }
6022
+ };
6023
+ options = { ...options, diagnostic };
5944
6024
  let cancelScheduledLoad = () => void 0;
5945
6025
  const target = marketingWindow();
5946
- const enabled = options.config.enabled && Boolean(
6026
+ const enabled = options.config.enabled && options.measurementAllowed !== false && Boolean(
5947
6027
  target && matchesCanonicalMarketingHost(
5948
6028
  options.canonicalDomain,
5949
6029
  target.location.hostname
@@ -5951,25 +6031,59 @@ function createWebsiteMarketingRuntime(options) {
5951
6031
  ) && Boolean(
5952
6032
  options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
5953
6033
  );
6034
+ if (!enabled)
6035
+ diagnostic({
6036
+ level: "debug",
6037
+ code: !options.config.enabled ? "MARKETING_DISABLED" : options.measurementAllowed === false ? "MEASUREMENT_NOT_ALLOWED" : !target ? "SERVER_RENDER" : !matchesCanonicalMarketingHost(
6038
+ options.canonicalDomain,
6039
+ target.location.hostname
6040
+ ) ? "DOMAIN_NOT_ALLOWED" : "PROVIDER_NOT_CONFIGURED",
6041
+ message: "Measurement is inactive; no events are emitted"
6042
+ });
5954
6043
  if (enabled) {
6044
+ const guard = (provider, action) => {
6045
+ try {
6046
+ action();
6047
+ } catch {
6048
+ diagnostic({
6049
+ level: "warning",
6050
+ code: "RUNTIME_INITIALIZATION_FAILED",
6051
+ message: `${provider}: initialization failed`
6052
+ });
6053
+ }
6054
+ };
6055
+ const scriptError = (provider) => () => diagnostic({
6056
+ level: "warning",
6057
+ code: "SCRIPT_LOAD_FAILED",
6058
+ message: `${provider}: script could not load (network, CSP or blocker); external reception is unverified`
6059
+ });
5955
6060
  try {
5956
6061
  if (options.config.destinations.ga4) {
5957
- prepareGa4(options.config.destinations.ga4.measurementId);
6062
+ const { measurementId } = options.config.destinations.ga4;
6063
+ guard("GA4", () => prepareGa4(measurementId));
5958
6064
  }
5959
6065
  if (options.config.destinations.meta) {
5960
- prepareMeta(options.config.destinations.meta.pixelId);
6066
+ const { pixelId } = options.config.destinations.meta;
6067
+ guard("Meta", () => prepareMeta(pixelId));
5961
6068
  }
5962
6069
  if (options.config.destinations.clarity) {
5963
- prepareClarity(options.config.destinations.clarity.projectId);
6070
+ const { projectId } = options.config.destinations.clarity;
6071
+ guard("Clarity", () => prepareClarity(projectId));
5964
6072
  }
5965
6073
  cancelScheduledLoad = scheduleScriptLoad(() => {
5966
6074
  if (destroyed) return;
5967
6075
  if (options.config.destinations.ga4) {
5968
- loadGa4Script(options.config.destinations.ga4.measurementId);
6076
+ const { measurementId } = options.config.destinations.ga4;
6077
+ guard("GA4", () => loadGa4Script(measurementId, scriptError("GA4")));
5969
6078
  }
5970
- if (options.config.destinations.meta) loadMetaScript();
6079
+ if (options.config.destinations.meta)
6080
+ guard("Meta", () => loadMetaScript(scriptError("Meta")));
5971
6081
  if (options.config.destinations.clarity) {
5972
- loadClarityScript(options.config.destinations.clarity.projectId);
6082
+ const { projectId } = options.config.destinations.clarity;
6083
+ guard(
6084
+ "Clarity",
6085
+ () => loadClarityScript(projectId, scriptError("Clarity"))
6086
+ );
5973
6087
  }
5974
6088
  }, options.scriptLoadStrategy);
5975
6089
  captureWebsiteMarketingAttribution(
@@ -5991,11 +6105,11 @@ function createWebsiteMarketingRuntime(options) {
5991
6105
  if (options.config.destinations.meta) {
5992
6106
  refreshWebsiteMarketingMetaContext(options.websiteId);
5993
6107
  }
5994
- } catch (error) {
6108
+ } catch {
5995
6109
  options.diagnostic?.({
5996
6110
  level: "warning",
5997
6111
  code: "RUNTIME_INITIALIZATION_FAILED",
5998
- message: error instanceof Error ? error.message : "Initialization failed"
6112
+ message: "Marketing initialization failed; external reception is unverified"
5999
6113
  });
6000
6114
  }
6001
6115
  }
@@ -6003,41 +6117,96 @@ function createWebsiteMarketingRuntime(options) {
6003
6117
  enabled,
6004
6118
  track(input) {
6005
6119
  if (!enabled || destroyed) return;
6120
+ if (!isBrowserMarketingEvent(input.eventName)) {
6121
+ diagnostic({
6122
+ level: "warning",
6123
+ code: "INVALID_EVENT",
6124
+ message: "Unknown or server-owned event rejected"
6125
+ });
6126
+ return;
6127
+ }
6128
+ try {
6129
+ captureWebsiteMarketingAttribution(
6130
+ options.websiteId,
6131
+ options.config.attributionWindowDays
6132
+ );
6133
+ } catch {
6134
+ }
6006
6135
  const event = {
6007
6136
  ...input,
6008
6137
  schemaVersion: 1,
6009
- eventId: input.eventId ?? newEventId(),
6138
+ eventId: input.eventId ?? newMarketingEventId(),
6010
6139
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
6011
6140
  currency: input.currency ?? options.config.currencyCode,
6012
6141
  pageLocation: sanitizeMarketingUrl(
6013
6142
  input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
6014
6143
  )
6015
6144
  };
6016
- try {
6017
- if (options.config.destinations.ga4) {
6018
- sendGa4(event, options.config.destinations.ga4.diagnosticMode);
6145
+ if (isInteractionEvent(event.eventName)) {
6146
+ if (!interactionParameters(event) || !/^[a-zA-Z0-9_-]{1,100}$/.test(event.eventId)) {
6147
+ diagnostic({
6148
+ level: "warning",
6149
+ code: "INVALID_INTERACTION",
6150
+ message: "Interaction rejected by the public contract",
6151
+ eventName: event.eventName
6152
+ });
6153
+ return;
6019
6154
  }
6020
- if (options.config.destinations.meta) sendMeta(event);
6021
- if (options.config.destinations.clarity) {
6022
- sendClarityEvent(event.eventName);
6155
+ for (const key of [
6156
+ "currency",
6157
+ "value",
6158
+ "searchTerm",
6159
+ "itemListId",
6160
+ "itemListName",
6161
+ "shippingTier",
6162
+ "paymentType",
6163
+ "contactMethod",
6164
+ "contactPlacement",
6165
+ "items",
6166
+ "pageTitle"
6167
+ ])
6168
+ delete event[key];
6169
+ }
6170
+ const identity = `${event.eventName}:${event.eventId}`;
6171
+ if (dispatched.has(identity)) return;
6172
+ dispatched.add(identity);
6173
+ const oldest = dispatched.values().next().value;
6174
+ if (dispatched.size > 1e3 && oldest) dispatched.delete(oldest);
6175
+ const dispatch = (provider, send) => {
6176
+ try {
6177
+ send();
6178
+ diagnostic({
6179
+ level: "debug",
6180
+ code: "EVENT_DISPATCHED",
6181
+ message: `${provider}: passed to adapter; external reception is unverified`,
6182
+ eventName: event.eventName
6183
+ });
6184
+ } catch {
6185
+ diagnostic({
6186
+ level: "warning",
6187
+ code: "EVENT_DISPATCH_FAILED",
6188
+ message: `${provider}: adapter failed`,
6189
+ eventName: event.eventName
6190
+ });
6023
6191
  }
6024
- options.diagnostic?.({
6025
- level: "debug",
6026
- code: "EVENT_DISPATCHED",
6027
- message: "Marketing event dispatched",
6028
- eventName: event.eventName
6029
- });
6030
- } catch (error) {
6031
- options.diagnostic?.({
6032
- level: "warning",
6033
- code: "EVENT_DISPATCH_FAILED",
6034
- message: error instanceof Error ? error.message : "Dispatch failed",
6035
- eventName: event.eventName
6036
- });
6192
+ };
6193
+ if (options.config.destinations.ga4) {
6194
+ const ga4 = options.config.destinations.ga4;
6195
+ dispatch(
6196
+ "GA4",
6197
+ () => sendGa4(event, ga4.diagnosticMode, ga4.measurementId)
6198
+ );
6199
+ }
6200
+ if (options.config.destinations.meta) {
6201
+ const meta = options.config.destinations.meta;
6202
+ dispatch("Meta", () => sendMeta(event, meta.pixelId));
6203
+ }
6204
+ if (options.config.destinations.clarity) {
6205
+ dispatch("Clarity", () => sendClarityEvent(event.eventName));
6037
6206
  }
6038
6207
  },
6039
6208
  captureAttribution() {
6040
- if (!enabled) return null;
6209
+ if (!enabled || destroyed) return null;
6041
6210
  captureWebsiteMarketingAttribution(
6042
6211
  options.websiteId,
6043
6212
  options.config.attributionWindowDays
@@ -6048,7 +6217,7 @@ function createWebsiteMarketingRuntime(options) {
6048
6217
  return getWebsiteMarketingAttribution(options.websiteId);
6049
6218
  },
6050
6219
  getAttribution() {
6051
- if (!enabled) return null;
6220
+ if (!enabled || destroyed) return null;
6052
6221
  if (options.config.destinations.meta) {
6053
6222
  refreshWebsiteMarketingMetaContext(options.websiteId);
6054
6223
  }
@@ -0,0 +1,30 @@
1
+ import type { WebsiteMarketingBrowserEvent, WebsiteMarketingRuntimeOptions } from "./types";
2
+ type TrackingFunction = (...args: unknown[]) => void;
3
+ type MetaTrackingFunction = TrackingFunction & {
4
+ callMethod?: TrackingFunction;
5
+ push?: TrackingFunction;
6
+ loaded?: boolean;
7
+ version?: string;
8
+ queue?: unknown[];
9
+ };
10
+ type MarketingWindow = Window & {
11
+ dataLayer?: unknown[];
12
+ gtag?: TrackingFunction;
13
+ fbq?: MetaTrackingFunction;
14
+ _fbq?: MetaTrackingFunction;
15
+ requestIdleCallback?: (callback: () => void, options?: {
16
+ timeout: number;
17
+ }) => number;
18
+ cancelIdleCallback?: (handle: number) => void;
19
+ };
20
+ export declare function marketingWindow(): MarketingWindow | null;
21
+ export declare function prepareGa4(measurementId: string): void;
22
+ export declare function loadGa4Script(measurementId: string, onError: () => void): void;
23
+ export declare function captureGa4ClientId(measurementId: string, websiteId: string, diagnostic?: WebsiteMarketingRuntimeOptions["diagnostic"]): void;
24
+ export declare function captureGa4SessionId(measurementId: string, websiteId: string, diagnostic?: WebsiteMarketingRuntimeOptions["diagnostic"]): void;
25
+ export declare function prepareMeta(pixelId: string): void;
26
+ export declare function loadMetaScript(onError: () => void): void;
27
+ export declare function scheduleScriptLoad(load: () => void, strategy: WebsiteMarketingRuntimeOptions["scriptLoadStrategy"]): () => void;
28
+ export declare function sendGa4(event: WebsiteMarketingBrowserEvent, diagnosticMode: boolean, measurementId: string): void;
29
+ export declare function sendMeta(event: WebsiteMarketingBrowserEvent, pixelId: string): void;
30
+ export {};
@@ -1,3 +1,3 @@
1
1
  export declare function prepareClarity(projectId: string): void;
2
- export declare function loadClarityScript(projectId: string): void;
2
+ export declare function loadClarityScript(projectId: string, onError?: () => void): void;
3
3
  export declare function sendClarityEvent(eventName: string): void;
@@ -0,0 +1,12 @@
1
+ import type { WebsiteFormResponseResult } from "../types";
2
+ import { type WebsiteFormErrorCategory } from "./interactions";
3
+ import type { WebsiteMarketingRuntime } from "./types";
4
+ /** One instance per mounted form. No answers, storage or provider listeners. */
5
+ export declare function createFormInteractionTracker(getRuntime: () => WebsiteMarketingRuntime, formKey: string): {
6
+ setVisible(next: boolean): void;
7
+ start(): void;
8
+ stepComplete(stepKey: string): void;
9
+ beginSubmit(): boolean;
10
+ fail(errorCategory: WebsiteFormErrorCategory): void;
11
+ complete(result: WebsiteFormResponseResult | void): void;
12
+ };
@@ -1,4 +1,6 @@
1
1
  export { captureWebsiteMarketingAttribution, getWebsiteMarketingAttribution, serializeWebsiteMarketingAttribution, } from "./attribution";
2
+ export type { WebsiteFormErrorCategory, WebsiteInteractionContext, WebsiteInteractionEventName, } from "./interactions";
3
+ export { WEBSITE_INTERACTION_EVENTS } from "./interactions";
2
4
  export { createWebsiteMarketingRuntime } from "./runtime";
3
5
  export { getAllowedMarketingParameters, sanitizeMarketingUrl, } from "./sanitize";
4
6
  export type { WebsiteAdvertisingConsent, WebsiteMarketingAttribution, WebsiteMarketingBrowserEvent, WebsiteMarketingBrowserEventName, WebsiteMarketingContactPlacement, WebsiteMarketingDiagnostic, WebsiteMarketingItem, WebsiteMarketingRuntime, WebsiteMarketingRuntimeOptions, WebsiteMarketingScriptLoadStrategy, WebsiteMarketingTouchpoint, } from "./types";
@@ -0,0 +1,19 @@
1
+ import type { WebsiteMarketingBrowserEvent } from "./types";
2
+ export declare const WEBSITE_INTERACTION_EVENTS: readonly ["cta_click", "form_open", "form_start", "form_step_complete", "form_submit_attempt", "form_submit_error", "form_submit_success"];
3
+ export type WebsiteInteractionEventName = (typeof WEBSITE_INTERACTION_EVENTS)[number];
4
+ export type WebsiteFormErrorCategory = "validation" | "network" | "rejected" | "unavailable";
5
+ export interface WebsiteInteractionContext {
6
+ /** Public definition key, never a field value. */
7
+ formKey?: string;
8
+ /** Stable technical identifiers (1–64 characters), not button text. */
9
+ ctaId?: string;
10
+ placement?: string;
11
+ stepKey?: string;
12
+ errorCategory?: WebsiteFormErrorCategory;
13
+ }
14
+ export declare function isInteractionEvent(name: string): name is WebsiteInteractionEventName;
15
+ export declare function isInteractionIdentifier(value: unknown): value is string;
16
+ export declare function isBrowserMarketingEvent(name: string): boolean;
17
+ /** Allowlisted projection: never spread caller fields into analytics payloads. */
18
+ export declare function interactionParameters(event: WebsiteMarketingBrowserEvent): Record<string, string> | null;
19
+ export declare function newMarketingEventId(): string;
@@ -1,6 +1,7 @@
1
1
  import type { WebsiteMarketingPublicConfig } from "../types";
2
+ import type { WebsiteInteractionContext, WebsiteInteractionEventName } from "./interactions";
2
3
  export declare const MARKETING_SCHEMA_VERSION: 1;
3
- export type WebsiteMarketingBrowserEventName = "page_view" | "search" | "view_item_list" | "select_item" | "view_item" | "add_to_cart" | "remove_from_cart" | "view_cart" | "begin_checkout" | "add_shipping_info" | "add_payment_info" | "contact";
4
+ export type WebsiteMarketingBrowserEventName = WebsiteInteractionEventName | "page_view" | "search" | "view_item_list" | "select_item" | "view_item" | "add_to_cart" | "remove_from_cart" | "view_cart" | "begin_checkout" | "add_shipping_info" | "add_payment_info" | "contact";
4
5
  export declare const WEBSITE_MARKETING_CONTACT_PLACEMENTS: readonly ["product_detail", "floating", "header", "footer", "contact_page"];
5
6
  export type WebsiteMarketingContactPlacement = (typeof WEBSITE_MARKETING_CONTACT_PLACEMENTS)[number];
6
7
  export interface WebsiteMarketingItem {
@@ -15,7 +16,7 @@ export interface WebsiteMarketingItem {
15
16
  itemVariant?: string | null;
16
17
  index?: number;
17
18
  }
18
- export interface WebsiteMarketingBrowserEvent {
19
+ export interface WebsiteMarketingBrowserEvent extends WebsiteInteractionContext {
19
20
  schemaVersion: typeof MARKETING_SCHEMA_VERSION;
20
21
  eventId: string;
21
22
  eventName: WebsiteMarketingBrowserEventName;
@@ -88,6 +89,8 @@ export type WebsiteMarketingDiagnostic = {
88
89
  };
89
90
  export type WebsiteMarketingScriptLoadStrategy = "immediate" | "idle";
90
91
  export interface WebsiteMarketingRuntimeOptions {
92
+ /** Host consent integration: false prevents scripts, events and attribution. */
93
+ measurementAllowed?: boolean;
91
94
  websiteId: string;
92
95
  config: WebsiteMarketingPublicConfig;
93
96
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacano/sdk",
3
- "version": "1.1.20",
3
+ "version": "1.2.1",
4
4
  "description": "TypeScript SDK for building websites on the Bacano ERP",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -0,0 +1,5 @@
1
+ import type { WebsiteInteractionContext } from "../marketing/interactions";
2
+ /** Invoke inside the existing onClick; never cancels or delays navigation. */
3
+ export declare function useBacanoCta(context: Pick<WebsiteInteractionContext, "formKey" | "placement"> & {
4
+ ctaId: string;
5
+ }): () => void;
@@ -0,0 +1,16 @@
1
+ import type { WebsiteFormField, WebsiteFormSection } from "../types";
2
+ import type { BacanoFormClassNames, BacanoFormValues } from "./forms";
3
+ export declare function FormFields(props: {
4
+ fields: WebsiteFormField[];
5
+ idPrefix: string;
6
+ values: BacanoFormValues;
7
+ classNames?: BacanoFormClassNames;
8
+ onChange(key: string, value: string | string[] | boolean): void;
9
+ }): import("react").JSX.Element[];
10
+ export declare function FormSections(props: {
11
+ sections: WebsiteFormSection[];
12
+ idPrefix: string;
13
+ values: BacanoFormValues;
14
+ classNames?: BacanoFormClassNames;
15
+ onChange(key: string, value: string | string[] | boolean): void;
16
+ }): import("react").JSX.Element[];
@@ -0,0 +1,11 @@
1
+ /** Use with custom renderers; BacanoForm already owns this instrumentation. */
2
+ export declare function useBacanoFormTracking(formKey: string, options?: {
3
+ visible?: boolean;
4
+ }): {
5
+ setVisible(next: boolean): void;
6
+ start(): void;
7
+ stepComplete(stepKey: string): void;
8
+ beginSubmit(): boolean;
9
+ fail(errorCategory: import("../marketing").WebsiteFormErrorCategory): void;
10
+ complete(result: import("..").WebsiteFormResponseResult | void): void;
11
+ };