@aranova/tracking-next 0.17.0 → 0.17.2

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/README.md CHANGED
@@ -169,6 +169,41 @@ export function LeadForm() {
169
169
 
170
170
  `fields[].value` can be any JSON value: string, number, boolean, null, array, or object. Only send reviewed, allowlisted, non-sensitive values; do not send names, emails, phone numbers entered by the visitor, addresses, payment data, medical details, passwords, file contents, or free-text messages.
171
171
 
172
+ ### Phone clicks (`tel:` taps) — manual or auto-capture
173
+
174
+ Opt into **auto-capture** and every `tel:` link is tracked with no per-link code. Add
175
+ `autoCapture` to the `phone_click` registration — a single delegated click listener does the rest:
176
+
177
+ ```tsx
178
+ manual: {
179
+ phone_click: { autoCapture: {} }, // default selector: a[href^="tel:"]
180
+ // or narrow it: { autoCapture: { selector: "a.call" } }
181
+ },
182
+ ```
183
+
184
+ ```tsx
185
+ <a href="tel:+14165550199" data-aranova-section="header">
186
+ (416) 555-0199
187
+ </a>
188
+ ```
189
+
190
+ The number is read from the `href` (normalized to E.164); `section` comes from an optional
191
+ `data-aranova-section`. **Unlike `cta_click`, a `phone_click` also fires the Google Ads
192
+ conversion** for a linked phone-call goal (a `CLICK_TO_CALL` action) — auto-fired the moment the
193
+ tap is captured, so you never call `trackConversion` (firing needs the sales client wired with the
194
+ same config; see below).
195
+
196
+ Prefer to instrument links yourself? Fire it from a click handler — this fires the conversion the
197
+ same way:
198
+
199
+ ```tsx
200
+ tracking.trackEvent("phone_click", {
201
+ phone_number: "+14165550199",
202
+ page: { path: window.location.pathname },
203
+ section: "header",
204
+ });
205
+ ```
206
+
172
207
  ### CTA clicks — manual or auto-capture
173
208
 
174
209
  Fire `cta_click` yourself for full control over the name:
package/dist/index.d.mts CHANGED
@@ -436,9 +436,28 @@ type PhoneClickMetadata = z.infer<typeof phoneClickMetadataSchema>;
436
436
  /**
437
437
  * Registration config for `phone_click`.
438
438
  *
439
- * This event is manual-only and currently has no registration options.
439
+ * The event stays manually fireable; `autoCapture` additionally attaches a
440
+ * delegated click listener that fires it for any `tel:` link matching
441
+ * `selector` (default `a[href^="tel:"]`) — link your phone number, get the
442
+ * analytics event (and, for a linked phone-click goal, the conversion) for free.
440
443
  */
441
- declare const phoneClickConfigSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
444
+ declare const phoneClickConfigSchema: z.ZodObject<{
445
+ autoCapture: z.ZodOptional<z.ZodObject<{
446
+ selector: z.ZodOptional<z.ZodString>;
447
+ }, "strict", z.ZodTypeAny, {
448
+ selector?: string | undefined;
449
+ }, {
450
+ selector?: string | undefined;
451
+ }>>;
452
+ }, "strict", z.ZodTypeAny, {
453
+ autoCapture?: {
454
+ selector?: string | undefined;
455
+ } | undefined;
456
+ }, {
457
+ autoCapture?: {
458
+ selector?: string | undefined;
459
+ } | undefined;
460
+ }>;
442
461
  type PhoneClickConfig = z.infer<typeof phoneClickConfigSchema>;
443
462
 
444
463
  /**
@@ -994,7 +1013,23 @@ declare const EVENT_REGISTRY: {
994
1013
  phone_number: string;
995
1014
  section?: string | null | undefined;
996
1015
  }>;
997
- readonly configSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
1016
+ readonly configSchema: z.ZodObject<{
1017
+ autoCapture: z.ZodOptional<z.ZodObject<{
1018
+ selector: z.ZodOptional<z.ZodString>;
1019
+ }, "strict", z.ZodTypeAny, {
1020
+ selector?: string | undefined;
1021
+ }, {
1022
+ selector?: string | undefined;
1023
+ }>>;
1024
+ }, "strict", z.ZodTypeAny, {
1025
+ autoCapture?: {
1026
+ selector?: string | undefined;
1027
+ } | undefined;
1028
+ }, {
1029
+ autoCapture?: {
1030
+ selector?: string | undefined;
1031
+ } | undefined;
1032
+ }>;
998
1033
  };
999
1034
  readonly cta_click: {
1000
1035
  readonly kind: "manual";
package/dist/index.d.ts CHANGED
@@ -436,9 +436,28 @@ type PhoneClickMetadata = z.infer<typeof phoneClickMetadataSchema>;
436
436
  /**
437
437
  * Registration config for `phone_click`.
438
438
  *
439
- * This event is manual-only and currently has no registration options.
439
+ * The event stays manually fireable; `autoCapture` additionally attaches a
440
+ * delegated click listener that fires it for any `tel:` link matching
441
+ * `selector` (default `a[href^="tel:"]`) — link your phone number, get the
442
+ * analytics event (and, for a linked phone-click goal, the conversion) for free.
440
443
  */
441
- declare const phoneClickConfigSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
444
+ declare const phoneClickConfigSchema: z.ZodObject<{
445
+ autoCapture: z.ZodOptional<z.ZodObject<{
446
+ selector: z.ZodOptional<z.ZodString>;
447
+ }, "strict", z.ZodTypeAny, {
448
+ selector?: string | undefined;
449
+ }, {
450
+ selector?: string | undefined;
451
+ }>>;
452
+ }, "strict", z.ZodTypeAny, {
453
+ autoCapture?: {
454
+ selector?: string | undefined;
455
+ } | undefined;
456
+ }, {
457
+ autoCapture?: {
458
+ selector?: string | undefined;
459
+ } | undefined;
460
+ }>;
442
461
  type PhoneClickConfig = z.infer<typeof phoneClickConfigSchema>;
443
462
 
444
463
  /**
@@ -994,7 +1013,23 @@ declare const EVENT_REGISTRY: {
994
1013
  phone_number: string;
995
1014
  section?: string | null | undefined;
996
1015
  }>;
997
- readonly configSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
1016
+ readonly configSchema: z.ZodObject<{
1017
+ autoCapture: z.ZodOptional<z.ZodObject<{
1018
+ selector: z.ZodOptional<z.ZodString>;
1019
+ }, "strict", z.ZodTypeAny, {
1020
+ selector?: string | undefined;
1021
+ }, {
1022
+ selector?: string | undefined;
1023
+ }>>;
1024
+ }, "strict", z.ZodTypeAny, {
1025
+ autoCapture?: {
1026
+ selector?: string | undefined;
1027
+ } | undefined;
1028
+ }, {
1029
+ autoCapture?: {
1030
+ selector?: string | undefined;
1031
+ } | undefined;
1032
+ }>;
998
1033
  };
999
1034
  readonly cta_click: {
1000
1035
  readonly kind: "manual";
package/dist/index.js CHANGED
@@ -784,8 +784,8 @@ function thresholdMet(goal, eventType, metadata) {
784
784
  return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
785
785
  case "page_view":
786
786
  case "form_start":
787
+ case "phone_click":
787
788
  return true;
788
- // no threshold — fire whenever the detector emits
789
789
  default:
790
790
  return false;
791
791
  }
@@ -1122,6 +1122,27 @@ function getOrCreateTrackingClient(config) {
1122
1122
  globalClientKey = key;
1123
1123
  return globalClient;
1124
1124
  }
1125
+ var clientCaptureRegistry = /* @__PURE__ */ new WeakMap();
1126
+ function attachClientCapturesOnce(client, build) {
1127
+ let entry = clientCaptureRegistry.get(client);
1128
+ if (entry === void 0) {
1129
+ entry = { detach: build(), refCount: 0 };
1130
+ clientCaptureRegistry.set(client, entry);
1131
+ }
1132
+ entry.refCount += 1;
1133
+ let released = false;
1134
+ return () => {
1135
+ if (released) return;
1136
+ released = true;
1137
+ const current = clientCaptureRegistry.get(client);
1138
+ if (current === void 0) return;
1139
+ current.refCount -= 1;
1140
+ if (current.refCount <= 0) {
1141
+ current.detach();
1142
+ clientCaptureRegistry.delete(client);
1143
+ }
1144
+ };
1145
+ }
1125
1146
  function createTrackingClient(config) {
1126
1147
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
1127
1148
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -1406,7 +1427,11 @@ var phoneClickMetadataSchema = import_zod8.z.object({
1406
1427
  }).strict(),
1407
1428
  section: import_zod8.z.string().nullable().optional()
1408
1429
  }).strict();
