@shoppexio/storefront 1.0.102 → 1.0.103

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/dist/index.js CHANGED
@@ -273,27 +273,9 @@ function buildEndpoint(template, params) {
273
273
  }
274
274
 
275
275
  // ../sdk/src/core/telemetry.ts
276
- var CONNECTION_ID_STORAGE_PREFIX = "presence_connection_";
277
276
  var CLIENT_ERROR_DEDUPE_WINDOW_MS = 15e3;
278
277
  var CLIENT_ERROR_TEXT_MAX_LENGTH = 500;
279
278
  var CLIENT_ERROR_URL_MAX_LENGTH = 2e3;
280
- function createPresenceConnectionId() {
281
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
282
- return crypto.randomUUID();
283
- }
284
- return `spx_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
285
- }
286
- function getStorefrontConnectionId(storeSlug) {
287
- if (typeof window === "undefined") return null;
288
- const storageKey2 = `${CONNECTION_ID_STORAGE_PREFIX}${storeSlug}`;
289
- const existing = getItem(storageKey2);
290
- if (existing && existing.trim()) {
291
- return existing;
292
- }
293
- const nextId = createPresenceConnectionId();
294
- setItem(storageKey2, nextId);
295
- return nextId;
296
- }
297
279
  function getRecentClientErrorKey(payload) {
298
280
  return [
299
281
  payload.method,
@@ -323,7 +305,6 @@ async function reportStorefrontClientError(payload) {
323
305
  const body = JSON.stringify({
324
306
  event_type: "client_error",
325
307
  referer: document.referrer || void 0,
326
- connection_id: getStorefrontConnectionId(config.storeSlug) ?? void 0,
327
308
  client_error: {
328
309
  source: payload.source ?? "sdk",
329
310
  phase: payload.phase ?? "request",
@@ -1508,6 +1489,138 @@ async function quoteCart(coupon, currency) {
1508
1489
  return response;
1509
1490
  }
1510
1491
 
1492
+ // ../contracts/src/storefront-consent-choice.ts
1493
+ var COOKIE_CONSENT_VERSION = 1;
1494
+ var COOKIE_CONSENT_COOKIE = "shoppex_consent";
1495
+ var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
1496
+ var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
1497
+ function parseCookieConsentValue(value) {
1498
+ const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
1499
+ return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
1500
+ }
1501
+
1502
+ // ../sdk/src/core/analytics-session.ts
1503
+ var ANALYTICS_SESSION_IDLE_MS = 30 * 60 * 1e3;
1504
+ var ANALYTICS_VISITS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
1505
+ var ANALYTICS_VISITS_MAX = 20;
1506
+ var MAX_ENGAGED_MS_PER_PAGE = 30 * 60 * 1e3;
1507
+ var analyticsSessionKey = (storeSlug) => `analytics_session_${storeSlug}`;
1508
+ var analyticsVisitsKey = (storeSlug) => `analytics_visits_${storeSlug}`;
1509
+ function readAnalyticsConsent() {
1510
+ if (typeof document === "undefined" || typeof navigator === "undefined") return "unknown";
1511
+ const privacy = navigator;
1512
+ if (privacy.globalPrivacyControl === true || privacy.doNotTrack === "1") return "declined";
1513
+ const prefix = `${COOKIE_CONSENT_COOKIE}=`;
1514
+ let raw;
1515
+ try {
1516
+ raw = document.cookie.split(/;\s*/).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1517
+ } catch {
1518
+ return "unknown";
1519
+ }
1520
+ const choice = parseCookieConsentValue(raw ?? null);
1521
+ if (!choice) return "unknown";
1522
+ return choice.analytics ? "granted" : "declined";
1523
+ }
1524
+ function readVisits(storeSlug, now) {
1525
+ const stored = getItem(analyticsVisitsKey(storeSlug));
1526
+ if (!Array.isArray(stored)) return [];
1527
+ return stored.filter((visit) => typeof visit?.id === "string" && typeof visit.at === "number" && now - visit.at < ANALYTICS_VISITS_WINDOW_MS);
1528
+ }
1529
+ function forgetAnalyticsData(storeSlug) {
1530
+ removeItem(analyticsSessionKey(storeSlug), "sessionStorage");
1531
+ removeItem(analyticsVisitsKey(storeSlug));
1532
+ }
1533
+ function analyticsSessionId(storeSlug, options, now = Date.now()) {
1534
+ removeItem(`presence_connection_${storeSlug}`);
1535
+ if (readAnalyticsConsent() !== "granted") {
1536
+ forgetAnalyticsData(storeSlug);
1537
+ return null;
1538
+ }
1539
+ const stored = getItem(analyticsSessionKey(storeSlug), "sessionStorage");
1540
+ const live = stored && typeof stored.id === "string" && typeof stored.at === "number" && now - stored.at < ANALYTICS_SESSION_IDLE_MS ? stored.id : null;
1541
+ if (!options.touch) return live;
1542
+ if (live) {
1543
+ setItem(analyticsSessionKey(storeSlug), { id: live, at: now }, "sessionStorage");
1544
+ return live;
1545
+ }
1546
+ if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") return null;
1547
+ const id = crypto.randomUUID();
1548
+ setItem(analyticsSessionKey(storeSlug), { id, at: now }, "sessionStorage");
1549
+ setItem(
1550
+ analyticsVisitsKey(storeSlug),
1551
+ [{ id, at: now }, ...readVisits(storeSlug, now)].slice(0, ANALYTICS_VISITS_MAX)
1552
+ );
1553
+ return id;
1554
+ }
1555
+ function checkoutAttribution(storeSlug, now = Date.now()) {
1556
+ const consent = readAnalyticsConsent();
1557
+ if (consent === "declined") {
1558
+ forgetAnalyticsData(storeSlug);
1559
+ return { declined: true };
1560
+ }
1561
+ if (consent === "unknown") return void 0;
1562
+ const sessionIds = readVisits(storeSlug, now).map((visit) => visit.id);
1563
+ return sessionIds.length > 0 ? { session_ids: sessionIds } : void 0;
1564
+ }
1565
+ var engagedMs = 0;
1566
+ var visibleSince = null;
1567
+ var clockInstalled = false;
1568
+ var pageViewed = false;
1569
+ var pageSession = null;
1570
+ function markPageViewed(sessionId) {
1571
+ pageViewed = true;
1572
+ pageSession = sessionId;
1573
+ }
1574
+ function currentPageSession() {
1575
+ return pageSession;
1576
+ }
1577
+ function isPageViewed() {
1578
+ return pageViewed;
1579
+ }
1580
+ function settleClock(now) {
1581
+ if (visibleSince !== null) {
1582
+ engagedMs += now - visibleSince;
1583
+ visibleSince = document.visibilityState === "visible" ? now : null;
1584
+ }
1585
+ }
1586
+ var consentListenerInstalled = false;
1587
+ function onAnalyticsConsent(onYes) {
1588
+ if (consentListenerInstalled || typeof window === "undefined") return;
1589
+ consentListenerInstalled = true;
1590
+ window.addEventListener("shoppex:consent", () => {
1591
+ if (readAnalyticsConsent() === "granted") onYes();
1592
+ });
1593
+ }
1594
+ function installEngagedClock(onHidden) {
1595
+ if (clockInstalled || typeof document === "undefined") return;
1596
+ clockInstalled = true;
1597
+ visibleSince = document.visibilityState === "visible" ? Date.now() : null;
1598
+ document.addEventListener("visibilitychange", () => {
1599
+ const now = Date.now();
1600
+ if (document.visibilityState === "visible") {
1601
+ visibleSince ?? (visibleSince = now);
1602
+ return;
1603
+ }
1604
+ settleClock(now);
1605
+ visibleSince = null;
1606
+ onHidden();
1607
+ });
1608
+ window.addEventListener("pagehide", () => {
1609
+ settleClock(Date.now());
1610
+ visibleSince = null;
1611
+ onHidden();
1612
+ });
1613
+ window.addEventListener("pageshow", (event) => {
1614
+ if (event.persisted && document.visibilityState === "visible") visibleSince = Date.now();
1615
+ });
1616
+ }
1617
+ function takeEngagedMs(now = Date.now()) {
1618
+ settleClock(now);
1619
+ const taken = Math.min(Math.max(0, Math.round(engagedMs)), MAX_ENGAGED_MS_PER_PAGE);
1620
+ engagedMs = 0;
1621
+ return taken;
1622
+ }
1623
+
1511
1624
  // ../sdk/src/core/attribution.ts
1512
1625
  var STORAGE_PREFIX2 = "shoppex:utm:";
1513
1626
  var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -1566,6 +1679,13 @@ function resolveUtmParameters(storeSlug) {
1566
1679
  return void 0;
1567
1680
  }
1568
1681
  const fromUrl = readFromUrl(window.location.search);
1682
+ if (readAnalyticsConsent() !== "granted") {
1683
+ try {
1684
+ window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1685
+ } catch {
1686
+ }
1687
+ return Object.keys(fromUrl).length > 0 ? fromUrl : void 0;
1688
+ }
1569
1689
  if (Object.keys(fromUrl).length > 0) {
1570
1690
  writeStored(storeSlug, fromUrl);
1571
1691
  return fromUrl;
@@ -1732,13 +1852,21 @@ async function trackPageView(cartValue, itemCount) {
1732
1852
  const sendView = async () => {
1733
1853
  const order2 = reservedOrder ? await reservedOrder : void 0;
1734
1854
  const settle = order2 ? claimCartReport(config.storeSlug, order2.seq, cartValue, itemCount) : void 0;
1855
+ const sessionId = analyticsSessionId(config.storeSlug, { touch: true });
1856
+ const previousSession = currentPageSession();
1857
+ if (previousSession && previousSession !== sessionId) reportPageLeave();
1858
+ markPageViewed(sessionId);
1859
+ if (sessionId) installEngagedClock(reportPageLeave);
1860
+ onAnalyticsConsent(startSessionOnConsent);
1735
1861
  const delivered = await postPing({
1736
1862
  referer: document.referrer || void 0,
1737
1863
  cart_value: cartValue,
1738
1864
  item_count: itemCount,
1739
1865
  cart_seq: order2?.seq,
1740
1866
  cart_seq_scope: order2?.scope,
1741
- connection_id: getStorefrontConnectionId(config.storeSlug) ?? void 0,
1867
+ session_id: sessionId ?? void 0,
1868
+ // Time on screen of the page before this one (client-side navigation).
1869
+ engaged_ms: sessionId ? takeEngagedMs() || void 0 : void 0,
1742
1870
  path: resolveStorefrontPagePath(),
1743
1871
  utm: resolveUtmParameters(config.storeSlug)
1744
1872
  });
@@ -1766,11 +1894,44 @@ async function sendCartActivity(order2, cartValue, itemCount) {
1766
1894
  item_count: itemCount,
1767
1895
  cart_seq: order2.seq,
1768
1896
  cart_seq_scope: order2.scope,
1769
- connection_id: getStorefrontConnectionId(config.storeSlug) ?? void 0,
1897
+ // A cart marks the analytics session as one that did not bounce.
1898
+ session_id: analyticsSessionId(config.storeSlug, { touch: false }) ?? void 0,
1770
1899
  path: resolveStorefrontPagePath()
1771
1900
  });
1772
1901
  settle(delivered);
1773
1902
  }
1903
+ function startSessionOnConsent() {
1904
+ const config = getConfig();
1905
+ if (!isPageViewed() || analyticsSessionId(config.storeSlug, { touch: false })) return;
1906
+ const sessionId = analyticsSessionId(config.storeSlug, { touch: true });
1907
+ if (!sessionId) return;
1908
+ markPageViewed(sessionId);
1909
+ installEngagedClock(reportPageLeave);
1910
+ void postPing({
1911
+ event_type: "session_start",
1912
+ referer: document.referrer || void 0,
1913
+ session_id: sessionId,
1914
+ path: resolveStorefrontPagePath(),
1915
+ utm: resolveUtmParameters(config.storeSlug)
1916
+ });
1917
+ }
1918
+ function reportPageLeave() {
1919
+ const config = getConfig();
1920
+ const sessionId = currentPageSession();
1921
+ const engagedMs2 = takeEngagedMs();
1922
+ if (!sessionId || engagedMs2 === 0 || readAnalyticsConsent() !== "granted") return;
1923
+ try {
1924
+ const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", { storeSlug: config.storeSlug });
1925
+ void fetch(`${config.apiBaseUrl}${endpoint}`, {
1926
+ method: "POST",
1927
+ headers: { "Content-Type": "application/json" },
1928
+ body: JSON.stringify({ event_type: "page_leave", session_id: sessionId, engaged_ms: engagedMs2 }),
1929
+ keepalive: true
1930
+ }).catch(() => {
1931
+ });
1932
+ } catch {
1933
+ }
1934
+ }
1774
1935
 
1775
1936
  // ../sdk/src/storefront-storage-sdk.ts
1776
1937
  function init(storeSlugOrOptions, options) {
@@ -2663,16 +2824,6 @@ function isSafeHttpsRedirectTemplateUrl(url) {
2663
2824
  }
2664
2825
  }
2665
2826
 
2666
- // ../contracts/src/storefront-consent-choice.ts
2667
- var COOKIE_CONSENT_VERSION = 1;
2668
- var COOKIE_CONSENT_COOKIE = "shoppex_consent";
2669
- var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
2670
- var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
2671
- function parseCookieConsentValue(value) {
2672
- const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
2673
- return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
2674
- }
2675
-
2676
2827
  // ../sdk/src/modules/checkout.ts
2677
2828
  var CheckoutCreateError = class _CheckoutCreateError extends Error {
2678
2829
  constructor(message, options = {}) {
@@ -2971,7 +3122,10 @@ async function performCheckout(resolvedOptions) {
2971
3122
  // automatically so the server can refuse an invoice priced above it.
2972
3123
  // Undefined when nothing has been quoted this session — the endpoint
2973
3124
  // treats that exactly as an older SDK.
2974
- quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0
3125
+ quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0,
3126
+ // Where the buyer came from: their analytics sessions after a yes on the
3127
+ // cookie banner, `declined` after a no. The server looks up the sources.
3128
+ attribution: checkoutAttribution(config.storeSlug)
2975
3129
  };
2976
3130
  const createAttempt = acquireCheckoutCreateIdempotency(checkoutRequestTarget, createIntent);
2977
3131
  const response = await post(
@@ -3820,7 +3974,6 @@ async function touchStorefrontPresence() {
3820
3974
  {
3821
3975
  event_type: "presence",
3822
3976
  referer: document.referrer || void 0,
3823
- connection_id: getStorefrontConnectionId(config.storeSlug) ?? void 0,
3824
3977
  path: resolveStorefrontPagePath(),
3825
3978
  utm: resolveUtmParameters(config.storeSlug)
3826
3979
  },