@aranova/tracking-react 0.12.0 → 0.12.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/dist/index.mjs CHANGED
@@ -16,31 +16,26 @@ function buildConsentPayload(state) {
16
16
  };
17
17
  }
18
18
  function getConsentState() {
19
- if (typeof window === "undefined")
20
- return "pending";
19
+ if (typeof window === "undefined") return "pending";
21
20
  const storedState = window.localStorage.getItem(CONSENT_STATE_KEY);
22
- if (storedState === "granted" || storedState === "denied")
23
- return storedState;
21
+ if (storedState === "granted" || storedState === "denied") return storedState;
24
22
  return "pending";
25
23
  }
26
24
  function setConsentState(state) {
27
- if (typeof window === "undefined")
28
- return;
25
+ if (typeof window === "undefined") return;
29
26
  window.localStorage.setItem(CONSENT_STATE_KEY, state);
30
27
  window.localStorage.setItem(CONSENT_TIMESTAMP_KEY, (/* @__PURE__ */ new Date()).toISOString());
31
28
  if (typeof window.gtag === "function")
32
29
  window.gtag("consent", "update", buildConsentPayload(state));
33
30
  }
34
31
  function resetConsent() {
35
- if (typeof window === "undefined")
36
- return;
32
+ if (typeof window === "undefined") return;
37
33
  window.localStorage.removeItem(CONSENT_STATE_KEY);
38
34
  window.localStorage.removeItem(CONSENT_TIMESTAMP_KEY);
39
35
  }
40
36
  function restoreStoredConsent() {
41
37
  const consentState = getConsentState();
42
- if (consentState === "granted" || consentState === "denied")
43
- setConsentState(consentState);
38
+ if (consentState === "granted" || consentState === "denied") setConsentState(consentState);
44
39
  return consentState;
45
40
  }
46
41
 
@@ -97,8 +92,7 @@ function isValidGtagId(id) {
97
92
  }
98
93
  function ensureGtagFunction() {
99
94
  window.dataLayer = window.dataLayer || [];
100
- if (typeof window.gtag === "function")
101
- return window.gtag;
95
+ if (typeof window.gtag === "function") return window.gtag;
102
96
  window.gtag = (...args) => {
103
97
  window.dataLayer?.push(args);
104
98
  };
@@ -115,17 +109,17 @@ function applyDefaultConsentState() {
115
109
  });
116
110
  }
117
111
  function loadGtagScript(gtagId) {
118
- if (typeof document === "undefined")
119
- return;
112
+ if (typeof document === "undefined") return;
120
113
  const marker = getScriptMarker("gtag-loader");
121
- const existingScript = document.querySelector(`script[${TRACKING_SCRIPT_ATTRIBUTE}="${marker}"]`);
122
- if (existingScript)
123
- return;
114
+ const existingScript = document.querySelector(
115
+ `script[${TRACKING_SCRIPT_ATTRIBUTE}="${marker}"]`
116
+ );
117
+ if (existingScript) return;
124
118
  const script = document.createElement("script");
125
119
  script.async = true;
126
120
  script.src = `${GTAG_SCRIPT_HOST}?id=${encodeURIComponent(gtagId)}`;
127
121
  script.setAttribute(TRACKING_SCRIPT_ATTRIBUTE, marker);
128
- document.head.appendChild(script);
122
+ document.head.append(script);
129
123
  }
130
124
  function initializeGtag(gtagId) {
131
125
  const gtag = ensureGtagFunction();
@@ -133,21 +127,19 @@ function initializeGtag(gtagId) {
133
127
  gtag("config", gtagId);
134
128
  }
135
129
  function bootstrapGoogleAdsTracking(gtagId) {
136
- if (typeof window === "undefined" || typeof document === "undefined")
137
- return;
138
- if (!isValidGtagId(gtagId))
139
- return;
130
+ if (typeof window === "undefined" || typeof document === "undefined") return;
131
+ if (!isValidGtagId(gtagId)) return;
140
132
  applyDefaultConsentState();
141
133
  loadGtagScript(gtagId);
142
134
  initializeGtag(gtagId);
143
135
  restoreStoredConsent();
144
136
  }
145
137
  function bootstrapMultipleGtags(gtagIds) {
146
- if (typeof window === "undefined" || typeof document === "undefined")
147
- return;
148
- const ids = Object.values(gtagIds).filter(isValidGtagId);
149
- if (ids.length === 0)
150
- return;
138
+ if (typeof window === "undefined" || typeof document === "undefined") return;
139
+ const ids = Object.values(gtagIds).filter(
140
+ (id) => typeof id === "string" && isValidGtagId(id)
141
+ );
142
+ if (ids.length === 0) return;
151
143
  applyDefaultConsentState();
152
144
  loadGtagScript(ids[0]);
153
145
  const gtag = ensureGtagFunction();
@@ -199,18 +191,15 @@ function getTrackingQueryValues(searchParams) {
199
191
  }, {});
200
192
  }
