From 99537b13e811acaff1b0545d1856075edb4d6442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= Date: Mon, 12 Jan 2026 09:01:44 +0000 Subject: [PATCH] Merged in chore/add-error-details-for-sentry (pull request #3378) Include more details when throwing errors for debugging in Sentry * WIP throw errors with more details for debugging in Sentry * Fix throwing response-data * Clearer message when a response fails * Add message to errors * better typings * . * Try to send profileID and membershipNumber to Sentry when we fail to parse the apiResponse * rename notFound -> notFoundError * Merge branch 'master' of bitbucket.org:scandic-swap/web into chore/add-error-details-for-sentry Approved-by: Linus Flood --- .../destinations/[country]/[city]/route.ts | 8 +- .../api/web/destinations/[country]/route.ts | 8 +- apps/scandic-web/instrumentation.ts | 17 +- packages/trpc/lib/errors.ts | 201 +++++++++++++++--- packages/trpc/lib/procedures.ts | 8 +- .../mutation/validatePartnerPayment.ts | 8 +- packages/trpc/lib/routers/booking/query.ts | 30 ++- packages/trpc/lib/routers/booking/utils.ts | 18 +- .../contentstack/UsePointsModal/query.ts | 20 +- .../routers/contentstack/accountPage/query.ts | 26 +-- .../lib/routers/contentstack/base/query.ts | 136 ++++++------ .../routers/contentstack/breadcrumbs/query.ts | 22 +- .../campaignOverviewPage/query.ts | 25 ++- .../contentstack/campaignPage/query.ts | 23 +- .../contentstack/campaignPage/utils.ts | 19 +- .../contentstack/collectionPage/utils.ts | 18 +- .../routers/contentstack/contentPage/utils.ts | 13 +- .../contentstack/destinationCityPage/query.ts | 23 +- .../destinationCountryPage/query.ts | 22 +- .../destinationOverviewPage/query.ts | 25 ++- .../routers/contentstack/hotelPage/query.ts | 15 +- .../contentstack/loyaltyLevel/query.ts | 15 +- .../routers/contentstack/loyaltyPage/query.ts | 14 +- .../routers/contentstack/metadata/query.ts | 9 +- .../lib/routers/contentstack/partner/query.ts | 9 +- .../contentstack/profilingConsent/query.ts | 8 +- .../contentstack/promoCampaignPage/query.ts | 14 +- .../lib/routers/contentstack/reward/query.ts | 4 +- .../lib/routers/contentstack/reward/utils.ts | 16 +- .../routers/contentstack/startPage/query.ts | 14 +- .../services/getHotelsAvailabilityByCity.ts | 14 +- .../services/getLocationsByCountries.test.ts | 32 +-- .../services/getLocationsByCountries.ts | 8 +- packages/trpc/lib/routers/user/query/index.ts | 6 +- .../lib/routers/user/utils/getBasicUser.ts | 34 ++- .../lib/routers/user/utils/getVerifiedUser.ts | 31 ++- packages/trpc/lib/utils/getResponseBody.ts | 21 ++ 37 files changed, 641 insertions(+), 293 deletions(-) create mode 100644 packages/trpc/lib/utils/getResponseBody.ts diff --git a/apps/scandic-web/app/api/web/destinations/[country]/[city]/route.ts b/apps/scandic-web/app/api/web/destinations/[country]/[city]/route.ts index 2e01cd98e..392e9b2ea 100644 --- a/apps/scandic-web/app/api/web/destinations/[country]/[city]/route.ts +++ b/apps/scandic-web/app/api/web/destinations/[country]/[city]/route.ts @@ -9,7 +9,7 @@ import { equalsIgnoreCaseAndAccents } from "@scandic-hotels/common/utils/stringE import { gatewayTimeout, httpStatusByErrorCode, - notFound, + notFoundError, } from "@scandic-hotels/trpc/errors" import { isCityLocation, @@ -36,7 +36,7 @@ export async function GET( ) if (!country) { - throw notFound(`Country "${countryParam.toLowerCase()}" not found`) + throw notFoundError(`Country "${countryParam.toLowerCase()}" not found`) } const caller = await serverClient() @@ -55,7 +55,7 @@ export async function GET( }) if (!city) { - throw notFound( + throw notFoundError( `City "${cityParam.toLowerCase()}" not found in country "${countryParam.toLowerCase()}"` ) } @@ -71,7 +71,7 @@ export async function GET( ) if (hotels.length === 0) { - throw notFound( + throw notFoundError( `No hotels found in city "${cityParam.toLowerCase()}" and country "${countryParam.toLowerCase()}"` ) } diff --git a/apps/scandic-web/app/api/web/destinations/[country]/route.ts b/apps/scandic-web/app/api/web/destinations/[country]/route.ts index 8a0d49400..f95053792 100644 --- a/apps/scandic-web/app/api/web/destinations/[country]/route.ts +++ b/apps/scandic-web/app/api/web/destinations/[country]/route.ts @@ -9,7 +9,7 @@ import { equalsIgnoreCaseAndAccents } from "@scandic-hotels/common/utils/stringE import { gatewayTimeout, httpStatusByErrorCode, - notFound, + notFoundError, } from "@scandic-hotels/trpc/errors" import { isCityLocation, @@ -36,7 +36,7 @@ export async function GET( ) if (!country) { - throw notFound(`Country "${countryParam.toLowerCase()}" not found`) + throw notFoundError(`Country "${countryParam.toLowerCase()}" not found`) } const caller = await serverClient() @@ -52,7 +52,7 @@ export async function GET( }) if (cities.length === 0) { - throw notFound( + throw notFoundError( `No cities found in country "${countryParam.toLowerCase()}"` ) } @@ -71,7 +71,7 @@ export async function GET( ) if (hotels.length === 0) { - throw notFound( + throw notFoundError( `No hotels found in country "${countryParam.toLowerCase()}"` ) } diff --git a/apps/scandic-web/instrumentation.ts b/apps/scandic-web/instrumentation.ts index 5506ed116..f8dba5a8d 100644 --- a/apps/scandic-web/instrumentation.ts +++ b/apps/scandic-web/instrumentation.ts @@ -1,8 +1,11 @@ import * as Sentry from "@sentry/nextjs" -import { TRPCError } from "@trpc/server" + +import { isCustomCause } from "@scandic-hotels/trpc/errors" import { env } from "./env/server" +import type { TRPCError } from "@trpc/server" + export const denyUrls: (string | RegExp)[] = [ // Ignore preview urls /\/.{2}\/preview\//, @@ -26,16 +29,24 @@ async function configureSentry() { release: env.RELEASE_TAG || undefined, beforeSend(event, hint) { const error = hint.originalException - // Don't send TRPCErrors with client error codes - if (error instanceof TRPCError) { + if (isTRPCError(error)) { const clientErrorCodes = ["CONFLICT", "NOT_FOUND", "UNAUTHORIZED"] if (clientErrorCodes.includes(error.code)) { return null // Don't send to Sentry } + + if (isCustomCause(error.cause)) { + event.contexts = event.contexts || {} + event.contexts.errorDetails = { data: error.cause.errorDetails } + } } return event }, }) } + +function isTRPCError(error: unknown): error is TRPCError { + return error instanceof Error && error.name === "TRPCError" +} diff --git a/packages/trpc/lib/errors.ts b/packages/trpc/lib/errors.ts index 5a3b71688..fc1acf724 100644 --- a/packages/trpc/lib/errors.ts +++ b/packages/trpc/lib/errors.ts @@ -1,66 +1,88 @@ import { TRPCError } from "@trpc/server" -export function gatewayTimeout(cause?: unknown) { +import { getResponseBody } from "./utils/getResponseBody" + +type CustomCause = { + message: string + errorDetails: Record +} + +type ResponseLike = { + status: number + statusText: string + body: string | Record + url?: string +} + +type TRPCCause = ResponseLike | CustomCause | Error | string + +export function isCustomCause(cause: unknown): cause is CustomCause { + return ( + !!cause && + typeof cause === "object" && + "message" in cause && + "errorDetails" in cause && + (cause as CustomCause).errorDetails !== undefined + ) +} + +export function isTRPCError(error: unknown): error is TRPCError { + return error instanceof Error && error.name === "TRPCError" +} + +export function gatewayTimeout(cause?: TRPCCause, message: string = "") { return new TRPCError({ code: "GATEWAY_TIMEOUT", - message: `Gateway Timeout`, - cause, + cause: harmonizeCause(cause, message), }) } -export function unauthorizedError(cause?: unknown) { +export function unauthorizedError(cause?: TRPCCause, message: string = "") { return new TRPCError({ code: "UNAUTHORIZED", - message: `Unauthorized`, - cause, + cause: harmonizeCause(cause, message), }) } -export function forbiddenError(cause?: unknown) { +export function forbiddenError(cause?: TRPCCause, message: string = "") { return new TRPCError({ code: "FORBIDDEN", - message: `Forbidden`, - cause, + cause: harmonizeCause(cause, message), }) } -export function conflictError(cause?: unknown) { +export function conflictError(cause?: TRPCCause, message: string = "") { return new TRPCError({ code: "CONFLICT", - message: `Conflict`, - cause, + cause: harmonizeCause(cause, message), }) } -export function badRequestError(cause?: unknown) { +export function badRequestError(cause?: TRPCCause, message: string = "") { return new TRPCError({ code: "BAD_REQUEST", - message: `Bad request`, - cause, + cause: harmonizeCause(cause, message), }) } -export function notFound(cause?: unknown) { +export function notFoundError(cause?: TRPCCause, message: string = "") { return new TRPCError({ code: "NOT_FOUND", - message: `Not found`, - cause, + cause: harmonizeCause(cause, message), }) } -export function unprocessableContent(cause?: unknown) { +export function unprocessableContent(cause?: TRPCCause, message: string = "") { return new TRPCError({ code: "UNPROCESSABLE_CONTENT", - message: "Unprocessable content", - cause, + cause: harmonizeCause(cause, message), }) } -export function internalServerError(cause?: unknown) { +export function internalServerError(cause?: TRPCCause, message: string = "") { return new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: `Internal Server Error`, - cause, + cause: harmonizeCause(cause, message), }) } @@ -106,20 +128,137 @@ export function httpStatusByErrorCode(error: TRPCError) { } } -export function serverErrorByStatus(status: number, cause?: unknown) { +function errorCodeByHttpStatus(status: number): TRPCError["code"] { switch (status) { + case 400: + return "BAD_REQUEST" case 401: - return unauthorizedError(cause) + return "UNAUTHORIZED" case 403: - return forbiddenError(cause) + return "FORBIDDEN" case 404: - return notFound(cause) + return "NOT_FOUND" case 409: - return conflictError(cause) + return "CONFLICT" case 422: - return unprocessableContent(cause) + return "UNPROCESSABLE_CONTENT" + case 504: + return "GATEWAY_TIMEOUT" case 500: default: - return internalServerError(cause) + return "INTERNAL_SERVER_ERROR" } } + +export function serverErrorByStatus( + status: number, + cause: CustomCause | Error | string +): TRPCError +export function serverErrorByStatus( + status: number, + cause: ResponseLike, + message: string +): TRPCError +export function serverErrorByStatus( + status: number, + cause?: TRPCCause, + message?: string +) { + switch (status) { + case 401: + return unauthorizedError(cause, message) + case 403: + return forbiddenError(cause, message) + case 404: + return notFoundError(cause, message) + case 409: + return conflictError(cause, message) + case 422: + return unprocessableContent(cause, message) + case 500: + return internalServerError(cause, message) + case 504: + return gatewayTimeout(cause, message) + default: + return internalServerError(cause, message) + } +} + +function harmonizeCause( + cause: TRPCCause | undefined, + message: string = "" +): CustomCause | Error | undefined { + if (!cause) { + return undefined + } + + if (isResponseLike(cause)) { + return { + message: message || `HTTP Error ${cause.status}: ${cause.statusText}`, + errorDetails: { + status: cause.status, + statusText: cause.statusText || errorCodeByHttpStatus(cause.status), + body: truncate(cause.body, 200), // Avoids issues in Sentry with large bodies + url: cause.url, + }, + } satisfies CustomCause + } + + if (typeof cause === "string") { + return { message: cause, errorDetails: {} } satisfies CustomCause + } + + return cause +} + +export async function extractResponseDetails( + response: Response +): Promise { + const body = await getResponseBody(response) + + return { + status: response.status, + statusText: response.statusText, + body, + url: response.url, + } +} + +function isResponseLike(cause: TRPCCause): cause is ResponseLike { + if (typeof cause !== "object" || !cause) { + return false + } + + if (!("status" in cause) || typeof cause.status !== "number") { + return false + } + + if (!("statusText" in cause) || typeof cause.statusText !== "string") { + return false + } + + if (!("body" in cause)) { + return false + } + + return true +} + +function truncate( + str: string | Record, + maxLength: number +): string { + if (typeof str !== "string") { + str = JSON.stringify(str) + } + + if (str.length <= maxLength) { + return str + } + const originalLength = str.length + + return ( + str.slice(0, maxLength) + + `... [truncated, original length: ${originalLength}]` + ) +} diff --git a/packages/trpc/lib/procedures.ts b/packages/trpc/lib/procedures.ts index f88367760..17764d7d7 100644 --- a/packages/trpc/lib/procedures.ts +++ b/packages/trpc/lib/procedures.ts @@ -31,9 +31,11 @@ export const languageProcedure = baseProcedure.use(async function (opts) { if (process.env.NODE_ENV === "development" && !parsedInput.success) { throw badRequestError({ message: "Missing lang in tRPC context", - path: opts.path, - type: opts.type, - input: opts.input, + errorDetails: { + path: opts.path, + type: opts.type, + input: opts.input, + }, }) } diff --git a/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts b/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts index 35bd3b723..6e2824efc 100644 --- a/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts +++ b/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts @@ -5,7 +5,7 @@ import z from "zod" import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../api" -import { serverErrorByStatus } from "../../../errors" +import { extractResponseDetails, serverErrorByStatus } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" import { toApiLang } from "../../../utils" @@ -51,7 +51,11 @@ export const validatePartnerPayment = safeProtectedServiceProcedure return null } - throw serverErrorByStatus(apiResponse.status, apiResponse) + throw serverErrorByStatus( + apiResponse.status, + await extractResponseDetails(apiResponse), + "validatePartnerPayment failed" + ) } metricsValidateBooking.success() diff --git a/packages/trpc/lib/routers/booking/query.ts b/packages/trpc/lib/routers/booking/query.ts index 6399511bd..1628c4370 100644 --- a/packages/trpc/lib/routers/booking/query.ts +++ b/packages/trpc/lib/routers/booking/query.ts @@ -2,7 +2,12 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../.." import * as api from "../../api" -import { badRequestError, serverErrorByStatus } from "../../errors" +import { + badRequestError, + extractResponseDetails, + notFoundError, + serverErrorByStatus, +} from "../../errors" import { createRefIdPlugin } from "../../plugins/refIdToConfirmationNumber" import { safeProtectedServiceProcedure, @@ -85,8 +90,10 @@ export const bookingQueryRouter = router({ hotelId: booking.hotelId, } ) - - throw serverErrorByStatus(404) + throw notFoundError({ + message: "Hotel data not found", + errorDetails: { hotelId: booking.hotelId }, + }) } metricsGetBooking.success() @@ -163,7 +170,10 @@ export const bookingQueryRouter = router({ } ) - throw serverErrorByStatus(404) + throw notFoundError({ + message: "Hotel data not found", + errorDetails: { hotelId: booking.hotelId }, + }) } metricsFindBooking.success() @@ -265,14 +275,22 @@ export const bookingQueryRouter = router({ if (!apiResponse.ok) { await metricsGetBookingStatus.httpError(apiResponse) - throw serverErrorByStatus(apiResponse.status, apiResponse) + throw serverErrorByStatus( + apiResponse.status, + await extractResponseDetails(apiResponse), + "getBookingStatus failed" + ) } const apiJson = await apiResponse.json() const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { metricsGetBookingStatus.validationError(verifiedData.error) - throw badRequestError() + + throw badRequestError({ + message: "Invalid booking data", + errorDetails: verifiedData.error.formErrors, + }) } metricsGetBookingStatus.success() diff --git a/packages/trpc/lib/routers/booking/utils.ts b/packages/trpc/lib/routers/booking/utils.ts index 954e47445..11b922652 100644 --- a/packages/trpc/lib/routers/booking/utils.ts +++ b/packages/trpc/lib/routers/booking/utils.ts @@ -1,7 +1,11 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../api" -import { badRequestError, serverErrorByStatus } from "../../errors" +import { + badRequestError, + extractResponseDetails, + serverErrorByStatus, +} from "../../errors" import { toApiLang } from "../../utils" import { createBookingSchema } from "./mutation/create/schema" import { bookingConfirmationSchema } from "./output" @@ -37,7 +41,11 @@ export async function getBooking( return null } - throw serverErrorByStatus(apiResponse.status, apiResponse) + throw serverErrorByStatus( + apiResponse.status, + await extractResponseDetails(apiResponse), + "getBooking failed" + ) } const apiJson = await apiResponse.json() @@ -94,7 +102,11 @@ export async function findBooking( return null } - throw serverErrorByStatus(apiResponse.status, apiResponse) + throw serverErrorByStatus( + apiResponse.status, + await extractResponseDetails(apiResponse), + "findBooking failed" + ) } const apiJson = await apiResponse.json() diff --git a/packages/trpc/lib/routers/contentstack/UsePointsModal/query.ts b/packages/trpc/lib/routers/contentstack/UsePointsModal/query.ts index e98124332..e3244235f 100644 --- a/packages/trpc/lib/routers/contentstack/UsePointsModal/query.ts +++ b/packages/trpc/lib/routers/contentstack/UsePointsModal/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetUsePointsModal, GetUsePointsModalRefs, @@ -34,18 +34,22 @@ export const usePointsModalQueryRouter = router({ metricsRefs.start() const refsTag = generateRefsResponseTag(lang, "usepointsmodal") + + const variables = { locale: lang } const refsResponse = await request( GetUsePointsModalRefs, - { locale: lang }, + variables, { key: refsTag, ttl: "max", } ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetUsePointsModalRefs returned no data", + errorDetails: variables, + }) } const validatedRefsData = usePointsModalRefsSchema.safeParse( @@ -76,7 +80,7 @@ export const usePointsModalQueryRouter = router({ const response = await request( GetUsePointsModal, - { locale: lang }, + variables, { key: tags, ttl: "max", @@ -84,9 +88,11 @@ export const usePointsModalQueryRouter = router({ ) if (!response.data) { - const notFoundError = notFound(response) metrics.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetUsePointsModal returned no data", + errorDetails: variables, + }) } const validatedResponse = usePointsModalSchema.safeParse(response.data) diff --git a/packages/trpc/lib/routers/contentstack/accountPage/query.ts b/packages/trpc/lib/routers/contentstack/accountPage/query.ts index ccf16fbe8..55f7b8f33 100644 --- a/packages/trpc/lib/routers/contentstack/accountPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/accountPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetAccountPage, GetAccountPageRefs, @@ -32,12 +32,10 @@ export const accountPageQueryRouter = router({ const metricsRefs = getAccountPageRefsCounter.init({ lang, uid }) metricsRefs.start() + const variables = { locale: lang, uid } const refsResponse = await request( GetAccountPageRefs, - { - locale: lang, - uid, - }, + variables, { key: generateRefsResponseTag(lang, uid), ttl: "max", @@ -45,9 +43,11 @@ export const accountPageQueryRouter = router({ ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetAccountPageRefs returned no data", + errorDetails: variables, + }) } const validatedAccountPageRefs = accountPageRefsSchema.safeParse( @@ -75,10 +75,7 @@ export const accountPageQueryRouter = router({ const response = await request( GetAccountPage, - { - locale: lang, - uid, - }, + variables, { key: tags, ttl: "max", @@ -86,9 +83,12 @@ export const accountPageQueryRouter = router({ ) if (!response.data) { - const notFoundError = notFound(response) metrics.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetAccountPage returned no data", + errorDetails: variables, + }) } const validatedAccountPage = accountPageSchema.safeParse(response.data) diff --git a/packages/trpc/lib/routers/contentstack/base/query.ts b/packages/trpc/lib/routers/contentstack/base/query.ts index 9753c6c0c..5726485c9 100644 --- a/packages/trpc/lib/routers/contentstack/base/query.ts +++ b/packages/trpc/lib/routers/contentstack/base/query.ts @@ -3,7 +3,7 @@ import { cache } from "react" import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetContactConfig } from "../../../graphql/Query/ContactConfig.graphql" import { GetFooter, GetFooterRef } from "../../../graphql/Query/Footer.graphql" import { GetHeader, GetHeaderRef } from "../../../graphql/Query/Header.graphql" @@ -65,12 +65,12 @@ const getContactConfig = cache(async (lang: Lang) => { const metricsGetContactConfig = getContactConfigCounter.init({ lang }) metricsGetContactConfig.start() - + const variables = { + locale: lang, + } const response = await request( GetContactConfig, - { - locale: lang, - }, + variables, { key: `${lang}:contact`, ttl: "max", @@ -78,9 +78,11 @@ const getContactConfig = cache(async (lang: Lang) => { ) if (!response.data) { - const notFoundError = notFound(response) metricsGetContactConfig.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetContactConfig returned no data", + errorDetails: variables, + }) } const verifiedData = validateContactConfigSchema.safeParse(response.data) @@ -109,21 +111,18 @@ export const baseQueryRouter = router({ metricsGetHeaderRefs.start() - const responseRef = await request( - GetHeaderRef, - { - locale: lang, - }, - { - key: generateRefsResponseTag(lang, "header"), - ttl: "max", - } - ) + const variables = { locale: lang } + const responseRef = await request(GetHeaderRef, variables, { + key: generateRefsResponseTag(lang, "header"), + ttl: "max", + }) if (!responseRef.data) { - const notFoundError = notFound(responseRef) metricsGetHeaderRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetHeaderRef returned no data", + errorDetails: variables, + }) } const validatedHeaderRefs = headerRefsSchema.safeParse(responseRef.data) @@ -147,16 +146,17 @@ export const baseQueryRouter = router({ generateTag(lang, validatedHeaderRefs.data.header.system.uid), ].flat() - const response = await request( - GetHeader, - { locale: lang }, - { key: tags, ttl: "max" } - ) + const response = await request(GetHeader, variables, { + key: tags, + ttl: "max", + }) if (!response.data) { - const notFoundError = notFound(response) metricsGetHeader.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetHeader returned no data", + errorDetails: variables, + }) } const validatedHeaderConfig = headerSchema.safeParse(response.data) @@ -182,11 +182,10 @@ export const baseQueryRouter = router({ metricsGetFooterRefs.start() + const variables = { locale: lang } const responseRef = await request( GetFooterRef, - { - locale: lang, - }, + variables, { key: generateRefsResponseTag(lang, "footer"), ttl: "max", @@ -194,9 +193,11 @@ export const baseQueryRouter = router({ ) if (!responseRef.data) { - const notFoundError = notFound(responseRef) metricsGetFooterRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetFooterRef returned no data", + errorDetails: variables, + }) } const validatedFooterRefs = validateFooterRefConfigSchema.safeParse( @@ -223,21 +224,17 @@ export const baseQueryRouter = router({ generateTag(lang, footerUID), ].flat() - const response = await request( - GetFooter, - { - locale: lang, - }, - { - key: tags, - ttl: "max", - } - ) + const response = await request(GetFooter, variables, { + key: tags, + ttl: "max", + }) if (!response.data) { - const notFoundError = notFound(response) metricsGetFooter.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetFooter returned no data", + errorDetails: variables, + }) } const validatedFooterConfig = validateFooterConfigSchema.safeParse( @@ -269,9 +266,10 @@ export const baseQueryRouter = router({ metricsGetSitewideCampaignBannerRefs.start() + const refVariables = { locale: lang } const responseRef = await request( GetSitewideCampaignBannerRef, - { locale: lang }, + refVariables, { key: generateRefsResponseTag(lang, "sitewide_campaign_banner"), ttl: "max", @@ -279,9 +277,11 @@ export const baseQueryRouter = router({ ) if (!responseRef.data) { - const notFoundError = notFound(responseRef) metricsGetSitewideCampaignBannerRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetSitewideCampaignBannerRef returned no data", + errorDetails: refVariables, + }) } const validatedSitewideCampaignBannerRef = @@ -312,17 +312,21 @@ export const baseQueryRouter = router({ metricsGetSitewideCampaignBanner.start() + const variables = { locale: lang } const sitewideCampaignBannerResponse = await request( GetSitewideCampaignBanner, - { locale: lang }, + variables, { key: tags, ttl: "max" } ) if (!sitewideCampaignBannerResponse.data) { - const notFoundError = notFound(sitewideCampaignBannerResponse) metricsGetSitewideCampaignBanner.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetSitewideCampaignBanner returned no data", + errorDetails: variables, + }) } const validatedSitewideCampaignBanner = @@ -354,11 +358,12 @@ export const baseQueryRouter = router({ metricsGetSiteConfigRefs.start() + const refVariables = { + locale: lang, + } const responseRef = await request( GetSiteConfigRef, - { - locale: lang, - }, + refVariables, { key: generateRefsResponseTag(lang, "site_config"), ttl: "max", @@ -366,9 +371,11 @@ export const baseQueryRouter = router({ ) if (!responseRef.data) { - const notFoundError = notFound(responseRef) metricsGetSiteConfigRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "SiteConfigRefs returned no data", + errorDetails: refVariables, + }) } const validatedSiteConfigRef = siteConfigRefSchema.safeParse( @@ -397,24 +404,21 @@ export const baseQueryRouter = router({ metricsGetSiteConfig.start() + const variables = { locale: lang } const [siteConfigResponse, contactConfig] = await Promise.all([ - request( - GetSiteConfig, - { - locale: lang, - }, - { - key: tags, - ttl: "max", - } - ), + request(GetSiteConfig, variables, { + key: tags, + ttl: "max", + }), getContactConfig(lang), ]) if (!siteConfigResponse.data) { - const notFoundError = notFound(siteConfigResponse) metricsGetSiteConfig.noDataError() - throw notFoundError + throw notFoundError({ + message: "SiteConfig returned no data", + errorDetails: variables, + }) } const validatedSiteConfig = siteConfigSchema.safeParse( diff --git a/packages/trpc/lib/routers/contentstack/breadcrumbs/query.ts b/packages/trpc/lib/routers/contentstack/breadcrumbs/query.ts index 92aff04e1..745c3086b 100644 --- a/packages/trpc/lib/routers/contentstack/breadcrumbs/query.ts +++ b/packages/trpc/lib/routers/contentstack/breadcrumbs/query.ts @@ -5,7 +5,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." import { PageContentTypeEnum } from "../../../enums/contentType" -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetMyPagesBreadcrumbs, GetMyPagesBreadcrumbsRefs, @@ -115,19 +115,19 @@ const getBreadcrumbs = cache(async function fetchMemoizedBreadcrumbs( metricsGetBreadcrumbs.start() - const response = await request( - query, - { locale: lang, uid }, - { - key: tags, - ttl: "max", - } - ) + const variables = { locale: lang, uid } + const response = await request(query, variables, { + key: tags, + ttl: "max", + }) if (!response.data) { - const notFoundError = notFound(response) metricsGetBreadcrumbs.noDataError() - throw notFoundError + + throw notFoundError({ + message: "Breadcrumbs query returned no data", + errorDetails: variables, + }) } const validatedBreadcrumbs = breadcrumbsSchema.safeParse( diff --git a/packages/trpc/lib/routers/contentstack/campaignOverviewPage/query.ts b/packages/trpc/lib/routers/contentstack/campaignOverviewPage/query.ts index 6dc0c8b23..dd09dd460 100644 --- a/packages/trpc/lib/routers/contentstack/campaignOverviewPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/campaignOverviewPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetCampaignOverviewPage, GetCampaignOverviewPageRefs, @@ -36,9 +36,10 @@ export const campaignOverviewPageQueryRouter = router({ metricsGetCampaignOverviewPageRefs.start() + const refVariables = { locale: lang, uid } const refsResponse = await request( GetCampaignOverviewPageRefs, - { locale: lang, uid }, + refVariables, { key: generateRefsResponseTag(lang, uid), ttl: "max", @@ -46,9 +47,12 @@ export const campaignOverviewPageQueryRouter = router({ ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetCampaignOverviewPageRefs.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetCampaignOverviewPageRefs returned no data", + errorDetails: refVariables, + }) } const validatedRefsData = campaignOverviewPageRefsSchema.safeParse( @@ -75,21 +79,22 @@ export const campaignOverviewPageQueryRouter = router({ metricsGetCampaignOverviewPage.start() + const variables = { locale: lang, uid } const response = await request( GetCampaignOverviewPage, - { - locale: lang, - uid, - }, + variables, { key: tags, ttl: "max", } ) if (!response.data) { - const notFoundError = notFound(response) metricsGetCampaignOverviewPage.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetCampaignOverviewPage returned no data", + errorDetails: variables, + }) } const validatedResponse = campaignOverviewPageSchema.safeParse( diff --git a/packages/trpc/lib/routers/contentstack/campaignPage/query.ts b/packages/trpc/lib/routers/contentstack/campaignPage/query.ts index 1e1e41299..255866079 100644 --- a/packages/trpc/lib/routers/contentstack/campaignPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/campaignPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetCampaignPage, GetCampaignPageRefs, @@ -32,18 +32,21 @@ export const campaignPageQueryRouter = router({ metricsGetCampaignPageRefs.start() + const refVariables = { locale: lang, uid } const refsResponse = await request( GetCampaignPageRefs, - { locale: lang, uid }, + refVariables, { key: generateRefsResponseTag(lang, uid), ttl: "max", } ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetCampaignPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetCampaignPageRefs returned no data", + errorDetails: refVariables, + }) } const validatedRefsData = campaignPageRefsSchema.safeParse( @@ -68,21 +71,21 @@ export const campaignPageQueryRouter = router({ metricsGetCampaignPage.start() + const variables = { locale: lang, uid } const response = await request( GetCampaignPage, - { - locale: lang, - uid, - }, + variables, { key: tags, ttl: "max", } ) if (!response.data) { - const notFoundError = notFound(response) metricsGetCampaignPage.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetCampaignPage returned no data", + errorDetails: variables, + }) } const validatedResponse = campaignPageSchema.safeParse(response.data) diff --git a/packages/trpc/lib/routers/contentstack/campaignPage/utils.ts b/packages/trpc/lib/routers/contentstack/campaignPage/utils.ts index f4deb80c9..ec014d60c 100644 --- a/packages/trpc/lib/routers/contentstack/campaignPage/utils.ts +++ b/packages/trpc/lib/routers/contentstack/campaignPage/utils.ts @@ -1,7 +1,7 @@ import { dt } from "@scandic-hotels/common/dt" import { createCounter } from "@scandic-hotels/common/telemetry" -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetCampaignPagesByHotelUid, GetCampaignPagesByHotelUidRefs, @@ -121,13 +121,14 @@ export async function getCampaignPagesByHotelPageUid( `${hotelPageUid}-${today}`, "hotel_page_campaigns" ) + const variables = { + locale: lang, + hotelPageUid, + today, + } const refsResponse = await request( GetCampaignPagesByHotelUidRefs, - { - locale: lang, - hotelPageUid, - today, - }, + variables, { key: refsTag, ttl: "1d", @@ -135,9 +136,11 @@ export async function getCampaignPagesByHotelPageUid( ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetCampaignPagesByHotelUidRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetCampaignPagesByHotelUidRefs returned no data", + errorDetails: variables, + }) } const validatedRefsData = campaignPagesByHotelUidRefsSchema.safeParse( diff --git a/packages/trpc/lib/routers/contentstack/collectionPage/utils.ts b/packages/trpc/lib/routers/contentstack/collectionPage/utils.ts index d5a987f66..27170f6d4 100644 --- a/packages/trpc/lib/routers/contentstack/collectionPage/utils.ts +++ b/packages/trpc/lib/routers/contentstack/collectionPage/utils.ts @@ -1,6 +1,6 @@ import { createCounter } from "@scandic-hotels/common/telemetry" -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetCollectionPageRefs } from "../../../graphql/Query/CollectionPage/CollectionPage.graphql" import { request } from "../../../graphql/request" import { @@ -32,13 +32,13 @@ export async function fetchCollectionPageRefs(lang: Lang, uid: string) { metricsGetCollectionPageRefs.start() const cacheKey = generateRefsResponseTag(lang, uid) - + const variables = { + locale: lang, + uid, + } const refsResponse = await request( GetCollectionPageRefs, - { - locale: lang, - uid, - }, + variables, { key: cacheKey, ttl: "max", @@ -46,9 +46,11 @@ export async function fetchCollectionPageRefs(lang: Lang, uid: string) { ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetCollectionPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetCollectionPageRefs returned no data", + errorDetails: variables, + }) } return refsResponse.data diff --git a/packages/trpc/lib/routers/contentstack/contentPage/utils.ts b/packages/trpc/lib/routers/contentstack/contentPage/utils.ts index 4a3a9349a..55a5a424c 100644 --- a/packages/trpc/lib/routers/contentstack/contentPage/utils.ts +++ b/packages/trpc/lib/routers/contentstack/contentPage/utils.ts @@ -1,6 +1,6 @@ import { createCounter } from "@scandic-hotels/common/telemetry" -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { batchRequest } from "../../../graphql/batchRequest" import { GetContentPageBlocksRefs, @@ -34,10 +34,11 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) { metricsGetContentPageRefs.start() + const variables = { locale: lang, uid } const res = await batchRequest([ { document: GetContentPageRefs, - variables: { locale: lang, uid }, + variables, cacheOptions: { key: generateRefsResponseTag(lang, uid), ttl: "max", @@ -45,7 +46,7 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) { }, { document: GetContentPageBlocksRefs, - variables: { locale: lang, uid }, + variables, cacheOptions: { key: generateTag(lang, uid + 1), ttl: "max", @@ -53,9 +54,11 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) { }, ]) if (!res.data) { - const notFoundError = notFound(res) metricsGetContentPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetContentPageRefs/GetContentPageBlocksRefs returned no data", + errorDetails: variables, + }) } const validatedData = contentPageRefsSchema.safeParse(res.data) diff --git a/packages/trpc/lib/routers/contentstack/destinationCityPage/query.ts b/packages/trpc/lib/routers/contentstack/destinationCityPage/query.ts index 263eb7445..31831de2c 100644 --- a/packages/trpc/lib/routers/contentstack/destinationCityPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/destinationCityPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetDestinationCityPage, GetDestinationCityPageRefs, @@ -34,9 +34,10 @@ export const destinationCityPageQueryRouter = router({ metricsGetDestinationCityPageRefs.start() + const variables = { locale: lang, uid } const refsResponse = await request( GetDestinationCityPageRefs, - { locale: lang, uid }, + variables, { key: generateRefsResponseTag(lang, uid), ttl: "max", @@ -44,9 +45,11 @@ export const destinationCityPageQueryRouter = router({ ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetDestinationCityPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetDestinationCityPageRefs returned no data", + errorDetails: variables, + }) } const validatedRefsData = destinationCityPageRefsSchema.safeParse( @@ -73,19 +76,19 @@ export const destinationCityPageQueryRouter = router({ const response = await request( GetDestinationCityPage, - { - locale: lang, - uid, - }, + variables, { key: tags, ttl: "max", } ) if (!response.data) { - const notFoundError = notFound(response) metricsGetDestinationCityPage.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetDestinationCityPage returned no data", + errorDetails: variables, + }) } const validatedResponse = destinationCityPageSchema.safeParse(response.data) diff --git a/packages/trpc/lib/routers/contentstack/destinationCountryPage/query.ts b/packages/trpc/lib/routers/contentstack/destinationCountryPage/query.ts index fa1386908..47681ad08 100644 --- a/packages/trpc/lib/routers/contentstack/destinationCountryPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/destinationCountryPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetDestinationCountryPage, GetDestinationCountryPageRefs, @@ -38,9 +38,10 @@ export const destinationCountryPageQueryRouter = router({ metricsGetDestinationCountryPageRefs.start() + const variables = { locale: lang, uid } const refsResponse = await request( GetDestinationCountryPageRefs, - { locale: lang, uid }, + variables, { key: generateRefsResponseTag(lang, uid), ttl: "max", @@ -48,9 +49,11 @@ export const destinationCountryPageQueryRouter = router({ ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetDestinationCountryPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetDestinationCountryPageRefs returned no data", + errorDetails: variables, + }) } const validatedRefsData = destinationCountryPageRefsSchema.safeParse( @@ -77,19 +80,18 @@ export const destinationCountryPageQueryRouter = router({ const response = await request( GetDestinationCountryPage, - { - locale: lang, - uid, - }, + variables, { key: tags, ttl: "max", } ) if (!response.data) { - const notFoundError = notFound(response) metricsGetDestinationCountryPage.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetDestinationCountryPage returned no data", + errorDetails: variables, + }) } const validatedResponse = destinationCountryPageSchema.safeParse( diff --git a/packages/trpc/lib/routers/contentstack/destinationOverviewPage/query.ts b/packages/trpc/lib/routers/contentstack/destinationOverviewPage/query.ts index 6484d5c9c..925ce2016 100644 --- a/packages/trpc/lib/routers/contentstack/destinationOverviewPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/destinationOverviewPage/query.ts @@ -2,7 +2,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetDestinationOverviewPage, GetDestinationOverviewPageRefs, @@ -49,21 +49,21 @@ export const destinationOverviewPageQueryRouter = router({ metricsGetDestinationOverviewPageRefs.start() + const variables = { locale: lang, uid } const refsResponse = await request( GetDestinationOverviewPageRefs, - { - locale: lang, - uid, - }, + variables, { key: generateRefsResponseTag(lang, uid), ttl: "max", } ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetDestinationOverviewPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetDestinationOverviewPageRefs returned no data", + errorDetails: variables, + }) } const validatedRefsData = destinationOverviewPageRefsSchema.safeParse( @@ -89,19 +89,18 @@ export const destinationOverviewPageQueryRouter = router({ const response = await request( GetDestinationOverviewPage, - { - locale: lang, - uid, - }, + variables, { key: generateTag(lang, uid), ttl: "max", } ) if (!response.data) { - const notFoundError = notFound(response) metricsGetDestinationOverviewPage.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetDestinationOverviewPage returned no data", + errorDetails: variables, + }) } const destinationOverviewPage = destinationOverviewPageSchema.safeParse( diff --git a/packages/trpc/lib/routers/contentstack/hotelPage/query.ts b/packages/trpc/lib/routers/contentstack/hotelPage/query.ts index 15bf62658..975711c6f 100644 --- a/packages/trpc/lib/routers/contentstack/hotelPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/hotelPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetHotelPage } from "../../../graphql/Query/HotelPage/HotelPage.graphql" import { request } from "../../../graphql/request" import { contentstackExtendedProcedureUID } from "../../../procedures" @@ -21,12 +21,10 @@ export const hotelPageQueryRouter = router({ metricsGetHotelPage.start() + const variables = { locale: lang, uid } const hotelPageResponse = await request( GetHotelPage, - { - locale: lang, - uid, - }, + variables, { key: generateTag(lang, uid), ttl: "max", @@ -34,9 +32,12 @@ export const hotelPageQueryRouter = router({ ) if (!hotelPageResponse.data) { - const notFoundError = notFound(hotelPageResponse) metricsGetHotelPage.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetHotelPage returned no data", + errorDetails: variables, + }) } const validatedHotelPage = hotelPageSchema.safeParse(hotelPageResponse.data) diff --git a/packages/trpc/lib/routers/contentstack/loyaltyLevel/query.ts b/packages/trpc/lib/routers/contentstack/loyaltyLevel/query.ts index 49c1434e5..f378b0a13 100644 --- a/packages/trpc/lib/routers/contentstack/loyaltyLevel/query.ts +++ b/packages/trpc/lib/routers/contentstack/loyaltyLevel/query.ts @@ -7,7 +7,7 @@ import { import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetAllLoyaltyLevels, GetLoyaltyLevel, @@ -48,9 +48,12 @@ export const getAllLoyaltyLevels = cache(async (lang: Lang) => { ) if (!loyaltyLevelsConfigResponse.data) { - const notFoundError = notFound(loyaltyLevelsConfigResponse) metricsGetLoyaltyLevelAll.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetAllLoyaltyLevels returned no data", + errorDetails: { lang, level_ids: allLevelIds }, + }) } const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse( @@ -90,9 +93,11 @@ export const getLoyaltyLevel = cache( !loyaltyLevelsConfigResponse.data || !loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length ) { - const notFoundError = notFound(loyaltyLevelsConfigResponse) metricsGetLoyaltyLevel.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetLoyaltyLevel returned no data", + errorDetails: { lang, level_id }, + }) } const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse( diff --git a/packages/trpc/lib/routers/contentstack/loyaltyPage/query.ts b/packages/trpc/lib/routers/contentstack/loyaltyPage/query.ts index 7235bc2f6..04b0db2e1 100644 --- a/packages/trpc/lib/routers/contentstack/loyaltyPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/loyaltyPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetLoyaltyPage, GetLoyaltyPageRefs, @@ -47,9 +47,11 @@ export const loyaltyPageQueryRouter = router({ ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetLoyaltyPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetLoyaltyPageRefs returned no data", + errorDetails: { ...variables }, + }) } const validatedLoyaltyPageRefs = loyaltyPageRefsSchema.safeParse( @@ -86,9 +88,11 @@ export const loyaltyPageQueryRouter = router({ ) if (!response.data) { - const notFoundError = notFound(response) metricsGetLoyaltyPage.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetLoyaltyPage returned no data", + errorDetails: { lang, uid }, + }) } const validatedLoyaltyPage = loyaltyPageSchema.safeParse(response.data) diff --git a/packages/trpc/lib/routers/contentstack/metadata/query.ts b/packages/trpc/lib/routers/contentstack/metadata/query.ts index 62b42ef0a..6e304f0b1 100644 --- a/packages/trpc/lib/routers/contentstack/metadata/query.ts +++ b/packages/trpc/lib/routers/contentstack/metadata/query.ts @@ -4,7 +4,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." import { PageContentTypeEnum } from "../../../enums/contentType" -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetAccountPageMetadata } from "../../../graphql/Query/AccountPage/Metadata.graphql" import { GetCampaignOverviewPageMetadata } from "../../../graphql/Query/CampaignOverviewPage/Metadata.graphql" import { GetCampaignPageMetadata } from "../../../graphql/Query/CampaignPage/Metadata.graphql" @@ -51,9 +51,12 @@ const fetchMetadata = cache(async function fetchMemoizedMetadata( ) if (!response.data) { - const notFoundError = notFound(response) metricsGetMetadata.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetMetadata returned no data", + errorDetails: { lang, uid }, + }) } metricsGetMetadata.success() diff --git a/packages/trpc/lib/routers/contentstack/partner/query.ts b/packages/trpc/lib/routers/contentstack/partner/query.ts index f85e809e3..0b62b1151 100644 --- a/packages/trpc/lib/routers/contentstack/partner/query.ts +++ b/packages/trpc/lib/routers/contentstack/partner/query.ts @@ -3,7 +3,7 @@ import { cache } from "react" import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetAllSasTierComparison } from "../../../graphql/Query/SASTierComparison.graphql" import { request } from "../../../graphql/request" import { contentstackBaseProcedure } from "../../../procedures" @@ -36,9 +36,12 @@ export const getSasTierComparison = cache(async (lang: Lang) => { ) if (!sasTierComparisonConfigResponse.data) { - const notFoundError = notFound(sasTierComparisonConfigResponse) metricsGetSasTierComparison.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetAllSasTierComparison returned no data", + errorDetails: { lang }, + }) } const validatedSasTierComparison = validateSasTierComparisonSchema.safeParse( diff --git a/packages/trpc/lib/routers/contentstack/profilingConsent/query.ts b/packages/trpc/lib/routers/contentstack/profilingConsent/query.ts index 45068590a..c71b76d0f 100644 --- a/packages/trpc/lib/routers/contentstack/profilingConsent/query.ts +++ b/packages/trpc/lib/routers/contentstack/profilingConsent/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetProfilingConsent } from "../../../graphql/Query/ProfilingConsent.graphql" import { request } from "../../../graphql/request" import { contentstackBaseProcedure } from "../../../procedures" @@ -38,9 +38,11 @@ export const profilingConsentQueryRouter = router({ } ) if (!response.data) { - const notFoundError = notFound(response) metricsGetProfilingConsent.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetProfilingConsent returned no data", + errorDetails: { lang }, + }) } const validatedResponse = profilingConsentSchema.safeParse(response.data) diff --git a/packages/trpc/lib/routers/contentstack/promoCampaignPage/query.ts b/packages/trpc/lib/routers/contentstack/promoCampaignPage/query.ts index 0ee6bd6ee..667972b8a 100644 --- a/packages/trpc/lib/routers/contentstack/promoCampaignPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/promoCampaignPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetPromoCampaignPage, GetPromoCampaignPageRefs, @@ -42,9 +42,11 @@ export const promoCampaignPageQueryRouter = router({ } ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetPromoCampaignPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetPromoCampaignPageRefs returned no data", + errorDetails: { lang, uid }, + }) } const validatedRefsData = promoCampaignPageRefsSchema.safeParse( @@ -81,9 +83,11 @@ export const promoCampaignPageQueryRouter = router({ } ) if (!response.data) { - const notFoundError = notFound(response) metricsGetPromoCampaignPage.noDataError() - throw notFoundError + throw notFoundError({ + message: "GetPromoCampaignPage returned no data", + errorDetails: { lang, uid }, + }) } const validatedResponse = promoCampaignPageSchema.safeParse(response.data) diff --git a/packages/trpc/lib/routers/contentstack/reward/query.ts b/packages/trpc/lib/routers/contentstack/reward/query.ts index 62f76e0c0..334f9f00e 100644 --- a/packages/trpc/lib/routers/contentstack/reward/query.ts +++ b/packages/trpc/lib/routers/contentstack/reward/query.ts @@ -2,7 +2,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." import * as api from "../../../api" -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { contentStackBaseWithProtectedProcedure, contentStackBaseWithServiceProcedure, @@ -92,7 +92,7 @@ export const rewardQueryRouter = router({ metricsGetContentstackRewardAll.dataError( `Failed to matched loyalty level between API and CMS for level ${level}` ) - throw notFound() + throw notFoundError() } const result: LevelWithRewards = { ...levelConfig, diff --git a/packages/trpc/lib/routers/contentstack/reward/utils.ts b/packages/trpc/lib/routers/contentstack/reward/utils.ts index a51e05385..05bf1439e 100644 --- a/packages/trpc/lib/routers/contentstack/reward/utils.ts +++ b/packages/trpc/lib/routers/contentstack/reward/utils.ts @@ -2,7 +2,7 @@ import { getCacheClient } from "@scandic-hotels/common/dataCache" import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../api" -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetRewards as GetRewards, GetRewardsRef as GetRewardsRef, @@ -109,9 +109,12 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) { ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetContentstackRewardAllRefs.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetRewardsRef returned no data", + errorDetails: { lang, rewardIds }, + }) } const validatedRefsData = rewardRefsSchema.safeParse(refsResponse) @@ -144,9 +147,12 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) { ) if (!cmsRewardsResponse.data) { - const notFoundError = notFound(cmsRewardsResponse) metricsGetContentstackRewardAll.noDataError() - throw notFoundError + + throw notFoundError({ + message: "GetRewards not found", + errorDetails: { lang, rewardIds }, + }) } const validatedCmsRewards = diff --git a/packages/trpc/lib/routers/contentstack/startPage/query.ts b/packages/trpc/lib/routers/contentstack/startPage/query.ts index b275a81e3..9e6c0a99c 100644 --- a/packages/trpc/lib/routers/contentstack/startPage/query.ts +++ b/packages/trpc/lib/routers/contentstack/startPage/query.ts @@ -1,7 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../../.." -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { GetStartPage, GetStartPageRefs, @@ -54,9 +54,11 @@ export const startPageQueryRouter = router({ } ) if (!refsResponse.data) { - const notFoundError = notFound(refsResponse) metricsGetStartPageRefs.noDataError() - throw notFoundError + throw notFoundError({ + message: "StartPage refs returned no data", + errorDetails: { lang, uid }, + }) } const validatedRefsData = startPageRefsSchema.safeParse(refsResponse.data) @@ -95,9 +97,11 @@ export const startPageQueryRouter = router({ ) if (!response.data) { - const notFoundError = notFound(response) metricsGetStartPage.noDataError() - throw notFoundError + throw notFoundError({ + message: "StartPage not found", + errorDetails: { lang, uid }, + }) } const startPage = startPageSchema.safeParse(response.data) diff --git a/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByCity.ts b/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByCity.ts index 7e437b279..2d7abd103 100644 --- a/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByCity.ts +++ b/packages/trpc/lib/routers/hotels/services/getHotelsAvailabilityByCity.ts @@ -1,7 +1,11 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../api" -import { badRequestError } from "../../../errors" +import { + badRequestError, + extractResponseDetails, + serverErrorByStatus, +} from "../../../errors" import { toApiLang } from "../../../utils" import { hotelsAvailabilitySchema } from "../output" @@ -70,9 +74,15 @@ export async function getHotelsAvailabilityByCity({ }, params ) + if (!apiResponse.ok) { await metricsGetHotelsAvailabilityByCity.httpError(apiResponse) - throw new Error("Failed to fetch hotels availability by city") + + throw serverErrorByStatus( + apiResponse.status, + await extractResponseDetails(apiResponse), + "getHotelsAvailabilityByCity failed" + ) } const apiJson = await apiResponse.json() diff --git a/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.test.ts b/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.test.ts index f75fcc7dd..61a0aaf5d 100644 --- a/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.test.ts +++ b/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.test.ts @@ -77,11 +77,13 @@ describe("getLocationsByCountries", () => { it("throws unauthorized on 401 response", async () => { mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient) - mockedApiGet.mockResolvedValueOnce({ - ok: false, - status: 401, - json: async () => ({}), - }) + mockedApiGet.mockResolvedValueOnce( + new Response(JSON.stringify({ data: "Unauthorized" }), { + status: 401, + statusText: "Unauthorized", + headers: { "Content-Type": "application/json" }, + }) + ) await expect( getLocationsByCountries({ @@ -89,7 +91,9 @@ describe("getLocationsByCountries", () => { citiesByCountry: null, serviceToken: "token", }) - ).rejects.toThrow("Unauthorized") + ).rejects.toMatchObject({ + code: "UNAUTHORIZED", + }) expect(mockedApiGet).toHaveBeenCalled() }) @@ -97,11 +101,13 @@ describe("getLocationsByCountries", () => { it("throws forbidden on 403 response", async () => { mockedGetCacheClient.mockResolvedValueOnce(mockedCacheClient) - mockedApiGet.mockResolvedValueOnce({ - ok: false, - status: 403, - json: async () => ({}), - }) + mockedApiGet.mockResolvedValueOnce( + new Response(JSON.stringify({ data: "Forbidden" }), { + status: 403, + statusText: "Forbidden", + headers: { "Content-Type": "application/json" }, + }) + ) await expect( getLocationsByCountries({ @@ -109,7 +115,9 @@ describe("getLocationsByCountries", () => { citiesByCountry: null, serviceToken: "token", }) - ).rejects.toThrow("Forbidden") + ).rejects.toMatchObject({ + code: "FORBIDDEN", + }) }) it("parses locations and enriches city country and hotel city via getCity", async () => { diff --git a/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.ts b/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.ts index 4bdd38a7e..e684a8c33 100644 --- a/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.ts +++ b/packages/trpc/lib/routers/hotels/services/getLocationsByCountries.ts @@ -4,7 +4,7 @@ import { getCacheClient } from "@scandic-hotels/common/dataCache" import { createLogger } from "@scandic-hotels/common/logger/createLogger" import * as api from "../../../api" -import { serverErrorByStatus } from "../../../errors" +import { extractResponseDetails, serverErrorByStatus } from "../../../errors" import { toApiLang } from "../../../utils" import { locationCitySchema } from "../schemas/location/city" import { locationHotelSchema } from "../schemas/location/hotel" @@ -60,7 +60,11 @@ export async function getLocationsByCountries({ ) if (!apiResponse.ok) { - throw serverErrorByStatus(apiResponse.status, { apiResponse }) + throw serverErrorByStatus( + apiResponse.status, + await extractResponseDetails(apiResponse), + "getLocationsByCountries failed" + ) } const apiJson = await apiResponse.json() diff --git a/packages/trpc/lib/routers/user/query/index.ts b/packages/trpc/lib/routers/user/query/index.ts index 860e58619..0e397f6ff 100644 --- a/packages/trpc/lib/routers/user/query/index.ts +++ b/packages/trpc/lib/routers/user/query/index.ts @@ -5,7 +5,7 @@ import { env } from "../../../../env/server" import { router } from "../../.." import * as api from "../../../api" import { Transactions } from "../../../enums/transactions" -import { notFound } from "../../../errors" +import { notFoundError } from "../../../errors" import { languageProtectedProcedure, protectedProcedure, @@ -47,7 +47,7 @@ export const userQueryRouter = router({ .query(async function getUser({ ctx }) { const user = await ctx.getScandicUser() if (!user) { - throw notFound() + throw notFoundError() } return parsedUser(user, !ctx.isMFA) @@ -68,7 +68,7 @@ export const userQueryRouter = router({ getBasic: protectedProcedure.query(async function getBasicUser({ ctx }) { const user = await ctx.getScandicBasicUser() if (!user) { - throw notFound() + throw notFoundError() } return user diff --git a/packages/trpc/lib/routers/user/utils/getBasicUser.ts b/packages/trpc/lib/routers/user/utils/getBasicUser.ts index 9cd4c7314..b12b260ae 100644 --- a/packages/trpc/lib/routers/user/utils/getBasicUser.ts +++ b/packages/trpc/lib/routers/user/utils/getBasicUser.ts @@ -1,10 +1,20 @@ +import * as Sentry from "@sentry/nextjs" + import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../api" import { cache } from "../../../DUPLICATED/cache" -import { serverErrorByStatus, sessionExpiredError } from "../../../errors" +import { + extractResponseDetails, + serverErrorByStatus, + sessionExpiredError, +} from "../../../errors" import { getBasicUserSchema } from "../output" +import type z from "zod" + +import type { DeepPartial } from "../../../types/deepPartial" + export const getBasicUser = cache( async ({ token, @@ -30,12 +40,17 @@ export const getBasicUser = cache( if (!apiResponse.ok) { await metricsGetBasicUser.httpError(apiResponse) - throw serverErrorByStatus(apiResponse.status, apiResponse) + throw serverErrorByStatus( + apiResponse.status, + await extractResponseDetails(apiResponse), + "getBasicUser failed" + ) } const apiJson = await apiResponse.json() const verifiedData = getBasicUserSchema.safeParse(apiJson) if (!verifiedData.success) { + addUserToSentry(apiJson) metricsGetBasicUser.validationError(verifiedData.error) throw verifiedData.error } @@ -45,3 +60,18 @@ export const getBasicUser = cache( return verifiedData.data } ) + +function addUserToSentry(apiJson: unknown) { + const typedData = apiJson as DeepPartial> + if ( + typeof typedData?.profileId === "undefined" || + typeof typedData?.membershipNumber === "undefined" + ) { + return + } + + Sentry.setUser({ + id: typedData?.profileId, + username: typedData?.membershipNumber, + }) +} diff --git a/packages/trpc/lib/routers/user/utils/getVerifiedUser.ts b/packages/trpc/lib/routers/user/utils/getVerifiedUser.ts index 9e91f1896..b93631900 100644 --- a/packages/trpc/lib/routers/user/utils/getVerifiedUser.ts +++ b/packages/trpc/lib/routers/user/utils/getVerifiedUser.ts @@ -1,14 +1,21 @@ +import * as Sentry from "@sentry/nextjs" + import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../api" import { cache } from "../../../DUPLICATED/cache" import { + extractResponseDetails, internalServerError, serverErrorByStatus, sessionExpiredError, } from "../../../errors" import { getUserSchema } from "../output" +import type { z } from "zod" + +import type { DeepPartial } from "../../../types/deepPartial" + export const getVerifiedUser = cache( async ({ token, @@ -42,7 +49,11 @@ export const getVerifiedUser = cache( if (!apiResponse.ok) { await metricsGetVerifiedUser.httpError(apiResponse) - throw serverErrorByStatus(apiResponse.status, apiResponse) + throw serverErrorByStatus( + apiResponse.status, + await extractResponseDetails(apiResponse), + "getVerifiedUser failed" + ) } const apiJson = await apiResponse.json() @@ -57,13 +68,29 @@ export const getVerifiedUser = cache( } const verifiedData = getUserSchema.safeParse(apiJson) + if (!verifiedData.success) { + addUserToSentry(apiJson) metricsGetVerifiedUser.validationError(verifiedData.error) throw verifiedData.error } metricsGetVerifiedUser.success() - return verifiedData.data } ) + +function addUserToSentry(apiJson: unknown) { + const typedData = apiJson as DeepPartial> + if ( + typeof typedData.data?.attributes?.profileId === "undefined" || + typeof typedData.data?.attributes?.membershipNumber === "undefined" + ) { + return + } + + Sentry.setUser({ + id: typedData?.data?.attributes?.profileId, + username: typedData?.data?.attributes?.membershipNumber, + }) +} diff --git a/packages/trpc/lib/utils/getResponseBody.ts b/packages/trpc/lib/utils/getResponseBody.ts new file mode 100644 index 000000000..506b219e8 --- /dev/null +++ b/packages/trpc/lib/utils/getResponseBody.ts @@ -0,0 +1,21 @@ +export async function getResponseBody( + response: Response +): Promise> { + const clone = response.clone() + + const contentType = clone.headers.get("content-type") + if (contentType && contentType.indexOf("application/json") !== -1) { + try { + return await clone.json() + } catch { + try { + return await clone.text() + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return `Unable to extract body '${message}'` + } + } + } + + return await clone.text() +}