1409
- var phoneClickConfigSchema = import_zod8.z.object({}).strict();
1430
+ var phoneClickConfigSchema = import_zod8.z.object({
1431
+ autoCapture: import_zod8.z.object({
1432
+ selector: import_zod8.z.string().optional()
1433
+ }).strict().optional()
1434
+ }).strict();
1410
1435
 
1411
1436
  // ../tracking-core/src/events/scroll-depth.ts
1412
1437
  var import_zod9 = require("zod");
@@ -1907,6 +1932,99 @@ function attachCtaClickCapture(client, config) {
1907
1932
  };
1908
1933
  }
1909
1934
 
1935
+ // ../tracking-core/src/phone.ts
1936
+ var import_libphonenumber_js = require("libphonenumber-js");
1937
+ var DEFAULT_PHONE_COUNTRY = "CA";
1938
+ function parsePhone(raw, country) {
1939
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
1940
+ const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
1941
+ if (!parsed) {
1942
+ return { e164: null, national: "", international: "", country: region, isValid: false };
1943
+ }
1944
+ const isValid = parsed.isValid();
1945
+ return {
1946
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
1947
+ // (e.g. too few digits) still parses but must not be transmitted.
1948
+ e164: isValid ? parsed.number : null,
1949
+ national: parsed.formatNational(),
1950
+ international: parsed.formatInternational(),
1951
+ country: parsed.country ?? region,
1952
+ isValid
1953
+ };
1954
+ }
1955
+ function toE164(raw, country) {
1956
+ return parsePhone(raw, country).e164;
1957
+ }
1958
+ function formatPhone(value, format = "national", country) {
1959
+ const parsed = parsePhone(value, country);
1960
+ if (typeof format === "function") return format(parsed);
1961
+ switch (format) {
1962
+ case "international":
1963
+ return parsed.international || value;
1964
+ case "e164":
1965
+ return parsed.e164 ?? value;
1966
+ case "national":
1967
+ default:
1968
+ return parsed.national || value;
1969
+ }
1970
+ }
1971
+ function formatPhoneAsTyped(raw, country) {
1972
+ return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
1973
+ }
1974
+
1975
+ // ../tracking-core/src/triggers/phone-click-capture.ts
1976
+ var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
1977
+ function safeDecodeURIComponent(value) {
1978
+ try {
1979
+ return decodeURIComponent(value);
1980
+ } catch {
1981
+ return value;
1982
+ }
1983
+ }
1984
+ function resolvePhoneNumber(el) {
1985
+ const href = el instanceof HTMLAnchorElement ? el.href : el.getAttribute("href") ?? "";
1986
+ const raw = safeDecodeURIComponent(href.replace(/^tel:/i, "").split(";")[0]).trim();
1987
+ return toE164(raw) ?? raw;
1988
+ }
1989
+ function attachPhoneClickCapture(client, config) {
1990
+ if (typeof window === "undefined" || typeof document === "undefined") {
1991
+ return () => {
1992
+ };
1993
+ }
1994
+ const autoCapture = config.autoCapture;
1995
+ if (!autoCapture) {
1996
+ return () => {
1997
+ };
1998
+ }
1999
+ const selector = autoCapture.selector ?? DEFAULT_TEL_SELECTOR;
2000
+ function onClick(event) {
2001
+ const target = event.target;
2002
+ if (!(target instanceof Element)) return;
2003
+ let matched = null;
2004
+ try {
2005
+ matched = target.closest(selector);
2006
+ } catch {
2007
+ return;
2008
+ }
2009
+ if (matched === null) return;
2010
+ const metadata = {
2011
+ phone_number: resolvePhoneNumber(matched),
2012
+ page: { path: window.location.pathname },
2013
+ section: matched.getAttribute("data-aranova-section")
2014
+ };
2015
+ client.trackEvent({
2016
+ eventType: "phone_click",
2017
+ metadata,
2018
+ pageUrl: window.location.href,
2019
+ occurredAt: null
2020
+ });
2021
+ }
2022
+ document.addEventListener("click", onClick, true);
2023
+ return () => {
2024
+ document.removeEventListener("click", onClick, true);
2025
+ };
2026
+ }
2027
+
1910
2028
  // ../tracking-core/src/resources/sales/errors.ts