201
193
  function getCookieValueFromDocument(key) {
202
- if (typeof document === "undefined")
203
- return null;
194
+ if (typeof document === "undefined") return null;
204
195
  const cookies = document.cookie ? document.cookie.split("; ") : [];
205
196
  const match = cookies.find((cookie) => cookie.startsWith(`${key}=`));
206
- if (!match)
207
- return null;
197
+ if (!match) return null;
208
198
  const [, rawValue = ""] = match.split("=");
209
199
  return normalizeTrackingCookieValue(decodeURIComponent(rawValue));
210
200
  }
211
201
  function setTrackingCookie(key, value, maxAgeSeconds = TRACKING_COOKIE_MAX_AGE_SECONDS) {
212
- if (typeof document === "undefined")
213
- return;
202
+ if (typeof document === "undefined") return;
214
203
  const encodedValue = encodeURIComponent(value);
215
204
  document.cookie = `${key}=${encodedValue}; Max-Age=${maxAgeSeconds}; Path=/; SameSite=Lax`;
216
205
  }
@@ -250,18 +239,15 @@ function writeLocalStorage(key, value) {
250
239
  }
251
240
  }
252
241
  function getVisitorId() {
253
- if (typeof window === "undefined")
254
- return safeUuid();
242
+ if (typeof window === "undefined") return safeUuid();
255
243
  const existing = readLocalStorage(VISITOR_STORAGE_KEY);
256
- if (existing && existing.length > 0)
257
- return existing;
244
+ if (existing && existing.length > 0) return existing;
258
245
  const fresh = safeUuid();
259
246
  writeLocalStorage(VISITOR_STORAGE_KEY, fresh);
260
247
  return fresh;
261
248
  }
