@aranova/tracking-next 0.15.0 → 0.16.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
@@ -1191,6 +1191,7 @@ function createTrackingClient(config) {
1191
1191
  return {
1192
1192
  trackEvent,
1193
1193
  flush,
1194
+ flushBeacon: flushOnUnload,
1194
1195
  getSessionId: () => sessionId,
1195
1196
  getVisitorId: () => visitorId,
1196
1197
  destroy: () => {
@@ -1215,9 +1216,17 @@ var ctaClickMetadataSchema = z2.object({
1215
1216
  path: z2.string()
1216
1217
  }).strict(),
1217
1218
  section: z2.string().nullable().optional(),
1218
- destination_url: z2.string().nullable().optional()
1219
+ destination_url: z2.string().nullable().optional(),
1220
+ // Set by auto-capture (and available to manual callers): the link target
1221
+ // and a short element descriptor (tag#id) for tying clicks to specific UI.
1222
+ href: z2.string().nullable().optional(),
1223
+ element: z2.string().nullable().optional()
1224
+ }).strict();
1225
+ var ctaClickConfigSchema = z2.object({
1226
+ autoCapture: z2.object({
1227
+ selector: z2.string().optional()
1228
+ }).strict().optional()
1219
1229
  }).strict();
1220
- var ctaClickConfigSchema = z2.object({}).strict();
1221
1230
 
1222
1231
  // ../tracking-core/src/events/sdk-heartbeat.ts
1223
1232
  import { z as z3 } from "zod";
@@ -1293,31 +1302,44 @@ var multiPageSessionConfigSchema = z6.object({
1293
1302
  pageThreshold: z6.number().int().min(2)
1294
1303
  }).strict();
1295
1304
 
1296
- // ../tracking-core/src/events/phone-click.ts
1305
+ // ../tracking-core/src/events/page-exit.ts
1297
1306
  import { z as z7 } from "zod";
1298
- var phoneClickMetadataSchema = z7.object({
1299
- phone_number: z7.string(),
1307
+ var pageExitMetadataSchema = z7.object({
1308
+ dwell_ms: z7.number().int().min(0),
1309
+ // null = left without any scroll signal; floor is 0 so a valid 0% is never
1310
+ // rejected (a single bad field 422s the whole keepalive beacon batch).
1311
+ max_scroll_percent: z7.number().int().min(0).max(100).nullable(),
1300
1312
  page: z7.object({
1301
1313
  path: z7.string()
1302
- }).strict(),
1303
- section: z7.string().nullable().optional()
1314
+ }).strict()
1304
1315
  }).strict();
1305
- var phoneClickConfigSchema = z7.object({}).strict();
1316
+ var pageExitConfigSchema = z7.object({}).strict();
1306
1317
 
1307
- // ../tracking-core/src/events/scroll-depth.ts
1318
+ // ../tracking-core/src/events/phone-click.ts
1308
1319
  import { z as z8 } from "zod";
1309
- var scrollDepthMetadataSchema = z8.object({
1310
- depth_percent: z8.number().int().min(1).max(100),
1320
+ var phoneClickMetadataSchema = z8.object({
1321
+ phone_number: z8.string(),
1311
1322
  page: z8.object({
1312
1323
  path: z8.string()
1324
+ }).strict(),
1325
+ section: z8.string().nullable().optional()
1326
+ }).strict();
1327
+ var phoneClickConfigSchema = z8.object({}).strict();
1328
+
1329
+ // ../tracking-core/src/events/scroll-depth.ts
1330
+ import { z as z9 } from "zod";
1331
+ var scrollDepthMetadataSchema = z9.object({
1332
+ depth_percent: z9.number().int().min(1).max(100),
1333
+ page: z9.object({
1334
+ path: z9.string()
1313
1335
  }).strict()
1314
1336
  }).strict();
1315
- var scrollDepthConfigSchema = z8.object({
1316
- thresholds: z8.array(z8.number().int().min(1).max(100)).min(1)
1337
+ var scrollDepthConfigSchema = z9.object({
1338
+ thresholds: z9.array(z9.number().int().min(1).max(100)).min(1)
1317
1339
  }).strict();
1318
1340
 
1319
1341
  // ../tracking-core/src/events/specific-page-visit.ts
1320
- import { z as z9 } from "zod";
1342
+ import { z as z10 } from "zod";
1321
1343
  var SPECIFIC_PAGE_NAMES = [
1322
1344
  "contact_page",
1323
1345
  "about_page",
@@ -1328,18 +1350,18 @@ var SPECIFIC_PAGE_NAMES = [
1328
1350
  "faq_page",
1329
1351
  "testimonials_page"
1330
1352
  ];
1331
- var specificPageNameSchema = z9.enum(SPECIFIC_PAGE_NAMES);
1332
- var specificPageVisitMetadataSchema = z9.object({
1353
+ var specificPageNameSchema = z10.enum(SPECIFIC_PAGE_NAMES);
1354
+ var specificPageVisitMetadataSchema = z10.object({
1333
1355
  page_name: specificPageNameSchema,
1334
- page: z9.object({
1335
- path: z9.string()
1356
+ page: z10.object({
1357
+ path: z10.string()
1336
1358
  }).strict()
1337
1359
  }).strict();
1338
- var specificPageVisitConfigSchema = z9.object({
1339
- pages: z9.array(
1340
- z9.object({
1360
+ var specificPageVisitConfigSchema = z10.object({
1361
+ pages: z10.array(
1362
+ z10.object({
1341
1363
  name: specificPageNameSchema,
1342
- pathPattern: z9.custom((value) => value instanceof RegExp, {
1364
+ pathPattern: z10.custom((value) => value instanceof RegExp, {
1343
1365
  message: "pathPattern must be a RegExp"
1344
1366
  })
1345
1367
  }).strict()
@@ -1347,15 +1369,15 @@ var specificPageVisitConfigSchema = z9.object({
1347
1369
  }).strict();
1348
1370
 
1349
1371
  // ../tracking-core/src/events/time-on-site.ts
1350
- import { z as z10 } from "zod";
1351
- var timeOnSiteMetadataSchema = z10.object({
1352
- duration_ms: z10.number().int().nonnegative(),
1353
- page: z10.object({
1354
- path: z10.string()
1372
+ import { z as z11 } from "zod";
1373
+ var timeOnSiteMetadataSchema = z11.object({
1374
+ duration_ms: z11.number().int().nonnegative(),
1375
+ page: z11.object({
1376
+ path: z11.string()
1355
1377
  }).strict()
1356
1378
  }).strict();
1357
- var timeOnSiteConfigSchema = z10.object({
1358
- thresholdSeconds: z10.number().int().positive()
1379
+ var timeOnSiteConfigSchema = z11.object({
1380
+ thresholdSeconds: z11.number().int().positive()
1359
1381
  }).strict();
1360
1382
 
1361
1383
  // ../tracking-core/src/events/registry.ts
@@ -1397,6 +1419,11 @@ var EVENT_REGISTRY = {
1397
1419
  metadataSchema: sdkHeartbeatMetadataSchema,
1398
1420
  configSchema: sdkHeartbeatConfigSchema
1399
1421
  },
1422
+ page_exit: {
1423
+ kind: "automatic",
1424
+ metadataSchema: pageExitMetadataSchema,
1425
+ configSchema: pageExitConfigSchema
1426
+ },
1400
1427
  // --- manual triggers ---
1401
1428
  form_submit: {
1402
1429
  kind: "manual",
@@ -1640,6 +1667,163 @@ function attachFormStart(client, config) {
1640
1667
  };
1641
1668
  }
1642
1669
 
1670
+ // ../tracking-core/src/triggers/page-exit.ts
1671
+ function attachPageExit(client) {
1672
+ if (typeof window === "undefined" || typeof document === "undefined") {
1673
+ return () => {
1674
+ };
1675
+ }
1676
+ let currentPath2 = window.location.pathname;
1677
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
1678
+ let accumulatedMs = 0;
1679
+ let maxScrollPercent = null;
1680
+ let rafId = null;
1681
+ function getScrollPercent() {
1682
+ const doc = document.documentElement;
1683
+ const scrollTop = window.scrollY || doc.scrollTop;
1684
+ const scrollHeight = doc.scrollHeight;
1685
+ const clientHeight = doc.clientHeight;
1686
+ if (scrollHeight <= clientHeight) return 100;
1687
+ return Math.round((scrollTop + clientHeight) / scrollHeight * 100);
1688
+ }
1689
+ function onScroll() {
1690
+ if (rafId !== null) return;
1691
+ rafId = requestAnimationFrame(() => {
1692
+ rafId = null;
1693
+ const percent = getScrollPercent();
1694
+ if (percent >= 1 && (maxScrollPercent === null || percent > maxScrollPercent)) {
1695
+ maxScrollPercent = Math.min(percent, 100);
1696
+ }
1697
+ });
1698
+ }
1699
+ function settledDwellMs() {
1700
+ let total = accumulatedMs;
1701
+ if (activeSince !== null) {
1702
+ total += Date.now() - activeSince;
1703
+ }
1704
+ return Math.max(0, Math.round(total));
1705
+ }
1706
+ function emitSegment(path, flush) {
1707
+ const dwell = settledDwellMs();
1708
+ if (dwell === 0) return;
1709
+ client.trackEvent({
1710
+ eventType: "page_exit",
1711
+ metadata: {
1712
+ dwell_ms: dwell,
1713
+ max_scroll_percent: maxScrollPercent,
1714
+ page: { path }
1715
+ },
1716
+ pageUrl: window.location.href,
1717
+ occurredAt: null
1718
+ });
1719
+ if (flush) {
1720
+ client.flushBeacon();
1721
+ }
1722
+ accumulatedMs = 0;
1723
+ activeSince = document.visibilityState === "visible" ? Date.now() : null;
1724
+ }
1725
+ function onNavigate() {
1726
+ const newPath = window.location.pathname;
1727
+ if (newPath === currentPath2) return;
1728
+ emitSegment(currentPath2, false);
1729
+ currentPath2 = newPath;
1730
+ maxScrollPercent = null;
1731
+ accumulatedMs = 0;
1732
+ activeSince = document.visibilityState === "visible" ? Date.now() : null;
1733
+ }
1734
+ function onVisibilityChange() {
1735
+ if (document.visibilityState === "hidden") {
1736
+ emitSegment(currentPath2, true);
1737
+ } else {
1738
+ activeSince = Date.now();
1739
+ }
1740
+ }
1741
+ function onPageHide() {
1742
+ emitSegment(currentPath2, true);
1743
+ }
1744
+ const originalPushState = history.pushState.bind(history);
1745
+ const originalReplaceState = history.replaceState.bind(history);
1746
+ function patchedPushState(...args) {
1747
+ originalPushState(...args);
1748
+ setTimeout(onNavigate, 0);
1749
+ }
1750
+ function patchedReplaceState(...args) {
1751
+ originalReplaceState(...args);
1752
+ setTimeout(onNavigate, 0);
1753
+ }
1754
+ history.pushState = patchedPushState;
1755
+ history.replaceState = patchedReplaceState;
1756
+ window.addEventListener("popstate", onNavigate);
1757
+ window.addEventListener("scroll", onScroll, { passive: true });
1758
+ document.addEventListener("visibilitychange", onVisibilityChange);
1759
+ window.addEventListener("pagehide", onPageHide);
1760
+ return () => {
1761
+ if (rafId !== null) cancelAnimationFrame(rafId);
1762
+ history.pushState = originalPushState;
1763
+ history.replaceState = originalReplaceState;
1764
+ window.removeEventListener("popstate", onNavigate);
1765
+ window.removeEventListener("scroll", onScroll);
1766
+ document.removeEventListener("visibilitychange", onVisibilityChange);
1767
+ window.removeEventListener("pagehide", onPageHide);
1768
+ };
1769
+ }
1770
+
1771
+ // ../tracking-core/src/triggers/cta-click-capture.ts
1772
+ var DEFAULT_CTA_SELECTOR = "[data-aranova-cta]";
1773
+ var CTA_NAME_MAX_LENGTH = 120;
1774
+ function describeElement(el) {
1775
+ const tag = el.tagName.toLowerCase();
1776
+ return el.id ? `${tag}#${el.id}` : tag;
1777
+ }
1778
+ function resolveCtaName(el) {
1779
+ const explicit = el.getAttribute("data-aranova-cta");
1780
+ if (explicit && explicit.trim().length > 0) return explicit.trim();
1781
+ const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");
1782
+ if (text.length > 0) return text.slice(0, CTA_NAME_MAX_LENGTH);
1783
+ return describeElement(el);
1784
+ }
1785
+ function attachCtaClickCapture(client, config) {
1786
+ if (typeof window === "undefined" || typeof document === "undefined") {
1787
+ return () => {
1788
+ };
1789
+ }
1790
+ const autoCapture = config.autoCapture;
1791
+ if (!autoCapture) {
1792
+ return () => {
1793
+ };
1794
+ }
1795
+ const selector = autoCapture.selector ?? DEFAULT_CTA_SELECTOR;
1796
+ function onClick(event) {
1797
+ const target = event.target;
1798
+ if (!(target instanceof Element)) return;
1799
+ let matched = null;
1800
+ try {
1801
+ matched = target.closest(selector);
1802
+ } catch {
1803
+ return;
1804
+ }
1805
+ if (matched === null) return;
1806
+ const href = matched instanceof HTMLAnchorElement ? matched.href || null : matched.getAttribute("href");
1807
+ client.trackEvent({
1808
+ eventType: "cta_click",
1809
+ metadata: {
1810
+ cta_name: resolveCtaName(matched),
1811
+ page: { path: window.location.pathname },
1812
+ section: matched.getAttribute("data-aranova-section"),
1813
+ destination_url: href,
1814
+ href,
1815
+ element: describeElement(matched)
1816
+ },
1817
+ pageUrl: window.location.href,
1818
+ occurredAt: null
1819
+ });
1820
+ }
1821
+ document.addEventListener("click", onClick, true);
1822
+ return () => {
1823
+ document.removeEventListener("click", onClick, true);
1824
+ };
1825
+ }
1826
+
1643
1827
  // ../tracking-core/src/resources/sales/errors.ts
1644
1828
  var AranovaApiError = class extends Error {
1645
1829
  constructor(message, options) {
@@ -1846,42 +2030,42 @@ function createSalesClient(config) {
1846
2030
  }
1847
2031
 
1848
2032
  // ../tracking-core/src/resources/sales/schema.ts
1849
- import { z as z11 } from "zod";
2033
+ import { z as z12 } from "zod";
1850
2034
  var SUPPORTED_CURRENCIES = ["USD", "CAD"];
1851
2035
  var TRACKING_ENVIRONMENTS = ["production", "development"];
1852
- var currencySchema = z11.enum(SUPPORTED_CURRENCIES);
1853
- var centsSchema = z11.number().int().nonnegative();
1854
- var quantitySchema = z11.string().regex(/^\d+(\.\d{1,3})?$/);
1855
- var metadataSchema = z11.record(z11.unknown());
1856
- var saleItemSchema = z11.object({
1857
- external_item_id: z11.string().nullable().optional(),
1858
- name: z11.string().nullable().optional(),
1859
- category: z11.string().nullable().optional(),
2036
+ var currencySchema = z12.enum(SUPPORTED_CURRENCIES);
2037
+ var centsSchema = z12.number().int().nonnegative();
2038
+ var quantitySchema = z12.string().regex(/^\d+(\.\d{1,3})?$/);
2039
+ var metadataSchema = z12.record(z12.unknown());
2040
+ var saleItemSchema = z12.object({
2041
+ external_item_id: z12.string().nullable().optional(),
2042
+ name: z12.string().nullable().optional(),
2043
+ category: z12.string().nullable().optional(),
1860
2044
  quantity: quantitySchema,
1861
2045
  unit_price_cents: centsSchema,
1862
2046
  // Non-negativity validated on the wire — same contract as the other cents
1863
2047
  // fields — and backstopped by the DB CHECK.
1864
2048
  unit_cost_cents: centsSchema.nullable().optional()
1865
2049
  }).strict();
1866
- var saleServiceSchema = z11.object({
1867
- service: z11.string(),
2050
+ var saleServiceSchema = z12.object({
2051
+ service: z12.string(),
1868
2052
  amount_cents: centsSchema
1869
2053
  }).strict();
1870
- var customerNameSchema = z11.string().max(200);
1871
- var customerPhoneSchema = z11.string().max(64);
1872
- var customerEmailSchema = z11.string().max(320).email();
2054
+ var customerNameSchema = z12.string().max(200);
2055
+ var customerPhoneSchema = z12.string().max(64);
2056
+ var customerEmailSchema = z12.string().max(320).email();
1873
2057
  function refineServiceXor(val, ctx, { requireAmount }) {
1874
2058
  if (val.services != null) {
1875
2059
  if (val.service != null) {
1876
2060
  ctx.addIssue({
1877
- code: z11.ZodIssueCode.custom,
2061
+ code: z12.ZodIssueCode.custom,
1878
2062
  message: "pass either `service` or `services`, not both",
1879
2063
  path: ["services"]
1880
2064
  });
1881
2065
  }
1882
2066
  if (val.services.length === 0) {
1883
2067
  ctx.addIssue({
1884
- code: z11.ZodIssueCode.custom,
2068
+ code: z12.ZodIssueCode.custom,
1885
2069
  message: "`services` must not be empty",
1886
2070
  path: ["services"]
1887
2071
  });
@@ -1889,7 +2073,7 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1889
2073
  const keys = val.services.map((s) => s.service);
1890
2074
  if (new Set(keys).size !== keys.length) {
1891
2075
  ctx.addIssue({
1892
- code: z11.ZodIssueCode.custom,
2076
+ code: z12.ZodIssueCode.custom,
1893
2077
  message: "`services` must not list the same service more than once",
1894
2078
  path: ["services"]
1895
2079
  });
@@ -1898,7 +2082,7 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1898
2082
  const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);
1899
2083
  if (val.amount_total_cents !== sum) {
1900
2084
  ctx.addIssue({
1901
- code: z11.ZodIssueCode.custom,
2085
+ code: z12.ZodIssueCode.custom,
1902
2086
  message: "amount_total_cents must equal the sum of the services amounts (omit it to derive it automatically)",
1903
2087
  path: ["amount_total_cents"]
1904
2088
  });
@@ -1906,37 +2090,37 @@ function refineServiceXor(val, ctx, { requireAmount }) {
1906
2090
  }
1907
2091
  } else if (requireAmount && val.amount_total_cents == null) {
1908
2092
  ctx.addIssue({
1909
- code: z11.ZodIssueCode.custom,
2093
+ code: z12.ZodIssueCode.custom,
1910
2094
  message: "amount_total_cents is required unless `services` is provided",
1911
2095
  path: ["amount_total_cents"]
1912
2096
  });
1913
2097
  }
1914
2098
  }
1915
- var saleCreateSchema = z11.object({
1916
- external_id: z11.string().nullable().optional(),
1917
- description: z11.string().nullable().optional(),
1918
- service: z11.string().nullable().optional(),
1919
- services: z11.array(saleServiceSchema).nullable().optional(),
2099
+ var saleCreateSchema = z12.object({
2100
+ external_id: z12.string().nullable().optional(),
2101
+ description: z12.string().nullable().optional(),
2102
+ service: z12.string().nullable().optional(),
2103
+ services: z12.array(saleServiceSchema).nullable().optional(),
1920
2104
  currency: currencySchema,
1921
2105
  // Optional only because the plural `services` form derives it from the sum
1922
2106
  // (see refineServiceXor); the singular/serviceless path still requires it.
1923
2107
  amount_total_cents: centsSchema.nullable().optional(),
1924
- occurred_at: z11.string().datetime(),
1925
- environment: z11.enum(TRACKING_ENVIRONMENTS).default("production"),
1926
- items: z11.array(saleItemSchema).default([]),
2108
+ occurred_at: z12.string().datetime(),
2109
+ environment: z12.enum(TRACKING_ENVIRONMENTS).default("production"),
2110
+ items: z12.array(saleItemSchema).default([]),
1927
2111
  metadata: metadataSchema.nullable().optional(),
1928
2112
  customer_name: customerNameSchema.nullable().optional(),
1929
2113
  customer_phone: customerPhoneSchema.nullable().optional(),
1930
2114
  customer_email: customerEmailSchema.nullable().optional()
1931
2115
  }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));
1932
- var saleUpdateSchema = z11.object({
1933
- description: z11.string().nullable().optional(),
1934
- service: z11.string().nullable().optional(),
1935
- services: z11.array(saleServiceSchema).nullable().optional(),
2116
+ var saleUpdateSchema = z12.object({
2117
+ description: z12.string().nullable().optional(),
2118
+ service: z12.string().nullable().optional(),
2119
+ services: z12.array(saleServiceSchema).nullable().optional(),
1936
2120
  currency: currencySchema.optional(),
1937
2121
  amount_total_cents: centsSchema.optional(),
1938
- occurred_at: z11.string().datetime().optional(),
1939
- items: z11.array(saleItemSchema).optional(),
2122
+ occurred_at: z12.string().datetime().optional(),
2123
+ items: z12.array(saleItemSchema).optional(),
1940
2124
  metadata: metadataSchema.nullable().optional(),
1941
2125
  customer_name: customerNameSchema.nullable().optional(),
1942
2126
  customer_phone: customerPhoneSchema.nullable().optional(),
@@ -2338,7 +2522,7 @@ function GoogleAdsTracking(props) {
2338
2522
  import { usePathname, useSearchParams } from "next/navigation";
2339
2523
 
2340
2524
  // package.json
2341
- var version = "0.15.0";
2525
+ var version = "0.16.0";
2342
2526
 
2343
2527
  // src/factory.tsx
2344
2528
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect3, useMemo as useMemo2, useRef } from "react";
@@ -2626,6 +2810,7 @@ function createTracking(options) {
2626
2810
  debug
2627
2811
  });
2628
2812
  const detectorClient = wrapClient(rawClient);
2813
+ detachers.push(attachPageExit(detectorClient));
2629
2814
  const scrollDepth = triggers.automatic.scroll_depth;
2630
2815
  if (scrollDepth) {
2631
2816
  detachers.push(attachScrollDepth(detectorClient, scrollDepth));
@@ -2634,6 +2819,10 @@ function createTracking(options) {
2634
2819
  if (formStart) {
2635
2820
  detachers.push(attachFormStart(detectorClient, formStart));
2636
2821
  }
2822
+ const ctaClick = triggers.manual?.cta_click;
2823
+ if (ctaClick) {
2824
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
2825
+ }
2637
2826
  return () => {
2638
2827
  for (let i = detachers.length - 1; i >= 0; i--) {
2639
2828
  detachers[i]();