2
0

(engine) Improve engine overall robustness

This commit is contained in:
Baptiste Arnaud
2023-01-25 11:27:47 +01:00
parent ff62b922a0
commit 30baa611e5
210 changed files with 1820 additions and 1919 deletions

View File

@ -15,6 +15,7 @@
"dependencies": {
"@sentry/nextjs": "7.31.1",
"@trpc/server": "10.9.0",
"@typebot.io/js": "workspace:*",
"@typebot.io/react": "workspace:*",
"aws-sdk": "2.1299.0",
"bot-engine": "workspace:*",

View File

@ -3,24 +3,21 @@ import { BackgroundType, Typebot } from 'models'
import { useRouter } from 'next/router'
import { SEO } from './Seo'
export type TypebotPageV2Props = {
export type TypebotPageProps = {
url: string
typebot: Pick<
Typebot,
'settings' | 'theme' | 'name' | 'isClosed' | 'isArchived' | 'publicId'
>
typebot?: Pick<Typebot, 'settings' | 'theme' | 'name' | 'publicId'>
}
export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => {
const { asPath, push } = useRouter()
export const TypebotPage = ({ url, typebot }: TypebotPageProps) => {
const { asPath, push, query } = useRouter()
const background = typebot.theme.general.background
const background = typebot?.theme.general.background
const clearQueryParamsIfNecessary = () => {
const hasQueryParams = asPath.includes('?')
if (
!hasQueryParams ||
!(typebot.settings.general.isHideQueryParamsEnabled ?? true)
!(typebot?.settings.general.isHideQueryParamsEnabled ?? true)
)
return
push(asPath.split('?')[0], undefined, { shallow: true })
@ -32,22 +29,22 @@ export const TypebotPageV2 = ({ url, typebot }: TypebotPageV2Props) => {
height: '100vh',
// Set background color to avoid SSR flash
backgroundColor:
background.type === BackgroundType.COLOR
? background.content
background?.type === BackgroundType.COLOR
? background?.content
: 'white',
}}
>
<SEO
url={url}
typebotName={typebot.name}
metadata={typebot.settings.metadata}
/>
{typebot.publicId && (
<Standard
typebot={typebot.publicId}
onInit={clearQueryParamsIfNecessary}
{typebot && (
<SEO
url={url}
typebotName={typebot.name}
metadata={typebot.settings.metadata}
/>
)}
<Standard
typebot={typebot?.publicId ?? query.publicId?.toString() ?? 'n'}
onInit={clearQueryParamsIfNecessary}
/>
</div>
)
}

View File

@ -50,7 +50,7 @@ if (window.$chatwoot) {
}`
export const executeChatwootBlock = (
{ typebot: { variables } }: SessionState,
{ typebot: { variables }, isPreview }: SessionState,
block: ChatwootBlock
): ExecuteIntegrationResponse => {
const chatwootCode = parseChatwootOpenCode(block.options)
@ -71,5 +71,14 @@ export const executeChatwootBlock = (
},
},
},
logs: isPreview
? [
{
status: 'info',
description: 'Chatwoot block is not supported in preview',
details: null,
},
]
: undefined,
}
}

View File

@ -4,6 +4,7 @@ import {
VariableWithValue,
ComparisonOperators,
LogicalOperator,
ReplyLog,
} from 'models'
import { saveErrorLog } from '@/features/logs/api'
import { getAuthenticatedGoogleDoc } from './helpers'
@ -20,7 +21,9 @@ export const getRow = async (
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
): Promise<ExecuteIntegrationResponse> => {
const { sheetId, cellsToExtract, referenceCell, filter } = options
if (!cellsToExtract || !sheetId || !referenceCell) return { outgoingEdgeId }
if (!sheetId) return { outgoingEdgeId }
let log: ReplyLog | undefined
const variables = state.typebot.variables
const resultId = state.result?.id
@ -40,11 +43,15 @@ export const getRow = async (
: matchFilter(row, filter)
)
if (filteredRows.length === 0) {
log = {
status: 'error',
description: `Couldn't find any rows matching the filter`,
}
await saveErrorLog({
resultId,
message: "Couldn't find reference cell",
message: log.description,
})
return { outgoingEdgeId }
return { outgoingEdgeId, logs: log ? [log] : undefined }
}
const randomIndex = Math.floor(Math.random() * filteredRows.length)
const extractingColumns = cellsToExtract
@ -81,13 +88,18 @@ export const getRow = async (
newSessionState,
}
} catch (err) {
log = {
status: 'error',
description: `An error occurred while fetching the spreadsheet data`,
details: err,
}
await saveErrorLog({
resultId,
message: "Couldn't fetch spreadsheet data",
message: log.description,
details: err,
})
}
return { outgoingEdgeId }
return { outgoingEdgeId, logs: log ? [log] : undefined }
}
const matchFilter = (

View File

@ -1,4 +1,4 @@
import { SessionState, GoogleSheetsInsertRowOptions } from 'models'
import { SessionState, GoogleSheetsInsertRowOptions, ReplyLog } from 'models'
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
import { getAuthenticatedGoogleDoc, parseCellValues } from './helpers'
import { ExecuteIntegrationResponse } from '@/features/chat'
@ -10,8 +10,11 @@ export const insertRow = async (
options,
}: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions }
): Promise<ExecuteIntegrationResponse> => {
console.log('insertRow', options)
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
let log: ReplyLog | undefined
const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId,
spreadsheetId: options.spreadsheetId,
@ -23,18 +26,27 @@ export const insertRow = async (
await doc.loadInfo()
const sheet = doc.sheetsById[options.sheetId]
await sheet.addRow(parsedValues)
log = {
status: 'success',
description: `Succesfully inserted row in ${doc.title} > ${sheet.title}`,
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: 'Succesfully inserted row',
message: log?.description,
}))
} catch (err) {
log = {
status: 'error',
description: `An error occured while inserting the row`,
details: err,
}
result &&
(await saveErrorLog({
resultId: result.id,
message: "Couldn't fetch spreadsheet data",
message: log.description,
details: err,
}))
}
return { outgoingEdgeId }
return { outgoingEdgeId, logs: log ? [log] : undefined }
}