262
249
  function getOrRotateSessionId(now = Date.now()) {
263
- if (typeof window === "undefined")
264
- return { id: safeUuid(), isNew: true };
250
+ if (typeof window === "undefined") return { id: safeUuid(), isNew: true };
265
251
  const raw = readLocalStorage(SESSION_STORAGE_KEY);
266
252
  if (raw) {
267
253
  try {
@@ -334,8 +320,7 @@ function setLastFiredUrl(url) {
334
320
  writeSessionStorage(LAST_FIRED_URL_STORAGE_KEY, url);
335
321
  }
336
322
  function buildPageViewMetadata(referrerOverride) {
337
- if (typeof window === "undefined" || typeof document === "undefined")
338
- return null;
323
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
339
324
  return pageViewMetadataSchema.parse({
340
325
  page: {
341
326
  title: document.title || null,
@@ -348,8 +333,7 @@ function buildPageViewMetadata(referrerOverride) {
348
333
  });
349
334
  }
350
335
  function fireManualPageView(client) {
351
- if (typeof window === "undefined")
352
- return;
336
+ if (typeof window === "undefined") return;
353
337
  const currentHref = window.location.href;
354
338
  const previousFiredUrl = getLastFiredUrl();
355
339
  const internalReferrer = previousFiredUrl !== null && previousFiredUrl !== currentHref ? previousFiredUrl : null;
@@ -385,8 +369,7 @@ function attachAutoPageView(client, options = {}) {
385
369
  let lastPath = window.location.pathname + window.location.search;
386
370
  function maybeFire() {
387
371
  const current = window.location.pathname + window.location.search;
388
- if (current === lastPath)
389
- return;
372
+ if (current === lastPath) return;
390
373
  lastPath = current;
391
374
  fireManualPageView(client);
392
375
  }
@@ -408,8 +391,7 @@ function attachAutoPageView(client, options = {}) {
408
391
  history.replaceState = patchedReplaceState;
409
392
  window.addEventListener("popstate", maybeFire);
410
393
  window.addEventListener("pageshow", handlePageShow);
411
- if (!options.skipInitial)
412
- fireManualPageView(client);
394
+ if (!options.skipInitial) fireManualPageView(client);
413
395
  return () => {
414
396
  history.pushState = originalPushState;
415
397
  history.replaceState = originalReplaceState;
@@ -491,8 +473,7 @@ function buildContext(surface, sdkVersion, packageName, environment, activeGtagI
491
473
  };
492
474
  }
493
475
  function readTrackingParams() {
494
- if (typeof window === "undefined")
495
- return createEmptyTrackingParams();
476
+ if (typeof window === "undefined") return createEmptyTrackingParams();
496
477
  try {
497
478
  captureTrackingParamsFromLocation();
498
479
  } catch {
@@ -507,8 +488,7 @@ function consentSnapshot() {
507
488
  }
508
489
  }
509
490
  async function postWithFetch(url, body, apiKey, identity, keepalive) {
510
- if (typeof fetch !== "function")
511
- return;
491
+ if (typeof fetch !== "function") return;
512
492
  try {
513
493
  await fetch(url, {
514
494
  method: "POST",
@@ -568,8 +548,7 @@ function createTrackingClient(config) {
568
548
  const visitorId = getVisitorId();
569
549
  const initialSession = getOrRotateSessionId();
570
550
  let sessionId = initialSession.id;
571
- if (typeof window !== "undefined")
572
- firstPage = window.location.href;
551
+ if (typeof window !== "undefined") firstPage = window.location.href;
573
552
  function enqueueHeartbeat() {
574
553
  const metadata = buildHeartbeatMetadata(
575
554
  config.surface,
@@ -595,7 +574,13 @@ function createTrackingClient(config) {
595
574
  }
596
575
  sessionId = rotated.id;
597
576
  const params = readTrackingParams();
598
- const context = buildContext(config.surface, sdkVersion, packageName, environment, activeGtagIds);
577
+ const context = buildContext(
578
+ config.surface,
579
+ sdkVersion,
580
+ packageName,
581
+ environment,
582
+ activeGtagIds
583
+ );
599
584
  return {
600
585
  session_id: sessionId,
601
586
  visitor_id: visitorId,
@@ -612,8 +597,7 @@ function createTrackingClient(config) {
612
597
  };
613
598
  }
614
599
  function scheduleFlush() {
615
- if (flushTimer !== null || destroyed)
616
- return;
600
+ if (flushTimer !== null || destroyed) return;
617
601
  flushTimer = setTimeout(() => {
618
602
  flushTimer = null;
619
603
  void flush();
@@ -626,8 +610,7 @@ function createTrackingClient(config) {
626
610
  }
627
611
  }
628
612
  async function flush() {
629
- if (queue.length === 0)
630
- return;
613
+ if (queue.length === 0) return;
631
614
  const events = queue.slice(0, HARD_MAX_BATCH);
632
615
  queue = queue.slice(events.length);
633
616
  clearScheduledFlush();
@@ -639,10 +622,8 @@ function createTrackingClient(config) {
639
622
  await postWithFetch(eventsUrl, serialized, config.apiKey, identityHeaders2, false);
640
623
  }
641
624
  function trackEvent(input) {
642
- if (destroyed)
643
- return;
644
- if (!input || typeof input.eventType !== "string" || input.eventType.length === 0)
645
- return;
625
+ if (destroyed) return;
626
+ if (!input || typeof input.eventType !== "string" || input.eventType.length === 0) return;
646
627
  const occurredAt = input.occurredAt instanceof Date ? input.occurredAt.toISOString() : typeof input.occurredAt === "string" ? input.occurredAt : (/* @__PURE__ */ new Date()).toISOString();
647
628
  queue.push({
648
629
  event_type: input.eventType,
@@ -657,8 +638,7 @@ function createTrackingClient(config) {
657
638
  }
658
639
  }
659
640
  function flushOnUnload() {
660
- if (queue.length === 0)
661
- return;
641
+ if (queue.length === 0) return;
662
642
  const events = queue.slice(0, HARD_MAX_BATCH);
663
643
  queue = queue.slice(events.length);
664
644
  clearScheduledFlush();
@@ -1424,11 +1404,6 @@ function createSalesClient(config) {
1424
1404
  };
1425
1405
  }
1426
1406
 
1427
- // ../tracking-core/src/resources/services.ts
1428
- async function fetchServices(config) {
1429
- return salesRequest(config, "GET", "/services");
1430
- }
1431
-
1432
1407
  // ../tracking-core/src/resources/sales/money.ts
1433
1408
  var MINOR_UNIT_EXPONENT = {
1434
1409
  USD: 2,
@@ -1462,6 +1437,123 @@ function formatDateInTz(iso, timeZone, opts, locale) {
1462
1437
  }).format(date);
1463
1438
  }
1464
1439
 
1440
+ // ../tracking-core/src/resources/sales/schema.ts
1441
+ import { z as z11 } from "zod";
1442
+ var SUPPORTED_CURRENCIES = ["USD", "CAD"];
1443
+ var TRACKING_ENVIRONMENTS = ["production", "development"];
1444
+ var currencySchema = z11.enum(SUPPORTED_CURRENCIES);
1445
+ var centsSchema = z11.number().int().nonnegative();
1446
+ var quantitySchema = z11.string().regex(/^\d+(\.\d{1,3})?$/);
1447
+ var metadataSchema = z11.record(z11.unknown());
1448
+ var saleItemSchema = z11.object({
1449
+ external_item_id: z11.string().nullable().optional(),
1450
+ name: z11.string().nullable().optional(),
1451
+ category: z11.string().nullable().optional(),
1452
+ quantity: quantitySchema,
1453
+ unit_price_cents: centsSchema,
1454
+ // Non-negativity validated on the wire — same contract as the other cents
1455
+ // fields — and backstopped by the DB CHECK.
1456
+ unit_cost_cents: centsSchema.nullable().optional()
1457
+ }).strict();
1458
+ var saleServiceSchema = z11.object({
1459
+ service: z11.string(),
1460
+ amount_cents: centsSchema
1461
+ }).strict();
1462
+ var customerNameSchema = z11.string().max(200);
1463
+ var customerPhoneSchema = z11.string().max(64);
1464
+ var customerEmailSchema = z11.string().max(320).email();
1465
+ function refineServiceXor(val, ctx, { requireAmount }) {
1466
+ if (val.services != null) {
1467
+ if (val.service != null) {
1468
+ ctx.addIssue({
1469
+ code: z11.ZodIssueCode.custom,
1470
+ message: "pass either `service` or `services`, not both",
1471
+ path: ["services"]
1472
+ });
1473
+ }
1474
+ if (val.services.length === 0) {
1475
+ ctx.addIssue({
1476
+ code: z11.ZodIssueCode.custom,
1477
+ message: "`services` must not be empty",
1478
+ path: ["services"]
1479
+ });
1480
+ }
1481
+ const keys = val.services.map((s) => s.service);
1482
+ if (new Set(keys).size !== keys.length) {
1483
+ ctx.addIssue({
1484
+ code: z11.ZodIssueCode.custom,
1485
+ message: "`services` must not list the same service more than once",
1486
+ path: ["services"]
1487
+ });
1488
+ }
1489
+ if (val.amount_total_cents != null) {
1490
+ const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);
1491
+ if (val.amount_total_cents !== sum) {
1492
+ ctx.addIssue({
1493
+ code: z11.ZodIssueCode.custom,
1494
+ message: "amount_total_cents must equal the sum of the services amounts (omit it to derive it automatically)",
1495
+ path: ["amount_total_cents"]
1496
+ });
1497
+ }
1498
+ }
1499
+ } else if (requireAmount && val.amount_total_cents == null) {
1500
+ ctx.addIssue({
1501
+ code: z11.ZodIssueCode.custom,
1502
+ message: "amount_total_cents is required unless `services` is provided",
1503
+ path: ["amount_total_cents"]
1504
+ });
1505
+ }
1506
+ }
1507
+ var saleCreateSchema = z11.object({
1508
+ external_id: z11.string().nullable().optional(),
1509
+ description: z11.string().nullable().optional(),
1510
+ service: z11.string().nullable().optional(),
1511
+ services: z11.array(saleServiceSchema).nullable().optional(),
1512
+ currency: currencySchema,
1513
+ // Optional only because the plural `services` form derives it from the sum
1514
+ // (see refineServiceXor); the singular/serviceless path still requires it.
1515
+ amount_total_cents: centsSchema.nullable().optional(),
1516
+ occurred_at: z11.string().datetime(),
1517
+ environment: z11.enum(TRACKING_ENVIRONMENTS).default("production"),
1518
+ items: z11.array(saleItemSchema).default([]),
1519
+ metadata: metadataSchema.nullable().optional(),
1520
+ customer_name: customerNameSchema.nullable().optional(),
1521
+ customer_phone: customerPhoneSchema.nullable().optional(),
1522
+ customer_email: customerEmailSchema.nullable().optional()
1523
+ }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));
1524
+ var saleUpdateSchema = z11.object({
1525
+ description: z11.string().nullable().optional(),
1526
+ service: z11.string().nullable().optional(),
1527
+ services: z11.array(saleServiceSchema).nullable().optional(),
1528
+ currency: currencySchema.optional(),
1529
+ amount_total_cents: centsSchema.optional(),
1530
+ occurred_at: z11.string().datetime().optional(),
1531
+ items: z11.array(saleItemSchema).optional(),
1532
+ metadata: metadataSchema.nullable().optional(),
1533
+ customer_name: customerNameSchema.nullable().optional(),
1534
+ customer_phone: customerPhoneSchema.nullable().optional(),
1535
+ customer_email: customerEmailSchema.nullable().optional()
1536
+ }).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: false }));
1537
+ var TRACKING_RANGES = ["24h", "7d", "30d"];
1538
+ var NAMED_RANGES = [
1539
+ "today",
1540
+ "yesterday",
1541
+ "wtd",
1542
+ "mtd",
1543
+ "qtd",
1544
+ "ytd",
1545
+ "24h",
1546
+ "7d",
1547
+ "30d",
1548
+ "90d",
1549
+ "custom"
1550
+ ];
1551
+
1552
+ // ../tracking-core/src/resources/services.ts
1553
+ async function fetchServices(config) {
1554
+ return salesRequest(config, "GET", "/services");
1555
+ }
1556
+
1465
1557
  // ../tracking-core/src/phone.ts
1466
1558
  import { AsYouType, parsePhoneNumberFromString } from "libphonenumber-js";
1467
1559
  var DEFAULT_PHONE_COUNTRY = "CA";
@@ -1747,10 +1839,7 @@ var ANIMATION_KEYFRAMES = `
1747
1839
  import { useEffect as useEffect3, useMemo } from "react";
1748
1840
  function GoogleAdsTracking(props) {
1749
1841
  const { gtagId, gtagIds } = props;
1750
- const gtagIdsKey = useMemo(
1751
- () => gtagIds ? JSON.stringify(gtagIds) : "",
1752
- [gtagIds]
1753
- );
1842
+ const gtagIdsKey = useMemo(() => gtagIds ? JSON.stringify(gtagIds) : "", [gtagIds]);
1754
1843
  useEffect3(() => {
1755
1844
  if (gtagIds && Object.keys(gtagIds).length > 0) {
1756
1845
  bootstrapMultipleGtags(gtagIds);
@@ -1762,15 +1851,10 @@ function GoogleAdsTracking(props) {
1762
1851
  }
1763
1852
 
1764
1853
  // src/factory.tsx
1765
- import {
1766
- createContext as createContext2,
1767
- useContext as useContext2,
1768
- useEffect as useEffect4,
1769
- useMemo as useMemo3
1770
- } from "react";
1854
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect4, useMemo as useMemo3 } from "react";
1771
1855
 
1772
1856
  // package.json
1773
- var version = "0.12.0";
1857
+ var version = "0.12.2";
1774
1858
 
1775
1859
  // ../tracking-core/src/phone-react.tsx
1776
1860
  import {
@@ -1782,9 +1866,19 @@ import {
1782
1866
  useState as useState3
1783
1867
  } from "react";
1784
1868
  import { jsx as jsx2 } from "react/jsx-runtime";
1785
- var PhoneConfigContext = createContext(null);
1869
+ var _phoneConfigContext;
1870
+ function phoneConfigContext() {
1871
+ return _phoneConfigContext ?? (_phoneConfigContext = createContext(null));
1872
+ }
1873
+ function PhoneConfigProvider({
1874
+ value,
1875
+ children
1876
+ }) {
1877
+ const Ctx = phoneConfigContext();
1878
+ return /* @__PURE__ */ jsx2(Ctx.Provider, { value, children });
1879
+ }
1786
1880
  function usePhoneConfig() {
1787
- const ctx = useContext(PhoneConfigContext);
1881
+ const ctx = useContext(phoneConfigContext());
1788
1882
  return {
1789
1883
  defaultCountry: ctx?.defaultCountry ?? DEFAULT_PHONE_COUNTRY,
1790
1884
  display: ctx?.display ?? "national"
@@ -1877,13 +1971,15 @@ function createTracking(options) {
1877
1971
  return {
1878
1972
  // Still publish phone config so usePhoneField/<PhoneField> work even when
1879
1973
  // tracking is disabled (missing apiKey/endpoint).
1880
- TrackingProvider: ({ children }) => /* @__PURE__ */ jsx3(PhoneConfigContext.Provider, { value: phone ?? null, children }),
1974
+ TrackingProvider: ({ children }) => /* @__PURE__ */ jsx3(PhoneConfigProvider, { value: phone ?? null, children }),
1881
1975
  useTracking: () => noopTyped
1882
1976
  };
1883
1977
  }
1884
1978
  const TrackingContext = createContext2(null);
1885
1979
  function TrackingProvider({ gtagId, gtagIds, children }) {
1886
- const resolvedGtagIds = gtagIds ?? (gtagId ? { default: gtagId } : void 0);
1980
+ const resolvedGtagIds = gtagIds ? Object.fromEntries(
1981
+ Object.entries(gtagIds).filter((e) => e[1] != null)
1982
+ ) : gtagId ? { default: gtagId } : void 0;
1887
1983
  const client = useMemo3(
1888
1984
  () => createTypedClient(
1889
1985
  getOrCreateTrackingClient({
@@ -1950,7 +2046,7 @@ function createTracking(options) {
1950
2046
  }
1951
2047
  };
1952
2048
  }, []);
1953
- return /* @__PURE__ */ jsx3(TrackingContext.Provider, { value: client, children: /* @__PURE__ */ jsx3(PhoneConfigContext.Provider, { value: phone ?? null, children }) });
2049
+ return /* @__PURE__ */ jsx3(TrackingContext.Provider, { value: client, children: /* @__PURE__ */ jsx3(PhoneConfigProvider, { value: phone ?? null, children }) });
1954
2050
  }
1955
2051
  function useTracking() {
1956
2052
  const client = useContext2(TrackingContext);
@@ -1968,9 +2064,11 @@ export {
1968
2064
  ConsentBanner,
1969
2065
  DEFAULT_PHONE_COUNTRY,
1970
2066
  GoogleAdsTracking,
1971
- PhoneConfigContext,
2067
+ NAMED_RANGES,
1972
2068
  PhoneField,
2069
+ SUPPORTED_CURRENCIES,
1973
2070
  TRACKING_PARAM_KEYS,
2071
+ TRACKING_RANGES,
1974
2072
  captureTrackingParamsFromLocation,
1975
2073
  createSalesClient,
1976
2074
  createTracking,
@@ -1987,6 +2085,11 @@ export {
1987
2085
  parsePhone,
1988
2086
  phoneField,
1989
2087
  resetConsent,
2088
+ saleCreateSchema,
2089
+ saleItemSchema,
2090
+ saleServiceSchema,
2091
+ saleUpdateSchema,
2092
+ salesRequest,
1990
2093
  setConsentState,
1991
2094
  toE164,
1992
2095
  toMinor,