From 4b6b9badf537c0fdd08be4e17cd34b667ea9a31c Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Tue, 29 Oct 2024 21:35:25 -0300 Subject: [PATCH] perf: Sentry perf tracing (#17202) * chore: Add Sentry perf tracing * Just to build * perf: Add Sentry perf tracing * Removed the extra integrations tracing config * Got spans working * Now only tracing individual endpoints * Removed check for booking event type * Fixed types * Added back for spans * Consolidate sentry imports * Added more traces * Moved the start of the span --- .env.example | 3 +- apps/web/next.config.js | 2 +- apps/web/pages/api/trpc/highPerf/[trpc].ts | 4 +- apps/web/sentry.server.config.ts | 1 + packages/app-store/dialpad/config.json | 6 +- .../trpc/getResponseWithFormFields.handler.ts | 8 +- .../routing-forms/trpc/response.handler.ts | 9 +- packages/core/getUserAvailability.ts | 27 +- packages/core/sentryWrapper.ts | 11 +- packages/features/embed/Embed.tsx | 2 +- .../trpc/server/routers/viewer/slots/util.ts | 511 ++++++++++-------- turbo.json | 1 + 12 files changed, 311 insertions(+), 274 deletions(-) diff --git a/.env.example b/.env.example index f3f4bc6b8c..c26160f3b0 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,7 @@ # - Acquire a commercial license to remove these terms by visiting: cal.com/sales # -# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions. +# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions. CALCOM_LICENSE_KEY= # Signature token for the Cal.com License API (used for self-hosted integrations) # We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key @@ -137,6 +137,7 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME= # Sentry # Used for capturing exceptions and logging messages NEXT_PUBLIC_SENTRY_DSN= +SENTRY_TRACES_SAMPLE_RATE= # Formbricks Experience Management Integration NEXT_PUBLIC_FORMBRICKS_HOST_URL=https://app.formbricks.com diff --git a/apps/web/next.config.js b/apps/web/next.config.js index d0875c2ec3..94529ec1f5 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -663,7 +663,7 @@ const nextConfig = { if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) { plugins.push((nextConfig) => withSentryConfig(nextConfig, { - autoInstrumentServerFunctions: true, + autoInstrumentServerFunctions: false, hideSourceMaps: true, // disable source map generation for the server code disableServerWebpackPlugin: !!process.env.SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN, diff --git a/apps/web/pages/api/trpc/highPerf/[trpc].ts b/apps/web/pages/api/trpc/highPerf/[trpc].ts index e4cc725948..384ac2dd20 100644 --- a/apps/web/pages/api/trpc/highPerf/[trpc].ts +++ b/apps/web/pages/api/trpc/highPerf/[trpc].ts @@ -1,4 +1,6 @@ +import { wrapApiHandlerWithSentry } from "@sentry/nextjs"; + import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler"; import { highPerfRouter } from "@calcom/trpc/server/routers/viewer/highPerf/_router"; -export default createNextApiHandler(highPerfRouter); +export default wrapApiHandlerWithSentry(createNextApiHandler(highPerfRouter), "/api/trpc/highPerf/[trpc]"); diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts index 842f09fccc..0baa332bc3 100644 --- a/apps/web/sentry.server.config.ts +++ b/apps/web/sentry.server.config.ts @@ -2,4 +2,5 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? "0.0") || 0.0, }); diff --git a/packages/app-store/dialpad/config.json b/packages/app-store/dialpad/config.json index b192947a0e..6d5c6ac4ec 100644 --- a/packages/app-store/dialpad/config.json +++ b/packages/app-store/dialpad/config.json @@ -6,9 +6,7 @@ "logo": "icon.svg", "url": "https://meetings.dialpad.com/", "variant": "conferencing", - "categories": [ - "conferencing" - ], + "categories": ["conferencing"], "publisher": "Cal.com", "email": "help@cal.com", "appData": { @@ -24,4 +22,4 @@ "isTemplate": false, "__createdUsingCli": true, "__template": "event-type-location-video-static" -} \ No newline at end of file +} diff --git a/packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts b/packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts index e75aacf89b..76602ff5f7 100644 --- a/packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts +++ b/packages/app-store/routing-forms/trpc/getResponseWithFormFields.handler.ts @@ -64,10 +64,10 @@ async function getResponseWithFormFieldsHandler({ ctx, input }: GetResponseWithF }); if (!formResponse) { - throw new TRPCError({ - code: "NOT_FOUND", - message: translate("form_response_not_found"), - }); + throw new TRPCError({ + code: "NOT_FOUND", + message: translate("form_response_not_found"), + }); } const form = formResponse.form; diff --git a/packages/app-store/routing-forms/trpc/response.handler.ts b/packages/app-store/routing-forms/trpc/response.handler.ts index 2a7c1d8d6a..013c9d3132 100644 --- a/packages/app-store/routing-forms/trpc/response.handler.ts +++ b/packages/app-store/routing-forms/trpc/response.handler.ts @@ -138,9 +138,12 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => safeStringify({ teamMembersMatchingAttributeLogicWithResult }) ); - const teamMemberIdsMatchingAttributeLogic = teamMembersMatchingAttributeLogicWithResult?.teamMembersMatchingAttributeLogic - ? teamMembersMatchingAttributeLogicWithResult.teamMembersMatchingAttributeLogic.map((member) => member.userId) - : null; + const teamMemberIdsMatchingAttributeLogic = + teamMembersMatchingAttributeLogicWithResult?.teamMembersMatchingAttributeLogic + ? teamMembersMatchingAttributeLogicWithResult.teamMembersMatchingAttributeLogic.map( + (member) => member.userId + ) + : null; const chosenRoute = serializableFormWithFields.routes?.find((route) => route.id === chosenRouteId); if (!chosenRoute) { diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index a4172e58cb..3469712b31 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -6,6 +6,7 @@ import type { User, EventType as PrismaEventType, } from "@prisma/client"; +import * as Sentry from "@sentry/nextjs"; import { z } from "zod"; import type { Dayjs } from "@calcom/dayjs"; @@ -18,7 +19,6 @@ import { ErrorCode } from "@calcom/lib/errorCodes"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; -import { performance } from "@calcom/lib/server/perfObserver"; import prisma, { availabilityUserSelect } from "@calcom/prisma"; import { SchedulingType } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; @@ -374,7 +374,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const getBusyTimesStart = dateFrom.toISOString(); const getBusyTimesEnd = dateTo.toISOString(); - const busyTimes = await getBusyTimes({ + const busyTimes = await monitorCallbackAsync(getBusyTimes, { credentials: user.credentials, startTime: getBusyTimesStart, endTime: getBusyTimesEnd, @@ -415,7 +415,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA }) ); - const startGetWorkingHours = performance.now(); + const getWorkingHoursSpan = Sentry.startInactiveSpan({ name: "getWorkingHours" }); if ( !(schedule?.availability || (eventType?.availability.length ? eventType.availability : user.availability)) @@ -431,14 +431,14 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA })); const workingHours = getWorkingHours({ timeZone }, availability); - - const endGetWorkingHours = performance.now(); + getWorkingHoursSpan.end(); const dateOverrides: TimeRange[] = []; // NOTE: getSchedule is currently calling this function for every user in a team event // but not using these values at all, wasting CPU. Adding this check here temporarily to avoid a larger refactor // since other callers do using this data. if (returnDateOverrides) { + const calculateDateOverridesSpan = Sentry.startInactiveSpan({ name: "calculateDateOverrides" }); const availabilityWithDates = availability.filter((availability) => !!availability.date); for (let i = 0; i < availabilityWithDates.length; i++) { @@ -457,6 +457,8 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA }); } } + + calculateDateOverridesSpan.end(); } const outOfOfficeDays = @@ -528,6 +530,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA const datesOutOfOffice: IOutOfOfficeData = calculateOutOfOfficeRanges(outOfOfficeDays, availability); + const buildDateRangesSpan = Sentry.startInactiveSpan({ name: "buildDateRanges" }); const { dateRanges, oooExcludedDateRanges } = buildDateRanges({ dateFrom, dateTo, @@ -545,26 +548,16 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA outOfOffice: datesOutOfOffice, }); + buildDateRangesSpan.end(); + const formattedBusyTimes = detailedBusyTimes.map((busy) => ({ start: dayjs(busy.start), end: dayjs(busy.end), })); const dateRangesInWhichUserIsAvailable = subtract(dateRanges, formattedBusyTimes); - const dateRangesInWhichUserIsAvailableWithoutOOO = subtract(oooExcludedDateRanges, formattedBusyTimes); - log.debug( - `getWorkingHours took ${endGetWorkingHours - startGetWorkingHours}ms for userId ${userId}`, - JSON.stringify({ - workingHoursInUtc: workingHours, - dateOverrides, - dateRangesAsPerAvailability: dateRanges, - dateRangesInWhichUserIsAvailable, - detailedBusyTimes, - }) - ); - return { busy: detailedBusyTimes, timeZone, diff --git a/packages/core/sentryWrapper.ts b/packages/core/sentryWrapper.ts index 24816b96e8..e19e5bcae8 100644 --- a/packages/core/sentryWrapper.ts +++ b/packages/core/sentryWrapper.ts @@ -2,8 +2,8 @@ import { startSpan, captureException } from "@sentry/nextjs"; /* WHEN TO USE -We ran a script that performs a simple mathematical calculation within a loop of 1000000 iterations. -Our results were: Plain execution time: 441, Monitored execution time: 8094. +We ran a script that performs a simple mathematical calculation within a loop of 1000000 iterations. +Our results were: Plain execution time: 441, Monitored execution time: 8094. This suggests that using these wrappers within large loops can incur significant overhead and is thus not recommended. For smaller loops, the cost incurred may not be very significant on an absolute scale @@ -15,7 +15,9 @@ const monitorCallbackAsync = async any>( ...args: Parameters ): Promise> => { // Check if Sentry set - if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return (await cb(...args)) as ReturnType; + if (!process.env.NEXT_PUBLIC_SENTRY_DSN || !process.env.SENTRY_TRACES_SAMPLE_RATE) { + return (await cb(...args)) as ReturnType; + } return await startSpan({ name: cb.name }, async () => { try { @@ -33,7 +35,8 @@ const monitorCallbackSync = any>( ...args: Parameters ): ReturnType => { // Check if Sentry set - if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return cb(...args) as ReturnType; + if (!process.env.NEXT_PUBLIC_SENTRY_DSN || !process.env.SENTRY_TRACES_SAMPLE_RATE) + return cb(...args) as ReturnType; return startSpan({ name: cb.name }, () => { try { diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index 3e2908b750..4b230ca024 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -17,7 +17,7 @@ import { useTimePreferences } from "@calcom/features/bookings/lib/timePreference import DatePicker from "@calcom/features/calendars/DatePicker"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import { useSlotsForDate } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate"; -import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants"; +import { APP_NAME } from "@calcom/lib/constants"; import { weekdayToWeekIndex } from "@calcom/lib/date-fns"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 4929813cca..4a6304ca50 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -6,6 +6,7 @@ import { getAggregatedAvailability } from "@calcom/core/getAggregatedAvailabilit import { getBusyTimesForLimitChecks } from "@calcom/core/getBusyTimes"; import type { CurrentSeats, IFromUser, IToUser, GetAvailabilityUser } from "@calcom/core/getUserAvailability"; import { getUsersAvailability } from "@calcom/core/getUserAvailability"; +import monitorCallbackAsync, { monitorCallbackSync } from "@calcom/core/sentryWrapper"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains"; @@ -22,7 +23,6 @@ import { } from "@calcom/lib/isOutOfBounds"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; -import { performance } from "@calcom/lib/server/perfObserver"; import { UserRepository } from "@calcom/lib/server/repository/user"; import getSlots from "@calcom/lib/slots"; import prisma, { availabilityUserSelect } from "@calcom/prisma"; @@ -306,14 +306,14 @@ export async function getDynamicEventType( }); } -export function getRegularOrDynamicEventType( +export async function getRegularOrDynamicEventType( input: TGetScheduleInputSchema, organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } ) { const isDynamicBooking = input.usernameList && input.usernameList.length > 1; return isDynamicBooking - ? getDynamicEventType(input, organizationDetails) - : getEventType(input, organizationDetails); + ? await getDynamicEventType(input, organizationDetails) + : await getEventType(input, organizationDetails); } const selectSelectedSlots = Prisma.validator()({ @@ -399,7 +399,13 @@ export function getUsersWithCredentialsConsideringContactOwner({ return contactOwnerAndFixedHosts; } -export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise { +export const getAvailableSlots = async ( + ...args: Parameters +): Promise> => { + return monitorCallbackAsync(_getAvailableSlots, ...args); +}; + +async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise { const { _enableTroubleshooter: enableTroubleshooter = false } = input; const orgDetails = input?.orgSlug ? { @@ -411,9 +417,8 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro if (process.env.INTEGRATION_TEST_MODE === "true") { logger.settings.minLevel = 2; } - const startPrismaEventTypeGet = performance.now(); - const eventType = await getRegularOrDynamicEventType(input, orgDetails); - const endPrismaEventTypeGet = performance.now(); + + const eventType = await monitorCallbackAsync(getRegularOrDynamicEventType, input, orgDetails); if (!eventType) { throw new TRPCError({ code: "NOT_FOUND" }); @@ -439,11 +444,6 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro prefix: ["getAvailableSlots", `${eventType.id}:${input.usernameList}/${input.eventTypeSlug}`], }); - loggerWithEventDetails.debug( - `Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms for event:${ - input.eventTypeId - }` - ); const getStartTime = (startTimeInput: string, timeZone?: string) => { const startTimeMin = dayjs.utc().add(eventType.minimumBookingNotice || 1, "minutes"); const startTime = timeZone === "Etc/GMT" ? dayjs.utc(startTimeInput) : dayjs(startTimeInput).tz(timeZone); @@ -510,7 +510,7 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro ); } - const usersWithCredentials = getUsersWithCredentialsConsideringContactOwner({ + const usersWithCredentials = monitorCallbackSync(getUsersWithCredentialsConsideringContactOwner, { contactOwnerEmail, hosts: routedHostsWithContactOwnerAndFixedHosts, }); @@ -538,155 +538,29 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro }; const allUserIds = usersWithCredentials.map((user) => user.id); - const bookingsSelect = Prisma.validator()({ - id: true, - uid: true, - userId: true, - startTime: true, - endTime: true, - title: true, - attendees: true, - eventType: { - select: { - id: true, - onlyShowFirstAvailableSlot: true, - afterEventBuffer: true, - beforeEventBuffer: true, - seatsPerTimeSlot: true, - requiresConfirmationWillBlockSlot: true, - requiresConfirmation: true, - }, - }, - ...(!!eventType?.seatsPerTimeSlot && { - _count: { - select: { - seatsReferences: true, - }, - }, - }), - }); + const currentBookingsAllUsers = await monitorCallbackAsync( + getExistingBookings, + startTimeDate, + endTimeDate, + eventType, + sharedQuery, + usersWithCredentials, + allUserIds + ); - const currentBookingsAllUsersQueryOne = prisma.booking.findMany({ - where: { - ...sharedQuery, - userId: { - in: allUserIds, - }, - }, - select: bookingsSelect, - }); - - const currentBookingsAllUsersQueryTwo = prisma.booking.findMany({ - where: { - ...sharedQuery, - attendees: { - some: { - email: { - in: usersWithCredentials.map((user) => user.email), - }, - }, - }, - }, - select: bookingsSelect, - }); - - const currentBookingsAllUsersQueryThree = prisma.booking.findMany({ - where: { - startTime: { lte: endTimeDate }, - endTime: { gte: startTimeDate }, - eventType: { - id: eventType.id, - requiresConfirmation: true, - requiresConfirmationWillBlockSlot: true, - }, - status: { - in: [BookingStatus.PENDING], - }, - }, - select: bookingsSelect, - }); - - const [resultOne, resultTwo, resultThree] = await Promise.all([ - currentBookingsAllUsersQueryOne, - currentBookingsAllUsersQueryTwo, - currentBookingsAllUsersQueryThree, - ]); - - const currentBookingsAllUsers = [...resultOne, ...resultTwo, ...resultThree]; - - const outOfOfficeDaysAllUsers = await prisma.outOfOfficeEntry.findMany({ - where: { - userId: { - in: allUserIds, - }, - OR: [ - // outside of range - // (start <= 'dateTo' AND end >= 'dateFrom') - { - start: { - lte: endTimeDate, - }, - end: { - gte: startTimeDate, - }, - }, - // start is between dateFrom and dateTo but end is outside of range - // (start <= 'dateTo' AND end >= 'dateTo') - { - start: { - lte: endTimeDate, - }, - - end: { - gte: endTimeDate, - }, - }, - // end is between dateFrom and dateTo but start is outside of range - // (start <= 'dateFrom' OR end <= 'dateTo') - { - start: { - lte: startTimeDate, - }, - - end: { - lte: endTimeDate, - }, - }, - ], - }, - select: { - id: true, - start: true, - end: true, - user: { - select: { - id: true, - name: true, - }, - }, - toUser: { - select: { - id: true, - username: true, - name: true, - }, - }, - reason: { - select: { - id: true, - emoji: true, - reason: true, - }, - }, - }, - }); + const outOfOfficeDaysAllUsers = await monitorCallbackAsync( + getOOODates, + startTimeDate, + endTimeDate, + allUserIds + ); const bookingLimits = parseBookingLimit(eventType?.bookingLimits); const durationLimits = parseDurationLimit(eventType?.durationLimits); let busyTimesFromLimitsBookingsAllUsers: Awaited> = []; if (eventType && (bookingLimits || durationLimits)) { - busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({ + busyTimesFromLimitsBookingsAllUsers = await monitorCallbackAsync(getBusyTimesForLimitChecks, { userIds: allUserIds, eventTypeId: eventType.id, startDate: startTime.format(), @@ -697,40 +571,43 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro }); } - const users = usersWithCredentials.map((currentUser) => { - return { - ...currentUser, - currentBookings: currentBookingsAllUsers - .filter((b) => b.userId === currentUser.id || b.attendees?.some((a) => a.email === currentUser.email)) - .map((bookings) => { - const { attendees: _attendees, ...bookingWithoutAttendees } = bookings; - return bookingWithoutAttendees; - }), - outOfOfficeDays: outOfOfficeDaysAllUsers.filter((o) => o.user.id === currentUser.id), - }; + const users = monitorCallbackSync(function enrichUsersWithData() { + return usersWithCredentials.map((currentUser) => { + return { + ...currentUser, + currentBookings: currentBookingsAllUsers + .filter( + (b) => b.userId === currentUser.id || b.attendees?.some((a) => a.email === currentUser.email) + ) + .map((bookings) => { + const { attendees: _attendees, ...bookingWithoutAttendees } = bookings; + return bookingWithoutAttendees; + }), + outOfOfficeDays: outOfOfficeDaysAllUsers.filter((o) => o.user.id === currentUser.id), + }; + }); }); + const premappedUsersAvailability = await getUsersAvailability({ + users, + query: { + dateFrom: startTime.format(), + dateTo: endTime.format(), + eventTypeId: eventType.id, + afterEventBuffer: eventType.afterEventBuffer, + beforeEventBuffer: eventType.beforeEventBuffer, + duration: input.duration || 0, + returnDateOverrides: false, + }, + initialData: { + eventType, + currentSeats, + rescheduleUid: input.rescheduleUid, + busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers, + }, + }); /* We get all users working hours and busy slots */ - const allUsersAvailability = ( - await getUsersAvailability({ - users, - query: { - dateFrom: startTime.format(), - dateTo: endTime.format(), - eventTypeId: eventType.id, - afterEventBuffer: eventType.afterEventBuffer, - beforeEventBuffer: eventType.beforeEventBuffer, - duration: input.duration || 0, - returnDateOverrides: false, - }, - initialData: { - eventType, - currentSeats, - rescheduleUid: input.rescheduleUid, - busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers, - }, - }) - ).map( + const allUsersAvailability = premappedUsersAvailability.map( ( { busy, dateRanges, oooExcludedDateRanges, currentSeats: _currentSeats, timeZone, datesOutOfOffice }, index @@ -753,11 +630,11 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro currentSeats, }; - const getSlotsTime = 0; - const checkForAvailabilityTime = 0; - const getSlotsCount = 0; - const checkForAvailabilityCount = 0; - const aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType); + const aggregatedAvailability = monitorCallbackSync( + getAggregatedAvailability, + allUsersAvailability, + eventType.schedulingType + ); const isTeamEvent = eventType.schedulingType === SchedulingType.COLLECTIVE || @@ -769,7 +646,7 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro // TODO: Also, handleNewBooking only seems to be using eventType?.schedule?.timeZone which seems to confirm that we should simplify it as well. const eventTimeZone = eventType.timeZone || eventType?.schedule?.timeZone || allUsersAvailability?.[0]?.timeZone; - const timeSlots = getSlots({ + const timeSlots = monitorCallbackSync(getSlots, { inviteeDate: startTime, eventLength: input.duration || eventType.length, offsetStart: eventType.offsetStart, @@ -874,45 +751,47 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro timeZone: input.timeZone, }); - const slotsMappedToDate = availableTimeSlots.reduce( - ( - r: Record, - { time, ...passThroughProps } - ) => { - // TODO: Adds unit tests to prevent regressions in getSchedule (try multiple timezones) + const slotsMappedToDate = monitorCallbackSync(function mapSlotsToDate() { + return availableTimeSlots.reduce( + ( + r: Record, + { time, ...passThroughProps } + ) => { + // TODO: Adds unit tests to prevent regressions in getSchedule (try multiple timezones) - // This used to be _time.tz(input.timeZone) but Dayjs tz() is slow. - // toLocaleDateString slugish, using Intl.DateTimeFormat we get the desired speed results. - const dateString = formatter.format(time.toDate()); + // This used to be _time.tz(input.timeZone) but Dayjs tz() is slow. + // toLocaleDateString slugish, using Intl.DateTimeFormat we get the desired speed results. + const dateString = formatter.format(time.toDate()); - r[dateString] = r[dateString] || []; - if (eventType.onlyShowFirstAvailableSlot && r[dateString].length > 0) { + r[dateString] = r[dateString] || []; + if (eventType.onlyShowFirstAvailableSlot && r[dateString].length > 0) { + return r; + } + r[dateString].push({ + ...passThroughProps, + time: time.toISOString(), + // Conditionally add the attendees and booking id to slots object if there is already a booking during that time + ...(currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString()) && { + attendees: + currentSeats[ + currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) + ]._count.attendees, + bookingUid: + currentSeats[ + currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) + ].uid, + }), + }); return r; - } - r[dateString].push({ - ...passThroughProps, - time: time.toISOString(), - // Conditionally add the attendees and booking id to slots object if there is already a booking during that time - ...(currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString()) && { - attendees: - currentSeats[ - currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) - ]._count.attendees, - bookingUid: - currentSeats[ - currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) - ].uid, - }), - }); - return r; - }, - Object.create(null) - ); + }, + Object.create(null) + ); + }); loggerWithEventDetails.debug(safeStringify({ slotsMappedToDate })); const availableDates = Object.keys(slotsMappedToDate); - const allDatesWithBookabilityStatus = getAllDatesWithBookabilityStatus(availableDates); + const allDatesWithBookabilityStatus = monitorCallbackSync(getAllDatesWithBookabilityStatus, availableDates); loggerWithEventDetails.debug(safeStringify({ availableDates })); const eventUtcOffset = getUTCOffsetByTimezone(eventTimeZone) ?? 0; @@ -928,8 +807,8 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro bookerUtcOffset, }); let foundAFutureLimitViolation = false; - const withinBoundsSlotsMappedToDate = Object.entries(slotsMappedToDate).reduce( - (withinBoundsSlotsMappedToDate, [date, slots]) => { + const withinBoundsSlotsMappedToDate = monitorCallbackSync(function mapWithinBoundsSlotsToDate() { + return Object.entries(slotsMappedToDate).reduce((withinBoundsSlotsMappedToDate, [date, slots]) => { // Computation Optimization: If a future limit violation has been found, we just consider all slots to be out of bounds beyond that slot. // We can't do the same for periodType=RANGE because it can start from a day other than today and today will hit the violation then. if (foundAFutureLimitViolation && doesRangeStartFromToday(eventType.periodType)) { @@ -957,16 +836,8 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro withinBoundsSlotsMappedToDate[date] = filteredSlots; return withinBoundsSlotsMappedToDate; - }, - {} as typeof slotsMappedToDate - ); - - loggerWithEventDetails.debug(`getSlots took ${getSlotsTime}ms and executed ${getSlotsCount} times`); - - loggerWithEventDetails.debug( - `checkForAvailability took ${checkForAvailabilityTime}ms and executed ${checkForAvailabilityCount} times` - ); - loggerWithEventDetails.debug(`Available slots: ${JSON.stringify(withinBoundsSlotsMappedToDate)}`); + }, {} as typeof slotsMappedToDate); + }); // We only want to run this on single targeted events and not dynamic if (!Object.keys(withinBoundsSlotsMappedToDate).length && input.usernameList?.length === 1) { @@ -981,7 +852,7 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro }); } catch (e) { loggerWithEventDetails.error( - `Something has went wrong. Upstash could be down and we have caught the error to not block availability: + `Something has gone wrong. Upstash could be down and we have caught the error to not block availability: ${e}` ); } @@ -1047,6 +918,170 @@ async function getTeamIdFromSlug( return team?.id; } +async function getExistingBookings( + startTimeDate: Date, + endTimeDate: Date, + eventType: Awaited>, + sharedQuery: { + startTime: { + lte: Date; + }; + endTime: { + gte: Date; + }; + status: { + in: "ACCEPTED"[]; + }; + }, + usersWithCredentials: ReturnType, + allUserIds: number[] +) { + const bookingsSelect = Prisma.validator()({ + id: true, + uid: true, + userId: true, + startTime: true, + endTime: true, + title: true, + attendees: true, + eventType: { + select: { + id: true, + onlyShowFirstAvailableSlot: true, + afterEventBuffer: true, + beforeEventBuffer: true, + seatsPerTimeSlot: true, + requiresConfirmationWillBlockSlot: true, + requiresConfirmation: true, + }, + }, + ...(!!eventType?.seatsPerTimeSlot && { + _count: { + select: { + seatsReferences: true, + }, + }, + }), + }); + + const currentBookingsAllUsersQueryOne = prisma.booking.findMany({ + where: { + ...sharedQuery, + userId: { + in: allUserIds, + }, + }, + select: bookingsSelect, + }); + + const currentBookingsAllUsersQueryTwo = prisma.booking.findMany({ + where: { + ...sharedQuery, + attendees: { + some: { + email: { + in: usersWithCredentials.map((user) => user.email), + }, + }, + }, + }, + select: bookingsSelect, + }); + + const currentBookingsAllUsersQueryThree = prisma.booking.findMany({ + where: { + startTime: { lte: endTimeDate }, + endTime: { gte: startTimeDate }, + eventType: { + id: eventType?.id, + requiresConfirmation: true, + requiresConfirmationWillBlockSlot: true, + }, + status: { + in: [BookingStatus.PENDING], + }, + }, + select: bookingsSelect, + }); + + const [resultOne, resultTwo, resultThree] = await Promise.all([ + currentBookingsAllUsersQueryOne, + currentBookingsAllUsersQueryTwo, + currentBookingsAllUsersQueryThree, + ]); + + return [...resultOne, ...resultTwo, ...resultThree]; +} + +async function getOOODates(startTimeDate: Date, endTimeDate: Date, allUserIds: number[]) { + return await prisma.outOfOfficeEntry.findMany({ + where: { + userId: { + in: allUserIds, + }, + OR: [ + // outside of range + // (start <= 'dateTo' AND end >= 'dateFrom') + { + start: { + lte: endTimeDate, + }, + end: { + gte: startTimeDate, + }, + }, + // start is between dateFrom and dateTo but end is outside of range + // (start <= 'dateTo' AND end >= 'dateTo') + { + start: { + lte: endTimeDate, + }, + + end: { + gte: endTimeDate, + }, + }, + // end is between dateFrom and dateTo but start is outside of range + // (start <= 'dateFrom' OR end <= 'dateTo') + { + start: { + lte: startTimeDate, + }, + + end: { + lte: endTimeDate, + }, + }, + ], + }, + select: { + id: true, + start: true, + end: true, + user: { + select: { + id: true, + name: true, + }, + }, + toUser: { + select: { + id: true, + username: true, + name: true, + }, + }, + reason: { + select: { + id: true, + emoji: true, + reason: true, + }, + }, + }, + }); +} + export function getAllDatesWithBookabilityStatus(availableDates: string[]) { const availableDatesSet = new Set(availableDates); const firstDate = dayjs(availableDates[0]); diff --git a/turbo.json b/turbo.json index da1c670b34..41fec2c104 100644 --- a/turbo.json +++ b/turbo.json @@ -38,6 +38,7 @@ "NEXT_PUBLIC_STRIPE_TEAM_MONTHLY_PRICE_ID", "NEXT_PUBLIC_WEBAPP_URL", "NEXT_PUBLIC_WEBSITE_URL", + "SENTRY_TRACES_SAMPLE_RATE", "STRIPE_PREMIUM_PLAN_PRODUCT_ID", "STRIPE_TEAM_MONTHLY_PRICE_ID", "STRIPE_TEAM_PRODUCT_ID",