@aranova/tracking-react 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
@@ -111,6 +111,33 @@ export function LeadForm() {
111
111
 
112
112
  `fields[].value` can be any JSON value: string, number, boolean, null, array, or object. Values must be JSON-serializable because events are stored as JSONB. 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.
113
113
 
114
+ ### Phone clicks (`tel:` taps) — manual or auto-capture
115
+
116
+ Opt into **auto-capture** and every `tel:` link is tracked with no per-link code. Add
117
+ `autoCapture` to the `phone_click` registration — a single delegated click listener does the rest:
118
+
119
+ ```tsx
120
+ manual: {
121
+ phone_click: { autoCapture: {} }, // default selector: a[href^="tel:"]
122
+ // or narrow it: { autoCapture: { selector: "a.call" } }
123
+ },
124
+ ```
125
+
126
+ ```tsx
127
+ <a href="tel:+14165550199" data-aranova-section="header">
128
+ (416) 555-0199
129
+ </a>
130
+ ```
131
+
132
+ The number is read from the `href` (normalized to E.164); `section` comes from an optional
133
+ `data-aranova-section`. **Unlike `cta_click`, a `phone_click` also fires the Google Ads
134
+ conversion** for a linked phone-call goal (a `CLICK_TO_CALL` action) — auto-fired the moment the
135
+ tap is captured, so you never call `trackConversion` (firing needs the sales client wired with the
136
+ same config; see below).
137
+
138
+ Prefer to instrument links yourself? Fire it from a click handler — this fires the conversion the
139
+ same way:
140
+
114
141
  ```tsx
115
142
  tracking.trackEvent("phone_click", {
116
143
  phone_number: "+14165550199",
package/dist/index.d.mts CHANGED
@@ -483,9 +483,28 @@ type PhoneClickMetadata = z.infer<typeof phoneClickMetadataSchema>;
483
483
  /**
484
484
  * Registration config for `phone_click`.
485
485
  *
486
- * This event is manual-only and currently has no registration options.
486
+ * The event stays manually fireable; `autoCapture` additionally attaches a
487
+ * delegated click listener that fires it for any `tel:` link matching
488
+ * `selector` (default `a[href^="tel:"]`) — link your phone number, get the
489
+ * analytics event (and, for a linked phone-click goal, the conversion) for free.
487
490
  */
488
- declare const phoneClickConfigSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
491
+ declare const phoneClickConfigSchema: z.ZodObject<{
492
+ autoCapture: z.ZodOptional<z.ZodObject<{
493
+ selector: z.ZodOptional<z.ZodString>;
494
+ }, "strict", z.ZodTypeAny, {
495
+ selector?: string | undefined;
496
+ }, {
497
+ selector?: string | undefined;
498
+ }>>;
499
+ }, "strict", z.ZodTypeAny, {
500
+ autoCapture?: {
501
+ selector?: string | undefined;
502
+ } | undefined;
503
+ }, {
504
+ autoCapture?: {
505
+ selector?: string | undefined;
506
+ } | undefined;
507
+ }>;
489
508
  type PhoneClickConfig = z.infer<typeof phoneClickConfigSchema>;
490
509
 
491
510
  /**
@@ -1041,7 +1060,23 @@ declare const EVENT_REGISTRY: {
1041
1060
  phone_number: string;
1042
1061
  section?: string | null | undefined;
1043
1062
  }>;
1044
- readonly configSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
1063
+ readonly configSchema: z.ZodObject<{
1064
+ autoCapture: z.ZodOptional<z.ZodObject<{
1065
+ selector: z.ZodOptional<z.ZodString>;
1066
+ }, "strict", z.ZodTypeAny, {
1067
+ selector?: string | undefined;
1068
+ }, {
1069
+ selector?: string | undefined;
1070
+ }>>;
1071
+ }, "strict", z.ZodTypeAny, {
1072
+ autoCapture?: {
1073
+ selector?: string | undefined;
1074
+ } | undefined;
1075
+ }, {
1076
+ autoCapture?: {
1077
+ selector?: string | undefined;
1078
+ } | undefined;
1079
+ }>;
1045
1080
  };
1046
1081
  readonly cta_click: {
1047
1082
  readonly kind: "manual";
package/dist/index.d.ts CHANGED
@@ -483,9 +483,28 @@ type PhoneClickMetadata = z.infer<typeof phoneClickMetadataSchema>;
483
483
  /**
484
484
  * Registration config for `phone_click`.
485
485
  *
486
- * This event is manual-only and currently has no registration options.
486
+ * The event stays manually fireable; `autoCapture` additionally attaches a
487
+ * delegated click listener that fires it for any `tel:` link matching
488
+ * `selector` (default `a[href^="tel:"]`) — link your phone number, get the
489
+ * analytics event (and, for a linked phone-click goal, the conversion) for free.
487
490
  */
488
- declare const phoneClickConfigSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
491
+ declare const phoneClickConfigSchema: z.ZodObject<{
492
+ autoCapture: z.ZodOptional<z.ZodObject<{
493
+ selector: z.ZodOptional<z.ZodString>;
494
+ }, "strict", z.ZodTypeAny, {
495
+ selector?: string | undefined;
496
+ }, {
497
+ selector?: string | undefined;
498
+ }>>;
499
+ }, "strict", z.ZodTypeAny, {
500
+ autoCapture?: {
501
+ selector?: string | undefined;
502
+ } | undefined;
503
+ }, {
504
+ autoCapture?: {
505
+ selector?: string | undefined;
506
+ } | undefined;
507
+ }>;
489
508
  type PhoneClickConfig = z.infer<typeof phoneClickConfigSchema>;
490
509
 
491
510
  /**
@@ -1041,7 +1060,23 @@ declare const EVENT_REGISTRY: {
1041
1060
  phone_number: string;
1042
1061
  section?: string | null | undefined;
1043
1062
  }>;
1044
- readonly configSchema: z.ZodObject<{}, "strict", z.ZodTypeAny, {}, {}>;
1063
+ readonly configSchema: z.ZodObject<{
1064
+ autoCapture: z.ZodOptional<z.ZodObject<{
1065
+ selector: z.ZodOptional<z.ZodString>;
1066
+ }, "strict", z.ZodTypeAny, {
1067
+ selector?: string | undefined;
1068
+ }, {
1069
+ selector?: string | undefined;
1070
+ }>>;
1071
+ }, "strict", z.ZodTypeAny, {
1072
+ autoCapture?: {
1073
+ selector?: string | undefined;
1074
+ } | undefined;
1075
+ }, {
1076
+ autoCapture?: {
1077
+ selector?: string | undefined;
1078
+ } | undefined;
1079
+ }>;
1045
1080
  };
1046
1081
  readonly cta_click: {
1047
1082
  readonly kind: "manual";
package/dist/index.js CHANGED
@@ -830,8 +830,8 @@ function thresholdMet(goal, eventType, metadata) {
830
830
  return typeof metadata.page_name === "string" && metadata.page_name === t.page_name;
831
831
  case "page_view":
832
832
  case "form_start":
833
+ case "phone_click":
833
834
  return true;
834
- // no threshold — fire whenever the detector emits
835
835
  default:
836
836
  return false;
837
837
  }
@@ -1206,6 +1206,27 @@ function getOrCreateTrackingClient(config) {
1206
1206
  globalClientKey = key;
1207
1207
  return globalClient;
1208
1208
  }
1209
+ var clientCaptureRegistry = /* @__PURE__ */ new WeakMap();
1210
+ function attachClientCapturesOnce(client, build) {
1211
+ let entry = clientCaptureRegistry.get(client);
1212
+ if (entry === void 0) {
1213
+ entry = { detach: build(), refCount: 0 };
1214
+ clientCaptureRegistry.set(client, entry);
1215
+ }
1216
+ entry.refCount += 1;
1217
+ let released = false;
1218
+ return () => {
1219
+ if (released) return;
1220
+ released = true;
1221
+ const current = clientCaptureRegistry.get(client);
1222
+ if (current === void 0) return;
1223
+ current.refCount -= 1;
1224
+ if (current.refCount <= 0) {
1225
+ current.detach();
1226
+ clientCaptureRegistry.delete(client);
1227
+ }
1228
+ };
1229
+ }
1209
1230
  function createTrackingClient(config) {
1210
1231
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
1211
1232
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -1490,7 +1511,11 @@ var phoneClickMetadataSchema = import_zod8.z.object({
1490
1511
  }).strict(),
1491
1512
  section: import_zod8.z.string().nullable().optional()
1492
1513
  }).strict();
1493
- var phoneClickConfigSchema = import_zod8.z.object({}).strict();
1514
+ var phoneClickConfigSchema = import_zod8.z.object({
1515
+ autoCapture: import_zod8.z.object({
1516
+ selector: import_zod8.z.string().optional()
1517
+ }).strict().optional()
1518
+ }).strict();
1494
1519
 
1495
1520
  // ../tracking-core/src/events/scroll-depth.ts
1496
1521
  var import_zod9 = require("zod");
@@ -2126,6 +2151,99 @@ function attachCtaClickCapture(client, config) {
2126
2151
  };
2127
2152
  }
2128
2153
 
2154
+ // ../tracking-core/src/phone.ts
2155
+ var import_libphonenumber_js = require("libphonenumber-js");
2156
+ var DEFAULT_PHONE_COUNTRY = "CA";
2157
+ function parsePhone(raw, country) {
2158
+ const region = country ?? DEFAULT_PHONE_COUNTRY;
2159
+ const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
2160
+ if (!parsed) {
2161
+ return { e164: null, national: "", international: "", country: region, isValid: false };
2162
+ }
2163
+ const isValid = parsed.isValid();
2164
+ return {
2165
+ // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
2166
+ // (e.g. too few digits) still parses but must not be transmitted.
2167
+ e164: isValid ? parsed.number : null,
2168
+ national: parsed.formatNational(),
2169
+ international: parsed.formatInternational(),
2170
+ country: parsed.country ?? region,
2171
+ isValid
2172
+ };
2173
+ }
2174
+ function toE164(raw, country) {
2175
+ return parsePhone(raw, country).e164;
2176
+ }
2177
+ function formatPhone(value, format = "national", country) {
2178
+ const parsed = parsePhone(value, country);
2179
+ if (typeof format === "function") return format(parsed);
2180
+ switch (format) {
2181
+ case "international":
2182
+ return parsed.international || value;
2183
+ case "e164":
2184
+ return parsed.e164 ?? value;
2185
+ case "national":
2186
+ default:
2187
+ return parsed.national || value;
2188
+ }
2189
+ }
2190
+ function formatPhoneAsTyped(raw, country) {
2191
+ return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
2192
+ }
2193
+
2194
+ // ../tracking-core/src/triggers/phone-click-capture.ts
2195
+ var DEFAULT_TEL_SELECTOR = 'a[href^="tel:"]';
2196
+ function safeDecodeURIComponent(value) {
2197
+ try {
2198
+ return decodeURIComponent(value);
2199
+ } catch {
2200
+ return value;
2201
+ }
2202
+ }
2203
+ function resolvePhoneNumber(el) {
2204
+ const href = el instanceof HTMLAnchorElement ? el.href : el.getAttribute("href") ?? "";
2205
+ const raw = safeDecodeURIComponent(href.replace(/^tel:/i, "").split(";")[0]).trim();
2206
+ return toE164(raw) ?? raw;
2207
+ }
2208
+ function attachPhoneClickCapture(client, config) {
2209
+ if (typeof window === "undefined" || typeof document === "undefined") {
2210
+ return () => {
2211
+ };
2212
+ }
2213
+ const autoCapture = config.autoCapture;
2214
+ if (!autoCapture) {
2215
+ return () => {
2216
+ };
2217
+ }
2218
+ const selector = autoCapture.selector ?? DEFAULT_TEL_SELECTOR;
2219
+ function onClick(event) {
2220
+ const target = event.target;
2221
+ if (!(target instanceof Element)) return;
2222
+ let matched = null;
2223
+ try {
2224
+ matched = target.closest(selector);
2225
+ } catch {
2226
+ return;
2227
+ }
2228
+ if (matched === null) return;
2229
+ const metadata = {
2230
+ phone_number: resolvePhoneNumber(matched),
2231
+ page: { path: window.location.pathname },
2232
+ section: matched.getAttribute("data-aranova-section")
2233
+ };
2234
+ client.trackEvent({
2235
+ eventType: "phone_click",
2236
+ metadata,
2237
+ pageUrl: window.location.href,
2238
+ occurredAt: null
2239
+ });
2240
+ }
2241
+ document.addEventListener("click", onClick, true);
2242
+ return () => {
2243
+ document.removeEventListener("click", onClick, true);
2244
+ };
2245
+ }
2246
+
2129
2247
  // ../tracking-core/src/resources/sales/errors.ts
2130
2248
  var AranovaApiError = class extends Error {
2131
2249
  constructor(message, options) {
@@ -2453,46 +2571,6 @@ async function fetchServices(config) {
2453
2571
  return salesRequest(config, "GET", "/services");
2454
2572
  }
2455
2573
 
2456
- // ../tracking-core/src/phone.ts
2457
- var import_libphonenumber_js = require("libphonenumber-js");
2458
- var DEFAULT_PHONE_COUNTRY = "CA";
2459
- function parsePhone(raw, country) {
2460
- const region = country ?? DEFAULT_PHONE_COUNTRY;
2461
- const parsed = (0, import_libphonenumber_js.parsePhoneNumberFromString)(raw ?? "", region);
2462
- if (!parsed) {
2463
- return { e164: null, national: "", international: "", country: region, isValid: false };
2464
- }
2465
- const isValid = parsed.isValid();
2466
- return {
2467
- // E.164 is only surfaced for a *valid* number — a possible-but-invalid input
2468
- // (e.g. too few digits) still parses but must not be transmitted.
2469
- e164: isValid ? parsed.number : null,
2470
- national: parsed.formatNational(),
2471
- international: parsed.formatInternational(),
2472
- country: parsed.country ?? region,
2473
- isValid
2474
- };
2475
- }
2476
- function toE164(raw, country) {
2477
- return parsePhone(raw, country).e164;
2478
- }
2479
- function formatPhone(value, format = "national", country) {
2480
- const parsed = parsePhone(value, country);
2481
- if (typeof format === "function") return format(parsed);
2482
- switch (format) {
2483
- case "international":
2484
- return parsed.international || value;
2485
- case "e164":
2486
- return parsed.e164 ?? value;
2487
- case "national":
2488
- default:
2489
- return parsed.national || value;
2490
- }
2491
- }
2492
- function formatPhoneAsTyped(raw, country) {
2493
- return new import_libphonenumber_js.AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? "");
2494
- }
2495
-
2496
2574
  // ../tracking-core/src/phone-field.ts
2497
2575
  function phoneField(name, raw, country) {
2498
2576
  return { name, type: "phone", value: toE164(raw, country) };
@@ -2815,7 +2893,7 @@ function GoogleAdsTracking(props) {
2815
2893
  var import_react6 = require("react");
2816
2894
 
2817
2895
  // package.json
2818
- var version = "0.17.0";
2896
+ var version = "0.17.2";
2819
2897
 
2820
2898
  // ../tracking-core/src/phone-react.tsx
2821
2899
  var import_react5 = require("react");
@@ -2978,7 +3056,6 @@ function createTracking(options) {
2978
3056
  }
2979
3057
  }, [metaPixelId, metaPixelIdsKey]);
2980
3058
  (0, import_react6.useEffect)(() => {
2981
- const detachers = [];
2982
3059
  const rawClient = getOrCreateTrackingClient({
2983
3060
  apiKey,
2984
3061
  endpoint,
@@ -2989,43 +3066,50 @@ function createTracking(options) {
2989
3066
  activeGtagIds: resolvedGtagIds,
2990
3067
  debug
2991
3068
  });
2992
- const conversionStore = conversionConfig ? resolveConversionConfig({
2993
- cdnUrl: conversionConfig.cdnUrl,
2994
- baked: conversionConfig.baked
2995
- }) : null;
2996
- const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
2997
- detachers.push(attachAutoPageView(detectorClient));
2998
- detachers.push(attachBfcacheRestore(detectorClient));
2999
- detachers.push(attachPageExit(detectorClient));
3000
- const timeOnSite = triggers.automatic.time_on_site;
3001
- if (timeOnSite) {
3002
- detachers.push(attachTimeOnSite(detectorClient, timeOnSite));
3003
- }
3004
- const specificPageVisit = triggers.automatic.specific_page_visit;
3005
- if (specificPageVisit) {
3006
- detachers.push(attachSpecificPageVisit(detectorClient, specificPageVisit));
3007
- }
3008
- const scrollDepth = triggers.automatic.scroll_depth;
3009
- if (scrollDepth) {
3010
- detachers.push(attachScrollDepth(detectorClient, scrollDepth));
3011
- }
3012
- const multiPageSession = triggers.automatic.multi_page_session;
3013
- if (multiPageSession) {
3014
- detachers.push(attachMultiPageSession(detectorClient, multiPageSession));
3015
- }
3016
- const formStart = triggers.automatic.form_start;
3017
- if (formStart) {
3018
- detachers.push(attachFormStart(detectorClient, formStart));
3019
- }
3020
- const ctaClick = triggers.manual?.cta_click;
3021
- if (ctaClick) {
3022
- detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
3023
- }
3024
- return () => {
3025
- for (let i = detachers.length - 1; i >= 0; i--) {
3026
- detachers[i]();
3069
+ return attachClientCapturesOnce(rawClient, () => {
3070
+ const detachers = [];
3071
+ const conversionStore = conversionConfig ? resolveConversionConfig({
3072
+ cdnUrl: conversionConfig.cdnUrl,
3073
+ baked: conversionConfig.baked
3074
+ }) : null;
3075
+ const detectorClient = conversionStore ? withConversionAutoFire(rawClient, createConversionAutoFire(conversionStore)) : rawClient;
3076
+ detachers.push(attachAutoPageView(detectorClient));
3077
+ detachers.push(attachBfcacheRestore(detectorClient));
3078
+ detachers.push(attachPageExit(detectorClient));
3079
+ const timeOnSite = triggers.automatic.time_on_site;
3080
+ if (timeOnSite) {
3081
+ detachers.push(attachTimeOnSite(detectorClient, timeOnSite));
3082
+ }
3083
+ const specificPageVisit = triggers.automatic.specific_page_visit;
3084
+ if (specificPageVisit) {
3085
+ detachers.push(attachSpecificPageVisit(detectorClient, specificPageVisit));
3027
3086
  }
3028
- };
3087
+ const scrollDepth = triggers.automatic.scroll_depth;
3088
+ if (scrollDepth) {
3089
+ detachers.push(attachScrollDepth(detectorClient, scrollDepth));
3090
+ }
3091
+ const multiPageSession = triggers.automatic.multi_page_session;
3092
+ if (multiPageSession) {
3093
+ detachers.push(attachMultiPageSession(detectorClient, multiPageSession));
3094
+ }
3095
+ const formStart = triggers.automatic.form_start;
3096
+ if (formStart) {
3097
+ detachers.push(attachFormStart(detectorClient, formStart));
3098
+ }
3099
+ const ctaClick = triggers.manual?.cta_click;
3100
+ if (ctaClick) {
3101
+ detachers.push(attachCtaClickCapture(detectorClient, ctaClick));
3102
+ }
3103
+ const phoneClick = triggers.manual?.phone_click;
3104
+ if (phoneClick) {
3105
+ detachers.push(attachPhoneClickCapture(detectorClient, phoneClick));
3106
+ }
3107
+ return () => {
3108
+ for (let i = detachers.length - 1; i >= 0; i--) {
3109
+ detachers[i]();
3110
+ }
3111
+ };
3112
+ });
3029
3113
  }, []);
3030
3114
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrackingContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PhoneConfigProvider, { value: phone ?? null, children }) });
3031
3115
  }