2
0

👷 Add email alert hourly sender

Closes #549
This commit is contained in:
Baptiste Arnaud
2023-06-06 13:25:13 +02:00
parent 40ef934740
commit a4cb6face8
16 changed files with 797 additions and 125 deletions

View File

@ -0,0 +1,26 @@
name: Send chats limit alert emails
on:
schedule:
- cron: '0 * * * *'
jobs:
send:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/scripts
env:
DATABASE_URL: '${{ secrets.DATABASE_URL }}'
TELEMETRY_WEBHOOK_URL: '${{ secrets.TELEMETRY_WEBHOOK_URL }}'
TELEMETRY_WEBHOOK_BEARER_TOKEN: '${{ secrets.TELEMETRY_WEBHOOK_BEARER_TOKEN }}'
SMTP_USERNAME: '${{ secrets.SMTP_USERNAME }}'
SMTP_PASSWORD: '${{ secrets.SMTP_PASSWORD }}'
SMTP_HOST: '${{ secrets.SMTP_HOST }}'
SMTP_PORT: '${{ secrets.SMTP_PORT }}'
NEXT_PUBLIC_SMTP_FROM: '${{ secrets.NEXT_PUBLIC_SMTP_FROM }}'
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2.2.2
- run: pnpm i --frozen-lockfile
- run: pnpm turbo run sendAlertEmails

View File

@ -343,6 +343,46 @@
"data"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"userId": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"name": {
"type": "string",
"enum": [
"Workspace automatically quarantined"
]
},
"data": {
"type": "object",
"properties": {
"chatsLimit": {
"type": "number"
},
"totalChatsUsed": {
"type": "number"
}
},
"required": [
"chatsLimit",
"totalChatsUsed"
],
"additionalProperties": false
}
},
"required": [
"userId",
"workspaceId",
"name",
"data"
],
"additionalProperties": false
}
]
}
@ -816,6 +856,68 @@
},
"content": {
"type": "string"
},
"displayCondition": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"condition": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Does not contain",
"Greater than",
"Less than",
"Is set",
"Is empty",
"Starts with",
"Ends with"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": [
@ -1502,6 +1604,68 @@
},
"description": {
"type": "string"
},
"displayCondition": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"condition": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Does not contain",
"Greater than",
"Less than",
"Is set",
"Is empty",
"Starts with",
"Ends with"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": [

View File

