feat: distributed tracing - 2 (#24861)
* feat: distributed tracing 2 * feat: distributed tracing 2 * refactor: feedback * refactor: feedback * fix: type error * fix: trpc error --------- Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
co-authored by
Hariom Balhara
parent
19641ffc05
commit
1ff5d57ded
@@ -10,10 +10,11 @@ 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 { 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);
|
||||
|
||||
if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") {
|
||||
@@ -52,8 +53,10 @@ 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: req.traceContext,
|
||||
},
|
||||
});
|
||||
|
||||
// const booking = await createBookingThroughFactory();
|
||||
return booking;
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -66,12 +66,12 @@ 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 +115,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 +125,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<IsFixedAwareUser, "credentials"> & {
|
||||
credentials: CredentialForCalendarService[];
|
||||
@@ -479,8 +477,19 @@ async function handler(
|
||||
skipAvailabilityCheck = false,
|
||||
skipEventLimitsCheck = false,
|
||||
skipCalendarSyncTaskCreation = false,
|
||||
traceContext: passedTraceContext,
|
||||
} = input;
|
||||
|
||||
const traceContext = passedTraceContext
|
||||
? passedTraceContext
|
||||
: distributedTracing.createTrace("booking_creation");
|
||||
|
||||
const tracingLogger = distributedTracing.getTracingLogger(traceContext, {
|
||||
eventTypeId: rawBookingData.eventTypeId,
|
||||
userId: userId,
|
||||
eventTypeSlug: rawBookingData.eventTypeSlug,
|
||||
});
|
||||
|
||||
const isPlatformBooking = !!platformClientId;
|
||||
|
||||
const eventType = await getEventType({
|
||||
@@ -527,8 +536,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({
|
||||
@@ -565,6 +573,7 @@ async function handler(
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "email_verification_required",
|
||||
data: { traceId: traceContext.traceId },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -574,12 +583,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);
|
||||
@@ -587,12 +597,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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -669,7 +685,7 @@ async function handler(
|
||||
|
||||
const shouldServeCache = false;
|
||||
|
||||
loggerWithEventDetails.info(
|
||||
tracingLogger.info(
|
||||
`Booking eventType ${eventTypeId} started`,
|
||||
safeStringify({
|
||||
reqBody: {
|
||||
@@ -704,7 +720,7 @@ async function handler(
|
||||
reqBody.timeZone,
|
||||
eventType,
|
||||
eventTimeZone,
|
||||
loggerWithEventDetails
|
||||
tracingLogger
|
||||
);
|
||||
|
||||
validateEventLength({
|
||||
@@ -712,7 +728,7 @@ async function handler(
|
||||
reqBodyEnd: reqBody.end,
|
||||
eventTypeMultipleDuration: eventType.metadata?.multipleDuration,
|
||||
eventTypeLength: eventType.length,
|
||||
logger: loggerWithEventDetails,
|
||||
logger: tracingLogger,
|
||||
});
|
||||
|
||||
const contactOwnerFromReq = reqBody.teamMemberEmail ?? null;
|
||||
@@ -731,7 +747,11 @@ 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({
|
||||
where: {
|
||||
@@ -757,7 +777,7 @@ async function handler(
|
||||
eventType,
|
||||
eventTypeId,
|
||||
dynamicUserList,
|
||||
logger: loggerWithEventDetails,
|
||||
logger: tracingLogger,
|
||||
routedTeamMemberIds: routedTeamMemberIds ?? null,
|
||||
contactOwnerEmail,
|
||||
rescheduleUid: reqBody.rescheduleUid || null,
|
||||
@@ -868,7 +888,7 @@ async function handler(
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking: originalRescheduledBooking ?? null,
|
||||
},
|
||||
loggerWithEventDetails,
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
);
|
||||
}
|
||||
@@ -883,7 +903,7 @@ async function handler(
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails,
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
);
|
||||
}
|
||||
@@ -902,7 +922,7 @@ async function handler(
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails,
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
);
|
||||
} else {
|
||||
@@ -910,7 +930,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),
|
||||
@@ -931,14 +951,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),
|
||||
@@ -967,7 +987,7 @@ async function handler(
|
||||
|
||||
const notAvailableLuckyUsers: typeof users = [];
|
||||
|
||||
loggerWithEventDetails.debug(
|
||||
tracingLogger.debug(
|
||||
"Computed available users",
|
||||
safeStringify({
|
||||
availableUsers: availableUsers.map((user) => user.id),
|
||||
@@ -1044,7 +1064,7 @@ async function handler(
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
loggerWithEventDetails,
|
||||
tracingLogger,
|
||||
shouldServeCache
|
||||
);
|
||||
}
|
||||
@@ -1054,7 +1074,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.`
|
||||
);
|
||||
}
|
||||
@@ -1119,7 +1139,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);
|
||||
}
|
||||
|
||||
@@ -1233,7 +1253,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()}`;
|
||||
@@ -1248,8 +1268,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];
|
||||
@@ -1663,7 +1683,7 @@ async function handler(
|
||||
|
||||
let booking: CreatedBooking | null = null;
|
||||
|
||||
loggerWithEventDetails.debug(
|
||||
tracingLogger.debug(
|
||||
"Going to create booking in DB now",
|
||||
safeStringify({
|
||||
organizerUser: organizerUser.id,
|
||||
@@ -1824,13 +1844,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;
|
||||
}
|
||||
@@ -1847,7 +1867,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);
|
||||
|
||||
@@ -1878,7 +1898,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,
|
||||
@@ -1949,7 +1969,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 })
|
||||
);
|
||||
@@ -1972,7 +1992,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,
|
||||
@@ -2074,9 +2094,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 })
|
||||
);
|
||||
@@ -2101,7 +2122,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,
|
||||
@@ -2179,7 +2200,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),
|
||||
@@ -2196,7 +2217,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),
|
||||
@@ -2236,7 +2257,7 @@ async function handler(
|
||||
const bookingRescheduledPayload = bookingCreatedPayload;
|
||||
|
||||
const bookingEventHandler = new BookingEventHandlerService({
|
||||
log: loggerWithEventDetails,
|
||||
log: tracingLogger,
|
||||
hashedLinkService: deps.hashedLinkService,
|
||||
});
|
||||
|
||||
@@ -2268,7 +2289,7 @@ 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({
|
||||
where: {
|
||||
@@ -2295,7 +2316,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
|
||||
@@ -2363,7 +2388,7 @@ async function handler(
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
loggerWithEventDetails.error(
|
||||
tracingLogger.error(
|
||||
"Error while scheduling workflow reminders for booking payment initiated",
|
||||
JSON.stringify({ error })
|
||||
);
|
||||
@@ -2394,7 +2419,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) {
|
||||
@@ -2456,7 +2481,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),
|
||||
@@ -2504,7 +2529,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 = {
|
||||
@@ -2540,7 +2565,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 {
|
||||
@@ -2561,7 +2586,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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -3,11 +3,19 @@ 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<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
|
||||
export interface TracedRequest extends NextApiRequest {
|
||||
traceContext: TraceContext;
|
||||
}
|
||||
|
||||
type Handle<T> = (req: TracedRequest, res: NextApiResponse) => Promise<T>;
|
||||
|
||||
/** Allows us to get type inference from API handler responses */
|
||||
export function defaultResponder<T>(
|
||||
@@ -17,22 +25,37 @@ export function defaultResponder<T>(
|
||||
) {
|
||||
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) {
|
||||
tracingLogger.error(`${operation} request failed`, { error: err });
|
||||
const tracedError = TracedError.createFromError(err, traceContext);
|
||||
let error: HttpError;
|
||||
if (err instanceof TRPCError) {
|
||||
const statusCode = getHTTPStatusCodeFromError(err);
|
||||
@@ -44,8 +67,9 @@ export function defaultResponder<T>(
|
||||
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", tracedError.traceId);
|
||||
return res
|
||||
.status(error.statusCode)
|
||||
.json({ message: error.message, url: error.url, method: error.method, data: error?.data || null });
|
||||
|
||||
@@ -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<string, string>;
|
||||
meta?: Record<string, string | number | boolean | null | undefined>;
|
||||
}
|
||||
|
||||
export interface IdGenerator {
|
||||
@@ -31,7 +31,7 @@ export class DistributedTracing {
|
||||
createSpan(
|
||||
parentContext: TraceContext,
|
||||
operation: string,
|
||||
additionalMeta?: Record<string, string>
|
||||
additionalMeta?: Record<string, string | number | boolean | null | undefined>
|
||||
): TraceContext {
|
||||
const mergedMeta = {
|
||||
...parentContext.meta,
|
||||
@@ -47,7 +47,15 @@ export class DistributedTracing {
|
||||
};
|
||||
}
|
||||
|
||||
getTracingLogger(context: TraceContext) {
|
||||
getTracingLogger(
|
||||
context: TraceContext,
|
||||
additionalMeta?: Record<string, string | number | boolean | null | undefined>
|
||||
) {
|
||||
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<string, string>): TraceContext {
|
||||
updateTrace(
|
||||
traceContext: TraceContext,
|
||||
additionalMeta?: Record<string, string | number | boolean | null | undefined>
|
||||
): TraceContext {
|
||||
return {
|
||||
...traceContext,
|
||||
meta: {
|
||||
|
||||
@@ -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<InnerContext> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user