@aranova/tracking-next 0.4.1 → 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.js CHANGED
@@ -555,26 +555,21 @@ function createTrackingClient(config) {
555
555
  };
556
556
  }
557
557
 
558
- // ../tracking-core/src/events/contact-page-visit.ts
558
+ // ../tracking-core/src/events/cta-click.ts
559
559
  var import_zod2 = require("zod");
560
- var contactPageVisitMetadataSchema = import_zod2.z.object({
560
+ var ctaClickMetadataSchema = import_zod2.z.object({
561
+ cta_name: import_zod2.z.string(),
561
562
  page: import_zod2.z.object({
562
563
  path: import_zod2.z.string()
563
- })
564
- }).strict();
565
- var contactPageVisitConfigSchema = import_zod2.z.object({
566
- // RegExp is runtime-only so we wrap it via z.custom. Consumers pass a
567
- // real regex at createTracking() time; the factory validates with this
568
- // schema and then keeps the live RegExp reference for runtime matching.
569
- pathPattern: import_zod2.z.custom(
570
- (value) => value instanceof RegExp,
571
- { message: "pathPattern must be a RegExp" }
572
- )
564
+ }),
565
+ section: import_zod2.z.string().nullable().optional(),
566
+ destination_url: import_zod2.z.string().nullable().optional()
573
567
  }).strict();
568
+ var ctaClickConfigSchema = import_zod2.z.object({}).strict();
574
569
 
575
- // ../tracking-core/src/events/form-submit.ts
570
+ // ../tracking-core/src/events/form-start.ts
576
571
  var import_zod3 = require("zod");