@ -429,6 +429,68 @@
},
"content": {
"type": "string"
},
"displayCondition": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"condition": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Does not contain",
"Greater than",
"Less than",
"Is set",
"Is empty",
"Starts with",
"Ends with"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": [
@ -1115,6 +1177,68 @@
},
"description": {
"type": "string"
},
"displayCondition": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"condition": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Does not contain",
"Greater than",
"Less than",
"Is set",
"Is empty",
"Starts with",
"Ends with"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": [
@ -3528,6 +3652,68 @@
},
"content": {
"type": "string"
},
"displayCondition": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"condition": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Does not contain",
"Greater than",
"Less than",
"Is set",
"Is empty",
"Starts with",
"Ends with"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": [
@ -4214,6 +4400,68 @@
},
"description": {
"type": "string"
},
"displayCondition": {
"type": "object",
"properties": {
"isEnabled": {
"type": "boolean"
},
"condition": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Does not contain",
"Greater than",
"Less than",
"Is set",
"Is empty",
"Starts with",
"Ends with"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": [

View File

@ -24,11 +24,13 @@ export const Faq = () => (
What happens once I reach the monthly chats limit?
</Heading>
<Text>
When you exceed the number of chats included in your plan, you will
receive a heads up by email. There won&apos;t be any immediate
additional charges and your bots will continue to run. If you continue
to exceed the limit, you will be kindly asked you to upgrade your
subscription.
You will receive a heads up email when you reach 80% of your monthly
limit. Once you have reached the limit, you will receive another email
alert. Your bots will continue to run. You will be kindly asked to
upgrade your subscription. If you don&apos;t provide an answer after
~48h, your bots will be closed for the remaining of the month. For a
FREE workspace, If you exceed 600 chats, your bots will be
automatically closed.
</Text>
</Stack>
<Stack borderWidth={1} p="8" rounded="lg" spacing={4}>

View File

@ -1,3 +1,4 @@
import React from 'react'
import { IMjmlImageProps, MjmlImage } from '@faire/mjml-react'
import { borderBase } from '../theme'

View File

@ -1,3 +1,4 @@
import React from 'react'
import { MjmlText, IMjmlTextProps } from '@faire/mjml-react'
import { leadingRelaxed, textBase } from '../theme'

View File

@ -0,0 +1,64 @@
import React, { ComponentProps } from 'react'
import {
Mjml,
MjmlBody,
MjmlSection,
MjmlColumn,
MjmlSpacer,
} from '@faire/mjml-react'
import { render } from '@faire/mjml-react/utils/render'
import { Button, Head, HeroImage, Text } from '../components'
import { parseNumberWithCommas } from '@typebot.io/lib'
import { SendMailOptions } from 'nodemailer'
import { sendEmail } from '../sendEmail'
type ReachedChatsLimitEmailProps = {
chatsLimit: number
url: string
}
export const ReachedChatsLimitEmail = ({
chatsLimit,
url,
}: ReachedChatsLimitEmailProps) => {
const readableChatsLimit = parseNumberWithCommas(chatsLimit)
return (
<Mjml>
<Head />
<MjmlBody width={600}>
<MjmlSection padding="0">
<MjmlColumn>
<HeroImage src="https://typebot.s3.fr-par.scw.cloud/public/assets/actionRequiredEmailBanner.png" />
</MjmlColumn>
</MjmlSection>
<MjmlSection padding="0 24px" cssClass="smooth">
<MjmlColumn>
<Text>
It just happened, you&apos;ve reached your monthly{' '}
{readableChatsLimit} chats limit 😮
</Text>
<Text>
If you&apos;d like your bots to continue chatting with your users
this month, then you need to upgrade your plan. 🚀
</Text>
<MjmlSpacer height="24px" />
<Button link={url}>Upgrade workspace</Button>
</MjmlColumn>
</MjmlSection>
</MjmlBody>
</Mjml>
)
}
export const sendReachedChatsLimitEmail = ({
to,
...props
}: Pick<SendMailOptions, 'to'> &
ComponentProps<typeof ReachedChatsLimitEmail>) =>
sendEmail({
to,
subject: "You've reached your chats limit",
html: render(<ReachedChatsLimitEmail {...props} />).html,
})

View File

@ -3,6 +3,7 @@ import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry'
import { isEmpty, isNotEmpty } from '../utils'
export const sendTelemetryEvents = async (events: TelemetryEvent[]) => {
if (events.length === 0) return { message: 'No events to send' }
if (isEmpty(process.env.TELEMETRY_WEBHOOK_URL))
return { message: 'Telemetry not enabled' }

View File

@ -90,6 +90,16 @@ const workspaceLimitReachedEventSchema = workspaceEvent.merge(
})
)
const workspaceAutoQuarantinedEventSchema = workspaceEvent.merge(
z.object({
name: z.literal('Workspace automatically quarantined'),
data: z.object({
chatsLimit: z.number(),
totalChatsUsed: z.number(),
}),
})
)
export const eventSchema = z.discriminatedUnion('name', [
workspaceCreatedEventSchema,
userCreatedEventSchema,
@ -98,6 +108,7 @@ export const eventSchema = z.discriminatedUnion('name', [
subscriptionUpdatedEventSchema,
newResultsCollectedEventSchema,
workspaceLimitReachedEventSchema,
workspaceAutoQuarantinedEventSchema,
])
export type TelemetryEvent = z.infer<typeof eventSchema>

View File

@ -1,8 +1,2 @@
DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot
ENCRYPTION_SECRET=
# For setCustomPlan
STRIPE_SECRET_KEY=
STRIPE_SUBSCRIPTION_ID=
STRIPE_PRODUCT_ID=
WORKSPACE_ID=

View File

@ -12,21 +12,22 @@
"db:setCustomPlan": "tsx setCustomPlan.ts",
"db:bulkUpdate": "tsx bulkUpdate.ts",
"db:fixTypebots": "tsx fixTypebots.ts",
"telemetry:sendTotalResultsDigest": "tsx sendTotalResultsDigest.ts"
"telemetry:sendTotalResultsDigest": "tsx sendTotalResultsDigest.ts",
"sendAlertEmails": "tsx sendAlertEmails.ts"
},
"devDependencies": {
"@typebot.io/emails": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@types/node": "20.2.3",
"@types/prompts": "2.4.4",
"@typebot.io/prisma": "workspace:*",
"deep-object-diff": "1.1.9",
"@typebot.io/emails": "workspace:*",
"got": "12.6.0",
"@typebot.io/schemas": "workspace:*",
"prompts": "2.4.2",
"stripe": "12.6.0",
"tsx": "3.12.7",
"typescript": "5.0.4",
"@typebot.io/lib": "workspace:*",
"zod": "3.21.4"
}
}

View File

@ -0,0 +1,247 @@
import {
MemberInWorkspace,
Plan,
PrismaClient,
WorkspaceRole,
} from '@typebot.io/prisma'
import { isDefined } from '@typebot.io/lib'
import { getChatsLimit } from '@typebot.io/lib/pricing'
import { promptAndSetEnvironment } from './utils'
import { Workspace } from '@typebot.io/schemas'
import { sendAlmostReachedChatsLimitEmail } from '@typebot.io/emails/src/emails/AlmostReachedChatsLimitEmail'
import { sendReachedChatsLimitEmail } from '@typebot.io/emails/src/emails/ReachedChatsLimitEmail'
import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry'
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
const prisma = new PrismaClient()
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
type WorkspaceForDigest = Pick<
Workspace,
| 'id'
| 'plan'
| 'customChatsLimit'
| 'additionalChatsIndex'
| 'isQuarantined'
| 'chatsLimitFirstEmailSentAt'
| 'chatsLimitSecondEmailSentAt'
> & {
members: (Pick<MemberInWorkspace, 'role'> & {
user: { id: string; email: string | null }
})[]
}
export const sendTotalResultsDigest = async () => {
await promptAndSetEnvironment('production')
console.log('Get collected results from the last hour...')
const hourAgo = new Date(Date.now() - 1000 * 60 * 60)
const results = await prisma.result.groupBy({
by: ['typebotId'],
_count: {
_all: true,
},
where: {
hasStarted: true,
createdAt: {
gte: hourAgo,
},
},
})
console.log(
`Found ${results.reduce(
(total, result) => total + result._count._all,
0
)} results collected for the last hour.`
)
const workspaces = await prisma.workspace.findMany({
where: {
typebots: {
some: {
id: { in: results.map((result) => result.typebotId) },
},
},
},
select: {
id: true,
typebots: { select: { id: true } },
members: {
select: { user: { select: { id: true, email: true } }, role: true },
},
additionalChatsIndex: true,
additionalStorageIndex: true,
customChatsLimit: true,
customStorageLimit: true,
plan: true,
isQuarantined: true,
chatsLimitFirstEmailSentAt: true,
chatsLimitSecondEmailSentAt: true,
},
})
const resultsWithWorkspaces = results
.flatMap((result) => {
const workspace = workspaces.find((workspace) =>
workspace.typebots.some((typebot) => typebot.id === result.typebotId)
)
if (!workspace) return
return workspace.members
.filter((member) => member.role !== WorkspaceRole.GUEST)
.map((member, memberIndex) => ({
userId: member.user.id,
workspace: workspace,
typebotId: result.typebotId,
totalResultsYesterday: result._count._all,
isFirstOfKind: memberIndex === 0 ? (true as const) : undefined,
}))
})
.filter(isDefined)
console.log('Check limits...')
const events = await sendAlertIfLimitReached(
resultsWithWorkspaces
.filter((result) => result.isFirstOfKind)
.map((result) => result.workspace)
)
console.log(`Send ${events.length} auto quarantine events...`)
await sendTelemetryEvents(events)
}
const sendAlertIfLimitReached = async (
workspaces: WorkspaceForDigest[]
): Promise<TelemetryEvent[]> => {
const events: TelemetryEvent[] = []
const taggedWorkspaces: string[] = []
for (const workspace of workspaces) {
if (taggedWorkspaces.includes(workspace.id) || workspace.isQuarantined)
continue
taggedWorkspaces.push(workspace.id)
const { totalChatsUsed } = await getUsage(workspace.id)
const chatsLimit = getChatsLimit(workspace)
if (
chatsLimit > 0 &&
totalChatsUsed >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
totalChatsUsed < chatsLimit &&
!workspace.chatsLimitFirstEmailSentAt
) {
const to = workspace.members
.map((member) => member.user.email)
.filter(isDefined)
console.log(
`Send almost reached chats limit email to ${to.join(', ')}...`
)
await sendAlmostReachedChatsLimitEmail({
to: workspace.members
.map((member) => member.user.email)
.filter(isDefined),
usagePercent: Math.round((totalChatsUsed / chatsLimit) * 100),
chatsLimit,
url: `https://app.typebot.io/typebots?workspaceId=${workspace.id}`,
})
await prisma.workspace.update({
where: { id: workspace.id },
data: { chatsLimitFirstEmailSentAt: new Date() },
})
}
if (
chatsLimit > 0 &&
totalChatsUsed >= chatsLimit &&
!workspace.chatsLimitSecondEmailSentAt
) {
const to = workspace.members
.map((member) => member.user.email)
.filter(isDefined)
console.log(`Send reached chats limit email to ${to.join(', ')}...`)
await sendReachedChatsLimitEmail({
to,
chatsLimit,
url: `https://app.typebot.io/typebots?workspaceId=${workspace.id}`,
})
await prisma.workspace.update({
where: { id: workspace.id },
data: { chatsLimitSecondEmailSentAt: new Date() },
})
}
if (totalChatsUsed > chatsLimit * 3 && workspace.plan === Plan.FREE) {
console.log(`Automatically quarantine workspace ${workspace.id}...`)
await prisma.workspace.update({
where: { id: workspace.id },
data: { isQuarantined: true },
})
events.push(
...workspace.members
.filter((member) => member.role === WorkspaceRole.ADMIN)
.map(
(member) =>
({
name: 'Workspace automatically quarantined',
userId: member.user.id,
workspaceId: workspace.id,
data: {
totalChatsUsed,
chatsLimit,
},
} satisfies TelemetryEvent)
)
)
}
}
return events
}
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,
{
_sum: { storageUsed: totalStorageUsed },
},
] = await Promise.all([
prisma.result.count({
where: {
typebotId: { in: typebots.map((typebot) => typebot.id) },
hasStarted: true,
createdAt: {
gte: firstDayOfMonth,
lt: firstDayOfNextMonth,
},
},
}),
prisma.answer.aggregate({
where: {
storageUsed: { gt: 0 },
result: {
typebotId: { in: typebots.map((typebot) => typebot.id) },
},
},
_sum: { storageUsed: true },
}),
])
return {
totalChatsUsed,
totalStorageUsed: totalStorageUsed ?? 0,
}
}
sendTotalResultsDigest().then()

