chore: Sentry Wrapper with Performance and Error Tracing (#12642)

* add wrapper for sentry and update functions in 'getUserAvailability'. Update tracesSampleRate to 1.0

* Make Sentry Wrapper utilize parent transaction, if it exists.

* Update wrapper for functions to inherit parameters from the child function

* add comment of when to use the wrapper

* check for sentry before wrapping, if not call unwrapped function

* refactored wrapper to have async and sync separate functions that utilize helpers for common behaviour

* update type of args to unknown

* fixed types of returns from wrapped functions

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Brendan Woodward 2023-12-03 21:06:01 -05:00 committed by GitHub
parent 4062ae8486
commit 9a6d4e63e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 139 additions and 5 deletions

View File

@ -2,4 +2,5 @@ import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
});

View File

@ -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<typeof _getEventType>
): Promise<ReturnType<typeof _getEventType>> => {
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<ReturnType<typeof getEventType>>;
const getUser = (where: Prisma.UserWhereInput) =>
const getUser = (...args: Parameters<typeof _getUser>): ReturnType<typeof _getUser> => {
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<ReturnType<typeof getUser>>;
export const getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) =>
export const getCurrentSeats = (
...args: Parameters<typeof _getCurrentSeats>
): ReturnType<typeof _getCurrentSeats> => {
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<ReturnType<typeof getCurrentSeats>>;
export const getUserAvailability = async (
...args: Parameters<typeof _getUserAvailability>
): Promise<ReturnType<typeof _getUserAvailability>> => {
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<typeof _getPeriodStartDatesBetween>
): ReturnType<typeof _getPeriodStartDatesBetween> => {
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<typeof _getBusyTimesFromLimits>
): Promise<ReturnType<typeof _getBusyTimesFromLimits>> => {
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<typeof _getBusyTimesFromBookingLimits>
): Promise<ReturnType<typeof _getBusyTimesFromBookingLimits>> => {
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<typeof _getBusyTimesFromDurationLimits>
): Promise<ReturnType<typeof _getBusyTimesFromDurationLimits>> => {
return monitorCallbackAsync(_getBusyTimesFromDurationLimits, ...args);
};
const _getBusyTimesFromDurationLimits = async (
bookings: EventBusyDetails[],
durationLimits: IntervalLimit,
dateFrom: Dayjs,

View File

@ -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 <T extends (...args: any[]) => any>(
cb: T,
...args: Parameters<T>
): Promise<ReturnType<T>> => {
// Check if Sentry set
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return (await cb(...args)) as ReturnType<T>;
const [transaction, span] = setUpMonitoring(cb.name);
try {
const result = await cb(...args);
return result as ReturnType<T>;
} catch (error) {
Sentry.captureException(error);
throw error;
} finally {
finishMonitoring(transaction, span);
}
};
const monitorCallbackSync = <T extends (...args: any[]) => any>(
cb: T,
...args: Parameters<T>
): ReturnType<T> => {
// Check if Sentry set
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return cb(...args) as ReturnType<T>;
const [transaction, span] = setUpMonitoring(cb.name);
try {
const result = cb(...args);
return result as ReturnType<T>;
} catch (error) {
Sentry.captureException(error);
throw error;
} finally {
finishMonitoring(transaction, span);
}
};
export default monitorCallbackAsync;
export { monitorCallbackSync };