@aranova/tracking-next 0.4.2 → 0.5.0

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.mjs CHANGED
@@ -507,26 +507,21 @@ function createTrackingClient(config) {
507
507
  };
508
508
  }
509
509
 
510
- // ../tracking-core/src/events/contact-page-visit.ts
510
+ // ../tracking-core/src/events/cta-click.ts
511
511
  import { z as z2 } from "zod";
512
- var contactPageVisitMetadataSchema = z2.object({
512
+ var ctaClickMetadataSchema = z2.object({
513
+ cta_name: z2.string(),
513
514
  page: z2.object({
514
515
  path: z2.string()
515
- })
516
- }).strict();
517
- var contactPageVisitConfigSchema = z2.object({
518
- // RegExp is runtime-only so we wrap it via z.custom. Consumers pass a
519
- // real regex at createTracking() time; the factory validates with this
520
- // schema and then keeps the live RegExp reference for runtime matching.
521
- pathPattern: z2.custom(
522
- (value) => value instanceof RegExp,
523
- { message: "pathPattern must be a RegExp" }
524
- )
516
+ }),
517
+ section: z2.string().nullable().optional(),
518
+ destination_url: z2.string().nullable().optional()
525
519
  }).strict();
520
+ var ctaClickConfigSchema = z2.object({}).strict();
526
521
 
527
- // ../tracking-core/src/events/form-submit.ts
522
+ // ../tracking-core/src/events/form-start.ts
528
523
  import { z as z3 } from "zod";
