From b3d3cb1deeb3b75978263e6540ff323a0f0148c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Mon, 12 May 2025 10:22:52 -0700 Subject: [PATCH] refactor: Uses withReporting to force named functions for tracing (#21219) --- .../routing-forms/lib/handleResponse.ts | 13 +- .../app-store/routing-forms/trpc/utils.ts | 4 +- packages/emails/email-manager.ts | 31 +- .../refreshCredentials.ts | 10 +- .../features/bookings/lib/handleNewBooking.ts | 93 ++-- .../checkBookingAndDurationLimits.ts | 8 +- .../lib/handleNewBooking/createBooking.ts | 10 +- .../handleNewBooking/ensureAvailableUsers.ts | 9 +- .../lib/handleNewBooking/findBookingQuery.ts | 6 +- .../lib/handleNewBooking/getBookingData.ts | 12 +- .../lib/handleNewBooking/getEventType.ts | 26 ++ .../getLocationValuesForDb.ts | 5 +- .../getRequiresConfirmationFlags.ts | 13 +- .../handleNewBooking/loadAndValidateUsers.ts | 9 +- .../scheduleNoShowTriggers.ts | 5 +- .../validateBookingTimeIsNotOutOfBounds.ts | 9 +- .../handleNewBooking/validateEventLength.ts | 6 +- .../bookings/lib/handleWebhookTrigger.ts | 5 +- .../AssignmentReasonRecorder.ts | 26 +- .../lib/reminders/reminderScheduler.ts | 14 +- .../reminders/scheduleMandatoryReminder.ts | 8 +- packages/features/webhooks/lib/getWebhooks.ts | 3 +- .../features/webhooks/lib/scheduleTrigger.ts | 8 +- ...QualifiedHostsWithDelegationCredentials.ts | 8 +- packages/lib/getBusyTimes.ts | 11 +- packages/lib/getUserAvailability.ts | 53 +-- .../server/checkBookingLimits.ts | 9 +- .../server/getBusyTimesFromLimits.ts | 42 +- packages/lib/redactSensitiveData.test.ts | 178 ++++++++ packages/lib/redactSensitiveData.ts | 84 ++++ packages/lib/sentryWrapper.ts | 89 ++++ packages/lib/server/getRoutedUrl.ts | 8 +- packages/lib/server/repository/booking.ts | 8 +- packages/lib/slots.ts | 3 +- .../trpc/server/routers/viewer/slots/util.ts | 420 +++++++++--------- 35 files changed, 853 insertions(+), 393 deletions(-) create mode 100644 packages/features/bookings/lib/handleNewBooking/getEventType.ts create mode 100644 packages/lib/redactSensitiveData.test.ts create mode 100644 packages/lib/redactSensitiveData.ts diff --git a/packages/app-store/routing-forms/lib/handleResponse.ts b/packages/app-store/routing-forms/lib/handleResponse.ts index 8f0ab7784f..a3845432c1 100644 --- a/packages/app-store/routing-forms/lib/handleResponse.ts +++ b/packages/app-store/routing-forms/lib/handleResponse.ts @@ -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) => { - 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"); diff --git a/packages/app-store/routing-forms/trpc/utils.ts b/packages/app-store/routing-forms/trpc/utils.ts index e42bce2c6d..f46922159b 100644 --- a/packages/app-store/routing-forms/trpc/utils.ts +++ b/packages/app-store/routing-forms/trpc/utils.ts @@ -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 & { user: Pick; userWithEmails?: string[] }, "fields" @@ -206,6 +207,7 @@ export async function onFormSubmission( } } } +export const onFormSubmission = withReporting(_onFormSubmission, "onFormSubmission"); export const sendResponseEmail = async ( form: Pick, diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index 4a27d91417..8e13af4506 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -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 diff --git a/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials.ts b/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials.ts index bed3712704..352e784fd8 100644 --- a/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials.ts +++ b/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials.ts @@ -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 -): Promise> { +): Promise> => { 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"); diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index feadb27ae7..20efcb7512 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -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 = 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 }, diff --git a/packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts b/packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts index c8b7d83859..7963c3b5f6 100644 --- a/packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts +++ b/packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts @@ -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" +); diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index a1f2897b47..ebacb09af0 100644 --- a/packages/features/bookings/lib/handleNewBooking/createBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/createBooking.ts @@ -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, diff --git a/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts b/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts index 9a613ca0b2..432b14da7f 100644 --- a/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts +++ b/packages/features/bookings/lib/handleNewBooking/ensureAvailableUsers.ts @@ -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 & { users: IsFixedAwareUser[]; }, @@ -58,7 +59,7 @@ export async function ensureAvailableUsers( loggerWithEventDetails: Logger, 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"); diff --git a/packages/features/bookings/lib/handleNewBooking/findBookingQuery.ts b/packages/features/bookings/lib/handleNewBooking/findBookingQuery.ts index ec339e585f..0cd3312700 100644 --- a/packages/features/bookings/lib/handleNewBooking/findBookingQuery.ts +++ b/packages/features/bookings/lib/handleNewBooking/findBookingQuery.ts @@ -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"); diff --git a/packages/features/bookings/lib/handleNewBooking/getBookingData.ts b/packages/features/bookings/lib/handleNewBooking/getBookingData.ts index 6c5a77a899..553e356e55 100644 --- a/packages/features/bookings/lib/handleNewBooking/getBookingData.ts +++ b/packages/features/bookings/lib/handleNewBooking/getBookingData.ts @@ -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({ +// Define the function with underscore prefix +const _getBookingData = async ({ reqBody, eventType, schema, @@ -20,7 +22,7 @@ export async function getBookingData({ reqBody: Record; 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({ // So TS doesn't complain about unknown properties customInputs: undefined, }; -} +}; -export type AwaitedBookingData = Awaited>; +export const getBookingData = withReporting(_getBookingData, "getBookingData"); + +export type AwaitedBookingData = Awaited>; export type RescheduleReason = AwaitedBookingData["rescheduleReason"]; export type NoEmail = AwaitedBookingData["noEmail"]; export type AdditionalNotes = AwaitedBookingData["notes"]; diff --git a/packages/features/bookings/lib/handleNewBooking/getEventType.ts b/packages/features/bookings/lib/handleNewBooking/getEventType.ts new file mode 100644 index 0000000000..27d83294ba --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/getEventType.ts @@ -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"); diff --git a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts index 5acd7f9cfb..966b48d1e4 100644 --- a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts +++ b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts @@ -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 = ( }); }; -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"); diff --git a/packages/features/bookings/lib/handleNewBooking/getRequiresConfirmationFlags.ts b/packages/features/bookings/lib/handleNewBooking/getRequiresConfirmationFlags.ts index 4ee9edd3bc..98421db271 100644 --- a/packages/features/bookings/lib/handleNewBooking/getRequiresConfirmationFlags.ts +++ b/packages/features/bookings/lib/handleNewBooking/getRequiresConfirmationFlags.ts @@ -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 { +): Promise => { 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, diff --git a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts index 7881746fcb..7bc5fe9c87 100644 --- a/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts +++ b/packages/features/bookings/lib/handleNewBooking/loadAndValidateUsers.ts @@ -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"); diff --git a/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts b/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts index 3b91dc473c..11dc5d3a3b 100644 --- a/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts +++ b/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts @@ -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"); diff --git a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts index dd78bbd270..df638c2960 100644 --- a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts +++ b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts @@ -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 ( +// Define the function with underscore prefix +const _validateBookingTimeIsNotOutOfBounds = async ( reqBodyStartTime: string, reqBodyTimeZone: string, eventType: T, @@ -54,3 +56,8 @@ export const validateBookingTimeIsNotOutOfBounds = async ; }; -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"); diff --git a/packages/features/bookings/lib/handleWebhookTrigger.ts b/packages/features/bookings/lib/handleWebhookTrigger.ts index 53758177af..57cd39b509 100644 --- a/packages/features/bookings/lib/handleWebhookTrigger.ts +++ b/packages/features/bookings/lib/handleWebhookTrigger.ts @@ -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"); diff --git a/packages/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder.ts b/packages/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder.ts index da6f918093..ba1da99b81 100644 --- a/packages/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder.ts +++ b/packages/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder.ts @@ -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, diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index aa48c09a52..793a6412b2 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -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"); diff --git a/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts b/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts index 15f6fa1b2f..76c80884e3 100644 --- a/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts +++ b/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts @@ -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> | 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" +); diff --git a/packages/features/webhooks/lib/getWebhooks.ts b/packages/features/webhooks/lib/getWebhooks.ts index ca5dc825aa..51fb0ea6ac 100644 --- a/packages/features/webhooks/lib/getWebhooks.ts +++ b/packages/features/webhooks/lib/getWebhooks.ts @@ -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"); diff --git a/packages/features/webhooks/lib/scheduleTrigger.ts b/packages/features/webhooks/lib/scheduleTrigger.ts index 262bfa6153..c1b42282ca 100644 --- a/packages/features/webhooks/lib/scheduleTrigger.ts +++ b/packages/features/webhooks/lib/scheduleTrigger.ts @@ -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[], diff --git a/packages/lib/bookings/findQualifiedHostsWithDelegationCredentials.ts b/packages/lib/bookings/findQualifiedHostsWithDelegationCredentials.ts index 70972979b4..3061292c18 100644 --- a/packages/lib/bookings/findQualifiedHostsWithDelegationCredentials.ts +++ b/packages/lib/bookings/findQualifiedHostsWithDelegationCredentials.ts @@ -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 = (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" +); diff --git a/packages/lib/getBusyTimes.ts b/packages/lib/getBusyTimes.ts index 1b53abc395..62107ab264 100644 --- a/packages/lib/getBusyTimes.ts +++ b/packages/lib/getBusyTimes.ts @@ -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"); diff --git a/packages/lib/getUserAvailability.ts b/packages/lib/getUserAvailability.ts index c50ed5feeb..2e5076ed45 100644 --- a/packages/lib/getUserAvailability.ts +++ b/packages/lib/getUserAvailability.ts @@ -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 -): Promise> => { - 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>; +export type EventType = Awaited>; -const getUser = async (...args: Parameters): Promise> => { - return monitorCallbackAsync(_getUser, ...args); -}; +export const getEventType = withReporting(_getEventType, "getEventType"); const _getUser = async (where: Prisma.UserWhereInput) => { return findUsersForAvailabilityCheck({ where }); }; -type GetUser = Awaited>; +type GetUser = Awaited>; + +const getUser = withReporting(_getUser, "getUser"); export type GetUserAvailabilityInitialData = { user?: GetUser; @@ -213,12 +207,6 @@ type GetUserAvailabilityQuery = { shouldServeCache?: boolean; }; -export const getCurrentSeats = async ( - ...args: Parameters -): Promise> => { - return monitorCallbackAsync(_getCurrentSeats, ...args); -}; - const _getCurrentSeats = async ( eventType: { id?: number; @@ -274,13 +262,9 @@ const _getCurrentSeats = async ( }); }; -export type CurrentSeats = Awaited>; +export type CurrentSeats = Awaited>; -export const getUserAvailability = async ( - ...args: Parameters -): Promise => { - return monitorCallbackAsync(_getUserAvailability, ...args); -}; +export const getCurrentSeats = withReporting(_getCurrentSeats, "getCurrentSeats"); type GetUserAvailabilityResult = ReturnType; @@ -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 -): ReturnType => { - 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 -): Promise[]> => { - return monitorCallbackAsync(_getUsersAvailability, ...args); -}; +export const getUsersAvailability = withReporting(_getUsersAvailability, "getUsersAvailability"); diff --git a/packages/lib/intervalLimits/server/checkBookingLimits.ts b/packages/lib/intervalLimits/server/checkBookingLimits.ts index c6c18011b8..e69f60ce23 100644 --- a/packages/lib/intervalLimits/server/checkBookingLimits.ts +++ b/packages/lib/intervalLimits/server/checkBookingLimits.ts @@ -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"); diff --git a/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts b/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts index 8757041c83..6599a48ed0 100644 --- a/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts +++ b/packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts @@ -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 -): Promise> => { - 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 -): Promise> => { - 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 -): Promise> => { - return monitorCallbackAsync(_getBusyTimesFromDurationLimits, ...args); -}; - const _getBusyTimesFromDurationLimits = async ( bookings: EventBusyDetails[], durationLimits: IntervalLimit, @@ -231,11 +212,10 @@ const _getBusyTimesFromDurationLimits = async ( } }; -export const getBusyTimesFromTeamLimits = async ( - ...args: Parameters -): Promise> => { - 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" +); diff --git a/packages/lib/redactSensitiveData.test.ts b/packages/lib/redactSensitiveData.test.ts new file mode 100644 index 0000000000..835907fda0 --- /dev/null +++ b/packages/lib/redactSensitiveData.test.ts @@ -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", + } + `); + }); +}); diff --git a/packages/lib/redactSensitiveData.ts b/packages/lib/redactSensitiveData.ts new file mode 100644 index 0000000000..683bc49a57 --- /dev/null +++ b/packages/lib/redactSensitiveData.ts @@ -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(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 = { + 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" }; + } +} diff --git a/packages/lib/sentryWrapper.ts b/packages/lib/sentryWrapper.ts index e19e5bcae8..1c2b0eef37 100644 --- a/packages/lib/sentryWrapper.ts +++ b/packages/lib/sentryWrapper.ts @@ -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 any>( cb: T, ...args: Parameters @@ -30,6 +36,10 @@ const monitorCallbackAsync = async any>( }); }; +/** + * @deprecated Use withReporting instead. This function will be removed in a future version. + * Example: withReporting(myFunction, "myFunction")(...args) + */ const monitorCallbackSync = any>( cb: T, ...args: Parameters @@ -49,5 +59,84 @@ const monitorCallbackSync = any>( }); }; +/** + * Type guard to check if a value is a Promise + */ +function isPromise(value: unknown): value is Promise { + 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(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 }; diff --git a/packages/lib/server/getRoutedUrl.ts b/packages/lib/server/getRoutedUrl.ts index b7308e0958..21ab6d92ba 100644 --- a/packages/lib/server/getRoutedUrl.ts +++ b/packages/lib/server/getRoutedUrl.ts @@ -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) => { - return monitorCallbackAsync(_getRoutedUrl, context); -}; - const _getRoutedUrl = async (context: Pick) => { const queryParsed = querySchema.safeParse(context.query); const isEmbed = hasEmbedPath(context.req.url || ""); @@ -221,3 +217,5 @@ const _getRoutedUrl = async (context: Pick { 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 -): Promise> => { - return monitorCallbackAsync(_getAvailableSlots, ...args); -}; - -async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise { +const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise => { const { _enableTroubleshooter: enableTroubleshooter = false, _bypassCalendarBusyTimes: bypassBusyCalendarTimes = false, @@ -309,7 +314,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise, - { + 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 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, @@ -597,7 +600,7 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise 0) { + if (eventType?.onlyShowFirstAvailableSlot && r[dateString].length > 0) { return r; } r[dateString].push({ @@ -619,12 +622,14 @@ async function _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise { // 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 { 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> = []; 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,