@aranova/tracking-react 0.3.0 → 0.4.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.js CHANGED
@@ -23,8 +23,8 @@ __export(src_exports, {
23
23
  ConsentBanner: () => ConsentBanner,
24
24
  GoogleAdsTracking: () => GoogleAdsTracking,
25
25
  TRACKING_PARAM_KEYS: () => TRACKING_PARAM_KEYS,
26
- TrackingProvider: () => TrackingProvider,
27
26
  captureTrackingParamsFromLocation: () => captureTrackingParamsFromLocation,
27
+ createTracking: () => createTracking,
28
28
  createTrackingClientContext: () => createTrackingClientContext,
29
29
  createTrackingEventCreatePayload: () => createTrackingEventCreatePayload,
30
30
  createTrackingSessionUpsertPayload: () => createTrackingSessionUpsertPayload,
@@ -32,7 +32,6 @@ __export(src_exports, {
32
32
  setConsentState: () => setConsentState,
33
33
  useConsentState: () => useConsentState,
34
34
  useGclid: () => useGclid,
35
- useTracking: () => useTracking,
36
35
  useTrackingParams: () => useTrackingParams
37
36
  });
38
37
  module.exports = __toCommonJS(src_exports);
@@ -288,6 +287,143 @@ function getOrRotateSessionId(now = Date.now()) {
288
287
  return fresh.id;
289
288
  }
290
289
 
290
+ // ../tracking-core/src/events/page-view.ts
291
+ var import_zod = require("zod");
292
+ var pageViewMetadataSchema = import_zod.z.object({
293
+ page: import_zod.z.object({
294
+ title: import_zod.z.string().nullable(),
295
+ path: import_zod.z.string(),
296
+ search: import_zod.z.string(),
297
+ hash: import_zod.z.string()
298
+ }),
299
+ referrer: import_zod.z.string().nullable(),
300
+ // `.nullable().optional()` — absent (undefined) OR explicit null OR a
301
+ // real viewport object. Mirrors Pydantic's `_Viewport | None = None`
302
+ // on the backend side so the drift test stays clean.
303
+ viewport: import_zod.z.object({
304
+ w: import_zod.z.number(),
305
+ h: import_zod.z.number()
306
+ }).nullable().optional()
307
+ }).strict();
308
+ var pageViewConfigSchema = import_zod.z.object({}).strict();
309
+
310
+ // ../tracking-core/src/page-view.ts
311
+ var LAST_FIRED_URL_STORAGE_KEY = "aranova_tracking_last_fired_url";
312
+ var lastFiredUrl = null;
313
+ var lastFiredUrlHydrated = false;
314
+ function readSessionStorage(key) {
315
+ try {
316
+ if (typeof window === "undefined") return null;
317
+ return window.sessionStorage.getItem(key);
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+ function writeSessionStorage(key, value) {
323
+ try {
324
+ if (typeof window === "undefined") return;
325
+ window.sessionStorage.setItem(key, value);
326
+ } catch {
327
+ }
328
+ }
329
+ function getLastFiredUrl() {
330
+ if (!lastFiredUrlHydrated) {
331
+ lastFiredUrlHydrated = true;
332
+ const stored = readSessionStorage(LAST_FIRED_URL_STORAGE_KEY);
333
+ if (stored !== null) lastFiredUrl = stored;
334
+ }
335
+ return lastFiredUrl;
336
+ }
337
+ function setLastFiredUrl(url) {
338
+ lastFiredUrl = url;
339
+ lastFiredUrlHydrated = true;
340
+ writeSessionStorage(LAST_FIRED_URL_STORAGE_KEY, url);
341
+ }
342
+ function buildPageViewMetadata(referrerOverride) {
343
+ if (typeof window === "undefined" || typeof document === "undefined")
344
+ return null;
345
+ return pageViewMetadataSchema.parse({
346
+ page: {
347
+ title: document.title || null,
348
+ path: window.location.pathname,
349
+ search: window.location.search,
350
+ hash: window.location.hash
351
+ },
352
+ referrer: referrerOverride !== void 0 ? referrerOverride : document.referrer || null,
353
+ viewport: { w: window.innerWidth, h: window.innerHeight }
354
+ });
355
+ }
356
+ function fireManualPageView(client) {
357
+ if (typeof window === "undefined")
358
+ return;
359
+ const currentHref = window.location.href;
360
+ const previousFiredUrl = getLastFiredUrl();
361
+ const internalReferrer = previousFiredUrl !== null && previousFiredUrl !== currentHref ? previousFiredUrl : null;
362
+ const externalReferrer = typeof document !== "undefined" ? document.referrer || null : null;
363
+ const referrer = internalReferrer ?? externalReferrer;
364
+ const metadata = buildPageViewMetadata(referrer);
365
+ client.trackEvent({
366
+ eventType: "page_view",
367
+ pageUrl: currentHref,
368
+ metadata
369
+ });
370
+ if (currentHref !== previousFiredUrl) {
371
+ setLastFiredUrl(currentHref);
372
+ }
373
+ }
374
+ function attachBfcacheRestore(client) {
375
+ if (typeof window === "undefined") return () => {
376
+ };
377
+ function handlePageShow(event) {
378
+ if (!event.persisted) return;
379
+ fireManualPageView(client);
380
+ }
381
+ window.addEventListener("pageshow", handlePageShow);
382
+ return () => {
383
+ window.removeEventListener("pageshow", handlePageShow);
384
+ };
385
+ }
386
+ function attachAutoPageView(client, options = {}) {
387
+ if (typeof window === "undefined" || typeof history === "undefined") {
388
+ return () => {
389
+ };
390
+ }
391
+ let lastPath = window.location.pathname + window.location.search;
392
+ function maybeFire() {
393
+ const current = window.location.pathname + window.location.search;
394
+ if (current === lastPath)
395
+ return;
396
+ lastPath = current;
397
+ fireManualPageView(client);
398
+ }
399
+ const originalPushState = history.pushState.bind(history);
400
+ const originalReplaceState = history.replaceState.bind(history);
401
+ function patchedPushState(...args) {
402
+ originalPushState(...args);
403
+ setTimeout(maybeFire, 0);
404
+ }
405
+ function patchedReplaceState(...args) {
406
+ originalReplaceState(...args);
407
+ setTimeout(maybeFire, 0);
408
+ }
409
+ function handlePageShow(event) {
410
+ if (!event.persisted) return;
411
+ fireManualPageView(client);
412
+ }
413
+ history.pushState = patchedPushState;
414
+ history.replaceState = patchedReplaceState;
415
+ window.addEventListener("popstate", maybeFire);
416
+ window.addEventListener("pageshow", handlePageShow);
417
+ if (!options.skipInitial)
418
+ fireManualPageView(client);
419
+ return () => {
420
+ history.pushState = originalPushState;
421
+ history.replaceState = originalReplaceState;
422
+ window.removeEventListener("popstate", maybeFire);
423
+ window.removeEventListener("pageshow", handlePageShow);
424
+ };
425
+ }
426
+
291
427
  // ../tracking-core/src/ingest.ts
292
428
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
293
429
  var DEFAULT_MAX_QUEUE_SIZE = 10;
@@ -338,6 +474,23 @@ async function postWithFetch(url, body, apiKey, keepalive) {
338
474
  } catch {
339
475
  }
340
476
  }
477
+ var globalClient = null;
478
+ var globalClientKey = null;
479
+ function clientConfigKey(config) {
480
+ return `${config.apiKey}@${config.endpoint}#${config.surface}`;
481
+ }
482
+ function getOrCreateTrackingClient(config) {
483
+ const key = clientConfigKey(config);
484
+ if (globalClient !== null && globalClientKey === key) {
485
+ return globalClient;
486
+ }
487
+ if (globalClient !== null) {
488
+ globalClient.destroy();
489
+ }
490
+ globalClient = createTrackingClient(config);
491
+ globalClientKey = key;
492
+ return globalClient;
493
+ }
341
494
  function createTrackingClient(config) {
342
495
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
343
496
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -443,6 +596,9 @@ function createTrackingClient(config) {
443
596
  getVisitorId: () => visitorId,
444
597
  destroy: () => {
445
598
  destroyed = true;
599
+ if (queue.length > 0) {
600
+ flushOnUnload();
601
+ }
446
602
  clearScheduledFlush();
447
603
  queue = [];
448
604
  if (typeof window !== "undefined") {
@@ -452,61 +608,218 @@ function createTrackingClient(config) {
452
608
  };
453
609
  }
454
610
 
455
- // ../tracking-core/src/page-view.ts
456
- function buildPageViewMetadata() {
457
- if (typeof window === "undefined" || typeof document === "undefined")
458
- return null;
611
+ // ../tracking-core/src/events/contact-page-visit.ts
612
+ var import_zod2 = require("zod");
613
+ var contactPageVisitMetadataSchema = import_zod2.z.object({
614
+ page: import_zod2.z.object({
615
+ path: import_zod2.z.string()
616
+ })
617
+ }).strict();
618
+ var contactPageVisitConfigSchema = import_zod2.z.object({
619
+ // RegExp is runtime-only so we wrap it via z.custom. Consumers pass a
620
+ // real regex at createTracking() time; the factory validates with this
621
+ // schema and then keeps the live RegExp reference for runtime matching.
622
+ pathPattern: import_zod2.z.custom(
623
+ (value) => value instanceof RegExp,
624
+ { message: "pathPattern must be a RegExp" }
625
+ )
626
+ }).strict();
627
+
628
+ // ../tracking-core/src/events/form-submit.ts
629
+ var import_zod3 = require("zod");
630
+ var formSubmitMetadataSchema = import_zod3.z.object({
631
+ form: import_zod3.z.object({
632
+ id: import_zod3.z.string(),
633
+ action: import_zod3.z.string().nullable()
634
+ }),
635
+ page: import_zod3.z.object({
636
+ path: import_zod3.z.string()
637
+ })
638
+ }).strict();
639
+ var formSubmitConfigSchema = import_zod3.z.object({}).strict();
640
+
641
+ // ../tracking-core/src/events/phone-click.ts
642
+ var import_zod4 = require("zod");
643
+ var phoneClickMetadataSchema = import_zod4.z.object({
644
+ element: import_zod4.z.object({
645
+ tag: import_zod4.z.string(),
646
+ text: import_zod4.z.string().nullable(),
647
+ href: import_zod4.z.string()
648
+ }),
649
+ page: import_zod4.z.object({
650
+ path: import_zod4.z.string()
651
+ })
652
+ }).strict();
653
+ var phoneClickConfigSchema = import_zod4.z.object({}).strict();
654
+
655
+ // ../tracking-core/src/events/time-on-site.ts
656
+ var import_zod5 = require("zod");
657
+ var timeOnSiteMetadataSchema = import_zod5.z.object({
658
+ duration_ms: import_zod5.z.number().int().nonnegative(),
659
+ page: import_zod5.z.object({
660
+ path: import_zod5.z.string()
661
+ })
662
+ }).strict();
663
+ var timeOnSiteConfigSchema = import_zod5.z.object({
664
+ thresholdSeconds: import_zod5.z.number().int().positive()
665
+ }).strict();
666
+
667
+ // ../tracking-core/src/events/registry.ts
668
+ var EVENT_REGISTRY = {
669
+ // --- automatic triggers ---
670
+ page_view: {
671
+ kind: "automatic",
672
+ metadataSchema: pageViewMetadataSchema,
673
+ configSchema: pageViewConfigSchema
674
+ },
675
+ time_on_site: {
676
+ kind: "automatic",
677
+ metadataSchema: timeOnSiteMetadataSchema,
678
+ configSchema: timeOnSiteConfigSchema
679
+ },
680
+ contact_page_visit: {
681
+ kind: "automatic",
682
+ metadataSchema: contactPageVisitMetadataSchema,
683
+ configSchema: contactPageVisitConfigSchema
684
+ },
685
+ // --- manual triggers ---
686
+ form_submit: {
687
+ kind: "manual",
688
+ metadataSchema: formSubmitMetadataSchema,
689
+ configSchema: formSubmitConfigSchema
690
+ },
691
+ phone_click: {
692
+ kind: "manual",
693
+ metadataSchema: phoneClickMetadataSchema,
694
+ configSchema: phoneClickConfigSchema
695
+ }
696
+ };
697
+ var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
698
+ var ALL_MANUAL_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "manual").map(([name]) => name);
699
+ function getEventDefinition(name) {
700
+ return EVENT_REGISTRY[name];
701
+ }
702
+
703
+ // ../tracking-core/src/ingest-typed.ts
704
+ function createTypedClient(raw, _registry, options = {}) {
705
+ const debug = options.debug ?? false;
459
706
  return {
460
- page: {
461
- title: document.title || null,
462
- path: window.location.pathname,
463
- search: window.location.search,
464
- hash: window.location.hash
707
+ trackEvent(eventType, metadata, opts) {
708
+ if (debug) {
709
+ const def = getEventDefinition(eventType);
710
+ def.metadataSchema.parse(metadata);
711
+ }
712
+ raw.trackEvent({
713
+ eventType,
714
+ metadata,
715
+ pageUrl: opts?.pageUrl ?? null,
716
+ occurredAt: opts?.occurredAt ?? null
717
+ });
465
718
  },
466
- referrer: document.referrer || null,
467
- viewport: { w: window.innerWidth, h: window.innerHeight }
719
+ flush: raw.flush.bind(raw),
720
+ getSessionId: raw.getSessionId.bind(raw),
721
+ getVisitorId: raw.getVisitorId.bind(raw)
468
722
  };
469
723
  }
470
- function fireManualPageView(client) {
471
- const metadata = buildPageViewMetadata();
472
- client.trackEvent({
473
- eventType: "page_view",
474
- pageUrl: typeof window === "undefined" ? null : window.location.href,
475
- metadata
476
- });
724
+
725
+ // ../tracking-core/src/triggers/time-on-site.ts
726
+ function attachTimeOnSite(client, config) {
727
+ if (typeof window === "undefined" || typeof document === "undefined") {
728
+ return () => {
729
+ };
730
+ }
731
+ const thresholdMs = config.thresholdSeconds * 1e3;
732
+ let accumulatedMs = 0;
733
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
734
+ let timer = null;
735
+ let fired = false;
736
+ function fire() {
737
+ if (fired) return;
738
+ fired = true;
739
+ client.trackEvent({
740
+ eventType: "time_on_site",
741
+ metadata: {
742
+ duration_ms: thresholdMs,
743
+ page: { path: window.location.pathname }
744
+ },
745
+ pageUrl: window.location.href,
746
+ occurredAt: null
747
+ });
748
+ }
749
+ function scheduleNext() {
750
+ if (fired || activeSince === null) return;
751
+ const remaining = thresholdMs - accumulatedMs;
752
+ if (remaining <= 0) {
753
+ fire();
754
+ return;
755
+ }
756
+ timer = setTimeout(fire, remaining);
757
+ }
758
+ function clearTimer() {
759
+ if (timer !== null) {
760
+ clearTimeout(timer);
761
+ timer = null;
762
+ }
763
+ }
764
+ function onVisibilityChange() {
765
+ if (fired) return;
766
+ if (document.visibilityState === "hidden") {
767
+ if (activeSince !== null) {
768
+ accumulatedMs += Date.now() - activeSince;
769
+ activeSince = null;
770
+ }
771
+ clearTimer();
772
+ } else {
773
+ activeSince = Date.now();
774
+ scheduleNext();
775
+ }
776
+ }
777
+ document.addEventListener("visibilitychange", onVisibilityChange);
778
+ scheduleNext();
779
+ return () => {
780
+ clearTimer();
781
+ document.removeEventListener("visibilitychange", onVisibilityChange);
782
+ };
477
783
  }
478
- function attachAutoPageView(client, options = {}) {
784
+
785
+ // ../tracking-core/src/triggers/contact-page-visit.ts
786
+ function attachContactPageVisit(client, config) {
479
787
  if (typeof window === "undefined" || typeof history === "undefined") {
480
788
  return () => {
481
789
  };
482
790
  }
483
- let lastPath = window.location.pathname + window.location.search;
484
- function maybeFire() {
485
- const current = window.location.pathname + window.location.search;
486
- if (current === lastPath)
487
- return;
488
- lastPath = current;
489
- fireManualPageView(client);
791
+ const { pathPattern } = config;
792
+ let lastFiredPath = null;
793
+ function check() {
794
+ const path = window.location.pathname;
795
+ if (!pathPattern.test(path)) return;
796
+ if (lastFiredPath === path) return;
797
+ lastFiredPath = path;
798
+ client.trackEvent({
799
+ eventType: "contact_page_visit",
800
+ metadata: { page: { path } },
801
+ pageUrl: window.location.href,
802
+ occurredAt: null
803
+ });
490
804
  }
491
805
  const originalPushState = history.pushState.bind(history);
492
806
  const originalReplaceState = history.replaceState.bind(history);
493
807
  function patchedPushState(...args) {
494
808
  originalPushState(...args);
495
- setTimeout(maybeFire, 0);
809
+ setTimeout(check, 0);
496
810
  }
497
811
  function patchedReplaceState(...args) {
498
812
  originalReplaceState(...args);
499
- setTimeout(maybeFire, 0);
813
+ setTimeout(check, 0);
500
814
  }
501
815
  history.pushState = patchedPushState;
502
816
  history.replaceState = patchedReplaceState;
503
- window.addEventListener("popstate", maybeFire);
504
- if (!options.skipInitial)
505
- fireManualPageView(client);
817
+ window.addEventListener("popstate", check);
818
+ check();
506
819
  return () => {
507
820
  history.pushState = originalPushState;
508
821
  history.replaceState = originalReplaceState;
509
- window.removeEventListener("popstate", maybeFire);
822
+ window.removeEventListener("popstate", check);
510
823
  };
511
824
  }
512
825
 
@@ -595,68 +908,78 @@ function useConsentState() {
595
908
  return consentState;
596
909
  }
597
910
 
598
- // src/TrackingProvider.tsx
911
+ // src/factory.tsx
599
912
  var import_react4 = require("react");
600
913
  var import_jsx_runtime2 = require("react/jsx-runtime");
601
- var TrackingContext = (0, import_react4.createContext)(null);
602
- function TrackingProvider({
603
- apiKey,
604
- endpoint,
605
- gtagId,
606
- disableAutoPageView,
607
- debug,
608
- children
609
- }) {
610
- if (!apiKey)
611
- throw new Error("TrackingProvider: apiKey is required");
612
- if (!endpoint)
613
- throw new Error("TrackingProvider: endpoint is required");
614
- const clientRef = (0, import_react4.useRef)(null);
615
- if (clientRef.current === null) {
616
- clientRef.current = createTrackingClient({
617
- apiKey,
618
- endpoint,
619
- surface: "react",
620
- packageName: "@aranova/tracking-react",
621
- debug
622
- });
914
+ function createTracking(options) {
915
+ const { apiKey, endpoint, triggers, debug } = options;
916
+ if (!apiKey) throw new Error("createTracking: apiKey is required");
917
+ if (!endpoint) throw new Error("createTracking: endpoint is required");
918
+ const TrackingContext = (0, import_react4.createContext)(null);
919
+ function TrackingProvider({ gtagId, children }) {
920
+ const client = (0, import_react4.useMemo)(
921
+ () => createTypedClient(
922
+ getOrCreateTrackingClient({
923
+ apiKey,
924
+ endpoint,
925
+ surface: "react",
926
+ packageName: "@aranova/tracking-react",
927
+ debug
928
+ }),
929
+ triggers,
930
+ { debug }
931
+ ),
932
+ []
933
+ );
934
+ (0, import_react4.useEffect)(() => {
935
+ if (!gtagId) return;
936
+ bootstrapGoogleAdsTracking(gtagId);
937
+ }, [gtagId]);
938
+ (0, import_react4.useEffect)(() => {
939
+ const detachers = [];
940
+ const rawClient = getOrCreateTrackingClient({
941
+ apiKey,
942
+ endpoint,
943
+ surface: "react",
944
+ packageName: "@aranova/tracking-react",
945
+ debug
946
+ });
947
+ detachers.push(attachAutoPageView(rawClient));
948
+ detachers.push(attachBfcacheRestore(rawClient));
949
+ const timeOnSite = triggers.automatic.time_on_site;
950
+ if (timeOnSite) {
951
+ detachers.push(attachTimeOnSite(rawClient, timeOnSite));
952
+ }
953
+ const contactPageVisit = triggers.automatic.contact_page_visit;
954
+ if (contactPageVisit) {
955
+ detachers.push(attachContactPageVisit(rawClient, contactPageVisit));
956
+ }
957
+ return () => {
958
+ for (let i = detachers.length - 1; i >= 0; i--) {
959
+ detachers[i]();
960
+ }
961
+ };
962
+ }, []);
963
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrackingContext.Provider, { value: client, children });
623
964
  }
624
- (0, import_react4.useEffect)(() => {
625
- if (!gtagId)
626
- return;
627
- bootstrapGoogleAdsTracking(gtagId);
628
- }, [gtagId]);
629
- (0, import_react4.useEffect)(() => {
630
- const client = clientRef.current;
631
- if (client === null || disableAutoPageView)
632
- return;
633
- const detach = attachAutoPageView(client);
634
- return () => {
635
- detach();
636
- };
637
- }, [disableAutoPageView]);
638
- (0, import_react4.useEffect)(() => {
639
- return () => {
640
- clientRef.current?.destroy();
641
- clientRef.current = null;
642
- };
643
- }, []);
644
- const contextValue = (0, import_react4.useMemo)(() => clientRef.current, []);
645
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrackingContext.Provider, { value: contextValue, children });
646
- }
647
- function useTracking() {
648
- const client = (0, import_react4.useContext)(TrackingContext);
649
- if (client === null)
650
- throw new Error("useTracking must be called inside a <TrackingProvider>");
651
- return client;
965
+ function useTracking() {
966
+ const client = (0, import_react4.useContext)(TrackingContext);
967
+ if (client === null) {
968
+ throw new Error(
969
+ "useTracking must be called inside a <TrackingProvider> returned by createTracking()"
970
+ );
971
+ }
972
+ return client;
973
+ }
974
+ return { TrackingProvider, useTracking };
652
975
  }
653
976
  // Annotate the CommonJS export names for ESM import in node:
654
977
  0 && (module.exports = {
655
978
  ConsentBanner,
656
979
  GoogleAdsTracking,
657
980
  TRACKING_PARAM_KEYS,
658
- TrackingProvider,
659
981
  captureTrackingParamsFromLocation,
982
+ createTracking,
660
983
  createTrackingClientContext,
661
984
  createTrackingEventCreatePayload,
662
985
  createTrackingSessionUpsertPayload,
@@ -664,7 +987,6 @@ function useTracking() {
664
987
  setConsentState,
665
988
  useConsentState,
666
989
  useGclid,
667
- useTracking,
668
990
  useTrackingParams
669
991
  });
670
992
  //# sourceMappingURL=index.js.map