@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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.103
4
+
5
+ - Web analytics (PR #3989): after a yes to analytics on the cookie banner, the SDK keeps an analytics session (30 idle minutes) and the session ids of the last 30 days for checkout, so an order keeps the visit it came from. A yes given without a reload starts the session on the page already counted (`session_start`, no second page view). Time on screen goes to the session the page was viewed in, also after that session went idle.
6
+
3
7
  ## 1.0.102
4
8
 
5
9
  - The checkout URL carries the buyer's cookie banner answer in its hash (`#shoppex_consent=…`, PR #3938), so the full hosted checkout page sends the shop's Purchase tags under the same consent. The hash never reaches a server; without an answer on a shop with the banner on, the checkout sends no tags.
package/README.md CHANGED
@@ -107,8 +107,8 @@ if (firstOrder) {
107
107
  }
108
108
  ```
109
109
 
110
- The publishable key and current browser origin must be configured under
111
- **Settings -> Developer -> Headless**. `rememberMe` is opt-in and keeps
110
+ The publishable key comes from **Settings -> Developer -> Headless**; the
111
+ current browser origin must be an allowed site in **Store -> Embed Hub**. `rememberMe` is opt-in and keeps
112
112
  the buyer signed in for up to 30 days.
113
113
 
114
114
  The order detail includes delivery status, service instructions, codes, serials,
package/dist/index.cjs CHANGED
@@ -399,27 +399,9 @@ function buildEndpoint(template, params) {
399
399
  }
400
400
 
401
401
  // ../sdk/src/core/telemetry.ts
402
- var CONNECTION_ID_STORAGE_PREFIX = "presence_connection_";
403
402
  var CLIENT_ERROR_DEDUPE_WINDOW_MS = 15e3;
404
403
  var CLIENT_ERROR_TEXT_MAX_LENGTH = 500;
405
404
  var CLIENT_ERROR_URL_MAX_LENGTH = 2e3;
406
- function createPresenceConnectionId() {
407
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
408
- return crypto.randomUUID();
409
- }
410
- return `spx_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
411
- }
412
- function getStorefrontConnectionId(storeSlug) {
413
- if (typeof window === "undefined") return null;
414
- const storageKey2 = `${CONNECTION_ID_STORAGE_PREFIX}${storeSlug}`;
415
- const existing = getItem(storageKey2);
416
- if (existing && existing.trim()) {
417
- return existing;
418
- }
419
- const nextId = createPresenceConnectionId();
420
- setItem(storageKey2, nextId);
421
- return nextId;
422
- }
423
405
  function getRecentClientErrorKey(payload) {
424
406
  return [
425
407
  payload.method,
@@ -449,7 +431,6 @@ async function reportStorefrontClientError(payload) {
449
431
  const body = JSON.stringify({
450
432
  event_type: "client_error",
451
433
  referer: document.referrer || void 0,
452
- connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
453
434
  client_error: {
454
435
  source: payload.source ?? "sdk",
455
436
  phase: payload.phase ?? "request",
@@ -1634,6 +1615,138 @@ async function quoteCart(coupon, currency) {
1634
1615
  return response;
1635
1616
  }
1636
1617
 
1618
+ // ../contracts/src/storefront-consent-choice.ts
1619
+ var COOKIE_CONSENT_VERSION = 1;
1620
+ var COOKIE_CONSENT_COOKIE = "shoppex_consent";
1621
+ var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
1622
+ var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
1623
+ function parseCookieConsentValue(value) {
1624
+ const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
1625
+ return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
1626
+ }
1627
+
1628
+ // ../sdk/src/core/analytics-session.ts
1629
+ var ANALYTICS_SESSION_IDLE_MS = 30 * 60 * 1e3;
1630
+ var ANALYTICS_VISITS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
1631
+ var ANALYTICS_VISITS_MAX = 20;
1632
+ var MAX_ENGAGED_MS_PER_PAGE = 30 * 60 * 1e3;
1633
+ var analyticsSessionKey = (storeSlug) => `analytics_session_${storeSlug}`;
1634
+ var analyticsVisitsKey = (storeSlug) => `analytics_visits_${storeSlug}`;
1635
+ function readAnalyticsConsent() {
1636
+ if (typeof document === "undefined" || typeof navigator === "undefined") return "unknown";
1637
+ const privacy = navigator;
1638
+ if (privacy.globalPrivacyControl === true || privacy.doNotTrack === "1") return "declined";
1639
+ const prefix = `${COOKIE_CONSENT_COOKIE}=`;
1640
+ let raw;
1641
+ try {
1642
+ raw = document.cookie.split(/;\s*/).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1643
+ } catch {
1644
+ return "unknown";
1645
+ }
1646
+ const choice = parseCookieConsentValue(raw ?? null);
1647
+ if (!choice) return "unknown";
1648
+ return choice.analytics ? "granted" : "declined";
1649
+ }
1650
+ function readVisits(storeSlug, now) {
1651
+ const stored = getItem(analyticsVisitsKey(storeSlug));
1652
+ if (!Array.isArray(stored)) return [];
1653
+ return stored.filter((visit2) => typeof visit2?.id === "string" && typeof visit2.at === "number" && now - visit2.at < ANALYTICS_VISITS_WINDOW_MS);
1654
+ }
1655
+ function forgetAnalyticsData(storeSlug) {
1656
+ removeItem(analyticsSessionKey(storeSlug), "sessionStorage");
1657
+ removeItem(analyticsVisitsKey(storeSlug));
1658
+ }
1659
+ function analyticsSessionId(storeSlug, options, now = Date.now()) {
1660
+ removeItem(`presence_connection_${storeSlug}`);
1661
+ if (readAnalyticsConsent() !== "granted") {
1662
+ forgetAnalyticsData(storeSlug);
1663
+ return null;
1664
+ }
1665
+ const stored = getItem(analyticsSessionKey(storeSlug), "sessionStorage");
1666
+ const live = stored && typeof stored.id === "string" && typeof stored.at === "number" && now - stored.at < ANALYTICS_SESSION_IDLE_MS ? stored.id : null;
1667
+ if (!options.touch) return live;
1668
+ if (live) {
1669
+ setItem(analyticsSessionKey(storeSlug), { id: live, at: now }, "sessionStorage");
1670
+ return live;
1671
+ }
1672
+ if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") return null;
1673
+ const id = crypto.randomUUID();
1674
+ setItem(analyticsSessionKey(storeSlug), { id, at: now }, "sessionStorage");
1675
+ setItem(
1676
+ analyticsVisitsKey(storeSlug),
1677
+ [{ id, at: now }, ...readVisits(storeSlug, now)].slice(0, ANALYTICS_VISITS_MAX)
1678
+ );
1679
+ return id;
1680
+ }
1681
+ function checkoutAttribution(storeSlug, now = Date.now()) {
1682
+ const consent = readAnalyticsConsent();
1683
+ if (consent === "declined") {
1684
+ forgetAnalyticsData(storeSlug);
1685
+ return { declined: true };
1686
+ }
1687
+ if (consent === "unknown") return void 0;
1688
+ const sessionIds = readVisits(storeSlug, now).map((visit2) => visit2.id);
1689
+ return sessionIds.length > 0 ? { session_ids: sessionIds } : void 0;
1690
+ }
1691
+ var engagedMs = 0;
1692
+ var visibleSince = null;
1693
+ var clockInstalled = false;
1694
+ var pageViewed = false;
1695
+ var pageSession = null;
1696
+ function markPageViewed(sessionId) {
1697
+ pageViewed = true;
1698
+ pageSession = sessionId;
1699
+ }
1700
+ function currentPageSession() {
1701
+ return pageSession;
1702
+ }
1703
+ function isPageViewed() {
1704
+ return pageViewed;
1705
+ }
1706
+ function settleClock(now) {
1707
+ if (visibleSince !== null) {
1708
+ engagedMs += now - visibleSince;
1709
+ visibleSince = document.visibilityState === "visible" ? now : null;
1710
+ }
1711
+ }
1712
+ var consentListenerInstalled = false;
1713
+ function onAnalyticsConsent(onYes) {
1714
+ if (consentListenerInstalled || typeof window === "undefined") return;
1715
+ consentListenerInstalled = true;
1716
+ window.addEventListener("shoppex:consent", () => {
1717
+ if (readAnalyticsConsent() === "granted") onYes();
1718
+ });
1719
+ }
1720
+ function installEngagedClock(onHidden) {
1721
+ if (clockInstalled || typeof document === "undefined") return;
1722
+ clockInstalled = true;
1723
+ visibleSince = document.visibilityState === "visible" ? Date.now() : null;
1724
+ document.addEventListener("visibilitychange", () => {
1725
+ const now = Date.now();
1726
+ if (document.visibilityState === "visible") {
1727
+ visibleSince ?? (visibleSince = now);
1728
+ return;
1729
+ }
1730
+ settleClock(now);
1731
+ visibleSince = null;
1732
+ onHidden();
1733
+ });
1734
+ window.addEventListener("pagehide", () => {
1735
+ settleClock(Date.now());
1736
+ visibleSince = null;
1737
+ onHidden();
1738
+ });
1739
+ window.addEventListener("pageshow", (event) => {
1740
+ if (event.persisted && document.visibilityState === "visible") visibleSince = Date.now();
1741
+ });
1742
+ }
1743
+ function takeEngagedMs(now = Date.now()) {
1744
+ settleClock(now);
1745
+ const taken = Math.min(Math.max(0, Math.round(engagedMs)), MAX_ENGAGED_MS_PER_PAGE);
1746
+ engagedMs = 0;
1747
+ return taken;
1748
+ }
1749
+
1637
1750
  // ../sdk/src/core/attribution.ts
1638
1751
  var STORAGE_PREFIX2 = "shoppex:utm:";
1639
1752
  var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -1692,6 +1805,13 @@ function resolveUtmParameters(storeSlug) {
1692
1805
  return void 0;
1693
1806
  }
1694
1807
  const fromUrl = readFromUrl(window.location.search);
1808
+ if (readAnalyticsConsent() !== "granted") {
1809
+ try {
1810
+ window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1811
+ } catch {
1812
+ }
1813
+ return Object.keys(fromUrl).length > 0 ? fromUrl : void 0;
1814
+ }
1695
1815
  if (Object.keys(fromUrl).length > 0) {
1696
1816
  writeStored(storeSlug, fromUrl);
1697
1817
  return fromUrl;
@@ -1858,13 +1978,21 @@ async function trackPageView(cartValue, itemCount) {
1858
1978
  const sendView = async () => {
1859
1979
  const order2 = reservedOrder ? await reservedOrder : void 0;
1860
1980
  const settle2 = order2 ? claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount) : void 0;
1981
+ const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
1982
+ const previousSession = currentPageSession();
1983
+ if (previousSession && previousSession !== sessionId) reportPageLeave();
1984
+ markPageViewed(sessionId);
1985
+ if (sessionId) installEngagedClock(reportPageLeave);
1986
+ onAnalyticsConsent(startSessionOnConsent);
1861
1987
  const delivered = await postPing({
1862
1988
  referer: document.referrer || void 0,
1863
1989
  cart_value: cartValue,
1864
1990
  item_count: itemCount,
1865
1991
  cart_seq: order2?.seq,
1866
1992
  cart_seq_scope: order2?.scope,
1867
- connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
1993
+ session_id: sessionId ?? void 0,
1994
+ // Time on screen of the page before this one (client-side navigation).
1995
+ engaged_ms: sessionId ? takeEngagedMs() || void 0 : void 0,
1868
1996
  path: resolveStorefrontPagePath(),
1869
1997
  utm: resolveUtmParameters(config2.storeSlug)
1870
1998
  });
@@ -1892,11 +2020,44 @@ async function sendCartActivity(order2, cartValue, itemCount) {
1892
2020
  item_count: itemCount,
1893
2021
  cart_seq: order2.seq,
1894
2022
  cart_seq_scope: order2.scope,
1895
- connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
2023
+ // A cart marks the analytics session as one that did not bounce.
2024
+ session_id: analyticsSessionId(config2.storeSlug, { touch: false }) ?? void 0,
1896
2025
  path: resolveStorefrontPagePath()
1897
2026
  });
1898
2027
  settle2(delivered);
1899
2028
  }
2029
+ function startSessionOnConsent() {
2030
+ const config2 = getConfig();
2031
+ if (!isPageViewed() || analyticsSessionId(config2.storeSlug, { touch: false })) return;
2032
+ const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
2033
+ if (!sessionId) return;
2034
+ markPageViewed(sessionId);
2035
+ installEngagedClock(reportPageLeave);
2036
+ void postPing({
2037
+ event_type: "session_start",
2038
+ referer: document.referrer || void 0,
2039
+ session_id: sessionId,
2040
+ path: resolveStorefrontPagePath(),
2041
+ utm: resolveUtmParameters(config2.storeSlug)
2042
+ });
2043
+ }
2044
+ function reportPageLeave() {
2045
+ const config2 = getConfig();
2046
+ const sessionId = currentPageSession();
2047
+ const engagedMs2 = takeEngagedMs();
2048
+ if (!sessionId || engagedMs2 === 0 || readAnalyticsConsent() !== "granted") return;
2049
+ try {
2050
+ const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", { storeSlug: config2.storeSlug });
2051
+ void fetch(`${config2.apiBaseUrl}${endpoint}`, {
2052
+ method: "POST",
2053
+ headers: { "Content-Type": "application/json" },
2054
+ body: JSON.stringify({ event_type: "page_leave", session_id: sessionId, engaged_ms: engagedMs2 }),
2055
+ keepalive: true
2056
+ }).catch(() => {
2057
+ });
2058
+ } catch {
2059
+ }
2060
+ }
1900
2061
 
1901
2062
  // ../sdk/src/storefront-storage-sdk.ts
1902
2063
  function init(storeSlugOrOptions, options) {
@@ -2789,16 +2950,6 @@ function isSafeHttpsRedirectTemplateUrl(url2) {
2789
2950
  }
2790
2951
  }
2791
2952
 
2792
- // ../contracts/src/storefront-consent-choice.ts
2793
- var COOKIE_CONSENT_VERSION = 1;
2794
- var COOKIE_CONSENT_COOKIE = "shoppex_consent";
2795
- var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
2796
- var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
2797
- function parseCookieConsentValue(value) {
2798
- const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
2799
- return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
2800
- }
2801
-
2802
2953
  // ../sdk/src/modules/checkout.ts
2803
2954
  var CheckoutCreateError = class _CheckoutCreateError extends Error {
2804
2955
  constructor(message, options = {}) {
@@ -3097,7 +3248,10 @@ async function performCheckout(resolvedOptions) {
3097
3248
  // automatically so the server can refuse an invoice priced above it.
3098
3249
  // Undefined when nothing has been quoted this session — the endpoint
3099
3250
  // treats that exactly as an older SDK.
3100
- quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0
3251
+ quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0,
3252
+ // Where the buyer came from: their analytics sessions after a yes on the
3253
+ // cookie banner, `declined` after a no. The server looks up the sources.
3254
+ attribution: checkoutAttribution(config2.storeSlug)
3101
3255
  };
3102
3256
  const createAttempt = acquireCheckoutCreateIdempotency(checkoutRequestTarget, createIntent);
3103
3257
  const response = await post(
@@ -3946,7 +4100,6 @@ async function touchStorefrontPresence() {
3946
4100
  {
3947
4101
  event_type: "presence",
3948
4102
  referer: document.referrer || void 0,
3949
- connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
3950
4103
  path: resolveStorefrontPagePath(),
3951
4104
  utm: resolveUtmParameters(config2.storeSlug)
3952
4105
  },