2
0

(bot) Use ky for queries in bot to improve reliability

This commit is contained in:
Baptiste Arnaud
2023-12-20 08:31:22 +01:00
parent f2cccbd33f
commit a6536461e5
11 changed files with 167 additions and 135 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.2.28", "version": "0.2.29",
"description": "Javascript library to display typebots on your website", "description": "Javascript library to display typebots on your website",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
@ -15,6 +15,7 @@
"@stripe/stripe-js": "1.54.1", "@stripe/stripe-js": "1.54.1",
"@udecode/plate-common": "21.1.5", "@udecode/plate-common": "21.1.5",
"dompurify": "3.0.6", "dompurify": "3.0.6",
"ky": "^1.1.3",
"marked": "9.0.3", "marked": "9.0.3",
"solid-element": "1.7.1", "solid-element": "1.7.1",
"solid-js": "1.7.8" "solid-js": "1.7.8"

View File

@ -16,6 +16,7 @@ import { InputBlock } from '@typebot.io/schemas'
import { StartFrom } from '@typebot.io/schemas' import { StartFrom } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { HTTPError } from 'ky'
export type BotProps = { export type BotProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -52,7 +53,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
typeof props.typebot === 'string' ? props.typebot : undefined typeof props.typebot === 'string' ? props.typebot : undefined
const isPreview = const isPreview =
typeof props.typebot !== 'string' || (props.isPreview ?? false) typeof props.typebot !== 'string' || (props.isPreview ?? false)
const { data, error, response } = await startChatQuery({ const { data, error } = await startChatQuery({
stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined, stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined,
typebot: props.typebot, typebot: props.typebot,
apiHost: props.apiHost, apiHost: props.apiHost,
@ -66,24 +67,42 @@ export const Bot = (props: BotProps & { class?: string }) => {
}, },
startFrom: props.startFrom, startFrom: props.startFrom,
}) })
if (error && 'code' in error && typeof error.code === 'string') { if (error instanceof HTTPError) {
if (isPreview) { if (isPreview) {
return setError( return setError(
new Error('An error occurred while loading the bot.', { new Error(`An error occurred while loading the bot.`, {
cause: error.message, cause: {
status: error.response.status,
body: await error.response.json(),
},
}) })
) )
} }
if (['BAD_REQUEST', 'FORBIDDEN'].includes(error.code)) if (error.response.status === 400 || error.response.status === 403)
return setError(new Error('This bot is now closed.')) return setError(new Error('This bot is now closed.'))
if (error.code === 'NOT_FOUND') if (error.response.status === 404)
return setError(new Error("The bot you're looking for doesn't exist.")) return setError(new Error("The bot you're looking for doesn't exist."))
return setError(
new Error(
`Error! Couldn't initiate the chat. (${error.response.statusText})`
)
)
} }
if (!data) { if (!data) {
if (error) console.error(error) if (error) {
console.error({ data, error, response }) console.error(error)
return setError(new Error("Error! Couldn't initiate the chat.")) if (isPreview) {
return setError(
new Error(`Error! Could not reach server. Check your connection.`, {
cause: error,
})
)
}
}
return setError(
new Error('Error! Could not reach server. Check your connection.')
)
} }
if (data.resultId && typebotIdFromProps) if (data.resultId && typebotIdFromProps)

View File

