Skip to content

Commit 8a23fe9

Browse files
committed
♻️ Unify totalResults digest and email alert scripts
1 parent 7db077b commit 8a23fe9

File tree

8 files changed

+158
-307
lines changed

8 files changed

+158
-307
lines changed

.github/workflows/send-limit-email-alerts.yml renamed to .github/workflows/check-and-report-chats-usage.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Send chats limit alert emails
1+
name: Check and report chats usage
22

33
on:
44
schedule:
@@ -22,8 +22,9 @@ jobs:
2222
SMTP_HOST: '${{ secrets.SMTP_HOST }}'
2323
SMTP_PORT: '${{ secrets.SMTP_PORT }}'
2424
NEXT_PUBLIC_SMTP_FROM: '${{ secrets.NEXT_PUBLIC_SMTP_FROM }}'
25+
STRIPE_SECRET_KEY: '${{ secrets.STRIPE_SECRET_KEY }}'
2526
steps:
2627
- uses: actions/checkout@v2
2728
- uses: pnpm/[email protected]
2829
- run: pnpm i --frozen-lockfile
29-
- run: pnpm turbo run sendAlertEmails
30+
- run: pnpm turbo run checkAndReportChatsUsage

.github/workflows/send-total-results-digest.yml

-24
This file was deleted.

apps/builder/src/features/billing/components/ProPlanPricingCard.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
1717
import { useI18n, useScopedI18n } from '@/locales'
1818
import { formatPrice } from '@typebot.io/lib/billing/formatPrice'
1919
import { ChatsProTiersModal } from './ChatsProTiersModal'
20+
import { prices } from '@typebot.io/lib/billing/constants'
2021

