@aranova/tracking-react 0.11.0 → 0.12.1

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
@@ -14,7 +14,7 @@ Create one shared tracking module and import the scoped `TrackingProvider` / `us
14
14
 
15
15
  ```ts
16
16
  // src/lib/tracking.ts
17
- import { createTracking } from '@aranova/tracking-react';
17
+ import { createTracking } from "@aranova/tracking-react";
18
18
 
19
19
  export const { TrackingProvider, useTracking } = createTracking({
20
20
  apiKey: import.meta.env.VITE_ARANOVA_TRACKING_API_KEY,
@@ -24,7 +24,7 @@ export const { TrackingProvider, useTracking } = createTracking({
24
24
  page_view: {},
25
25
  time_on_site: { thresholdSeconds: 60 },
26
26
  specific_page_visit: {
27
- pages: [{ name: 'contact_page', pathPattern: /^\/(contact|book|get-started)/ }],
27
+ pages: [{ name: "contact_page", pathPattern: /^\/(contact|book|get-started)/ }],
28
28
  },
29
29
  },
30
30
  manual: {
@@ -33,7 +33,7 @@ export const { TrackingProvider, useTracking } = createTracking({
33
33
  cta_click: {},
34
34
  },
35
35
  },
36
- debug: import.meta.env.MODE !== 'production',
36
+ debug: import.meta.env.MODE !== "production",
37
37
  });
38
38
  ```
39
39
 
@@ -41,11 +41,11 @@ Mount the provider at the root of your React tree.
41
41
 
42
42
  ```tsx
43
43
  // src/main.tsx
44
- import { createRoot } from 'react-dom/client';
45
- import { App } from './App';
46
- import { TrackingProvider } from './lib/tracking';
44
+ import { createRoot } from "react-dom/client";
45
+ import { App } from "./App";
46
+ import { TrackingProvider } from "./lib/tracking";
47
47
 
48
- createRoot(document.getElementById('root')!).render(
48
+ createRoot(document.getElementById("root")!).render(
49
49
  <TrackingProvider gtagId={import.meta.env.VITE_GTAG_ID}>
50
50
  <App />
51
51
  </TrackingProvider>,
@@ -54,36 +54,22 @@ createRoot(document.getElementById('root')!).render(
54
54
 
55
55
  ### Multiple gtag IDs
56
56
 
57
- To install multiple Google Ads tags simultaneously (for example a production MCC and a test MCC for verifying conversion actions before they touch the live account), pass `gtagIds` instead of `gtagId`. Every entry fires `gtag('config', ...)` on every pagegtag natively supports multiple configured tags.
57
+ Pass `gtagIds` (instead of `gtagId`) to install several Google Ads tags at oncee.g. a real
58
+ account plus a test MCC. Each entry fires `gtag('config', …)` on every page; the labels surface in
59
+ the dashboard's SDK table. Stamp events with `environment` to filter out test traffic.
58
60
 
59
61
  ```tsx
60
- <TrackingProvider
61
- gtagIds={{
62
- production: 'AW-111111111', // real client account
63
- test: 'AW-222222222', // test MCC for development
64
- }}
65
- >
62
+ <TrackingProvider gtagIds={{ production: "AW-111111111", test: "AW-222222222" }}>
66
63
  <App />
67
64
  </TrackingProvider>
68
65
  ```
69
66
 
70
- The labels are arbitrary and surface in the Aranova dashboard's SDK versions table. You can also stamp events with a deployment environment so the dashboard can filter out test traffic:
71
-
72
- ```ts
73
- createTracking({
74
- apiKey: import.meta.env.VITE_ARANOVA_TRACKING_API_KEY,
75
- endpoint: import.meta.env.VITE_ARANOVA_TRACKING_ENDPOINT,
76
- environment: import.meta.env.MODE, // 'production' | 'development'
77
- triggers: { /* ... */ },
78
- });
79
- ```
80
-
81
67
  ## Manual Events
82
68
 
83
69
  Manual events must be registered under `triggers.manual` before `trackEvent()` accepts them.
84
70
 
85
71
  ```tsx
86
- import { useTracking } from './lib/tracking';
72
+ import { useTracking } from "./lib/tracking";
87
73
 
88
74
  export function LeadForm() {
89
75
  const tracking = useTracking();
@@ -92,22 +78,22 @@ export function LeadForm() {
92
78
  const form = event.currentTarget;
93
79
  const data = new FormData(form);
94
80
 
95
- tracking.trackEvent('form_submit', {
81
+ tracking.trackEvent("form_submit", {
96
82
  form: {
97
83
  id: form.id,
98
- action: form.getAttribute('action'),
84
+ action: form.getAttribute("action"),
99
85
  fields: [
100
86
  {
101
- name: 'service_interest',
102
- type: 'select',
103
- label: 'Service interest',
104
- value: String(data.get('service_interest') ?? ''),
87
+ name: "service_interest",
88
+ type: "select",
89
+ label: "Service interest",
90
+ value: String(data.get("service_interest") ?? ""),
105
91
  },
106
92
  {
107
- name: 'is_existing_patient',
108
- type: 'checkbox',
109
- label: 'Existing patient',
110
- value: data.get('is_existing_patient') === 'on',
93
+ name: "is_existing_patient",
94
+ type: "checkbox",
95
+ label: "Existing patient",
96
+ value: data.get("is_existing_patient") === "on",
111
97
  },
112
98
  ],
113
99
  },
@@ -115,20 +101,41 @@ export function LeadForm() {
115
101
  });
116
102
  }
117
103
 
118
- return <form id="lead-form" action="/api/lead" onSubmit={handleSubmit}>{/* fields */}</form>;
104
+ return (
105
+ <form id="lead-form" action="/api/lead" onSubmit={handleSubmit}>
106
+ {/* fields */}
107
+ </form>
108
+ );
119
109
  }
120
110
  ```
121
111
 
122
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.
123
113
 
124
114
  ```tsx
125
- tracking.trackEvent('phone_click', {
126
- phone_number: '+14165550199',
115
+ tracking.trackEvent("phone_click", {
116
+ phone_number: "+14165550199",
127
117
  page: { path: window.location.pathname },
128
- section: 'header',
118
+ section: "header",
129
119
  });
130
120
  ```
131
121
 
122
+ ## Phone Fields
123
+
124
+ Bundled `libphonenumber-js`: parse/format utils + a React input. Display is configurable; the
125
+ value sent to the backend is **always E.164**. Configure once via `createTracking({ phone: { defaultCountry: 'CA', display: 'national' } })`.
126
+
127
+ ```tsx
128
+ import { usePhoneField, PhoneField, toE164 } from "@aranova/tracking-react";
129
+
130
+ const phone = usePhoneField(); // phone.value (display), phone.e164 (wire), .isValid, .error
131
+ <input {...phone.inputProps} />; // or the batteries-included <PhoneField name="phone" />
132
+
133
+ toE164("416-555-0199"); // '+14165550199' (null if invalid)
134
+ ```
135
+
136
+ Pure, isomorphic utils are also at `@aranova/tracking-react/phone` (no React). Full guide:
137
+ [phone.md](https://github.com/AranovaIO/aranova_internal/blob/master/docs/tracking-package/phone.md).
138
+
132
139
  ## Recording Sales
133
140
 
134
141
  Record a sale / conversion (a first-class, mutable resource — not a fire-and-forget
@@ -139,19 +146,23 @@ key may do is enforced by the backend, not by hiding methods. Browser write with
139
146
  your **public** key:
140
147
 
141
148
  ```tsx
142
- import { createSalesClient, toMinor } from '@aranova/tracking-react';
149
+ import { createSalesClient, toMinor } from "@aranova/tracking-react";
143
150
 
144
- const sales = createSalesClient({ apiKey: import.meta.env.VITE_ARANOVA_TRACKING_API_KEY, endpoint });
145
- await sales.record({ currency: 'CAD', amount_total_cents: toMinor(250, 'CAD'), service: 'tires' });
151
+ const sales = createSalesClient({
152
+ apiKey: import.meta.env.VITE_ARANOVA_TRACKING_API_KEY,
153
+ endpoint,
154
+ });
155
+ await sales.record({ currency: "CAD", amount_total_cents: toMinor(250, "CAD"), service: "tires" });
146
156
  ```
147
157
 
148
158
  Reads / full CRUD require a **secret** key and must run server-side — never ship a
149
159
  secret key in the browser bundle. In a Vite + Vercel app, hold it in a serverless
150
- function (same import, secret key):
160
+ function. Import from the **`/sales`** subpath — it's React-free, so the server
161
+ bundle never pulls in the provider/components:
151
162
 
152
163
  ```ts
153
164
  // api/sales.ts (Vercel serverless function — runs on the server)
154
- import { createSalesClient } from '@aranova/tracking-react';
165
+ import { createSalesClient } from "@aranova/tracking-react/sales";
155
166
 
156
167
  const sales = createSalesClient({
157
168
  apiKey: process.env.ARANOVA_TRACKING_SECRET_KEY!,
@@ -162,12 +173,23 @@ export default async function handler(_req, res) {
162
173
  }
163
174
  ```
164
175
 
165
- `sales.summary({ range: '30d' })` returns currency-grouped aggregations (revenue and
166
- average order value per currency, distinct customers, by-service / by-currency /
167
- by-category breakdowns, and a revenue/count trend) — also a secret-key read.
176
+ Secret-key reads power dashboards (all currency-grouped never summed across currencies):
177
+
178
+ ```ts
179
+ await sales.summary({ range: "mtd", timezone: "America/Toronto", compare_to: "previous_period" });
180
+ await sales.list({ sort: "amount_total_cents", want_total: true }); // → { items, total_count, … }
181
+ await sales.customers.list({ segment: "returning", sort: "total_spent" }); // phone-keyed roster
182
+ await sales.customers.summary({ range: "mtd" });
183
+ const cfg = await sales.business.config(); // tz / currencies / services (pk or sk)
184
+ ```
185
+
186
+ `summary()` gains tz-aware calendar/custom ranges, `granularity`, and period-over-period
187
+ `compare_to`; legacy `24h/7d/30d` are unchanged. A **customer is their phone (E.164)** — `customers.*`
188
+ is a live roster, no separate table. A public-key client calling any read gets a `403`.
168
189
 
169
- A public-key client calling `list`/`summary`/`get`/`update`/`delete` gets a `403`
170
- telling it to use a secret key server-side.
190
+ The root entry (`@aranova/tracking-react`) still re-exports `createSalesClient` and the
191
+ money/date helpers for back-compat, so existing imports keep working — but prefer `/sales`
192
+ in server code so the React surface never reaches your server bundle.
171
193
 
172
194
  Generate the typed `AranovaService` union from your dashboard services with the CLI
173
195
  (install it as a **devDependency**):
@@ -185,10 +207,10 @@ and the CLI reference: [cli.md](https://github.com/AranovaIO/aranova_internal/bl
185
207
  The bundled `<ConsentBanner />` renders a non-blocking bottom-docked banner while consent is `pending`, persists the visitor's choice to `localStorage`, and propagates it to Google Consent Mode v2 when gtag is loaded. **Inline-styled** — no Tailwind or CSS imports required at the consumer.
186
208
 
187
209
  ```tsx
188
- import { ConsentBanner } from '@aranova/tracking-react';
210
+ import { ConsentBanner } from "@aranova/tracking-react";
189
211
 
190
212
  // Drop-in, defaults work everywhere
191
- <ConsentBanner />
213
+ <ConsentBanner />;
192
214
  ```
193
215
 
194
216
  All props are optional:
@@ -200,13 +222,13 @@ All props are optional:
200
222
  acceptLabel="Sure"
201
223
  declineLabel="No thanks"
202
224
  policyHref="/privacy"
203
- policyLabel="Privacy policy" // default: "Learn more"
204
- onAccept={() => track('consent_accepted')}
205
- onDecline={() => track('consent_declined')}
206
- position="bottom" // or "top"
207
- theme="light" // "light" | "dark" | "auto"
225
+ policyLabel="Privacy policy" // default: "Learn more"
226
+ onAccept={() => track("consent_accepted")}
227
+ onDecline={() => track("consent_declined")}
228
+ position="bottom" // or "top"
229
+ theme="light" // "light" | "dark" | "auto"
208
230
  className="my-extra-classes"
209
- style={{ background: '#fafafa' }} // wins over the theme defaults
231
+ style={{ background: "#fafafa" }} // wins over the theme defaults
210
232
  />
211
233
  ```
212
234
 
@@ -215,7 +237,7 @@ All props are optional:
215
237
  For a bespoke banner, skip the component and drive your own UI with the headless hook:
216
238
 
217
239
  ```tsx
218
- import { useConsent } from '@aranova/tracking-react';
240
+ import { useConsent } from "@aranova/tracking-react";
219
241
 
220
242
  function CookieBar() {
221
243
  const { state, accept, decline, reset, isPending } = useConsent();
@@ -244,7 +266,7 @@ The hook handles localStorage persistence, gtag sync, and cross-tab propagation
244
266
  - Consent hooks: `useConsent()` + `UseConsentResult` (headless); `useConsentState()` (read-only alias)
245
267
  - Standalone consent helpers: `getConsentState()`, `setConsentState()`, `resetConsent()`
246
268
  - Attribution hooks: `useTrackingParams()`, `useGclid()`
247
- - `createSalesClient()` (isomorphic sales client — public key writes, secret key reads/CRUD) + money helpers (`toMinor`/`fromMinor`/`formatMoney`)
269
+ - `createSalesClient()` (isomorphic — public key writes; secret key reads/CRUD, `summary`, `customers.*`, `business.config`) + money/date helpers (`toMinor`/`fromMinor`/`formatMoney`/`formatDateInTz`). Also available React-free at **`@aranova/tracking-react/sales`** (with all sale/customer/config types) — the recommended import for server/serverless code.
270
+ - Phone: `parsePhone`/`toE164`/`formatPhone`/`formatPhoneAsTyped`/`phoneField`, `usePhoneField`, `PhoneField` (utils also at `/phone`)
248
271
  - Codegen: [`@aranova/tracking-cli`](https://www.npmjs.com/package/@aranova/tracking-cli) — `gen` typed service unions (devDependency)
249
272
  - Event metadata/config types such as `FormSubmitMetadata`, `PhoneClickMetadata`, and `JsonValue`
250
-