@neowhale/storefront 0.2.32 → 0.2.34

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.
@@ -436,8 +436,9 @@ interface LandingPageProps {
436
436
  visitorId?: string;
437
437
  sessionId?: string;
438
438
  };
439
+ enableAnalytics?: boolean;
439
440
  }
440
- declare function LandingPage({ slug, gatewayUrl, renderSection, onDataLoaded, onError, onEvent, analyticsContext, }: LandingPageProps): react_jsx_runtime.JSX.Element | null;
441
+ declare function LandingPage({ slug, gatewayUrl, renderSection, onDataLoaded, onError, onEvent, analyticsContext, enableAnalytics, }: LandingPageProps): react_jsx_runtime.JSX.Element | null;
441
442
 
442
443
  interface SectionTheme {
443
444
  bg: string;
@@ -436,8 +436,9 @@ interface LandingPageProps {
436
436
  visitorId?: string;
437
437
  sessionId?: string;
438
438
  };
439
+ enableAnalytics?: boolean;
439
440
  }
440
- declare function LandingPage({ slug, gatewayUrl, renderSection, onDataLoaded, onError, onEvent, analyticsContext, }: LandingPageProps): react_jsx_runtime.JSX.Element | null;
441
+ declare function LandingPage({ slug, gatewayUrl, renderSection, onDataLoaded, onError, onEvent, analyticsContext, enableAnalytics, }: LandingPageProps): react_jsx_runtime.JSX.Element | null;
441
442
 
442
443
  interface SectionTheme {
443
444
  bg: string;
@@ -1,5 +1,6 @@
1
1
  import { PixelManager } from '../chunk-MZO7BCGU.js';
2
2
  import { resilientSend, WhaleClient } from '../chunk-2XODSXJT.js';
3
+ import { BehavioralTracker } from '../chunk-AKWSW7DW.js';
3
4
  import { createContext, useContext, useRef, useCallback, useEffect, useState, useMemo } from 'react';
4
5
  import { usePathname } from 'next/navigation';
5
6
  import { createStore } from 'zustand/vanilla';
@@ -683,334 +684,6 @@ function PixelInitializer({ onReady, onTheme }) {
683
684
  }, [ctx, onReady, onTheme]);
684
685
  return null;
685
686
  }
