diff --git a/packages/emails/src/emails/ReachedChatsLimitEmail.tsx b/packages/emails/src/emails/ReachedChatsLimitEmail.tsx index 8859780332..8e8d7f2079 100644 --- a/packages/emails/src/emails/ReachedChatsLimitEmail.tsx +++ b/packages/emails/src/emails/ReachedChatsLimitEmail.tsx @@ -65,6 +65,6 @@ export const sendReachedChatsLimitEmail = ({ ComponentProps) => sendEmail({ to, - subject: "You've reached your chats limit", + subject: "[Action Required] Chats limit reached", html: render().html, }); diff --git a/packages/scripts/src/checkAndReportChatsUsage.ts b/packages/scripts/src/checkAndReportChatsUsage.ts index ab93a89561..68ca6dbd7a 100644 --- a/packages/scripts/src/checkAndReportChatsUsage.ts +++ b/packages/scripts/src/checkAndReportChatsUsage.ts @@ -1,6 +1,7 @@ import { createId } from "@paralleldrive/cuid2"; import { getChatsLimit } from "@typebot.io/billing/helpers/getChatsLimit"; import { sendAlmostReachedChatsLimitEmail } from "@typebot.io/emails/emails/AlmostReachedChatsLimitEmail"; +import { sendReachedChatsLimitEmail } from "@typebot.io/emails/emails/ReachedChatsLimitEmail"; import { isDefined, isEmpty } from "@typebot.io/lib/utils"; import prisma from "@typebot.io/prisma"; import { Plan, WorkspaceRole } from "@typebot.io/prisma/enum"; @@ -9,6 +10,7 @@ import { trackEvents } from "@typebot.io/telemetry/trackEvents"; import type { Workspace } from "@typebot.io/workspaces/schemas"; import Stripe from "stripe"; import { promptAndSetEnvironment } from "./utils"; +import type { MemberInWorkspace } from ".prisma/client"; const LIMIT_EMAIL_TRIGGER_PERCENT = 0.75; @@ -75,6 +77,7 @@ export const checkAndReportChatsUsage = async () => { apiVersion: "2024-09-30.acacia", }); + const limitWarningEmailEvents: TelemetryEvent[] = []; const quarantineEvents: TelemetryEvent[] = []; const autoUpgradeEvents: TelemetryEvent[] = []; @@ -87,34 +90,14 @@ export const checkAndReportChatsUsage = async () => { subscription, }); if (chatsLimit === "inf") continue; - if ( - chatsLimit > 0 && - totalChatsUsed >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT && - totalChatsUsed < chatsLimit && - !workspace.chatsLimitFirstEmailSentAt - ) { - const to = workspace.members - .filter((member) => member.role === WorkspaceRole.ADMIN) - .map((member) => member.user.email) - .filter(isDefined); - console.log( - `Send almost reached chats limit email to ${to.join(", ")}...`, - ); - try { - await sendAlmostReachedChatsLimitEmail({ - to, - usagePercent: Math.round((totalChatsUsed / chatsLimit) * 100), - chatsLimit, - workspaceName: workspace.name, - }); - await prisma.workspace.updateMany({ - where: { id: workspace.id }, - data: { chatsLimitFirstEmailSentAt: new Date() }, - }); - } catch (err) { - console.error(err); - } - } + + limitWarningEmailEvents.push( + ...(await sendLimitWarningEmails({ + chatsLimit, + totalChatsUsed, + workspace, + })), + ); const isUsageBasedSubscription = isDefined( subscription?.items.data.find( @@ -219,10 +202,14 @@ export const checkAndReportChatsUsage = async () => { ); console.log( - `Send ${newResultsCollectedEvents.length} new results events and ${quarantineEvents.length} auto quarantine events...`, + `Send ${limitWarningEmailEvents.length}, ${newResultsCollectedEvents.length} new results events and ${quarantineEvents.length} auto quarantine events...`, ); - await trackEvents(quarantineEvents.concat(newResultsCollectedEvents)); + await trackEvents( + limitWarningEmailEvents.concat( + quarantineEvents.concat(newResultsCollectedEvents), + ), + ); }; const getSubscription = async ( @@ -372,4 +359,89 @@ const autoUpgradeToPro = async ( return newSubscription; }; +async function sendLimitWarningEmails({ + chatsLimit, + totalChatsUsed, + workspace, +}: { + chatsLimit: number; + totalChatsUsed: number; + workspace: Pick< + Workspace, + "id" | "name" | "chatsLimitFirstEmailSentAt" | "chatsLimitSecondEmailSentAt" + > & { + members: (Pick & { + user: { id: string; email: string | null }; + })[]; + }; +}): Promise { + if ( + chatsLimit <= 0 || + totalChatsUsed < chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT + ) + return []; + + const emailEvents: TelemetryEvent[] = []; + const adminMembers = workspace.members.filter( + (member) => member.role === WorkspaceRole.ADMIN, + ); + const to = adminMembers.map((member) => member.user.email).filter(isDefined); + if (!workspace.chatsLimitFirstEmailSentAt) { + console.log(`Send almost reached chats limit email to ${to.join(", ")}...`); + try { + await sendAlmostReachedChatsLimitEmail({ + to, + usagePercent: Math.round((totalChatsUsed / chatsLimit) * 100), + chatsLimit, + workspaceName: workspace.name, + }); + emailEvents.push( + ...adminMembers.map( + (m) => + ({ + name: "Limit warning email sent", + userId: m.user.id, + workspaceId: workspace.id, + }) satisfies TelemetryEvent, + ), + ); + await prisma.workspace.updateMany({ + where: { id: workspace.id }, + data: { chatsLimitFirstEmailSentAt: new Date() }, + }); + } catch (err) { + console.error(err); + } + } + + if (totalChatsUsed >= chatsLimit && !workspace.chatsLimitSecondEmailSentAt) { + console.log(`Send reached chats limit email to ${to.join(", ")}...`); + try { + await sendReachedChatsLimitEmail({ + to, + chatsLimit, + url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`, + }); + emailEvents.push( + ...adminMembers.map( + (m) => + ({ + name: "Limit reached email sent", + userId: m.user.id, + workspaceId: workspace.id, + }) satisfies TelemetryEvent, + ), + ); + await prisma.workspace.updateMany({ + where: { id: workspace.id }, + data: { chatsLimitSecondEmailSentAt: new Date() }, + }); + } catch (err) { + console.error(err); + } + } + + return emailEvents; +} + checkAndReportChatsUsage().then(); diff --git a/packages/telemetry/src/schemas.ts b/packages/telemetry/src/schemas.ts index b87e6958b3..c35a53dd1c 100644 --- a/packages/telemetry/src/schemas.ts +++ b/packages/telemetry/src/schemas.ts @@ -174,6 +174,18 @@ export const visitedAnalyticsEventSchema = typebotEvent.merge( }), ); +export const limitFirstEmailSentEventSchema = workspaceEvent.merge( + z.object({ + name: z.literal("Limit warning email sent"), + }), +); + +export const limitSecondEmailSentEventSchema = workspaceEvent.merge( + z.object({ + name: z.literal("Limit reached email sent"), + }), +); + export const clientSideEvents = [removedBrandingEventSchema] as const; export const eventSchema = z.discriminatedUnion("name", [ @@ -195,6 +207,8 @@ export const eventSchema = z.discriminatedUnion("name", [ createdFolderEventSchema, publishedFileUploadBlockEventSchema, visitedAnalyticsEventSchema, + limitFirstEmailSentEventSchema, + limitSecondEmailSentEventSchema, ...clientSideEvents, ]);