refactor: Uses withReporting to force named functions for tracing (#21219)

This commit is contained in:
Omar López
2025-05-12 13:22:52 -04:00
committed by GitHub
parent 79d695236f
commit b3d3cb1dee
35 changed files with 853 additions and 393 deletions
@@ -5,7 +5,7 @@ import { emailSchema } from "@calcom/lib/emailSchema";
import logger from "@calcom/lib/logger";
import { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic";
import { safeStringify } from "@calcom/lib/safeStringify";
import monitorCallbackAsync from "@calcom/lib/sentryWrapper";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { prisma } from "@calcom/prisma";
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
@@ -32,10 +32,6 @@ export type Form = SerializableForm<
const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/lib/handleResponse"] });
export const handleResponse = (...args: Parameters<typeof _handleResponse>) => {
return monitorCallbackAsync(_handleResponse, ...args);
};
const _handleResponse = async ({
response,
form,
@@ -185,7 +181,7 @@ const _handleResponse = async ({
})(),
]);
await monitorCallbackAsync(getRoutedMembers);
await withReporting(getRoutedMembers, "getRoutedMembers")();
} else {
// It currently happens for a Router route. Such a route id isn't present in the form.routes
}
@@ -202,8 +198,7 @@ const _handleResponse = async ({
},
});
await monitorCallbackAsync(
onFormSubmission,
await onFormSubmission(
{ ...serializableFormWithFields, userWithEmails },
dbFormResponse.response as FormResponse,
dbFormResponse.id,
@@ -247,3 +242,5 @@ const _handleResponse = async ({
throw e;
}
};
export const handleResponse = withReporting(_handleResponse, "handleResponse");
@@ -6,6 +6,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { WebhookTriggerEvents } from "@calcom/prisma/client";
import type { Ensure } from "@calcom/types/utils";
@@ -84,7 +85,7 @@ export function getFieldResponse({
* Not called in preview mode or dry run mode
* It takes care of sending webhooks and emails for form submissions
*/
export async function onFormSubmission(
async function _onFormSubmission(
form: Ensure<
SerializableForm<App_RoutingForms_Form> & { user: Pick<User, "id" | "email">; userWithEmails?: string[] },
"fields"
@@ -206,6 +207,7 @@ export async function onFormSubmission(
}
}
}
export const onFormSubmission = withReporting(_onFormSubmission, "onFormSubmission");
export const sendResponseEmail = async (
form: Pick<App_RoutingForms_Form, "id" | "name" | "fields">,
+24 -7
View File
@@ -8,6 +8,7 @@ import { getEventName } from "@calcom/lib/event";
import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
@@ -98,7 +99,7 @@ const eventTypeDisableHostEmail = (metadata?: EventTypeMetadata) => {
return !!metadata?.disableStandardEmails?.all?.host;
};
export const sendScheduledEmailsAndSMS = async (
const _sendScheduledEmailsAndSMS = async (
calEvent: CalendarEvent,
eventNameObject?: EventNameObjectType,
hostEmailDisabled?: boolean,
@@ -145,6 +146,11 @@ export const sendScheduledEmailsAndSMS = async (
await successfullyScheduledSms.sendSMSToAttendees();
};
export const sendScheduledEmailsAndSMS = withReporting(
_sendScheduledEmailsAndSMS,
"sendScheduledEmailsAndSMS"
);
// for rescheduled round robin booking that assigned new members
export const sendRoundRobinScheduledEmailsAndSMS = async ({
calEvent,
@@ -245,7 +251,7 @@ export const sendRoundRobinCancelledEmailsAndSMS = async (
await Promise.all(emailsAndSMSToSend);
};
export const sendRescheduledEmailsAndSMS = async (
const _sendRescheduledEmailsAndSMS = async (
calEvent: CalendarEvent,
eventTypeMetadata?: EventTypeMetadata
) => {
@@ -276,6 +282,10 @@ export const sendRescheduledEmailsAndSMS = async (
const successfullyReScheduledSms = new EventSuccessfullyReScheduledSMS(calEvent);
await successfullyReScheduledSms.sendSMSToAttendees();
};
export const sendRescheduledEmailsAndSMS = withReporting(
_sendRescheduledEmailsAndSMS,
"sendRescheduledEmailsAndSMS"
);
export const sendRescheduledSeatEmailAndSMS = async (
calEvent: CalendarEvent,
@@ -370,10 +380,7 @@ export const sendCancelledSeatEmailsAndSMS = async (
await cancelledSeatSMS.sendSMSToAttendee(cancelledAttendee);
};
export const sendOrganizerRequestEmail = async (
calEvent: CalendarEvent,
eventTypeMetadata?: EventTypeMetadata
) => {
const _sendOrganizerRequestEmail = async (calEvent: CalendarEvent, eventTypeMetadata?: EventTypeMetadata) => {
if (eventTypeDisableHostEmail(eventTypeMetadata)) return;
const calendarEvent = formatCalEvent(calEvent);
@@ -390,7 +397,12 @@ export const sendOrganizerRequestEmail = async (
await Promise.all(emailsToSend);
};
export const sendAttendeeRequestEmailAndSMS = async (
export const sendOrganizerRequestEmail = withReporting(
_sendOrganizerRequestEmail,
"sendOrganizerRequestEmail"
);
const _sendAttendeeRequestEmailAndSMS = async (
calEvent: CalendarEvent,
attendee: Person,
eventTypeMetadata?: EventTypeMetadata
@@ -403,6 +415,11 @@ export const sendAttendeeRequestEmailAndSMS = async (
await eventRequestSms.sendSMSToAttendee(attendee);
};
export const sendAttendeeRequestEmailAndSMS = withReporting(
_sendAttendeeRequestEmailAndSMS,
"sendAttendeeRequestEmailAndSMS"
);
export const sendDeclinedEmailsAndSMS = async (
calEvent: CalendarEvent,
eventTypeMetadata?: EventTypeMetadata
@@ -2,6 +2,7 @@ import async from "async";
import { isDelegationCredential } from "@calcom/lib/delegationCredential/clientAndServer";
import { buildAllCredentials } from "@calcom/lib/delegationCredential/server";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
import { refreshCredential } from "./refreshCredential";
@@ -11,9 +12,10 @@ import { refreshCredential } from "./refreshCredential";
*
* @param credentials
*/
export async function refreshCredentials(
// Define the function with underscore prefix
const _refreshCredentials = async (
credentials: Array<CredentialForCalendarService>
): Promise<Array<CredentialForCalendarService>> {
): Promise<Array<CredentialForCalendarService>> => {
const nonDelegationCredentials = credentials.filter(
(cred) => !isDelegationCredential({ credentialId: cred.id })
);
@@ -22,4 +24,6 @@ export async function refreshCredentials(
);
const refreshedDbCredentials = await async.mapLimit(nonDelegationCredentials, 5, refreshCredential);
return buildAllCredentials({ delegationCredentials, existingCredentials: refreshedDbCredentials });
}
};
export const refreshCredentials = withReporting(_refreshCredentials, "refreshCredentials");
@@ -25,7 +25,6 @@ import {
} from "@calcom/emails";
import getICalUID from "@calcom/emails/lib/getICalUID";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
import { getShouldServeCache } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
@@ -46,7 +45,7 @@ import {
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import EventManager from "@calcom/lib/EventManager";
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import {
enrichHostsWithDelegationCredentials,
getFirstDelegationConferencingCredentialAppLocation,
@@ -64,7 +63,6 @@ import logger from "@calcom/lib/logger";
import { handlePayment } from "@calcom/lib/payment/handlePayment";
import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import monitorCallbackAsync from "@calcom/lib/sentryWrapper";
import { getLuckyUser } from "@calcom/lib/server/getLuckyUser";
import { getTranslation } from "@calcom/lib/server/i18n";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
@@ -94,7 +92,7 @@ import type { Booking } from "./handleNewBooking/createBooking";
import { ensureAvailableUsers } from "./handleNewBooking/ensureAvailableUsers";
import { getBookingData } from "./handleNewBooking/getBookingData";
import { getCustomInputsResponses } from "./handleNewBooking/getCustomInputsResponses";
import { getEventTypesFromDB } from "./handleNewBooking/getEventTypesFromDB";
import { getEventType } from "./handleNewBooking/getEventType";
import type { getEventTypeResponse } from "./handleNewBooking/getEventTypesFromDB";
import { getLocationValuesForDb } from "./handleNewBooking/getLocationValuesForDb";
import { getRequiresConfirmationFlags } from "./handleNewBooking/getRequiresConfirmationFlags";
@@ -140,25 +138,6 @@ function getICalSequence(originalRescheduledBooking: BookingType | null) {
return originalRescheduledBooking.iCalSequence + 1;
}
const getEventType = async ({
eventTypeId,
eventTypeSlug,
}: {
eventTypeId: number;
eventTypeSlug?: string;
}) => {
// handle dynamic user
const eventType =
!eventTypeId && !!eventTypeSlug ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId);
const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId;
return {
...eventType,
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
};
};
type BookingDataSchemaGetter =
| typeof getBookingDataSchema
| typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default;
@@ -347,7 +326,7 @@ async function handler(
const isPlatformBooking = !!platformClientId;
const eventType = await monitorCallbackAsync(getEventType, {
const eventType = await getEventType({
eventTypeId: rawBookingData.eventTypeId,
eventTypeSlug: rawBookingData.eventTypeSlug,
});
@@ -551,22 +530,19 @@ async function handler(
});
}
const { qualifiedRRUsers, additionalFallbackRRUsers, fixedUsers } = await monitorCallbackAsync(
loadAndValidateUsers,
{
hostname,
forcedSlug,
isPlatform: isPlatformBooking,
eventType,
eventTypeId,
dynamicUserList,
logger: loggerWithEventDetails,
routedTeamMemberIds: routedTeamMemberIds ?? null,
contactOwnerEmail,
rescheduleUid: reqBody.rescheduleUid || null,
routingFormResponse,
}
);
const { qualifiedRRUsers, additionalFallbackRRUsers, fixedUsers } = await loadAndValidateUsers({
hostname,
forcedSlug,
isPlatform: isPlatformBooking,
eventType,
eventTypeId,
dynamicUserList,
logger: loggerWithEventDetails,
routedTeamMemberIds: routedTeamMemberIds ?? null,
contactOwnerEmail,
rescheduleUid: reqBody.rescheduleUid || null,
routingFormResponse,
});
// We filter out users but ensure allHostUsers remain same.
let users = [...qualifiedRRUsers, ...additionalFallbackRRUsers, ...fixedUsers];
@@ -579,7 +555,7 @@ async function handler(
location,
});
await monitorCallbackAsync(checkBookingAndDurationLimits, {
await checkBookingAndDurationLimits({
eventType,
reqBodyStart: reqBody.start,
reqBodyRescheduleUid: reqBody.rescheduleUid,
@@ -1289,7 +1265,7 @@ async function handler(
try {
if (!isDryRun) {
booking = await monitorCallbackAsync(createBooking, {
booking = await createBooking({
uid,
rescheduledBy: reqBody.rescheduledBy,
routingFormResponseId: routingFormResponseId,
@@ -1327,7 +1303,7 @@ async function handler(
// If it's a round robin event, record the reason for the host assignment
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
if (reqBody.crmOwnerRecordType && reqBody.crmAppSlug && contactOwnerEmail && routingFormResponseId) {
await monitorCallbackAsync(AssignmentReasonRecorder.CRMOwnership, {
await AssignmentReasonRecorder.CRMOwnership({
bookingId: booking.id,
crmAppSlug: reqBody.crmAppSlug,
teamMemberEmail: contactOwnerEmail,
@@ -1335,7 +1311,7 @@ async function handler(
routingFormResponseId,
});
} else if (routingFormResponseId && teamId) {
await monitorCallbackAsync(AssignmentReasonRecorder.routingFormRoute, {
await AssignmentReasonRecorder.routingFormRoute({
bookingId: booking.id,
routingFormResponseId,
organizerId: organizerUser.id,
@@ -1417,7 +1393,7 @@ async function handler(
}
// After polling videoBusyTimes, credentials might have been changed due to refreshment, so query them again.
const credentials = await monitorCallbackAsync(refreshCredentials, allCredentials);
const credentials = await refreshCredentials(allCredentials);
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
const eventManager = !isDryRun
? new EventManager({ ...organizerUser, credentials }, apps)
@@ -1798,8 +1774,7 @@ async function handler(
);
if (!isDryRun) {
await monitorCallbackAsync(
sendScheduledEmailsAndSMS,
await sendScheduledEmailsAndSMS(
{
...evt,
additionalInformation,
@@ -1840,13 +1815,8 @@ async function handler(
})
);
if (!isDryRun) {
await monitorCallbackAsync(sendOrganizerRequestEmail, { ...evt, additionalNotes }, eventType.metadata);
await monitorCallbackAsync(
sendAttendeeRequestEmailAndSMS,
{ ...evt, additionalNotes },
attendeesList[0],
eventType.metadata
);
await sendOrganizerRequestEmail({ ...evt, additionalNotes }, eventType.metadata);
await sendAttendeeRequestEmailAndSMS({ ...evt, additionalNotes }, attendeesList[0], eventType.metadata);
}
}
@@ -1969,11 +1939,8 @@ async function handler(
// We are here so, booking doesn't require payment and booking is also created in DB already, through createBooking call
if (isConfirmedByDefault) {
const subscribersMeetingEnded = await monitorCallbackAsync(getWebhooks, subscriberOptionsMeetingEnded);
const subscribersMeetingStarted = await monitorCallbackAsync(
getWebhooks,
subscriberOptionsMeetingStarted
);
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
const subscribersMeetingStarted = await getWebhooks(subscriberOptionsMeetingStarted);
let deleteWebhookScheduledTriggerPromise: Promise<unknown> = Promise.resolve();
const scheduleTriggerPromises = [];
@@ -2024,7 +1991,7 @@ async function handler(
});
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
await monitorCallbackAsync(handleWebhookTrigger, {
await handleWebhookTrigger({
subscriberOptions,
eventTrigger,
webhookData,
@@ -2035,7 +2002,7 @@ async function handler(
const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REQUESTED;
subscriberOptions.triggerEvent = eventTrigger;
webhookData.status = "PENDING";
await monitorCallbackAsync(handleWebhookTrigger, {
await handleWebhookTrigger({
subscriberOptions,
eventTrigger,
webhookData,
@@ -2099,7 +2066,7 @@ async function handler(
}
try {
await monitorCallbackAsync(scheduleWorkflowReminders, {
await scheduleWorkflowReminders({
workflows,
smsReminderNumber: smsReminderNumber || null,
calendarEvent: evtWithMetadata,
@@ -2118,7 +2085,7 @@ async function handler(
try {
if (isConfirmedByDefault) {
await monitorCallbackAsync(scheduleNoShowTriggers, {
await scheduleNoShowTriggers({
booking: { startTime: booking.startTime, id: booking.id, location: booking.location },
triggerForUser,
organizerUser: { id: organizerUser.id },
@@ -2,6 +2,7 @@ import dayjs from "@calcom/dayjs";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { checkBookingLimits } from "@calcom/lib/intervalLimits/server/checkBookingLimits";
import { checkDurationLimits } from "@calcom/lib/intervalLimits/server/checkDurationLimits";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { NewBookingEventType } from "./getEventTypesFromDB";
@@ -13,7 +14,7 @@ type InputProps = {
reqBodyRescheduleUid?: string;
};
export const checkBookingAndDurationLimits = async ({
const _checkBookingAndDurationLimits = async ({
eventType,
reqBodyStart,
reqBodyRescheduleUid,
@@ -42,3 +43,8 @@ export const checkBookingAndDurationLimits = async ({
}
}
};
export const checkBookingAndDurationLimits = withReporting(
_checkBookingAndDurationLimits,
"checkBookingAndDurationLimits"
);
@@ -5,6 +5,7 @@ import type { z } from "zod";
import type { routingFormResponseInDbSchema } from "@calcom/app-store/routing-forms/zod";
import dayjs from "@calcom/dayjs";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { withReporting } from "@calcom/lib/sentryWrapper";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import type { CreationSource } from "@calcom/prisma/enums";
@@ -71,7 +72,8 @@ async function getAssociatedBookingForFormResponse(formResponseId: number) {
return formResponse?.routedToBookingUid ?? null;
}
export async function createBooking({
// Define the function with underscore prefix
const _createBooking = async ({
uid,
reqBody,
eventType,
@@ -83,7 +85,7 @@ export async function createBooking({
rescheduledBy,
creationSource,
tracking,
}: CreateBookingParams & { rescheduledBy: string | undefined }) {
}: CreateBookingParams & { rescheduledBy: string | undefined }) => {
updateEventDetails(evt, originalRescheduledBooking);
const associatedBookingForFormResponse = routingFormResponseId
? await getAssociatedBookingForFormResponse(routingFormResponseId)
@@ -126,7 +128,9 @@ export async function createBooking({
}
return true;
}
}
};
export const createBooking = withReporting(_createBooking, "createBooking");
async function saveBooking(
bookingAndAssociatedData: ReturnType<typeof buildNewBookingData>,
@@ -10,6 +10,7 @@ import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits";
import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { getEventTypeResponse } from "./getEventTypesFromDB";
import type { BookingType } from "./originalRescheduledBookingUtils";
@@ -50,7 +51,7 @@ const hasDateRangeForBooking = (
return dateRangeForBooking;
};
export async function ensureAvailableUsers(
const _ensureAvailableUsers = async (
eventType: Omit<getEventTypeResponse, "users"> & {
users: IsFixedAwareUser[];
},
@@ -58,7 +59,7 @@ export async function ensureAvailableUsers(
loggerWithEventDetails: Logger<unknown>,
shouldServeCache?: boolean
// ReturnType hint of at least one IsFixedAwareUser, as it's made sure at least one entry exists
): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> {
): Promise<[IsFixedAwareUser, ...IsFixedAwareUser[]]> => {
const availableUsers: IsFixedAwareUser[] = [];
const startDateTimeUtc = getDateTimeInUtc(input.dateFrom, input.timeZone);
@@ -162,4 +163,6 @@ export async function ensureAvailableUsers(
}
// make sure TypeScript understands availableUsers is at least one.
return availableUsers.length === 1 ? [availableUsers[0]] : [availableUsers[0], ...availableUsers.slice(1)];
}
};
export const ensureAvailableUsers = withReporting(_ensureAvailableUsers, "ensureAvailableUsers");
@@ -1,6 +1,8 @@
import { withReporting } from "@calcom/lib/sentryWrapper";
import prisma from "@calcom/prisma";
export const findBookingQuery = async (bookingId: number) => {
// Define the function with underscore prefix
const _findBookingQuery = async (bookingId: number) => {
const foundBooking = await prisma.booking.findUnique({
where: {
id: bookingId,
@@ -46,3 +48,5 @@ export const findBookingQuery = async (bookingId: number) => {
// Don't leak any sensitive data
return foundBooking;
};
export const findBookingQuery = withReporting(_findBookingQuery, "findBookingQuery");
@@ -4,6 +4,7 @@ import type z from "zod";
import dayjs from "@calcom/dayjs";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod/custom/booking";
import type { TgetBookingDataSchema } from "../getBookingDataSchema";
@@ -12,7 +13,8 @@ import { handleCustomInputs } from "./handleCustomInputs";
type ReqBodyWithEnd = TgetBookingDataSchema & { end: string };
export async function getBookingData<T extends z.ZodType>({
// Define the function with underscore prefix
const _getBookingData = async <T extends z.ZodType>({
reqBody,
eventType,
schema,
@@ -20,7 +22,7 @@ export async function getBookingData<T extends z.ZodType>({
reqBody: Record<string, any>;
eventType: getEventTypeResponse;
schema: T;
}) {
}) => {
const parsedBody = await schema.parseAsync(reqBody);
const parsedBodyWithEnd = (body: TgetBookingDataSchema): body is ReqBodyWithEnd => {
// Use the event length to auto-set the event end time.
@@ -78,9 +80,11 @@ export async function getBookingData<T extends z.ZodType>({
// So TS doesn't complain about unknown properties
customInputs: undefined,
};
}
};
export type AwaitedBookingData = Awaited<ReturnType<typeof getBookingData>>;
export const getBookingData = withReporting(_getBookingData, "getBookingData");
export type AwaitedBookingData = Awaited<ReturnType<typeof _getBookingData>>;
export type RescheduleReason = AwaitedBookingData["rescheduleReason"];
export type NoEmail = AwaitedBookingData["noEmail"];
export type AdditionalNotes = AwaitedBookingData["notes"];
@@ -0,0 +1,26 @@
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { getBookingFieldsWithSystemFields } from "../getBookingFields";
import { getEventTypesFromDB } from "./getEventTypesFromDB";
const _getEventType = async ({
eventTypeId,
eventTypeSlug,
}: {
eventTypeId: number;
eventTypeSlug?: string;
}) => {
// handle dynamic user
const eventType =
!eventTypeId && !!eventTypeSlug ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId);
const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId;
return {
...eventType,
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
};
};
export const getEventType = withReporting(_getEventType, "getEventType");
@@ -1,4 +1,5 @@
import { getFirstDelegationConferencingCredentialAppLocation } from "@calcom/lib/delegationCredential/server";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { Prisma } from "@calcom/prisma/client";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
@@ -14,7 +15,7 @@ const sortUsersByDynamicList = <TUser extends { username: string | null }>(
});
};
export const getLocationValuesForDb = <
const _getLocationValuesForDb = <
TUser extends {
username: string | null;
metadata: Prisma.JsonValue;
@@ -53,3 +54,5 @@ export const getLocationValuesForDb = <
organizerOrFirstDynamicGroupMemberDefaultLocationUrl: firstDynamicGroupMemberDefaultLocationUrl,
};
};
export const getLocationValuesForDb = withReporting(_getLocationValuesForDb, "getLocationValuesForDb");
@@ -1,5 +1,6 @@
import dayjs from "@calcom/dayjs";
import { checkIfFreeEmailDomain } from "@calcom/lib/freeEmailDomainCheck/checkIfFreeEmailDomain";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { getEventTypeResponse } from "./getEventTypesFromDB";
@@ -44,11 +45,12 @@ export async function getRequiresConfirmationFlags({
};
}
async function determineRequiresConfirmation(
// Define the function with underscore prefix
const _determineRequiresConfirmation = async (
eventType: EventType,
bookingStartTime: string,
bookerEmail: string
): Promise<boolean> {
): Promise<boolean> => {
let requiresConfirmation = eventType?.requiresConfirmation;
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
const requiresConfirmationForFreeEmail = eventType?.requiresConfirmationForFreeEmail;
@@ -65,7 +67,12 @@ async function determineRequiresConfirmation(
}
return requiresConfirmation;
}
};
export const determineRequiresConfirmation = withReporting(
_determineRequiresConfirmation,
"determineRequiresConfirmation"
);
function isUserReschedulingOwner(
userId: number | undefined,
@@ -8,6 +8,7 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import { HttpError } from "@calcom/lib/http-error";
import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { RoutingFormResponse } from "@calcom/lib/server/getLuckyUser";
import { withSelectedCalendars } from "@calcom/lib/server/repository/user";
import { userSelect } from "@calcom/prisma";
@@ -67,7 +68,7 @@ type InputProps = {
forcedSlug: string | undefined;
};
export async function loadAndValidateUsers({
const _loadAndValidateUsers = async ({
eventType,
eventTypeId,
dynamicUserList,
@@ -83,7 +84,7 @@ export async function loadAndValidateUsers({
qualifiedRRUsers: UsersWithDelegationCredentials;
additionalFallbackRRUsers: UsersWithDelegationCredentials;
fixedUsers: UsersWithDelegationCredentials;
}> {
}> => {
let users: Users = await loadUsers({
eventType,
dynamicUserList,
@@ -222,4 +223,6 @@ export async function loadAndValidateUsers({
additionalFallbackRRUsers, // without qualified
fixedUsers,
};
}
};
export const loadAndValidateUsers = withReporting(_loadAndValidateUsers, "loadAndValidateUsers");
@@ -2,6 +2,7 @@ import { DailyLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import tasker from "@calcom/features/tasker";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
type ScheduleNoShowTriggersArgs = {
@@ -19,7 +20,7 @@ type ScheduleNoShowTriggersArgs = {
isDryRun?: boolean;
};
export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) => {
const _scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) => {
const {
booking,
triggerForUser,
@@ -110,3 +111,5 @@ export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) =
// (workflow) => workflow.trigger === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW
// );
};
export const scheduleNoShowTriggers = withReporting(_scheduleNoShowTriggers, "scheduleNoShowTriggers");
@@ -4,6 +4,7 @@ import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { HttpError } from "@calcom/lib/http-error";
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { EventType } from "@calcom/prisma/client";
type ValidateBookingTimeEventType = Pick<
@@ -19,7 +20,8 @@ type ValidateBookingTimeEventType = Pick<
| "title"
>;
export const validateBookingTimeIsNotOutOfBounds = async <T extends ValidateBookingTimeEventType>(
// Define the function with underscore prefix
const _validateBookingTimeIsNotOutOfBounds = async <T extends ValidateBookingTimeEventType>(
reqBodyStartTime: string,
reqBodyTimeZone: string,
eventType: T,
@@ -54,3 +56,8 @@ export const validateBookingTimeIsNotOutOfBounds = async <T extends ValidateBook
if (timeOutOfBounds) throw new HttpError({ statusCode: 400, message: ErrorCode.BookingTimeOutOfBounds });
};
export const validateBookingTimeIsNotOutOfBounds = withReporting(
_validateBookingTimeIsNotOutOfBounds,
"validateBookingTimeIsNotOutOfBounds"
);
@@ -2,6 +2,7 @@ import type { Logger } from "tslog";
import dayjs from "@calcom/dayjs";
import { HttpError } from "@calcom/lib/http-error";
import { withReporting } from "@calcom/lib/sentryWrapper";
type Props = {
reqBodyStart: string;
@@ -11,7 +12,8 @@ type Props = {
logger: Logger<unknown>;
};
export const validateEventLength = ({
// Define the function with underscore prefix
const _validateEventLength = ({
reqBodyStart,
reqBodyEnd,
eventTypeMultipleDuration,
@@ -25,3 +27,5 @@ export const validateEventLength = ({
throw new HttpError({ statusCode: 400, message: "Invalid event length" });
}
};
export const validateEventLength = withReporting(_validateEventLength, "validateEventLength");
@@ -4,8 +4,9 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import { isEventPayload, type WebhookPayloadType } from "@calcom/features/webhooks/lib/sendPayload";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
export async function handleWebhookTrigger(args: {
async function _handleWebhookTrigger(args: {
subscriberOptions: GetSubscriberOptions;
eventTrigger: string;
webhookData: WebhookPayloadType;
@@ -32,3 +33,5 @@ export async function handleWebhookTrigger(args: {
logger.error("Error while sending webhook", error);
}
}
export const handleWebhookTrigger = withReporting(_handleWebhookTrigger, "handleWebhookTrigger");
@@ -1,6 +1,7 @@
import type { FormResponse, Fields } from "@calcom/app-store/routing-forms/types/types";
import { zodRoutes } from "@calcom/app-store/routing-forms/zod";
import { acrossQueryValueCompatiblity } from "@calcom/lib/raqb/raqbUtils";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { getUsersAttributes } from "@calcom/lib/service/attribute/server/getAttributes";
import prisma from "@calcom/prisma";
import { AssignmentReasonEnum } from "@calcom/prisma/enums";
@@ -13,7 +14,16 @@ export enum RRReassignmentType {
}
export default class AssignmentReasonRecorder {
static async routingFormRoute({
/**
* We should use decorators to wrap the methods with withReporting
* but we can't don't have support for static methods in decorators
* so this is a workaround to wrap the methods with withReporting.
*/
static routingFormRoute = withReporting(
AssignmentReasonRecorder._routingFormRoute,
"AssignmentReasonRecorder.routingFormRoute"
);
static async _routingFormRoute({
bookingId,
routingFormResponseId,
organizerId,
@@ -113,8 +123,11 @@ export default class AssignmentReasonRecorder {
}
// Separate method to handle rerouting
static async CRMOwnership({
static CRMOwnership = withReporting(
AssignmentReasonRecorder._CRMOwnership,
"AssignmentReasonRecorder.CRMOwnership"
);
static async _CRMOwnership({
bookingId,
crmAppSlug,
teamMemberEmail,
@@ -143,8 +156,11 @@ export default class AssignmentReasonRecorder {
},
});
}
static async roundRobinReassignment({
static roundRobinReassignment = withReporting(
AssignmentReasonRecorder._roundRobinReassignment,
"AssignmentReasonRecorder.roundRobinReassignment"
);
static async _roundRobinReassignment({
bookingId,
reassignById,
reassignReason,
@@ -6,6 +6,7 @@ import {
import type { Workflow, WorkflowStep } from "@calcom/features/ee/workflows/lib/types";
import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
import { SENDER_NAME } from "@calcom/lib/constants";
import { withReporting } from "@calcom/lib/sentryWrapper";
import prisma from "@calcom/prisma";
import { SchedulingType, WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -177,7 +178,7 @@ const processWorkflowStep = async (
}
};
export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersArgs) => {
const _scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersArgs) => {
const {
workflows,
smsReminderNumber,
@@ -232,7 +233,7 @@ export interface SendCancelledRemindersArgs {
hideBranding?: boolean;
}
export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) => {
const _sendCancelledReminders = async (args: SendCancelledRemindersArgs) => {
const { smsReminderNumber, evt, workflows, hideBranding } = args;
if (!workflows.length) return;
@@ -241,7 +242,7 @@ export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) =
if (workflow.trigger !== WorkflowTriggerEvents.EVENT_CANCELLED) continue;
for (const step of workflow.steps) {
processWorkflowStep(workflow, step, {
await processWorkflowStep(workflow, step, {
smsReminderNumber,
hideBranding,
calendarEvent: evt,
@@ -249,3 +250,10 @@ export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) =
}
}
};
// Export functions wrapped with withReporting
export const scheduleWorkflowReminders = withReporting(
_scheduleWorkflowReminders,
"scheduleWorkflowReminders"
);
export const sendCancelledReminders = withReporting(_sendCancelledReminders, "sendCancelledReminders");
@@ -3,6 +3,7 @@ import { scheduleEmailReminder } from "@calcom/features/ee/workflows/lib/reminde
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
import type { getDefaultEvent } from "@calcom/lib/defaultEvents";
import logger from "@calcom/lib/logger";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { WorkflowTriggerEvents, TimeUnit, WorkflowActions, WorkflowTemplates } from "@calcom/prisma/enums";
import type { ExtendedCalendarEvent } from "./reminderScheduler";
@@ -11,7 +12,7 @@ const log = logger.getSubLogger({ prefix: ["[scheduleMandatoryReminder]"] });
export type NewBookingEventType = Awaited<ReturnType<typeof getDefaultEvent>> | getEventTypeResponse;
export async function scheduleMandatoryReminder({
async function _scheduleMandatoryReminder({
evt,
workflows,
requiresConfirmation,
@@ -75,3 +76,8 @@ export async function scheduleMandatoryReminder({
log.error("Error while scheduling mandatory reminders", JSON.stringify({ error }));
}
}
export const scheduleMandatoryReminder = withReporting(
_scheduleMandatoryReminder,
"scheduleMandatoryReminder"
);
@@ -1,3 +1,4 @@
import { withReporting } from "@calcom/lib/sentryWrapper";
import defaultPrisma from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
@@ -80,4 +81,4 @@ const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient =
return allWebhooks;
};
export default getWebhooks;
export default withReporting(getWebhooks, "getWebhooks");
@@ -6,6 +6,7 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR
import { getHumanReadableLocationValue } from "@calcom/lib/location";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import type { ApiKey } from "@calcom/prisma/client";
@@ -304,7 +305,7 @@ export async function scheduleTrigger({
}
}
export async function deleteWebhookScheduledTriggers({
async function _deleteWebhookScheduledTriggers({
booking,
appId,
triggerEvent,
@@ -361,6 +362,11 @@ export async function deleteWebhookScheduledTriggers({
}
}
export const deleteWebhookScheduledTriggers = withReporting(
_deleteWebhookScheduledTriggers,
"deleteWebhookScheduledTriggers"
);
export async function updateTriggerForExistingBookings(
webhook: Webhook,
existingEventTriggers: WebhookTriggerEvents[],
@@ -3,6 +3,7 @@ import {
getNormalizedHostsWithDelegationCredentials,
} from "@calcom/lib/bookings/getRoutedUsers";
import type { EventType } from "@calcom/lib/bookings/getRoutedUsers";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { SelectedCalendar } from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
@@ -46,7 +47,7 @@ const isFixedHost = <T extends { isFixed: boolean }>(host: T): host is T & { isF
return host.isFixed;
};
export const findQualifiedHostsWithDelegationCredentials = async <
const _findQualifiedHostsWithDelegationCredentials = async <
T extends {
email: string;
id: number;
@@ -206,3 +207,8 @@ export const findQualifiedHostsWithDelegationCredentials = async <
fixedHosts,
};
};
export const findQualifiedHostsWithDelegationCredentials = withReporting(
_findQualifiedHostsWithDelegationCredentials,
"findQualifiedHostsWithDelegationCredentials"
);
+7 -4
View File
@@ -9,6 +9,7 @@ import { intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import logger from "@calcom/lib/logger";
import { getPiiFreeBooking } from "@calcom/lib/piiFreeData";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { performance } from "@calcom/lib/server/perfObserver";
import prisma from "@calcom/prisma";
import type { SelectedCalendar } from "@calcom/prisma/client";
@@ -19,7 +20,7 @@ import type { CredentialForCalendarService } from "@calcom/types/Credential";
import { getDefinedBufferTimes } from "../features/eventtypes/lib/getDefinedBufferTimes";
import { BookingRepository as BookingRepo } from "./server/repository/booking";
export async function getBusyTimes(params: {
const _getBusyTimes = async (params: {
credentials: CredentialForCalendarService[];
userId: number;
userEmail: string;
@@ -46,7 +47,7 @@ export async function getBusyTimes(params: {
| null;
bypassBusyCalendarTimes: boolean;
shouldServeCache?: boolean;
}) {
}) => {
const {
credentials,
userId,
@@ -244,7 +245,9 @@ export async function getBusyTimes(params: {
})
);
return busyTimes;
}
};
export const getBusyTimes = withReporting(_getBusyTimes, "getBusyTimes");
export function getStartEndDateforLimitCheck(
startDate: string,
@@ -359,4 +362,4 @@ export async function getBusyTimesForLimitChecks(params: {
return busyTimes;
}
export default getBusyTimes;
export default withReporting(_getBusyTimes, "getBusyTimes");
+17 -36
View File
@@ -35,7 +35,7 @@ import type { EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar
import type { TimeRange } from "@calcom/types/schedule";
import { getBusyTimes } from "./getBusyTimes";
import monitorCallbackAsync, { monitorCallbackSync } from "./sentryWrapper";
import { withReporting } from "./sentryWrapper";
const log = logger.getSubLogger({ prefix: ["getUserAvailability"] });
const availabilitySchema = z
@@ -55,12 +55,6 @@ const availabilitySchema = z
})
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
const getEventType = async (
...args: Parameters<typeof _getEventType>
): Promise<ReturnType<typeof _getEventType>> => {
return monitorCallbackAsync(_getEventType, ...args);
};
const _getEventType = async (id: number) => {
const eventType = await prisma.eventType.findUnique({
where: { id },
@@ -150,17 +144,17 @@ const _getEventType = async (id: number) => {
};
};
export type EventType = Awaited<ReturnType<typeof getEventType>>;
export type EventType = Awaited<ReturnType<typeof _getEventType>>;
const getUser = async (...args: Parameters<typeof _getUser>): Promise<ReturnType<typeof _getUser>> => {
return monitorCallbackAsync(_getUser, ...args);
};
export const getEventType = withReporting(_getEventType, "getEventType");
const _getUser = async (where: Prisma.UserWhereInput) => {
return findUsersForAvailabilityCheck({ where });
};
type GetUser = Awaited<ReturnType<typeof getUser>>;
type GetUser = Awaited<ReturnType<typeof _getUser>>;
const getUser = withReporting(_getUser, "getUser");
export type GetUserAvailabilityInitialData = {
user?: GetUser;
@@ -213,12 +207,6 @@ type GetUserAvailabilityQuery = {
shouldServeCache?: boolean;
};
export const getCurrentSeats = async (
...args: Parameters<typeof _getCurrentSeats>
): Promise<ReturnType<typeof _getCurrentSeats>> => {
return monitorCallbackAsync(_getCurrentSeats, ...args);
};
const _getCurrentSeats = async (
eventType: {
id?: number;
@@ -274,13 +262,9 @@ const _getCurrentSeats = async (
});
};
export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
export type CurrentSeats = Awaited<ReturnType<typeof _getCurrentSeats>>;
export const getUserAvailability = async (
...args: Parameters<typeof _getUserAvailability>
): Promise<GetUserAvailabilityResult> => {
return monitorCallbackAsync(_getUserAvailability, ...args);
};
export const getCurrentSeats = withReporting(_getCurrentSeats, "getCurrentSeats");
type GetUserAvailabilityResult = ReturnType<typeof _getUserAvailability>;
@@ -416,7 +400,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
? EventTypeRepository.getSelectedCalendarsFromUser({ user, eventTypeId: eventType.id })
: user.userLevelSelectedCalendars;
const busyTimes = await monitorCallbackAsync(getBusyTimes, {
const busyTimes = await getBusyTimes({
credentials: user.credentials,
startTime: getBusyTimesStart,
endTime: getBusyTimesEnd,
@@ -613,18 +597,14 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
return result;
};
export const getPeriodStartDatesBetween = (
...args: Parameters<typeof _getPeriodStartDatesBetween>
): ReturnType<typeof _getPeriodStartDatesBetween> => {
return monitorCallbackSync(_getPeriodStartDatesBetween, ...args);
};
export const getUserAvailability = withReporting(_getUserAvailability, "getUserAvailability");
const _getPeriodStartDatesBetween = (
dateFrom: Dayjs,
dateTo: Dayjs,
period: IntervalLimitUnit,
timeZone?: string
) => {
): Dayjs[] => {
const dates = [];
let startDate = timeZone ? dayjs(dateFrom).tz(timeZone).startOf(period) : dayjs(dateFrom).startOf(period);
const endDate = timeZone ? dayjs(dateTo).tz(timeZone).endOf(period) : dayjs(dateTo).endOf(period);
@@ -636,6 +616,11 @@ const _getPeriodStartDatesBetween = (
return dates;
};
export const getPeriodStartDatesBetween = withReporting(
_getPeriodStartDatesBetween,
"getPeriodStartDatesBetween"
);
interface GetUserAvailabilityParamsDTO {
availability: (DateOverride | WorkingHours)[];
}
@@ -735,8 +720,4 @@ const _getUsersAvailability = async ({ users, query, initialData }: GetUsersAvai
);
};
export const getUsersAvailability = async (
...args: Parameters<typeof _getUsersAvailability>
): Promise<Awaited<GetUserAvailabilityResult>[]> => {
return monitorCallbackAsync(_getUsersAvailability, ...args);
};
export const getUsersAvailability = withReporting(_getUsersAvailability, "getUsersAvailability");
@@ -1,6 +1,7 @@
import dayjs from "@calcom/dayjs";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError } from "@calcom/lib/http-error";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -9,7 +10,7 @@ import { ascendingLimitKeys, intervalLimitKeyToUnit } from "../intervalLimit";
import type { IntervalLimit, IntervalLimitKey } from "../intervalLimitSchema";
import { parseBookingLimit } from "../isBookingLimits";
export async function checkBookingLimits(
async function _checkBookingLimits(
bookingLimits: IntervalLimit,
eventStartDate: Date,
eventId: number,
@@ -40,7 +41,9 @@ export async function checkBookingLimits(
}
}
export async function checkBookingLimit({
export const checkBookingLimits = withReporting(_checkBookingLimits, "checkBookingLimits");
async function _checkBookingLimit({
eventStartDate,
eventId,
key,
@@ -110,3 +113,5 @@ export async function checkBookingLimit({
});
}
}
export const checkBookingLimit = withReporting(_checkBookingLimit, "checkBookingLimit");
@@ -3,7 +3,7 @@ import dayjs from "@calcom/dayjs";
import { getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
import type { EventType } from "@calcom/lib/getUserAvailability";
import { getPeriodStartDatesBetween } from "@calcom/lib/getUserAvailability";
import monitorCallbackAsync from "@calcom/lib/sentryWrapper";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { performance } from "@calcom/lib/server/perfObserver";
import { getTotalBookingDuration } from "@calcom/lib/server/queries";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
@@ -15,12 +15,6 @@ import LimitManager from "../limitManager";
import { isBookingWithinPeriod } from "../utils";
import { checkBookingLimit } from "./checkBookingLimits";
export const getBusyTimesFromLimits = async (
...args: Parameters<typeof _getBusyTimesFromLimits>
): Promise<ReturnType<typeof _getBusyTimesFromLimits>> => {
return monitorCallbackAsync(_getBusyTimesFromLimits, ...args);
};
const _getBusyTimesFromLimits = async (
bookingLimits: IntervalLimit | null,
durationLimits: IntervalLimit | null,
@@ -78,12 +72,6 @@ const _getBusyTimesFromLimits = async (
return limitManager.getBusyTimes();
};
const getBusyTimesFromBookingLimits = async (
...args: Parameters<typeof _getBusyTimesFromBookingLimits>
): Promise<ReturnType<typeof _getBusyTimesFromBookingLimits>> => {
return monitorCallbackAsync(_getBusyTimesFromBookingLimits, ...args);
};
const _getBusyTimesFromBookingLimits = async (params: {
bookings: EventBusyDetails[];
bookingLimits: IntervalLimit;
@@ -161,13 +149,6 @@ const _getBusyTimesFromBookingLimits = async (params: {
}
}
};
const getBusyTimesFromDurationLimits = async (
...args: Parameters<typeof _getBusyTimesFromDurationLimits>
): Promise<ReturnType<typeof _getBusyTimesFromDurationLimits>> => {
return monitorCallbackAsync(_getBusyTimesFromDurationLimits, ...args);
};
const _getBusyTimesFromDurationLimits = async (
bookings: EventBusyDetails[],
durationLimits: IntervalLimit,
@@ -231,11 +212,10 @@ const _getBusyTimesFromDurationLimits = async (
}
};
export const getBusyTimesFromTeamLimits = async (
...args: Parameters<typeof _getBusyTimesFromTeamLimits>
): Promise<ReturnType<typeof _getBusyTimesFromTeamLimits>> => {
return monitorCallbackAsync(_getBusyTimesFromTeamLimits, ...args);
};
const getBusyTimesFromDurationLimits = withReporting(
_getBusyTimesFromDurationLimits,
"getBusyTimesFromDurationLimits"
);
const _getBusyTimesFromTeamLimits = async (
user: { id: number; email: string },
@@ -287,3 +267,15 @@ const _getBusyTimesFromTeamLimits = async (
return limitManager.getBusyTimes();
};
export const getBusyTimesFromLimits = withReporting(_getBusyTimesFromLimits, "getBusyTimesFromLimits");
export const getBusyTimesFromBookingLimits = withReporting(
_getBusyTimesFromBookingLimits,
"getBusyTimesFromBookingLimits"
);
export const getBusyTimesFromTeamLimits = withReporting(
_getBusyTimesFromTeamLimits,
"getBusyTimesFromTeamLimits"
);
+178
View File
@@ -0,0 +1,178 @@
import { describe, it, expect } from "vitest";
import { redactSensitiveData } from "./redactSensitiveData";
describe("redactSensitiveData", () => {
it("should return non-object values as is", () => {
expect(redactSensitiveData(null)).toMatchInlineSnapshot(`null`);
expect(redactSensitiveData(undefined)).toMatchInlineSnapshot(`undefined`);
expect(redactSensitiveData("string")).toMatchInlineSnapshot(`"string"`);
expect(redactSensitiveData(123)).toMatchInlineSnapshot(`123`);
expect(redactSensitiveData(true)).toMatchInlineSnapshot(`true`);
});
it("should redact sensitive fields from flat objects", () => {
const input = {
username: "test",
password: "secret123",
apiKey: "key123",
token: "token123",
normalField: "value",
};
expect(redactSensitiveData(input)).toMatchInlineSnapshot(`
{
"apiKey": "[REDACTED]",
"normalField": "value",
"password": "[REDACTED]",
"token": "[REDACTED]",
"username": "test",
}
`);
});
it("should redact sensitive fields from nested objects", () => {
const input = {
user: {
name: "John",
password: "secret123",
credentials: {
apiKey: "key123",
token: "token123",
},
},
settings: {
theme: "dark",
auth: {
secret: "secret123",
},
},
};
expect(redactSensitiveData(input)).toMatchInlineSnapshot(`
{
"settings": {
"auth": "[REDACTED]",
"theme": "dark",
},
"user": {
"credentials": {
"apiKey": "[REDACTED]",
"token": "[REDACTED]",
},
"name": "John",
"password": "[REDACTED]",
},
}
`);
});
it("should handle arrays of objects", () => {
const input = {
users: [
{ name: "John", password: "pass1", token: "token1" },
{ name: "Jane", password: "pass2", token: "token2" },
],
config: {
apiKey: "key123",
items: [
{ id: 1, secret: "secret1" },
{ id: 2, secret: "secret2" },
],
},
};
expect(redactSensitiveData(input)).toMatchInlineSnapshot(`
{
"config": {
"apiKey": "[REDACTED]",
"items": [
{
"id": 1,
"secret": "[REDACTED]",
},
{
"id": 2,
"secret": "[REDACTED]",
},
],
},
"users": [
{
"name": "John",
"password": "[REDACTED]",
"token": "[REDACTED]",
},
{
"name": "Jane",
"password": "[REDACTED]",
"token": "[REDACTED]",
},
],
}
`);
});
it("should handle all sensitive field variations", () => {
const input = {
accessToken: "token1",
api_key: "key1",
apiKey: "key2",
auth: "auth1",
authorization: "auth2",
client_id: "id1",
client_secret: "secret1",
credential: "cred1",
hash: "hash1",
key: "key3",
password: "pass1",
refreshToken: "token2",
secret: "secret2",
token: "token3",
normalField: "value",
};
expect(redactSensitiveData(input)).toMatchInlineSnapshot(`
{
"accessToken": "[REDACTED]",
"apiKey": "[REDACTED]",
"api_key": "[REDACTED]",
"auth": "[REDACTED]",
"authorization": "[REDACTED]",
"client_id": "[REDACTED]",
"client_secret": "[REDACTED]",
"credential": "[REDACTED]",
"hash": "[REDACTED]",
"key": "[REDACTED]",
"normalField": "value",
"password": "[REDACTED]",
"refreshToken": "[REDACTED]",
"secret": "[REDACTED]",
"token": "[REDACTED]",
}
`);
});
it("should handle circular references gracefully", () => {
const input: any = { name: "test" };
input.self = input;
expect(redactSensitiveData(input)).toMatchInlineSnapshot(`
{
"name": "test",
"self": "[Circular]",
}
`);
});
it("should handle invalid JSON data gracefully", () => {
const input = {
name: "test",
invalid: {
toJSON: () => {
throw new Error("Invalid JSON");
},
},
};
expect(redactSensitiveData(input)).toMatchInlineSnapshot(`
{
"error": "Redaction failed",
}
`);
});
});
+84
View File
@@ -0,0 +1,84 @@
import logger from "./logger";
// Fields that might contain sensitive data
const SENSITIVE_FIELDS = [
"accessToken",
"api_key",
"apiKey",
"auth",
"authorization",
"client_id",
"client_secret",
"credential",
"hash",
"key",
"password",
"refreshToken",
"secret",
"token",
];
// Create a Set for efficient O(1) average time complexity lookups
const SENSITIVE_FIELDS_SET = new Set<string>(SENSITIVE_FIELDS);
/**
* Creates a replacer function for JSON.stringify that redacts sensitive fields.
* This approach is more efficient as it leverages native JSON serialization.
*/
function createReplacer(): (key: string, value: unknown) => unknown {
// Use a WeakSet to track circular references
const seen = new WeakSet();
return function replacer(key: string, value: unknown): unknown {
// Handle circular references
if (value && typeof value === "object") {
if (seen.has(value)) {
return "[Circular]";
}
seen.add(value);
}
// Redact sensitive fields
if (SENSITIVE_FIELDS_SET.has(key)) {
return "[REDACTED]";
}
// Handle Error objects specially to preserve their structure
if (value instanceof Error) {
const errorObj: Record<string, unknown> = {
name: value.name,
message: value.message,
};
if (value.stack) {
errorObj.stack = value.stack;
}
// Handle cause property safely
if ("cause" in value && value.cause !== undefined) {
errorObj.cause = value.cause;
}
return errorObj;
}
return value;
};
}
/**
* Redacts sensitive data from an object by replacing sensitive fields with a placeholder.
* Uses JSON.stringify's replacer function for efficient serialization and redaction.
*/
export function redactSensitiveData(data: unknown): unknown {
if (!data || typeof data !== "object") return data;
try {
const replacer = createReplacer();
const jsonString = JSON.stringify(data, replacer);
return JSON.parse(jsonString);
} catch (error) {
logger.error("Failed to redact sensitive data", error);
return { error: "Redaction failed" };
}
}
+89
View File
@@ -1,5 +1,7 @@
import { startSpan, captureException } from "@sentry/nextjs";
import { redactSensitiveData } from "./redactSensitiveData";
/*
WHEN TO USE
We ran a script that performs a simple mathematical calculation within a loop of 1000000 iterations.
@@ -10,6 +12,10 @@ For smaller loops, the cost incurred may not be very significant on an absolute
considering that a million monitored iterations only took roughly 8 seconds when monitored.
*/
/**
* @deprecated Use withReporting instead. This function will be removed in a future version.
* Example: withReporting(myFunction, "myFunction")(...args)
*/
const monitorCallbackAsync = async <T extends (...args: any[]) => any>(
cb: T,
...args: Parameters<T>
@@ -30,6 +36,10 @@ const monitorCallbackAsync = async <T extends (...args: any[]) => any>(
});
};
/**
* @deprecated Use withReporting instead. This function will be removed in a future version.
* Example: withReporting(myFunction, "myFunction")(...args)
*/
const monitorCallbackSync = <T extends (...args: any[]) => any>(
cb: T,
...args: Parameters<T>
@@ -49,5 +59,84 @@ const monitorCallbackSync = <T extends (...args: any[]) => any>(
});
};
/**
* Type guard to check if a value is a Promise
*/
function isPromise<T>(value: unknown): value is Promise<T> {
return value instanceof Promise;
}
/**
* Creates an error handler that captures the error in Sentry with context
*/
function createErrorHandler(name: string, args: unknown[]) {
return (error: unknown) => {
const context = {
functionName: name,
args: redactSensitiveData(args),
};
if (error instanceof Error) {
(error as any).context = context;
}
captureException(error, {
extra: context,
tags: { functionName: name },
});
throw error;
};
}
/**
* Recommended way to wrap functions for Sentry reporting.
* Properly handles both sync and async functions with proper error context capture.
*
* @example
* // Async function
* const myAsyncFunction = withReporting(async (arg1: string, arg2: number) => {
* // async implementation
* }, "myAsyncFunction");
*
* // Sync function
* const mySyncFunction = withReporting((arg1: string, arg2: number) => {
* // sync implementation
* }, "mySyncFunction");
*/
export function withReporting<T extends any[], R>(func: (...args: T) => R, name: string): (...args: T) => R {
if (!name?.trim()) {
throw new Error("withReporting requires a non-empty name parameter");
}
// Early return if Sentry is not configured
if (!process.env.NEXT_PUBLIC_SENTRY_DSN || !process.env.SENTRY_TRACES_SAMPLE_RATE) {
return func;
}
return (...args: T): R => {
return startSpan({ name }, () => {
const onError = createErrorHandler(name, args);
try {
const result = func(...args);
// Handle Promise case with proper type guard
if (isPromise(result)) {
return result.catch(onError) as R;
}
// Handle sync case
return result;
} catch (error) {
onError(error);
// This line is technically unreachable due to onError throwing,
// but TypeScript needs it for type safety
throw error;
}
});
};
}
export default monitorCallbackAsync;
export { monitorCallbackSync };
+3 -5
View File
@@ -18,7 +18,7 @@ import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm";
import logger from "@calcom/lib/logger";
import monitorCallbackAsync from "@calcom/lib/sentryWrapper";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
import { UserRepository } from "@calcom/lib/server/repository/user";
@@ -36,10 +36,6 @@ function hasEmbedPath(pathWithQuery: string) {
return onlyPath.endsWith("/embed") || onlyPath.endsWith("/embed/");
}
export const getRoutedUrl = (context: Pick<GetServerSidePropsContext, "query" | "req">) => {
return monitorCallbackAsync(_getRoutedUrl, context);
};
const _getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "query" | "req">) => {
const queryParsed = querySchema.safeParse(context.query);
const isEmbed = hasEmbedPath(context.req.url || "");
@@ -221,3 +217,5 @@ const _getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "query" |
},
};
};
export const getRoutedUrl = withReporting(_getRoutedUrl, "getRoutedUrl");
+7 -1
View File
@@ -1,6 +1,7 @@
import { Prisma } from "@prisma/client";
import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
import { withReporting } from "@calcom/lib/sentryWrapper";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { Booking } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -165,7 +166,7 @@ export class BookingRepository {
});
}
static async findAllExistingBookingsForEventTypeBetween({
private static async _findAllExistingBookingsForEventTypeBetween({
eventTypeId,
seatedEvent = false,
startDate,
@@ -277,6 +278,11 @@ export class BookingRepository {
return [...resultOne, ...resultTwoWithOrganizersRemoved, ...resultThree];
}
static findAllExistingBookingsForEventTypeBetween = withReporting(
BookingRepository._findAllExistingBookingsForEventTypeBetween,
"findAllExistingBookingsForEventTypeBetween"
);
static async getAllBookingsForRoundRobin({
users,
eventTypeId,
+2 -1
View File
@@ -1,6 +1,7 @@
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import type { IFromUser, IOutOfOfficeData, IToUser } from "@calcom/lib/getUserAvailability";
import { withReporting } from "@calcom/lib/sentryWrapper";
import type { DateRange } from "./date-ranges";
import { getTimeZone } from "./dayjs";
@@ -168,4 +169,4 @@ const getSlots = ({
});
};
export default getSlots;
export default withReporting(getSlots, "getSlots");
+218 -202
View File
@@ -18,19 +18,19 @@ import { getAggregatedAvailability } from "@calcom/lib/getAggregatedAvailability
import { getBusyTimesForLimitChecks, getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
import type {
CurrentSeats,
EventType,
GetAvailabilityUser,
IFromUser,
IToUser,
EventType,
} from "@calcom/lib/getUserAvailability";
import { getUsersAvailability, getPeriodStartDatesBetween } from "@calcom/lib/getUserAvailability";
import { getPeriodStartDatesBetween, getUsersAvailability } from "@calcom/lib/getUserAvailability";
import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits";
import LimitManager from "@calcom/lib/intervalLimits/limitManager";
import { checkBookingLimit } from "@calcom/lib/intervalLimits/server/checkBookingLimits";
import { isBookingWithinPeriod, getUnitFromBusyTime } from "@calcom/lib/intervalLimits/utils";
import { getUnitFromBusyTime, isBookingWithinPeriod } from "@calcom/lib/intervalLimits/utils";
import {
calculatePeriodLimits,
isTimeOutOfBounds,
@@ -38,7 +38,7 @@ import {
} from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import monitorCallbackAsync, { monitorCallbackSync } from "@calcom/lib/sentryWrapper";
import { withReporting } from "@calcom/lib/sentryWrapper";
import { getTotalBookingDuration } from "@calcom/lib/server/queries";
import { BookingRepository as BookingRepo } from "@calcom/lib/server/repository/booking";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
@@ -49,7 +49,7 @@ import { PeriodType, Prisma } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { EventBusyDate, EventBusyDetails } from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialForCalendarService } from "@calcom/types/Credential";
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
import { TRPCError } from "@trpc/server";
@@ -158,10 +158,10 @@ async function _getReservedSlotsAndCleanupExpired({
}
}
export async function getEventType(
const _getEventType = async (
input: TGetScheduleInputSchema,
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
) {
) => {
const { eventTypeSlug, usernameList, isTeamEvent } = input;
log.info("getEventType", safeStringify({ usernameList, eventTypeSlug, isTeamEvent, organizationDetails }));
const eventTypeId =
@@ -179,12 +179,14 @@ export async function getEventType(
}
return await EventTypeRepository.findForSlots({ id: eventTypeId });
}
};
export async function getDynamicEventType(
export const getEventType = withReporting(_getEventType, "getEventType");
const _getDynamicEventType = async (
input: TGetScheduleInputSchema,
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
) {
) => {
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
// For dynamic booking, we need to get and update user credentials, schedule and availability in the eventTypeObject as they're required in the new availability logic
if (!input.eventTypeSlug) {
@@ -226,17 +228,24 @@ export async function getDynamicEventType(
return Object.assign({}, dynamicEventType, {
users,
});
}
};
export async function getRegularOrDynamicEventType(
export const getDynamicEventType = withReporting(_getDynamicEventType, "getDynamicEventType");
const _getRegularOrDynamicEventType = async (
input: TGetScheduleInputSchema,
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
) {
) => {
const isDynamicBooking = input.usernameList && input.usernameList.length > 1;
return isDynamicBooking
? await getDynamicEventType(input, organizationDetails)
: await getEventType(input, organizationDetails);
}
};
export const getRegularOrDynamicEventType = withReporting(
_getRegularOrDynamicEventType,
"getRegularOrDynamicEventType"
);
function applyOccupiedSeatsToCurrentSeats(currentSeats: CurrentSeats, occupiedSeats: SelectedSlots[]) {
const occupiedSeatsCount = countBy(occupiedSeats, (item) => item.slotUtcStartDate.toISOString());
@@ -267,16 +276,18 @@ export interface IGetAvailableSlots {
troubleshooter?: any;
}
export function getUsersWithCredentials({
const _getUsersWithCredentials = ({
hosts,
}: {
hosts: {
isFixed?: boolean;
user: GetAvailabilityUserWithDelegationCredentials;
}[];
}) {
}) => {
return hosts.map(({ isFixed, user }) => ({ isFixed, ...user }));
}
};
export const getUsersWithCredentials = withReporting(_getUsersWithCredentials, "getUsersWithCredentials");
const getStartTime = (startTimeInput: string, timeZone?: string, minimumBookingNotice?: number) => {
const startTimeMin = dayjs.utc().add(minimumBookingNotice || 1, "minutes");
@@ -285,13 +296,7 @@ const getStartTime = (startTimeInput: string, timeZone?: string, minimumBookingN
return startTimeMin.isAfter(startTime) ? startTimeMin.tz(timeZone) : startTime;
};
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 _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise<IGetAvailableSlots> => {
const {
_enableTroubleshooter: enableTroubleshooter = false,
_bypassCalendarBusyTimes: bypassBusyCalendarTimes = false,
@@ -309,7 +314,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
logger.settings.minLevel = 2;
}
const eventType = await monitorCallbackAsync(getRegularOrDynamicEventType, input, orgDetails);
const eventType = await getRegularOrDynamicEventType(input, orgDetails);
if (!eventType) {
throw new TRPCError({ code: "NOT_FOUND" });
@@ -380,16 +385,14 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
});
}
const { qualifiedRRHosts, allFallbackRRHosts, fixedHosts } = await monitorCallbackAsync(
findQualifiedHostsWithDelegationCredentials<GetAvailabilityUserWithoutDelegationCredentials>,
{
const { qualifiedRRHosts, allFallbackRRHosts, fixedHosts } =
await findQualifiedHostsWithDelegationCredentials({
eventType,
rescheduleUid: input.rescheduleUid ?? null,
routedTeamMemberIds,
contactOwnerEmail,
routingFormResponse,
}
);
});
const allHosts = [...qualifiedRRHosts, ...fixedHosts];
@@ -481,7 +484,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
eventType.schedulingType === SchedulingType.ROUND_ROBIN ||
allUsersAvailability.length > 1;
const timeSlots = monitorCallbackSync(getSlots, {
const timeSlots = getSlots({
inviteeDate: startTime,
eventLength: input.duration || eventType.length,
offsetStart: eventType.offsetStart,
@@ -584,7 +587,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
timeZone: input.timeZone,
});
const slotsMappedToDate = monitorCallbackSync(function mapSlotsToDate() {
function _mapSlotsToDate() {
return availableTimeSlots.reduce(
(
r: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>,
@@ -597,7 +600,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
const dateString = formatter.format(time.toDate());
r[dateString] = r[dateString] || [];
if (eventType.onlyShowFirstAvailableSlot && r[dateString].length > 0) {
if (eventType?.onlyShowFirstAvailableSlot && r[dateString].length > 0) {
return r;
}
r[dateString].push({
@@ -619,12 +622,14 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
},
Object.create(null)
);
});
}
const mapSlotsToDate = withReporting(_mapSlotsToDate, "mapSlotsToDate");
const slotsMappedToDate = mapSlotsToDate();
loggerWithEventDetails.debug({ slotsMappedToDate });
const availableDates = Object.keys(slotsMappedToDate);
const allDatesWithBookabilityStatus = monitorCallbackSync(getAllDatesWithBookabilityStatus, availableDates);
const allDatesWithBookabilityStatus = getAllDatesWithBookabilityStatus(availableDates);
loggerWithEventDetails.debug({ availableDates });
// timeZone isn't directly set on eventType now(So, it is legacy)
@@ -646,7 +651,9 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
bookerUtcOffset,
});
let foundAFutureLimitViolation = false;
const withinBoundsSlotsMappedToDate = monitorCallbackSync(function mapWithinBoundsSlotsToDate() {
function _mapWithinBoundsSlotsToDate() {
// This should never happen. Just for type safety, we already check in the upper scope
if (!eventType) throw new TRPCError({ code: "NOT_FOUND" });
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.
@@ -676,7 +683,9 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
withinBoundsSlotsMappedToDate[date] = filteredSlots;
return withinBoundsSlotsMappedToDate;
}, {} as typeof slotsMappedToDate);
});
}
const mapWithinBoundsSlotsToDate = withReporting(_mapWithinBoundsSlotsToDate, "mapWithinBoundsSlotsToDate");
const withinBoundsSlotsMappedToDate = mapWithinBoundsSlotsToDate();
// We only want to run this on single targeted events and not dynamic
if (!Object.keys(withinBoundsSlotsMappedToDate).length && input.usernameList?.length === 1) {
@@ -724,7 +733,9 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise<I
slots: withinBoundsSlotsMappedToDate,
...troubleshooterData,
};
}
};
export const getAvailableSlots = withReporting(_getAvailableSlots, "getAvailableSlots");
function doesRangeStartFromToday(periodType: PeriodType) {
return periodType === PeriodType.ROLLING_WINDOW || periodType === PeriodType.ROLLING;
@@ -760,7 +771,7 @@ async function getTeamIdFromSlug(
return team?.id;
}
async function getOOODates(startTimeDate: Date, endTimeDate: Date, allUserIds: number[]) {
const _getOOODates = async (startTimeDate: Date, endTimeDate: Date, allUserIds: number[]) => {
return await prisma.outOfOfficeEntry.findMany({
where: {
userId: {
@@ -827,9 +838,11 @@ async function getOOODates(startTimeDate: Date, endTimeDate: Date, allUserIds: n
},
},
});
}
};
export function getAllDatesWithBookabilityStatus(availableDates: string[]) {
export const getOOODates = withReporting(_getOOODates, "getOOODates");
const _getAllDatesWithBookabilityStatus = (availableDates: string[]) => {
const availableDatesSet = new Set(availableDates);
const firstDate = dayjs(availableDates[0]);
const lastDate = dayjs(availableDates[availableDates.length - 1]);
@@ -844,12 +857,14 @@ export function getAllDatesWithBookabilityStatus(availableDates: string[]) {
currentDate = currentDate.add(1, "day");
}
return allDates;
}
};
/**
* Gets busy times from limits for multiple users
*/
const getBusyTimesFromLimitsForUsers = async (
export const getAllDatesWithBookabilityStatus = withReporting(
_getAllDatesWithBookabilityStatus,
"getAllDatesWithBookabilityStatus"
);
const _getBusyTimesFromLimitsForUsers = async (
users: { id: number; email: string }[],
bookingLimits: IntervalLimit | null,
durationLimits: IntervalLimit | null,
@@ -925,112 +940,109 @@ const getBusyTimesFromLimitsForUsers = async (
limitManager.addBusyTime(start, unit, timeZone);
}
await monitorCallbackAsync(async () => {
if (bookingLimits) {
for (const key of descendingLimitKeys) {
const limit = bookingLimits?.[key];
if (!limit) continue;
if (bookingLimits) {
for (const key of descendingLimitKeys) {
const limit = bookingLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
if (unit === "year") {
try {
await checkBookingLimit({
eventStartDate: periodStart.toDate(),
limitingNumber: limit,
eventId: eventType.id,
key,
user,
rescheduleUid,
timeZone,
});
} catch (_) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (
periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))
) {
break;
}
}
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalBookings = 0;
for (const booking of userBookings) {
if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) {
continue;
}
totalBookings++;
if (totalBookings >= limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
}
}
if (durationLimits) {
for (const key of descendingLimitKeys) {
const limit = durationLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
const selectedDuration = (duration || eventType.length) ?? 0;
if (selectedDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
continue;
}
if (unit === "year") {
const totalYearlyDuration = await getTotalBookingDuration({
if (unit === "year") {
try {
await checkBookingLimit({
eventStartDate: periodStart.toDate(),
limitingNumber: limit,
eventId: eventType.id,
startDate: periodStart.toDate(),
endDate: periodStart.endOf(unit).toDate(),
key,
user,
rescheduleUid,
timeZone,
});
if (totalYearlyDuration + selectedDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (
periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))
) {
break;
}
} catch (_) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (
periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))
) {
break;
}
}
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalBookings = 0;
for (const booking of userBookings) {
if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) {
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalDuration = selectedDuration;
for (const booking of userBookings) {
if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) {
continue;
}
totalDuration += dayjs(booking.end).diff(dayjs(booking.start), "minute");
if (totalDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
totalBookings++;
if (totalBookings >= limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
}
});
}
if (durationLimits) {
for (const key of descendingLimitKeys) {
const limit = durationLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
const selectedDuration = (duration || eventType.length) ?? 0;
if (selectedDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
continue;
}
if (unit === "year") {
const totalYearlyDuration = await getTotalBookingDuration({
eventId: eventType.id,
startDate: periodStart.toDate(),
endDate: periodStart.endOf(unit).toDate(),
rescheduleUid,
});
if (totalYearlyDuration + selectedDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (
periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))
) {
break;
}
}
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalDuration = selectedDuration;
for (const booking of userBookings) {
if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) {
continue;
}
totalDuration += dayjs(booking.end).diff(dayjs(booking.start), "minute");
if (totalDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
}
}
userBusyTimesMap.set(user.id, limitManager.getBusyTimes());
}
@@ -1038,10 +1050,12 @@ const getBusyTimesFromLimitsForUsers = async (
return userBusyTimesMap;
};
/**
* Gets busy times from team booking limits for multiple users
*/
const getBusyTimesFromTeamLimitsForUsers = async (
export const getBusyTimesFromLimitsForUsers = withReporting(
_getBusyTimesFromLimitsForUsers,
"getBusyTimesFromLimitsForUsers"
);
const _getBusyTimesFromTeamLimitsForUsers = async (
users: { id: number; email: string }[],
bookingLimits: IntervalLimit,
dateFrom: Dayjs,
@@ -1116,71 +1130,66 @@ const getBusyTimesFromTeamLimitsForUsers = async (
limitManager.addBusyTime(start, unit, timeZone);
}
const bookingLimitsParams = {
bookings: userBusyTimes,
bookingLimits,
dateFrom,
dateTo,
limitManager,
rescheduleUid,
teamId,
user,
includeManagedEvents,
timeZone,
};
await monitorCallbackAsync(async () => {
const bookingLimitsParams = {
bookings: userBusyTimes,
bookingLimits,
dateFrom,
dateTo,
limitManager,
rescheduleUid,
teamId,
user,
includeManagedEvents,
timeZone,
};
for (const key of descendingLimitKeys) {
const limit = bookingLimits?.[key];
if (!limit) continue;
for (const key of descendingLimitKeys) {
const limit = bookingLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
if (unit === "year") {
try {
await checkBookingLimit({
eventStartDate: periodStart.toDate(),
limitingNumber: limit,
key,
teamId,
user,
rescheduleUid,
includeManagedEvents,
timeZone,
});
} catch (_) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (
periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))
) {
return;
}
if (unit === "year") {
try {
await checkBookingLimit({
eventStartDate: periodStart.toDate(),
limitingNumber: limit,
key,
teamId,
user,
rescheduleUid,
includeManagedEvents,
timeZone,
});
} catch (_) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))) {
return;
}
}
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalBookings = 0;
for (const booking of userBusyTimes) {
if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) {
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalBookings = 0;
for (const booking of userBusyTimes) {
if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) {
continue;
}
totalBookings++;
if (totalBookings >= limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
totalBookings++;
if (totalBookings >= limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
});
}
userBusyTimesMap.set(user.id, limitManager.getBusyTimes());
}
@@ -1188,6 +1197,11 @@ const getBusyTimesFromTeamLimitsForUsers = async (
return userBusyTimesMap;
};
export const getBusyTimesFromTeamLimitsForUsers = withReporting(
_getBusyTimesFromTeamLimitsForUsers,
"getBusyTimesFromTeamLimitsForUsers"
);
const calculateHostsAndAvailabilities = async ({
input,
eventType,
@@ -1210,7 +1224,7 @@ const calculateHostsAndAvailabilities = async ({
bypassBusyCalendarTimes: boolean;
shouldServeCache?: boolean;
}) => {
const usersWithCredentials = monitorCallbackSync(getUsersWithCredentials, {
const usersWithCredentials = getUsersWithCredentials({
hosts,
});
@@ -1233,14 +1247,14 @@ const calculateHostsAndAvailabilities = async ({
const allUserIds = Array.from(userIdAndEmailMap.keys());
const [currentBookingsAllUsers, outOfOfficeDaysAllUsers] = await Promise.all([
monitorCallbackAsync(BookingRepo.findAllExistingBookingsForEventTypeBetween, {
BookingRepo.findAllExistingBookingsForEventTypeBetween({
startDate: startTimeDate,
endDate: endTimeDate,
eventTypeId: eventType.id,
seatedEvent: Boolean(eventType.seatsPerTimeSlot),
userIdAndEmailMap,
}),
monitorCallbackAsync(getOOODates, startTimeDate, endTimeDate, allUserIds),
getOOODates(startTimeDate, endTimeDate, allUserIds),
]);
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
@@ -1248,7 +1262,7 @@ const calculateHostsAndAvailabilities = async ({
let busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> = [];
if (eventType && (bookingLimits || durationLimits)) {
busyTimesFromLimitsBookingsAllUsers = await monitorCallbackAsync(getBusyTimesForLimitChecks, {
busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({
userIds: allUserIds,
eventTypeId: eventType.id,
startDate: startTime.format(),
@@ -1296,7 +1310,7 @@ const calculateHostsAndAvailabilities = async ({
);
}
const users = monitorCallbackSync(function enrichUsersWithData() {
function _enrichUsersWithData() {
return usersWithCredentials.map((currentUser) => {
return {
...currentUser,
@@ -1311,7 +1325,9 @@ const calculateHostsAndAvailabilities = async ({
outOfOfficeDays: outOfOfficeDaysAllUsers.filter((o) => o.user.id === currentUser.id),
};
});
});
}
const enrichUsersWithData = withReporting(_enrichUsersWithData, "enrichUsersWithData");
const users = enrichUsersWithData();
const premappedUsersAvailability = await getUsersAvailability({
users,