686
-
687
- // src/behavioral/tracker.ts
688
- var SCROLL_MILESTONES = [25, 50, 75, 100];
689
- var TIME_MILESTONES = [30, 60, 120, 300];
690
- var MOUSE_THROTTLE_MS = 200;
691
- var MOUSE_BUFFER_MAX = 100;
692
- var RAGE_CLICK_COUNT = 3;
693
- var RAGE_CLICK_RADIUS = 50;
694
- var RAGE_CLICK_WINDOW_MS = 2e3;
695
- var MAX_CLICK_HISTORY = 10;
696
- var BehavioralTracker = class {
697
- constructor(config) {
698
- this.buffer = [];
699
- this.pageUrl = "";
700
- this.pagePath = "";
701
- this.flushTimer = null;
702
- this.scrollMilestones = /* @__PURE__ */ new Set();
703
- this.timeMilestones = /* @__PURE__ */ new Set();
704
- this.timeTimers = [];
705
- this.exitIntentFired = false;
706
- this.startTime = 0;
707
- this.clickHistory = [];
708
- this.mouseBuffer = [];
709
- this.lastMouseTime = 0;
710
- this.listeners = [];
711
- this.observer = null;
712
- this.sentinels = [];
713
- // ---------------------------------------------------------------------------
714
- // Event handlers (arrow functions for stable `this`)
715
- // ---------------------------------------------------------------------------
716
- this.handleClick = (e) => {
717
- const me = e;
718
- const target = me.target;
719
- if (!target) return;
720
- const now2 = Date.now();
721
- const x = me.clientX;
722
- const y = me.clientY;
723
- this.clickHistory.push({ x, y, t: now2 });
724
- if (this.clickHistory.length > MAX_CLICK_HISTORY) {
725
- this.clickHistory.shift();
726
- }
727
- const tag = target.tagName?.toLowerCase() ?? "";
728
- const rawText = target.textContent ?? "";
729
- const text = rawText.trim().slice(0, 50);
730
- this.push({
731
- data_type: "click",
732
- data: {
733
- tag,
734
- text,
735
- selector: this.getSelector(target),
736
- x,
737
- y,
738
- timestamp: now2
739
- },
740
- page_url: this.pageUrl,
741
- page_path: this.pagePath
742
- });
743
- this.detectRageClick(x, y, now2);
744
- };
745
- this.handleMouseMove = (e) => {
746
- const me = e;
747
- const now2 = Date.now();
748
- if (now2 - this.lastMouseTime < MOUSE_THROTTLE_MS) return;
749
- this.lastMouseTime = now2;
750
- this.mouseBuffer.push({ x: me.clientX, y: me.clientY, t: now2 });
751
- if (this.mouseBuffer.length > MOUSE_BUFFER_MAX) {
752
- this.mouseBuffer.shift();
753
- }
754
- };
755
- this.handleMouseOut = (e) => {
756
- const me = e;
757
- if (this.exitIntentFired) return;
758
- if (me.clientY > 0) return;
759
- if (me.relatedTarget !== null) return;
760
- this.exitIntentFired = true;
761
- this.push({
762
- data_type: "exit_intent",
763
- data: {
764
- time_on_page_ms: Date.now() - this.startTime,
765
- timestamp: Date.now()
766
- },
767
- page_url: this.pageUrl,
768
- page_path: this.pagePath
769
- });
770
- };
771
- this.handleCopy = () => {
772
- const selection = window.getSelection();
773
- const length = selection?.toString().length ?? 0;
774
- this.push({
775
- data_type: "copy",
776
- data: {
777
- text_length: length,
778
- timestamp: Date.now()
779
- },
780
- page_url: this.pageUrl,
781
- page_path: this.pagePath
782
- });
783
- };
784
- this.handleVisibilityChange = () => {
785
- if (document.visibilityState !== "hidden") return;
786
- const timeSpent = Date.now() - this.startTime;
787
- this.push({
788
- data_type: "page_exit",
789
- data: {
790
- time_spent_ms: timeSpent,
791
- timestamp: Date.now()
792
- },
793
- page_url: this.pageUrl,
794
- page_path: this.pagePath
795
- });
796
- this.flushMouseBuffer();
797
- this.flush();
798
- };
799
- this.config = {
800
- sendBatch: config.sendBatch,
801
- sessionId: config.sessionId,
802
- visitorId: config.visitorId,
803
- flushIntervalMs: config.flushIntervalMs ?? 1e4,
804
- maxBufferSize: config.maxBufferSize ?? 500
805
- };
806
- }
807
- start() {
808
- this.startTime = Date.now();
809
- this.addListener(document, "click", this.handleClick);
810
- this.addListener(document, "mousemove", this.handleMouseMove);
811
- this.addListener(document, "mouseout", this.handleMouseOut);
812
- this.addListener(document, "copy", this.handleCopy);
813
- this.addListener(document, "visibilitychange", this.handleVisibilityChange);
814
- this.setupScrollTracking();
815
- this.setupTimeMilestones();
816
- this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
817
- }
818
- stop() {
819
- for (const [target, event, handler] of this.listeners) {
820
- target.removeEventListener(event, handler, { capture: true });
821
- }
822
- this.listeners = [];
823
- if (this.flushTimer !== null) {
824
- clearInterval(this.flushTimer);
825
- this.flushTimer = null;
826
- }
827
- this.clearTimeMilestones();
828
- this.cleanupScrollTracking();
829
- this.flushMouseBuffer();
830
- this.flush();
831
- }
832
- setPageContext(url, path) {
833
- this.flushMouseBuffer();
834
- this.flush();
835
- this.pageUrl = url;
836
- this.pagePath = path;
837
- this.scrollMilestones.clear();
838
- this.timeMilestones.clear();
839
- this.exitIntentFired = false;
840
- this.startTime = Date.now();
841
- this.clickHistory = [];
842
- this.clearTimeMilestones();
843
- this.cleanupScrollTracking();
844
- this.setupTimeMilestones();
845
- requestAnimationFrame(() => this.setupScrollTracking());
846
- }
847
- // ---------------------------------------------------------------------------
848
- // Buffer management
849
- // ---------------------------------------------------------------------------
850
- push(event) {
851
- this.buffer.push(event);
852
- if (this.buffer.length >= this.config.maxBufferSize) {
853
- this.flush();
854
- }
855
- }
856
- flush() {
857
- if (this.buffer.length === 0) return;
858
- const batch = {
859
- session_id: this.config.sessionId,
860
- visitor_id: this.config.visitorId,
861
- events: this.buffer
862
- };
863
- this.buffer = [];
864
- this.config.sendBatch(batch).catch(() => {
865
- });
866
- }
867
- addListener(target, event, handler) {
868
- target.addEventListener(event, handler, { passive: true, capture: true });
869
- this.listeners.push([target, event, handler]);
870
- }
871
- // ---------------------------------------------------------------------------
872
- // Scroll tracking with IntersectionObserver
873
- // ---------------------------------------------------------------------------
874
- setupScrollTracking() {
875
- if (typeof IntersectionObserver === "undefined") return;
876
- this.observer = new IntersectionObserver(
877
- (entries) => {
878
- for (const entry of entries) {
879
- if (!entry.isIntersecting) continue;
880
- const milestone = Number(entry.target.getAttribute("data-scroll-milestone"));
881
- if (isNaN(milestone) || this.scrollMilestones.has(milestone)) continue;
882
- this.scrollMilestones.add(milestone);
883
- this.push({
884
- data_type: "scroll_depth",
885
- data: {
886
- depth_percent: milestone,
887
- timestamp: Date.now()
888
- },
889
- page_url: this.pageUrl,
890
- page_path: this.pagePath
891
- });
892
- }
893
- },
894
- { threshold: 0 }
895
- );
896
- const docHeight = document.documentElement.scrollHeight;
897
- for (const pct of SCROLL_MILESTONES) {
898
- const sentinel = document.createElement("div");
899
- sentinel.setAttribute("data-scroll-milestone", String(pct));
900
- sentinel.style.position = "absolute";
901
- sentinel.style.left = "0";
902
- sentinel.style.width = "1px";
903
- sentinel.style.height = "1px";
904
- sentinel.style.pointerEvents = "none";
905
- sentinel.style.opacity = "0";
906
- sentinel.style.top = `${docHeight * pct / 100 - 1}px`;
907
- document.body.appendChild(sentinel);
908
- this.sentinels.push(sentinel);
909
- this.observer.observe(sentinel);
910
- }
911
- }
912
- cleanupScrollTracking() {
913
- if (this.observer) {
914
- this.observer.disconnect();
915
- this.observer = null;
916
- }
917
- for (const sentinel of this.sentinels) {
918
- sentinel.remove();
919
- }
920
- this.sentinels = [];
921
- }
922
- // ---------------------------------------------------------------------------
923
- // Time milestones
924
- // ---------------------------------------------------------------------------
925
- setupTimeMilestones() {
926
- for (const seconds of TIME_MILESTONES) {
927
- const timer = setTimeout(() => {
928
- if (this.timeMilestones.has(seconds)) return;
929
- this.timeMilestones.add(seconds);
930
- this.push({
931
- data_type: "time_on_page",
932
- data: {
933
- milestone_seconds: seconds,
934
- timestamp: Date.now()
935
- },
936
- page_url: this.pageUrl,
937
- page_path: this.pagePath
938
- });
939
- }, seconds * 1e3);
940
- this.timeTimers.push(timer);
941
- }
942
- }
943
- clearTimeMilestones() {
944
- for (const timer of this.timeTimers) {
945
- clearTimeout(timer);
946
- }
947
- this.timeTimers = [];
948
- }
949
- // ---------------------------------------------------------------------------
950
- // Rage click detection
951
- // ---------------------------------------------------------------------------
952
- detectRageClick(x, y, now2) {
953
- const windowStart = now2 - RAGE_CLICK_WINDOW_MS;
954
- const nearby = this.clickHistory.filter((c) => {
955
- if (c.t < windowStart) return false;
956
- const dx = c.x - x;
957
- const dy = c.y - y;
958
- return Math.sqrt(dx * dx + dy * dy) <= RAGE_CLICK_RADIUS;
959
- });
960
- if (nearby.length >= RAGE_CLICK_COUNT) {
961
- this.push({
962
- data_type: "rage_click",
963
- data: {
964
- x,
965
- y,
966
- click_count: nearby.length,
967
- timestamp: now2
968
- },
969
- page_url: this.pageUrl,
970
- page_path: this.pagePath
971
- });
972
- this.clickHistory = [];
973
- }
974
- }
975
- // ---------------------------------------------------------------------------
976
- // Mouse buffer flush
977
- // ---------------------------------------------------------------------------
978
- flushMouseBuffer() {
979
- if (this.mouseBuffer.length === 0) return;
980
- this.push({
981
- data_type: "mouse_movement",
982
- data: {
983
- points: [...this.mouseBuffer],
984
- timestamp: Date.now()
985
- },
986
- page_url: this.pageUrl,
987
- page_path: this.pagePath
988
- });
989
- this.mouseBuffer = [];
990
- }
991
- // ---------------------------------------------------------------------------
992
- // CSS selector helper
993
- // ---------------------------------------------------------------------------
994
- getSelector(el) {
995
- const parts = [];
996
- let current = el;
997
- let depth = 0;
998
- while (current && depth < 3) {
999
- let segment = current.tagName.toLowerCase();
1000
- if (current.id) {
1001
- segment += `#${current.id}`;
1002
- } else if (current.classList.length > 0) {
1003
- segment += `.${Array.from(current.classList).join(".")}`;
1004
- }
1005
- parts.unshift(segment);
1006
- current = current.parentElement;
1007
- depth++;
1008
- }
1009
- return parts.join(" > ");
1010
- }
1011
- };
1012
-
1013
- // src/react/components/behavioral-tracker.tsx
1014
687
  var SESSION_KEY_SUFFIX2 = "-analytics-session";