2122
type Props = {
2223
currentPlan: Plan
@@ -85,7 +86,7 @@ export const ProPlanPricingCard = ({
8586
<Stack spacing="8">
8687
<Stack spacing="4">
8788
<Heading>
88-
{formatPrice(89, { currency })}
89+
{formatPrice(prices.PRO, { currency })}
8990
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
9091
</Heading>
9192
<Text fontWeight="bold">

apps/builder/src/features/billing/components/StarterPlanPricingCard.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FeaturesList } from './FeaturesList'
1212
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
1313
import { useI18n, useScopedI18n } from '@/locales'
1414
import { formatPrice } from '@typebot.io/lib/billing/formatPrice'
15+
import { prices } from '@typebot.io/lib/billing/constants'
1516

1617
type Props = {
1718
currentPlan: Plan
@@ -57,7 +58,7 @@ export const StarterPlanPricingCard = ({
5758
<Text>{scopedT('starter.description')}</Text>
5859
</Stack>
5960
<Heading>
60-
{formatPrice(39, { currency })}
61+
{formatPrice(prices.STARTER, { currency })}
6162
<chakra.span fontSize="md">{scopedT('perMonth')}</chakra.span>
6263
</Heading>
6364
</Stack>

packages/scripts/sendTotalResultsDigest.ts renamed to packages/scripts/checkAndReportChatsUsage.ts

+66-64
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,27 @@ import {
66
} from '@typebot.io/prisma'
77
import { isDefined, isEmpty } from '@typebot.io/lib'
88
import { getChatsLimit } from '@typebot.io/lib/billing/getChatsLimit'
9+
import { getUsage } from '@typebot.io/lib/api/getUsage'
910
import { promptAndSetEnvironment } from './utils'
11+
import { Workspace } from '@typebot.io/schemas'
12+
import { sendAlmostReachedChatsLimitEmail } from '@typebot.io/emails/src/emails/AlmostReachedChatsLimitEmail'
1013
import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry'
1114
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
12-
import { Workspace } from '@typebot.io/schemas'
13-
import { Stripe } from 'stripe'
15+
import Stripe from 'stripe'
1416
import { createId } from '@paralleldrive/cuid2'
1517

1618
const prisma = new PrismaClient()
19+
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.75
1720

1821
type WorkspaceForDigest = Pick<
1922
Workspace,
2023
| 'id'
2124
| 'plan'
25+
| 'name'
2226
| 'customChatsLimit'
23-
| 'customStorageLimit'
24-
| 'additionalStorageIndex'
2527
| 'isQuarantined'
28+
| 'chatsLimitFirstEmailSentAt'
29+
| 'chatsLimitSecondEmailSentAt'
2630
> & {
2731
members: (Pick<MemberInWorkspace, 'role'> & {
2832
user: { id: string; email: string | null }
@@ -55,14 +59,14 @@ type ResultWithWorkspace = {
5559
isFirstOfKind: true | undefined
5660
}
5761

58-
export const sendTotalResultsDigest = async () => {
62+
export const checkAndReportChatsUsage = async () => {
5963
await promptAndSetEnvironment('production')
6064

61-
console.log("Generating total results yesterday's digest...")
62-
const todayMidnight = new Date()
63-
todayMidnight.setUTCHours(0, 0, 0, 0)
64-
const yesterday = new Date(todayMidnight)
65-
yesterday.setDate(yesterday.getDate() - 1)
65+
console.log('Get collected results from the last hour...')
66+
67+
const zeroedMinutesHour = new Date()
68+
zeroedMinutesHour.setUTCMinutes(0, 0, 0)
69+
const hourAgo = new Date(zeroedMinutesHour.getTime() - 1000 * 60 * 60)
6670

6771
const results = await prisma.result.groupBy({
6872
by: ['typebotId'],
@@ -72,8 +76,8 @@ export const sendTotalResultsDigest = async () => {
7276
where: {
7377
hasStarted: true,
7478
createdAt: {
75-
gte: yesterday,
76-
lt: todayMidnight,
79+
lt: zeroedMinutesHour,
80+
gte: hourAgo,
7781
},
7882
},
7983
})
@@ -82,7 +86,7 @@ export const sendTotalResultsDigest = async () => {
8286
`Found ${results.reduce(
8387
(total, result) => total + result._count._all,
8488
0
85-
)} results collected yesterday.`
89+
)} results collected for the last hour.`
8690
)
8791

8892
const workspaces = await prisma.workspace.findMany({
@@ -95,6 +99,7 @@ export const sendTotalResultsDigest = async () => {
9599
},
96100
select: {
97101
id: true,
102+
name: true,
98103
typebots: { select: { id: true } },
99104
members: {
100105
select: { user: { select: { id: true, email: true } }, role: true },
@@ -104,6 +109,8 @@ export const sendTotalResultsDigest = async () => {
104109
customStorageLimit: true,
105110
plan: true,
106111
isQuarantined: true,
112+
chatsLimitFirstEmailSentAt: true,
113+
chatsLimitSecondEmailSentAt: true,
107114
stripeId: true,
108115
},
109116
})
@@ -124,20 +131,18 @@ export const sendTotalResultsDigest = async () => {
124131
isFirstOfKind: memberIndex === 0 ? (true as const) : undefined,
125132
}))
126133
})
127-
.filter(isDefined) satisfies ResultWithWorkspace[]
128-
129-
console.log('Reporting usage to Stripe...')
134+
.filter(isDefined)
130135

131-
await reportUsageToStripe(resultsWithWorkspaces)
132-
133-
console.log('Computing workspaces limits...')
136+
console.log('Check limits...')
134137

135-
const workspaceLimitReachedEvents = await sendAlertIfLimitReached(
138+
const events = await sendAlertIfLimitReached(
136139
resultsWithWorkspaces
137140
.filter((result) => result.isFirstOfKind)
138141
.map((result) => result.workspace)
139142
)
140143

144+
await reportUsageToStripe(resultsWithWorkspaces)
145+
141146
const newResultsCollectedEvents = resultsWithWorkspaces.map(
142147
(result) =>
143148
({
@@ -152,16 +157,11 @@ export const sendTotalResultsDigest = async () => {
152157
} satisfies TelemetryEvent)
153158
)
154159

155-
await sendTelemetryEvents(
156-
workspaceLimitReachedEvents.concat(newResultsCollectedEvents)
157-
)
158-
159-
console.log(
160-
`Sent ${workspaceLimitReachedEvents.length} workspace limit reached events.`
161-
)
162160
console.log(
163-
`Sent ${newResultsCollectedEvents.length} new results collected events.`
161+
`Send ${newResultsCollectedEvents.length} new results events and ${events.length} auto quarantine events...`
164162
)
163+
164+
await sendTelemetryEvents(events.concat(newResultsCollectedEvents))
165165
}
166166

167167
const sendAlertIfLimitReached = async (
@@ -173,16 +173,50 @@ const sendAlertIfLimitReached = async (
173173
if (taggedWorkspaces.includes(workspace.id) || workspace.isQuarantined)
174174
continue
175175
taggedWorkspaces.push(workspace.id)
176-
const { totalChatsUsed } = await getUsage(workspace.id)
176+
const { totalChatsUsed } = await getUsage(prisma)(workspace.id)
177177
const chatsLimit = getChatsLimit(workspace)
178-
if (chatsLimit > 0 && totalChatsUsed >= chatsLimit) {
178+
if (
179+
chatsLimit > 0 &&
180+
totalChatsUsed >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
181+
totalChatsUsed < chatsLimit &&
182+
!workspace.chatsLimitFirstEmailSentAt
183+
) {
184+
const to = workspace.members
185+
.filter((member) => member.role === WorkspaceRole.ADMIN)
186+
.map((member) => member.user.email)
187+
.filter(isDefined)
188+
console.log(
189+
`Send almost reached chats limit email to ${to.join(', ')}...`
190+
)
191+
try {
192+
await sendAlmostReachedChatsLimitEmail({
193+
to,
194+
usagePercent: Math.round((totalChatsUsed / chatsLimit) * 100),
195+
chatsLimit,
196+
workspaceName: workspace.name,
197+
})
198+
await prisma.workspace.updateMany({
199+
where: { id: workspace.id },
200+
data: { chatsLimitFirstEmailSentAt: new Date() },
201+
})
202+
} catch (err) {
203+
console.error(err)
204+
}
205+
}
206+
207+
if (totalChatsUsed > chatsLimit * 1.5 && workspace.plan === Plan.FREE) {
208+
console.log(`Automatically quarantine workspace ${workspace.id}...`)
209+
await prisma.workspace.updateMany({
210+
where: { id: workspace.id },
211+
data: { isQuarantined: true },
212+
})
179213
events.push(
180214
...workspace.members
181215
.filter((member) => member.role === WorkspaceRole.ADMIN)
182216
.map(
183217
(member) =>
184218
({
185-
name: 'Workspace limit reached',
219+
name: 'Workspace automatically quarantined',
186220
userId: member.user.id,
187221
workspaceId: workspace.id,
188222
data: {
@@ -192,7 +226,6 @@ const sendAlertIfLimitReached = async (
192226
} satisfies TelemetryEvent)
193227
)
194228
)
195-
continue
196229
}
197230
}
198231
return events
@@ -261,35 +294,4 @@ const reportUsageToStripe = async (
261294
}
262295
}
263296

264-
const getUsage = async (workspaceId: string) => {
265-
const now = new Date()
266-
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
267-
const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
268-
const typebots = await prisma.typebot.findMany({
269-
where: {
270-
workspace: {
271-
id: workspaceId,
272-
},
273-
},
274-
select: { id: true },
275-
})
276-
277-
const [totalChatsUsed] = await Promise.all([
278-
prisma.result.count({
279-
where: {
280-
typebotId: { in: typebots.map((typebot) => typebot.id) },
281-
hasStarted: true,
282-
createdAt: {
283-
gte: firstDayOfMonth,
284-
lt: firstDayOfNextMonth,
285-
},
286-
},
287-
}),
288-
])
289-
290-
return {
291-
totalChatsUsed,
292-
}
293-
}
294-
295-
sendTotalResultsDigest().then()
297+
checkAndReportChatsUsage().then()

0 commit comments

Comments
 (0)