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
This commit is contained in:
Keith Williams
2024-10-30 00:35:25 +00:00
committed by GitHub
parent 5a9197f357
commit 4b6b9badf5
12 changed files with 311 additions and 274 deletions
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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,
+3 -1
View File
@@ -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]");
+1
View File
@@ -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,
});
+2 -4
View File
@@ -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"
}
}
@@ -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;
@@ -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) {
+10 -17
View File
@@ -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,
+7 -4
View File
@@ -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 <T extends (...args: any[]) => any>(
...args: Parameters<T>
): Promise<ReturnType<T>> => {
// Check if Sentry set
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return (await cb(...args)) as ReturnType<T>;
if (!process.env.NEXT_PUBLIC_SENTRY_DSN || !process.env.SENTRY_TRACES_SAMPLE_RATE) {
return (await cb(...args)) as ReturnType<T>;
}
return await startSpan({ name: cb.name }, async () => {
try {
@@ -33,7 +35,8 @@ const monitorCallbackSync = <T extends (...args: any[]) => any>(
...args: Parameters<T>
): ReturnType<T> => {
// Check if Sentry set
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return cb(...args) as ReturnType<T>;
if (!process.env.NEXT_PUBLIC_SENTRY_DSN || !process.env.SENTRY_TRACES_SAMPLE_RATE)
return cb(...args) as ReturnType<T>;
return startSpan({ name: cb.name }, () => {
try {
+1 -1
View File
@@ -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";
+273 -238
View File
@@ -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<Prisma.SelectedSlotsDefaultArgs>()({
@@ -399,7 +399,13 @@ export function getUsersWithCredentialsConsideringContactOwner({
return contactOwnerAndFixedHosts;
}
export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<IGetAvailableSlots> {
export const getAvailableSlots = async (
...args: Parameters<typeof _getAvailableSlots>
): Promise<ReturnType<typeof _getAvailableSlots>> => {
return monitorCallbackAsync(_getAvailableSlots, ...args);
};
async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<IGetAvailableSlots> {
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<Prisma.BookingSelect>()({
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<ReturnType<typeof getBusyTimesForLimitChecks>> = [];
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<string, { time: string; attendees?: number; bookingUid?: string }[]>,
{ time, ...passThroughProps }
) => {
// TODO: Adds unit tests to prevent regressions in getSchedule (try multiple timezones)
const slotsMappedToDate = monitorCallbackSync(function mapSlotsToDate() {
return availableTimeSlots.reduce(
(
r: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>,
{ 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<ReturnType<typeof getEventType>>,
sharedQuery: {
startTime: {
lte: Date;
};
endTime: {
gte: Date;
};
status: {
in: "ACCEPTED"[];
};
},
usersWithCredentials: ReturnType<typeof getUsersWithCredentialsConsideringContactOwner>,
allUserIds: number[]
) {
const bookingsSelect = Prisma.validator<Prisma.BookingSelect>()({
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]);
+1
View File
@@ -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",