@aranova/tracking-next 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.js CHANGED
@@ -33,8 +33,8 @@ __export(src_exports, {
33
33
  ConsentBanner: () => ConsentBanner,
34
34
  GoogleAdsTracking: () => GoogleAdsTracking,
35
35
  TRACKING_PARAM_KEYS: () => TRACKING_PARAM_KEYS,
36
- TrackingProvider: () => TrackingProvider,
37
36
  captureTrackingParamsFromLocation: () => captureTrackingParamsFromLocation,
37
+ createTracking: () => createTracking,
38
38
  createTrackingClientContext: () => createTrackingClientContext,
39
39
  createTrackingEventCreatePayload: () => createTrackingEventCreatePayload,
40
40
  createTrackingSessionUpsertPayload: () => createTrackingSessionUpsertPayload,
@@ -42,7 +42,6 @@ __export(src_exports, {
42
42
  setConsentState: () => setConsentState,
43
43
  useConsentState: () => useConsentState,
44
44
  useGclid: () => useGclid,
45
- useTracking: () => useTracking,
46
45
  useTrackingParams: () => useTrackingParams
47
46
  });
48
47
  module.exports = __toCommonJS(src_exports);
@@ -275,6 +274,103 @@ function getOrRotateSessionId(now = Date.now()) {
275
274
  return fresh.id;
276
275
  }
277
276
 
277
+ // ../tracking-core/src/events/page-view.ts
278
+ var import_zod = require("zod");
279
+ var pageViewMetadataSchema = import_zod.z.object({
280
+ page: import_zod.z.object({
281
+ title: import_zod.z.string().nullable(),
282
+ path: import_zod.z.string(),
283
+ search: import_zod.z.string(),
284
+ hash: import_zod.z.string()
285
+ }),
286
+ referrer: import_zod.z.string().nullable(),
287
+ // `.nullable().optional()` — absent (undefined) OR explicit null OR a
288
+ // real viewport object. Mirrors Pydantic's `_Viewport | None = None`
289
+ // on the backend side so the drift test stays clean.
290
+ viewport: import_zod.z.object({
291
+ w: import_zod.z.number(),
292
+ h: import_zod.z.number()
293
+ }).nullable().optional()
294
+ }).strict();
295
+ var pageViewConfigSchema = import_zod.z.object({}).strict();
296
+
297
+ // ../tracking-core/src/page-view.ts
298
+ var LAST_FIRED_URL_STORAGE_KEY = "aranova_tracking_last_fired_url";
299
+ var lastFiredUrl = null;
300
+ var lastFiredUrlHydrated = false;
301
+ function readSessionStorage(key) {
302
+ try {
303
+ if (typeof window === "undefined") return null;
304
+ return window.sessionStorage.getItem(key);
305
+ } catch {
306
+ return null;
307
+ }
308
+ }
309
+ function writeSessionStorage(key, value) {
310
+ try {
311
+ if (typeof window === "undefined") return;
312
+ window.sessionStorage.setItem(key, value);
313
+ } catch {
314
+ }
315
+ }
316
+ function getLastFiredUrl() {
317
+ if (!lastFiredUrlHydrated) {
318
+ lastFiredUrlHydrated = true;
319
+ const stored = readSessionStorage(LAST_FIRED_URL_STORAGE_KEY);
320
+ if (stored !== null) lastFiredUrl = stored;
321
+ }
322
+ return lastFiredUrl;
323
+ }
324
+ function setLastFiredUrl(url) {
325
+ lastFiredUrl = url;
326
+ lastFiredUrlHydrated = true;
327
+ writeSessionStorage(LAST_FIRED_URL_STORAGE_KEY, url);
328
+ }
329
+ function buildPageViewMetadata(referrerOverride) {
330
+ if (typeof window === "undefined" || typeof document === "undefined")
331
+ return null;
332
+ return pageViewMetadataSchema.parse({
333
+ page: {
334
+ title: document.title || null,
335
+ path: window.location.pathname,
336
+ search: window.location.search,
337
+ hash: window.location.hash
338
+ },
339
+ referrer: referrerOverride !== void 0 ? referrerOverride : document.referrer || null,
340
+ viewport: { w: window.innerWidth, h: window.innerHeight }
341
+ });
342
+ }
343
+ function fireManualPageView(client) {
344
+ if (typeof window === "undefined")
345
+ return;
346
+ const currentHref = window.location.href;
347
+ const previousFiredUrl = getLastFiredUrl();
348
+ const internalReferrer = previousFiredUrl !== null && previousFiredUrl !== currentHref ? previousFiredUrl : null;
349
+ const externalReferrer = typeof document !== "undefined" ? document.referrer || null : null;
350
+ const referrer = internalReferrer ?? externalReferrer;
351
+ const metadata = buildPageViewMetadata(referrer);
352
+ client.trackEvent({
353
+ eventType: "page_view",
354
+ pageUrl: currentHref,
355
+ metadata
356
+ });
357
+ if (currentHref !== previousFiredUrl) {
358
+ setLastFiredUrl(currentHref);
359
+ }
360
+ }
361
+ function attachBfcacheRestore(client) {
362
+ if (typeof window === "undefined") return () => {
363
+ };
364
+ function handlePageShow(event) {
365
+ if (!event.persisted) return;
366
+ fireManualPageView(client);
367
+ }
368
+ window.addEventListener("pageshow", handlePageShow);
369
+ return () => {
370
+ window.removeEventListener("pageshow", handlePageShow);
371
+ };
372
+ }
373
+
278
374
  // ../tracking-core/src/ingest.ts
279
375
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
280
376
  var DEFAULT_MAX_QUEUE_SIZE = 10;
@@ -325,6 +421,23 @@ async function postWithFetch(url, body, apiKey, keepalive) {
325
421
  } catch {
326
422
  }
327
423
  }
424
+ var globalClient = null;
425
+ var globalClientKey = null;
426
+ function clientConfigKey(config) {
427
+ return `${config.apiKey}@${config.endpoint}#${config.surface}`;
428
+ }
429
+ function getOrCreateTrackingClient(config) {
430
+ const key = clientConfigKey(config);
431
+ if (globalClient !== null && globalClientKey === key) {
432
+ return globalClient;
433
+ }
434
+ if (globalClient !== null) {
435
+ globalClient.destroy();
436
+ }
437
+ globalClient = createTrackingClient(config);
438
+ globalClientKey = key;
439
+ return globalClient;
440
+ }
328
441
  function createTrackingClient(config) {
329
442
  const flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
330
443
  const maxQueueSize = Math.min(config.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, HARD_MAX_BATCH);
@@ -430,6 +543,9 @@ function createTrackingClient(config) {
430
543
  getVisitorId: () => visitorId,
431
544
  destroy: () => {
432
545
  destroyed = true;
546
+ if (queue.length > 0) {
547
+ flushOnUnload();
548
+ }
433
549
  clearScheduledFlush();
434
550
  queue = [];
435
551
  if (typeof window !== "undefined") {
@@ -439,28 +555,178 @@ function createTrackingClient(config) {
439
555
  };
440
556
  }
441
557
 
442
- // ../tracking-core/src/page-view.ts
443
- function buildPageViewMetadata() {
444
- if (typeof window === "undefined" || typeof document === "undefined")
445
- return null;
558
+ // ../tracking-core/src/events/contact-page-visit.ts
559
+ var import_zod2 = require("zod");
560
+ var contactPageVisitMetadataSchema = import_zod2.z.object({
561
+ page: import_zod2.z.object({
562
+ path: import_zod2.z.string()
563
+ })
564
+ }).strict();
565
+ var contactPageVisitConfigSchema = import_zod2.z.object({
566
+ // RegExp is runtime-only so we wrap it via z.custom. Consumers pass a
567
+ // real regex at createTracking() time; the factory validates with this
568
+ // schema and then keeps the live RegExp reference for runtime matching.
569
+ pathPattern: import_zod2.z.custom(
570
+ (value) => value instanceof RegExp,
571
+ { message: "pathPattern must be a RegExp" }
572
+ )
573
+ }).strict();
574
+
575
+ // ../tracking-core/src/events/form-submit.ts
576
+ var import_zod3 = require("zod");
577
+ var formSubmitMetadataSchema = import_zod3.z.object({
578
+ form: import_zod3.z.object({
579
+ id: import_zod3.z.string(),
580
+ action: import_zod3.z.string().nullable()
581
+ }),
582
+ page: import_zod3.z.object({
583
+ path: import_zod3.z.string()
584
+ })
585
+ }).strict();
586
+ var formSubmitConfigSchema = import_zod3.z.object({}).strict();
587
+
588
+ // ../tracking-core/src/events/phone-click.ts
589
+ var import_zod4 = require("zod");
590
+ var phoneClickMetadataSchema = import_zod4.z.object({
591
+ element: import_zod4.z.object({
592
+ tag: import_zod4.z.string(),
593
+ text: import_zod4.z.string().nullable(),
594
+ href: import_zod4.z.string()
595
+ }),
596
+ page: import_zod4.z.object({
597
+ path: import_zod4.z.string()
598
+ })
599
+ }).strict();
600
+ var phoneClickConfigSchema = import_zod4.z.object({}).strict();
601
+
602
+ // ../tracking-core/src/events/time-on-site.ts
603
+ var import_zod5 = require("zod");
604
+ var timeOnSiteMetadataSchema = import_zod5.z.object({
605
+ duration_ms: import_zod5.z.number().int().nonnegative(),
606
+ page: import_zod5.z.object({
607
+ path: import_zod5.z.string()
608
+ })
609
+ }).strict();
610
+ var timeOnSiteConfigSchema = import_zod5.z.object({
611
+ thresholdSeconds: import_zod5.z.number().int().positive()
612
+ }).strict();
613
+
614
+ // ../tracking-core/src/events/registry.ts
615
+ var EVENT_REGISTRY = {
616
+ // --- automatic triggers ---
617
+ page_view: {
618
+ kind: "automatic",
619
+ metadataSchema: pageViewMetadataSchema,
620
+ configSchema: pageViewConfigSchema
621
+ },
622
+ time_on_site: {
623
+ kind: "automatic",
624
+ metadataSchema: timeOnSiteMetadataSchema,
625
+ configSchema: timeOnSiteConfigSchema
626
+ },
627
+ contact_page_visit: {
628
+ kind: "automatic",
629
+ metadataSchema: contactPageVisitMetadataSchema,
630
+ configSchema: contactPageVisitConfigSchema
631
+ },
632
+ // --- manual triggers ---
633
+ form_submit: {
634
+ kind: "manual",
635
+ metadataSchema: formSubmitMetadataSchema,
636
+ configSchema: formSubmitConfigSchema
637
+ },
638
+ phone_click: {
639
+ kind: "manual",
640
+ metadataSchema: phoneClickMetadataSchema,
641
+ configSchema: phoneClickConfigSchema
642
+ }
643
+ };
644
+ var ALL_AUTOMATIC_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "automatic").map(([name]) => name);
645
+ var ALL_MANUAL_EVENT_NAMES = Object.entries(EVENT_REGISTRY).filter(([, def]) => def.kind === "manual").map(([name]) => name);
646
+ function getEventDefinition(name) {
647
+ return EVENT_REGISTRY[name];
648
+ }
649
+
650
+ // ../tracking-core/src/ingest-typed.ts
651
+ function createTypedClient(raw, _registry, options = {}) {
652
+ const debug = options.debug ?? false;
446
653
  return {
447
- page: {
448
- title: document.title || null,
449
- path: window.location.pathname,
450
- search: window.location.search,
451
- hash: window.location.hash
654
+ trackEvent(eventType, metadata, opts) {
655
+ if (debug) {
656
+ const def = getEventDefinition(eventType);
657
+ def.metadataSchema.parse(metadata);
658
+ }
659
+ raw.trackEvent({
660
+ eventType,
661
+ metadata,
662
+ pageUrl: opts?.pageUrl ?? null,
663
+ occurredAt: opts?.occurredAt ?? null
664
+ });
452
665
  },
453
- referrer: document.referrer || null,
454
- viewport: { w: window.innerWidth, h: window.innerHeight }
666
+ flush: raw.flush.bind(raw),
667
+ getSessionId: raw.getSessionId.bind(raw),
668
+ getVisitorId: raw.getVisitorId.bind(raw)
455
669
  };
456
670
  }
457
- function fireManualPageView(client) {
458
- const metadata = buildPageViewMetadata();
459
- client.trackEvent({
460
- eventType: "page_view",
461
- pageUrl: typeof window === "undefined" ? null : window.location.href,
462
- metadata
463
- });
671
+
672
+ // ../tracking-core/src/triggers/time-on-site.ts
673
+ function attachTimeOnSite(client, config) {
674
+ if (typeof window === "undefined" || typeof document === "undefined") {
675
+ return () => {
676
+ };
677
+ }
678
+ const thresholdMs = config.thresholdSeconds * 1e3;
679
+ let accumulatedMs = 0;
680
+ let activeSince = document.visibilityState === "visible" ? Date.now() : null;
681
+ let timer = null;
682
+ let fired = false;
683
+ function fire() {
684
+ if (fired) return;
685
+ fired = true;
686
+ client.trackEvent({
687
+ eventType: "time_on_site",
688
+ metadata: {
689
+ duration_ms: thresholdMs,
690
+ page: { path: window.location.pathname }
691
+ },
692
+ pageUrl: window.location.href,
693
+ occurredAt: null
694
+ });
695
+ }
696
+ function scheduleNext() {
697
+ if (fired || activeSince === null) return;
698
+ const remaining = thresholdMs - accumulatedMs;
699
+ if (remaining <= 0) {
700
+ fire();
701
+ return;
702
+ }
703
+ timer = setTimeout(fire, remaining);
704
+ }
705
+ function clearTimer() {
706
+ if (timer !== null) {
707
+ clearTimeout(timer);
708
+ timer = null;
709
+ }
710
+ }
711
+ function onVisibilityChange() {
712
+ if (fired) return;
713
+ if (document.visibilityState === "hidden") {
714
+ if (activeSince !== null) {
715
+ accumulatedMs += Date.now() - activeSince;
716
+ activeSince = null;
717
+ }
718
+ clearTimer();
719
+ } else {
720
+ activeSince = Date.now();
721
+ scheduleNext();
722
+ }
723
+ }
724
+ document.addEventListener("visibilitychange", onVisibilityChange);
725
+ scheduleNext();
726
+ return () => {
727
+ clearTimer();
728
+ document.removeEventListener("visibilitychange", onVisibilityChange);
729
+ };
464
730
  }
465
731
 
466
732
  // src/ConsentBanner.tsx
@@ -558,64 +824,122 @@ function GoogleAdsTracking({ gtagId }) {
558
824
  ] });
559
825
  }
560
826
 
561
- // src/TrackingProvider.tsx
827
+ // src/factory.tsx
562
828
  var import_navigation = require("next/navigation");
563
829
  var import_react3 = require("react");
564
830
  var import_jsx_runtime3 = require("react/jsx-runtime");
565
- var TrackingContext = (0, import_react3.createContext)(null);
566
- function TrackingProvider({
567
- apiKey,
568
- endpoint,
569
- disableAutoPageView,
570
- debug,
571
- children
572
- }) {
573
- if (!apiKey)
574
- throw new Error("TrackingProvider: apiKey is required");
575
- if (!endpoint)
576
- throw new Error("TrackingProvider: endpoint is required");
577
- const pathname = (0, import_navigation.usePathname)();
578
- const searchParams = (0, import_navigation.useSearchParams)();
579
- const clientRef = (0, import_react3.useRef)(null);
580
- if (clientRef.current === null) {
581
- clientRef.current = createTrackingClient({
582
- apiKey,
583
- endpoint,
584
- surface: "next",
585
- packageName: "@aranova/tracking-next",
586
- debug
587
- });
588
- }
589
- (0, import_react3.useEffect)(() => {
590
- if (disableAutoPageView)
591
- return;
592
- const client = clientRef.current;
593
- if (client === null)
594
- return;
595
- fireManualPageView(client);
596
- }, [pathname, searchParams, disableAutoPageView]);
597
- (0, import_react3.useEffect)(() => {
598
- return () => {
599
- clientRef.current?.destroy();
600
- clientRef.current = null;
831
+ function createTracking(options) {
832
+ const { apiKey, endpoint, triggers, debug } = options;
833
+ if (!apiKey || !endpoint) {
834
+ if (apiKey || endpoint) {
835
+ console.warn(
836
+ "[AranovaTracking] createTracking() requires both `apiKey` and `endpoint`. Tracking is disabled for this session."
837
+ );
838
+ }
839
+ const noopClient = {
840
+ trackEvent: () => {
841
+ },
842
+ flush: async () => {
843
+ },
844
+ getSessionId: () => "",
845
+ getVisitorId: () => ""
601
846
  };
602
- }, []);
603
- const contextValue = (0, import_react3.useMemo)(() => clientRef.current, []);
604
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrackingContext.Provider, { value: contextValue, children });
605
- }
606
- function useTracking() {
607
- const client = (0, import_react3.useContext)(TrackingContext);
608
- if (client === null)
609
- throw new Error("useTracking must be called inside a <TrackingProvider>");
610
- return client;
847
+ return {
848
+ TrackingProvider: ({ children }) => children,
849
+ useTracking: () => noopClient
850
+ };
851
+ }
852
+ const TrackingContext = (0, import_react3.createContext)(null);
853
+ const contactPageVisitConfig = triggers.automatic.contact_page_visit ?? null;
854
+ const timeOnSiteConfig = triggers.automatic.time_on_site ?? null;
855
+ function TrackingProvider({ children }) {
856
+ const pathname = (0, import_navigation.usePathname)();
857
+ const searchParams = (0, import_navigation.useSearchParams)();
858
+ const client = (0, import_react3.useMemo)(
859
+ () => createTypedClient(
860
+ getOrCreateTrackingClient({
861
+ apiKey,
862
+ endpoint,
863
+ surface: "next",
864
+ packageName: "@aranova/tracking-next",
865
+ debug
866
+ }),
867
+ triggers,
868
+ { debug }
869
+ ),
870
+ []
871
+ );
872
+ (0, import_react3.useEffect)(() => {
873
+ const rawClient = getOrCreateTrackingClient({
874
+ apiKey,
875
+ endpoint,
876
+ surface: "next",
877
+ packageName: "@aranova/tracking-next",
878
+ debug
879
+ });
880
+ fireManualPageView(rawClient);
881
+ }, [pathname, searchParams]);
882
+ const lastContactPathRef = (0, import_react3.useRef)(null);
883
+ (0, import_react3.useEffect)(() => {
884
+ if (!contactPageVisitConfig || pathname === null) return;
885
+ if (!contactPageVisitConfig.pathPattern.test(pathname)) return;
886
+ if (lastContactPathRef.current === pathname) return;
887
+ lastContactPathRef.current = pathname;
888
+ const rawClient = getOrCreateTrackingClient({
889
+ apiKey,
890
+ endpoint,
891
+ surface: "next",
892
+ packageName: "@aranova/tracking-next",
893
+ debug
894
+ });
895
+ rawClient.trackEvent({
896
+ eventType: "contact_page_visit",
897
+ metadata: { page: { path: pathname } },
898
+ pageUrl: null,
899
+ occurredAt: null
900
+ });
901
+ }, [pathname]);
902
+ (0, import_react3.useEffect)(() => {
903
+ const rawClient = getOrCreateTrackingClient({
904
+ apiKey,
905
+ endpoint,
906
+ surface: "next",
907
+ packageName: "@aranova/tracking-next",
908
+ debug
909
+ });
910
+ return attachBfcacheRestore(rawClient);
911
+ }, []);
912
+ (0, import_react3.useEffect)(() => {
913
+ if (!timeOnSiteConfig) return;
914
+ const rawClient = getOrCreateTrackingClient({
915
+ apiKey,
916
+ endpoint,
917
+ surface: "next",
918
+ packageName: "@aranova/tracking-next",
919
+ debug
920
+ });
921
+ return attachTimeOnSite(rawClient, timeOnSiteConfig);
922
+ }, []);
923
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TrackingContext.Provider, { value: client, children });
924
+ }
925
+ function useTracking() {
926
+ const client = (0, import_react3.useContext)(TrackingContext);
927
+ if (client === null) {
928
+ throw new Error(
929
+ "useTracking must be called inside a <TrackingProvider> returned by createTracking()"
930
+ );
931
+ }
932
+ return client;
933
+ }
934
+ return { TrackingProvider, useTracking };
611
935
  }
612
936
  // Annotate the CommonJS export names for ESM import in node:
613
937
  0 && (module.exports = {
614
938
  ConsentBanner,
615
939
  GoogleAdsTracking,
616
940
  TRACKING_PARAM_KEYS,
617
- TrackingProvider,
618
941
  captureTrackingParamsFromLocation,
942
+ createTracking,
619
943
  createTrackingClientContext,
620
944
  createTrackingEventCreatePayload,
621
945
  createTrackingSessionUpsertPayload,
@@ -623,7 +947,6 @@ function useTracking() {
623
947
  setConsentState,
624
948
  useConsentState,
625
949
  useGclid,
626
- useTracking,
627
950
  useTrackingParams
628
951
  });
629
952
  //# sourceMappingURL=index.js.map