View File

@ -1,4 +1,4 @@
import { SessionState, GoogleSheetsUpdateRowOptions } from 'models'
import { SessionState, GoogleSheetsUpdateRowOptions, ReplyLog } from 'models'
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
import { getAuthenticatedGoogleDoc, parseCellValues } from './helpers'
import { TRPCError } from '@trpc/server'
@ -16,6 +16,8 @@ export const updateRow = async (
if (!options.cellsToUpsert || !sheetId || !referenceCell)
return { outgoingEdgeId }
let log: ReplyLog | undefined
const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId,
spreadsheetId: options.spreadsheetId,
@ -45,18 +47,27 @@ export const updateRow = async (
rows[updatingRowIndex][key] = parsedValues[key]
}
await rows[updatingRowIndex].save()
log = log = {
status: 'success',
description: `Succesfully updated row in ${doc.title} > ${sheet.title}`,
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: 'Succesfully updated row',
message: log.description,
}))
} catch (err) {
log = {
status: 'error',
description: `An error occured while updating the row`,
details: err,
}
result &&
(await saveErrorLog({
resultId: result.id,
message: "Couldn't fetch spreadsheet data",
message: log.description,
details: err,
}))
}
return { outgoingEdgeId }
return { outgoingEdgeId, logs: log ? [log] : undefined }
}

View File

