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:
+2
-1
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user