diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts index 842f09fccc..a429088bd9 100644 --- a/apps/web/sentry.server.config.ts +++ b/apps/web/sentry.server.config.ts @@ -2,4 +2,5 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, }); diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 3c4f77497b..cfb0b4b727 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -25,6 +25,7 @@ import type { } from "@calcom/types/Calendar"; import { getBusyTimes, getBusyTimesForLimitChecks } from "./getBusyTimes"; +import monitorCallbackAsync, { monitorCallbackSync } from "./sentryWrapper"; const log = logger.getSubLogger({ prefix: ["getUserAvailability"] }); const availabilitySchema = z @@ -41,7 +42,13 @@ const availabilitySchema = z }) .refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in."); -const getEventType = async (id: number) => { +const getEventType = async ( + ...args: Parameters +): Promise> => { + return monitorCallbackAsync(_getEventType, ...args); +}; + +const _getEventType = async (id: number) => { const eventType = await prisma.eventType.findUnique({ where: { id }, select: { @@ -86,7 +93,11 @@ const getEventType = async (id: number) => { type EventType = Awaited>; -const getUser = (where: Prisma.UserWhereInput) => +const getUser = (...args: Parameters): ReturnType => { + return monitorCallbackSync(_getUser, ...args); +}; + +const _getUser = (where: Prisma.UserWhereInput) => prisma.user.findFirst({ where, select: { @@ -99,7 +110,13 @@ const getUser = (where: Prisma.UserWhereInput) => type User = Awaited>; -export const getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) => +export const getCurrentSeats = ( + ...args: Parameters +): ReturnType => { + return monitorCallbackSync(_getCurrentSeats, ...args); +}; + +const _getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) => prisma.booking.findMany({ where: { eventTypeId, @@ -122,8 +139,14 @@ export const getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Da export type CurrentSeats = Awaited>; +export const getUserAvailability = async ( + ...args: Parameters +): Promise> => { + return monitorCallbackAsync(_getUserAvailability, ...args); +}; + /** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */ -export const getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseAndEverythingElse( +const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseAndEverythingElse( query: { withSource?: boolean; username?: string; @@ -305,7 +328,13 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni }; }; -const getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => { +const getPeriodStartDatesBetween = ( + ...args: Parameters +): ReturnType => { + return monitorCallbackSync(_getPeriodStartDatesBetween, ...args); +}; + +const _getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => { const dates = []; let startDate = dayjs(dateFrom).startOf(period); const endDate = dayjs(dateTo).endOf(period); @@ -378,6 +407,12 @@ class LimitManager { } const getBusyTimesFromLimits = async ( + ...args: Parameters +): Promise> => { + return monitorCallbackAsync(_getBusyTimesFromLimits, ...args); +}; + +const _getBusyTimesFromLimits = async ( bookingLimits: IntervalLimit | null, durationLimits: IntervalLimit | null, dateFrom: Dayjs, @@ -450,6 +485,12 @@ const getBusyTimesFromLimits = async ( }; const getBusyTimesFromBookingLimits = async ( + ...args: Parameters +): Promise> => { + return monitorCallbackAsync(_getBusyTimesFromBookingLimits, ...args); +}; + +const _getBusyTimesFromBookingLimits = async ( bookings: EventBusyDetails[], bookingLimits: IntervalLimit, dateFrom: Dayjs, @@ -504,6 +545,12 @@ const getBusyTimesFromBookingLimits = async ( }; const getBusyTimesFromDurationLimits = async ( + ...args: Parameters +): Promise> => { + return monitorCallbackAsync(_getBusyTimesFromDurationLimits, ...args); +}; + +const _getBusyTimesFromDurationLimits = async ( bookings: EventBusyDetails[], durationLimits: IntervalLimit, dateFrom: Dayjs, diff --git a/packages/core/sentryWrapper.ts b/packages/core/sentryWrapper.ts new file mode 100644 index 0000000000..5d2ae4b384 --- /dev/null +++ b/packages/core/sentryWrapper.ts @@ -0,0 +1,86 @@ +import * as Sentry from "@sentry/nextjs"; +import type { Span, Transaction } from "@sentry/types"; + +/* +WHEN TO USE +We ran a script that performs a simple mathematical calculation within a loop of 1000000 iterations. +Our results were: Plain execution time: 441, Monitored execution time: 8094. +This suggests that using these wrappers within large loops can incur significant overhead and is thus not recommended. + +For smaller loops, the cost incurred may not be very significant on an absolute scale +considering that a million monitored iterations only took roughly 8 seconds when monitored. +*/ + +const setUpMonitoring = (name: string) => { + // Attempt to retrieve the current transaction from Sentry's scope + let transaction = Sentry.getCurrentHub().getScope()?.getTransaction(); + + // Check if there's an existing transaction, if not, start a new one + if (!transaction) { + transaction = Sentry.startTransaction({ + op: name, + name: name, + }); + } + + // Start a new span in the current transaction + const span = transaction.startChild({ + op: name, + description: `Executing ${name}`, + }); + return [transaction, span]; +}; + +// transaction will always be Transaction, since returned in a list with Span type must be listed as either or here +const finishMonitoring = (transaction: Transaction | Span, span: Span) => { + // Attempt to retrieve the current transaction from Sentry's scope + span.finish(); + + // If this was a new transaction, finish it + if (!Sentry.getCurrentHub().getScope()?.getTransaction()) { + transaction.finish(); + } +}; + +const monitorCallbackAsync = async any>( + cb: T, + ...args: Parameters +): Promise> => { + // Check if Sentry set + if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return (await cb(...args)) as ReturnType; + + const [transaction, span] = setUpMonitoring(cb.name); + + try { + const result = await cb(...args); + return result as ReturnType; + } catch (error) { + Sentry.captureException(error); + throw error; + } finally { + finishMonitoring(transaction, span); + } +}; + +const monitorCallbackSync = any>( + cb: T, + ...args: Parameters +): ReturnType => { + // Check if Sentry set + if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return cb(...args) as ReturnType; + + const [transaction, span] = setUpMonitoring(cb.name); + + try { + const result = cb(...args); + return result as ReturnType; + } catch (error) { + Sentry.captureException(error); + throw error; + } finally { + finishMonitoring(transaction, span); + } +}; + +export default monitorCallbackAsync; +export { monitorCallbackSync };