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,5 +1,6 @@
import * as api from "@/lib/api"
import { notFound } from "@/server/errors/trpc"
import { createCounter } from "@/server/telemetry"
import {
contentStackBaseWithProtectedProcedure,
contentStackBaseWithServiceProcedure,
@@ -22,24 +23,9 @@ import {
} from "./input"
import { validateCategorizedRewardsSchema } from "./output"
import {
getAllRewardCounter,
getAllRewardFailCounter,
getAllRewardSuccessCounter,
getByLevelRewardCounter,
getByLevelRewardFailCounter,
getByLevelRewardSuccessCounter,
getCachedAllTierRewards,
getCmsRewards,
getCurrentRewardCounter,
getCurrentRewardFailCounter,
getCurrentRewardSuccessCounter,
getRedeemCounter,
getRedeemFailCounter,
getRedeemSuccessCounter,
getUniqueRewardIds,
getUnwrapSurpriseCounter,
getUnwrapSurpriseFailCounter,
getUnwrapSurpriseSuccessCounter,
} from "./utils"
import type { BaseReward, Surprise } from "@/types/components/myPages/rewards"
@@ -50,7 +36,14 @@ export const rewardQueryRouter = router({
all: contentStackBaseWithServiceProcedure
.input(rewardsAllInput)
.query(async function ({ input, ctx }) {
getAllRewardCounter.add(1)
const getContentstackRewardAllCounter = createCounter(
"trpc.contentstack",
"reward.all"
)
const metricsGetContentstackRewardAll =
getContentstackRewardAllCounter.init()
metricsGetContentstackRewardAll.start()
const allApiRewards = await getCachedAllTierRewards(ctx.serviceToken)
@@ -75,16 +68,23 @@ export const rewardQueryRouter = router({
const levelsWithRewards = Object.entries(allApiRewards).map(
([level, rewards]) => {
const combinedRewards = rewards
.filter((r) => (input.unique ? r?.rewardTierLevel === level : true))
.filter((reward) =>
input.unique ? reward.rewardTierLevel === level : true
)
.map((reward) => {
const contentStackReward = contentStackRewards.find((r) => {
return r.reward_id === reward?.rewardId
return r.reward_id === reward.rewardId
})
if (contentStackReward) {
return contentStackReward
} else {
console.error("No contentStackReward found", reward?.rewardId)
metricsGetContentstackRewardAll.dataError(
`Failed to find reward in CMS for reward ${reward.rewardId} `,
{
rewardId: reward.rewardId,
}
)
}
})
.filter((reward): reward is CMSReward => Boolean(reward))
@@ -94,9 +94,9 @@ export const rewardQueryRouter = router({
)
if (!levelConfig) {
getAllRewardFailCounter.add(1)
console.error("contentstack.loyaltyLevels level not found")
metricsGetContentstackRewardAll.dataError(
`Failed to matched loyalty level between API and CMS for level ${level}`
)
throw notFound()
}
const result: LevelWithRewards = {
@@ -107,21 +107,31 @@ export const rewardQueryRouter = router({
}
)
getAllRewardSuccessCounter.add(1)
metricsGetContentstackRewardAll.success()
return levelsWithRewards
}),
byLevel: contentStackBaseWithServiceProcedure
.input(rewardsByLevelInput)
.query(async function ({ input, ctx }) {
getByLevelRewardCounter.add(1)
const { level_id } = input
const getRewardByLevelCounter = createCounter(
"trpc.contentstack",
"reward.byLevel"
)
const metricsGetRewardByLevel = getRewardByLevelCounter.init({
level_id,
})
metricsGetRewardByLevel.start()
const allUpcomingApiRewards = await getCachedAllTierRewards(
ctx.serviceToken
)
if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) {
getByLevelRewardFailCounter.add(1)
metricsGetRewardByLevel.noDataError()
return null
}
@@ -150,24 +160,36 @@ export const rewardQueryRouter = router({
const levelsWithRewards = apiRewards
.map((reward) => {
const contentStackReward = contentStackRewards.find((r) => {
return r.reward_id === reward?.rewardId
return r.reward_id === reward.rewardId
})
if (contentStackReward) {
return contentStackReward
} else {
console.info("No contentStackReward found", reward?.rewardId)
metricsGetRewardByLevel.dataError(
`Failed to find reward in Contentstack with rewardId: ${reward.rewardId}`,
{
rewardId: reward.rewardId,
}
)
}
})
.filter((reward): reward is CMSReward => Boolean(reward))
getByLevelRewardSuccessCounter.add(1)
metricsGetRewardByLevel.success()
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
}),
current: contentStackBaseWithProtectedProcedure
.input(langInput.optional()) // lang is required for client, but not for server
.query(async function ({ ctx }) {
getCurrentRewardCounter.add(1)
const getCurrentRewardCounter = createCounter(
"trpc.contentstack",
"reward.current"
)
const metricsGetCurrentReward = getCurrentRewardCounter.init()
metricsGetCurrentReward.start()
const apiResponse = await api.get(
api.endpoints.v1.Profile.Reward.reward,
@@ -179,25 +201,7 @@ export const rewardQueryRouter = router({
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getCurrentRewardFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.reward error ",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
await metricsGetCurrentReward.httpError(apiResponse)
return null
}
@@ -207,19 +211,7 @@ export const rewardQueryRouter = router({
validateCategorizedRewardsSchema.safeParse(data)
if (!validatedApiRewards.success) {
getCurrentRewardFailCounter.add(1, {
locale: ctx.lang,
error_type: "validation_error",
error: JSON.stringify(validatedApiRewards.error),
})
console.error(validatedApiRewards.error)
console.error(
"contentstack.rewards validation error",
JSON.stringify({
query: { locale: ctx.lang },
error: validatedApiRewards.error,
})
)
metricsGetCurrentReward.validationError(validatedApiRewards.error)
return null
}
@@ -243,14 +235,20 @@ export const rewardQueryRouter = router({
}
})
getCurrentRewardSuccessCounter.add(1)
metricsGetCurrentReward.success()
return { rewards }
}),
surprises: contentStackBaseWithProtectedProcedure
.input(langInput.optional()) // lang is required for client, but not for server
.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1)
const getSurprisesCounter = createCounter(
"trpc.contentstack",
"surprises"
)
const metricsGetSurprises = getSurprisesCounter.init()
metricsGetSurprises.start()
const endpoint = api.endpoints.v1.Profile.Reward.reward
@@ -262,25 +260,7 @@ export const rewardQueryRouter = router({
})
if (!apiResponse.ok) {
const text = await apiResponse.text()
getCurrentRewardFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.reward error ",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
await metricsGetSurprises.httpError(apiResponse)
return null
}
@@ -289,19 +269,7 @@ export const rewardQueryRouter = router({
validateCategorizedRewardsSchema.safeParse(data)
if (!validatedApiRewards.success) {
getCurrentRewardFailCounter.add(1, {
locale: ctx.lang,
error_type: "validation_error",
error: JSON.stringify(validatedApiRewards.error),
})
console.error(validatedApiRewards.error)
console.error(
"contentstack.surprises validation error",
JSON.stringify({
query: { locale: ctx.lang },
error: validatedApiRewards.error,
})
)
metricsGetSurprises.validationError(validatedApiRewards.error)
return null
}
@@ -316,8 +284,6 @@ export const rewardQueryRouter = router({
return null
}
getCurrentRewardSuccessCounter.add(1)
const surprises: Surprise[] = cmsRewards
.map((cmsReward) => {
// Non-null assertion is used here because we know our reward exist
@@ -336,73 +302,80 @@ export const rewardQueryRouter = router({
})
.flatMap((surprises) => (surprises ? [surprises] : []))
metricsGetSurprises.success()
return surprises
}),
unwrap: protectedProcedure
.input(rewardsUpdateInput)
.mutation(async ({ input, ctx }) => {
getUnwrapSurpriseCounter.add(1)
const results = await Promise.allSettled(
// Execute each unwrap individually
input.map(({ rewardId, couponCode }) => {
async function handleUnwrap() {
const getUnwrapSurpriseCounter = createCounter(
"trpc.contentstack",
"reward.unwrap"
)
const promises = input.map(({ rewardId, couponCode }) => {
return api.post(api.endpoints.v1.Profile.Reward.unwrap, {
body: {
rewardId,
couponCode,
},
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
})
})
const responses = await Promise.all(promises)
const errors = await Promise.all(
responses.map(async (apiResponse) => {
if (!apiResponse.ok) {
const text = await apiResponse.text()
getUnwrapSurpriseFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
const metricsGetUnwrapSurprise = getUnwrapSurpriseCounter.init({
rewardId,
couponCode,
})
console.error(
"contentstack.unwrap API error",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
metricsGetUnwrapSurprise.start()
const apiResponse = await api.post(
api.endpoints.v1.Profile.Reward.unwrap,
{
body: {
rewardId,
couponCode,
},
query: {},
})
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
}
)
return false
if (!apiResponse.ok) {
metricsGetUnwrapSurprise.httpError(apiResponse)
return false
}
metricsGetUnwrapSurprise.success()
return true
}
return true
return handleUnwrap()
})
)
if (errors.filter((ok) => !ok).length > 0) {
if (
results.some(
(result) => result.status === "rejected" || result.value === false
)
) {
return null
}
getUnwrapSurpriseSuccessCounter.add(1)
return true
}),
redeem: protectedProcedure
.input(rewardsRedeemInput)
.mutation(async ({ input, ctx }) => {
getRedeemCounter.add(1)
const { rewardId, couponCode } = input
const getRedeemCounter = createCounter(
"trpc.contentstack",
"reward.redeem"
)
const metricGetRedeem = getRedeemCounter.init({ rewardId, couponCode })
metricGetRedeem.start()
const apiResponse = await api.post(
api.endpoints.v1.Profile.Reward.redeem,
{
@@ -417,29 +390,11 @@ export const rewardQueryRouter = router({
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getRedeemFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.redeem error ",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
metricGetRedeem.httpError(apiResponse)
return null
}
getRedeemSuccessCounter.add(1)
metricGetRedeem.success()
return true
}),