@ -31,6 +31,7 @@ import {
} from '@/utils/formattedMessagesSignal' } from '@/utils/formattedMessagesSignal'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery' import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery'
import { HTTPError } from 'ky'
const parseDynamicTheme = ( const parseDynamicTheme = (
initialTheme: Theme, initialTheme: Theme,
@ -173,7 +174,13 @@ export const ConversationContainer = (props: Props) => {
const errorLogs = [ const errorLogs = [
{ {
description: 'Failed to send the reply', description: 'Failed to send the reply',
details: error, details:
error instanceof HTTPError
? {
status: error.response.status,
body: await error.response.json(),
}
: error,
status: 'error', status: 'error',
}, },
] ]

View File

@ -5,7 +5,7 @@ export const ErrorMessage = (props: Props) => {
return ( return (
<div class="h-full flex justify-center items-center flex-col"> <div class="h-full flex justify-center items-center flex-col">
<p class="text-2xl text-center">{props.error.message}</p> <p class="text-2xl text-center">{props.error.message}</p>
<p class="text-center">{props.error.cause as string}</p> <pre>{JSON.stringify(props.error.cause, null, 2) as string}</pre>
</div> </div>
) )
} }

View File

@ -1,8 +1,9 @@
import { guessApiHost } from '@/utils/guessApiHost' import { guessApiHost } from '@/utils/guessApiHost'
import { isNotEmpty, sendRequest } from '@typebot.io/lib' import { isNotEmpty } from '@typebot.io/lib'
import { ContinueChatResponse } from '@typebot.io/schemas' import { ContinueChatResponse } from '@typebot.io/schemas'
import ky from 'ky'
export const continueChatQuery = ({ export const continueChatQuery = async ({
apiHost, apiHost,
message, message,
sessionId, sessionId,
@ -10,13 +11,24 @@ export const continueChatQuery = ({
apiHost?: string apiHost?: string
message: string | undefined message: string | undefined
sessionId: string sessionId: string
}) => }) => {
sendRequest<ContinueChatResponse>({ try {
method: 'POST', const data = await ky
url: `${ .post(
isNotEmpty(apiHost) ? apiHost : guessApiHost() `${
}/api/v1/sessions/${sessionId}/continueChat`, isNotEmpty(apiHost) ? apiHost : guessApiHost()
body: { }/api/v1/sessions/${sessionId}/continueChat`,
message, {
}, json: {
}) message,
},
timeout: false,
}
)
.json<ContinueChatResponse>()
return { data }
} catch (error) {
return { error }
}
}

View File

@ -1,33 +0,0 @@
import { guessApiHost } from '@/utils/guessApiHost'
import { isNotEmpty } from '@typebot.io/lib'
export const getOpenAiStreamerQuery =
({ apiHost, sessionId }: { apiHost?: string; sessionId: string }) =>
async (
messages: {
content?: string | undefined
role?: 'system' | 'user' | 'assistant' | undefined
}[]
) => {
const response = await fetch(
`${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/integrations/openai/streamer`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sessionId,
messages,
}),
}
)
if (!response.ok) {
throw new Error(response.statusText)
}
const data = response.body
return data
}

View File

