"use client" import { usePathname } from "next/navigation" import { startTransition, useCallback, useEffect, useRef, useState, } from "react" import { type Control, type FieldValues, useFormState, type UseFromSubscribe, } from "react-hook-form" import { useSessionId } from "@scandic-hotels/common/hooks/useSessionId" import { logger } from "@scandic-hotels/common/logger" import { trpc } from "@scandic-hotels/trpc/client" import useRouterTransitionStore from "@/stores/router-transition" import useTrackingStore from "@/stores/tracking" import useLang from "@/hooks/useLang" import { promiseWithTimeout } from "@/utils/promiseWithTimeout" import { createSDKPageObject, trackPageView } from "@/utils/tracking" import { type FormType, trackFormAbandonment, trackFormCompletion, trackFormInputStarted, } from "@/utils/tracking/form" import type { TrackingSDKProps, TrackingSDKUserData, } from "@/types/components/tracking" enum TransitionStatusEnum { NotRun = "NotRun", Running = "Running", Done = "Done", } type TransitionStatus = keyof typeof TransitionStatusEnum let hasTrackedHardNavigation = false export const useTrackHardNavigation = ({ pageData, hotelInfo, paymentInfo, ancillaries, }: TrackingSDKProps) => { const lang = useLang() const { data: userTrackingData, isPending, isError, } = trpc.user.userTrackingInfo.useQuery({ lang }) const sessionId = useSessionId() const pathName = usePathname() useEffect(() => { if (isPending) { return } const userData: TrackingSDKUserData = isError ? { loginStatus: "Error" } : userTrackingData if (hasTrackedHardNavigation) { return } hasTrackedHardNavigation = true const track = () => { trackPerformance({ pathName, sessionId, paymentInfo, hotelInfo, userData, pageData, ancillaries, }) } if (document.readyState === "complete") { track() return } window.addEventListener("load", track) return () => window.removeEventListener("load", track) }, [ isError, pathName, hotelInfo, userTrackingData, pageData, sessionId, paymentInfo, isPending, ancillaries, ]) } export const useTrackSoftNavigation = ({ pageData, hotelInfo, paymentInfo, ancillaries, }: TrackingSDKProps) => { const lang = useLang() const { data: userTrackingData, isPending, isError, } = trpc.user.userTrackingInfo.useQuery({ lang }) const [status, setStatus] = useState( TransitionStatusEnum.NotRun ) const { getPageLoadTime } = useTrackingStore() const sessionId = useSessionId() const pathName = usePathname() const { isTransitioning, stopRouterTransition } = useRouterTransitionStore() const previousPathname = useRef(null) useEffect(() => { if (isPending) { return } if (isTransitioning && status === TransitionStatusEnum.NotRun) { startTransition(() => { setStatus(TransitionStatusEnum.Running) }) return } if (isTransitioning && status === TransitionStatusEnum.Running) { setStatus(TransitionStatusEnum.Done) stopRouterTransition() return } if (!isTransitioning && status === TransitionStatusEnum.Done) { const pageLoadTime = getPageLoadTime() const trackingData = { ...pageData, sessionId, pathName, pageLoadTime: pageLoadTime, } const pageObject = createSDKPageObject(trackingData) const userData: TrackingSDKUserData = isError ? { loginStatus: "Error" } : userTrackingData trackPageView({ event: "pageView", pageInfo: pageObject, userInfo: userData, hotelInfo: hotelInfo, paymentInfo, ancillaries, }) setStatus(TransitionStatusEnum.NotRun) // Reset status previousPathname.current = pathName // Update for next render } }, [ isError, isPending, isTransitioning, status, stopRouterTransition, pageData, pathName, hotelInfo, getPageLoadTime, sessionId, paymentInfo, userTrackingData, ancillaries, ]) } const trackPerformance = async ({ pathName, sessionId, paymentInfo, hotelInfo, userData, pageData, ancillaries, }: { pathName: string sessionId: string | null paymentInfo: TrackingSDKProps["paymentInfo"] hotelInfo: TrackingSDKProps["hotelInfo"] userData: TrackingSDKUserData pageData: TrackingSDKProps["pageData"] ancillaries: TrackingSDKProps["ancillaries"] }) => { let pageLoadTime: number | undefined = undefined let lcpTime: number | undefined = undefined try { pageLoadTime = await promiseWithTimeout(getPageLoadTimeEntry(), 3000) } catch (error) { logger.error("Error obtaining pageLoadTime:", error) } try { lcpTime = await promiseWithTimeout(getLCPTimeEntry(), 3000) } catch (error) { logger.error("Error obtaining lcpTime:", error) } const trackingData = { ...pageData, sessionId, pathName, pageLoadTime, lcpTime, } const pageObject = createSDKPageObject(trackingData) trackPageView({ event: "pageView", pageInfo: pageObject, userInfo: userData, hotelInfo, paymentInfo, ancillaries, }) } const getLCPTimeEntry = () => { return new Promise((resolve) => { const observer = new PerformanceObserver((entries) => { const lastEntry = entries.getEntries().at(-1) if (lastEntry) { observer.disconnect() resolve(lastEntry.startTime / 1000) } }) const lcpSupported = PerformanceObserver.supportedEntryTypes?.includes( "largest-contentful-paint" ) if (lcpSupported) { observer.observe({ type: "largest-contentful-paint", buffered: true, }) } else { resolve(undefined) } }) } const getPageLoadTimeEntry = () => { return new Promise((resolve) => { const observer = new PerformanceObserver((entries) => { const navEntry = entries.getEntriesByType("navigation")[0] if (navEntry) { observer.disconnect() resolve(navEntry.duration / 1000) } }) observer.observe({ type: "navigation", buffered: true }) }) } export function useFormTracking( formType: FormType, subscribe: UseFromSubscribe, control: Control, nameSuffix: string = "" ) { const [formStarted, setFormStarted] = useState(false) const lastAccessedField = useRef(undefined) const formState = useFormState({ control }) useEffect(() => { const unsubscribe = subscribe({ formState: { dirtyFields: true }, callback: (data) => { if ("name" in data) { lastAccessedField.current = data.name as string } if (!formStarted) { trackFormInputStarted(formType, nameSuffix) setFormStarted(true) } }, }) return () => unsubscribe() }, [subscribe, formType, nameSuffix, formStarted]) useEffect(() => { if (!formStarted || !lastAccessedField.current || formState.isValid) return const lastField = lastAccessedField.current function handleBeforeUnload() { trackFormAbandonment(formType, lastField, nameSuffix) } function handleVisibilityChange() { if (document.visibilityState === "hidden") { trackFormAbandonment(formType, lastField, nameSuffix) } } window.addEventListener("beforeunload", handleBeforeUnload) window.addEventListener("visibilitychange", handleVisibilityChange) return () => { window.removeEventListener("beforeunload", handleBeforeUnload) window.removeEventListener("visibilitychange", handleVisibilityChange) } }, [formStarted, formType, nameSuffix, formState.isValid]) const trackFormSubmit = useCallback(() => { if (formState.isValid) { trackFormCompletion(formType, nameSuffix) } }, [formType, nameSuffix, formState.isValid]) return { trackFormSubmit, } }