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:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
38ed00d71a
commit
cfbcd0f95b
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
+7
@@ -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";
|
||||
|
||||
+122
-147
@@ -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> {
|
||||
|
||||
+108
-86
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user