@ -1,8 +1,9 @@
import { guessApiHost } from '@/utils/guessApiHost' import { guessApiHost } from '@/utils/guessApiHost'
import type { ChatLog } from '@typebot.io/schemas' import type { ChatLog } from '@typebot.io/schemas'
import { isNotEmpty, sendRequest } from '@typebot.io/lib' import { isNotEmpty } from '@typebot.io/lib'
import ky from 'ky'
export const saveClientLogsQuery = ({ export const saveClientLogsQuery = async ({
apiHost, apiHost,
sessionId, sessionId,
clientLogs, clientLogs,
@ -10,13 +11,19 @@ export const saveClientLogsQuery = ({
apiHost?: string apiHost?: string
sessionId: string sessionId: string
clientLogs: ChatLog[] clientLogs: ChatLog[]
}) => }) => {
sendRequest({ try {
method: 'POST', await ky.post(
url: `${ `${
isNotEmpty(apiHost) ? apiHost : guessApiHost() isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/sessions/${sessionId}/clientLogs`, }/api/v1/sessions/${sessionId}/clientLogs`,
body: { {
clientLogs, json: {
}, clientLogs,
}) },
}
)
} catch (e) {
console.log(e)
}
}

View File

@ -1,6 +1,6 @@
import { BotContext, InitialChatReply } from '@/types' import { BotContext, InitialChatReply } from '@/types'
import { guessApiHost } from '@/utils/guessApiHost' import { guessApiHost } from '@/utils/guessApiHost'
import { isNotDefined, isNotEmpty, sendRequest } from '@typebot.io/lib' import { isNotDefined, isNotEmpty } from '@typebot.io/lib'
import { import {
getPaymentInProgressInStorage, getPaymentInProgressInStorage,
removePaymentInProgressFromStorage, removePaymentInProgressFromStorage,
@ -10,6 +10,18 @@ import {
StartFrom, StartFrom,
StartPreviewChatInput, StartPreviewChatInput,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import ky from 'ky'
type Props = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typebot: string | any
stripeRedirectStatus?: string
apiHost?: string
startFrom?: StartFrom
isPreview: boolean
prefilledVariables?: Record<string, unknown>
resultId?: string
}
export async function startChatQuery({ export async function startChatQuery({
typebot, typebot,
@ -19,16 +31,7 @@ export async function startChatQuery({
resultId, resultId,
stripeRedirectStatus, stripeRedirectStatus,
startFrom, startFrom,
}: { }: Props) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typebot: string | any
stripeRedirectStatus?: string
apiHost?: string
startFrom?: StartFrom
isPreview: boolean
prefilledVariables?: Record<string, unknown>
resultId?: string
}) {
if (isNotDefined(typebot)) if (isNotDefined(typebot))
throw new Error('Typebot ID is required to get initial messages') throw new Error('Typebot ID is required to get initial messages')
@ -41,67 +44,75 @@ export async function startChatQuery({
: undefined : undefined
if (paymentInProgressState) { if (paymentInProgressState) {
removePaymentInProgressFromStorage() removePaymentInProgressFromStorage()
const { data, error, response } = await sendRequest<InitialChatReply>({
method: 'POST', try {
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sessions/${ const data = await ky
paymentInProgressState.sessionId .post(
}/continueChat`, `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sessions/${
body: { paymentInProgressState.sessionId
message: paymentInProgressState }/continueChat`,
? stripeRedirectStatus === 'failed' {
? 'fail' json: {
: 'Success' message: paymentInProgressState
: undefined, ? stripeRedirectStatus === 'failed'
}, ? 'fail'
}) : 'Success'
return { : undefined,
data: data },
? { timeout: false,
...data,
...(paymentInProgressState
? { typebot: paymentInProgressState.typebot }
: {}),
} }
: undefined, )
error, .json<InitialChatReply>()
response,
return { data }
} catch (error) {
return { error }
} }
} }
const typebotId = typeof typebot === 'string' ? typebot : typebot.id const typebotId = typeof typebot === 'string' ? typebot : typebot.id
if (isPreview) { if (isPreview) {
const { data, error, response } = await sendRequest<InitialChatReply>({ try {
method: 'POST', const data = await ky
url: `${ .post(
isNotEmpty(apiHost) ? apiHost : guessApiHost() `${
}/api/v1/typebots/${typebotId}/preview/startChat`, isNotEmpty(apiHost) ? apiHost : guessApiHost()
body: { }/api/v1/typebots/${typebotId}/preview/startChat`,
isStreamEnabled: true, {
startFrom, json: {
typebot, isStreamEnabled: true,
} satisfies Omit<StartPreviewChatInput, 'typebotId'>, startFrom,
}) typebot,
return { } satisfies Omit<StartPreviewChatInput, 'typebotId'>,
data, timeout: false,
error, }
response, )
.json<InitialChatReply>()
return { data }
} catch (error) {
return { error }
} }
} }
const { data, error, response } = await sendRequest<InitialChatReply>({ try {
method: 'POST', const data = await ky
url: `${ .post(
isNotEmpty(apiHost) ? apiHost : guessApiHost() `${
}/api/v1/typebots/${typebotId}/startChat`, isNotEmpty(apiHost) ? apiHost : guessApiHost()
body: { }/api/v1/typebots/${typebotId}/startChat`,
isStreamEnabled: true, {
prefilledVariables, json: {
resultId, isStreamEnabled: true,
} satisfies Omit<StartChatInput, 'publicId'>, prefilledVariables,
}) resultId,
} satisfies Omit<StartChatInput, 'publicId'>,
timeout: false,
}
)
.json<InitialChatReply>()
return { return { data }
data, } catch (error) {
error, return { error }
response,
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/nextjs", "name": "@typebot.io/nextjs",
"version": "0.2.28", "version": "0.2.29",
"description": "Convenient library to display typebots on your Next.js website", "description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.2.28", "version": "0.2.29",
"description": "Convenient library to display typebots on your React app", "description": "Convenient library to display typebots on your React app",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

8
pnpm-lock.yaml generated
View File

@ -956,6 +956,9 @@ importers:
dompurify: dompurify:
specifier: 3.0.6 specifier: 3.0.6
version: 3.0.6 version: 3.0.6
ky:
specifier: ^1.1.3
version: 1.1.3
marked: marked:
specifier: 9.0.3 specifier: 9.0.3
version: 9.0.3 version: 9.0.3
@ -17743,6 +17746,11 @@ packages:
resolution: {integrity: sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==} resolution: {integrity: sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==}
dev: true dev: true
/ky@1.1.3:
resolution: {integrity: sha512-t7q8sJfazzHbfYxiCtuLIH4P+pWoCgunDll17O/GBZBqMt2vHjGSx5HzSxhOc2BDEg3YN/EmeA7VKrHnwuWDag==}
engines: {node: '>=18'}
dev: false
/language-subtag-registry@0.3.22: /language-subtag-registry@0.3.22:
resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==}
dev: false dev: false