Skip to content

Commit

Permalink
♻️ Unify totalResults digest and email alert scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Oct 13, 2023
1 parent 7db077b commit ec7aed3
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 275 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Send chats limit alert emails
name: Check and report chats usage

on:
schedule:
Expand All @@ -22,8 +22,9 @@ jobs:
SMTP_HOST: '${{ secrets.SMTP_HOST }}'
SMTP_PORT: '${{ secrets.SMTP_PORT }}'
NEXT_PUBLIC_SMTP_FROM: '${{ secrets.NEXT_PUBLIC_SMTP_FROM }}'
STRIPE_SECRET_KEY: '${{ secrets.STRIPE_SECRET_KEY }}'
steps:
- uses: actions/checkout@v2
- uses: pnpm/[email protected]
- run: pnpm i --frozen-lockfile
- run: pnpm turbo run sendAlertEmails
- run: pnpm turbo run checkAndReportChatsUsage
24 changes: 0 additions & 24 deletions .github/workflows/send-total-results-digest.yml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,27 @@ import {
} from '@typebot.io/prisma'
import { isDefined, isEmpty } from '@typebot.io/lib'
import { getChatsLimit } from '@typebot.io/lib/billing/getChatsLimit'
import { getUsage } from '@typebot.io/lib/api/getUsage'
import { promptAndSetEnvironment } from './utils'
import { Workspace } from '@typebot.io/schemas'
import { sendAlmostReachedChatsLimitEmail } from '@typebot.io/emails/src/emails/AlmostReachedChatsLimitEmail'
import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry'
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
import { Workspace } from '@typebot.io/schemas'
import { Stripe } from 'stripe'
import Stripe from 'stripe'
import { createId } from '@paralleldrive/cuid2'

const prisma = new PrismaClient()
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.75