1911
2029
  var AranovaApiError = class extends Error {
1912
2030
  constructor(message, options) {
@@ -2234,46 +2352,6 @@ async function fetchServices(config) {
2234
2352
  return salesRequest(config, "GET", "/services");
2235
2353
  }
2236
2354
 
2237
- // ../tracking-core/src/phone.ts
2238
- var import_libphonenumber_js = require("libphonenumber-js");
2239
- var DEFAULT_PHONE_COUNTRY = "CA";
2240
- function parsePhone(raw, country) {
2241
- const region = country ?? DEFAULT_PHONE_COUNTRY;
2242
- const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
2243
- if (!parsed) {
2244
- return { e164: null, national: "", international: "", country: region, isValid: false };
2245
- }
2246
- const isValid = parsed.isValid();
2247
- return {
2248
- // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
2249
- // (e.g. too few digits) still parses but must not be transmitted.
2250
- e164: isValid ? parsed.number : null,
2251
- national: parsed.formatNational(),
2252
- international: parsed.formatInternational(),
2253
- country: parsed.country ?? region,
2254
- isValid
2255
- };
2256
- }
2257
- function toE164(raw, country) {
2258
- return parsePhone(raw, country).e164;
2259
- }
2260
- function formatPhone(value, format = "national", country) {
2261
- const parsed = parsePhone(value, country);
2262
- if (typeof format === "function") return format(parsed);
2263
- switch (format) {
2264
- case "international":
2265
- return parsed.international || value;
2266
- case "e164":
2267
- return parsed.e164 ?? value;
2268
- case "national":
2269
- default:
2270
- return parsed.national || value;
2271
- }
2272
- }
2273
- function formatPhoneAsTyped(raw, country) {
2274
- return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
2275
- }
2276
-
2277
2355
  // ../tracking-core/src/phone-field.ts
2278
2356
  function phoneField(name, raw, country) {
2279
2357
  return { name, type: "phone", value: toE164(raw, country) };
@@ -2610,7 +2688,7 @@ function GoogleAdsTracking(props) {
2610
2688
  var import_navigation = require("next/navigation");
2611
2689
 
2612
2690
  // package.json
2613
- var version = "0.17.0";
2691
+ var version = "0.17.2";
2614
2692
 
2615
2693
  // src/factory.tsx
2616
2694
  var import_react4 = require("react");
@@ -2809,33 +2887,6 @@ function createTracking(options) {
2809
2887
  });
2810
2888
  }
2811
2889
  }, [pathname]);
