chore: Refactor feature flags (#13734)
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,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"));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user