Files
web/apps/scandic-web/server/routers/contentstack/metadata/query.ts
Joakim Jäderberg fa63b20ed0 Merged in feature/redis (pull request #1478)
Distributed cache

* cache deleteKey now uses an options object instead of a lonely argument variable fuzzy

* merge

* remove debug logs and cleanup

* cleanup

* add fault handling

* add fault handling

* add pid when logging redis client creation

* add identifier when logging redis client creation

* cleanup

* feat: add redis-api as it's own app

* feature: use http wrapper for redis

* feat: add the possibility to fallback to unstable_cache

* Add error handling if redis cache is unresponsive

* add logging for unstable_cache

* merge

* don't cache errors

* fix: metadatabase on branchdeploys

* Handle when /en/destinations throws
add ErrorBoundary

* Add sentry-logging when ErrorBoundary catches exception

* Fix error handling for distributed cache

* cleanup code

* Added Application Insights back

* Update generateApiKeys script and remove duplicate

* Merge branch 'feature/redis' of bitbucket.org:scandic-swap/web into feature/redis

* merge


Approved-by: Linus Flood
2025-03-14 07:54:21 +00:00

277 lines
9.2 KiB
TypeScript

import { metrics } from "@opentelemetry/api"
import { cache } from "react"
import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql"
import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql"
import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql"
import { GetDestinationCityPageMetadata } from "@/lib/graphql/Query/DestinationCityPage/Metadata.graphql"
import { GetDestinationCountryPageMetadata } from "@/lib/graphql/Query/DestinationCountryPage/Metadata.graphql"
import { GetDestinationOverviewPageMetadata } from "@/lib/graphql/Query/DestinationOverviewPage/Metadata.graphql"
import { GetHotelPageMetadata } from "@/lib/graphql/Query/HotelPage/Metadata.graphql"
import { GetLoyaltyPageMetadata } from "@/lib/graphql/Query/LoyaltyPage/Metadata.graphql"
import { GetStartPageMetadata } from "@/lib/graphql/Query/StartPage/Metadata.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
import { getHotel } from "../../hotels/query"
import { getUrlsOfAllLanguages } from "../languageSwitcher/utils"
import { getMetadataInput } from "./input"
import { getNonContentstackUrls, metadataSchema } from "./output"
import { affix, getCityData, getCountryData } from "./utils"
import type { Metadata } from "next"
import { PageContentTypeEnum } from "@/types/requests/contentType"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
import type { Lang } from "@/constants/languages"
const meter = metrics.getMeter("trpc.metadata")
// OpenTelemetry metrics
const fetchMetadataCounter = meter.createCounter(
"trpc.contentstack.metadata.get"
)
const fetchMetadataSuccessCounter = meter.createCounter(
"trpc.contentstack.metadata.get-success"
)
const fetchMetadataFailCounter = meter.createCounter(
"trpc.contentstack.metadata.get-fail"
)
const transformMetadataCounter = meter.createCounter(
"trpc.contentstack.metadata.transform"
)
const transformMetadataSuccessCounter = meter.createCounter(
"trpc.contentstack.metadata.transform-success"
)
const transformMetadataFailCounter = meter.createCounter(
"trpc.contentstack.metadata.transform-fail"
)
const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
query: string,
{ uid, lang }: { uid: string; lang: Lang }
) {
fetchMetadataCounter.add(1, { lang, uid })
console.info(
"contentstack.metadata fetch start",
JSON.stringify({ query: { lang, uid } })
)
const response = await request<T>(
query,
{ locale: lang, uid },
{
key: generateTag(lang, uid, affix),
ttl: "max",
}
)
if (!response.data) {
const notFoundError = notFound(response)
fetchMetadataFailCounter.add(1, {
lang,
uid,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.metadata fetch not found error",
JSON.stringify({
query: { lang, uid },
error: { code: notFoundError.code },
})
)
throw notFoundError
}
fetchMetadataSuccessCounter.add(1, { lang, uid })
console.info(
"contentstack.metadata fetch success",
JSON.stringify({ query: { lang, uid } })
)
return response.data
})
async function getTransformedMetadata(
data: unknown,
alternates: Metadata["alternates"]
) {
transformMetadataCounter.add(1)
console.info("contentstack.metadata transform start")
const validatedMetadata = await metadataSchema.safeParseAsync(data)
if (!validatedMetadata.success) {
transformMetadataFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(validatedMetadata.error),
})
console.error(
"contentstack.metadata validation error",
JSON.stringify({
error: validatedMetadata.error,
})
)
return null
}
transformMetadataSuccessCounter.add(1)
console.info("contentstack.metadata transform success")
if (alternates) {
validatedMetadata.data.alternates = alternates
}
return validatedMetadata.data
}
export const metadataQueryRouter = router({
get: contentStackUidWithServiceProcedure
.input(getMetadataInput)
.query(async ({ ctx, input }) => {
const variables = {
lang: ctx.lang,
uid: ctx.uid,
}
let urls: LanguageSwitcherData | null = null
if (
input.subpage ||
input.filterFromUrl ||
!ctx.uid ||
!ctx.contentType
) {
urls = getNonContentstackUrls(ctx.lang, `${ctx.lang}/${ctx.pathname}`)
} else {
urls = await getUrlsOfAllLanguages(ctx.lang, ctx.uid, ctx.contentType)
}
let alternates: Metadata["alternates"] = null
if (urls) {
const languages: Record<string, string> = {}
Object.entries(urls)
.filter(([lang]) => lang !== ctx.lang)
.forEach(([lang, { url }]) => {
languages[lang] = url
})
const canonical = urls[ctx.lang]?.url
alternates = {
canonical,
languages,
}
}
switch (ctx.contentType) {
case PageContentTypeEnum.accountPage:
const accountPageResponse = await fetchMetadata<{
account_page: RawMetadataSchema
}>(GetAccountPageMetadata, variables)
return getTransformedMetadata(
accountPageResponse.account_page,
alternates
)
case PageContentTypeEnum.collectionPage:
const collectionPageResponse = await fetchMetadata<{
collection_page: RawMetadataSchema
}>(GetCollectionPageMetadata, variables)
return getTransformedMetadata(
collectionPageResponse.collection_page,
alternates
)
case PageContentTypeEnum.contentPage:
const contentPageResponse = await fetchMetadata<{
content_page: RawMetadataSchema
}>(GetContentPageMetadata, variables)
return getTransformedMetadata(
contentPageResponse.content_page,
alternates
)
case PageContentTypeEnum.destinationOverviewPage:
const destinationOverviewPageResponse = await fetchMetadata<{
destination_overview_page: RawMetadataSchema
}>(GetDestinationOverviewPageMetadata, variables)
return getTransformedMetadata(
destinationOverviewPageResponse.destination_overview_page,
alternates
)
case PageContentTypeEnum.destinationCountryPage:
const destinationCountryPageResponse = await fetchMetadata<{
destination_country_page: RawMetadataSchema
}>(GetDestinationCountryPageMetadata, variables)
const countryData = await getCountryData(
destinationCountryPageResponse.destination_country_page,
input,
ctx.serviceToken,
ctx.lang
)
return getTransformedMetadata(
{
...destinationCountryPageResponse.destination_country_page,
...countryData,
},
alternates
)
case PageContentTypeEnum.destinationCityPage:
const destinationCityPageResponse = await fetchMetadata<{
destination_city_page: RawMetadataSchema
}>(GetDestinationCityPageMetadata, variables)
const cityData = await getCityData(
destinationCityPageResponse.destination_city_page,
input,
ctx.serviceToken,
ctx.lang
)
return getTransformedMetadata(
{
...destinationCityPageResponse.destination_city_page,
...cityData,
},
alternates
)
case PageContentTypeEnum.loyaltyPage:
const loyaltyPageResponse = await fetchMetadata<{
loyalty_page: RawMetadataSchema
}>(GetLoyaltyPageMetadata, variables)
return getTransformedMetadata(
loyaltyPageResponse.loyalty_page,
alternates
)
case PageContentTypeEnum.hotelPage:
const hotelPageResponse = await fetchMetadata<{
hotel_page: RawMetadataSchema
}>(GetHotelPageMetadata, variables)
const hotelPageData = hotelPageResponse.hotel_page
const hotelData = hotelPageData.hotel_page_id
? await getHotel(
{
hotelId: hotelPageData.hotel_page_id,
isCardOnlyPayment: false,
language: ctx.lang,
},
ctx.serviceToken
)
: null
return getTransformedMetadata(
{
...hotelPageData,
hotelData: hotelData?.hotel,
},
alternates
)
case PageContentTypeEnum.startPage:
const startPageResponse = await fetchMetadata<{
start_page: RawMetadataSchema
}>(GetStartPageMetadata, variables)
return getTransformedMetadata(
startPageResponse.start_page,
alternates
)
default:
return null
}
}),
})