@aranova/tracking-react 0.3.0 → 0.4.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/dist/index.mjs CHANGED
@@ -249,6 +249,143 @@ function getOrRotateSessionId(now = Date.now()) {
249
249
  return fresh.id;
250
250
  }
251
251
 
252
+ // ../tracking-core/src/events/page-view.ts
253
+ import { z } from "zod";
254
+ var pageViewMetadataSchema = z.object({
255
+ page: z.object({
256
+ title: z.string().nullable(),
257
+ path: z.string(),
258
+ search: z.string(),
259
+ hash: z.string()
260
+ }),
261
+ referrer: z.string().nullable(),
262
+ // `.nullable().optional()` — absent (undefined) OR explicit null OR a
263
+ // real viewport object. Mirrors Pydantic's `_Viewport | None = None`
264
+ // on the backend side so the drift test stays clean.
265
+ viewport: z.object({
266
+ w: z.number(),
267
+ h: z.number()
268
+ }).nullable().optional()
269
+ }).strict();
270
+ var pageViewConfigSchema = z.object({}).strict();
271
+
272
+ // ../tracking-core/src/page-view.ts
273
+ var LAST_FIRED_URL_STORAGE_KEY = "aranova_tracking_last_fired_url";
274
+ var lastFiredUrl = null;
275
+ var lastFiredUrlHydrated = false;
276
+ function readSessionStorage(key) {
277
+ try {
278
+ if (typeof window === "undefined") return null;
279
+ return window.sessionStorage.getItem(key);
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+ function writeSessionStorage(key, value) {
285
+ try {
286
+ if (typeof window === "undefined") return;
287
+ window.sessionStorage.setItem(key, value);
288
+ } catch {
289
+ }
290
+ }
291
+ function getLastFiredUrl() {
292
+ if (!lastFiredUrlHydrated) {
293
+ lastFiredUrlHydrated = true;
294
+ const stored = readSessionStorage(LAST_FIRED_URL_STORAGE_KEY);
295
+ if (stored !== null) lastFiredUrl = stored;
296
+ }
297
+ return lastFiredUrl;
298
+ }
299
+ function setLastFiredUrl(url) {
300
+ lastFiredUrl = url;
301
+ lastFiredUrlHydrated = true;
302
+ writeSessionStorage(LAST_FIRED_URL_STORAGE_KEY, url);
303
+ }
304
+ function buildPageViewMetadata(referrerOverride) {
305
+ if (typeof window === "undefined" || typeof document === "undefined")
306
+ return null;
307
+ return pageViewMetadataSchema.parse({
308
+ page: {
309
+ title: document.title || null,
310
+ path: window.location.pathname,
311
+ search: window.location.search,
312
+ hash: window.location.hash
313
+ },
314
+ referrer: referrerOverride !== void 0 ? referrerOverride : document.referrer || null,
315
+ viewport: { w: window.innerWidth, h: window.innerHeight }
316
+ });
317
+ }
318
+ function fireManualPageView(client) {
319
+ if (typeof window === "undefined")
320
+ return;
321
+ const currentHref = window.location.href;
322
+ const previousFiredUrl = getLastFiredUrl();
323
+ const internalReferrer = previousFiredUrl !== null && previousFiredUrl !== currentHref ? previousFiredUrl : null;
324
+ const externalReferrer = typeof document !== "undefined" ? document.referrer || null : null;
325
+ const referrer = internalReferrer ?? externalReferrer;
326
+ const metadata = buildPageViewMetadata(referrer);
327
+ client.trackEvent({
328
+ eventType: "page_view",
329
+ pageUrl: currentHref,
330
+ metadata
331
+ });
332
+ if (currentHref !== previousFiredUrl) {
333
+ setLastFiredUrl(currentHref);
334
+ }
335
+ }
336
+ function attachBfcacheRestore(client) {
337
+ if (typeof window === "undefined") return () => {
338
+ };
339
+ function handlePageShow(event) {
340
+ if (!event.persisted) return;
341
+ fireManualPageView(client);
342
+ }
343
+ window.addEventListener("pageshow", handlePageShow);
344
+ return () => {
345
+ window.removeEventListener("pageshow", handlePageShow);
346
+ };
347
+ }
348
+ function attachAutoPageView(client, options = {}) {
349
+ if (typeof window === "undefined" || typeof history === "undefined") {
350
+ return () => {
351
+ };
352
+ }
353
+ let lastPath = window.location.pathname + window.location.search;
354
+ function maybeFire() {
355
+ const current = window.location.pathname + window.location.search;
356
+ if (current === lastPath)
357
+ return;
358
+ lastPath = current;
359
+ fireManualPageView(client);
360
+ }
361
+ const originalPushState = history.pushState.bind(history);
362
+ const originalReplaceState = history.replaceState.bind(history);
363
+ function patchedPushState(...args) {
364
+ originalPushState(...args);
365
+ setTimeout(maybeFire, 0);
366
+ }
367
+ function patchedReplaceState(...args) {
368
+ originalReplaceState(...args);
369
+ setTimeout(maybeFire, 0);
370
+ }
371
+ function handlePageShow(event) {
372
+ if (!event.persisted) return;
373
+ fireManualPageView(client);
374
+ }
375
+ history.pushState = patchedPushState;
376
+ history.replaceState = patchedReplaceState;
377
+ window.addEventListener("popstate", maybeFire);
378
+ window.addEventListener("pageshow", handlePageShow);
379
+ if (!options.skipInitial)
380
+ fireManualPageView(client);
381
+ return () => {
382
+ history.pushState = originalPushState;
383
+ history.replaceState = originalReplaceState;
384
+ window.removeEventListener("popstate", maybeFire);
385
+ window.removeEventListener("pageshow", handlePageShow);
386
+ };
387
+ }
388
+
252
389
  // ../tracking-core/src/ingest.ts
253
390
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
254
391
  var DEFAULT_MAX_QUEUE_SIZE = 10;
@@ -299,6 +436,23 @@ async function postWithFetch(url, body, apiKey, keepalive) {
299
436
  } catch {
300
437
  }
301
438
  }
439
+ var globalClient = null;
440
+ var globalClientKey = null;
441
+ function clientConfigKey(config) {
442
+ return `${config.apiKey}@${config.endpoint}#${config.surface}`;
443
+ }
444
+ function getOrCreateTrackingClient(config) {
445
+ const key = clientConfigKey(config);
446
+ if (globalClient !== null && globalClientKey === key) {
447
+ return globalClient;
448
+ }
449
+ if (globalClient !== null) {
450
+ globalClient.destroy();
451
+ }
452
+ globalClient = createTrackingClient(config);
453
+ globalClientKey = key;
454
+ return globalClient;
455
+ }
302
456
  function createTrackingClient(config) {
303
457
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
304
458
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -404,6 +558,9 @@ function createTrackingClient(config) {
404
558
  getVisitorId: () => visitorId,
405
559
  destroy: () => {
406
560
  destroyed = true;
561
+ if (queue.length > 0) {
562
+ flushOnUnload();
563
+ }
407
564
  clearScheduledFlush();
408
565
  queue = [];
409
566
  if (typeof window !== "undefined") {
@@ -413,61 +570,218 @@ function createTrackingClient(config) {
413
570
  };
414
571
  }
415
572
 
416
- // ../tracking-core/src/page-view.ts
417
- function buildPageViewMetadata() {
418
- if (typeof window === "undefined" || typeof document === "undefined")
419
- return null;
573
+ // ../tracking-core/src/events/contact-page-visit.ts
574
+ import { z as z2 } from "zod";
575
+ var contactPageVisitMetadataSchema = z2.object({
576
+ page: z2.object({
577
+ path: z2.string()
578
+ })
579
+ }).strict();
580
+ var contactPageVisitConfigSchema = z2.object({
581
+ // RegExp is runtime-only so we wrap it via z.custom. Consumers pass a
582
+ // real regex at createTracking() time; the factory validates with this
583
+ // schema and then keeps the live RegExp reference for runtime matching.
584
+ pathPattern: z2.custom(
585
+ (value) => value instanceof RegExp,
586
+ { message: "pathPattern must be a RegExp" }
587
+ )
588
+ }).strict();
589
+
590
+ // ../tracking-core/src/events/form-submit.ts
591
+ import { z as z3 } from "zod";
592
+ var formSubmitMetadataSchema = z3.object({
593
+ form: z3.object({
594
+ id: z3.string(),
595
+ action: z3.string().nullable()
596
+ }),
597
+ page: z3.object({
598
+ path: z3.string()
599
+ })
600
+ }).strict();
601
+ var formSubmitConfigSchema = z3.object({}).strict();
602
+
603
+ // ../tracking-core/src/events/phone-click.ts
604
+ import { z as z4 } from "zod";
605
+ var phoneClickMetadataSchema = z4.object({
606
+ element: z4.object({
607
+ tag: z4.string(),
608
+ text: z4.string().nullable(),
609
+ href: z4.string()
610
+ }),
611
+ page: z4.object({
612
+ path: z4.string()
613
+ })
614
+ }).strict();
615
+ var phoneClickConfigSchema = z4.object({}).strict();
616
+
617
+ // ../tracking-core/src/events/time-on-site.ts
618
+ import { z as z5 } from "zod";
619
+ var timeOnSiteMetadataSchema = z5.object({
620
+ duration_ms: z5.number().int().nonnegative(),
621
+ page: z5.object({
622
+ path: z5.string()
623
+ })
624
+ }).strict();
625
+ var timeOnSiteConfigSchema = z5.object({
626
+ thresholdSeconds: z5.number().int().positive()
627
+ }).strict();
628
+
629
+ // ../tracking-core/src/events/registry.ts
630
+ var EVENT_REGISTRY = {
631
+ // --- automatic triggers ---
632
+ page_view: {
633
+ kind: "automatic",
634
+ metadataSchema: pageViewMetadataSchema,
635
+ configSchema: pageViewConfigSchema
636
+ },
637
+ time_on_site: {
638
+ kind: "automatic",
639
+ metadataSchema: timeOnSiteMetadataSchema,
640
+ configSchema: timeOnSiteConfigSchema
641
+ },
642
+ contact_page_visit: {
643
+ kind: "automatic",
644
+ metadataSchema: contactPageVisitMetadataSchema,
645
+ configSchema: contactPageVisitConfigSchema
646
+ },
647
+ // --- manual triggers ---
648
+ form_submit: {
649
+ kind: "manual",
650
+ metadataSchema: formSubmitMetadataSchema,
651
+ configSchema: formSubmitConfigSchema
652
+ },
653
+ phone_click: {
654
+ kind: "manual",
655
+ metadataSchema: phoneClickMetadataSchema,
656
+ configSchema: phoneClickConfigSchema
657
+ }
658
+ };
659
+ var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
660
+ var ALL_MANUAL_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "manual").map(([name]) => name);
661
+ function getEventDefinition(name) {
662
+ return EVENT_REGISTRY[name];
663
+ }
664
+
665
+ // ../tracking-core/src/ingest-typed.ts
666
+ function createTypedClient(raw, _registry, options = {}) {
667
+ const debug = options.debug ?? false;
420
668
  return {
421
- page: {
422
- title: document.title || null,
423
- path: window.location.pathname,
424
- search: window.location.search,
425
- hash: window.location.hash
669
+ trackEvent(eventType, metadata, opts) {
670
+ if (debug) {
671
+ const def = getEventDefinition(eventType);
672
+ def.metadataSchema.parse(metadata);
673
+ }
674
+ raw.trackEvent({
675
+ eventType,
676
+ metadata,
677
+ pageUrl: opts?.pageUrl ?? null,
678
+ occurredAt: opts?.occurredAt ?? null
679
+ });
426
680
  },
427
- referrer: document.referrer || null,
428
- viewport: { w: window.innerWidth, h: window.innerHeight }
681
+ flush: raw.flush.bind(raw),
682
+ getSessionId: raw.getSessionId.bind(raw),
683
+ getVisitorId: raw.getVisitorId.bind(raw)
429
684
  };
430
685
  }
431
- function fireManualPageView(client) {
432
- const metadata = buildPageViewMetadata();
433
- client.trackEvent({
434
- eventType: "page_view",
435
- pageUrl: typeof window === "undefined" ? null : window.location.href,
436
- metadata
437
- });
686
+
687
+ // ../tracking-core/src/triggers/time-on-site.ts
688
+ function attachTimeOnSite(client, config) {
689
+ if (typeof window === "undefined" || typeof document === "undefined") {
690
+ return () => {
691
+ };
692
+ }
693
+ const thresholdMs = config.thresholdSeconds * 1e3;
694
+ let accumulatedMs = 0;
695
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
696
+ let timer = null;
697
+ let fired = false;
698
+ function fire() {
699
+ if (fired) return;
700
+ fired = true;
701
+ client.trackEvent({
702
+ eventType: "time_on_site",
703
+ metadata: {
704
+ duration_ms: thresholdMs,
705
+ page: { path: window.location.pathname }
706
+ },
707
+ pageUrl: window.location.href,
708
+ occurredAt: null
709
+ });
710
+ }
711
+ function scheduleNext() {
712
+ if (fired || activeSince === null) return;
713
+ const remaining = thresholdMs - accumulatedMs;
714
+ if (remaining <= 0) {
715
+ fire();
716
+ return;
717
+ }
718
+ timer = setTimeout(fire, remaining);
719
+ }
720
+ function clearTimer() {
721
+ if (timer !== null) {
722
+ clearTimeout(timer);
723
+ timer = null;
724
+ }
725
+ }
726
+ function onVisibilityChange() {
727
+ if (fired) return;
728
+ if (document.visibilityState === "hidden") {
729
+ if (activeSince !== null) {
730
+ accumulatedMs += Date.now() - activeSince;
731
+ activeSince = null;
732
+ }
733
+ clearTimer();
734
+ } else {
735
+ activeSince = Date.now();
736
+ scheduleNext();
737
+ }
738
+ }
739
+ document.addEventListener("visibilitychange", onVisibilityChange);
740
+ scheduleNext();
741
+ return () => {
742
+ clearTimer();
743
+ document.removeEventListener("visibilitychange", onVisibilityChange);
744
+ };
438
745
  }
439
- function attachAutoPageView(client, options = {}) {
746
+
747
+ // ../tracking-core/src/triggers/contact-page-visit.ts
748
+ function attachContactPageVisit(client, config) {
440
749
  if (typeof window === "undefined" || typeof history === "undefined") {
441
750
  return () => {
442
751
  };
443
752
  }
444
- let lastPath = window.location.pathname + window.location.search;
445
- function maybeFire() {
446
- const current = window.location.pathname + window.location.search;
447
- if (current === lastPath)
448
- return;
449
- lastPath = current;
450
- fireManualPageView(client);
753
+ const { pathPattern } = config;
754
+ let lastFiredPath = null;
755
+ function check() {
756
+ const path = window.location.pathname;
757
+ if (!pathPattern.test(path)) return;
758
+ if (lastFiredPath === path) return;
759
+ lastFiredPath = path;
760
+ client.trackEvent({
761
+ eventType: "contact_page_visit",
762
+ metadata: { page: { path } },
763
+ pageUrl: window.location.href,
764
+ occurredAt: null
765
+ });
451
766
  }
452
767
  const originalPushState = history.pushState.bind(history);
453
768
  const originalReplaceState = history.replaceState.bind(history);
454
769
  function patchedPushState(...args) {
455
770
  originalPushState(...args);
456
- setTimeout(maybeFire, 0);
771
+ setTimeout(check, 0);
457
772
  }
458
773
  function patchedReplaceState(...args) {
459
774
  originalReplaceState(...args);
460
- setTimeout(maybeFire, 0);
775
+ setTimeout(check, 0);
461
776
  }
462
777
  history.pushState = patchedPushState;
463
778
  history.replaceState = patchedReplaceState;
464
- window.addEventListener("popstate", maybeFire);
465
- if (!options.skipInitial)
466
- fireManualPageView(client);
779
+ window.addEventListener("popstate", check);
780
+ check();
467
781
  return () => {
468
782
  history.pushState = originalPushState;
469
783
  history.replaceState = originalReplaceState;
470
- window.removeEventListener("popstate", maybeFire);
784
+ window.removeEventListener("popstate", check);
471
785
  };
472
786
  }
473
787
 
@@ -556,67 +870,100 @@ function useConsentState() {
556
870
  return consentState;
557
871
  }
558
872
 
559
- // src/TrackingProvider.tsx
560
- import { createContext, useContext, useEffect as useEffect4, useMemo, useRef } from "react";
873
+ // src/factory.tsx
874
+ import {
875
+ createContext,
876
+ useContext,
877
+ useEffect as useEffect4,
878
+ useMemo
879
+ } from "react";
561
880
  import { jsx as jsx2 } from "react/jsx-runtime";
562
- var TrackingContext = createContext(null);
563
- function TrackingProvider({
564
- apiKey,
565
- endpoint,
566
- gtagId,
567
- disableAutoPageView,
568
- debug,
569
- children
570
- }) {
571
- if (!apiKey)
572
- throw new Error("TrackingProvider: apiKey is required");
573
- if (!endpoint)
574
- throw new Error("TrackingProvider: endpoint is required");
575
- const clientRef = useRef(null);
576
- if (clientRef.current === null) {
577
- clientRef.current = createTrackingClient({
578
- apiKey,
579
- endpoint,
580
- surface: "react",
581
- packageName: "@aranova/tracking-react",
582
- debug
583
- });
584
- }
585
- useEffect4(() => {
586
- if (!gtagId)
587
- return;
588
- bootstrapGoogleAdsTracking(gtagId);
589
- }, [gtagId]);
590
- useEffect4(() => {
591
- const client = clientRef.current;
592
- if (client === null || disableAutoPageView)
593
- return;
594
- const detach = attachAutoPageView(client);
595
- return () => {
596
- detach();
597
- };
598
- }, [disableAutoPageView]);
599
- useEffect4(() => {
600
- return () => {
601
- clientRef.current?.destroy();
602
- clientRef.current = null;
881
+ var NOOP_CLIENT = {
882
+ trackEvent: () => {
883
+ },
884
+ flush: async () => {
885
+ },
886
+ getSessionId: () => "",
887
+ getVisitorId: () => ""
888
+ };
889
+ function createTracking(options) {
890
+ const { apiKey, endpoint, triggers, debug } = options;
891
+ if (!apiKey || !endpoint) {
892
+ if (apiKey || endpoint) {
893
+ console.warn(
894
+ "[AranovaTracking] createTracking() requires both `apiKey` and `endpoint`. Tracking is disabled for this session."
895
+ );
896
+ }
897
+ const noopTyped = NOOP_CLIENT;
898
+ return {
899
+ TrackingProvider: ({ children }) => children,
900
+ useTracking: () => noopTyped
603
901
  };
604
- }, []);
605
- const contextValue = useMemo(() => clientRef.current, []);
606
- return /* @__PURE__ */ jsx2(TrackingContext.Provider, { value: contextValue, children });
607
- }
608
- function useTracking() {
609
- const client = useContext(TrackingContext);
610
- if (client === null)
611
- throw new Error("useTracking must be called inside a <TrackingProvider>");
612
- return client;
902
+ }
903
+ const TrackingContext = createContext(null);
904
+ function TrackingProvider({ gtagId, children }) {
905
+ const client = useMemo(
906
+ () => createTypedClient(
907
+ getOrCreateTrackingClient({
908
+ apiKey,
909
+ endpoint,
910
+ surface: "react",
911
+ packageName: "@aranova/tracking-react",
912
+ debug
913
+ }),
914
+ triggers,
915
+ { debug }
916
+ ),
917
+ []
918
+ );
919
+ useEffect4(() => {
920
+ if (!gtagId) return;
921
+ bootstrapGoogleAdsTracking(gtagId);
922
+ }, [gtagId]);
923
+ useEffect4(() => {
924
+ const detachers = [];
925
+ const rawClient = getOrCreateTrackingClient({
926
+ apiKey,
927
+ endpoint,
928
+ surface: "react",
929
+ packageName: "@aranova/tracking-react",
930
+ debug
931
+ });
932
+ detachers.push(attachAutoPageView(rawClient));
933
+ detachers.push(attachBfcacheRestore(rawClient));
934
+ const timeOnSite = triggers.automatic.time_on_site;
935
+ if (timeOnSite) {
936
+ detachers.push(attachTimeOnSite(rawClient, timeOnSite));
937
+ }
938
+ const contactPageVisit = triggers.automatic.contact_page_visit;
939
+ if (contactPageVisit) {
940
+ detachers.push(attachContactPageVisit(rawClient, contactPageVisit));
941
+ }
942
+ return () => {
943
+ for (let i = detachers.length - 1; i >= 0; i--) {
944
+ detachers[i]();
945
+ }
946
+ };
947
+ }, []);
948
+ return /* @__PURE__ */ jsx2(TrackingContext.Provider, { value: client, children });
949
+ }
950
+ function useTracking() {
951
+ const client = useContext(TrackingContext);
952
+ if (client === null) {
953
+ throw new Error(
954
+ "useTracking must be called inside a <TrackingProvider> returned by createTracking()"
955
+ );
956
+ }
957
+ return client;
958
+ }
959
+ return { TrackingProvider, useTracking };
613
960
  }
614
961
  export {
615
962
  ConsentBanner,
616
963
  GoogleAdsTracking,
617
964
  TRACKING_PARAM_KEYS,
618
- TrackingProvider,
619
965
  captureTrackingParamsFromLocation,
966
+ createTracking,
620
967
  createTrackingClientContext,
621
968
  createTrackingEventCreatePayload,
622
969
  createTrackingSessionUpsertPayload,
@@ -624,7 +971,6 @@ export {
624
971
  setConsentState,
625
972
  useConsentState,
626
973
  useGclid,
627
- useTracking,
628
974
  useTrackingParams
629
975
  };
630
976
  //# sourceMappingURL=index.mjs.map