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

@@ -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
)