Skip to content

Commit

Permalink
📈 Add limit warning email telemetry events
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Nov 26, 2024
1 parent c6b09f6 commit a7a15fd
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 31 deletions.
2 changes: 1 addition & 1 deletion packages/emails/src/emails/ReachedChatsLimitEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ export const sendReachedChatsLimitEmail = ({
ComponentProps<typeof ReachedChatsLimitEmail>) =>
sendEmail({
to,
subject: "You've reached your chats limit",
subject: "[Action Required] Chats limit reached",
html: render(<ReachedChatsLimitEmail {...props} />).html,
});
132 changes: 102 additions & 30 deletions packages/scripts/src/checkAndReportChatsUsage.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;

Expand Down Expand Up @@ -75,6 +77,7 @@ export const checkAndReportChatsUsage = async () => {
apiVersion: "2024-09-30.acacia",
});

const limitWarningEmailEvents: TelemetryEvent[] = [];
const quarantineEvents: TelemetryEvent[] = [];
const autoUpgradeEvents: TelemetryEvent[] = [];

Expand All @@ -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(
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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<MemberInWorkspace, "role"> & {
user: { id: string; email: string | null };
})[];
};
}): Promise<TelemetryEvent[]> {
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();
14 changes: 14 additions & 0 deletions packages/telemetry/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", [
Expand All @@ -195,6 +207,8 @@ export const eventSchema = z.discriminatedUnion("name", [
createdFolderEventSchema,
publishedFileUploadBlockEventSchema,
visitedAnalyticsEventSchema,
limitFirstEmailSentEventSchema,
limitSecondEmailSentEventSchema,
...clientSideEvents,
]);

Expand Down

0 comments on commit a7a15fd

Please sign in to comment.