type WorkspaceForDigest = Pick<
Workspace,
| 'id'
| 'plan'
| 'name'
| 'customChatsLimit'
| 'customStorageLimit'
| 'additionalStorageIndex'
| 'isQuarantined'
| 'chatsLimitFirstEmailSentAt'
| 'chatsLimitSecondEmailSentAt'
> & {
members: (Pick<MemberInWorkspace, 'role'> & {
user: { id: string; email: string | null }
Expand Down Expand Up @@ -55,14 +59,14 @@ type ResultWithWorkspace = {
isFirstOfKind: true | undefined
}

export const sendTotalResultsDigest = async () => {
export const checkAndReportChatsUsage = async () => {
await promptAndSetEnvironment('production')

console.log("Generating total results yesterday's digest...")
const todayMidnight = new Date()
todayMidnight.setUTCHours(0, 0, 0, 0)
const yesterday = new Date(todayMidnight)
yesterday.setDate(yesterday.getDate() - 1)
console.log('Get collected results from the last hour...')

const zeroedMinutesHour = new Date()
zeroedMinutesHour.setUTCMinutes(0, 0, 0)
const hourAgo = new Date(zeroedMinutesHour.getTime() - 1000 * 60 * 60)

const results = await prisma.result.groupBy({
by: ['typebotId'],
Expand All @@ -72,8 +76,8 @@ export const sendTotalResultsDigest = async () => {
where: {
hasStarted: true,
createdAt: {
gte: yesterday,
lt: todayMidnight,
lt: zeroedMinutesHour,
gte: hourAgo,
},
},
})
Expand All @@ -82,7 +86,7 @@ export const sendTotalResultsDigest = async () => {
`Found ${results.reduce(
(total, result) => total + result._count._all,
0
)} results collected yesterday.`
)} results collected for the last hour.`
)

const workspaces = await prisma.workspace.findMany({
Expand All @@ -95,6 +99,7 @@ export const sendTotalResultsDigest = async () => {
},
select: {
id: true,
name: true,
typebots: { select: { id: true } },
members: {
select: { user: { select: { id: true, email: true } }, role: true },
Expand All @@ -104,6 +109,8 @@ export const sendTotalResultsDigest = async () => {
customStorageLimit: true,
plan: true,
isQuarantined: true,
chatsLimitFirstEmailSentAt: true,
chatsLimitSecondEmailSentAt: true,
stripeId: true,
},
})
Expand All @@ -124,20 +131,18 @@ export const sendTotalResultsDigest = async () => {
isFirstOfKind: memberIndex === 0 ? (true as const) : undefined,
}))
})
.filter(isDefined) satisfies ResultWithWorkspace[]

console.log('Reporting usage to Stripe...')
.filter(isDefined)

await reportUsageToStripe(resultsWithWorkspaces)

console.log('Computing workspaces limits...')
console.log('Check limits...')

const workspaceLimitReachedEvents = await sendAlertIfLimitReached(
const events = await sendAlertIfLimitReached(
resultsWithWorkspaces
.filter((result) => result.isFirstOfKind)
.map((result) => result.workspace)
)

await reportUsageToStripe(resultsWithWorkspaces)

const newResultsCollectedEvents = resultsWithWorkspaces.map(
(result) =>
({
Expand All @@ -152,16 +157,11 @@ export const sendTotalResultsDigest = async () => {
} satisfies TelemetryEvent)
)

await sendTelemetryEvents(
workspaceLimitReachedEvents.concat(newResultsCollectedEvents)
)

console.log(
`Sent ${workspaceLimitReachedEvents.length} workspace limit reached events.`
)
console.log(
`Sent ${newResultsCollectedEvents.length} new results collected events.`
`Send ${newResultsCollectedEvents.length} new results events and ${events.length} auto quarantine events...`
)

await sendTelemetryEvents(events.concat(newResultsCollectedEvents))
}

const sendAlertIfLimitReached = async (
Expand All @@ -173,16 +173,50 @@ const sendAlertIfLimitReached = async (
if (taggedWorkspaces.includes(workspace.id) || workspace.isQuarantined)
continue
taggedWorkspaces.push(workspace.id)
const { totalChatsUsed } = await getUsage(workspace.id)
const { totalChatsUsed } = await getUsage(prisma)(workspace.id)
const chatsLimit = getChatsLimit(workspace)
if (chatsLimit > 0 && totalChatsUsed >= chatsLimit) {
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)
}
}

if (totalChatsUsed > chatsLimit * 1.5 && workspace.plan === Plan.FREE) {
console.log(`Automatically quarantine workspace ${workspace.id}...`)
await prisma.workspace.updateMany({
where: { id: workspace.id },
data: { isQuarantined: true },
})
events.push(
...workspace.members
.filter((member) => member.role === WorkspaceRole.ADMIN)
.map(
(member) =>
({
name: 'Workspace limit reached',
name: 'Workspace automatically quarantined',
userId: member.user.id,
workspaceId: workspace.id,
data: {
Expand All @@ -192,7 +226,6 @@ const sendAlertIfLimitReached = async (
} satisfies TelemetryEvent)
)
)
continue
}
}
return events
Expand Down Expand Up @@ -261,35 +294,4 @@ const reportUsageToStripe = async (
}
}

const getUsage = async (workspaceId: string) => {
const now = new Date()
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
const typebots = await prisma.typebot.findMany({
where: {
workspace: {
id: workspaceId,
},
},
select: { id: true },
})

const [totalChatsUsed] = await Promise.all([
prisma.result.count({
where: {
typebotId: { in: typebots.map((typebot) => typebot.id) },
hasStarted: true,
createdAt: {
gte: firstDayOfMonth,
lt: firstDayOfNextMonth,
},
},
}),
])

return {
totalChatsUsed,
}
}

sendTotalResultsDigest().then()
checkAndReportChatsUsage().then()
2 changes: 1 addition & 1 deletion packages/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"db:bulkUpdate": "tsx bulkUpdate.ts",
"db:fixTypebots": "tsx fixTypebots.ts",
"telemetry:sendTotalResultsDigest": "tsx sendTotalResultsDigest.ts",
"sendAlertEmails": "tsx sendAlertEmails.ts",
"checkAndReportChatsUsage": "tsx checkAndReportChatsUsage.ts",
"inspectUser": "tsx inspectUser.ts",
"checkSubscriptionsStatus": "tsx checkSubscriptionsStatus.ts",
"createChatsPrices": "tsx createChatsPrices.ts"
Expand Down
Loading

0 comments on commit ec7aed3

Please sign in to comment.