2812
- (0, import_react4.useEffect)(() => {
2813
- const rawClient = getOrCreateTrackingClient({
2814
- apiKey,
2815
- endpoint,
2816
- surface: "next",
2817
- packageName: "@aranova/tracking-next",
2818
- triggers,
2819
- environment,
2820
- activeGtagIds: gtagIds,
2821
- debug
2822
- });
2823
- return attachBfcacheRestore(wrapClient(rawClient));
2824
- }, []);
2825
- (0, import_react4.useEffect)(() => {
2826
- if (!timeOnSiteConfig) return;
2827
- const rawClient = getOrCreateTrackingClient({
2828
- apiKey,
2829
- endpoint,
2830
- surface: "next",
2831
- packageName: "@aranova/tracking-next",
2832
- triggers,
2833
- environment,
2834
- activeGtagIds: gtagIds,
2835
- debug
2836
- });
2837
- return attachTimeOnSite(wrapClient(rawClient), timeOnSiteConfig);
2838
- }, []);
2839
2890
  const multiPageSessionConfig = triggers.automatic.multi_page_session ?? null;
2840
2891
  const mpsLastPathRef = (0, import_react4.useRef)(null);
2841
2892
  (0, import_react4.useEffect)(() => {
@@ -2886,7 +2937,6 @@ function createTracking(options) {
2886
2937
  }
2887
2938
  }, [pathname]);
