@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/index.js CHANGED
@@ -337,7 +337,8 @@ async function getWebsitePublicConfig(options) {
337
337
  pixelId: marketing.destinations.meta.pixel_id,
338
338
  diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
339
339
  } : null,
340
- clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
340
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null,
341
+ ...marketing.destinations.gtm ? { gtm: { containerId: marketing.destinations.gtm.container_id } } : {}
341
342
  }
342
343
  },
343
344
  legal: {
@@ -2900,6 +2901,9 @@ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
2900
2901
  function sanitizeMarketingUrl(input, baseUrl) {
2901
2902
  try {
2902
2903
  const url = new URL(input, baseUrl);
2904
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "";
2905
+ url.username = "";
2906
+ url.password = "";
2903
2907
  const safeParameters = new URLSearchParams();
2904
2908
  for (const [key, value] of url.searchParams.entries()) {
2905
2909
  if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
@@ -5675,50 +5679,65 @@ function createContentModule(graphqlUrl, getHeaders, getContext, storageUrl) {
5675
5679
  };
5676
5680
  }
5677
5681
 
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;
5682
+ // libs/sdk/src/marketing/interactions.ts
5683
+ var WEBSITE_INTERACTION_EVENTS = [
5684
+ "cta_click",
5685
+ "form_open",
5686
+ "form_start",
5687
+ "form_step_complete",
5688
+ "form_submit_attempt",
5689
+ "form_submit_error",
5690
+ "form_submit_success"
5691
+ ];
5692
+ function isInteractionEvent(name) {
5693
+ return WEBSITE_INTERACTION_EVENTS.some((candidate) => candidate === name);
5694
+ }
5695
+ function isInteractionIdentifier(value) {
5696
+ return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value);
5697
+ }
5698
+ function isBrowserMarketingEvent(name) {
5699
+ return isInteractionEvent(name) || [
5700
+ "page_view",
5701
+ "search",
5702
+ "view_item_list",
5703
+ "select_item",
5704
+ "view_item",
5705
+ "add_to_cart",
5706
+ "remove_from_cart",
5707
+ "view_cart",
5708
+ "begin_checkout",
5709
+ "add_shipping_info",
5710
+ "add_payment_info",
5711
+ "contact"
5712
+ ].includes(name);
5713
+ }
5714
+ function interactionParameters(event) {
5715
+ if (!isInteractionEvent(event.eventName)) return {};
5716
+ if (event.eventName === "cta_click" ? !isInteractionIdentifier(event.ctaId) : !isInteractionIdentifier(event.formKey))
5717
+ return null;
5718
+ if (event.eventName === "form_step_complete" && !isInteractionIdentifier(event.stepKey))
5719
+ return null;
5720
+ if (event.eventName === "form_submit_error" && !["validation", "network", "rejected", "unavailable"].includes(
5721
+ event.errorCategory ?? ""
5722
+ ))
5723
+ return null;
5724
+ const params = {};
5725
+ for (const [key, value] of Object.entries({
5726
+ form_key: event.formKey,
5727
+ cta_id: event.ctaId,
5728
+ placement: event.placement,
5729
+ step_key: event.stepKey
5730
+ })) {
5731
+ if (value === void 0) continue;
5732
+ if (!isInteractionIdentifier(value)) return null;
5733
+ params[key] = value;
5692
5734
  }
5693
- initializedDestinations.add(projectId);
5694
- }
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);
5735
+ if (event.eventName === "form_submit_error" && event.errorCategory)
5736
+ params.error_category = event.errorCategory;
5737
+ return params;
5708
5738
  }
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
- }
5739
+ function newMarketingEventId() {
5740
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5722
5741
  }
5723
5742
 
5724
5743
  // libs/sdk/src/marketing/ga4.ts
@@ -5746,19 +5765,20 @@ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
5746
5765
  "contact_page"
5747
5766
  ];
5748
5767
 