@ -19,11 +19,21 @@ import { decrypt } from 'utils/api'
import { defaultFrom, defaultTransportOptions } from '../constants'
export const executeSendEmailBlock = async (
{ result, typebot }: SessionState,
{ result, typebot, isPreview }: SessionState,
block: SendEmailBlock
): Promise<ExecuteIntegrationResponse> => {
const { options } = block
const { variables } = typebot
if (isPreview)
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [
{
status: 'info',
description: 'Emails are not sent in preview mode',
},
],
}
await sendEmail({
typebotId: typebot.id,
resultId: result?.id,

View File

@ -19,6 +19,7 @@ import {
ResultValues,
PublicTypebot,
KeyValue,
ReplyLog,
} from 'models'
import { stringify } from 'qs'
import { byId, omit, parseAnswers } from 'utils'
@ -31,6 +32,7 @@ export const executeWebhookBlock = async (
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
): Promise<ExecuteIntegrationResponse> => {
const { typebot, result } = state
let log: ReplyLog | undefined
const webhook = (await prisma.webhook.findUnique({
where: { id: block.webhookId },
})) as Webhook | null
@ -56,20 +58,27 @@ export const executeWebhookBlock = async (
const isError = status.startsWith('4') || status.startsWith('5')
if (isError) {
log = {
status: 'error',
description: `Webhook returned error: ${webhookResponse.data}`,
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
}
result &&
(await saveErrorLog({
resultId: result.id,
message: `Webhook returned error: ${webhookResponse.data}`,
details: JSON.stringify(webhookResponse.data, null, 2).substring(
0,
1000
),
message: log.description,
details: log.details,
}))
} else {
log = {
status: 'success',
description: `Webhook executed successfully!`,
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: `Webhook returned success: ${webhookResponse.data}`,
message: log.description,
details: JSON.stringify(webhookResponse.data, null, 2).substring(
0,
1000
@ -102,7 +111,7 @@ export const executeWebhookBlock = async (
}
}
return { outgoingEdgeId: block.outgoingEdgeId }
return { outgoingEdgeId: block.outgoingEdgeId, logs: log ? [log] : undefined }
}
const prepareWebhookAttributes = (

View File

@ -40,8 +40,15 @@ export const sendMessageProcedure = publicProcedure
const session = sessionId ? await getSession(sessionId) : null
if (!session) {
const { sessionId, typebot, messages, input, resultId, dynamicTheme } =
await startSession(startParams)
const {
sessionId,
typebot,
messages,
input,
resultId,
dynamicTheme,
logs,
} = await startSession(startParams)
return {
sessionId,
typebot: typebot
@ -55,9 +62,10 @@ export const sendMessageProcedure = publicProcedure
input,
resultId,
dynamicTheme,
logs,
}
} else {
const { messages, input, logic, newSessionState, integrations } =
const { messages, input, logic, newSessionState, integrations, logs } =
await continueBotFlow(session.state)(message)
await prisma.chatSession.updateMany({
@ -73,6 +81,7 @@ export const sendMessageProcedure = publicProcedure
logic,
integrations,
dynamicTheme: parseDynamicThemeReply(newSessionState),
logs,
}
}
})
@ -84,6 +93,9 @@ const startSession = async (startParams?: StartParams) => {
message: 'No typebot provided in startParams',
})
const isPreview =
startParams?.isPreview || typeof startParams?.typebot !== 'string'
const typebot = await getTypebot(startParams)
const startVariables = startParams.prefilledVariables
@ -92,6 +104,7 @@ const startSession = async (startParams?: StartParams) => {
const result = await getResult({
...startParams,
isPreview,
typebot: typebot.id,
startVariables,
isNewResultOnRefreshEnabled:
@ -112,7 +125,7 @@ const startSession = async (startParams?: StartParams) => {
result: result
? { id: result.id, variables: result.variables, hasStarted: false }
: undefined,
isPreview: startParams.isPreview || typeof startParams.typebot !== 'string',
isPreview,
currentTypebotId: typebot.id,
dynamicTheme: parseDynamicThemeInState(typebot.theme),
}
@ -122,6 +135,7 @@ const startSession = async (startParams?: StartParams) => {
input,
logic,
newSessionState: newInitialState,
logs,
} = await startBotFlow(initialState, startParams.startGroupId)
if (!input)
@ -138,6 +152,7 @@ const startSession = async (startParams?: StartParams) => {
),
},
dynamicTheme: parseDynamicThemeReply(newInitialState),
logs,
}
const sessionState: ChatSession['state'] = {
@ -170,6 +185,7 @@ const startSession = async (startParams?: StartParams) => {
input,
logic,
dynamicTheme: parseDynamicThemeReply(newInitialState),
logs,
} satisfies ChatReply
}

View File

@ -27,6 +27,7 @@ export const executeGroup =
const messages: ChatReply['messages'] = currentReply?.messages ?? []
let logic: ChatReply['logic'] = currentReply?.logic
let integrations: ChatReply['integrations'] = currentReply?.integrations
let logs: ChatReply['logs'] = currentReply?.logs
let nextEdgeId = null
let newSessionState = state
@ -58,6 +59,9 @@ export const executeGroup =
blockId: block.id,
},
},
logic,
integrations,
logs,
}
const executionResponse = isLogicBlock(block)
? await executeLogic(newSessionState)(block)
@ -67,9 +71,11 @@ export const executeGroup =
if (!executionResponse) continue
if ('logic' in executionResponse && executionResponse.logic)
logic = executionResponse.logic
logic = { ...logic, ...executionResponse.logic }
if ('integrations' in executionResponse && executionResponse.integrations)
integrations = executionResponse.integrations
integrations = { ...integrations, ...executionResponse.integrations }
if (executionResponse.logs)
logs = [...(logs ?? []), ...executionResponse.logs]
if (executionResponse.newSessionState)
newSessionState = executionResponse.newSessionState
if (executionResponse.outgoingEdgeId) {
@ -78,19 +84,23 @@ export const executeGroup =
}
}
if (!nextEdgeId) return { messages, newSessionState, logic, integrations }
if (!nextEdgeId)
return { messages, newSessionState, logic, integrations, logs }
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
if (nextGroup?.updatedContext) newSessionState = nextGroup.updatedContext
if (!nextGroup) {
return { messages, newSessionState, logic, integrations }
return { messages, newSessionState, logic, integrations, logs }
}
return executeGroup(newSessionState, { messages, logic, integrations })(
nextGroup.group
)
return executeGroup(newSessionState, {
messages,
logic,
integrations,
logs,
})(nextGroup.group)
}
const computeRuntimeOptions =