529
- var formSubmitMetadataSchema = z3.object({
524
+ var formStartMetadataSchema = z3.object({
530
525
  form: z3.object({
531
526
  id: z3.string(),
532
527
  action: z3.string().nullable()
@@ -535,32 +530,106 @@ var formSubmitMetadataSchema = z3.object({
535
530
  path: z3.string()
536
531
  })
537
532
  }).strict();
538
- var formSubmitConfigSchema = z3.object({}).strict();
533
+ var formStartConfigSchema = z3.object({
534
+ selector: z3.string().optional()
535
+ }).strict();
539
536
 
540
- // ../tracking-core/src/events/phone-click.ts
537
+ // ../tracking-core/src/events/form-submit.ts
541
538
  import { z as z4 } from "zod";
542
- var phoneClickMetadataSchema = z4.object({
543
- element: z4.object({
544
- tag: z4.string(),
545
- text: z4.string().nullable(),
546
- href: z4.string()
539
+ var formSubmitMetadataSchema = z4.object({
540
+ form: z4.object({
541
+ id: z4.string(),
542
+ action: z4.string().nullable(),
543
+ fields: z4.array(
544
+ z4.object({
545
+ name: z4.string(),
546
+ type: z4.string(),
547
+ label: z4.string().nullable(),
548
+ has_value: z4.boolean()
549
+ })
550
+ ).optional()
547
551
  }),
548
552
  page: z4.object({
549
553
  path: z4.string()
550
554
  })
551
555
  }).strict();
552
- var phoneClickConfigSchema = z4.object({}).strict();
556
+ var formSubmitConfigSchema = z4.object({}).strict();
553
557
 
554
- // ../tracking-core/src/events/time-on-site.ts
558
+ // ../tracking-core/src/events/multi-page-session.ts
555
559
  import { z as z5 } from "zod";
556
- var timeOnSiteMetadataSchema = z5.object({
557
- duration_ms: z5.number().int().nonnegative(),
560
+ var multiPageSessionMetadataSchema = z5.object({
561
+ page_count: z5.number().int().min(2),
558
562
  page: z5.object({
559
563
  path: z5.string()
560
564
  })
561
565
  }).strict();
562
- var timeOnSiteConfigSchema = z5.object({
563
- thresholdSeconds: z5.number().int().positive()
566
+ var multiPageSessionConfigSchema = z5.object({
567
+ pageThreshold: z5.number().int().min(2)
568
+ }).strict();
569
+
570
+ // ../tracking-core/src/events/phone-click.ts
571
+ import { z as z6 } from "zod";
572
+ var phoneClickMetadataSchema = z6.object({
573
+ phone_number: z6.string(),
574
+ page: z6.object({
575
+ path: z6.string()
576
+ }),
577
+ section: z6.string().nullable().optional()
578
+ }).strict();
579
+ var phoneClickConfigSchema = z6.object({}).strict();
580
+
581
+ // ../tracking-core/src/events/scroll-depth.ts
582
+ import { z as z7 } from "zod";
583
+ var scrollDepthMetadataSchema = z7.object({
584
+ depth_percent: z7.number().int().min(1).max(100),
585
+ page: z7.object({
586
+ path: z7.string()
587
+ })
588
+ }).strict();
589
+ var scrollDepthConfigSchema = z7.object({
590
+ thresholds: z7.array(z7.number().int().min(1).max(100)).min(1)
591
+ }).strict();
592
+
593
+ // ../tracking-core/src/events/specific-page-visit.ts
594
+ import { z as z8 } from "zod";
595
+ var SPECIFIC_PAGE_NAMES = [
596
+ "contact_page",
597
+ "about_page",
598
+ "services_page",
599
+ "booking_page",
600
+ "location_page",
601
+ "pricing_page",
602
+ "faq_page",
603
+ "testimonials_page"
604
+ ];
605
+ var specificPageNameSchema = z8.enum(SPECIFIC_PAGE_NAMES);
606
+ var specificPageVisitMetadataSchema = z8.object({
607
+ page_name: specificPageNameSchema,
608
+ page: z8.object({
609
+ path: z8.string()
610
+ })
611
+ }).strict();
612
+ var specificPageVisitConfigSchema = z8.object({
613
+ pages: z8.array(
614
+ z8.object({
615
+ name: specificPageNameSchema,
616
+ pathPattern: z8.custom((value) => value instanceof RegExp, {
617
+ message: "pathPattern must be a RegExp"
618
+ })
619
+ })
620
+ ).min(1)
621
+ }).strict();
622
+
623
+ // ../tracking-core/src/events/time-on-site.ts
624
+ import { z as z9 } from "zod";
625
+ var timeOnSiteMetadataSchema = z9.object({
626
+ duration_ms: z9.number().int().nonnegative(),
627
+ page: z9.object({
628
+ path: z9.string()
629
+ })
630
+ }).strict();
631
+ var timeOnSiteConfigSchema = z9.object({
632
+ thresholdSeconds: z9.number().int().positive()
564
633
  }).strict();
565
634
 
566
635
  // ../tracking-core/src/events/registry.ts
@@ -576,10 +645,25 @@ var EVENT_REGISTRY = {
576
645
  metadataSchema: timeOnSiteMetadataSchema,
577
646
  configSchema: timeOnSiteConfigSchema
578
647
  },
579
- contact_page_visit: {
648
+ specific_page_visit: {
580
649
  kind: "automatic",
581
- metadataSchema: contactPageVisitMetadataSchema,
582
- configSchema: contactPageVisitConfigSchema
650
+ metadataSchema: specificPageVisitMetadataSchema,
651
+ configSchema: specificPageVisitConfigSchema
652
+ },
653
+ scroll_depth: {
654
+ kind: "automatic",
655
+ metadataSchema: scrollDepthMetadataSchema,
656
+ configSchema: scrollDepthConfigSchema
657
+ },
658
+ multi_page_session: {
659
+ kind: "automatic",
660
+ metadataSchema: multiPageSessionMetadataSchema,
661
+ configSchema: multiPageSessionConfigSchema
662
+ },
663
+ form_start: {
664
+ kind: "automatic",
665
+ metadataSchema: formStartMetadataSchema,
666
+ configSchema: formStartConfigSchema
583
667
  },
584
668
  // --- manual triggers ---
585
669
  form_submit: {
@@ -591,6 +675,11 @@ var EVENT_REGISTRY = {
591
675
  kind: "manual",
592
676
  metadataSchema: phoneClickMetadataSchema,
593
677
  configSchema: phoneClickConfigSchema
678
+ },
679
+ cta_click: {
680
+ kind: "manual",
681
+ metadataSchema: ctaClickMetadataSchema,
682
+ configSchema: ctaClickConfigSchema
594
683
  }
595
684
  };
596
685
  var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
@@ -681,6 +770,144 @@ function attachTimeOnSite(client, config) {
681
770
  };
682
771
  }
683
772
 
773
+ // ../tracking-core/src/triggers/scroll-depth.ts
774
+ function attachScrollDepth(client, config) {
775
+ if (typeof window === "undefined" || typeof document === "undefined") {
776
+ return () => {
777
+ };
778
+ }
779
+ const thresholds = new Set(config.thresholds);
780
+ let firedForPath = /* @__PURE__ */ new Set();
781
+ let currentPath = window.location.pathname;
782
+ let rafId = null;
783
+ function getScrollPercent() {
784
+ const doc = document.documentElement;
785
+ const scrollTop = window.scrollY || doc.scrollTop;
786
+ const scrollHeight = doc.scrollHeight;
787
+ const clientHeight = doc.clientHeight;
788
+ if (scrollHeight <= clientHeight) return 100;
789
+ return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
790
+ }
791
+ function checkThresholds() {
792
+ const percent = getScrollPercent();
793
+ for (const threshold of thresholds) {
794
+ if (percent >= threshold && !firedForPath.has(threshold)) {
795
+ firedForPath.add(threshold);
796
+ client.trackEvent({
797
+ eventType: "scroll_depth",
798
+ metadata: {
799
+ depth_percent: threshold,
800
+ page: { path: currentPath }
801
+ },
802
+ pageUrl: window.location.href,
803
+ occurredAt: null
804
+ });
805
+ }
806
+ }
807
+ }
808
+ function onScroll() {
809
+ if (rafId !== null) return;
810
+ rafId = requestAnimationFrame(() => {
811
+ rafId = null;
812
+ checkThresholds();
813
+ });
814
+ }
815
+ function resetIfPathChanged() {
816
+ const newPath = window.location.pathname;
817
+ if (newPath === currentPath) return;
818
+ currentPath = newPath;
819
+ firedForPath = /* @__PURE__ */ new Set();
820
+ setTimeout(checkThresholds, 0);
821
+ }
822
+ const originalPushState = history.pushState.bind(history);
823
+ const originalReplaceState = history.replaceState.bind(history);
824
+ function patchedPushState(...args) {
825
+ originalPushState(...args);
826
+ setTimeout(resetIfPathChanged, 0);
827
+ }
828
+ function patchedReplaceState(...args) {
829
+ originalReplaceState(...args);
830
+ setTimeout(resetIfPathChanged, 0);
831
+ }
832
+ history.pushState = patchedPushState;
833
+ history.replaceState = patchedReplaceState;
834
+ window.addEventListener("popstate", resetIfPathChanged);
835
+ window.addEventListener("scroll", onScroll, { passive: true });
836
+ setTimeout(checkThresholds, 0);
837
+ return () => {
838
+ if (rafId !== null) cancelAnimationFrame(rafId);
839
+ history.pushState = originalPushState;
840
+ history.replaceState = originalReplaceState;
841
+ window.removeEventListener("popstate", resetIfPathChanged);
842
+ window.removeEventListener("scroll", onScroll);
843
+ };
844
+ }
845
+
846
+ // ../tracking-core/src/triggers/form-start.ts
847
+ function attachFormStart(client, config) {
848
+ if (typeof window === "undefined" || typeof document === "undefined") {
849
+ return () => {
850
+ };
851
+ }
852
+ const selector = config.selector ?? "form";
853
+ let firedForms = /* @__PURE__ */ new Set();
854
+ let currentPath = window.location.pathname;
855
+ function getFormKey(form) {
856
+ if (form.id) return `id:${form.id}`;
857
+ const explicitAction = form.getAttribute("action");
858
+ if (explicitAction) return `action:${explicitAction}`;
859
+ const forms = Array.from(document.querySelectorAll(selector));
860
+ return `index:${forms.indexOf(form)}`;
861
+ }
862
+ function onFocusIn(event) {
863
+ const target = event.target;
864
+ if (!(target instanceof HTMLElement)) return;
865
+ const form = target.closest(selector);
866
+ if (!form || form.tagName !== "FORM") return;
867
+ const key = getFormKey(form);
868
+ if (firedForms.has(key)) return;
869
+ firedForms.add(key);
870
+ client.trackEvent({
871
+ eventType: "form_start",
872
+ metadata: {
873
+ form: {
874
+ id: form.id || "",
875
+ action: form.getAttribute("action") ?? null
876
+ },
877
+ page: { path: window.location.pathname }
878
+ },
879
+ pageUrl: window.location.href,
880
+ occurredAt: null
881
+ });
882
+ }
883
+ function resetIfPathChanged() {
884
+ const newPath = window.location.pathname;
885
+ if (newPath === currentPath) return;
886
+ currentPath = newPath;
887
+ firedForms = /* @__PURE__ */ new Set();
888
+ }
889
+ const originalPushState = history.pushState.bind(history);
890
+ const originalReplaceState = history.replaceState.bind(history);
891
+ function patchedPushState(...args) {
892
+ originalPushState(...args);
893
+ setTimeout(resetIfPathChanged, 0);
894
+ }
895
+ function patchedReplaceState(...args) {
896
+ originalReplaceState(...args);
897
+ setTimeout(resetIfPathChanged, 0);
898
+ }
899
+ history.pushState = patchedPushState;
900
+ history.replaceState = patchedReplaceState;
901
+ window.addEventListener("popstate", resetIfPathChanged);
902
+ document.addEventListener("focusin", onFocusIn);
903
+ return () => {
904
+ history.pushState = originalPushState;
905
+ history.replaceState = originalReplaceState;
906
+ window.removeEventListener("popstate", resetIfPathChanged);
907
+ document.removeEventListener("focusin", onFocusIn);
908
+ };
909
+ }
910
+
684
911
  // src/ConsentBanner.tsx
685
912
  import { useEffect, useState } from "react";
686
913
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -780,7 +1007,7 @@ function GoogleAdsTracking({ gtagId }) {
780
1007
  import { usePathname, useSearchParams } from "next/navigation";
781
1008
 
782
1009
  // package.json
783
- var version = "0.4.2";
1010
+ var version = "0.5.0";
784
1011
 
785
1012
  // src/factory.tsx
786
1013
  import {
@@ -813,7 +1040,7 @@ function createTracking(options) {
813
1040
  };
814
1041
  }
815
1042
  const TrackingContext = createContext(null);
816
- const contactPageVisitConfig = triggers.automatic.contact_page_visit ?? null;
1043
+ const specificPageVisitConfig = triggers.automatic.specific_page_visit ?? null;
817
1044
  const timeOnSiteConfig = triggers.automatic.time_on_site ?? null;
818
1045
  function TrackingProvider({ children }) {
819
1046
  const pathname = usePathname();
@@ -843,12 +1070,9 @@ function createTracking(options) {
843
1070
  });
844
1071
  fireManualPageView(rawClient);
845
1072
  }, [pathname, searchParams]);
846
- const lastContactPathRef = useRef(null);
1073
+ const firedPagesRef = useRef(/* @__PURE__ */ new Set());
847
1074
  useEffect3(() => {
848
- if (!contactPageVisitConfig || pathname === null) return;
849
- if (!contactPageVisitConfig.pathPattern.test(pathname)) return;
850
- if (lastContactPathRef.current === pathname) return;
851
- lastContactPathRef.current = pathname;
1075
+ if (!specificPageVisitConfig || pathname === null) return;
852
1076
  const rawClient = getOrCreateTrackingClient({
853
1077
  apiKey,
854
1078
  endpoint,
@@ -856,12 +1080,18 @@ function createTracking(options) {
856
1080
  packageName: "@aranova/tracking-next",
857
1081
  debug
858
1082
  });
859
- rawClient.trackEvent({
860
- eventType: "contact_page_visit",
861
- metadata: { page: { path: pathname } },
862
- pageUrl: null,
863
- occurredAt: null
864
- });
1083
+ for (const { name, pathPattern } of specificPageVisitConfig.pages) {
1084
+ if (!pathPattern.test(pathname)) continue;
1085
+ const key = `${name}:${pathname}`;
1086
+ if (firedPagesRef.current.has(key)) continue;
1087
+ firedPagesRef.current.add(key);
1088
+ rawClient.trackEvent({
1089
+ eventType: "specific_page_visit",
1090
+ metadata: { page_name: name, page: { path: pathname } },
1091
+ pageUrl: null,
1092
+ occurredAt: null
1093
+ });
1094
+ }
865
1095
  }, [pathname]);
866
1096
  useEffect3(() => {
867
1097
  const rawClient = getOrCreateTrackingClient({
@@ -884,6 +1114,74 @@ function createTracking(options) {
884
1114
  });
885
1115
  return attachTimeOnSite(rawClient, timeOnSiteConfig);
886
1116
  }, []);
1117
+ const multiPageSessionConfig = triggers.automatic.multi_page_session ?? null;
1118
+ const mpsLastPathRef = useRef(null);
1119
+ useEffect3(() => {
1120
+ if (!multiPageSessionConfig || pathname === null) return;
1121
+ if (pathname === mpsLastPathRef.current) return;
1122
+ mpsLastPathRef.current = pathname;
1123
+ const MPS_PATHS_KEY = "aranova_tracking_mps_paths";
1124
+ const MPS_SESSION_KEY = "aranova_tracking_mps_session";
1125
+ const MPS_FIRED_KEY = "aranova_tracking_mps_fired";
1126
+ try {
1127
+ const store = window.sessionStorage;
1128
+ const currentSession = getOrRotateSessionId();
1129
+ const storedSession = store.getItem(MPS_SESSION_KEY);
1130
+ if (storedSession !== currentSession) {
1131
+ store.setItem(MPS_SESSION_KEY, currentSession);
1132
+ store.removeItem(MPS_PATHS_KEY);
1133
+ store.removeItem(MPS_FIRED_KEY);
1134
+ }
1135
+ if (store.getItem(MPS_FIRED_KEY) === "1") return;
1136
+ const raw = store.getItem(MPS_PATHS_KEY);
1137
+ const paths = raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
1138
+ paths.add(pathname);
1139
+ store.setItem(MPS_PATHS_KEY, JSON.stringify([...paths]));
1140
+ if (paths.size >= multiPageSessionConfig.pageThreshold) {
1141
+ store.setItem(MPS_FIRED_KEY, "1");
1142
+ const rawClient = getOrCreateTrackingClient({
1143
+ apiKey,
1144
+ endpoint,
1145
+ surface: "next",
1146
+ packageName: "@aranova/tracking-next",
1147
+ debug
1148
+ });
1149
+ rawClient.trackEvent({
1150
+ eventType: "multi_page_session",
1151
+ metadata: {
1152
+ page_count: paths.size,
1153
+ page: { path: pathname }
1154
+ },
1155
+ pageUrl: null,
1156
+ occurredAt: null
1157
+ });
1158
+ }
1159
+ } catch {
1160
+ }
1161
+ }, [pathname]);
1162
+ useEffect3(() => {
1163
+ const detachers = [];
1164
+ const rawClient = getOrCreateTrackingClient({
1165
+ apiKey,
1166
+ endpoint,
1167
+ surface: "next",
1168
+ packageName: "@aranova/tracking-next",
1169
+ debug
1170
+ });
1171
+ const scrollDepth = triggers.automatic.scroll_depth;
1172
+ if (scrollDepth) {
1173
+ detachers.push(attachScrollDepth(rawClient, scrollDepth));
1174
+ }
1175
+ const formStart = triggers.automatic.form_start;
1176
+ if (formStart) {
1177
+ detachers.push(attachFormStart(rawClient, formStart));
1178
+ }
1179
+ return () => {
1180
+ for (let i = detachers.length - 1; i >= 0; i--) {
1181
+ detachers[i]();
1182
+ }
1183
+ };
1184
+ }, []);
887
1185
  return /* @__PURE__ */ jsx3(TrackingContext.Provider, { value: client, children });
888
1186
  }
889
1187
  function useTracking() {