5749
- // libs/sdk/src/marketing/runtime.ts
5768
+ // libs/sdk/src/marketing/browser-adapters.ts
5750
5769
  var initializedGa4Destinations = /* @__PURE__ */ new Set();
5751
5770
  var initializedMetaDestinations = /* @__PURE__ */ new Set();
5752
5771
  function marketingWindow() {
5753
5772
  return typeof window === "undefined" ? null : window;
5754
5773
  }
5755
- function loadScriptOnce(id, source) {
5774
+ function loadScriptOnce(id, source, onError) {
5756
5775
  if (typeof document === "undefined" || document.getElementById(id)) return;
5757
5776
  const script = document.createElement("script");
5758
5777
  script.id = id;
5759
5778
  script.async = true;
5760
5779
  script.src = source;
5761
5780
  script.referrerPolicy = "strict-origin-when-cross-origin";
5781
+ script.onerror = () => onError?.();
5762
5782
  document.head.appendChild(script);
5763
5783
  }
5764
5784
  function prepareGa4(measurementId) {
@@ -5773,10 +5793,11 @@ function prepareGa4(measurementId) {
5773
5793
  target.gtag("config", measurementId, { send_page_view: false });
5774
5794
  initializedGa4Destinations.add(measurementId);
5775
5795
  }
5776
- function loadGa4Script(measurementId) {
5796
+ function loadGa4Script(measurementId, onError) {
5777
5797
  loadScriptOnce(
5778
5798
  `bacano-ga4-${measurementId}`,
5779
- `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`
5799
+ `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`,
5800
+ onError
5780
5801
  );
5781
5802
  }
5782
5803
  function captureGa4ClientId(measurementId, websiteId, diagnostic) {
@@ -5824,10 +5845,11 @@ function prepareMeta(pixelId) {
5824
5845
  target.fbq("init", pixelId);
5825
5846
  initializedMetaDestinations.add(pixelId);
5826
5847
  }
5827
- function loadMetaScript() {
5848
+ function loadMetaScript(onError) {
5828
5849
  loadScriptOnce(
5829
5850
  "bacano-meta-pixel",
5830
- "https://connect.facebook.net/en_US/fbevents.js"
5851
+ "https://connect.facebook.net/en_US/fbevents.js",
5852
+ onError
5831
5853
  );
5832
5854
  }
5833
5855
  function scheduleScriptLoad(load, strategy) {
@@ -5891,10 +5913,12 @@ function contactPlacementFor(event) {
5891
5913
  }
5892
5914
  return placement;
5893
5915
  }
5894
- function sendGa4(event, diagnosticMode) {
5916
+ function sendGa4(event, diagnosticMode, measurementId) {
5895
5917
  const target = marketingWindow();
5896
5918
  const contactPlacement = contactPlacementFor(event);
5897
5919
  target?.gtag?.("event", event.eventName, {
5920
+ send_to: measurementId,
5921
+ ...interactionParameters(event),
5898
5922
  currency: event.currency,
5899
5923
  value: event.value,
5900
5924
  search_term: event.searchTerm,
@@ -5911,15 +5935,17 @@ function sendGa4(event, diagnosticMode) {
5911
5935
  debug_mode: diagnosticMode ? true : void 0
5912
5936
  });
5913
5937
  }
5914
- function sendMeta(event) {
5938
+ function sendMeta(event, pixelId) {
5915
5939
  const target = marketingWindow();
5916
5940
  const eventName = metaEventName(event.eventName);
5917
5941
  const isStandard = eventName !== event.eventName;
5918
5942
  const contactPlacement = contactPlacementFor(event);
5919
5943
  target?.fbq?.(
5920
- isStandard ? "track" : "trackCustom",
5944
+ isStandard ? "trackSingle" : "trackSingleCustom",
5945
+ pixelId,
5921
5946
  eventName,
5922
5947
  {
5948
+ ...interactionParameters(event),
5923
5949
  currency: event.currency,
5924
5950
  value: event.value,
5925
5951
  search_string: event.searchTerm,
@@ -5936,40 +5962,192 @@ function sendMeta(event) {
5936
5962
  { eventID: event.eventId }
5937
5963
  );
5938
5964
  }
5939
- function newEventId() {
5940
- return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
5965
+
5966
+ // libs/sdk/src/marketing/clarity.ts
5967
+ var initializedDestinations = /* @__PURE__ */ new Set();
5968
+ function clarityWindow() {
5969
+ return typeof window === "undefined" ? null : window;
5970
+ }
5971
+ function prepareClarity(projectId) {
5972
+ const target = clarityWindow();
5973
+ if (!target || initializedDestinations.has(projectId)) return;
5974
+ if (!target.clarity) {
5975
+ const clarity = function() {
5976
+ clarity.q ??= [];
5977
+ clarity.q.push(arguments);
5978
+ };
5979
+ target.clarity = clarity;
5980
+ }
5981
+ initializedDestinations.add(projectId);
5982
+ }
5983
+ function loadClarityScript(projectId, onError) {
5984
+ if (typeof document === "undefined") return;
5985
+ const id = `bacano-clarity-${projectId}`;
5986
+ if (document.getElementById(id)) return;
5987
+ const script = document.createElement("script");
5988
+ script.id = id;
5989
+ script.async = true;
5990
+ script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
5991
+ script.referrerPolicy = "strict-origin-when-cross-origin";
5992
+ script.onerror = () => onError?.();
5993
+ document.head.appendChild(script);
5994
+ }
5995
+ function sendClarityEvent(eventName) {
5996
+ clarityWindow()?.clarity?.("event", eventName);
5997
+ }
5998
+
5999
+ // libs/sdk/src/marketing/domain.ts
6000
+ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
6001
+ if (canonicalDomain === void 0) return true;
6002
+ if (!canonicalDomain?.trim()) return false;
6003
+ try {
6004
+ const configuredHost = new URL(
6005
+ canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
6006
+ ).hostname.toLowerCase();
6007
+ return currentHostname.toLowerCase() === configuredHost;
6008
+ } catch {
6009
+ return false;
6010
+ }
6011
+ }
6012
+
6013
+ // libs/sdk/src/marketing/gtm.ts
6014
+ var CONTAINER_ID = /^GTM-[A-Z0-9]{4,32}$/;
6015
+ var preparedQueues = /* @__PURE__ */ new WeakMap();
6016
+ function prepareGtm(containerId) {
6017
+ const target = marketingWindow();
6018
+ if (!target || !CONTAINER_ID.test(containerId)) return;
6019
+ target.dataLayer ??= [];
6020
+ const containers = preparedQueues.get(target.dataLayer) ?? /* @__PURE__ */ new Set();
6021
+ if (containers.has(containerId)) return;
6022
+ target.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
6023
+ containers.add(containerId);
6024
+ preparedQueues.set(target.dataLayer, containers);
6025
+ }
6026
+ function loadGtmScript(containerId, onError) {
6027
+ const target = marketingWindow();
6028
+ if (!target || typeof document === "undefined" || !CONTAINER_ID.test(containerId))
6029
+ return;
6030
+ const id = `bacano-gtm-${containerId}`;
6031
+ if (document.getElementById(id)) return;
6032
+ prepareGtm(containerId);
6033
+ const script = document.createElement("script");
6034
+ script.id = id;
6035
+ script.async = true;
6036
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
6037
+ script.referrerPolicy = "strict-origin-when-cross-origin";
6038
+ script.onerror = () => onError();
6039
+ document.head.appendChild(script);
6040
+ }
6041
+ function sendGtm(event) {
6042
+ const target = marketingWindow();
6043
+ if (!target?.dataLayer) return;
6044
+ target.dataLayer.push({ bacano: null });
6045
+ target.dataLayer.push({
6046
+ event: `bacano_${event.eventName}`,
6047
+ bacano: {
6048
+ schema_version: event.schemaVersion,
6049
+ event_name: event.eventName,
6050
+ event_id: event.eventId,
6051
+ ...interactionParameters(event),
6052
+ page_location: event.pageLocation,
6053
+ currency: event.currency,
6054
+ value: event.value,
6055
+ items: mapGa4Items(event.items),
6056
+ item_list_id: event.itemListId,
6057
+ item_list_name: event.itemListName,
6058
+ shipping_tier: event.shippingTier,
6059
+ payment_type: event.paymentType,
6060
+ method: event.contactMethod,
6061
+ contact_placement: event.eventName === "contact" && WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
6062
+ (value) => value === event.contactPlacement
6063
+ ) ? event.contactPlacement : void 0
6064
+ }
6065
+ });
5941
6066
  }
6067
+
6068
+ // libs/sdk/src/marketing/runtime.ts
5942
6069
  function createWebsiteMarketingRuntime(options) {
5943
6070
  let destroyed = false;
6071
+ const dispatched = /* @__PURE__ */ new Set();
6072
+ const observer = options.diagnostic;
6073
+ const diagnostic = (value) => {
6074
+ try {
6075
+ observer?.(value);
6076
+ } catch {
6077
+ }
6078
+ };
6079
+ options = { ...options, diagnostic };
5944
6080
  let cancelScheduledLoad = () => void 0;
5945
6081
  const target = marketingWindow();
5946
- const enabled = options.config.enabled && Boolean(
6082
+ const enabled = options.config.enabled && options.measurementAllowed !== false && Boolean(
5947
6083
  target && matchesCanonicalMarketingHost(
5948
6084
  options.canonicalDomain,
5949
6085
  target.location.hostname
5950
6086
  )
5951
6087
  ) && Boolean(
5952
- options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
6088
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity || options.config.destinations.gtm
5953
6089
  );
6090
+ if (!enabled)
6091
+ diagnostic({
6092
+ level: "debug",
6093
+ code: !options.config.enabled ? "MARKETING_DISABLED" : options.measurementAllowed === false ? "MEASUREMENT_NOT_ALLOWED" : !target ? "SERVER_RENDER" : !matchesCanonicalMarketingHost(
6094
+ options.canonicalDomain,
6095
+ target.location.hostname
6096
+ ) ? "DOMAIN_NOT_ALLOWED" : "PROVIDER_NOT_CONFIGURED",
6097
+ message: "Measurement is inactive; no events are emitted"
6098
+ });
5954
6099
  if (enabled) {
6100
+ const guard = (provider, action) => {
6101
+ try {
6102
+ action();
6103
+ } catch {
6104
+ diagnostic({
6105
+ level: "warning",
6106
+ code: "RUNTIME_INITIALIZATION_FAILED",
6107
+ message: `${provider}: initialization failed`
6108
+ });
6109
+ }
6110
+ };
6111
+ const scriptError = (provider) => () => diagnostic({
6112
+ level: "warning",
6113
+ code: "SCRIPT_LOAD_FAILED",
6114
+ message: `${provider}: script could not load (network, CSP or blocker); external reception is unverified`
6115
+ });
5955
6116
  try {
5956
6117
  if (options.config.destinations.ga4) {
5957
- prepareGa4(options.config.destinations.ga4.measurementId);
6118
+ const { measurementId } = options.config.destinations.ga4;
6119
+ guard("GA4", () => prepareGa4(measurementId));
5958
6120
  }
5959
6121
  if (options.config.destinations.meta) {
5960
- prepareMeta(options.config.destinations.meta.pixelId);
6122
+ const { pixelId } = options.config.destinations.meta;
6123
+ guard("Meta", () => prepareMeta(pixelId));
5961
6124
  }
5962
6125
  if (options.config.destinations.clarity) {
5963
- prepareClarity(options.config.destinations.clarity.projectId);
6126
+ const { projectId } = options.config.destinations.clarity;
6127
+ guard("Clarity", () => prepareClarity(projectId));
6128
+ }
6129
+ if (options.config.destinations.gtm) {
6130
+ const { containerId } = options.config.destinations.gtm;
6131
+ guard("GTM", () => prepareGtm(containerId));
5964
6132
  }
5965
6133
  cancelScheduledLoad = scheduleScriptLoad(() => {
5966
6134
  if (destroyed) return;
5967
6135
  if (options.config.destinations.ga4) {
5968
- loadGa4Script(options.config.destinations.ga4.measurementId);
6136
+ const { measurementId } = options.config.destinations.ga4;
6137
+ guard("GA4", () => loadGa4Script(measurementId, scriptError("GA4")));
5969
6138
  }
5970
- if (options.config.destinations.meta) loadMetaScript();
6139
+ if (options.config.destinations.meta)
6140
+ guard("Meta", () => loadMetaScript(scriptError("Meta")));
5971
6141
  if (options.config.destinations.clarity) {
5972
- loadClarityScript(options.config.destinations.clarity.projectId);
6142
+ const { projectId } = options.config.destinations.clarity;
6143
+ guard(
6144
+ "Clarity",
6145
+ () => loadClarityScript(projectId, scriptError("Clarity"))
6146
+ );
6147
+ }
6148
+ if (options.config.destinations.gtm) {
6149
+ const { containerId } = options.config.destinations.gtm;
6150
+ guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
5973
6151
  }
5974
6152
  }, options.scriptLoadStrategy);
5975
6153
  captureWebsiteMarketingAttribution(
@@ -5991,11 +6169,11 @@ function createWebsiteMarketingRuntime(options) {
5991
6169
  if (options.config.destinations.meta) {
5992
6170
  refreshWebsiteMarketingMetaContext(options.websiteId);
5993
6171
  }
5994
- } catch (error) {
6172
+ } catch {
5995
6173
  options.diagnostic?.({
5996
6174
  level: "warning",
5997
6175
  code: "RUNTIME_INITIALIZATION_FAILED",
5998
- message: error instanceof Error ? error.message : "Initialization failed"
6176
+ message: "Marketing initialization failed; external reception is unverified"
5999
6177
  });
6000
6178
  }
6001
6179
  }
@@ -6003,41 +6181,99 @@ function createWebsiteMarketingRuntime(options) {
6003
6181
  enabled,
6004
6182
  track(input) {
6005
6183
  if (!enabled || destroyed) return;
6184
+ if (!isBrowserMarketingEvent(input.eventName)) {
6185
+ diagnostic({
6186
+ level: "warning",
6187
+ code: "INVALID_EVENT",
6188
+ message: "Unknown or server-owned event rejected"
6189
+ });
6190
+ return;
6191
+ }
6192
+ try {
6193
+ captureWebsiteMarketingAttribution(
6194
+ options.websiteId,
6195
+ options.config.attributionWindowDays
6196
+ );
6197
+ } catch {
6198
+ }
6006
6199
  const event = {
6007
6200
  ...input,
6008
6201
  schemaVersion: 1,
6009
- eventId: input.eventId ?? newEventId(),
6202
+ eventId: input.eventId ?? newMarketingEventId(),
6010
6203
  occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
6011
6204
  currency: input.currency ?? options.config.currencyCode,
6012
6205
  pageLocation: sanitizeMarketingUrl(
6013
6206
  input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
6014
6207
  )
6015
6208
  };
6016
- try {
6017
- if (options.config.destinations.ga4) {
6018
- sendGa4(event, options.config.destinations.ga4.diagnosticMode);
6209
+ if (isInteractionEvent(event.eventName)) {
6210
+ if (!interactionParameters(event) || !/^[a-zA-Z0-9_-]{1,100}$/.test(event.eventId)) {
6211
+ diagnostic({
6212
+ level: "warning",
6213
+ code: "INVALID_INTERACTION",
6214
+ message: "Interaction rejected by the public contract",
6215
+ eventName: event.eventName
6216
+ });
6217
+ return;
6019
6218
  }
6020
- if (options.config.destinations.meta) sendMeta(event);
6021
- if (options.config.destinations.clarity) {
6022
- sendClarityEvent(event.eventName);
6219
+ for (const key of [
6220
+ "currency",
6221
+ "value",
6222
+ "searchTerm",
6223
+ "itemListId",
6224
+ "itemListName",
6225
+ "shippingTier",
6226
+ "paymentType",
6227
+ "contactMethod",
6228
+ "contactPlacement",
6229
+ "items",
6230
+ "pageTitle"
6231
+ ])
6232
+ delete event[key];
6233
+ }
6234
+ const identity = `${event.eventName}:${event.eventId}`;
6235
+ if (dispatched.has(identity)) return;
6236
+ dispatched.add(identity);
6237
+ const oldest = dispatched.values().next().value;
6238
+ if (dispatched.size > 1e3 && oldest) dispatched.delete(oldest);
6239
+ const dispatch = (provider, send) => {
6240
+ try {
6241
+ send();
6242
+ diagnostic({
6243
+ level: "debug",
6244
+ code: "EVENT_DISPATCHED",
6245
+ message: `${provider}: passed to adapter; external reception is unverified`,
6246
+ eventName: event.eventName
6247
+ });
6248
+ } catch {
6249
+ diagnostic({
6250
+ level: "warning",
6251
+ code: "EVENT_DISPATCH_FAILED",
6252
+ message: `${provider}: adapter failed`,
6253
+ eventName: event.eventName
6254
+ });
6023
6255
  }
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
- });
6256
+ };
6257
+ if (options.config.destinations.ga4) {
6258
+ const ga4 = options.config.destinations.ga4;
6259
+ dispatch(
6260
+ "GA4",
6261
+ () => sendGa4(event, ga4.diagnosticMode, ga4.measurementId)
6262
+ );
6263
+ }
6264
+ if (options.config.destinations.meta) {
6265
+ const meta = options.config.destinations.meta;
6266
+ dispatch("Meta", () => sendMeta(event, meta.pixelId));
6267
+ }
6268
+ if (options.config.destinations.clarity) {
6269
+ dispatch("Clarity", () => sendClarityEvent(event.eventName));
6270
+ }
6271
+ if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
6272
+ dispatch("GTM", () => sendGtm(event));
6037
6273
  }
6038
6274
  },
6039
6275
  captureAttribution() {
6040
- if (!enabled) return null;
6276
+ if (!enabled || destroyed) return null;
6041
6277
  captureWebsiteMarketingAttribution(
6042
6278
  options.websiteId,
6043
6279
  options.config.attributionWindowDays
@@ -6048,7 +6284,7 @@ function createWebsiteMarketingRuntime(options) {
6048
6284
  return getWebsiteMarketingAttribution(options.websiteId);
6049
6285
  },
6050
6286
  getAttribution() {
6051
- if (!enabled) return null;
6287
+ if (!enabled || destroyed) return null;
6052
6288
  if (options.config.destinations.meta) {
6053
6289
  refreshWebsiteMarketingMetaContext(options.websiteId);
6054
6290
  }
@@ -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
+ };
@@ -0,0 +1,6 @@
1
+ import type { WebsiteMarketingBrowserEvent } from "./types";
2
+ /** Preserve the shared Google queue, including an existing native gtag.js. */
3
+ export declare function prepareGtm(containerId: string): void;
4
+ export declare function loadGtmScript(containerId: string, onError: () => void): void;
5
+ /** Explicit allowlist: never spread caller input, customer data or attribution. */
6
+ export declare function sendGtm(event: WebsiteMarketingBrowserEvent): void;
@@ -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
  /**