View File

@ -5,9 +5,9 @@ export type EdgeId = string
export type ExecuteLogicResponse = {
outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState
} & Pick<ChatReply, 'logic'>
} & Pick<ChatReply, 'logic' | 'logs'>
export type ExecuteIntegrationResponse = {
outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState
} & Pick<ChatReply, 'integrations'>
} & Pick<ChatReply, 'integrations' | 'logs'>

View File

@ -5,7 +5,8 @@ import {
sendReachedChatsLimitEmail,
} from 'emails'
import { Workspace } from 'models'
import { env, getChatsLimit, isDefined } from 'utils'
import { env, isDefined } from 'utils'
import { getChatsLimit } from 'utils/pricing'
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8

View File

@ -11,7 +11,7 @@ import Stripe from 'stripe'
import Cors from 'cors'
import { PaymentInputOptions, StripeCredentialsData, Variable } from 'models'
import prisma from '@/lib/prisma'
import { parseVariables } from 'bot-engine'
import { parseVariables } from '@/features/variables'
const cors = initMiddleware(Cors())

View File

@ -11,7 +11,6 @@ import {
WebhookBlock,
HttpMethod,
} from 'models'
import { parseVariables } from 'bot-engine'
import { NextApiRequest, NextApiResponse } from 'next'
import got, { Method, Headers, HTTPError } from 'got'
import { byId, omit, parseAnswers } from 'utils'
@ -25,6 +24,7 @@ import {
getLinkedTypebots,
getLinkedTypebotsChildren,
} from '@/features/blocks/logic/typebotLink/api'
import { parseVariables } from '@/features/variables'
const cors = initMiddleware(Cors())

View File

@ -2,7 +2,8 @@ import prisma from '@/lib/prisma'
import { InputBlockType, PublicTypebot } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils/api'
import { byId, env, getStorageLimit, isDefined } from 'utils'
import { byId, env, isDefined } from 'utils'
import { getStorageLimit } from 'utils/pricing'
import {
sendAlmostReachedStorageLimitEmail,
sendReachedStorageLimitEmail,

View File

@ -5,15 +5,7 @@ import cors from 'nextjs-cors'
import { NextApiRequest, NextApiResponse } from 'next'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res, {
origin: [
'https://docs.typebot.io',
'https://app.typebot.io',
'http://localhost:3005',
'http://localhost:3006',
'http://localhost:3000',
],
})
await cors(req, res)
return createOpenApiNextHandler({
router: appRouter,
@ -25,4 +17,5 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
},
})(req, res)
}
export default handler

View File

@ -1,10 +1,8 @@
import { IncomingMessage } from 'http'
import { NotFoundPage } from '@/components/NotFoundPage'
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
import { env, getViewerUrl, isNotDefined } from 'utils'
import prisma from '@/lib/prisma'
import { TypebotPageV2, TypebotPageV2Props } from '@/components/TypebotPageV2'
import { ErrorPage } from '@/components/ErrorPage'
import { TypebotPage, TypebotPageProps } from '@/components/TypebotPageV2'
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
@ -51,18 +49,16 @@ export const getServerSideProps: GetServerSideProps = async (
const getTypebotFromPublicId = async (
publicId?: string
): Promise<TypebotPageV2Props['typebot'] | null> => {
): Promise<TypebotPageProps['typebot'] | null> => {
const typebot = (await prisma.typebot.findUnique({
where: { publicId: publicId ?? '' },
select: {
theme: true,
name: true,
settings: true,
isArchived: true,
isClosed: true,
publicId: true,
},
})) as TypebotPageV2Props['typebot'] | null
})) as TypebotPageProps['typebot'] | null
if (isNotDefined(typebot)) return null
return typebot
}
@ -74,11 +70,8 @@ const getHost = (
forwardedHost: req?.headers['x-forwarded-host'] as string | undefined,
})
const App = ({ typebot, url }: TypebotPageV2Props) => {
if (!typebot || typebot.isArchived) return <NotFoundPage />
if (typebot.isClosed)
return <ErrorPage error={new Error('This bot is now closed')} />
return <TypebotPageV2 typebot={typebot} url={url} />
}
const App = ({ typebot, url }: TypebotPageProps) => (
<TypebotPage typebot={typebot} url={url} />
)
export default App