All files / app/features/analytics hooks.ts

100% Statements 41/41
100% Branches 13/13
100% Functions 10/10
100% Lines 39/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 1111x   1x         3x 3x           1x                         6x       6x 6x 6x   6x 5x     6x   7x 5x 5x 5x                 6x                   11x         11x 11x 11x 11x 11x 11x     11x 9x 9x       11x     8x   8x   7x   4x 4x 3x 3x 3x 3x             7x         11x    
import { useCallback, useEffect, useRef } from "react";
 
import { logEvent } from "./eventCollector";
 
/**
 * Returns a stable `logEvent` function for use in components.
 */
export function useLogEvent() {
  return useCallback(
    (
      eventType: string,
      properties?: Record<string, unknown>,
      value?: number,
    ) => {
      logEvent(eventType, properties, value);
    },
    [],
  );
}
 
/**
 * Returns [start, stop] callbacks for timing an event.
 * On `stop()`, logs the event with value = duration in seconds.
 *
 * `properties` is stored in a ref so callers don't need to memoize it.
 * `stop` accepts optional `extraProperties` for dynamic data at measurement time.
 */
export function useDurationEvent(
  eventType: string,
  properties: Record<string, unknown> = {},
) {
  const startTimeRef = useRef<number | null>(null);
  const propsRef = useRef(properties);
  propsRef.current = properties;
 
  const start = useCallback(() => {
    startTimeRef.current = performance.now();
  }, []);
 
  const stop = useCallback(
    (extraProperties?: Record<string, unknown>) => {
      if (startTimeRef.current === null) return;
      const durationS = (performance.now() - startTimeRef.current) / 1000;
      startTimeRef.current = null;
      logEvent(
        eventType,
        { ...propsRef.current, ...extraProperties },
        durationS,
      );
    },
    [eventType],
  );
 
  return [start, stop] as const;
}
 
/**
 * Returns a callback ref. When the element enters the viewport,
 * fires the event once via IntersectionObserver.
 *
 * `properties` and `threshold` are stored in refs so callers don't
 * need to memoize them — the observer is only recreated when `eventType` changes.
 */
export function useImpressionRef(
  eventType: string,
  properties: Record<string, unknown> = {},
  options?: { threshold?: number },
) {
  const observerRef = useRef<IntersectionObserver | null>(null);
  const hasFiredRef = useRef(false);
  const propsRef = useRef(properties);
  propsRef.current = properties;
  const thresholdRef = useRef(options?.threshold ?? 0.5);
  thresholdRef.current = options?.threshold ?? 0.5;
 
  // Cleanup on unmount
  useEffect(() => {
    return () => {
      observerRef.current?.disconnect();
    };
  }, []);
 
  const callbackRef = useCallback(
    (node: HTMLElement | null) => {
      // Disconnect previous observer
      observerRef.current?.disconnect();
 
      if (!node || hasFiredRef.current) return;
 
      observerRef.current = new IntersectionObserver(
        (entries) => {
          for (const entry of entries) {
            if (entry.isIntersecting && !hasFiredRef.current) {
              hasFiredRef.current = true;
              logEvent(eventType, propsRef.current);
              observerRef.current?.disconnect();
              break;
            }
          }
        },
        { threshold: thresholdRef.current },
      );
 
      observerRef.current.observe(node);
    },
    [eventType],
  );
 
  return callbackRef;
}