@neowhale/storefront 0.2.33 → 0.2.35

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,7 +3225,7 @@ 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 });
3529
3229
  }
3530
3230
  function isSectionVisible(section, urlParams) {
3531
3231
  const vis = section.config?.visibility;
@@ -3541,9 +3241,97 @@ function PageLayout({
3541
3241
  gatewayUrl,
3542
3242
  renderSection,
3543
3243
  onEvent,
3544
- analyticsContext
3244
+ analyticsContext,
3245
+ enableAnalytics
3545
3246
  }) {
3546
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]);
3547
3335
  const theme = {
3548
3336
  bg: lp.background_color || store?.theme?.background || "#050505",
3549
3337
  fg: lp.text_color || store?.theme?.foreground || "#fafafa",