diff --git a/apps/scandic-web/components/TrackingSDK/RouterTransition.tsx b/apps/scandic-web/components/TrackingSDK/RouterTransition.tsx deleted file mode 100644 index 7c59825cf..000000000 --- a/apps/scandic-web/components/TrackingSDK/RouterTransition.tsx +++ /dev/null @@ -1,203 +0,0 @@ -"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 -} diff --git a/apps/scandic-web/components/TrackingSDK/hooks.ts b/apps/scandic-web/components/TrackingSDK/hooks.ts new file mode 100644 index 000000000..69d9835b9 --- /dev/null +++ b/apps/scandic-web/components/TrackingSDK/hooks.ts @@ -0,0 +1,265 @@ +"use client" + +import { usePathname } from "next/navigation" +import { + startTransition, + useEffect, + useOptimistic, + useRef, + useState, +} from "react" + +import { trpc } from "@/lib/trpc/client" +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, + 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, +}: TrackingSDKProps) => { + const { + data: userTrackingData, + isPending, + isError, + } = trpc.user.userTrackingInfo.useQuery() + + 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, + }) + } + + if (document.readyState === "complete") { + track() + return + } + + window.addEventListener("load", track) + return () => window.removeEventListener("load", track) + }, [ + isError, + pathName, + hotelInfo, + userTrackingData, + pageData, + sessionId, + paymentInfo, + isPending, + ]) +} + +export const useTrackSoftNavigation = ({ + pageData, + hotelInfo, + paymentInfo, +}: TrackingSDKProps) => { + const { + data: userTrackingData, + isPending, + isError, + } = trpc.user.userTrackingInfo.useQuery() + + const [loading, setLoading] = useOptimistic(false) + 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) + setLoading(true) + }) + return + } + + if ( + !loading && + isTransitioning && + status === TransitionStatusEnum.Running + ) { + setStatus(TransitionStatusEnum.Done) + stopRouterTransition() + return + } + + if (!loading && !isTransitioning && status === TransitionStatusEnum.Done) { + const pageLoadTime = getPageLoadTime() + const trackingData = { + ...pageData, + sessionId, + pathName, + pageLoadTime: pageLoadTime, + } + const pageObject = createSDKPageObject(trackingData) + if (previousPathname.current !== pathName) { + const userData: TrackingSDKUserData = isError + ? { loginStatus: "Error" } + : userTrackingData + + trackPageView({ + event: "pageView", + pageInfo: pageObject, + userInfo: userData, + hotelInfo: hotelInfo, + paymentInfo, + }) + } + previousPathname.current = pathName // Update for next render + } + }, [ + isError, + isPending, + isTransitioning, + loading, + status, + setLoading, + stopRouterTransition, + pageData, + pathName, + hotelInfo, + getPageLoadTime, + sessionId, + paymentInfo, + userTrackingData, + ]) +} + +const trackPerformance = async ({ + pathName, + sessionId, + paymentInfo, + hotelInfo, + userData, + pageData, +}: { + pathName: string + sessionId: string | null + paymentInfo: TrackingSDKProps["paymentInfo"] + hotelInfo: TrackingSDKProps["hotelInfo"] + userData: TrackingSDKUserData + pageData: TrackingSDKProps["pageData"] +}) => { + 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, + }) +} + +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 }) + }) +} diff --git a/apps/scandic-web/components/TrackingSDK/index.tsx b/apps/scandic-web/components/TrackingSDK/index.tsx index 9b9caddb2..6c908e82b 100644 --- a/apps/scandic-web/components/TrackingSDK/index.tsx +++ b/apps/scandic-web/components/TrackingSDK/index.tsx @@ -1,8 +1,9 @@ "use client" -import { trpc } from "@/lib/trpc/client" - -import RouterTransition from "@/components/TrackingSDK/RouterTransition" +import { + useTrackHardNavigation, + useTrackSoftNavigation, +} from "@/components/TrackingSDK/hooks" import type { TrackingSDKHotelInfo, @@ -19,19 +20,8 @@ export default function TrackingSDK({ hotelInfo?: TrackingSDKHotelInfo paymentInfo?: TrackingSDKPaymentInfo }) { - const { data: userTrackingData, isPending } = - trpc.user.userTrackingInfo.useQuery() + useTrackHardNavigation({ pageData, hotelInfo, paymentInfo }) + useTrackSoftNavigation({ pageData, hotelInfo, paymentInfo }) - if (isPending || !userTrackingData) { - return null - } - - return ( - - ) + return null } diff --git a/apps/scandic-web/lib/trpc/Provider.tsx b/apps/scandic-web/lib/trpc/Provider.tsx index 8f30e734a..8d85b75fd 100644 --- a/apps/scandic-web/lib/trpc/Provider.tsx +++ b/apps/scandic-web/lib/trpc/Provider.tsx @@ -5,7 +5,7 @@ import { QueryClient, QueryClientProvider, } from "@tanstack/react-query" -import { httpBatchLink, loggerLink, TRPCClientError } from "@trpc/client" +import { httpLink, loggerLink, TRPCClientError } from "@trpc/client" import { useState } from "react" import { login } from "@/constants/routes/handleAuth" @@ -30,8 +30,7 @@ function initializeTrpcClient() { typeof window !== "undefined") || (opts.direction === "down" && opts.result instanceof Error), }), - httpBatchLink({ - fetch, + httpLink({ transformer, /** * This is locally in Next.js diff --git a/apps/scandic-web/package.json b/apps/scandic-web/package.json index 0d0ad756a..e854a0b79 100644 --- a/apps/scandic-web/package.json +++ b/apps/scandic-web/package.json @@ -49,9 +49,9 @@ "@tanstack/react-query-devtools": "^5.64.2", "@tanstack/react-table": "^8.20.5", "@testing-library/dom": "^10.0.0", - "@trpc/client": "^11.0.0-rc.467", - "@trpc/react-query": "^11.0.0-rc.467", - "@trpc/server": "^11.0.0-rc.467", + "@trpc/client": "^11.0.1", + "@trpc/react-query": "^11.0.1", + "@trpc/server": "^11.0.1", "@tsparticles/confetti": "^3.5.0", "@types/geojson": "^7946.0.16", "@types/supercluster": "^7.1.3", diff --git a/apps/scandic-web/server/routers/user/query.ts b/apps/scandic-web/server/routers/user/query.ts index 76b050033..51f7a55ee 100644 --- a/apps/scandic-web/server/routers/user/query.ts +++ b/apps/scandic-web/server/routers/user/query.ts @@ -368,10 +368,8 @@ export const userQueryRouter = router({ return notLoggedInUserTrackingData } - let verifiedUserData - try { - verifiedUserData = await getVerifiedUser({ session: ctx.session }) + const verifiedUserData = await getVerifiedUser({ session: ctx.session }) if (!verifiedUserData || "error" in verifiedUserData) { return notLoggedInUserTrackingData diff --git a/apps/scandic-web/server/trpc.ts b/apps/scandic-web/server/trpc.ts index 2a368c284..957b72f97 100644 --- a/apps/scandic-web/server/trpc.ts +++ b/apps/scandic-web/server/trpc.ts @@ -95,6 +95,7 @@ export const contentstackExtendedProcedureUID = contentstackBaseProcedure.use( export const protectedProcedure = baseProcedure.use(async function (opts) { const authRequired = opts.meta?.authRequired ?? true const session = await opts.ctx.auth() + if (!authRequired && env.NODE_ENV === "development") { console.info( `❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌` diff --git a/apps/scandic-web/stores/tracking.ts b/apps/scandic-web/stores/tracking.ts index 99737fea5..63ced2bf4 100644 --- a/apps/scandic-web/stores/tracking.ts +++ b/apps/scandic-web/stores/tracking.ts @@ -3,8 +3,6 @@ import { create } from "zustand" interface TrackingStoreState { - hasRun: boolean - setHasRun: () => void initialStartTime: number setInitialPageLoadTime: (time: number) => void getPageLoadTime: () => number @@ -17,10 +15,8 @@ interface TrackingStoreState { } const useTrackingStore = create((set, get) => ({ - hasRun: false, initialStartTime: Date.now(), setInitialPageLoadTime: (time) => set({ initialStartTime: time }), - setHasRun: () => set(() => ({ hasRun: true })), getPageLoadTime: () => { const { initialStartTime } = get() return (Date.now() - initialStartTime) / 1000 diff --git a/apps/scandic-web/types/components/tracking.ts b/apps/scandic-web/types/components/tracking.ts index 7a986eaca..65f615508 100644 --- a/apps/scandic-web/types/components/tracking.ts +++ b/apps/scandic-web/types/components/tracking.ts @@ -35,16 +35,21 @@ export enum LoginTypeEnum { } export type LoginType = keyof typeof LoginTypeEnum -export type TrackingSDKUserData = { - loginStatus: "logged in" | "Non-logged in" - loginType?: LoginType - memberId?: string - membershipNumber?: string - memberLevel?: MembershipLevel - noOfNightsStayed?: number - totalPointsAvailableToSpend?: number - loginAction?: "login success" -} +export type TrackingSDKUserData = + | { + loginStatus: "logged in" + loginType?: LoginType + memberId?: string + membershipNumber?: string + memberLevel?: MembershipLevel + noOfNightsStayed?: number + totalPointsAvailableToSpend?: number + loginAction?: "login success" + } + | { + loginStatus: "Non-logged in" + } + | { loginStatus: "Error" } export type TrackingSDKHotelInfo = { ageOfChildren?: string // "10", "2,5,10" @@ -112,7 +117,6 @@ export type TrackingSDKPaymentInfo = { export type TrackingSDKProps = { pageData: TrackingSDKPageData - userData: TrackingSDKUserData hotelInfo?: TrackingSDKHotelInfo paymentInfo?: TrackingSDKPaymentInfo } diff --git a/yarn.lock b/yarn.lock index 7a94033a8..9ca7a7ad8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6262,9 +6262,9 @@ __metadata: "@testing-library/jest-dom": "npm:^6.4.6" "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.5.2" - "@trpc/client": "npm:^11.0.0-rc.467" - "@trpc/react-query": "npm:^11.0.0-rc.467" - "@trpc/server": "npm:^11.0.0-rc.467" + "@trpc/client": "npm:^11.0.1" + "@trpc/react-query": "npm:^11.0.1" + "@trpc/server": "npm:^11.0.1" "@tsparticles/confetti": "npm:^3.5.0" "@types/geojson": "npm:^7946.0.16" "@types/jest": "npm:^29.5.12" @@ -7493,36 +7493,36 @@ __metadata: languageName: node linkType: hard -"@trpc/client@npm:^11.0.0-rc.467": - version: 11.0.0-rc.804 - resolution: "@trpc/client@npm:11.0.0-rc.804" +"@trpc/client@npm:^11.0.1": + version: 11.0.1 + resolution: "@trpc/client@npm:11.0.1" peerDependencies: - "@trpc/server": 11.0.0-rc.804+ab12d16b0 + "@trpc/server": 11.0.1 typescript: ">=5.7.2" - checksum: 10c0/713da95dc8cadc3bd9a667193301358676858977828506bf9711b2b1747aa974b522780ce46b124d2c57b39bff2f8f5112bbbf7ddef3f736256c6a01e9d21b2b + checksum: 10c0/ee6fb2567f30c1b5bc020b37efd373029e1fa15330de317d2ab7f4d1c4c4c41420010059679f803817e7eb49ff51bfc4727808ab49f9750f7ac6698fe4deed3f languageName: node linkType: hard -"@trpc/react-query@npm:^11.0.0-rc.467": - version: 11.0.0-rc.804 - resolution: "@trpc/react-query@npm:11.0.0-rc.804" +"@trpc/react-query@npm:^11.0.1": + version: 11.0.1 + resolution: "@trpc/react-query@npm:11.0.1" peerDependencies: - "@tanstack/react-query": ^5.62.8 - "@trpc/client": 11.0.0-rc.804+ab12d16b0 - "@trpc/server": 11.0.0-rc.804+ab12d16b0 + "@tanstack/react-query": ^5.67.1 + "@trpc/client": 11.0.1 + "@trpc/server": 11.0.1 react: ">=18.2.0" react-dom: ">=18.2.0" typescript: ">=5.7.2" - checksum: 10c0/40795ff9f6f1796ff8ed6075d91a24fc4833448b0c2fe6a8e061aa98d839729c38c9bd46ef8794833517846816093a75998ea40cdda0b05a8c70ddd91b90fd03 + checksum: 10c0/e7fa6bdff4a58a3d6f49896299d63fb768a923d7af196a173e9252b3bcc88ab6b5f2233300e898857506858d41a453f8ac9d8940bc24e8f502402a5e8950f637 languageName: node linkType: hard -"@trpc/server@npm:^11.0.0-rc.467": - version: 11.0.0-rc.804 - resolution: "@trpc/server@npm:11.0.0-rc.804" +"@trpc/server@npm:^11.0.1": + version: 11.0.1 + resolution: "@trpc/server@npm:11.0.1" peerDependencies: typescript: ">=5.7.2" - checksum: 10c0/72390c8e4d96c9897bf03a88e7d8c9e413289097f88803cc312552946f0ac2ea191ed53cd5d29103eee2d09a4d782f8b58ec390d3f38f2f192cfd3b929c91031 + checksum: 10c0/e96adc385e2bf2c20cdcf7cf79ec13299681e79640b8b3e22026e6816b474b83d7b1c99c0b4be485f4f9953dc401c18cb496bc3b3ea0d7a47c7944637ccaed4c languageName: node linkType: hard