From f6aa35d0a2a72d4e6e5db5706d3dea9613827e90 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Mon, 3 Nov 2025 15:50:34 +0530 Subject: [PATCH 1/9] feat: distributed tracing 2 --- apps/web/pages/api/book/event.ts | 20 +++++++++- packages/lib/server/defaultResponder.test.ts | 24 ++++++++++-- packages/lib/server/defaultResponder.ts | 40 +++++++++++++------- packages/trpc/server/createContext.ts | 14 ++++++- 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 7f5aa63d875e07..de45ccbc5e653e 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -10,12 +10,23 @@ import getIP from "@calcom/lib/getIP"; import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; +import type { TraceContext } from "@calcom/lib/tracing"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; +import { prisma } from "@calcom/prisma"; import { CreationSource } from "@calcom/prisma/enums"; -async function handler(req: NextApiRequest & { userId?: number }) { +async function handler(req: NextApiRequest & { userId?: number; traceContext: TraceContext }) { const userIp = getIP(req); + const traceContext = distributedTracing.updateTrace(req.traceContext, { + eventTypeId: req.body?.eventTypeId?.toString() || "null", + }); + const tracingLogger = distributedTracing.getTracingLogger(traceContext); + + tracingLogger.info("API book event request started", { + eventTypeId: req.body?.eventTypeId, + }); + if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") { await checkCfTurnstileToken({ token: req.body["cfToken"] as string, @@ -52,8 +63,13 @@ async function handler(req: NextApiRequest & { userId?: number }) { userId: session?.user?.id || -1, hostname: req.headers.host || "", forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, + traceContext, }, }); + + tracingLogger.info("API book event request completed successfully", { + bookingUid: booking?.uid, + }); // const booking = await createBookingThroughFactory(); return booking; diff --git a/packages/lib/server/defaultResponder.test.ts b/packages/lib/server/defaultResponder.test.ts index 24926ad316159f..8f4b967241cec5 100644 --- a/packages/lib/server/defaultResponder.test.ts +++ b/packages/lib/server/defaultResponder.test.ts @@ -11,7 +11,11 @@ describe("defaultResponder", () => { it("should call res.json when response is still writable and result is not null", async () => { const f = vi.fn().mockResolvedValue({}); const req = {} as NextApiRequest; - const res = { json: vi.fn(), writableEnded: false } as unknown as NextApiResponse; + const res = { + json: vi.fn(), + writableEnded: false, + setHeader: vi.fn(), + } as unknown as NextApiResponse; await defaultResponder(f)(req, res); expect(res.json).toHaveBeenCalled(); }); @@ -19,21 +23,33 @@ describe("defaultResponder", () => { it("should not call res.json when response is not writable", async () => { const f = vi.fn().mockResolvedValue({}); const req = {} as NextApiRequest; - const res = { json: vi.fn(), writableEnded: true } as unknown as NextApiResponse; + const res = { + json: vi.fn(), + writableEnded: true, + setHeader: vi.fn(), + } as unknown as NextApiResponse; await defaultResponder(f)(req, res); expect(res.json).not.toHaveBeenCalled(); }); it("should respond with status code 409 for NoAvailableUsersFound", async () => { const f = vi.fn().mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound)); const req = {} as NextApiRequest; - const res = { status: vi.fn().mockReturnThis(), json: vi.fn() } as unknown as NextApiResponse; + const res = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + setHeader: vi.fn(), + } as unknown as NextApiResponse; await defaultResponder(f)(req, res); expect(res.status).toHaveBeenCalledWith(409); }); it("Rate limit should respond with a 429 status code", async () => { const f = vi.fn().mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" })); const req = {} as NextApiRequest; - const res = { status: vi.fn().mockReturnThis(), json: vi.fn() } as unknown as NextApiResponse; + const res = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + setHeader: vi.fn(), + } as unknown as NextApiResponse; await defaultResponder(f)(req, res); expect(res.status).toHaveBeenCalledWith(429); }); diff --git a/packages/lib/server/defaultResponder.ts b/packages/lib/server/defaultResponder.ts index 2fc4de3045eb77..f06125f1f80489 100644 --- a/packages/lib/server/defaultResponder.ts +++ b/packages/lib/server/defaultResponder.ts @@ -1,13 +1,17 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { TRPCError } from "@trpc/server"; -import { getHTTPStatusCodeFromError } from "@trpc/server/http"; +import { type TraceContext } from "@calcom/lib/tracing"; +import { TracedError } from "@calcom/lib/tracing/error"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; -import { HttpError } from "../http-error"; import { getServerErrorFromUnknown } from "./getServerErrorFromUnknown"; import { performance } from "./perfObserver"; -type Handle = (req: NextApiRequest, res: NextApiResponse) => Promise; +export interface TracedRequest extends NextApiRequest { + traceContext: TraceContext; +} + +type Handle = (req: TracedRequest, res: NextApiResponse) => Promise; /** Allows us to get type inference from API handler responses */ export function defaultResponder( @@ -17,35 +21,45 @@ export function defaultResponder( ) { return async (req: NextApiRequest, res: NextApiResponse) => { let ok = false; + const operation = endpointRoute ? endpointRoute.replace(/^\//, "").replace(/\//g, "_") : "api_request"; + const traceContext = distributedTracing.createTrace(operation, { + meta: { + method: req.method || "", + url: req.url || "", + }, + }); + const tracingLogger = distributedTracing.getTracingLogger(traceContext); + + const tracedReq = req as TracedRequest; + tracedReq.traceContext = traceContext; + try { performance.mark("Start"); let result: T | undefined; if (process.env.NODE_ENV === "development" || !endpointRoute) { - result = await f(req, res); + result = await f(tracedReq, res); } else { const { wrapApiHandlerWithSentry } = await import("@sentry/nextjs"); - result = await wrapApiHandlerWithSentry(f, endpointRoute)(req, res); + result = await wrapApiHandlerWithSentry(f, endpointRoute)(tracedReq, res); } ok = true; if (result && !res.writableEnded) { + res.setHeader("X-Trace-Id", traceContext.traceId); return res.json(result); } } catch (err) { - let error: HttpError; - if (err instanceof TRPCError) { - const statusCode = getHTTPStatusCodeFromError(err); - error = new HttpError({ statusCode, message: err.message }); - } else { - error = getServerErrorFromUnknown(err); - } + tracingLogger.error(`${operation} request failed`, { error: err }); + const tracedError = TracedError.createFromError(err, traceContext); + const error = getServerErrorFromUnknown(tracedError); // we don't want to report Bad Request errors to Sentry / console if (!(error.statusCode >= 400 && error.statusCode < 500)) { console.error(error); const { captureException } = await import("@sentry/nextjs"); captureException(error); } + res.setHeader("X-Trace-Id", traceContext.traceId); return res .status(error.statusCode) .json({ message: error.message, url: error.url, method: error.method, data: error?.data || null }); diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 7b3affa5ddb250..007d0347a2197e 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -5,7 +5,9 @@ import type { serverSideTranslations } from "next-i18next/serverSideTranslations import { getLocale } from "@calcom/features/auth/lib/getLocale"; import getIP from "@calcom/lib/getIP"; -import prisma, { readonlyPrisma } from "@calcom/prisma"; +import type { TraceContext } from "@calcom/lib/tracing"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; +import { prisma, readonlyPrisma } from "@calcom/prisma"; import type { SelectedCalendar, User as PrismaUser } from "@calcom/prisma/client"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; @@ -50,6 +52,7 @@ export type GetSessionFn = export type InnerContext = CreateInnerContextOptions & { prisma: typeof prisma; insightsDb: typeof readonlyPrisma; + traceContext: TraceContext; }; /** @@ -62,10 +65,17 @@ export type InnerContext = CreateInnerContextOptions & { * @see https://trpc.io/docs/context#inner-and-outer-context */ export async function createContextInner(opts: CreateInnerContextOptions): Promise { + const traceContext = distributedTracing.createTrace("trpc_request", { + meta: { + userId: opts.session?.user?.id?.toString() || "anonymous", + }, + }); + return { prisma, insightsDb: readonlyPrisma, ...opts, + traceContext, }; } @@ -87,7 +97,7 @@ export const createContext = async ( // This type may not be accurate if this request is coming from SSG init but they both should satisfy the requirements of getIP. // TODO: @sean - figure out a way to make getIP be happy with trpc req. params const sourceIp = getIP(req as NextApiRequest); - const session = !!sessionGetter ? await sessionGetter({ req, res }) : null; + const session = sessionGetter ? await sessionGetter({ req, res }) : null; const contextInner = await createContextInner({ locale, session, sourceIp }); return { ...contextInner, From 8ac527ac932e595e725a0cfeaf19b701cf6e9f9a Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Mon, 3 Nov 2025 19:38:46 +0530 Subject: [PATCH 2/9] feat: distributed tracing 2 --- packages/features/bookings/lib/dto/types.d.ts | 2 + .../lib/service/RegularBookingService.ts | 178 +++++++++++------- 2 files changed, 116 insertions(+), 64 deletions(-) diff --git a/packages/features/bookings/lib/dto/types.d.ts b/packages/features/bookings/lib/dto/types.d.ts index 386b71e99305a5..75e6efc10bd05a 100644 --- a/packages/features/bookings/lib/dto/types.d.ts +++ b/packages/features/bookings/lib/dto/types.d.ts @@ -4,6 +4,7 @@ */ import type getBookingDataSchema from "@calcom/features/bookings/lib/getBookingDataSchema"; import type getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi"; +import type { TraceContext } from "@calcom/lib/tracing"; import type { SchedulingType } from "@calcom/prisma/enums"; import type { ExtendedBookingCreateBody } from "../bookingCreateBodySchema"; @@ -37,6 +38,7 @@ export type CreateBookingMeta = { hostname?: string; forcedSlug?: string; noEmail?: boolean; + traceContext?: TraceContext; } & PlatformParams; export type BookingHandlerInput = { diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index e5d00571302712..6b14ae74993fcc 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -66,12 +66,11 @@ import { extractBaseEmail } from "@calcom/lib/extract-base-email"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; -import { criticalLogger } from "@calcom/lib/logger.server"; import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; import type { PrismaClient } from "@calcom/prisma"; import type { DestinationCalendar, Prisma, User, AssignmentReasonEnum } from "@calcom/prisma/client"; import { @@ -115,7 +114,6 @@ import { getSeatedBooking } from "../handleNewBooking/getSeatedBooking"; import { getVideoCallDetails } from "../handleNewBooking/getVideoCallDetails"; import { handleAppsStatus } from "../handleNewBooking/handleAppsStatus"; import { loadAndValidateUsers } from "../handleNewBooking/loadAndValidateUsers"; -import { createLoggerWithEventDetails } from "../handleNewBooking/logger"; import { getOriginalRescheduledBooking } from "../handleNewBooking/originalRescheduledBookingUtils"; import type { BookingType } from "../handleNewBooking/originalRescheduledBookingUtils"; import { scheduleNoShowTriggers } from "../handleNewBooking/scheduleNoShowTriggers"; @@ -126,7 +124,6 @@ import handleSeats from "../handleSeats/handleSeats"; import type { IBookingService } from "../interfaces/IBookingService"; const translator = short(); -const log = logger.getSubLogger({ prefix: ["[api] book:user"] }); type IsFixedAwareUserWithCredentials = Omit & { credentials: CredentialForCalendarService[]; @@ -479,8 +476,45 @@ async function handler( skipAvailabilityCheck = false, skipEventLimitsCheck = false, skipCalendarSyncTaskCreation = false, + traceContext: passedTraceContext, } = input; + const { + /** + * TODO: In a followup PR, we aim to remove prisma dependency and instead inject the repositories as dependencies. + * This would require moving multiple queries to appropriate repositories. + */ + prismaClient: prisma, + bookingRepository, + userRepository, + cacheService, + checkBookingAndDurationLimitsService, + luckyUserService, + } = deps; + + const traceContext = passedTraceContext + ? distributedTracing.updateTrace(passedTraceContext, { + eventTypeId: rawBookingData.eventTypeId?.toString() || "null", + userId: userId?.toString() || "null", + eventTypeSlug: rawBookingData.eventTypeSlug || "unknown", + }) + : distributedTracing.createTrace("booking_creation", { + meta: { + eventTypeId: rawBookingData.eventTypeId?.toString() || "null", + userId: userId?.toString() || "null", + eventTypeSlug: rawBookingData.eventTypeSlug || "unknown", + }, + }); + + const tracingLogger = distributedTracing.getTracingLogger(traceContext); + + tracingLogger.info("Booking creation started", { + eventTypeId: rawBookingData.eventTypeId, + userId: userId, + rescheduleUid: rawBookingData.rescheduleUid, + isPlatformBooking: !!platformClientId, + }); + const isPlatformBooking = !!platformClientId; const eventType = await getEventType({ @@ -520,7 +554,7 @@ async function handler( reroutingFormResponses, routingFormResponseId, _isDryRun: isDryRun = false, - _shouldServeCache: shouldServeCache = false, + _shouldServeCache, ...reqBody } = bookingData; @@ -528,8 +562,7 @@ async function handler( eventType, }); - const loggerWithEventDetails = createLoggerWithEventDetails(eventTypeId, reqBody.user, eventTypeSlug); - const emailsAndSmsHandler = new BookingEmailSmsHandler({ logger: loggerWithEventDetails }); + const emailsAndSmsHandler = new BookingEmailSmsHandler({ logger: tracingLogger }); try { await checkIfBookerEmailIsBlocked({ @@ -566,6 +599,7 @@ async function handler( throw new HttpError({ statusCode: 400, message: "email_verification_required", + data: { traceId: traceContext.traceId }, }); } @@ -575,12 +609,13 @@ async function handler( throw new HttpError({ statusCode: 400, message: "invalid_verification_code", + data: { traceId: traceContext.traceId }, }); } } if (isEventTypeLoggingEnabled({ eventTypeId, usernameOrTeamName: reqBody.user })) { - logger.settings.minLevel = 0; + tracingLogger.settings.minLevel = 0; } const fullName = getFullName(bookerName); @@ -588,12 +623,18 @@ async function handler( const tGuests = await getTranslation("en", "common"); const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user); - if (!eventType) throw new HttpError({ statusCode: 404, message: "event_type_not_found" }); + if (!eventType) + throw new HttpError({ + statusCode: 404, + message: "event_type_not_found", + data: { traceId: traceContext.traceId }, + }); if (eventType.seatsPerTimeSlot && eventType.recurringEvent) { throw new HttpError({ statusCode: 400, message: "recurring_event_seats_error", + data: { traceId: traceContext.traceId }, }); } @@ -628,7 +669,7 @@ async function handler( ) { const requiresPayment = !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0; - const existingBooking = await deps.bookingRepository.getValidBookingFromEventTypeForAttendee({ + const existingBooking = await bookingRepository.getValidBookingFromEventTypeForAttendee({ eventTypeId, bookerEmail, bookerPhoneNumber, @@ -665,12 +706,12 @@ async function handler( } } - // const shouldServeCache = await cacheService.getShouldServeCache(_shouldServeCache, eventType.team?.id); + const shouldServeCache = await cacheService.getShouldServeCache(_shouldServeCache, eventType.team?.id); const isTeamEventType = !!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType); - loggerWithEventDetails.info( + tracingLogger.info( `Booking eventType ${eventTypeId} started`, safeStringify({ reqBody: { @@ -705,7 +746,7 @@ async function handler( reqBody.timeZone, eventType, eventTimeZone, - loggerWithEventDetails + tracingLogger ); validateEventLength({ @@ -713,7 +754,7 @@ async function handler( reqBodyEnd: reqBody.end, eventTypeMultipleDuration: eventType.metadata?.multipleDuration, eventTypeLength: eventType.length, - logger: loggerWithEventDetails, + logger: tracingLogger, }); const contactOwnerFromReq = reqBody.teamMemberEmail ?? null; @@ -732,9 +773,13 @@ async function handler( if (routedTeamMemberIds) { //routingFormResponseId could be 0 for dry run. So, we just avoid undefined value if (routingFormResponseId === undefined) { - throw new HttpError({ statusCode: 400, message: "Missing routingFormResponseId" }); + throw new HttpError({ + statusCode: 400, + message: "Missing routingFormResponseId", + data: { traceId: traceContext.traceId }, + }); } - routingFormResponse = await deps.prismaClient.app_RoutingForms_FormResponse.findUnique({ + routingFormResponse = await prisma.app_RoutingForms_FormResponse.findUnique({ where: { id: routingFormResponseId, }, @@ -758,7 +803,7 @@ async function handler( eventType, eventTypeId, dynamicUserList, - logger: loggerWithEventDetails, + logger: tracingLogger, routedTeamMemberIds: routedTeamMemberIds ?? null, contactOwnerEmail, rescheduleUid: reqBody.rescheduleUid || null, @@ -777,7 +822,7 @@ async function handler( }); if (!skipEventLimitsCheck) { - await deps.checkBookingAndDurationLimitsService.checkBookingAndDurationLimits({ + await checkBookingAndDurationLimitsService.checkBookingAndDurationLimits({ eventType, reqBodyStart: reqBody.start, reqBodyRescheduleUid: reqBody.rescheduleUid, @@ -789,7 +834,7 @@ async function handler( let availableUsers: IsFixedAwareUser[] = []; if (eventType.seatsPerTimeSlot) { - const booking = await deps.prismaClient.booking.findFirst({ + const booking = await prisma.booking.findFirst({ where: { eventTypeId: eventType.id, startTime: new Date(dayjs(reqBody.start).utc().format()), @@ -869,7 +914,7 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking: originalRescheduledBooking ?? null, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } @@ -884,7 +929,7 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } @@ -903,7 +948,7 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } else { @@ -911,7 +956,7 @@ async function handler( } } catch { if (additionalFallbackRRUsers.length) { - loggerWithEventDetails.debug( + tracingLogger.debug( "Qualified users not available, check for fallback users", safeStringify({ qualifiedRRUsers: qualifiedRRUsers.map((user) => user.id), @@ -932,14 +977,14 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } else { availableUsers = [...additionalFallbackRRUsers, ...fixedUsers] as IsFixedAwareUser[]; } } else { - loggerWithEventDetails.debug( + tracingLogger.debug( "Qualified users not available, no fallback users", safeStringify({ qualifiedRRUsers: qualifiedRRUsers.map((user) => user.id), @@ -968,7 +1013,7 @@ async function handler( const notAvailableLuckyUsers: typeof users = []; - loggerWithEventDetails.debug( + tracingLogger.debug( "Computed available users", safeStringify({ availableUsers: availableUsers.map((user) => user.id), @@ -997,7 +1042,7 @@ async function handler( memberId: eventTypeWithUsers.users[0].id ?? null, teamId: eventType.teamId, }); - const newLuckyUser = await deps.luckyUserService.getLuckyUser({ + const newLuckyUser = await luckyUserService.getLuckyUser({ // find a lucky user that is not already in the luckyUsers array availableUsers: freeUsers, // only hosts from the same group @@ -1045,7 +1090,7 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } @@ -1055,7 +1100,7 @@ async function handler( luckUserFound = true; } catch { notAvailableLuckyUsers.push(newLuckyUser); - loggerWithEventDetails.info( + tracingLogger.info( `Round robin host ${newLuckyUser.name} not available for first two slots. Trying to find another host.` ); } @@ -1120,7 +1165,7 @@ async function handler( } if (users.length === 0 && eventType.schedulingType === SchedulingType.ROUND_ROBIN) { - loggerWithEventDetails.error(`No available users found for round robin event.`); + tracingLogger.error(`No available users found for round robin event.`); throw new Error(ErrorCode.RoundRobinHostsUnavailableForBooking); } @@ -1194,7 +1239,7 @@ async function handler( : []; const guestEmails = (reqGuests || []).map((email) => extractBaseEmail(email).toLowerCase()); - const guestUsers = await deps.userRepository.findManyByEmailsWithEmailVerificationSettings({ + const guestUsers = await userRepository.findManyByEmailsWithEmailVerificationSettings({ emails: guestEmails, }); @@ -1234,7 +1279,7 @@ async function handler( }, [] as Invitee); if (guestsRemoved.length > 0) { - log.info("Removed guests from the booking", guestsRemoved); + tracingLogger.info("Removed guests from the booking", guestsRemoved); } const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`; @@ -1249,8 +1294,8 @@ async function handler( } : getLocationValueForDB(locationBodyString, eventType.locations); - log.info("locationBodyString", locationBodyString); - log.info("event type locations", eventType.locations); + tracingLogger.info("locationBodyString", locationBodyString); + tracingLogger.info("event type locations", eventType.locations); const customInputs = getCustomInputsResponses(reqBody, eventType.customInputs); const attendeesList = [...invitee, ...guests]; @@ -1278,7 +1323,7 @@ async function handler( }); // For bookings made before introducing iCalSequence, assume that the sequence should start at 1. For new bookings start at 0. const iCalSequence = getICalSequence(originalRescheduledBooking); - const organizerOrganizationProfile = await deps.prismaClient.profile.findFirst({ + const organizerOrganizationProfile = await prisma.profile.findFirst({ where: { userId: organizerUser.id, }, @@ -1664,7 +1709,7 @@ async function handler( let booking: CreatedBooking | null = null; - loggerWithEventDetails.debug( + tracingLogger.debug( "Going to create booking in DB now", safeStringify({ organizerUser: organizerUser.id, @@ -1714,7 +1759,7 @@ async function handler( await usersRepository.updateLastActiveAt(booking.userId); const organizerUserAvailability = availableUsers.find((user) => user.id === booking?.userId); - criticalLogger.info(`Booking created`, { + tracingLogger.info(`Booking created`, { bookingUid: booking.uid, selectedCalendarIds: organizerUser.allSelectedCalendars?.map((c) => c.id) ?? [], availabilitySnapshot: organizerUserAvailability?.availabilityData @@ -1782,7 +1827,7 @@ async function handler( // Save description to bookingSeat const uniqueAttendeeId = uuid(); - await deps.prismaClient.bookingSeat.create({ + await prisma.bookingSeat.create({ data: { referenceUid: uniqueAttendeeId, data: { @@ -1825,13 +1870,13 @@ async function handler( } } catch (_err) { const err = getErrorFromUnknown(_err); - loggerWithEventDetails.error( - `Booking ${eventTypeId} failed`, - "Error when saving booking to db", - err.message - ); + tracingLogger.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message); if (err.code === "P2002") { - throw new HttpError({ statusCode: 409, message: ErrorCode.BookingConflict }); + throw new HttpError({ + statusCode: 409, + message: ErrorCode.BookingConflict, + data: { traceId: traceContext.traceId }, + }); } throw err; } @@ -1848,7 +1893,7 @@ async function handler( // this is the actual rescheduling logic if (!eventType.seatsPerTimeSlot && originalRescheduledBooking?.uid) { - log.silly("Rescheduling booking", originalRescheduledBooking.uid); + tracingLogger.silly("Rescheduling booking", originalRescheduledBooking.uid); // cancel workflow reminders from previous rescheduled booking await WorkflowRepository.deleteAllWorkflowReminders(originalRescheduledBooking.workflowReminders); @@ -1879,7 +1924,7 @@ async function handler( { ...originalRescheduledBooking.user, credentials: refreshedOriginalHostCredentials }, apps ); - log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking"); + tracingLogger.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking"); // Create deletion event with original host's organizer info and original booking properties const deletionEvent = { ...evt, @@ -1950,7 +1995,7 @@ async function handler( message: "Booking Rescheduling failed", }; - loggerWithEventDetails.error( + tracingLogger.error( `EventManager.reschedule failure in some of the integrations ${organizerUser.username}`, safeStringify({ error, results }) ); @@ -1973,7 +2018,7 @@ async function handler( const googleCalResult = results[googleCalIndex]; if (!googleCalResult) { - loggerWithEventDetails.warn("Google Calendar not installed but using Google Meet as location"); + tracingLogger.warn("Google Calendar not installed but using Google Meet as location"); results.push({ ...googleMeetResult, success: false, @@ -2075,9 +2120,10 @@ async function handler( const error = { errorCode: "BookingCreatingMeetingFailed", message: "Booking failed", + data: { traceId: traceContext.traceId }, }; - loggerWithEventDetails.error( + tracingLogger.error( `EventManager.create failure in some of the integrations ${organizerUser.username}`, safeStringify({ error, results }) ); @@ -2102,7 +2148,7 @@ async function handler( const googleCalResult = results[googleCalIndex]; if (!googleCalResult) { - loggerWithEventDetails.warn("Google Calendar not installed but using Google Meet as location"); + tracingLogger.warn("Google Calendar not installed but using Google Meet as location"); results.push({ ...googleMeetResult, success: false, @@ -2148,7 +2194,7 @@ async function handler( if (!isDryRun && evt.iCalUID !== booking.iCalUID) { // The eventManager could change the iCalUID. At this point we can update the DB record - await deps.prismaClient.booking.update({ + await prisma.booking.update({ where: { id: booking.id, }, @@ -2180,7 +2226,7 @@ async function handler( } } else { // If isConfirmedByDefault is false, then booking can't be considered ACCEPTED and thus EventManager has no role to play. Booking is created as PENDING - loggerWithEventDetails.debug( + tracingLogger.debug( `EventManager doesn't need to create or reschedule event for booking ${organizerUser.username}`, safeStringify({ calEvent: getPiiFreeCalendarEvent(evt), @@ -2197,7 +2243,7 @@ async function handler( !!booking; if (!isConfirmedByDefault && noEmail !== true && !bookingRequiresPayment) { - loggerWithEventDetails.debug( + tracingLogger.debug( `Emails: Booking ${organizerUser.username} requires confirmation, sending request emails`, safeStringify({ calEvent: getPiiFreeCalendarEvent(evt), @@ -2237,7 +2283,7 @@ async function handler( const bookingRescheduledPayload = bookingCreatedPayload; const bookingEventHandler = new BookingEventHandlerService({ - log: loggerWithEventDetails, + log: tracingLogger, hashedLinkService: deps.hashedLinkService, }); @@ -2269,9 +2315,9 @@ async function handler( }; if (bookingRequiresPayment) { - loggerWithEventDetails.debug(`Booking ${organizerUser.username} requires payment`); + tracingLogger.debug(`Booking ${organizerUser.username} requires payment`); // Load credentials.app.categories - const credentialPaymentAppCategories = await deps.prismaClient.credential.findMany({ + const credentialPaymentAppCategories = await prisma.credential.findMany({ where: { ...(paymentAppData.credentialId ? { id: paymentAppData.credentialId } : { userId: organizerUser.id }), app: { @@ -2296,7 +2342,11 @@ async function handler( }); if (!eventTypePaymentAppCredential) { - throw new HttpError({ statusCode: 400, message: "Missing payment credentials" }); + throw new HttpError({ + statusCode: 400, + message: "Missing payment credentials", + data: { traceId: traceContext.traceId }, + }); } // Convert type of eventTypePaymentAppCredential to appId: EventTypeAppList @@ -2364,7 +2414,7 @@ async function handler( }); } } catch (error) { - loggerWithEventDetails.error( + tracingLogger.error( "Error while scheduling workflow reminders for booking payment initiated", JSON.stringify({ error }) ); @@ -2395,7 +2445,7 @@ async function handler( }; } - loggerWithEventDetails.debug(`Booking ${organizerUser.username} completed`); + tracingLogger.debug(`Booking ${organizerUser.username} completed`); // We are here so, booking doesn't require payment and booking is also created in DB already, through createBooking call if (isConfirmedByDefault) { @@ -2457,7 +2507,7 @@ async function handler( const failures = scheduledTriggerResults.filter((result) => result.status === "rejected"); if (failures.length > 0) { - loggerWithEventDetails.error( + tracingLogger.error( "Error while scheduling or canceling webhook triggers", safeStringify({ errors: failures.map((f) => f.reason), @@ -2489,7 +2539,7 @@ async function handler( try { if (!isDryRun) { - await deps.prismaClient.booking.update({ + await prisma.booking.update({ where: { uid: booking.uid, }, @@ -2505,7 +2555,7 @@ async function handler( }); } } catch (error) { - loggerWithEventDetails.error("Error while creating booking references", JSON.stringify({ error })); + tracingLogger.error("Error while creating booking references", JSON.stringify({ error })); } const evtWithMetadata = { @@ -2541,7 +2591,7 @@ async function handler( isRescheduleEvent: !!rescheduleUid, }); } catch (error) { - loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error })); + tracingLogger.error("Error while scheduling workflow reminders", JSON.stringify({ error })); } try { @@ -2562,7 +2612,7 @@ async function handler( }); } } catch (error) { - loggerWithEventDetails.error("Error while scheduling no show triggers", JSON.stringify({ error })); + tracingLogger.error("Error while scheduling no show triggers", JSON.stringify({ error })); } if (!isDryRun) { From fef102773cc4a7178578f6a5d031c4e3625faefa Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 6 Nov 2025 14:20:55 +0530 Subject: [PATCH 3/9] refactor: feedback --- apps/web/pages/api/book/event.ts | 8 +-- .../lib/service/RegularBookingService.ts | 61 ++++++------------- packages/lib/tracing/index.ts | 23 +++++-- 3 files changed, 37 insertions(+), 55 deletions(-) diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index de45ccbc5e653e..8df4fd90ac8fb6 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -18,10 +18,9 @@ import { CreationSource } from "@calcom/prisma/enums"; async function handler(req: NextApiRequest & { userId?: number; traceContext: TraceContext }) { const userIp = getIP(req); - const traceContext = distributedTracing.updateTrace(req.traceContext, { - eventTypeId: req.body?.eventTypeId?.toString() || "null", + const tracingLogger = distributedTracing.getTracingLogger(req.traceContext, { + eventTypeId: req.body?.eventTypeId, }); - const tracingLogger = distributedTracing.getTracingLogger(traceContext); tracingLogger.info("API book event request started", { eventTypeId: req.body?.eventTypeId, @@ -67,9 +66,6 @@ async function handler(req: NextApiRequest & { userId?: number; traceContext: Tr }, }); - tracingLogger.info("API book event request completed successfully", { - bookingUid: booking?.uid, - }); // const booking = await createBookingThroughFactory(); return booking; diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 6b14ae74993fcc..7c5aa0ed249bb2 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -66,6 +66,7 @@ import { extractBaseEmail } from "@calcom/lib/extract-base-email"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { HttpError } from "@calcom/lib/http-error"; +import { criticalLogger } from "@calcom/lib/logger.server"; import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; @@ -479,40 +480,14 @@ async function handler( traceContext: passedTraceContext, } = input; - const { - /** - * TODO: In a followup PR, we aim to remove prisma dependency and instead inject the repositories as dependencies. - * This would require moving multiple queries to appropriate repositories. - */ - prismaClient: prisma, - bookingRepository, - userRepository, - cacheService, - checkBookingAndDurationLimitsService, - luckyUserService, - } = deps; - const traceContext = passedTraceContext - ? distributedTracing.updateTrace(passedTraceContext, { - eventTypeId: rawBookingData.eventTypeId?.toString() || "null", - userId: userId?.toString() || "null", - eventTypeSlug: rawBookingData.eventTypeSlug || "unknown", - }) - : distributedTracing.createTrace("booking_creation", { - meta: { - eventTypeId: rawBookingData.eventTypeId?.toString() || "null", - userId: userId?.toString() || "null", - eventTypeSlug: rawBookingData.eventTypeSlug || "unknown", - }, - }); + ? passedTraceContext + : distributedTracing.createTrace("booking_creation"); - const tracingLogger = distributedTracing.getTracingLogger(traceContext); - - tracingLogger.info("Booking creation started", { + const tracingLogger = distributedTracing.getTracingLogger(traceContext, { eventTypeId: rawBookingData.eventTypeId, userId: userId, - rescheduleUid: rawBookingData.rescheduleUid, - isPlatformBooking: !!platformClientId, + eventTypeSlug: rawBookingData.eventTypeSlug, }); const isPlatformBooking = !!platformClientId; @@ -669,7 +644,7 @@ async function handler( ) { const requiresPayment = !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0; - const existingBooking = await bookingRepository.getValidBookingFromEventTypeForAttendee({ + const existingBooking = await deps.bookingRepository.getValidBookingFromEventTypeForAttendee({ eventTypeId, bookerEmail, bookerPhoneNumber, @@ -706,11 +681,11 @@ async function handler( } } - const shouldServeCache = await cacheService.getShouldServeCache(_shouldServeCache, eventType.team?.id); - const isTeamEventType = !!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType); + const shouldServeCache = false; + tracingLogger.info( `Booking eventType ${eventTypeId} started`, safeStringify({ @@ -779,7 +754,7 @@ async function handler( data: { traceId: traceContext.traceId }, }); } - routingFormResponse = await prisma.app_RoutingForms_FormResponse.findUnique({ + routingFormResponse = await deps.prismaClient.app_RoutingForms_FormResponse.findUnique({ where: { id: routingFormResponseId, }, @@ -822,7 +797,7 @@ async function handler( }); if (!skipEventLimitsCheck) { - await checkBookingAndDurationLimitsService.checkBookingAndDurationLimits({ + await deps.checkBookingAndDurationLimitsService.checkBookingAndDurationLimits({ eventType, reqBodyStart: reqBody.start, reqBodyRescheduleUid: reqBody.rescheduleUid, @@ -834,7 +809,7 @@ async function handler( let availableUsers: IsFixedAwareUser[] = []; if (eventType.seatsPerTimeSlot) { - const booking = await prisma.booking.findFirst({ + const booking = await deps.prismaClient.booking.findFirst({ where: { eventTypeId: eventType.id, startTime: new Date(dayjs(reqBody.start).utc().format()), @@ -1042,7 +1017,7 @@ async function handler( memberId: eventTypeWithUsers.users[0].id ?? null, teamId: eventType.teamId, }); - const newLuckyUser = await luckyUserService.getLuckyUser({ + const newLuckyUser = await deps.luckyUserService.getLuckyUser({ // find a lucky user that is not already in the luckyUsers array availableUsers: freeUsers, // only hosts from the same group @@ -1239,7 +1214,7 @@ async function handler( : []; const guestEmails = (reqGuests || []).map((email) => extractBaseEmail(email).toLowerCase()); - const guestUsers = await userRepository.findManyByEmailsWithEmailVerificationSettings({ + const guestUsers = await deps.userRepository.findManyByEmailsWithEmailVerificationSettings({ emails: guestEmails, }); @@ -1323,7 +1298,7 @@ async function handler( }); // For bookings made before introducing iCalSequence, assume that the sequence should start at 1. For new bookings start at 0. const iCalSequence = getICalSequence(originalRescheduledBooking); - const organizerOrganizationProfile = await prisma.profile.findFirst({ + const organizerOrganizationProfile = await deps.prismaClient.profile.findFirst({ where: { userId: organizerUser.id, }, @@ -1759,7 +1734,7 @@ async function handler( await usersRepository.updateLastActiveAt(booking.userId); const organizerUserAvailability = availableUsers.find((user) => user.id === booking?.userId); - tracingLogger.info(`Booking created`, { + criticalLogger.info(`Booking created`, { bookingUid: booking.uid, selectedCalendarIds: organizerUser.allSelectedCalendars?.map((c) => c.id) ?? [], availabilitySnapshot: organizerUserAvailability?.availabilityData @@ -2194,7 +2169,7 @@ async function handler( if (!isDryRun && evt.iCalUID !== booking.iCalUID) { // The eventManager could change the iCalUID. At this point we can update the DB record - await prisma.booking.update({ + await deps.prismaClient.booking.update({ where: { id: booking.id, }, @@ -2317,7 +2292,7 @@ async function handler( if (bookingRequiresPayment) { tracingLogger.debug(`Booking ${organizerUser.username} requires payment`); // Load credentials.app.categories - const credentialPaymentAppCategories = await prisma.credential.findMany({ + const credentialPaymentAppCategories = await deps.prismaClient.credential.findMany({ where: { ...(paymentAppData.credentialId ? { id: paymentAppData.credentialId } : { userId: organizerUser.id }), app: { @@ -2539,7 +2514,7 @@ async function handler( try { if (!isDryRun) { - await prisma.booking.update({ + await deps.prismaClient.booking.update({ where: { uid: booking.uid, }, diff --git a/packages/lib/tracing/index.ts b/packages/lib/tracing/index.ts index 73750e72e38d9e..b5431252e6a1fd 100644 --- a/packages/lib/tracing/index.ts +++ b/packages/lib/tracing/index.ts @@ -6,7 +6,7 @@ export interface TraceContext { parentSpanId?: string; operation: string; // So that we don't violate open closed principle, we allow meta to be added to the trace context - meta?: Record; + meta?: Record; } export interface IdGenerator { @@ -31,7 +31,7 @@ export class DistributedTracing { createSpan( parentContext: TraceContext, operation: string, - additionalMeta?: Record + additionalMeta?: Record ): TraceContext { const mergedMeta = { ...parentContext.meta, @@ -47,7 +47,15 @@ export class DistributedTracing { }; } - getTracingLogger(context: TraceContext) { + getTracingLogger( + context: TraceContext, + additionalMeta?: Record + ) { + const mergedMeta = { + ...context.meta, + ...additionalMeta, + }; + const prefixes = [ "distributed-trace", `trace:${context.traceId}`, @@ -55,8 +63,8 @@ export class DistributedTracing { `op:${context.operation}`, ]; - if (context.meta) { - Object.entries(context.meta).forEach(([key, value]) => { + if (mergedMeta) { + Object.entries(mergedMeta).forEach(([key, value]) => { if (value !== undefined && value !== null) { prefixes.push(`${key}:${value}`); } @@ -85,7 +93,10 @@ export class DistributedTracing { }; } - updateTrace(traceContext: TraceContext, additionalMeta?: Record): TraceContext { + updateTrace( + traceContext: TraceContext, + additionalMeta?: Record + ): TraceContext { return { ...traceContext, meta: { From 1cb772a8c878fd1cc2975d20cfe1e048d08482eb Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 6 Nov 2025 14:24:28 +0530 Subject: [PATCH 4/9] refactor: feedback --- apps/web/pages/api/book/event.ts | 11 +---------- .../bookings/lib/service/RegularBookingService.ts | 6 +++--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 8df4fd90ac8fb6..3782520a947207 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -11,21 +11,12 @@ import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import type { TraceContext } from "@calcom/lib/tracing"; -import { distributedTracing } from "@calcom/lib/tracing/factory"; import { prisma } from "@calcom/prisma"; import { CreationSource } from "@calcom/prisma/enums"; async function handler(req: NextApiRequest & { userId?: number; traceContext: TraceContext }) { const userIp = getIP(req); - const tracingLogger = distributedTracing.getTracingLogger(req.traceContext, { - eventTypeId: req.body?.eventTypeId, - }); - - tracingLogger.info("API book event request started", { - eventTypeId: req.body?.eventTypeId, - }); - if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") { await checkCfTurnstileToken({ token: req.body["cfToken"] as string, @@ -62,7 +53,7 @@ async function handler(req: NextApiRequest & { userId?: number; traceContext: Tr userId: session?.user?.id || -1, hostname: req.headers.host || "", forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, - traceContext, + traceContext: req.traceContext, }, }); diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 7c5aa0ed249bb2..d8302855d90c97 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -529,7 +529,7 @@ async function handler( reroutingFormResponses, routingFormResponseId, _isDryRun: isDryRun = false, - _shouldServeCache, + _shouldServeCache: shouldServeCache = false, ...reqBody } = bookingData; @@ -681,11 +681,11 @@ async function handler( } } + // const shouldServeCache = await cacheService.getShouldServeCache(_shouldServeCache, eventType.team?.id); + const isTeamEventType = !!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType); - const shouldServeCache = false; - tracingLogger.info( `Booking eventType ${eventTypeId} started`, safeStringify({ From dc3edc227eeccfe60f70fc06ea9b43cd5a01543d Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 6 Nov 2025 15:35:23 +0530 Subject: [PATCH 5/9] fix: type error --- .../features/bookings/lib/service/RegularBookingService.ts | 2 +- packages/lib/server/defaultResponder.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index d8302855d90c97..dd96f153360245 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -1802,7 +1802,7 @@ async function handler( // Save description to bookingSeat const uniqueAttendeeId = uuid(); - await prisma.bookingSeat.create({ + await deps.prismaClient.bookingSeat.create({ data: { referenceUid: uniqueAttendeeId, data: { diff --git a/packages/lib/server/defaultResponder.ts b/packages/lib/server/defaultResponder.ts index f06125f1f80489..604e5d72b05953 100644 --- a/packages/lib/server/defaultResponder.ts +++ b/packages/lib/server/defaultResponder.ts @@ -52,14 +52,14 @@ export function defaultResponder( } catch (err) { tracingLogger.error(`${operation} request failed`, { error: err }); const tracedError = TracedError.createFromError(err, traceContext); - const error = getServerErrorFromUnknown(tracedError); + const error = getServerErrorFromUnknown(err); // we don't want to report Bad Request errors to Sentry / console if (!(error.statusCode >= 400 && error.statusCode < 500)) { console.error(error); const { captureException } = await import("@sentry/nextjs"); - captureException(error); + captureException(tracedError); } - res.setHeader("X-Trace-Id", traceContext.traceId); + res.setHeader("X-Trace-Id", tracedError.traceId); return res .status(error.statusCode) .json({ message: error.message, url: error.url, method: error.method, data: error?.data || null }); From cdb1cd002e9f84a64fef8496527755d47ad681a7 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 6 Nov 2025 15:39:32 +0530 Subject: [PATCH 6/9] fix: trpc error --- packages/lib/server/defaultResponder.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/lib/server/defaultResponder.ts b/packages/lib/server/defaultResponder.ts index 604e5d72b05953..454a1e767395da 100644 --- a/packages/lib/server/defaultResponder.ts +++ b/packages/lib/server/defaultResponder.ts @@ -1,9 +1,13 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { TRPCError } from "@trpc/server"; +import { getHTTPStatusCodeFromError } from "@trpc/server/http"; + import { type TraceContext } from "@calcom/lib/tracing"; import { TracedError } from "@calcom/lib/tracing/error"; import { distributedTracing } from "@calcom/lib/tracing/factory"; +import { HttpError } from "../http-error"; import { getServerErrorFromUnknown } from "./getServerErrorFromUnknown"; import { performance } from "./perfObserver"; @@ -52,7 +56,13 @@ export function defaultResponder( } catch (err) { tracingLogger.error(`${operation} request failed`, { error: err }); const tracedError = TracedError.createFromError(err, traceContext); - const error = getServerErrorFromUnknown(err); + let error: HttpError; + if (err instanceof TRPCError) { + const statusCode = getHTTPStatusCodeFromError(err); + error = new HttpError({ statusCode, message: err.message }); + } else { + error = getServerErrorFromUnknown(err); + } // we don't want to report Bad Request errors to Sentry / console if (!(error.statusCode >= 400 && error.statusCode < 500)) { console.error(error); From 11b5f70891af78d6fad6ce710c86b70a0157bd53 Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Wed, 12 Nov 2025 15:43:15 +0530 Subject: [PATCH 7/9] feat: distributed tracing - 3 --- .../BookEventForm/BookEventForm.tsx | 6 + .../Booker/components/hooks/useBookings.ts | 6 + .../server/getServerErrorFromUnknown.test.ts | 134 ++++++++++++++++++ .../lib/server/getServerErrorFromUnknown.ts | 60 ++++++-- 4 files changed, 198 insertions(+), 8 deletions(-) diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index a9abddc0cb7b0c..49c10637082c67 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -323,6 +323,12 @@ const getError = ({ return error?.message ? ( <> {responseVercelIdHeader ?? ""} {t(messageKey, { date, count })} + {error.data?.traceId && ( +
+ {t("trace_reference_id")}: + {error.data.traceId} +
+ )} ) : ( <>{t("can_you_try_again")} diff --git a/packages/features/bookings/Booker/components/hooks/useBookings.ts b/packages/features/bookings/Booker/components/hooks/useBookings.ts index 37095979b7bf8e..80877bdcfee917 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookings.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookings.ts @@ -400,6 +400,7 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin const error = err as Error & { data: { rescheduleUid: string; startTime: string; attendees: string[] }; + traceId?: string; }; if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) { @@ -521,6 +522,11 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookin : event?.data?.forwardParamsSuccessRedirect, }); }, + onError: (err, _, ctx) => { + console.error("Error creating recurring booking", err); + // eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- It is only called when user takes an action in embed + bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" }); + }, }); const handleBookEvent = useHandleBookEvent({ diff --git a/packages/lib/server/getServerErrorFromUnknown.test.ts b/packages/lib/server/getServerErrorFromUnknown.test.ts index 10996e37e4abf3..b922312b0b61df 100644 --- a/packages/lib/server/getServerErrorFromUnknown.test.ts +++ b/packages/lib/server/getServerErrorFromUnknown.test.ts @@ -1,9 +1,14 @@ +import { Prisma } from "@calcom/prisma/client"; import { describe, expect, test } from "vitest"; +import { ZodError } from "zod"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { ErrorWithCode } from "@calcom/lib/errors"; +import { TRPCError } from "@trpc/server"; + import { HttpError } from "../http-error"; +import { TracedError } from "../tracing/error"; import { getServerErrorFromUnknown } from "./getServerErrorFromUnknown"; const test400Codes = [ @@ -113,6 +118,135 @@ describe("getServerErrorFromUnknown", () => { }); }); +describe("TracedError handling", () => { + test("should extract traceId and tracedData from TracedError", () => { + const originalError = new Error("Original error message"); + const tracedData = { userId: "123", operation: "booking" }; + const traceContext = { + traceId: "trace_abc123", + spanId: "span_123", + operation: "test_operation", + }; + + const tracedError = new TracedError(originalError, traceContext, tracedData); + + const result = getServerErrorFromUnknown(tracedError); + + expect(result.statusCode).toBe(500); + expect(result.message).toBe("Original error message"); + expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId }); + }); + + test("should handle TracedError wrapping ErrorWithCode", () => { + const originalError = new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found"); + const tracedData = { bookingId: "456" }; + const traceContext = { + traceId: "trace_def456", + spanId: "span_456", + operation: "booking_lookup", + }; + + const tracedError = new TracedError(originalError, traceContext, tracedData); + + const result = getServerErrorFromUnknown(tracedError); + + expect(result.statusCode).toBe(404); + expect(result.message).toBe("Booking not found"); + expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId }); + }); +}); + +describe("TRPCError handling", () => { + test("should handle TRPCError with BAD_REQUEST", () => { + const trpcError = new TRPCError({ + code: "BAD_REQUEST", + message: "Invalid input data", + }); + + const result = getServerErrorFromUnknown(trpcError); + + expect(result.statusCode).toBe(400); + expect(result.message).toBe("Invalid input data"); + expect(result.data).toBeUndefined(); + }); + + test("should handle TracedError wrapping TRPCError", () => { + const trpcError = new TRPCError({ + code: "NOT_FOUND", + message: "Resource not found", + }); + const tracedData = { resourceId: "789" }; + const traceContext = { + traceId: "trace_trpc123", + spanId: "span_trpc123", + operation: "resource_lookup", + }; + + const tracedError = new TracedError(trpcError, traceContext, tracedData); + + const result = getServerErrorFromUnknown(tracedError); + + expect(result.statusCode).toBe(404); + expect(result.message).toBe("Resource not found"); + expect(result.data).toEqual({ ...tracedData, traceId: traceContext.traceId }); + }); +}); + +describe("ZodError handling", () => { + test("should handle ZodError with validation issues", () => { + const zodError = new ZodError([ + { + code: "invalid_type", + expected: "string", + received: "number", + path: ["name"], + message: "Expected string, received number", + }, + ]); + + const result = getServerErrorFromUnknown(zodError); + + expect(result.statusCode).toBe(400); + expect(result.message).toBe("invalid_type in 'name': Expected string, received number"); + expect(result.cause).toBe(zodError); + }); +}); + +describe("Prisma error handling", () => { + test("should handle Prisma P2025 error (record not found)", () => { + const prismaError = new Error("Record to delete does not exist.") as any; + prismaError.code = "P2025"; + prismaError.clientVersion = "5.0.0"; + Object.setPrototypeOf(prismaError, Prisma.PrismaClientKnownRequestError.prototype); + + const result = getServerErrorFromUnknown(prismaError); + + expect(result.statusCode).toBe(404); + expect(result.message).toBe("Record to delete does not exist."); + expect(result.cause).toBe(prismaError); + }); + + test("should handle other Prisma errors as 400", () => { + const prismaError = new Error("Foreign key constraint failed") as any; + prismaError.code = "P2003"; + prismaError.clientVersion = "5.0.0"; + Object.setPrototypeOf(prismaError, Prisma.PrismaClientKnownRequestError.prototype); + + const result = getServerErrorFromUnknown(prismaError); + + expect(result.statusCode).toBe(400); + expect(result.message).toBe("Foreign key constraint failed"); + }); +}); + +test("should handle unknown type fallback", () => { + const result = getServerErrorFromUnknown({ someProperty: "value" }); + + expect(result.statusCode).toBe(500); + expect(result.message).toBe("Unhandled error of type 'object'. Please reach out for our customer support."); + expect(result.data).toBeUndefined(); +}); + test400Codes.forEach((errorCode) => { test(`${errorCode} should return 400 Bad Request`, () => { const error = new ErrorWithCode(errorCode, `Test message for ${errorCode}`); diff --git a/packages/lib/server/getServerErrorFromUnknown.ts b/packages/lib/server/getServerErrorFromUnknown.ts index dcbdae850baaed..514e31e46de4f9 100644 --- a/packages/lib/server/getServerErrorFromUnknown.ts +++ b/packages/lib/server/getServerErrorFromUnknown.ts @@ -5,9 +5,13 @@ import { ErrorCode } from "@calcom/lib/errorCodes"; import { ErrorWithCode } from "@calcom/lib/errors"; import { Prisma } from "@calcom/prisma/client"; +import { TRPCError } from "@trpc/server"; +import { getHTTPStatusCodeFromError } from "@trpc/server/http"; + import { HttpError } from "../http-error"; import { redactError } from "../redactError"; import { stripeInvalidRequestErrorSchema } from "../stripe-error"; +import { TracedError } from "../tracing/error"; function hasName(cause: unknown): cause is { name: string } { return !!cause && typeof cause === "object" && "name" in cause; @@ -34,37 +38,71 @@ function parseZodErrorIssues(issues: ZodIssue[]): string { } export function getServerErrorFromUnknown(cause: unknown): HttpError { + let traceId: string | undefined; + let tracedData: Record | undefined; + + if (cause instanceof TracedError) { + traceId = cause.traceId; + tracedData = cause.data; + cause = cause.originalError; + } + + if (cause instanceof TRPCError) { + const statusCode = getHTTPStatusCodeFromError(cause); + return new HttpError({ + statusCode, + message: cause.message, + data: traceId ? { ...tracedData, traceId } : undefined, + }); + } if (isZodError(cause)) { return new HttpError({ statusCode: 400, message: parseZodErrorIssues(cause.issues), cause, + data: traceId ? { ...tracedData, traceId } : undefined, }); } if (cause instanceof SyntaxError) { return new HttpError({ statusCode: 500, message: "Unexpected error, please reach out for our customer support.", + data: traceId ? { ...tracedData, traceId } : undefined, }); } if (isPrismaError(cause)) { - return getServerErrorFromPrismaError(cause); + const prismaError = getServerErrorFromPrismaError(cause); + return new HttpError({ + statusCode: prismaError.statusCode, + message: prismaError.message, + cause: prismaError.cause, + data: traceId ? { ...tracedData, ...prismaError.data, traceId } : prismaError.data, + }); } const parsedStripeError = stripeInvalidRequestErrorSchema.safeParse(cause); if (parsedStripeError.success) { - return getHttpError({ statusCode: 400, cause: parsedStripeError.data }); + const stripeErrorObj = new Error(parsedStripeError.data.message || "Stripe error"); + stripeErrorObj.name = parsedStripeError.data.type || "StripeInvalidRequestError"; + const stripeError = getHttpError({ statusCode: 400, cause: stripeErrorObj }); + return new HttpError({ + statusCode: stripeError.statusCode, + message: stripeError.message, + cause: stripeError.cause, + data: traceId ? { ...tracedData, ...stripeError.data, traceId } : stripeError.data, + }); } if (cause instanceof ErrorWithCode) { const statusCode = getStatusCode(cause); return new HttpError({ statusCode, message: cause.message ?? "", - data: cause.data, + data: traceId ? { ...cause.data, ...tracedData, traceId } : cause.data, cause, }); } if (cause instanceof HttpError) { const redactedCause = redactError(cause); + const originalData = cause.data; return { ...redactedCause, name: cause.name, @@ -73,22 +111,28 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError { url: cause.url, statusCode: cause.statusCode, method: cause.method, + data: traceId ? { ...originalData, ...tracedData, traceId } : originalData, }; } if (cause instanceof Error) { const statusCode = getStatusCode(cause); - return getHttpError({ statusCode, cause }); - } - if (typeof cause === "string") { + const error = getHttpError({ statusCode, cause }); return new HttpError({ - statusCode: 500, - message: cause, + statusCode: error.statusCode, + message: error.message, + cause: error.cause, + data: traceId ? { ...tracedData, ...error.data, traceId } : error.data, }); } + if (typeof cause === "string") { + // @ts-expect-error https://github.com/tc39/proposal-error-cause + return new Error(cause, { cause }); + } return new HttpError({ statusCode: 500, message: `Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`, + data: traceId ? { ...tracedData, traceId } : tracedData, }); } From b65221074ade8c57fbb2e053da89a325bc0aa07d Mon Sep 17 00:00:00 2001 From: Udit Takkar Date: Thu, 13 Nov 2025 16:50:33 +0530 Subject: [PATCH 8/9] chore: translation --- apps/web/public/static/locales/en/common.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index d4aad4d37e7bb3..f8ed938219ad16 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -30,6 +30,7 @@ "verify_email_change_success_toast": "Updated your email to {{email}}", "verify_email_change_failure_toast": "Failed to update email.", "change_of_email": "Verify your new {{appName}} email address", + "trace_reference_id": "Trace reference ID", "change_of_email_toast": "We have sent a verification link to {{email}}. We will update your email address once you click this link.", "copy_somewhere_safe": "Save this API key somewhere safe. You will not be able to view it again.", "verify_email_email_body": "Please verify your email address by clicking the button below.", From a82100134bd8588c50dd1fd81a2dda43a4529c89 Mon Sep 17 00:00:00 2001 From: sanxroz Date: Wed, 19 Nov 2025 22:36:04 -0500 Subject: [PATCH 9/9] enhance error handling in getServerErrorFromUnknown function --- packages/lib/server/getServerErrorFromUnknown.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/lib/server/getServerErrorFromUnknown.ts b/packages/lib/server/getServerErrorFromUnknown.ts index 514e31e46de4f9..dbc9abb5f669cf 100644 --- a/packages/lib/server/getServerErrorFromUnknown.ts +++ b/packages/lib/server/getServerErrorFromUnknown.ts @@ -126,7 +126,14 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError { } if (typeof cause === "string") { // @ts-expect-error https://github.com/tc39/proposal-error-cause - return new Error(cause, { cause }); + const error = new Error(cause, { cause }); + const httpError = getHttpError({ statusCode: 500, cause: error }); + return new HttpError({ + statusCode: httpError.statusCode, + message: httpError.message, + cause: httpError.cause, + data: traceId ? { ...tracedData, ...httpError.data, traceId } : httpError.data, + }); } return new HttpError({