@aranova/tracking-next 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.mjs CHANGED
@@ -226,6 +226,103 @@ function getOrRotateSessionId(now = Date.now()) {
226
226
  return fresh.id;
227
227
  }
228
228
 
229
+ // ../tracking-core/src/events/page-view.ts
230
+ import { z } from "zod";
231
+ var pageViewMetadataSchema = z.object({
232
+ page: z.object({
233
+ title: z.string().nullable(),
234
+ path: z.string(),
235
+ search: z.string(),
236
+ hash: z.string()
237
+ }),
238
+ referrer: z.string().nullable(),
239
+ // `.nullable().optional()` — absent (undefined) OR explicit null OR a
240
+ // real viewport object. Mirrors Pydantic's `_Viewport | None = None`
241
+ // on the backend side so the drift test stays clean.
242
+ viewport: z.object({
243
+ w: z.number(),
244
+ h: z.number()
245
+ }).nullable().optional()
246
+ }).strict();
247
+ var pageViewConfigSchema = z.object({}).strict();
248
+
249
+ // ../tracking-core/src/page-view.ts
250
+ var LAST_FIRED_URL_STORAGE_KEY = "aranova_tracking_last_fired_url";
251
+ var lastFiredUrl = null;
252
+ var lastFiredUrlHydrated = false;
253
+ function readSessionStorage(key) {
254
+ try {
255
+ if (typeof window === "undefined") return null;
256
+ return window.sessionStorage.getItem(key);
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+ function writeSessionStorage(key, value) {
262
+ try {
263
+ if (typeof window === "undefined") return;
264
+ window.sessionStorage.setItem(key, value);
265
+ } catch {
266
+ }
267
+ }
268
+ function getLastFiredUrl() {
269
+ if (!lastFiredUrlHydrated) {
270
+ lastFiredUrlHydrated = true;
271
+ const stored = readSessionStorage(LAST_FIRED_URL_STORAGE_KEY);
272
+ if (stored !== null) lastFiredUrl = stored;
273
+ }
274
+ return lastFiredUrl;
275
+ }
276
+ function setLastFiredUrl(url) {
277
+ lastFiredUrl = url;
278
+ lastFiredUrlHydrated = true;
279
+ writeSessionStorage(LAST_FIRED_URL_STORAGE_KEY, url);
280
+ }
281
+ function buildPageViewMetadata(referrerOverride) {
282
+ if (typeof window === "undefined" || typeof document === "undefined")
283
+ return null;
284
+ return pageViewMetadataSchema.parse({
285
+ page: {
286
+ title: document.title || null,
287
+ path: window.location.pathname,
288
+ search: window.location.search,
289
+ hash: window.location.hash
290
+ },
291
+ referrer: referrerOverride !== void 0 ? referrerOverride : document.referrer || null,
292
+ viewport: { w: window.innerWidth, h: window.innerHeight }
293
+ });
294
+ }
295
+ function fireManualPageView(client) {
296
+ if (typeof window === "undefined")
297
+ return;
298
+ const currentHref = window.location.href;
299
+ const previousFiredUrl = getLastFiredUrl();
300
+ const internalReferrer = previousFiredUrl !== null && previousFiredUrl !== currentHref ? previousFiredUrl : null;
301
+ const externalReferrer = typeof document !== "undefined" ? document.referrer || null : null;
302
+ const referrer = internalReferrer ?? externalReferrer;
303
+ const metadata = buildPageViewMetadata(referrer);
304
+ client.trackEvent({
305
+ eventType: "page_view",
306
+ pageUrl: currentHref,
307
+ metadata
308
+ });
309
+ if (currentHref !== previousFiredUrl) {
310
+ setLastFiredUrl(currentHref);
311
+ }
312
+ }
313
+ function attachBfcacheRestore(client) {
314
+ if (typeof window === "undefined") return () => {
315
+ };
316
+ function handlePageShow(event) {
317
+ if (!event.persisted) return;
318
+ fireManualPageView(client);
319
+ }
320
+ window.addEventListener("pageshow", handlePageShow);
321
+ return () => {
322
+ window.removeEventListener("pageshow", handlePageShow);
323
+ };
324
+ }
325
+
229
326
  // ../tracking-core/src/ingest.ts
230
327
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
231
328
  var DEFAULT_MAX_QUEUE_SIZE = 10;
@@ -276,6 +373,23 @@ async function postWithFetch(url, body, apiKey, keepalive) {
276
373
  } catch {
277
374
  }
278
375
  }
376
+ var globalClient = null;
377
+ var globalClientKey = null;
378
+ function clientConfigKey(config) {
379
+ return `${config.apiKey}@${config.endpoint}#${config.surface}`;
380
+ }
381
+ function getOrCreateTrackingClient(config) {
382
+ const key = clientConfigKey(config);
383
+ if (globalClient !== null && globalClientKey === key) {
384
+ return globalClient;
385
+ }
386
+ if (globalClient !== null) {
387
+ globalClient.destroy();
388
+ }
389
+ globalClient = createTrackingClient(config);
390
+ globalClientKey = key;
391
+ return globalClient;
392
+ }
279
393
  function createTrackingClient(config) {
280
394
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
281
395
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -381,6 +495,9 @@ function createTrackingClient(config) {
381
495
  getVisitorId: () => visitorId,
382
496
  destroy: () => {
383
497
  destroyed = true;
498
+ if (queue.length > 0) {
499
+ flushOnUnload();
500
+ }
384
501
  clearScheduledFlush();
385
502
  queue = [];
386
503
  if (typeof window !== "undefined") {
@@ -390,28 +507,178 @@ function createTrackingClient(config) {
390
507
  };
391
508
  }
392
509
 
393
- // ../tracking-core/src/page-view.ts
394
- function buildPageViewMetadata() {
395
- if (typeof window === "undefined" || typeof document === "undefined")
396
- return null;
510
+ // ../tracking-core/src/events/contact-page-visit.ts
511
+ import { z as z2 } from "zod";
512
+ var contactPageVisitMetadataSchema = z2.object({
513
+ page: z2.object({
514
+ path: z2.string()
515
+ })
516
+ }).strict();
517
+ var contactPageVisitConfigSchema = z2.object({
518
+ // RegExp is runtime-only so we wrap it via z.custom. Consumers pass a
519
+ // real regex at createTracking() time; the factory validates with this
520
+ // schema and then keeps the live RegExp reference for runtime matching.
521
+ pathPattern: z2.custom(
522
+ (value) => value instanceof RegExp,
523
+ { message: "pathPattern must be a RegExp" }
524
+ )
525
+ }).strict();
526
+
527
+ // ../tracking-core/src/events/form-submit.ts
528
+ import { z as z3 } from "zod";
529
+ var formSubmitMetadataSchema = z3.object({
530
+ form: z3.object({
531
+ id: z3.string(),
532
+ action: z3.string().nullable()
533
+ }),
534
+ page: z3.object({
535
+ path: z3.string()
536
+ })
537
+ }).strict();
538
+ var formSubmitConfigSchema = z3.object({}).strict();
539
+
540
+ // ../tracking-core/src/events/phone-click.ts
541
+ import { z as z4 } from "zod";
542
+ var phoneClickMetadataSchema = z4.object({
543
+ element: z4.object({
544
+ tag: z4.string(),
545
+ text: z4.string().nullable(),
546
+ href: z4.string()
547
+ }),
548
+ page: z4.object({
549
+ path: z4.string()
550
+ })
551
+ }).strict();
552
+ var phoneClickConfigSchema = z4.object({}).strict();
553
+
554
+ // ../tracking-core/src/events/time-on-site.ts
555
+ import { z as z5 } from "zod";
556
+ var timeOnSiteMetadataSchema = z5.object({
557
+ duration_ms: z5.number().int().nonnegative(),
558
+ page: z5.object({
559
+ path: z5.string()
560
+ })
561
+ }).strict();
562
+ var timeOnSiteConfigSchema = z5.object({
563
+ thresholdSeconds: z5.number().int().positive()
564
+ }).strict();
565
+
566
+ // ../tracking-core/src/events/registry.ts
567
+ var EVENT_REGISTRY = {
568
+ // --- automatic triggers ---
569
+ page_view: {
570
+ kind: "automatic",
571
+ metadataSchema: pageViewMetadataSchema,
572
+ configSchema: pageViewConfigSchema
573
+ },
574
+ time_on_site: {
575
+ kind: "automatic",
576
+ metadataSchema: timeOnSiteMetadataSchema,
577
+ configSchema: timeOnSiteConfigSchema
578
+ },
579
+ contact_page_visit: {
580
+ kind: "automatic",
581
+ metadataSchema: contactPageVisitMetadataSchema,
582
+ configSchema: contactPageVisitConfigSchema
583
+ },
584
+ // --- manual triggers ---
585
+ form_submit: {
586
+ kind: "manual",
587
+ metadataSchema: formSubmitMetadataSchema,
588
+ configSchema: formSubmitConfigSchema
589
+ },
590
+ phone_click: {
591
+ kind: "manual",
592
+ metadataSchema: phoneClickMetadataSchema,
593
+ configSchema: phoneClickConfigSchema
594
+ }
595
+ };
596
+ var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
597
+ var ALL_MANUAL_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "manual").map(([name]) => name);
598
+ function getEventDefinition(name) {
599
+ return EVENT_REGISTRY[name];
600
+ }
601
+
602
+ // ../tracking-core/src/ingest-typed.ts
603
+ function createTypedClient(raw, _registry, options = {}) {
604
+ const debug = options.debug ?? false;
397
605
  return {
398
- page: {
399
- title: document.title || null,
400
- path: window.location.pathname,
401
- search: window.location.search,
402
- hash: window.location.hash
606
+ trackEvent(eventType, metadata, opts) {
607
+ if (debug) {
608
+ const def = getEventDefinition(eventType);
609
+ def.metadataSchema.parse(metadata);
610
+ }
611
+ raw.trackEvent({
612
+ eventType,
613
+ metadata,
614
+ pageUrl: opts?.pageUrl ?? null,
615
+ occurredAt: opts?.occurredAt ?? null
616
+ });
403
617
  },
404
- referrer: document.referrer || null,
405
- viewport: { w: window.innerWidth, h: window.innerHeight }
618
+ flush: raw.flush.bind(raw),
619
+ getSessionId: raw.getSessionId.bind(raw),
620
+ getVisitorId: raw.getVisitorId.bind(raw)
406
621
  };
407
622
  }
408
- function fireManualPageView(client) {
409
- const metadata = buildPageViewMetadata();
410
- client.trackEvent({
411
- eventType: "page_view",
412
- pageUrl: typeof window === "undefined" ? null : window.location.href,
413
- metadata
414
- });
623
+
624
+ // ../tracking-core/src/triggers/time-on-site.ts
625
+ function attachTimeOnSite(client, config) {
626
+ if (typeof window === "undefined" || typeof document === "undefined") {
627
+ return () => {
628
+ };
629
+ }
630
+ const thresholdMs = config.thresholdSeconds * 1e3;
631
+ let accumulatedMs = 0;
632
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
633
+ let timer = null;
634
+ let fired = false;
635
+ function fire() {
636
+ if (fired) return;
637
+ fired = true;
638
+ client.trackEvent({
639
+ eventType: "time_on_site",
640
+ metadata: {
641
+ duration_ms: thresholdMs,
642
+ page: { path: window.location.pathname }
643
+ },
644
+ pageUrl: window.location.href,
645
+ occurredAt: null
646
+ });
647
+ }
648
+ function scheduleNext() {
649
+ if (fired || activeSince === null) return;
650
+ const remaining = thresholdMs - accumulatedMs;
651
+ if (remaining <= 0) {
652
+ fire();
653
+ return;
654
+ }
655
+ timer = setTimeout(fire, remaining);
656
+ }
657
+ function clearTimer() {
658
+ if (timer !== null) {
659
+ clearTimeout(timer);
660
+ timer = null;
661
+ }
662
+ }
663
+ function onVisibilityChange() {
664
+ if (fired) return;
665
+ if (document.visibilityState === "hidden") {
666
+ if (activeSince !== null) {
667
+ accumulatedMs += Date.now() - activeSince;
668
+ activeSince = null;
669
+ }
670
+ clearTimer();
671
+ } else {
672
+ activeSince = Date.now();
673
+ scheduleNext();
674
+ }
675
+ }
676
+ document.addEventListener("visibilitychange", onVisibilityChange);
677
+ scheduleNext();
678
+ return () => {
679
+ clearTimer();
680
+ document.removeEventListener("visibilitychange", onVisibilityChange);
681
+ };
415
682
  }
416
683
 
417
684
  // src/ConsentBanner.tsx
@@ -509,7 +776,7 @@ function GoogleAdsTracking({ gtagId }) {
509
776
  ] });
510
777
  }
511
778
 
512
- // src/TrackingProvider.tsx
779
+ // src/factory.tsx
513
780
  import { usePathname, useSearchParams } from "next/navigation";
514
781
  import {
515
782
  createContext,
@@ -519,59 +786,100 @@ import {
519
786
  useRef
520
787
  } from "react";
521
788
  import { jsx as jsx3 } from "react/jsx-runtime";
522
- var TrackingContext = createContext(null);
523
- function TrackingProvider({
524
- apiKey,
525
- endpoint,
526
- disableAutoPageView,
527
- debug,
528
- children
529
- }) {
530
- if (!apiKey)
531
- throw new Error("TrackingProvider: apiKey is required");
532
- if (!endpoint)
533
- throw new Error("TrackingProvider: endpoint is required");
534
- const pathname = usePathname();
535
- const searchParams = useSearchParams();
536
- const clientRef = useRef(null);
537
- if (clientRef.current === null) {
538
- clientRef.current = createTrackingClient({
539
- apiKey,
540
- endpoint,
541
- surface: "next",
542
- packageName: "@aranova/tracking-next",
543
- debug
544
- });
789
+ function createTracking(options) {
790
+ const { apiKey, endpoint, triggers, debug } = options;
791
+ if (!apiKey) throw new Error("createTracking: apiKey is required");
792
+ if (!endpoint) throw new Error("createTracking: endpoint is required");
793
+ const TrackingContext = createContext(null);
794
+ const contactPageVisitConfig = triggers.automatic.contact_page_visit ?? null;
795
+ const timeOnSiteConfig = triggers.automatic.time_on_site ?? null;
796
+ function TrackingProvider({ children }) {
797
+ const pathname = usePathname();
798
+ const searchParams = useSearchParams();
799
+ const client = useMemo(
800
+ () => createTypedClient(
801
+ getOrCreateTrackingClient({
802
+ apiKey,
803
+ endpoint,
804
+ surface: "next",
805
+ packageName: "@aranova/tracking-next",
806
+ debug
807
+ }),
808
+ triggers,
809
+ { debug }
810
+ ),
811
+ []
812
+ );
813
+ useEffect3(() => {
814
+ const rawClient = getOrCreateTrackingClient({
815
+ apiKey,
816
+ endpoint,
817
+ surface: "next",
818
+ packageName: "@aranova/tracking-next",
819
+ debug
820
+ });
821
+ fireManualPageView(rawClient);
822
+ }, [pathname, searchParams]);
823
+ const lastContactPathRef = useRef(null);
824
+ useEffect3(() => {
825
+ if (!contactPageVisitConfig || pathname === null) return;
826
+ if (!contactPageVisitConfig.pathPattern.test(pathname)) return;
827
+ if (lastContactPathRef.current === pathname) return;
828
+ lastContactPathRef.current = pathname;
829
+ const rawClient = getOrCreateTrackingClient({
830
+ apiKey,
831
+ endpoint,
832
+ surface: "next",
833
+ packageName: "@aranova/tracking-next",
834
+ debug
835
+ });
836
+ rawClient.trackEvent({
837
+ eventType: "contact_page_visit",
838
+ metadata: { page: { path: pathname } },
839
+ pageUrl: null,
840
+ occurredAt: null
841
+ });
842
+ }, [pathname]);
843
+ useEffect3(() => {
844
+ const rawClient = getOrCreateTrackingClient({
845
+ apiKey,
846
+ endpoint,
847
+ surface: "next",
848
+ packageName: "@aranova/tracking-next",
849
+ debug
850
+ });
851
+ return attachBfcacheRestore(rawClient);
852
+ }, []);
853
+ useEffect3(() => {
854
+ if (!timeOnSiteConfig) return;
855
+ const rawClient = getOrCreateTrackingClient({
856
+ apiKey,
857
+ endpoint,
858
+ surface: "next",
859
+ packageName: "@aranova/tracking-next",
860
+ debug
861
+ });
862
+ return attachTimeOnSite(rawClient, timeOnSiteConfig);
863
+ }, []);
864
+ return /* @__PURE__ */ jsx3(TrackingContext.Provider, { value: client, children });
545
865
  }
546
- useEffect3(() => {
547
- if (disableAutoPageView)
548
- return;
549
- const client = clientRef.current;
550
- if (client === null)
551
- return;
552
- fireManualPageView(client);
553
- }, [pathname, searchParams, disableAutoPageView]);
554
- useEffect3(() => {
555
- return () => {
556
- clientRef.current?.destroy();
557
- clientRef.current = null;
558
- };
559
- }, []);
560
- const contextValue = useMemo(() => clientRef.current, []);
561
- return /* @__PURE__ */ jsx3(TrackingContext.Provider, { value: contextValue, children });
562
- }
563
- function useTracking() {
564
- const client = useContext(TrackingContext);
565
- if (client === null)
566
- throw new Error("useTracking must be called inside a <TrackingProvider>");
567
- return client;
866
+ function useTracking() {
867
+ const client = useContext(TrackingContext);
868
+ if (client === null) {
869
+ throw new Error(
870
+ "useTracking must be called inside a <TrackingProvider> returned by createTracking()"
871
+ );
872
+ }
873
+ return client;
874
+ }
875
+ return { TrackingProvider, useTracking };
568
876
  }
569
877
  export {
570
878
  ConsentBanner,
571
879
  GoogleAdsTracking,
572
880
  TRACKING_PARAM_KEYS,
573
- TrackingProvider,
574
881
  captureTrackingParamsFromLocation,
882
+ createTracking,
575
883
  createTrackingClientContext,
576
884
  createTrackingEventCreatePayload,
577
885
  createTrackingSessionUpsertPayload,
@@ -579,7 +887,6 @@ export {
579
887
  setConsentState,
580
888
  useConsentState,
581
889
  useGclid,
582
- useTracking,
583
890
  useTrackingParams
584
891
  };
585
892
  //# sourceMappingURL=index.mjs.map