feat: harmonize log and metrics

This commit is contained in:
Michael Zetterberg
2025-04-17 07:16:11 +02:00
parent 858a81b16f
commit 5323a8e46e
58 changed files with 2324 additions and 4726 deletions

View File

@@ -1,8 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { countries } from "@/constants/countries"
import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { createCounter } from "@/server/telemetry"
import {
languageProtectedProcedure,
protectedProcedure,
@@ -10,8 +7,6 @@ import {
safeProtectedProcedure,
} from "@/server/trpc"
import { cache } from "@/utils/cache"
import * as maskValue from "@/utils/maskValue"
import { isValidSession } from "@/utils/session"
import { getFriendsMembership, getMembershipCards } from "@/utils/user"
@@ -20,277 +15,21 @@ import {
getSavedPaymentCardsInput,
staysInput,
} from "./input"
import { getFriendTransactionsSchema } from "./output"
import {
creditCardsSchema,
getFriendTransactionsSchema,
getStaysSchema,
getUserSchema,
} from "./output"
import { updateStaysBookingUrl } from "./utils"
import type { Session } from "next-auth"
getCreditCards,
getPreviousStays,
getUpcomingStays,
getVerifiedUser,
parsedUser,
updateStaysBookingUrl,
} from "./utils"
import type {
LoginType,
TrackingSDKUserData,
} from "@/types/components/tracking"
import { Transactions } from "@/types/enums/transactions"
import type { User } from "@/types/user"
// OpenTelemetry metrics: User
const meter = metrics.getMeter("trpc.user")
const getVerifiedUserCounter = meter.createCounter("trpc.user.get")
const getVerifiedUserSuccessCounter = meter.createCounter(
"trpc.user.get-success"
)
const getVerifiedUserFailCounter = meter.createCounter("trpc.user.get-fail")
// OpenTelemetry metrics: Stays
const getPreviousStaysCounter = meter.createCounter("trpc.user.stays.previous")
const getPreviousStaysSuccessCounter = meter.createCounter(
"trpc.user.stays.previous-success"
)
const getPreviousStaysFailCounter = meter.createCounter(
"trpc.user.stays.previous-fail"
)
const getUpcomingStaysCounter = meter.createCounter("trpc.user.stays.upcoming")
const getUpcomingStaysSuccessCounter = meter.createCounter(
"trpc.user.stays.upcoming-success"
)
const getUpcomingStaysFailCounter = meter.createCounter(
"trpc.user.stays.upcoming-fail"
)
// OpenTelemetry metrics: Transactions
const getFriendTransactionsCounter = meter.createCounter(
"trpc.user.transactions.friendTransactions"
)
const getFriendTransactionsSuccessCounter = meter.createCounter(
"trpc.user.transactions.friendTransactions-success"
)
const getFriendTransactionsFailCounter = meter.createCounter(
"trpc.user.transactions.friendTransactions-fail"
)
// OpenTelemetry metrics: Credit Cards
const getCreditCardsCounter = meter.createCounter("trpc.user.creditCards")
const getCreditCardsSuccessCounter = meter.createCounter(
"trpc.user.creditCards-success"
)
const getCreditCardsFailCounter = meter.createCounter(
"trpc.user.creditCards-fail"
)
export const getVerifiedUser = cache(
async ({
session,
includeExtendedPartnerData,
}: {
session: Session
includeExtendedPartnerData?: boolean
}) => {
const now = Date.now()
if (session.token.expires_at && session.token.expires_at < now) {
return { error: true, cause: "token_expired" } as const
}
getVerifiedUserCounter.add(1)
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
const apiResponse = await api.get(
api.endpoints.v2.Profile.profile,
{
headers: {
Authorization: `Bearer ${session.token.access_token}`,
},
},
includeExtendedPartnerData
? { includes: "extendedPartnerInformation" }
: {}
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getVerifiedUserFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.user.profile getVerifiedUser error",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
if (apiResponse.status === 401) {
return { error: true, cause: "unauthorized" } as const
} else if (apiResponse.status === 403) {
return { error: true, cause: "forbidden" } as const
} else if (apiResponse.status === 404) {
return { error: true, cause: "notfound" } as const
}
return {
error: true,
cause: "unknown",
status: apiResponse.status,
} as const
}
const apiJson = await apiResponse.json()
if (!apiJson.data?.attributes) {
getVerifiedUserFailCounter.add(1, {
error_type: "data_error",
})
console.error(
"api.user.profile getVerifiedUser data error",
JSON.stringify({
apiResponse: apiJson,
})
)
return null
}
const verifiedData = getUserSchema.safeParse(apiJson)
if (!verifiedData.success) {
getVerifiedUserFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
console.error(
"api.user.profile validation error",
JSON.stringify({
errors: verifiedData.error,
apiResponse: apiJson,
})
)
return null
}
getVerifiedUserSuccessCounter.add(1)
console.info("api.user.profile getVerifiedUser success", JSON.stringify({}))
return verifiedData
}
)
export function parsedUser(data: User, isMFA: boolean) {
const country = countries.find((c) => c.code === data.address?.countryCode)
const user = {
address: {
city: data.address?.city,
country: country?.name ?? "",
countryCode: data.address?.countryCode,
streetAddress: data.address?.streetAddress,
zipCode: data.address?.zipCode,
},
dateOfBirth: data.dateOfBirth,
email: data.email,
firstName: data.firstName,
language: data.language,
lastName: data.lastName,
membershipNumber: data.membershipNumber,
membership: getFriendsMembership(data.loyalty),
loyalty: data.loyalty,
name: `${data.firstName} ${data.lastName}`,
phoneNumber: data.phoneNumber,
profileId: data.profileId,
}
if (!isMFA) {
if (user.address.city) {
user.address.city = maskValue.text(user.address.city)
}
if (user.address.streetAddress) {
user.address.streetAddress = maskValue.text(user.address.streetAddress)
}
user.address.zipCode = data.address?.zipCode
? maskValue.text(data.address.zipCode)
: ""
user.dateOfBirth = maskValue.all(user.dateOfBirth)
user.email = maskValue.email(user.email)
user.phoneNumber = user.phoneNumber ? maskValue.phone(user.phoneNumber) : ""
}
return user
}
const getCreditCards = cache(
async ({
session,
onlyNonExpired,
}: {
session: Session
onlyNonExpired?: boolean
}) => {
getCreditCardsCounter.add(1)
console.info("api.profile.creditCards start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, {
headers: {
Authorization: `Bearer ${session.token.access_token}`,
},
})
if (!apiResponse.ok) {
const text = await apiResponse.text()
getCreditCardsFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.profile.creditCards error ",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = creditCardsSchema.safeParse(apiJson)
if (!verifiedData.success) {
getCreditCardsFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
console.error(
"api.profile.creditCards validation error ",
JSON.stringify({ error: verifiedData.error })
)
return null
}
getCreditCardsSuccessCounter.add(1)
console.info("api.profile.creditCards success", JSON.stringify({}))
return verifiedData.data.data.filter((card) => {
if (onlyNonExpired) {
try {
const expirationDate = dt(card.expirationDate).startOf("day")
const currentDate = dt().startOf("day")
return expirationDate > currentDate
} catch (_) {
return false
}
}
return true
})
}
)
export const userQueryRouter = router({
get: protectedProcedure
@@ -385,11 +124,20 @@ export const userQueryRouter = router({
return membershipLevel
}),
userTrackingInfo: safeProtectedProcedure.query(async function ({ ctx }) {
const userTrackingInfoCounter = createCounter("user", "userTrackingInfo")
const metricsUserTrackingInfo = userTrackingInfoCounter.init()
metricsUserTrackingInfo.start()
const notLoggedInUserTrackingData: TrackingSDKUserData = {
loginStatus: "Non-logged in",
}
if (!isValidSession(ctx.session)) {
metricsUserTrackingInfo.success({
reason: "invalid session",
data: notLoggedInUserTrackingData,
})
return notLoggedInUserTrackingData
}
@@ -397,62 +145,24 @@ export const userQueryRouter = router({
const verifiedUserData = await getVerifiedUser({ session: ctx.session })
if (!verifiedUserData || "error" in verifiedUserData) {
return notLoggedInUserTrackingData
}
const params = new URLSearchParams()
params.set("limit", "1")
getPreviousStaysCounter.add(1, { query: JSON.stringify({ params }) })
console.info(
"api.booking.stays.past start",
JSON.stringify({ query: { params } })
)
const previousStaysResponse = await api.get(
api.endpoints.v1.Booking.Stays.past,
{
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
},
params
)
if (!previousStaysResponse.ok) {
getPreviousStaysFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: previousStaysResponse.status,
statusText: previousStaysResponse.statusText,
}),
metricsUserTrackingInfo.success({
reason: "invalid user data",
data: notLoggedInUserTrackingData,
})
console.error(
"api.booking.stays.past error",
JSON.stringify({
error: {
status: previousStaysResponse.status,
statusText: previousStaysResponse.statusText,
},
})
)
return notLoggedInUserTrackingData
}
const previousStaysApiJson = await previousStaysResponse.json()
const verifiedPreviousStaysData =
getStaysSchema.safeParse(previousStaysApiJson)
if (!verifiedPreviousStaysData.success) {
getPreviousStaysFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedPreviousStaysData.error),
const previousStaysData = await getPreviousStays(
ctx.session.token.access_token,
1
)
if (!previousStaysData) {
metricsUserTrackingInfo.success({
reason: "no previous stays data",
data: notLoggedInUserTrackingData,
})
console.error(
"api.booking.stays.past validation error, ",
JSON.stringify({ error: verifiedPreviousStaysData.error })
)
return notLoggedInUserTrackingData
}
getPreviousStaysSuccessCounter.add(1)
console.info("api.booking.stays.past success", JSON.stringify({}))
const membership = getFriendsMembership(verifiedUserData.data.loyalty)
@@ -462,13 +172,19 @@ export const userQueryRouter = router({
memberId: verifiedUserData.data.profileId,
membershipNumber: membership?.membershipNumber,
memberLevel: membership?.membershipLevel,
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
noOfNightsStayed: previousStaysData.links?.totalCount ?? 0,
totalPointsAvailableToSpend: membership?.currentPoints,
loginAction: "login success",
}
metricsUserTrackingInfo.success({
reason: "valid logged in",
data: loggedInUserTrackingData,
})
return loggedInUserTrackingData
} catch (error) {
console.error("Error in userTrackingInfo:", error)
metricsUserTrackingInfo.fail(error)
return notLoggedInUserTrackingData
}
}),
@@ -479,93 +195,31 @@ export const userQueryRouter = router({
.query(async ({ ctx, input }) => {
const { limit, cursor, lang } = input
const language = lang || ctx.lang
const params: Record<string, string> = { limit }
if (cursor) {
params.offset = cursor
}
getPreviousStaysCounter.add(1, { query: JSON.stringify({ params }) })
console.info(
"api.booking.stays.past start",
JSON.stringify({ query: { params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Booking.Stays.past,
{
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getPreviousStaysFailCounter.add(1, {
query: JSON.stringify({ params }),
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.booking.stays.past error ",
JSON.stringify({
query: { params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getStaysSchema.safeParse(apiJson)
if (!verifiedData.success) {
getPreviousStaysFailCounter.add(1, {
query: JSON.stringify({ params }),
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
console.error(
"api.booking.stays.past validation error ",
JSON.stringify({
query: { params },
error: verifiedData.error,
})
)
return null
}
getPreviousStaysSuccessCounter.add(1, {
query: JSON.stringify({ params }),
})
console.info(
"api.booking.stays.past success",
JSON.stringify({ query: { params } })
)
const nextCursor =
verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset
: undefined
const updatedData = await updateStaysBookingUrl(
verifiedData.data.data,
const data = await getPreviousStays(
ctx.session.token.access_token,
language
limit,
cursor
)
return {
data: updatedData,
nextCursor,
if (data) {
const nextCursor =
data.links && data.links.offset < data.links.totalCount
? data.links.offset
: undefined
const updatedData = await updateStaysBookingUrl(
data.data,
ctx.session,
language
)
return {
data: updatedData,
nextCursor,
}
}
return null
}),
upcoming: languageProtectedProcedure
@@ -573,92 +227,31 @@ export const userQueryRouter = router({
.query(async ({ ctx, input }) => {
const { limit, cursor, lang } = input
const language = lang || ctx.lang
const params: Record<string, string> = { limit }
if (cursor) {
params.offset = cursor
}
getUpcomingStaysCounter.add(1, {
query: JSON.stringify({ params }),
})
console.info(
"api.booking.stays.future start",
JSON.stringify({ query: { params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Booking.Stays.future,
{
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getUpcomingStaysFailCounter.add(1, {
query: JSON.stringify({ params }),
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.booking.stays.future error ",
JSON.stringify({
query: { params },
error_type: "http_error",
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getStaysSchema.safeParse(apiJson)
if (!verifiedData.success) {
getUpcomingStaysFailCounter.add(1, {
query: JSON.stringify({ params }),
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
console.error(
"api.booking.stays.future validation error ",
JSON.stringify({
query: { params },
error: verifiedData.error,
})
)
return null
}
getUpcomingStaysSuccessCounter.add(1, {
query: JSON.stringify({ params }),
})
console.info("api.booking.stays.future success", {
query: JSON.stringify({ params }),
})
const nextCursor =
verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset
: undefined
const updatedData = await updateStaysBookingUrl(
verifiedData.data.data,
const data = await getUpcomingStays(
ctx.session.token.access_token,
language
limit,
cursor
)
return {
data: updatedData,
nextCursor,
if (data) {
const nextCursor =
data.links && data.links.offset < data.links.totalCount
? data.links.offset
: undefined
const updatedData = await updateStaysBookingUrl(
data.data,
ctx.session,
language
)
return {
data: updatedData,
nextCursor,
}
}
return null
}),
}),
transaction: router({
@@ -667,11 +260,18 @@ export const userQueryRouter = router({
.query(async ({ ctx, input }) => {
const { limit, page } = input
getFriendTransactionsCounter.add(1)
console.info(
"api.transaction.friendTransactions start",
JSON.stringify({})
const friendTransactionsCounter = createCounter(
"trpc.user.transactions",
"friendTransactions"
)
const metricsFriendTransactions = friendTransactionsCounter.init({
limit,
page,
})
metricsFriendTransactions.start()
const apiResponse = await api.get(
api.endpoints.v1.Profile.Transaction.friendTransactions,
{
@@ -682,61 +282,20 @@ export const userQueryRouter = router({
)
if (!apiResponse.ok) {
// switch (apiResponse.status) {
// case 400:
// throw badRequestError()
// case 401:
// throw unauthorizedError()
// case 403:
// throw forbiddenError()
// default:
// throw internalServerError()
// }
const text = await apiResponse.text()
getFriendTransactionsFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.transaction.friendTransactions error ",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
await metricsFriendTransactions.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
if (!verifiedData.success) {
getFriendTransactionsFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
console.error(
"api.transaction.friendTransactions validation error ",
JSON.stringify({ error: verifiedData.error })
)
metricsFriendTransactions.validationError(verifiedData.error)
return null
}
getFriendTransactionsSuccessCounter.add(1)
console.info(
"api.transaction.friendTransactions success",
JSON.stringify({})
)
const updatedData = await updateStaysBookingUrl(
verifiedData.data.data,
ctx.session.token.access_token,
ctx.session,
ctx.lang
)
@@ -763,7 +322,7 @@ export const userQueryRouter = router({
const slicedData = pageData.slice(limit * (page - 1), limit * page)
return {
const result = {
data: {
transactions: slicedData.map(({ type, attributes }) => {
return {
@@ -786,6 +345,10 @@ export const userQueryRouter = router({
totalPages: Math.ceil(pageData.length / limit),
},
}
metricsFriendTransactions.success()
return result
}),
}),