"use client" import { usePathname } from "next/navigation" import { startTransition, useEffect, useOptimistic, useRef, useState, } from "react" import useRouterTransitionStore from "@/stores/router-transition" import useTrackingStore from "@/stores/tracking" import { useSessionId } from "@/hooks/useSessionId" import { promiseWithTimeout } from "@/utils/promiseWithTimeout" import { createSDKPageObject, trackPageView } from "@/utils/tracking" import type { TrackingSDKProps } from "@/types/components/tracking" enum TransitionStatusEnum { NotRun = "NotRun", Running = "Running", Done = "Done", } type TransitionStatus = keyof typeof TransitionStatusEnum export default function RouterTransition({ pageData, userData, hotelInfo, paymentInfo, }: TrackingSDKProps) { const [loading, setLoading] = useOptimistic(false) const [status, setStatus] = useState( TransitionStatusEnum.NotRun ) const { getPageLoadTime, hasRun, setHasRun } = useTrackingStore() const sessionId = useSessionId() const pathName = usePathname() const { isTransitioning, stopRouterTransition } = useRouterTransitionStore() const previousPathname = useRef(null) // We need this check to differentiate hard vs soft navigations // This is not because of StrictMode const hasRunInitial = useRef(false) useEffect(() => { if (!hasRun && !hasRunInitial.current) { hasRunInitial.current = true setHasRun() 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 }) }) } 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 trackPerformance = async () => { let pageLoadTime: number | undefined = undefined let lcpTime: number | undefined = undefined try { pageLoadTime = await promiseWithTimeout(getPageLoadTimeEntry(), 3000) } catch (error) { console.error("Error obtaining pageLoadTime:", error) } try { lcpTime = await promiseWithTimeout(getLCPTimeEntry(), 3000) } catch (error) { console.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, }) } if (document.readyState === "complete") { trackPerformance() } else { window.addEventListener("load", trackPerformance) return () => window.removeEventListener("load", trackPerformance) } } }, [ pathName, hasRun, setHasRun, hotelInfo, userData, pageData, sessionId, paymentInfo, ]) useEffect(() => { if (isTransitioning && status === TransitionStatusEnum.NotRun) { startTransition(() => { setStatus(TransitionStatusEnum.Running) setLoading(true) }) } else if ( !loading && isTransitioning && status === TransitionStatusEnum.Running ) { setStatus(TransitionStatusEnum.Done) stopRouterTransition() } else if ( !loading && !isTransitioning && status === TransitionStatusEnum.Done ) { if (hasRun && !hasRunInitial.current) { const pageLoadTime = getPageLoadTime() const trackingData = { ...pageData, sessionId, pathName, pageLoadTime: pageLoadTime, } const pageObject = createSDKPageObject(trackingData) if (previousPathname.current !== pathName) { trackPageView({ event: "pageView", pageInfo: pageObject, userInfo: userData, hotelInfo: hotelInfo, paymentInfo, }) } previousPathname.current = pathName // Update for next render } } }, [ isTransitioning, loading, status, setLoading, stopRouterTransition, pageData, pathName, userData, hotelInfo, getPageLoadTime, hasRun, sessionId, paymentInfo, ]) return null }