2888
2939
  (0, import_react4.useEffect)(() => {
2889
- const detachers = [];
2890
2940
  const rawClient = getOrCreateTrackingClient({
2891
2941
  apiKey,
2892
2942
  endpoint,
@@ -2897,25 +2947,36 @@ function createTracking(options) {
2897
2947
  activeGtagIds: gtagIds,
2898
2948
  debug
2899
2949
  });
2900
- const detectorClient = wrapClient(rawClient);
2901
- detachers.push(attachPageExit(detectorClient));
2902
- const scrollDepth = triggers.automatic.scroll_depth;
2903
- if (scrollDepth) {
2904
- detachers.push(attachScrollDepth(detectorClient, scrollDepth));
2905
- }
2906
- const formStart = triggers.automatic.form_start;
2907
- if (formStart) {
2908
- detachers.push(attachFormStart(detectorClient, formStart));
2909
- }
2910
- const ctaClick = triggers.manual?.cta_click;
2911
- if (ctaClick) {
2912
- detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
2913
- }
2914
- return () => {
2915
- for (let i = detachers.length - 1; i >= 0; i--) {
2916
- detachers[i]();
2950
+ return attachClientCapturesOnce(rawClient, () => {
2951
+ const detachers = [];
2952
+ const detectorClient = wrapClient(rawClient);
2953
+ detachers.push(attachPageExit(detectorClient));
2954
+ detachers.push(attachBfcacheRestore(detectorClient));
2955
+ if (timeOnSiteConfig) {
2956
+ detachers.push(attachTimeOnSite(detectorClient, timeOnSiteConfig));
2957
+ }
2958
+ const scrollDepth = triggers.automatic.scroll_depth;
2959
+ if (scrollDepth) {
2960
+ detachers.push(attachScrollDepth(detectorClient, scrollDepth));
2961
+ }
2962
+ const formStart = triggers.automatic.form_start;
2963
+ if (formStart) {
2964
+ detachers.push(attachFormStart(detectorClient, formStart));
2965
+ }
2966
+ const ctaClick = triggers.manual?.cta_click;
2967
+ if (ctaClick) {
2968
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
2917
2969
  }
2918
- };
2970
+ const phoneClick = triggers.manual?.phone_click;
2971
+ if (phoneClick) {
2972
+ detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
2973
+ }
2974
+ return () => {
2975
+ for (let i = detachers.length - 1; i >= 0; i--) {
2976
+ detachers[i]();
2977
+ }
2978
+ };
2979
+ });
2919
2980
  }, []);
2920
2981
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(TrackingContext.Provider, { value: client, children: [
2921
2982
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react4.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PageViewTracker, { fire: firePageView }) }),