chore: upgrade sentry and start using metrics for Calendar Cache (#26827)

* upgrade sentry and start using metrics

* format changes

* format changes

* format changes

* type error

* type error

* format changes

* format changes

* fix: add updateSyncStatus call on API error and mock Sentry metrics in tests

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Volnei Munhoz
2026-01-14 12:29:09 +00:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 38ed00d71a
commit cfbcd0f95b
8 changed files with 1155 additions and 665 deletions
+1 -1
View File
@@ -33,7 +33,7 @@
"@calcom/prisma": "workspace:*",
"@calcom/trpc": "workspace:*",
"@prisma/nextjs-monorepo-workaround-plugin": "6.16.1",
"@sentry/nextjs": "9.15.0",
"@sentry/nextjs": "10.33.0",
"bcryptjs": "2.4.3",
"memory-cache": "0.2.0",
"next": "16.1.0",
+1 -1
View File
@@ -75,7 +75,7 @@
"@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-toggle-group": "1.0.4",
"@radix-ui/react-tooltip": "1.0.6",
"@sentry/nextjs": "9.15.0",
"@sentry/nextjs": "10.33.0",
"@stripe/react-stripe-js": "1.10.0",
"@stripe/stripe-js": "1.35.0",
"@tanstack/react-query": "5.17.19",
+1
View File
@@ -112,6 +112,7 @@ export const getCalendar = async (
cacheSupported: isCacheSupported,
cacheEnabled: useCache,
credentialId: credential.id,
mode,
});
}
@@ -12,7 +12,7 @@ import type { CalendarSyncService } from "@calcom/features/calendar-subscription
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
import type { ISelectedCalendarRepository } from "@calcom/features/selectedCalendar/repositories/SelectedCalendarRepository.interface";
import logger from "@calcom/lib/logger";
import { withSpan } from "@calcom/lib/sentryWrapper";
import { metrics } from "@sentry/nextjs";
import type { SelectedCalendar } from "@calcom/prisma/client";
// biome-ignore lint/nursery/useExplicitType: logger type is inferred
@@ -68,7 +68,6 @@ export class CalendarSubscriptionService {
syncSubscribedAt: new Date(),
});
// initial event loading
await this.processEvents(selectedCalendar);
}
@@ -112,93 +111,96 @@ export class CalendarSubscriptionService {
/**
* Process webhook
*/
// biome-ignore lint/nursery/useExplicitType: return type is inferred from withSpan
// biome-ignore lint/complexity/noExcessiveLinesPerFunction: webhook processing requires multiple steps
async processWebhook(provider: CalendarSubscriptionProvider, request: Request) {
return withSpan(
{
name: "CalendarSubscriptionService.processWebhook",
op: "calendar.subscription.webhook",
attributes: {
provider,
},
},
// biome-ignore lint/complexity/noExcessiveLinesPerFunction: webhook processing requires multiple steps
async (span) => {
const startTime = performance.now();
log.debug("processWebhook", { provider });
async processWebhook(
provider: CalendarSubscriptionProvider,
request: Request
) {
const startTime = performance.now();
log.debug("processWebhook", { provider });
try {
const calendarSubscriptionAdapter = this.deps.adapterFactory.get(provider);
try {
const calendarSubscriptionAdapter =
this.deps.adapterFactory.get(provider);
const isValid = await calendarSubscriptionAdapter.validate(request);
if (!isValid) {
span.setAttribute("success", false);
span.setAttribute("errorMessage", "Invalid webhook request");
throw new Error("Invalid webhook request");
}
const channelId = await calendarSubscriptionAdapter.extractChannelId(request);
if (!channelId) {
span.setAttribute("success", false);
span.setAttribute("errorMessage", "Missing channel ID in webhook");
throw new Error("Missing channel ID in webhook");
}
log.debug("Processing webhook", { channelId });
span.setAttribute("channelId", channelId);
const selectedCalendar = await this.deps.selectedCalendarRepository.findByChannelId(channelId);
// it maybe caused by an old subscription being triggered
if (!selectedCalendar) {
span.setAttribute("success", true);
span.setAttribute("skipped", true);
span.setAttribute("skipReason", "Calendar not found for channel");
log.info("Webhook processed - calendar not found", {
provider,
channelId,
durationMs: performance.now() - startTime,
});
return null;
}
span.setAttribute("selectedCalendarId", selectedCalendar.id);
span.setAttribute("userId", selectedCalendar.userId);
// incremental event loading
const eventsProcessed = await this.processEvents(selectedCalendar);
const durationMs = performance.now() - startTime;
span.setAttribute("success", true);
span.setAttribute("durationMs", durationMs);
span.setAttribute("eventsProcessedCount", eventsProcessed.eventsFetched);
span.setAttribute("eventsCachedCount", eventsProcessed.eventsCached);
span.setAttribute("eventsSyncedCount", eventsProcessed.eventsSynced);
log.info("Webhook processed successfully", {
provider,
channelId,
selectedCalendarId: selectedCalendar.id,
durationMs,
});
} catch (error) {
const durationMs = performance.now() - startTime;
span.setAttribute("success", false);
span.setAttribute("durationMs", durationMs);
// biome-ignore lint/nursery/noTernary: simple error message extraction
span.setAttribute("errorMessage", error instanceof Error ? error.message : "Unknown error");
log.error("Webhook processing failed", {
provider,
durationMs,
// biome-ignore lint/nursery/noTernary: simple error message extraction
error: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
const isValid = await calendarSubscriptionAdapter.validate(request);
if (!isValid) {
metrics.count("calendar.subscription.webhook.invalid", 1, {
attributes: { provider },
});
throw new Error("Invalid webhook request");
}
);
const channelId = await calendarSubscriptionAdapter.extractChannelId(
request
);
if (!channelId) {
metrics.count("calendar.subscription.webhook.missing_channel", 1, {
attributes: { provider },
});
throw new Error("Missing channel ID in webhook");
}
log.debug("Processing webhook", { channelId });
const selectedCalendar =
await this.deps.selectedCalendarRepository.findByChannelId(channelId);
if (!selectedCalendar) {
metrics.count("calendar.subscription.webhook.skipped", 1, {
attributes: { provider, reason: "calendar_not_found" },
});
log.info("Webhook processed - calendar not found", {
provider,
channelId,
durationMs: performance.now() - startTime,
});
return null;
}
const eventsProcessed = await this.processEvents(selectedCalendar);
const durationMs = performance.now() - startTime;
metrics.count("calendar.subscription.webhook.success", 1, {
attributes: { provider },
});
metrics.distribution(
"calendar.subscription.webhook.duration_ms",
durationMs,
{
attributes: { provider },
}
);
metrics.distribution(
"calendar.subscription.webhook.events_fetched",
eventsProcessed.eventsFetched,
{ attributes: { provider } }
);
} catch (error) {
const durationMs = performance.now() - startTime;
metrics.count("calendar.subscription.webhook.error", 1, {
attributes: { provider },
});
metrics.distribution(
"calendar.subscription.webhook.duration_ms",
durationMs,
{
attributes: { provider, outcome: "error" },
}
);
log.error("Webhook processing failed", {
provider,
durationMs,
error: error instanceof Error ? error.message : "Unknown error",
});
throw error;
}
}
/**
@@ -219,129 +221,160 @@ export class CalendarSubscriptionService {
eventsSynced: number;
propagationLagMs?: { avg: number; max: number; min: number; count: number };
}> {
return withSpan(
const startTime = performance.now();
const result: {
eventsFetched: number;
eventsCached: number;
eventsSynced: number;
propagationLagMs?: { avg: number; max: number; min: number; count: number };
} = {
eventsFetched: 0,
eventsCached: 0,
eventsSynced: 0,
};
const calendarSubscriptionAdapter = this.deps.adapterFactory.get(
selectedCalendar.integration as CalendarSubscriptionProvider
);
if (
!selectedCalendar.credentialId &&
!selectedCalendar.delegationCredentialId
) {
log.debug("Selected Calendar doesn't have credentials", {
selectedCalendarId: selectedCalendar.id,
});
return result;
}
const [cacheEnabled, syncEnabled, cacheEnabledForUser] = await Promise.all([
this.isCacheEnabled(),
this.isSyncEnabled(),
this.isCacheEnabledForUser(selectedCalendar.userId),
]);
if (!cacheEnabled && !syncEnabled) {
log.info("Cache and sync are globally disabled", {
channelId: selectedCalendar.channelId,
});
return result;
}
log.debug("Processing events", { channelId: selectedCalendar.channelId });
const credential = await this.getCredential(selectedCalendar);
if (!credential) {
return result;
}
let events: CalendarSubscriptionEvent | null = null;
try {
events = await calendarSubscriptionAdapter.fetchEvents(
selectedCalendar,
credential
);
} catch (err) {
metrics.count("calendar.subscription.events.fetch.error", 1, {
attributes: { provider: selectedCalendar.integration },
});
await this.deps.selectedCalendarRepository.updateSyncStatus(selectedCalendar.id, {
syncErrorAt: new Date(),
syncErrorCount: { increment: 1 },
});
throw err;
}
if (!events?.items?.length) {
log.debug("No events fetched", { channelId: selectedCalendar.channelId });
return result;
}
result.eventsFetched = events.items.length;
metrics.distribution(
"calendar.subscription.events.fetched",
events.items.length,
{
name: "CalendarSubscriptionService.processEvents",
op: "calendar.subscription.processEvents",
attributes: {
provider: selectedCalendar.integration,
selectedCalendarId: selectedCalendar.id,
userId: selectedCalendar.userId,
isIncremental: !!selectedCalendar.syncToken,
incremental: !!selectedCalendar.syncToken,
},
},
// biome-ignore lint/complexity/noExcessiveLinesPerFunction: event processing requires multiple steps
async (span) => {
const result: {
eventsFetched: number;
eventsCached: number;
eventsSynced: number;
propagationLagMs?: { avg: number; max: number; min: number; count: number };
} = { eventsFetched: 0, eventsCached: 0, eventsSynced: 0 };
const calendarSubscriptionAdapter = this.deps.adapterFactory.get(
selectedCalendar.integration as CalendarSubscriptionProvider
);
if (!selectedCalendar.credentialId && !selectedCalendar.delegationCredentialId) {
log.debug("Selected Calendar doesn't have credentials", { selectedCalendarId: selectedCalendar.id });
span.setAttribute("skipped", true);
span.setAttribute("skipReason", "No credentials");
return result;
}
// for cache the feature should be enabled globally and by user/team features
const [cacheEnabled, syncEnabled, cacheEnabledForUser] = await Promise.all([
this.isCacheEnabled(),
this.isSyncEnabled(),
this.isCacheEnabledForUser(selectedCalendar.userId),
]);
if (!cacheEnabled && !syncEnabled) {
log.info("Cache and sync are globally disabled", { channelId: selectedCalendar.channelId });
span.setAttribute("skipped", true);
span.setAttribute("skipReason", "Cache and sync disabled");
return result;
}
log.debug("Processing events", { channelId: selectedCalendar.channelId });
const credential = await this.getCredential(selectedCalendar);
if (!credential) {
span.setAttribute("skipped", true);
span.setAttribute("skipReason", "No credential found");
return result;
}
let events: CalendarSubscriptionEvent | null = null;
try {
events = await calendarSubscriptionAdapter.fetchEvents(selectedCalendar, credential);
} catch (err) {
log.debug("Error fetching events", { channelId: selectedCalendar.channelId, err });
await this.deps.selectedCalendarRepository.updateSyncStatus(selectedCalendar.id, {
syncErrorAt: new Date(),
syncErrorCount: { increment: 1 },
});
span.setAttribute("success", false);
// biome-ignore lint/nursery/noTernary: simple error message extraction
span.setAttribute("errorMessage", err instanceof Error ? err.message : "Unknown error");
throw err;
}
if (!events?.items?.length) {
log.debug("No events fetched", { channelId: selectedCalendar.channelId });
span.setAttribute("eventsFetched", 0);
return result;
}
result.eventsFetched = events.items.length;
span.setAttribute("eventsFetched", events.items.length);
// Calculate propagation lag: time between event update in Google and now
// Only meaningful for incremental syncs (when syncToken exists)
const now = Date.now();
const lagStats = this.calculatePropagationLag(events.items, now);
if (lagStats) {
result.propagationLagMs = lagStats;
span.setAttribute("propagationLagAvgMs", lagStats.avg);
span.setAttribute("propagationLagMaxMs", lagStats.max);
span.setAttribute("propagationLagMinMs", lagStats.min);
span.setAttribute("propagationLagEventCount", lagStats.count);
log.info("Propagation lag calculated", {
channelId: selectedCalendar.channelId,
avgMs: lagStats.avg,
maxMs: lagStats.max,
minMs: lagStats.min,
eventCount: lagStats.count,
});
}
log.debug("Processing events", { channelId: selectedCalendar.channelId, count: events.items.length });
await this.deps.selectedCalendarRepository.updateSyncStatus(selectedCalendar.id, {
syncToken: events.syncToken || selectedCalendar.syncToken,
syncedAt: new Date(),
syncErrorAt: null,
syncErrorCount: 0,
});
// it requires both global and team/user feature cache enabled
if (cacheEnabled && cacheEnabledForUser) {
log.debug("Caching events", { count: events.items.length });
await this.deps.calendarCacheEventService.handleEvents(selectedCalendar, events.items);
result.eventsCached = events.items.length;
span.setAttribute("eventsCached", events.items.length);
}
if (syncEnabled) {
log.debug("Syncing events", { count: events.items.length });
await this.deps.calendarSyncService.handleEvents(selectedCalendar, events.items);
result.eventsSynced = events.items.length;
span.setAttribute("eventsSynced", events.items.length);
}
span.setAttribute("success", true);
return result;
}
);
const now = Date.now();
const lagStats = this.calculatePropagationLag(events.items, now);
if (lagStats) {
result.propagationLagMs = lagStats;
metrics.distribution(
"calendar.subscription.propagation_lag.avg_ms",
lagStats.avg,
{
attributes: { provider: selectedCalendar.integration },
}
);
metrics.distribution(
"calendar.subscription.propagation_lag.max_ms",
lagStats.max,
{
attributes: { provider: selectedCalendar.integration },
}
);
}
await this.deps.selectedCalendarRepository.updateSyncStatus(selectedCalendar.id, {
syncToken: events.syncToken || selectedCalendar.syncToken,
syncedAt: new Date(),
syncErrorAt: null,
syncErrorCount: 0,
}
);
if (cacheEnabled && cacheEnabledForUser) {
log.debug("Caching events", { count: events.items.length });
await this.deps.calendarCacheEventService.handleEvents(selectedCalendar, events.items );
result.eventsCached = events.items.length;
metrics.distribution(
"calendar.subscription.events.cached",
events.items.length,
{
attributes: { provider: selectedCalendar.integration },
}
);
}
if (syncEnabled) {
log.debug("Syncing events", { count: events.items.length });
await this.deps.calendarSyncService.handleEvents(
selectedCalendar,
events.items
);
result.eventsSynced = events.items.length;
metrics.distribution(
"calendar.subscription.events.synced",
events.items.length,
{
attributes: { provider: selectedCalendar.integration },
}
);
}
metrics.distribution(
"calendar.subscription.processEvents.duration_ms",
performance.now() - startTime,
{
attributes: {
provider: selectedCalendar.integration,
cache: cacheEnabled && cacheEnabledForUser ? "on" : "off",
sync: syncEnabled ? "on" : "off",
},
}
);
return result;
}
/**
@@ -385,8 +418,8 @@ export class CalendarSubscriptionService {
// biome-ignore lint/nursery/useExplicitType: return type is void
async checkForNewSubscriptions() {
const teamIds = await this.deps.featuresRepository.getTeamsWithFeatureEnabled(
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
);
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
);
const rows = await this.deps.selectedCalendarRepository.findNextSubscriptionBatch({
take: 100,
@@ -2,6 +2,13 @@ import "../__mocks__/delegationCredential";
import { describe, test, expect, vi, beforeEach } from "vitest";
vi.mock("@sentry/nextjs", () => ({
metrics: {
count: vi.fn(),
distribution: vi.fn(),
},
}));
import type { AdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import type { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import type { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
@@ -1,6 +1,6 @@
import type { ICalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository.interface";
import logger from "@calcom/lib/logger";
import { withSpan } from "@calcom/lib/sentryWrapper";
import { metrics } from "@sentry/nextjs";
import type {
Calendar,
CalendarEvent,
@@ -13,11 +13,6 @@ import type {
const log = logger.getSubLogger({ prefix: ["CachedCalendarWrapper"] });
/**
* A wrapper to load cache from database and cache it.
*
* @see Calendar
*/
export class CalendarCacheWrapper implements Calendar {
constructor(
private deps: {
@@ -59,85 +54,82 @@ export class CalendarCacheWrapper implements Calendar {
*/
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
return withSpan(
{
name: "CalendarCacheWrapper.getAvailability",
op: "calendar.cache.internal.getAvailability",
attributes: {
calendarCount: selectedCalendars?.length ?? 0,
},
},
async (span) => {
log.debug("getAvailability (mixed cache + original)", {
dateFrom,
dateTo,
calendarIds: selectedCalendars.map((c) => c.id),
calendarCount: selectedCalendars.length,
});
if (!selectedCalendars?.length) return [];
log.debug("getAvailability (mixed cache + original)", {
dateFrom,
dateTo,
calendarIds: selectedCalendars.map((c) => c.id),
calendarCount: selectedCalendars.length,
});
const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt);
if (!selectedCalendars?.length) return [];
const results: EventBusyDate[] = [];
const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt);
// Fetch from cache for synced calendars
if (withSync.length) {
const cacheStartTime = performance.now();
const ids = withSync.map((c) => c.id).filter((id): id is string => Boolean(id));
const cached = await this.deps.calendarCacheEventRepository.findAllBySelectedCalendarIdsBetween(
ids,
new Date(dateFrom),
new Date(dateTo)
);
const cacheDurationMs = performance.now() - cacheStartTime;
results.push(
...cached.map((event) => ({
...event,
timeZone: event.timeZone ?? undefined,
}))
);
const results: EventBusyDate[] = [];
span.setAttribute("cachedCalendarCount", withSync.length);
span.setAttribute("cacheFetchDurationMs", cacheDurationMs);
span.setAttribute("cachedEventsCount", cached.length);
log.info("Calendar cache fetch completed", {
cachedCalendarCount: withSync.length,
cacheFetchDurationMs: cacheDurationMs,
cachedEventsCount: cached.length,
});
}
// ===== CACHE PATH =====
if (withSync.length) {
const cacheStartTime = performance.now();
// Fetch from original calendar for unsynced ones
if (withoutSync.length) {
const originalStartTime = performance.now();
const original = await this.deps.originalCalendar.getAvailability({
dateFrom,
dateTo,
selectedCalendars: withoutSync,
mode: params.mode,
fallbackToPrimary: params.fallbackToPrimary,
});
const originalDurationMs = performance.now() - originalStartTime;
results.push(...original);
const ids = withSync.map((c) => c.id).filter((id): id is string => Boolean(id));
const cached = await this.deps.calendarCacheEventRepository.findAllBySelectedCalendarIdsBetween(
ids,
new Date(dateFrom),
new Date(dateTo)
);
span.setAttribute("originalCalendarCount", withoutSync.length);
span.setAttribute("originalFetchDurationMs", originalDurationMs);
span.setAttribute("originalEventsCount", original.length);
log.info("Original calendar fetch completed", {
originalCalendarCount: withoutSync.length,
originalFetchDurationMs: originalDurationMs,
originalEventsCount: original.length,
});
}
const cacheDurationMs = performance.now() - cacheStartTime;
span.setAttribute("cacheUsed", withSync.length > 0);
span.setAttribute("totalEventsCount", results.length);
results.push(
...cached.map((event) => ({
...event,
timeZone: event.timeZone ?? undefined,
}))
);
return results;
}
);
metrics.count("calendar.cache.hit.calls", 1);
metrics.distribution("calendar.cache.hit.duration_ms", cacheDurationMs);
metrics.distribution("calendar.cache.hit.events_count", cached.length);
log.info("Calendar cache fetch completed", {
cachedCalendarCount: withSync.length,
cacheFetchDurationMs: cacheDurationMs,
cachedEventsCount: cached.length,
});
}
// ===== ORIGINAL PATH =====
if (withoutSync.length) {
const originalStartTime = performance.now();
const original = await this.deps.originalCalendar.getAvailability({
dateFrom,
dateTo,
selectedCalendars: withoutSync,
mode: params.mode,
fallbackToPrimary: params.fallbackToPrimary,
});
const originalDurationMs = performance.now() - originalStartTime;
results.push(...original);
metrics.count("calendar.cache.miss.calls", 1);
metrics.distribution("calendar.cache.miss.duration_ms", originalDurationMs);
metrics.distribution("calendar.cache.miss.events_count", original.length);
log.info("Original calendar fetch completed", {
originalCalendarCount: withoutSync.length,
originalFetchDurationMs: originalDurationMs,
originalEventsCount: original.length,
});
}
metrics.distribution("calendar.getAvailability.total_events_count", results.length);
return results;
}
/**
@@ -149,86 +141,69 @@ export class CalendarCacheWrapper implements Calendar {
*/
async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
return withSpan(
{
name: "CalendarCacheWrapper.getAvailabilityWithTimeZones",
op: "calendar.cache.internal.getAvailabilityWithTimeZones",
attributes: {
calendarCount: selectedCalendars?.length ?? 0,
},
},
async (span) => {
log.debug("getAvailabilityWithTimeZones (mixed cache + original)", {
dateFrom,
dateTo,
calendarIds: selectedCalendars.map((c) => c.id),
calendarCount: selectedCalendars.length,
});
if (!selectedCalendars?.length) return [];
log.debug("getAvailabilityWithTimeZones (mixed cache + original)", {
dateFrom,
dateTo,
calendarIds: selectedCalendars.map((c) => c.id),
calendarCount: selectedCalendars.length,
});
const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt);
if (!selectedCalendars?.length) return [];
const results: EventBusyDate[] = [];
const withSync = selectedCalendars.filter((c) => c.syncToken && c.syncSubscribedAt);
const withoutSync = selectedCalendars.filter((c) => !c.syncToken || !c.syncSubscribedAt);
// Fetch from cache for synced calendars
if (withSync.length) {
const cacheStartTime = performance.now();
const ids = withSync.map((c) => c.id).filter((id): id is string => Boolean(id));
const cached = await this.deps.calendarCacheEventRepository.findAllBySelectedCalendarIdsBetween(
ids,
new Date(dateFrom),
new Date(dateTo)
);
const cacheDurationMs = performance.now() - cacheStartTime;
results.push(
...cached.map(({ start, end, timeZone }) => ({
start,
end,
timeZone: timeZone || "UTC",
}))
);
const results: EventBusyDate[] = [];
span.setAttribute("cachedCalendarCount", withSync.length);
span.setAttribute("cacheFetchDurationMs", cacheDurationMs);
span.setAttribute("cachedEventsCount", cached.length);
log.info("Calendar cache fetch with timezones completed", {
cachedCalendarCount: withSync.length,
cacheFetchDurationMs: cacheDurationMs,
cachedEventsCount: cached.length,
});
}
if (withSync.length) {
const cacheStartTime = performance.now();
// Fetch from original calendar for unsynced ones
if (withoutSync.length) {
const originalStartTime = performance.now();
const original = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.({
dateFrom,
dateTo,
selectedCalendars: withoutSync,
mode: params.mode,
fallbackToPrimary: params.fallbackToPrimary,
});
const originalDurationMs = performance.now() - originalStartTime;
if (original?.length) results.push(...original);
const ids = withSync.map((c) => c.id).filter((id): id is string => Boolean(id));
const cached = await this.deps.calendarCacheEventRepository.findAllBySelectedCalendarIdsBetween(
ids,
new Date(dateFrom),
new Date(dateTo)
);
span.setAttribute("originalCalendarCount", withoutSync.length);
span.setAttribute("originalFetchDurationMs", originalDurationMs);
span.setAttribute("originalEventsCount", original?.length ?? 0);
log.info("Original calendar fetch with timezones completed", {
originalCalendarCount: withoutSync.length,
originalFetchDurationMs: originalDurationMs,
originalEventsCount: original?.length ?? 0,
});
}
const cacheDurationMs = performance.now() - cacheStartTime;
span.setAttribute("cacheUsed", withSync.length > 0);
span.setAttribute("totalEventsCount", results.length);
results.push(
...cached.map(({ start, end, timeZone }) => ({
start,
end,
timeZone: timeZone || "UTC",
}))
);
return results;
}
);
metrics.count("calendar.cache.hit.timezone.calls", 1);
metrics.distribution("calendar.cache.hit.timezone.duration_ms", cacheDurationMs);
metrics.distribution("calendar.cache.hit.timezone.events_count", cached.length);
}
if (withoutSync.length) {
const originalStartTime = performance.now();
const original = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.({
dateFrom,
dateTo,
selectedCalendars: withoutSync,
mode: params.mode,
fallbackToPrimary: params.fallbackToPrimary,
});
const originalDurationMs = performance.now() - originalStartTime;
if (original?.length) results.push(...original);
metrics.count("calendar.cache.miss.timezone.calls", 1);
metrics.distribution("calendar.cache.miss.timezone.duration_ms", originalDurationMs);
metrics.distribution("calendar.cache.miss.timezone.events_count", original?.length ?? 0);
}
metrics.distribution("calendar.getAvailabilityWithTimeZones.total_events_count", results.length);
return results;
}
fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise<unknown> {
@@ -1,8 +1,9 @@
import logger from "@calcom/lib/logger";
import { withSpan } from "@calcom/lib/sentryWrapper";
import { metrics } from "@sentry/nextjs";
import type {
Calendar,
CalendarEvent,
CalendarFetchMode,
CalendarServiceEvent,
EventBusyDate,
GetAvailabilityParams,
@@ -26,6 +27,7 @@ export class CalendarTelemetryWrapper implements Calendar {
cacheSupported: boolean;
cacheEnabled: boolean;
credentialId: number;
mode: CalendarFetchMode;
}
) {}
@@ -55,109 +57,129 @@ export class CalendarTelemetryWrapper implements Calendar {
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
return withSpan(
{
name: "CalendarTelemetryWrapper.getAvailability",
op: "calendar.getAvailability",
attributes: {
credentialId: this.deps.credentialId,
calendarCount: selectedCalendars?.length ?? 0,
calendarType: this.deps.calendarType,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
},
if (!selectedCalendars?.length) return [];
log.debug("getAvailability", {
dateFrom,
dateTo,
calendarCount: selectedCalendars.length,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
mode: this.deps.mode,
});
const startTime = performance.now();
const results = await this.deps.originalCalendar.getAvailability({
dateFrom,
dateTo,
selectedCalendars,
mode: params.mode,
fallbackToPrimary: params.fallbackToPrimary,
});
const totalFetchDurationMs = performance.now() - startTime;
metrics.count("calendar.getAvailability.calls", 1, {
attributes: {
cache: this.deps.cacheEnabled ? "on" : "off",
calendarType: this.deps.calendarType,
mode: String(this.deps.mode),
},
async (span) => {
log.debug("getAvailability", {
dateFrom,
dateTo,
calendarCount: selectedCalendars.length,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
});
});
if (!selectedCalendars?.length) return [];
metrics.distribution("calendar.getAvailability.duration_ms", totalFetchDurationMs, {
attributes: {
cache: this.deps.cacheEnabled ? "on" : "off",
calendarType: this.deps.calendarType,
mode: String(this.deps.mode),
},
});
const startTime = performance.now();
const results = await this.deps.originalCalendar.getAvailability({
dateFrom,
dateTo,
selectedCalendars,
mode: params.mode,
fallbackToPrimary: params.fallbackToPrimary,
});
const totalFetchDurationMs = performance.now() - startTime;
metrics.distribution("calendar.getAvailability.events_count", results.length, {
attributes: {
cache: this.deps.cacheEnabled ? "on" : "off",
calendarType: this.deps.calendarType,
},
});
span.setAttribute("totalFetchDurationMs", totalFetchDurationMs);
span.setAttribute("totalEventsCount", results.length);
log.info("Calendar fetch completed", {
calendarCount: selectedCalendars.length,
totalFetchDurationMs,
totalEventsCount: results.length,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
mode: this.deps.mode,
});
log.info("Calendar fetch completed", {
calendarCount: selectedCalendars.length,
totalFetchDurationMs,
totalEventsCount: results.length,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
});
return results;
}
);
return results;
}
async getAvailabilityWithTimeZones(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
const { dateFrom, dateTo, selectedCalendars } = params;
// Check if the original calendar supports this method
if (!this.deps.originalCalendar.getAvailabilityWithTimeZones) {
return [];
}
return withSpan(
{
name: "CalendarTelemetryWrapper.getAvailabilityWithTimeZones",
op: "calendar.getAvailabilityWithTimeZones",
if (!selectedCalendars?.length) return [];
log.debug("getAvailabilityWithTimeZones", {
dateFrom,
dateTo,
calendarCount: selectedCalendars.length,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
mode: this.deps.mode,
});
const startTime = performance.now();
const results = await this.deps.originalCalendar.getAvailabilityWithTimeZones({
dateFrom,
dateTo,
selectedCalendars,
mode: params.mode,
fallbackToPrimary: params.fallbackToPrimary,
});
const totalFetchDurationMs = performance.now() - startTime;
metrics.count("calendar.getAvailabilityWithTimeZones.calls", 1, {
attributes: {
cache: this.deps.cacheEnabled ? "on" : "off",
calendarType: this.deps.calendarType,
mode: String(this.deps.mode),
}
});
metrics.distribution("calendar.getAvailabilityWithTimeZones.duration_ms", totalFetchDurationMs, {
attributes: {
cache: this.deps.cacheEnabled ? "on" : "off",
calendarType: this.deps.calendarType,
mode: String(this.deps.mode),
}
});
metrics.distribution("calendar.getAvailabilityWithTimeZones.events_count", results?.length ?? 0, {
attributes: {
credentialId: this.deps.credentialId,
calendarCount: selectedCalendars?.length ?? 0,
cache: this.deps.cacheEnabled ? "on" : "off",
calendarType: this.deps.calendarType,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
},
},
async (span) => {
log.debug("getAvailabilityWithTimeZones", {
dateFrom,
dateTo,
calendarCount: selectedCalendars.length,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
});
if (!selectedCalendars?.length) return [];
const startTime = performance.now();
const results = await this.deps.originalCalendar.getAvailabilityWithTimeZones?.({
dateFrom,
dateTo,
selectedCalendars,
mode: params.mode,
fallbackToPrimary: params.fallbackToPrimary,
});
const totalFetchDurationMs = performance.now() - startTime;
span.setAttribute("totalFetchDurationMs", totalFetchDurationMs);
span.setAttribute("totalEventsCount", results?.length ?? 0);
log.info("Calendar fetch with timezones completed", {
calendarCount: selectedCalendars.length,
totalFetchDurationMs,
totalEventsCount: results?.length ?? 0,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
});
return results ?? [];
}
);
log.info("Calendar fetch with timezones completed", {
calendarCount: selectedCalendars.length,
totalFetchDurationMs,
totalEventsCount: results?.length ?? 0,
cacheSupported: this.deps.cacheSupported,
cacheEnabled: this.deps.cacheEnabled,
mode: this.deps.mode,
});
return results ?? [];
}
fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise<unknown> {
+678 -226
View File
File diff suppressed because it is too large Load Diff