577
- var formSubmitMetadataSchema = import_zod3.z.object({
572
+ var formStartMetadataSchema = import_zod3.z.object({
578
573
  form: import_zod3.z.object({
579
574
  id: import_zod3.z.string(),
580
575
  action: import_zod3.z.string().nullable()
@@ -583,32 +578,106 @@ var formSubmitMetadataSchema = import_zod3.z.object({
583
578
  path: import_zod3.z.string()
584
579
  })
585
580
  }).strict();
586
- var formSubmitConfigSchema = import_zod3.z.object({}).strict();
581
+ var formStartConfigSchema = import_zod3.z.object({
582
+ selector: import_zod3.z.string().optional()
583
+ }).strict();
587
584
 
588
- // ../tracking-core/src/events/phone-click.ts
585
+ // ../tracking-core/src/events/form-submit.ts
589
586
  var import_zod4 = require("zod");
590
- var phoneClickMetadataSchema = import_zod4.z.object({
591
- element: import_zod4.z.object({
592
- tag: import_zod4.z.string(),
593
- text: import_zod4.z.string().nullable(),
594
- href: import_zod4.z.string()
587
+ var formSubmitMetadataSchema = import_zod4.z.object({
588
+ form: import_zod4.z.object({
589
+ id: import_zod4.z.string(),
590
+ action: import_zod4.z.string().nullable(),
591
+ fields: import_zod4.z.array(
592
+ import_zod4.z.object({
593
+ name: import_zod4.z.string(),
594
+ type: import_zod4.z.string(),
595
+ label: import_zod4.z.string().nullable(),
596
+ has_value: import_zod4.z.boolean()
597
+ })
598
+ ).optional()
595
599
  }),
596
600
  page: import_zod4.z.object({
597
601
  path: import_zod4.z.string()
598
602
  })
599
603
  }).strict();
600
- var phoneClickConfigSchema = import_zod4.z.object({}).strict();
604
+ var formSubmitConfigSchema = import_zod4.z.object({}).strict();
601
605
 
602
- // ../tracking-core/src/events/time-on-site.ts
606
+ // ../tracking-core/src/events/multi-page-session.ts
603
607
  var import_zod5 = require("zod");
604
- var timeOnSiteMetadataSchema = import_zod5.z.object({
605
- duration_ms: import_zod5.z.number().int().nonnegative(),
608
+ var multiPageSessionMetadataSchema = import_zod5.z.object({
609
+ page_count: import_zod5.z.number().int().min(2),
606
610
  page: import_zod5.z.object({
607
611
  path: import_zod5.z.string()
608
612
  })
609
613
  }).strict();
610
- var timeOnSiteConfigSchema = import_zod5.z.object({
611
- thresholdSeconds: import_zod5.z.number().int().positive()
614
+ var multiPageSessionConfigSchema = import_zod5.z.object({
615
+ pageThreshold: import_zod5.z.number().int().min(2)
616
+ }).strict();
617
+
618
+ // ../tracking-core/src/events/phone-click.ts
619
+ var import_zod6 = require("zod");
620
+ var phoneClickMetadataSchema = import_zod6.z.object({
621
+ phone_number: import_zod6.z.string(),
622
+ page: import_zod6.z.object({
623
+ path: import_zod6.z.string()
624
+ }),
625
+ section: import_zod6.z.string().nullable().optional()
626
+ }).strict();
627
+ var phoneClickConfigSchema = import_zod6.z.object({}).strict();
628
+
629
+ // ../tracking-core/src/events/scroll-depth.ts
630
+ var import_zod7 = require("zod");
631
+ var scrollDepthMetadataSchema = import_zod7.z.object({
632
+ depth_percent: import_zod7.z.number().int().min(1).max(100),
633
+ page: import_zod7.z.object({
634
+ path: import_zod7.z.string()
635
+ })
636
+ }).strict();
637
+ var scrollDepthConfigSchema = import_zod7.z.object({
638
+ thresholds: import_zod7.z.array(import_zod7.z.number().int().min(1).max(100)).min(1)
639
+ }).strict();
640
+
641
+ // ../tracking-core/src/events/specific-page-visit.ts
642
+ var import_zod8 = require("zod");
643
+ var SPECIFIC_PAGE_NAMES = [
644
+ "contact_page",
645
+ "about_page",
646
+ "services_page",
647
+ "booking_page",
648
+ "location_page",
649
+ "pricing_page",
650
+ "faq_page",
651
+ "testimonials_page"
652
+ ];
653
+ var specificPageNameSchema = import_zod8.z.enum(SPECIFIC_PAGE_NAMES);
654
+ var specificPageVisitMetadataSchema = import_zod8.z.object({
655
+ page_name: specificPageNameSchema,
656
+ page: import_zod8.z.object({
657
+ path: import_zod8.z.string()
658
+ })
659
+ }).strict();
660
+ var specificPageVisitConfigSchema = import_zod8.z.object({
661
+ pages: import_zod8.z.array(
662
+ import_zod8.z.object({
663
+ name: specificPageNameSchema,
664
+ pathPattern: import_zod8.z.custom((value) => value instanceof RegExp, {
665
+ message: "pathPattern must be a RegExp"
666
+ })
667
+ })
668
+ ).min(1)
669
+ }).strict();
670
+
671
+ // ../tracking-core/src/events/time-on-site.ts
672
+ var import_zod9 = require("zod");
673
+ var timeOnSiteMetadataSchema = import_zod9.z.object({
674
+ duration_ms: import_zod9.z.number().int().nonnegative(),
675
+ page: import_zod9.z.object({
676
+ path: import_zod9.z.string()
677
+ })
678
+ }).strict();
679
+ var timeOnSiteConfigSchema = import_zod9.z.object({
680
+ thresholdSeconds: import_zod9.z.number().int().positive()
612
681
  }).strict();
613
682
 
614
683
  // ../tracking-core/src/events/registry.ts
@@ -624,10 +693,25 @@ var EVENT_REGISTRY = {
624
693
  metadataSchema: timeOnSiteMetadataSchema,
625
694
  configSchema: timeOnSiteConfigSchema
626
695
  },
627
- contact_page_visit: {
696
+ specific_page_visit: {
697
+ kind: "automatic",
698
+ metadataSchema: specificPageVisitMetadataSchema,
699
+ configSchema: specificPageVisitConfigSchema
700
+ },
701
+ scroll_depth: {
702
+ kind: "automatic",
703
+ metadataSchema: scrollDepthMetadataSchema,
704
+ configSchema: scrollDepthConfigSchema
705
+ },
706
+ multi_page_session: {
628
707
  kind: "automatic",
629
- metadataSchema: contactPageVisitMetadataSchema,
630
- configSchema: contactPageVisitConfigSchema
708
+ metadataSchema: multiPageSessionMetadataSchema,
709
+ configSchema: multiPageSessionConfigSchema
710
+ },
711
+ form_start: {
712
+ kind: "automatic",
713
+ metadataSchema: formStartMetadataSchema,
714
+ configSchema: formStartConfigSchema
631
715
  },
632
716
  // --- manual triggers ---
633
717
  form_submit: {
@@ -639,6 +723,11 @@ var EVENT_REGISTRY = {
639
723
  kind: "manual",
640
724
  metadataSchema: phoneClickMetadataSchema,
641
725
  configSchema: phoneClickConfigSchema
726
+ },
727
+ cta_click: {
728
+ kind: "manual",
729
+ metadataSchema: ctaClickMetadataSchema,
730
+ configSchema: ctaClickConfigSchema
642
731
  }
643
732
  };
644
733
  var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
@@ -729,6 +818,144 @@ function attachTimeOnSite(client, config) {
729
818
  };
730
819
  }
731
820
 
821
+ // ../tracking-core/src/triggers/scroll-depth.ts
822
+ function attachScrollDepth(client, config) {
823
+ if (typeof window === "undefined" || typeof document === "undefined") {
824
+ return () => {
825
+ };
826
+ }
827
+ const thresholds = new Set(config.thresholds);
828
+ let firedForPath = /* @__PURE__ */ new Set();
829
+ let currentPath = window.location.pathname;
830
+ let rafId = null;
831
+ function getScrollPercent() {
832
+ const doc = document.documentElement;
833
+ const scrollTop = window.scrollY || doc.scrollTop;
834
+ const scrollHeight = doc.scrollHeight;
835
+ const clientHeight = doc.clientHeight;
836
+ if (scrollHeight <= clientHeight) return 100;
837
+ return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
838
+ }
839
+ function checkThresholds() {
840
+ const percent = getScrollPercent();
841
+ for (const threshold of thresholds) {
842
+ if (percent >= threshold && !firedForPath.has(threshold)) {
843
+ firedForPath.add(threshold);
844
+ client.trackEvent({
845
+ eventType: "scroll_depth",
846
+ metadata: {
847
+ depth_percent: threshold,
848
+ page: { path: currentPath }
849
+ },
850
+ pageUrl: window.location.href,
851
+ occurredAt: null
852
+ });
853
+ }
854
+ }
855
+ }
856
+ function onScroll() {
857
+ if (rafId !== null) return;
858
+ rafId = requestAnimationFrame(() => {
859
+ rafId = null;
860
+ checkThresholds();
861
+ });
862
+ }
863
+ function resetIfPathChanged() {
864
+ const newPath = window.location.pathname;
865
+ if (newPath === currentPath) return;
866
+ currentPath = newPath;
867
+ firedForPath = /* @__PURE__ */ new Set();
868
+ setTimeout(checkThresholds, 0);
869
+ }
870
+ const originalPushState = history.pushState.bind(history);
871
+ const originalReplaceState = history.replaceState.bind(history);
872
+ function patchedPushState(...args) {
873
+ originalPushState(...args);
874
+ setTimeout(resetIfPathChanged, 0);
875
+ }
876
+ function patchedReplaceState(...args) {
877
+ originalReplaceState(...args);
878
+ setTimeout(resetIfPathChanged, 0);
879
+ }
880
+ history.pushState = patchedPushState;
881
+ history.replaceState = patchedReplaceState;
882
+ window.addEventListener("popstate", resetIfPathChanged);
883
+ window.addEventListener("scroll", onScroll, { passive: true });
884
+ setTimeout(checkThresholds, 0);
885
+ return () => {
886
+ if (rafId !== null) cancelAnimationFrame(rafId);
887
+ history.pushState = originalPushState;
888
+ history.replaceState = originalReplaceState;
889
+ window.removeEventListener("popstate", resetIfPathChanged);
890
+ window.removeEventListener("scroll", onScroll);
891
+ };
892
+ }
893
+
894
+ // ../tracking-core/src/triggers/form-start.ts
895
+ function attachFormStart(client, config) {
896
+ if (typeof window === "undefined" || typeof document === "undefined") {
897
+ return () => {
898
+ };
899
+ }
900
+ const selector = config.selector ?? "form";
901
+ let firedForms = /* @__PURE__ */ new Set();
902
+ let currentPath = window.location.pathname;
903
+ function getFormKey(form) {
904
+ if (form.id) return `id:${form.id}`;
905
+ const explicitAction = form.getAttribute("action");
906
+ if (explicitAction) return `action:${explicitAction}`;
907
+ const forms = Array.from(document.querySelectorAll(selector));
908
+ return `index:${forms.indexOf(form)}`;
909
+ }
910
+ function onFocusIn(event) {
911
+ const target = event.target;
912
+ if (!(target instanceof HTMLElement)) return;
913
+ const form = target.closest(selector);
914
+ if (!form || form.tagName !== "FORM") return;
915
+ const key = getFormKey(form);
916
+ if (firedForms.has(key)) return;
917
+ firedForms.add(key);
918
+ client.trackEvent({
919
+ eventType: "form_start",
920
+ metadata: {
921
+ form: {
922
+ id: form.id || "",
923
+ action: form.getAttribute("action") ?? null
924
+ },
925
+ page: { path: window.location.pathname }
926
+ },
927
+ pageUrl: window.location.href,
928
+ occurredAt: null
929
+ });
930
+ }
931
+ function resetIfPathChanged() {
932
+ const newPath = window.location.pathname;
933
+ if (newPath === currentPath) return;
934
+ currentPath = newPath;
935
+ firedForms = /* @__PURE__ */ new Set();
936
+ }
937
+ const originalPushState = history.pushState.bind(history);
938
+ const originalReplaceState = history.replaceState.bind(history);
939
+ function patchedPushState(...args) {
940
+ originalPushState(...args);
941
+ setTimeout(resetIfPathChanged, 0);
942
+ }
943
+ function patchedReplaceState(...args) {
944
+ originalReplaceState(...args);
945
+ setTimeout(resetIfPathChanged, 0);
946
+ }
947
+ history.pushState = patchedPushState;
948
+ history.replaceState = patchedReplaceState;
949
+ window.addEventListener("popstate", resetIfPathChanged);
950
+ document.addEventListener("focusin", onFocusIn);
951
+ return () => {
952
+ history.pushState = originalPushState;
953
+ history.replaceState = originalReplaceState;
954
+ window.removeEventListener("popstate", resetIfPathChanged);
955
+ document.removeEventListener("focusin", onFocusIn);
956
+ };
957
+ }
958
+
732
959
  // src/ConsentBanner.tsx
733
960
  var import_react = require("react");
734
961
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -826,6 +1053,11 @@ function GoogleAdsTracking({ gtagId }) {
826
1053
 
827
1054
  // src/factory.tsx
828
1055
  var import_navigation = require("next/navigation");
1056
+
1057
+ // package.json
1058
+ var version = "0.5.0";
1059
+
1060
+ // src/factory.tsx
829
1061
  var import_react3 = require("react");
830
1062
  var import_jsx_runtime3 = require("react/jsx-runtime");
831
1063
  function createTracking(options) {
@@ -850,7 +1082,7 @@ function createTracking(options) {
850
1082
  };
851
1083
  }
852
1084
  const TrackingContext = (0, import_react3.createContext)(null);
853
- const contactPageVisitConfig = triggers.automatic.contact_page_visit ?? null;
1085
+ const specificPageVisitConfig = triggers.automatic.specific_page_visit ?? null;
854
1086
  const timeOnSiteConfig = triggers.automatic.time_on_site ?? null;
855
1087
  function TrackingProvider({ children }) {
856
1088
  const pathname = (0, import_navigation.usePathname)();
@@ -862,6 +1094,7 @@ function createTracking(options) {
862
1094
  endpoint,
863
1095
  surface: "next",
864
1096
  packageName: "@aranova/tracking-next",
1097
+ sdkVersion: version,
865
1098
  debug
866
1099
  }),
867
1100
  triggers,
@@ -879,12 +1112,9 @@ function createTracking(options) {
879
1112
  });
880
1113
  fireManualPageView(rawClient);
881
1114
  }, [pathname, searchParams]);
882
- const lastContactPathRef = (0, import_react3.useRef)(null);
1115
+ const firedPagesRef = (0, import_react3.useRef)(/* @__PURE__ */ new Set());
883
1116
  (0, import_react3.useEffect)(() => {
884
- if (!contactPageVisitConfig || pathname === null) return;
885
- if (!contactPageVisitConfig.pathPattern.test(pathname)) return;
886
- if (lastContactPathRef.current === pathname) return;
887
- lastContactPathRef.current = pathname;
1117
+ if (!specificPageVisitConfig || pathname === null) return;
888
1118
  const rawClient = getOrCreateTrackingClient({
889
1119
  apiKey,
890
1120
  endpoint,
@@ -892,12 +1122,18 @@ function createTracking(options) {
892
1122
  packageName: "@aranova/tracking-next",
893
1123
  debug
894
1124
  });
895
- rawClient.trackEvent({
896
- eventType: "contact_page_visit",
897
- metadata: { page: { path: pathname } },
898
- pageUrl: null,
899
- occurredAt: null
900
- });
1125
+ for (const { name, pathPattern } of specificPageVisitConfig.pages) {
1126
+ if (!pathPattern.test(pathname)) continue;
1127
+ const key = `${name}:${pathname}`;
1128
+ if (firedPagesRef.current.has(key)) continue;
1129
+ firedPagesRef.current.add(key);
1130
+ rawClient.trackEvent({
1131
+ eventType: "specific_page_visit",
1132
+ metadata: { page_name: name, page: { path: pathname } },
1133
+ pageUrl: null,
1134
+ occurredAt: null
1135
+ });
1136
+ }
901
1137
  }, [pathname]);
902
1138
  (0, import_react3.useEffect)(() => {
903
1139
  const rawClient = getOrCreateTrackingClient({
@@ -920,6 +1156,74 @@ function createTracking(options) {
920
1156
  });
921
1157
  return attachTimeOnSite(rawClient, timeOnSiteConfig);
922
1158
  }, []);
1159
+ const multiPageSessionConfig = triggers.automatic.multi_page_session ?? null;
1160
+ const mpsLastPathRef = (0, import_react3.useRef)(null);
1161
+ (0, import_react3.useEffect)(() => {
1162
+ if (!multiPageSessionConfig || pathname === null) return;
1163
+ if (pathname === mpsLastPathRef.current) return;
1164
+ mpsLastPathRef.current = pathname;
1165
+ const MPS_PATHS_KEY = "aranova_tracking_mps_paths";
1166
+ const MPS_SESSION_KEY = "aranova_tracking_mps_session";
1167
+ const MPS_FIRED_KEY = "aranova_tracking_mps_fired";
1168
+ try {
1169
+ const store = window.sessionStorage;
1170
+ const currentSession = getOrRotateSessionId();
1171
+ const storedSession = store.getItem(MPS_SESSION_KEY);
1172
+ if (storedSession !== currentSession) {
1173
+ store.setItem(MPS_SESSION_KEY, currentSession);
1174
+ store.removeItem(MPS_PATHS_KEY);
1175
+ store.removeItem(MPS_FIRED_KEY);
1176
+ }
1177
+ if (store.getItem(MPS_FIRED_KEY) === "1") return;
1178
+ const raw = store.getItem(MPS_PATHS_KEY);
1179
+ const paths = raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
1180
+ paths.add(pathname);
1181
+ store.setItem(MPS_PATHS_KEY, JSON.stringify([...paths]));
1182
+ if (paths.size >= multiPageSessionConfig.pageThreshold) {
1183
+ store.setItem(MPS_FIRED_KEY, "1");
1184
+ const rawClient = getOrCreateTrackingClient({
1185
+ apiKey,
1186
+ endpoint,
1187
+ surface: "next",
1188
+ packageName: "@aranova/tracking-next",
1189
+ debug
1190
+ });
1191
+ rawClient.trackEvent({
1192
+ eventType: "multi_page_session",
1193
+ metadata: {
1194
+ page_count: paths.size,
1195
+ page: { path: pathname }
1196
+ },
1197
+ pageUrl: null,
1198
+ occurredAt: null
1199
+ });
1200
+ }
1201
+ } catch {
1202
+ }
1203
+ }, [pathname]);
1204
+ (0, import_react3.useEffect)(() => {
1205
+ const detachers = [];
1206
+ const rawClient = getOrCreateTrackingClient({
1207
+ apiKey,
1208
+ endpoint,
1209
+ surface: "next",
1210
+ packageName: "@aranova/tracking-next",
1211
+ debug
1212
+ });
1213
+ const scrollDepth = triggers.automatic.scroll_depth;
1214
+ if (scrollDepth) {
1215
+ detachers.push(attachScrollDepth(rawClient, scrollDepth));
1216
+ }
1217
+ const formStart = triggers.automatic.form_start;
1218
+ if (formStart) {
1219
+ detachers.push(attachFormStart(rawClient, formStart));
1220
+ }
1221
+ return () => {
1222
+ for (let i = detachers.length - 1; i >= 0; i--) {
1223
+ detachers[i]();
1224
+ }
1225
+ };
1226
+ }, []);
923
1227
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrackingContext.Provider, { value: client, children });
924
1228
  }
925
1229
  function useTracking() {