feat: performance improvements

This commit is contained in:
Linus Flood
2024-11-05 12:53:57 +01:00
parent 249a5f6cb8
commit 7e4bbfb3e6
8 changed files with 290 additions and 300 deletions

View File

@@ -69,7 +69,6 @@ export default async function StepPage({
const hotelData = await getHotelData({
hotelId,
language: lang,
include: [HotelIncludeEnum.RoomCategories],
})
const roomAvailability = await getSelectedRoomAvailability({
hotelId,

View File

@@ -61,7 +61,6 @@ export default async function SelectRatePage({
serverClient().hotel.hotelData.get({
hotelId: searchParams.hotel,
language: params.lang,
include: [HotelIncludeEnum.RoomCategories],
}),
serverClient().hotel.availability.rooms({
hotelId: parseInt(searchParams.hotel, 10),

View File

@@ -31,9 +31,7 @@ export default async function HotelPage() {
const lang = getLang()
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const hotelData = await serverClient().hotel.get({
include: ["RoomCategories"],
})
const hotelData = await serverClient().hotel.get()
if (!hotelData) {
return null
}

View File

@@ -1,4 +1,4 @@
import { serverClient } from "@/lib/trpc/server"
import { getCurrentFooter } from "@/lib/trpc/memoizedRequests"
import Image from "@/components/Image"
import { getLang } from "@/i18n/serverContext"
@@ -8,9 +8,7 @@ import Navigation from "./Navigation"
import styles from "./footer.module.css"
export default async function Footer() {
const footerData = await serverClient().contentstack.base.currentFooter({
lang: getLang(),
})
const footerData = await getCurrentFooter(getLang())
if (!footerData) {
return null
}

View File

@@ -61,18 +61,15 @@ export const getHotelData = cache(async function getMemoizedHotelData({
hotelId,
language,
isCardOnlyPayment,
include,
}: {
hotelId: string
language: string
isCardOnlyPayment?: boolean
include?: HotelIncludeEnum[]
}) {
return serverClient().hotel.hotelData.get({
hotelId,
language,
isCardOnlyPayment,
include,
})
})
@@ -120,6 +117,12 @@ export const getCurrentHeader = cache(async function getMemoizedCurrentHeader(
return serverClient().contentstack.base.currentHeader({ lang })
})
export const getCurrentFooter = cache(async function getMemoizedCurrentFooter(
lang: Lang
) {
return serverClient().contentstack.base.currentFooter({ lang })
})
export const getMyPagesNavigation = cache(
async function getMemoizedMyPagesNavigation() {
return serverClient().contentstack.myPages.navigation.get()

View File

@@ -1,4 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { cache } from "react"
import {
MembershipLevel,
@@ -42,7 +43,7 @@ const getByLevelLoyaltyLevelFailCounter = meter.createCounter(
"trpc.contentstack.loyaltyLevel.byLevel-fail"
)
export async function getAllLoyaltyLevels(ctx: Context) {
export const getAllLoyaltyLevels = cache(async (ctx: Context) => {
getAllLoyaltyLevelCounter.add(1)
// Ideally we should fetch all available tiers from API, but since they
@@ -95,58 +96,60 @@ export async function getAllLoyaltyLevels(ctx: Context) {
getAllLoyaltyLevelSuccessCounter.add(1)
return validatedLoyaltyLevels.data
}
})
export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) {
getByLevelLoyaltyLevelCounter.add(1, {
query: JSON.stringify({ lang: ctx.lang, level_id }),
})
export const getLoyaltyLevel = cache(
async (ctx: Context, level_id: MembershipLevel) => {
getByLevelLoyaltyLevelCounter.add(1, {
query: JSON.stringify({ lang: ctx.lang, level_id }),
})
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
GetLoyaltyLevel,
{ lang: ctx.lang, level_id },
{
next: {
tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)],
},
cache: "force-cache",
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
GetLoyaltyLevel,
{ lang: ctx.lang, level_id },
{
next: {
tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)],
},
cache: "force-cache",
}
)
if (
!loyaltyLevelsConfigResponse.data ||
!loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length
) {
getByLevelLoyaltyLevelFailCounter.add(1)
const notFoundError = notFound(loyaltyLevelsConfigResponse)
console.error(
"contentstack.loyaltyLevel not found error",
JSON.stringify({
query: { lang: ctx.lang, level_id },
error: { code: notFoundError.code },
})
)
throw notFoundError
}
)
if (
!loyaltyLevelsConfigResponse.data ||
!loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length
) {
getByLevelLoyaltyLevelFailCounter.add(1)
const notFoundError = notFound(loyaltyLevelsConfigResponse)
console.error(
"contentstack.loyaltyLevel not found error",
JSON.stringify({
query: { lang: ctx.lang, level_id },
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse(
loyaltyLevelsConfigResponse.data
)
if (!validatedLoyaltyLevels.success) {
getByLevelLoyaltyLevelFailCounter.add(1)
console.error(validatedLoyaltyLevels.error)
console.error(
"contentstack.loyaltyLevel validation error",
JSON.stringify({
query: { lang: ctx.lang, level_id },
error: validatedLoyaltyLevels.error,
})
const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse(
loyaltyLevelsConfigResponse.data
)
return null
}
if (!validatedLoyaltyLevels.success) {
getByLevelLoyaltyLevelFailCounter.add(1)
console.error(validatedLoyaltyLevels.error)
console.error(
"contentstack.loyaltyLevel validation error",
JSON.stringify({
query: { lang: ctx.lang, level_id },
error: validatedLoyaltyLevels.error,
})
)
return null
}
getByLevelLoyaltyLevelSuccessCounter.add(1)
return validatedLoyaltyLevels.data[0]
}
getByLevelLoyaltyLevelSuccessCounter.add(1)
return validatedLoyaltyLevels.data[0]
}
)
export const loyaltyLevelQueryRouter = router({
byLevel: contentstackBaseProcedure

View File

@@ -1,11 +1,5 @@
import { z } from "zod"
export const getHotelInputSchema = z.object({
include: z
.array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"]))
.optional(),
})
export const getHotelsAvailabilityInputSchema = z.object({
cityId: z.string(),
roomStayStartDate: z.string(),
@@ -54,18 +48,17 @@ export const getRatesInputSchema = z.object({
hotelId: z.string(),
})
export enum HotelIncludeEnum {
export const HotelIncludeEnum = z.enum([
"RoomCategories",
"NearbyHotels",
"Restaurants",
"City",
}
])
export const getHotelDataInputSchema = z.object({
hotelId: z.string(),
language: z.string(),
isCardOnlyPayment: z.boolean().optional(),
include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(),
})
export type HotelDataInput = z.input<typeof getHotelDataInputSchema>

View File

@@ -1,4 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { cache } from "react"
import { Lang } from "@/constants/languages"
import * as api from "@/lib/api"
@@ -34,7 +35,6 @@ import {
import {
getBreakfastPackageInputSchema,
getHotelDataInputSchema,
getHotelInputSchema,
getHotelsAvailabilityInputSchema,
getRatesInputSchema,
getRoomsAvailabilityInputSchema,
@@ -165,251 +165,249 @@ async function getContentstackData(lang: Lang, uid?: string | null) {
return hotelPageData.data.hotel_page
}
export async function getHotelData(
input: HotelDataInput,
serviceToken: string
) {
const { hotelId, language, include, isCardOnlyPayment } = input
export const getHotelData = cache(
async (input: HotelDataInput, serviceToken: string) => {
const { hotelId, language, isCardOnlyPayment } = input
const params: Record<string, string> = {
hotelId,
language,
}
if (include) {
params.include = include.join(",")
}
getHotelCounter.add(1, {
hotelId,
language,
include,
})
console.info(
"api.hotels.hotelData start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{
headers: {
Authorization: `Bearer ${serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getHotelFailCounter.add(1, {
const params: Record<string, string> = {
hotelId,
language,
}
params.include = HotelIncludeEnum.options.join(",")
getHotelCounter.add(1, {
hotelId,
language,
include,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelData error",
JSON.stringify({
query: { hotelId, params },
error: {
console.info(
"api.hotels.hotelData start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{
headers: {
Authorization: `Bearer ${serviceToken}`,
},
// needs to clear default option as only
// cache or next.revalidate is permitted
cache: undefined,
next: {
revalidate: 60 * 30, // 30 minutes
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getHotelFailCounter.add(1, {
hotelId,
language,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
}),
})
)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getHotelDataSchema.safeParse(apiJson)
if (!validateHotelData.success) {
getHotelFailCounter.add(1, {
hotelId,
language,
include,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.hotelData validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
throw badRequestError()
}
getHotelSuccessCounter.add(1, {
hotelId,
language,
include,
})
console.info(
"api.hotels.hotelData success",
JSON.stringify({
query: { hotelId, params: params },
})
)
if (isCardOnlyPayment) {
validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions =
[]
}
return validateHotelData.data
}
export const hotelQueryRouter = router({
get: contentStackUidWithServiceProcedure
.input(getHotelInputSchema)
.query(async ({ ctx, input }) => {
const { lang, uid } = ctx
const { include } = input
const contentstackData = await getContentstackData(lang, uid)
const hotelId = contentstackData?.hotel_page_id
if (!hotelId) {
throw notFound(`Hotel not found for uid: ${uid}`)
}
const apiLang = toApiLang(lang)
const params: Record<string, string> = {
hotelId,
language: apiLang,
}
if (include) {
params.include = include.join(",")
}
getHotelCounter.add(1, { hotelId, lang, include })
console.info(
"api.hotels.hotel start",
console.error(
"api.hotels.hotelData error",
JSON.stringify({
query: { hotelId, params },
})
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getHotelFailCounter.add(1, {
hotelId,
lang,
include,
error_type: "http_error",
error: JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotel error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const validatedHotelData = getHotelDataSchema.safeParse(apiJson)
if (!validatedHotelData.success) {
getHotelFailCounter.add(1, {
hotelId,
lang,
include,
error_type: "validation_error",
error: JSON.stringify(validatedHotelData.error),
})
console.error(
"api.hotels.hotel validation error",
JSON.stringify({
query: { hotelId, params },
error: validatedHotelData.error,
})
)
throw badRequestError()
}
const included = validatedHotelData.data.included || []
const hotelAttributes = validatedHotelData.data.data.attributes
const images = hotelAttributes.gallery?.smallerImages
const hotelAlerts = hotelAttributes.meta?.specialAlerts || []
const roomCategories = included
? included.filter((item) => item.type === "roomcategories")
: []
const activities = contentstackData?.content
? contentstackData?.content[0]
: null
const facilities: Facility[] = [
{
...apiJson.data.attributes.restaurantImages,
id: FacilityCardTypeEnum.restaurant,
},
{
...apiJson.data.attributes.conferencesAndMeetings,
id: FacilityCardTypeEnum.conference,
},
{
...apiJson.data.attributes.healthAndWellness,
id: FacilityCardTypeEnum.wellness,
},
]
getHotelSuccessCounter.add(1, { hotelId, lang, include })
console.info(
"api.hotels.hotel success",
JSON.stringify({
query: { hotelId, params: params },
},
})
)
return {
hotelName: hotelAttributes.name,
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
hotelLocation: hotelAttributes.location,
hotelAddress: hotelAttributes.address,
hotelRatings: hotelAttributes.ratings,
hotelDetailedFacilities: hotelAttributes.detailedFacilities,
hotelImages: images,
pointsOfInterest: hotelAttributes.pointsOfInterest,
roomCategories,
activitiesCard: activities?.upcoming_activities_card,
facilities,
alerts: hotelAlerts,
faq: contentstackData?.faq,
}
}),
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getHotelDataSchema.safeParse(apiJson)
if (!validateHotelData.success) {
getHotelFailCounter.add(1, {
hotelId,
language,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.hotelData validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
throw badRequestError()
}
getHotelSuccessCounter.add(1, {
hotelId,
language,
})
console.info(
"api.hotels.hotelData success",
JSON.stringify({
query: { hotelId, params: params },
})
)
if (isCardOnlyPayment) {
validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions =
[]
}
return validateHotelData.data
}
)
export const hotelQueryRouter = router({
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
const { lang, uid } = ctx
const contentstackData = await getContentstackData(lang, uid)
const hotelId = contentstackData?.hotel_page_id
if (!hotelId) {
throw notFound(`Hotel not found for uid: ${uid}`)
}
const apiLang = toApiLang(lang)
const params: Record<string, string> = {
hotelId,
language: apiLang,
}
params.include = HotelIncludeEnum.options.join(",")
getHotelCounter.add(1, { hotelId, lang })
console.info(
"api.hotels.hotel start",
JSON.stringify({
query: { hotelId, params },
})
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
// needs to clear default option as only
// cache or next.revalidate is permitted
cache: undefined,
next: {
revalidate: 60 * 30, // 30 minutes
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getHotelFailCounter.add(1, {
hotelId,
lang,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotel error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const validatedHotelData = getHotelDataSchema.safeParse(apiJson)
if (!validatedHotelData.success) {
getHotelFailCounter.add(1, {
hotelId,
lang,
error_type: "validation_error",
error: JSON.stringify(validatedHotelData.error),
})
console.error(
"api.hotels.hotel validation error",
JSON.stringify({
query: { hotelId, params },
error: validatedHotelData.error,
})
)
throw badRequestError()
}
const included = validatedHotelData.data.included || []
const hotelAttributes = validatedHotelData.data.data.attributes
const images = hotelAttributes.gallery?.smallerImages
const hotelAlerts = hotelAttributes.meta?.specialAlerts || []
const roomCategories = included
? included.filter((item) => item.type === "roomcategories")
: []
const activities = contentstackData?.content
? contentstackData?.content[0]
: null
const facilities: Facility[] = [
{
...apiJson.data.attributes.restaurantImages,
id: FacilityCardTypeEnum.restaurant,
},
{
...apiJson.data.attributes.conferencesAndMeetings,
id: FacilityCardTypeEnum.conference,
},
{
...apiJson.data.attributes.healthAndWellness,
id: FacilityCardTypeEnum.wellness,
},
]
getHotelSuccessCounter.add(1, { hotelId, lang })
console.info(
"api.hotels.hotel success",
JSON.stringify({
query: { hotelId, params: params },
})
)
return {
hotelName: hotelAttributes.name,
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
hotelLocation: hotelAttributes.location,
hotelAddress: hotelAttributes.address,
hotelRatings: hotelAttributes.ratings,
hotelDetailedFacilities: hotelAttributes.detailedFacilities,
hotelImages: images,
pointsOfInterest: hotelAttributes.pointsOfInterest,
roomCategories,
activitiesCard: activities?.upcoming_activities_card,
facilities,
alerts: hotelAlerts,
faq: contentstackData?.faq,
}
}),
availability: router({
hotels: serviceProcedure
.input(getHotelsAvailabilityInputSchema)
@@ -773,7 +771,6 @@ export const hotelQueryRouter = router({
{
hotelId,
language: ctx.lang,
include: [HotelIncludeEnum.RoomCategories],
},
ctx.serviceToken
)