* feat(calendar-subscription): add telemetry metrics for cache performance and webhook calls - Add Sentry span telemetry to CalendarCacheWrapper.getAvailability() and getAvailabilityWithTimeZones() to measure: - Cache fetch duration (cacheFetchDurationMs) - Original calendar fetch duration (originalFetchDurationMs) - Number of calendars from cache vs original source - Number of events from each source - Whether cache was used (cacheUsed) - Add Sentry span telemetry to CalendarSubscriptionService.processWebhook() to track: - Provider (google_calendar, office365_calendar) - Success/failure status - Processing duration (durationMs) - Error messages on failure - Channel ID and selected calendar ID - Create telemetry module with types, sentry-span, and no-op-span implementations Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor(calendar-subscription): move telemetry to @calcom/lib for reusability - Add withSpan helper to @calcom/lib/sentryWrapper for reusable telemetry - Update CalendarCacheWrapper to use withSpan from @calcom/lib - Update CalendarTelemetryWrapper to use withSpan from @calcom/lib - Update CalendarSubscriptionService to use withSpan from @calcom/lib - Remove old telemetry module from calendar-subscription feature - Add telemetry tracking when cache is disabled for performance comparison Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix(calendar-subscription): only apply telemetry wrapper when Sentry is configured - Fix unit test failures by checking if Sentry is configured before wrapping - Remove ESLint inline rules from getCalendar.ts Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor(calendar-subscription): move CalendarTelemetryWrapper to telemetry directory - Move CalendarTelemetryWrapper from cache/ to telemetry/ directory - Update import path in getCalendar.ts Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * feat(telemetry): add console fallback when Sentry is not configured - Add console logging fallback in development mode when Sentry is not configured - Create isTelemetryEnabled() helper to check if telemetry should be enabled - Update getCalendar.ts to use isTelemetryEnabled() helper - Logs span name, operation, duration, and attributes to console via @calcom/lib/logger Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor(calendar-subscription): wrap all calendars with CalendarTelemetryWrapper - CalendarTelemetryWrapper now wraps ALL cache-supported calendars with cacheEnabled parameter - CalendarCacheWrapper keeps cache-specific metrics with distinct op (calendar.cache.internal.*) - CalendarTelemetryWrapper uses canonical op (calendar.getAvailability) for consistent querying - Nested spans: outer telemetry wrapper measures end-to-end, inner cache wrapper measures cache internals Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
106 lines
4.3 KiB
TypeScript
106 lines
4.3 KiB
TypeScript
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
|
|
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
|
|
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
|
|
import { CalendarCacheWrapper } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheWrapper";
|
|
import { CalendarTelemetryWrapper } from "@calcom/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper";
|
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
|
import logger from "@calcom/lib/logger";
|
|
import { isTelemetryEnabled } from "@calcom/lib/sentryWrapper";
|
|
import { prisma } from "@calcom/prisma";
|
|
import type { Calendar } from "@calcom/types/Calendar";
|
|
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
|
|
|
import { CalendarServiceMap } from "../calendar.services.generated";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["CalendarManager"] });
|
|
|
|
export const getCalendar = async (
|
|
credential: CredentialForCalendarService | null,
|
|
shouldServeCache?: boolean
|
|
): Promise<Calendar | null> => {
|
|
if (!credential || !credential.key) return null;
|
|
let { type: calendarType } = credential;
|
|
if (calendarType?.endsWith("_other_calendar")) {
|
|
calendarType = calendarType.split("_other_calendar")[0];
|
|
}
|
|
// Backwards compatibility until CRM manager is created
|
|
if (calendarType?.endsWith("_crm")) {
|
|
calendarType = calendarType.split("_crm")[0];
|
|
}
|
|
|
|
const calendarAppImportFn =
|
|
CalendarServiceMap[calendarType.split("_").join("") as keyof typeof CalendarServiceMap];
|
|
|
|
if (!calendarAppImportFn) {
|
|
log.warn(`calendar of type ${calendarType} is not implemented`);
|
|
return null;
|
|
}
|
|
|
|
const calendarApp = await calendarAppImportFn;
|
|
|
|
const CalendarService = calendarApp.default;
|
|
|
|
if (!CalendarService || typeof CalendarService !== "function") {
|
|
log.warn(`calendar of type ${calendarType} is not implemented`);
|
|
return null;
|
|
}
|
|
// if shouldServeCache is not supplied, determine on the fly.
|
|
if (typeof shouldServeCache === "undefined") {
|
|
const featuresRepository = new FeaturesRepository(prisma);
|
|
const [isCalendarSubscriptionCacheEnabled, isCalendarSubscriptionCacheEnabledForUser] = await Promise.all(
|
|
[
|
|
featuresRepository.checkIfFeatureIsEnabledGlobally(
|
|
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
|
|
),
|
|
featuresRepository.checkIfUserHasFeatureNonHierarchical(
|
|
credential.userId as number,
|
|
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
|
|
),
|
|
]
|
|
);
|
|
shouldServeCache = isCalendarSubscriptionCacheEnabled && isCalendarSubscriptionCacheEnabledForUser;
|
|
log.debug("Cache feature flag check", {
|
|
credentialId: credential.id,
|
|
userId: credential.userId,
|
|
isCalendarSubscriptionCacheEnabled,
|
|
isCalendarSubscriptionCacheEnabledForUser,
|
|
shouldServeCache,
|
|
});
|
|
}
|
|
const isCacheSupported = CalendarCacheEventService.isCalendarTypeSupported(calendarType);
|
|
|
|
const originalCalendar = new CalendarService(credential as any);
|
|
|
|
// Determine if we should use cache
|
|
const useCache = isCacheSupported && shouldServeCache;
|
|
|
|
// Build the calendar chain: original -> cache (if enabled) -> telemetry (if enabled)
|
|
let calendar: Calendar = originalCalendar;
|
|
|
|
if (useCache) {
|
|
log.info(`Calendar Cache is enabled, using CalendarCacheWrapper for credential ${credential.id}`);
|
|
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
|
|
calendar = new CalendarCacheWrapper({
|
|
originalCalendar: calendar,
|
|
calendarCacheEventRepository,
|
|
});
|
|
}
|
|
|
|
// Wrap ALL calendars with telemetry when telemetry is enabled
|
|
// This provides consistent metrics for all calendar types
|
|
if (isTelemetryEnabled()) {
|
|
log.info(
|
|
`Using CalendarTelemetryWrapper for credential ${credential.id} (cacheSupported: ${isCacheSupported}, cacheEnabled: ${useCache})`
|
|
);
|
|
calendar = new CalendarTelemetryWrapper({
|
|
originalCalendar: calendar,
|
|
calendarType,
|
|
cacheSupported: isCacheSupported,
|
|
cacheEnabled: useCache,
|
|
credentialId: credential.id,
|
|
});
|
|
}
|
|
|
|
return calendar;
|
|
};
|