View File

@ -11,7 +11,6 @@ import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEven
import { Workspace } from '@typebot.io/schemas'
const prisma = new PrismaClient()
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
type WorkspaceForDigest = Pick<
Workspace,
@ -170,18 +169,6 @@ const sendAlertIfLimitReached = async (
)
continue
}
// if (
// chatsLimit > 0 &&
// totalChatsUsed >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT
// )
// await sendAlmostReachedChatsLimitEmail({
// to: workspace.members
// .map((member) => member.user.email)
// .filter(isDefined),
// usagePercent: Math.round((totalChatsUsed / chatsLimit) * 100),
// chatsLimit,
// url: `https://app.typebot.io/typebots?workspaceId=${workspace.id}`,
// })
}
return events
}

View File

@ -1,86 +0,0 @@
import { Plan, PrismaClient } from '@typebot.io/prisma'
import Stripe from 'stripe'
import { promptAndSetEnvironment } from './utils'
const setCustomPlan = async () => {
await promptAndSetEnvironment()
const prisma = new PrismaClient()
if (
!process.env.STRIPE_SECRET_KEY ||
!process.env.STRIPE_PRODUCT_ID ||
!process.env.STRIPE_SUBSCRIPTION_ID ||
!process.env.WORKSPACE_ID
)
throw Error(
'STRIPE_SECRET_KEY or STRIPE_SUBSCRIPTION_ID or STRIPE_PRODUCT_ID or process.env.WORKSPACE_ID var is missing'
)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-11-15',
})
const claimablePlan = await prisma.claimableCustomPlan.findFirst({
where: { workspaceId: process.env.WORKSPACE_ID, claimedAt: null },
})
if (!claimablePlan) throw Error('No claimable plan found')
console.log('Claimable plan found')
const { items: existingItems } = await stripe.subscriptions.retrieve(
process.env.STRIPE_SUBSCRIPTION_ID
)
if (existingItems.data.length === 0) return
const planItem = existingItems.data.find(
(item) => item.plan.product === process.env.STRIPE_PRODUCT_ID
)
if (!planItem) throw Error("Couldn't find plan item")
console.log('Updating subscription...')
await stripe.subscriptions.update(process.env.STRIPE_SUBSCRIPTION_ID, {
items: [
{
id: planItem.id,
price_data: {
currency: 'usd',
tax_behavior: 'exclusive',
recurring: { interval: 'month' },
product: process.env.STRIPE_PRODUCT_ID,
unit_amount: claimablePlan.price * 100,
},
},
...existingItems.data
.filter((item) => item.plan.product !== process.env.STRIPE_PRODUCT_ID)
.map((item) => ({ id: item.id, deleted: true })),
],
})
console.log('Subscription updated!')
console.log('Updating workspace...')
await prisma.workspace.update({
where: { id: process.env.WORKSPACE_ID },
data: {
plan: Plan.CUSTOM,
customChatsLimit: claimablePlan.chatsLimit,
customSeatsLimit: claimablePlan.seatsLimit,
customStorageLimit: claimablePlan.storageLimit,
},
})
console.log('Workspace updated!')
console.log('Updating claimable plan...')
await prisma.claimableCustomPlan.update({
where: { id: claimablePlan.id },
data: { claimedAt: new Date() },
})
console.log('Claimable plan updated!')
}
setCustomPlan()