1015
688
  var VISITOR_KEY_SUFFIX2 = "-visitor-id";
1016
689
  var MAX_SESSION_WAIT_MS = 1e4;
@@ -2570,7 +2243,26 @@ function SectionRenderer({
2570
2243
  return null;
2571
2244
  }
2572
2245
  })();
2573
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2246
+ const sectionRef = useRef(null);
2247
+ useEffect(() => {
2248
+ const el2 = sectionRef.current;
2249
+ if (!el2 || typeof IntersectionObserver === "undefined") return;
2250
+ const obs = new IntersectionObserver(
2251
+ ([entry]) => {
2252
+ if (entry.isIntersecting) {
2253
+ onEvent?.("section_view", {
2254
+ section_id: section.id,
2255
+ section_type: section.type
2256
+ });
2257
+ obs.disconnect();
2258
+ }
2259
+ },
2260
+ { threshold: 0.5 }
2261
+ );
2262
+ obs.observe(el2);
2263
+ return () => obs.disconnect();
2264
+ }, [section.id, section.type, onEvent]);
2265
+ return /* @__PURE__ */ jsxs("div", { ref: sectionRef, "data-section-id": section.id, "data-section-type": section.type, children: [
2574
2266
  el,
2575
2267
  showCOA && data?.coa && /* @__PURE__ */ jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
2576
2268
  ] });
