@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.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: {
649
+ kind: "automatic",
650
+ metadataSchema: specificPageVisitMetadataSchema,
651
+ configSchema: specificPageVisitConfigSchema
652
+ },
653
+ scroll_depth: {
654
+ kind: "automatic",
655
+ metadataSchema: scrollDepthMetadataSchema,
656
+ configSchema: scrollDepthConfigSchema
657
+ },
658
+ multi_page_session: {
580
659
  kind: "automatic",
581
- metadataSchema: contactPageVisitMetadataSchema,
582
- configSchema: contactPageVisitConfigSchema
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";
@@ -778,6 +1005,11 @@ function GoogleAdsTracking({ gtagId }) {
778
1005
 
779
1006
  // src/factory.tsx
780
1007
  import { usePathname, useSearchParams } from "next/navigation";
1008
+
1009
+ // package.json
1010
+ var version = "0.5.0";
1011
+
1012
+ // src/factory.tsx
781
1013
  import {
782
1014
  createContext,
783
1015
  useContext,
@@ -808,7 +1040,7 @@ function createTracking(options) {
808
1040
  };
809
1041
  }
810
1042
  const TrackingContext = createContext(null);
811
- const contactPageVisitConfig = triggers.automatic.contact_page_visit ?? null;
1043
+ const specificPageVisitConfig = triggers.automatic.specific_page_visit ?? null;
812
1044
  const timeOnSiteConfig = triggers.automatic.time_on_site ?? null;
813
1045
  function TrackingProvider({ children }) {
814
1046
  const pathname = usePathname();
@@ -820,6 +1052,7 @@ function createTracking(options) {
820
1052
  endpoint,
821
1053
  surface: "next",
822
1054
  packageName: "@aranova/tracking-next",
1055
+ sdkVersion: version,
823
1056
  debug
824
1057
  }),
825
1058
  triggers,
@@ -837,12 +1070,9 @@ function createTracking(options) {
837
1070
  });
838
1071
  fireManualPageView(rawClient);
839
1072
  }, [pathname, searchParams]);
840
- const lastContactPathRef = useRef(null);
1073
+ const firedPagesRef = useRef(/* @__PURE__ */ new Set());
841
1074
  useEffect3(() => {
842
- if (!contactPageVisitConfig || pathname === null) return;
843
- if (!contactPageVisitConfig.pathPattern.test(pathname)) return;
844
- if (lastContactPathRef.current === pathname) return;
845
- lastContactPathRef.current = pathname;
1075
+ if (!specificPageVisitConfig || pathname === null) return;
846
1076
  const rawClient = getOrCreateTrackingClient({
847
1077
  apiKey,
848
1078
  endpoint,
@@ -850,12 +1080,18 @@ function createTracking(options) {
850
1080
  packageName: "@aranova/tracking-next",
851
1081
  debug
852
1082
  });
853
- rawClient.trackEvent({
854
- eventType: "contact_page_visit",
855
- metadata: { page: { path: pathname } },
856
- pageUrl: null,
857
- occurredAt: null
858
- });
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
+ }
859
1095
  }, [pathname]);
860
1096
  useEffect3(() => {
861
1097
  const rawClient = getOrCreateTrackingClient({
@@ -878,6 +1114,74 @@ function createTracking(options) {
878
1114
  });
879
1115
  return attachTimeOnSite(rawClient, timeOnSiteConfig);
880
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
+ }, []);
881
1185
  return /* @__PURE__ */ jsx3(TrackingContext.Provider, { value: client, children });
882
1186
  }
883
1187
  function useTracking() {