27
pnpm-lock.yaml generated
View File

@ -4360,7 +4360,7 @@ packages:
'@docusaurus/react-loadable': 5.5.2(react@17.0.2)
'@docusaurus/types': 2.4.1(react-dom@17.0.2)(react@17.0.2)
'@types/history': 4.7.11
'@types/react': 18.2.7
'@types/react': 18.2.8
'@types/react-router-config': 5.0.7
'@types/react-router-dom': 5.3.3
react: 17.0.2
@ -4667,7 +4667,7 @@ packages:
'@docusaurus/utils': 2.4.1(@docusaurus/types@2.3.1)
'@docusaurus/utils-common': 2.4.1(@docusaurus/types@2.3.1)
'@types/history': 4.7.11
'@types/react': 18.2.7
'@types/react': 18.2.8
'@types/react-router-config': 5.0.7
clsx: 1.2.1
parse-numeric-range: 1.3.0
@ -4769,7 +4769,7 @@ packages:
react-dom: ^16.8.4 || ^17.0.0
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.7
'@types/react': 18.2.8
commander: 5.1.0
joi: 17.9.2
react: 17.0.2
@ -4791,7 +4791,7 @@ packages:
react-dom: ^16.8.4 || ^17.0.0
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.7
'@types/react': 18.2.8
commander: 5.1.0
joi: 17.9.2
react: 17.0.2
@ -7970,7 +7970,7 @@ packages:
/@types/react-phone-number-input@3.0.14:
resolution: {integrity: sha512-xOje1m+Z9n3kxj5/bCJzpDeokA95aYYuFbmcK8myyod+KLy3h5tKwCQwSEcU+603EeyfgTMkjz7GYY9S0aZLCQ==}
dependencies:
'@types/react': 18.2.7
'@types/react': 18.2.8
dev: true
/@types/react-redux@7.1.25:
@ -7986,7 +7986,7 @@ packages:
resolution: {integrity: sha512-pFFVXUIydHlcJP6wJm7sDii5mD/bCmmAY0wQzq+M+uX7bqS95AQqHZWP1iNMKrWVQSuHIzj5qi9BvrtLX2/T4w==}
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.7
'@types/react': 18.2.8
'@types/react-router': 5.1.20
dev: false
@ -7994,7 +7994,7 @@ packages:
resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==}
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.7
'@types/react': 18.2.8
'@types/react-router': 5.1.20
dev: false
@ -8002,19 +8002,19 @@ packages:
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.7
'@types/react': 18.2.8
dev: false
/@types/react-scroll@1.8.6:
resolution: {integrity: sha512-aMTiNgcmA7dwn1yjoHsiL78RfRnKCXzFyMbv63VrZTXloSfNePBdKtVObC3/My6irwDf0Oz0U4VjEC/vrv6/9w==}
dependencies:
'@types/react': 18.2.7
'@types/react': 18.2.8
dev: true
/@types/react-transition-group@4.4.5:
resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==}
dependencies:
'@types/react': 18.2.7
'@types/react': 18.2.8
dev: true
/@types/react@18.0.27:
@ -8039,6 +8039,13 @@ packages:
'@types/scheduler': 0.16.3
csstype: 3.1.2
/@types/react@18.2.8:
resolution: {integrity: sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==}
dependencies:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.3
csstype: 3.1.2
/@types/resolve@1.20.2:
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
dev: true

View File

@ -47,6 +47,10 @@
"telemetry:sendTotalResultsDigest": {
"dependsOn": ["@typebot.io/prisma#db:generate"],
"cache": false
},
"sendAlertEmails": {
"dependsOn": ["@typebot.io/prisma#db:generate"],
"cache": false
}
}
}