@@ -3477,7 +3169,8 @@ function LandingPage({
3477
3169
  onDataLoaded,
3478
3170
  onError,
3479
3171
  onEvent,
3480
- analyticsContext
3172
+ analyticsContext,
3173
+ enableAnalytics = true
3481
3174
  }) {
3482
3175
  const [state, setState] = useState("loading");
3483
3176
  const [data, setData] = useState(null);
@@ -3485,6 +3178,13 @@ function LandingPage({
3485
3178
  useEffect(() => {
3486
3179
  if (!slug) return;
3487
3180
  let cancelled = false;
3181
+ if (typeof window !== "undefined" && window.__LANDING_DATA__) {
3182
+ const json = window.__LANDING_DATA__;
3183
+ setData(json);
3184
+ setState("ready");
3185
+ onDataLoaded?.(json);
3186
+ return;
3187
+ }
3488
3188
  async function load() {
3489
3189
  try {
3490
3190
  const res = await fetch(`${gatewayUrl}/l/${encodeURIComponent(slug)}`);
@@ -3525,16 +3225,113 @@ function LandingPage({
3525
3225
  if (state === "expired") return /* @__PURE__ */ jsx(DefaultExpired2, {});
3526
3226
  if (state === "error") return /* @__PURE__ */ jsx(DefaultError2, { message: errorMsg });
3527
3227
  if (!data) return null;
3528
- return /* @__PURE__ */ jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent, analyticsContext });
3228
+ return /* @__PURE__ */ jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent, analyticsContext, enableAnalytics });
3229
+ }
3230
+ function isSectionVisible(section, urlParams) {
3231
+ const vis = section.config?.visibility;
3232
+ if (!vis?.params) return true;
3233
+ for (const [key, allowed] of Object.entries(vis.params)) {
3234
+ const val = urlParams.get(key);
3235
+ if (!val || !allowed.includes(val)) return false;
3236
+ }
3237
+ return true;
3529
3238
  }
3530
3239
  function PageLayout({
3531
3240
  data,
3532
3241
  gatewayUrl,
3533
3242
  renderSection,
3534
3243
  onEvent,
3535
- analyticsContext
3244
+ analyticsContext,
3245
+ enableAnalytics
3536
3246
  }) {
3537
3247
  const { landing_page: lp, store } = data;
3248
+ const trackerRef = useRef(null);
3249
+ useEffect(() => {
3250
+ if (!enableAnalytics || typeof window === "undefined") return;
3251
+ const analyticsConfig = window.__LANDING_ANALYTICS__;
3252
+ if (!analyticsConfig?.slug) return;
3253
+ let visitorId = localStorage.getItem("wt_vid") || "";
3254
+ if (!visitorId) {
3255
+ visitorId = crypto.randomUUID();
3256
+ localStorage.setItem("wt_vid", visitorId);
3257
+ }
3258
+ let sessionId = sessionStorage.getItem("wt_sid") || "";
3259
+ if (!sessionId) {
3260
+ sessionId = crypto.randomUUID();
3261
+ sessionStorage.setItem("wt_sid", sessionId);
3262
+ }
3263
+ import('../tracker-34GTXU54.js').then(({ BehavioralTracker: BehavioralTracker2 }) => {
3264
+ const gwUrl = analyticsConfig.gatewayUrl || gatewayUrl;
3265
+ const slug = analyticsConfig.slug;
3266
+ const utmParams = new URLSearchParams(window.location.search);
3267
+ const tracker = new BehavioralTracker2({
3268
+ sessionId,
3269
+ visitorId,
3270
+ sendBatch: async (batch) => {
3271
+ const events = batch.events.map((e) => ({
3272
+ event_type: e.data_type,
3273
+ event_data: e.data,
3274
+ session_id: batch.session_id,
3275
+ visitor_id: batch.visitor_id,
3276
+ campaign_id: analyticsConfig.campaignId || utmParams.get("utm_campaign_id") || void 0,
3277
+ utm_source: utmParams.get("utm_source") || void 0,
3278
+ utm_medium: utmParams.get("utm_medium") || void 0,
3279
+ utm_campaign: utmParams.get("utm_campaign") || void 0
3280
+ }));
3281
+ const body = JSON.stringify({ events });
3282
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
3283
+ navigator.sendBeacon(
3284
+ `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
3285
+ new Blob([body], { type: "application/json" })
3286
+ );
3287
+ } else {
3288
+ await fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
3289
+ method: "POST",
3290
+ headers: { "Content-Type": "application/json" },
3291
+ body,
3292
+ keepalive: true
3293
+ });
3294
+ }
3295
+ }
3296
+ });
3297
+ tracker.setPageContext(window.location.href, window.location.pathname);
3298
+ tracker.start();
3299
+ trackerRef.current = tracker;
3300
+ const pageViewBody = JSON.stringify({
3301
+ events: [{
3302
+ event_type: "page_view",
3303
+ event_data: { referrer: document.referrer, url: window.location.href },
3304
+ session_id: sessionId,
3305
+ visitor_id: visitorId,
3306
+ campaign_id: analyticsConfig.campaignId || void 0,
3307
+ utm_source: utmParams.get("utm_source") || void 0,
3308
+ utm_medium: utmParams.get("utm_medium") || void 0,
3309
+ utm_campaign: utmParams.get("utm_campaign") || void 0
3310
+ }]
3311
+ });
3312
+ if (navigator.sendBeacon) {
3313
+ navigator.sendBeacon(
3314
+ `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
3315
+ new Blob([pageViewBody], { type: "application/json" })
3316
+ );
3317
+ } else {
3318
+ fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
3319
+ method: "POST",
3320
+ headers: { "Content-Type": "application/json" },
3321
+ body: pageViewBody,
3322
+ keepalive: true
3323
+ }).catch(() => {
3324
+ });
3325
+ }
3326
+ }).catch(() => {
3327
+ });
3328
+ return () => {
3329
+ if (trackerRef.current) {
3330
+ trackerRef.current.stop();
3331
+ trackerRef.current = null;
3332
+ }
3333
+ };
3334
+ }, [enableAnalytics, gatewayUrl]);
3538
3335
  const theme = {
3539
3336
  bg: lp.background_color || store?.theme?.background || "#050505",
3540
3337
  fg: lp.text_color || store?.theme?.foreground || "#fafafa",
@@ -3546,7 +3343,8 @@ function PageLayout({
3546
3343
  };
3547
3344
  const fontFamily = lp.font_family || theme.fontDisplay || "system-ui, -apple-system, sans-serif";
3548
3345
  const logoUrl = store?.logo_url;
3549
- const sorted = [...lp.sections].sort((a, b) => a.order - b.order);
3346
+ const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : new URLSearchParams();
3347
+ const sorted = [...lp.sections].sort((a, b) => a.order - b.order).filter((s) => isSectionVisible(s, urlParams));
3550
3348
  const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug }, analyticsContext };
3551
3349
  return /* @__PURE__ */ jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
3552
3350
  lp.custom_css && /* @__PURE__ */ jsx("style", { children: lp.custom_css }),