chore: Refactor feature flags (#13734)

Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Erik
2024-02-20 21:49:48 -03:00
committed by GitHub
co-authored by Omar López
parent 75d5576e17
commit 83ceb46cca
11 changed files with 85 additions and 38 deletions
+3 -3
View File
@@ -1,11 +1,11 @@
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
// If feature flag is disabled, return not found on getServerSideProps
export const getServerSideProps = async () => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
const insightsEnabled = await getFeatureFlag(prisma, "insights");
if (flags.insights === false) {
if (!insightsEnabled) {
return {
notFound: true,
} as const;
@@ -1,12 +1,12 @@
import type { GetServerSidePropsContext } from "next";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
const organizations = await getFeatureFlag(prisma, "organizations");
// Check if organizations are enabled
if (flags["organizations"] !== true) {
if (!organizations) {
return {
notFound: true,
} as const;
+3 -3
View File
@@ -4,7 +4,7 @@ import { z } from "zod";
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -24,7 +24,7 @@ const querySchema = z.object({
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
const signupDisabled = await getFeatureFlag(prisma, "disable-signup");
const ssr = await ssrInit(ctx);
const token = z.string().optional().parse(ctx.query.token);
@@ -38,7 +38,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
// username + email prepopulated from query params
const { username: preFillusername, email: prefilEmail } = querySchema.parse(ctx.query);
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || flags["disable-signup"]) {
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
return {
notFound: true,
} as const;
@@ -1,7 +1,7 @@
import type { GetServerSidePropsContext } from "next";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
@@ -37,12 +37,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
// Provided by Rewrite from next.config.js
const isOrgProfile = context.query?.isOrgProfile === "1";
const flags = await getFeatureFlagMap(prisma);
const isOrganizationFeatureEnabled = flags["organizations"];
const organizationsEnabled = await getFeatureFlag(prisma, "organizations");
log.debug("getServerSideProps", {
isOrgProfile,
isOrganizationFeatureEnabled,
isOrganizationFeatureEnabled: organizationsEnabled,
isValidOrgDomain,
currentOrgDomain,
});
@@ -74,7 +73,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (
(!isValidOrgDomain && team?.parent) ||
(!isValidOrgDomain && !!metadata?.isOrganization) ||
!isOrganizationFeatureEnabled
!organizationsEnabled
) {
return { notFound: true } as const;
}
@@ -9,9 +9,7 @@ afterEach(() => {
});
vi.mock("@calcom/features/flags/server/utils", () => ({
getFeatureFlagMap: vi.fn().mockResolvedValue({
"calendar-cache": true,
}),
getFeatureFlag: vi.fn().mockReturnValue(true),
}));
vi.mock("./getGoogleAppKeys", () => ({
@@ -6,7 +6,7 @@ import { RRule } from "rrule";
import { MeetLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import type CalendarService from "@calcom/lib/CalendarService";
import logger from "@calcom/lib/logger";
@@ -461,10 +461,10 @@ export default class GoogleCalendarService implements Calendar {
items: { id: string }[];
}): Promise<EventBusyDate[] | null> {
const calendar = await this.authedCalendar();
const flags = await getFeatureFlagMap(prisma);
const calendarCacheEnabled = await getFeatureFlag(prisma, "calendar-cache");
let freeBusyResult: calendar_v3.Schema$FreeBusyResponse = {};
if (!flags["calendar-cache"]) {
if (!calendarCacheEnabled) {
this.log.warn("Calendar Cache is disabled - Skipping");
const { timeMin, timeMax, items } = args;
const apires = await calendar.freebusy.query({
+3 -3
View File
@@ -3,7 +3,7 @@ import { createTransport } from "nodemailer";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@calcom/lib/serverConfig";
import { setTestEmail } from "@calcom/lib/testEmails";
@@ -28,9 +28,9 @@ export default class BaseEmail {
return {};
}
public async sendEmail() {
const featureFlags = await getFeatureFlagMap(prisma);
const emailsDisabled = await getFeatureFlag(prisma, "emails");
/** If email kill switch exists and is active, we prevent emails being sent. */
if (featureFlags.emails) {
if (emailsDisabled) {
console.warn("Skipped Sending Email due to active Kill Switch");
return new Promise((r) => r("Skipped Sending Email due to active Kill Switch"));
}
+5 -5
View File
@@ -6,7 +6,7 @@ import {
sendEmailVerificationLink,
sendChangeOfEmailVerificationLink,
} from "@calcom/emails/email-manager";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
@@ -24,9 +24,9 @@ interface VerifyEmailType {
export const sendEmailVerification = async ({ email, language, username }: VerifyEmailType) => {
const token = randomBytes(32).toString("hex");
const translation = await getTranslation(language ?? "en", "common");
const flags = await getFeatureFlagMap(prisma);
const emailVerification = await getFeatureFlag(prisma, "email-verification");
if (!flags["email-verification"]) {
if (!emailVerification) {
log.warn("Email verification is disabled - Skipping");
return { ok: true, skipped: true };
}
@@ -93,9 +93,9 @@ interface ChangeOfEmail {
export const sendChangeOfEmailVerification = async ({ user, language }: ChangeOfEmail) => {
const token = randomBytes(32).toString("hex");
const translation = await getTranslation(language ?? "en", "common");
const flags = await getFeatureFlagMap(prisma);
const emailVerification = await getFeatureFlag(prisma, "email-verification");
if (!flags["email-verification"]) {
if (!emailVerification) {
log.warn("Email verification is disabled - Skipping");
return { ok: true, skipped: true };
}
@@ -1,14 +1,14 @@
import type { GetServerSidePropsContext } from "next";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import { MembershipRole } from "@calcom/prisma/client";
export const getServerSideProps = async ({ req, res }: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
const organizationsEnabled = await getFeatureFlag(prisma, "organizations");
// Check if organizations are enabled
if (flags["organizations"] !== true) {
if (!organizationsEnabled) {
return {
notFound: true,
};
+52
View File
@@ -12,3 +12,55 @@ export async function getFeatureFlagMap(prisma: PrismaClient) {
return acc;
}, {} as Partial<AppFlags>);
}
interface CacheEntry {
value: boolean; // adapt to other supported value types in the future
expiry: number;
}
interface CacheOptions {
ttl: number; // time in ms
}
const featureFlagCache = new Map<keyof AppFlags, CacheEntry>();
const isExpired = (entry: CacheEntry): boolean => {
return Date.now() > entry.expiry;
};
export const getFeatureFlag = async (
prisma: PrismaClient,
slug: keyof AppFlags,
options: CacheOptions = { ttl: 5 * 60 * 1000 }
): Promise<boolean> => {
// pre-compute all app flags, each one will independelty reload it's own state after expiry.
if (featureFlagCache.size === 0) {
const flags = await prisma.feature.findMany({ orderBy: { slug: "asc" } });
flags.forEach((flag) => {
featureFlagCache.set(flag.slug as keyof AppFlags, {
value: flag.enabled,
expiry: Date.now() + options.ttl,
});
});
}
const cacheEntry = featureFlagCache.get(slug);
if (cacheEntry && !isExpired(cacheEntry)) {
return cacheEntry.value;
}
const flag = await prisma.feature.findUnique({
where: {
slug,
},
});
const isEnabled = Boolean(flag && flag.enabled);
const expiry = Date.now() + options.ttl;
featureFlagCache.set(slug, { value: isEnabled, expiry });
return isEnabled;
};
@@ -6,7 +6,7 @@ import stripe from "@calcom/app-store/stripepayment/lib/server";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
import { sendChangeOfEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
@@ -63,7 +63,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
const { user } = ctx;
const userMetadata = handleUserMetadata({ ctx, input });
const locale = input.locale || user.locale;
const flags = await getFeatureFlagMap(prisma);
const emailVerification = await getFeatureFlag(prisma, "email-verification");
const data: Prisma.UserUpdateInput = {
...input,
@@ -135,10 +135,8 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
hasEmailBeenChanged && user.identityProvider !== IdentityProvider.CAL;
const hasEmailChangedOnCalProvider = hasEmailBeenChanged && user.identityProvider === IdentityProvider.CAL;
const sendEmailVerification = flags["email-verification"];
if (hasEmailBeenChanged) {
if (sendEmailVerification && hasEmailChangedOnCalProvider) {
if (emailVerification && hasEmailChangedOnCalProvider) {
// Set metadata of the user so we can set it to this updated email once it is confirmed
data.metadata = {
...userMetadata,
@@ -284,12 +282,12 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
return {
...input,
email: sendEmailVerification && hasEmailChangedOnCalProvider ? user.email : input.email,
email: emailVerification && hasEmailChangedOnCalProvider ? user.email : input.email,
signOutUser,
passwordReset,
avatarUrl: updatedUser.avatarUrl,
hasEmailBeenChanged,
sendEmailVerification,
sendEmailVerification: emailVerification,
};
};