2
0

feat(results): Add logs in results

This commit is contained in:
Baptiste Arnaud
2022-03-01 11:40:22 +01:00
parent 4630512b8b
commit ebf92b5536
27 changed files with 408 additions and 120 deletions

View File

@ -31,8 +31,14 @@ export const ChatBlock = ({
onScroll,
onBlockEnd,
}: ChatBlockProps) => {
const { typebot, updateVariableValue, createEdge, apiHost, isPreview } =
useTypebot()
const {
typebot,
updateVariableValue,
createEdge,
apiHost,
isPreview,
onNewLog,
} = useTypebot()
const { resultValues } = useAnswers()
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
@ -72,6 +78,7 @@ export const ChatBlock = ({
updateVariableValue,
resultValues,
blocks: typebot.blocks,
onNewLog,
},
})
nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()

View File

@ -4,8 +4,7 @@ import { ChatBlock } from './ChatBlock/ChatBlock'
import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme'
import { useAnswers } from '../contexts/AnswersContext'
import { deepEqual } from 'fast-equals'
import { Answer, Block, Edge, Theme, VariableWithValue } from 'models'
import { Block, Edge, Theme, VariableWithValue } from 'models'
import { byId, isNotDefined } from 'utils'
import { animateScroll as scroll } from 'react-scroll'
import { useTypebot } from 'contexts/TypebotContext'
@ -13,14 +12,12 @@ import { useTypebot } from 'contexts/TypebotContext'
type Props = {
theme: Theme
onNewBlockVisible: (edge: Edge) => void
onNewAnswer: (answer: Answer) => void
onCompleted: () => void
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
}
export const ConversationContainer = ({
theme,
onNewBlockVisible,
onNewAnswer,
onCompleted,
onVariablesPrefilled,
}: Props) => {
@ -29,11 +26,7 @@ export const ConversationContainer = ({
const [displayedBlocks, setDisplayedBlocks] = useState<
{ block: Block; startStepIndex: number }[]
>([])
const [localAnswer, setLocalAnswer] = useState<Answer | undefined>()
const {
resultValues: { answers },
setPrefilledVariables,
} = useAnswers()
const { setPrefilledVariables } = useAnswers()
const bottomAnchor = useRef<HTMLDivElement | null>(null)
const scrollableContainer = useRef<HTMLDivElement | null>(null)
@ -80,14 +73,6 @@ export const ConversationContainer = ({
setCssVariablesValue(theme, frameDocument.body.style)
}, [theme, frameDocument])
useEffect(() => {
const answer = [...answers].pop()
if (!answer || deepEqual(localAnswer, answer)) return
setLocalAnswer(answer)
onNewAnswer(answer)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [answers])
const autoScrollToBottom = () => {
if (!scrollableContainer.current) return
scroll.scrollToBottom({

View File

@ -17,6 +17,7 @@ import {
PublicTypebot,
VariableWithValue,
} from 'models'
import { Log } from 'db'
export type TypebotViewerProps = {
typebot: PublicTypebot
@ -24,6 +25,7 @@ export type TypebotViewerProps = {
apiHost?: string
onNewBlockVisible?: (edge: Edge) => void
onNewAnswer?: (answer: Answer) => void
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
onCompleted?: () => void
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
}
@ -31,6 +33,7 @@ export const TypebotViewer = ({
typebot,
apiHost = process.env.NEXT_PUBLIC_VIEWER_HOST,
isPreview = false,
onNewLog,
onNewBlockVisible,
onNewAnswer,
onCompleted,
@ -43,15 +46,15 @@ export const TypebotViewer = ({
: 'transparent',
[typebot?.theme?.general?.background]
)
const handleNewBlockVisible = (edge: Edge) => {
if (onNewBlockVisible) onNewBlockVisible(edge)
}
const handleNewAnswer = (answer: Answer) => {
if (onNewAnswer) onNewAnswer(answer)
}
const handleCompleted = () => {
if (onCompleted) onCompleted()
}
const handleNewBlockVisible = (edge: Edge) =>
onNewBlockVisible && onNewBlockVisible(edge)
const handleNewAnswer = (answer: Answer) => onNewAnswer && onNewAnswer(answer)
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>
onNewLog && onNewLog(log)
const handleCompleted = () => onCompleted && onCompleted()
if (!apiHost)
return <p>process.env.NEXT_PUBLIC_VIEWER_HOST is missing in env</p>
@ -76,8 +79,13 @@ export const TypebotViewer = ({
}:wght@300;400;600&display=swap');`,
}}
/>
<TypebotContext typebot={typebot} apiHost={apiHost} isPreview={isPreview}>
<AnswersContext>
<TypebotContext
typebot={typebot}
apiHost={apiHost}
isPreview={isPreview}
onNewLog={handleNewLog}
>
<AnswersContext onNewAnswer={handleNewAnswer}>
<div
className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container"
style={{
@ -90,7 +98,6 @@ export const TypebotViewer = ({
<ConversationContainer
theme={typebot.theme}
onNewBlockVisible={handleNewBlockVisible}
onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted}
onVariablesPrefilled={onVariablesPrefilled}
/>

View File

@ -9,18 +9,26 @@ const answersContext = createContext<{
//@ts-ignore
}>({})
export const AnswersContext = ({ children }: { children: ReactNode }) => {
export const AnswersContext = ({
children,
onNewAnswer,
}: {
onNewAnswer: (answer: Answer) => void
children: ReactNode
}) => {
const [resultValues, setResultValues] = useState<ResultValues>({
answers: [],
prefilledVariables: [],
createdAt: new Date().toISOString(),
})
const addAnswer = (answer: Answer) =>
const addAnswer = (answer: Answer) => {
setResultValues((resultValues) => ({
...resultValues,
answers: [...resultValues.answers, answer],
}))
onNewAnswer(answer)
}
const setPrefilledVariables = (variables: VariableWithValue[]) =>
setResultValues((resultValues) => ({

View File

@ -1,3 +1,4 @@
import { Log } from 'db'
import { Edge, PublicTypebot } from 'models'
import React, {
createContext,
@ -13,6 +14,7 @@ const typebotContext = createContext<{
isPreview: boolean
updateVariableValue: (variableId: string, value: string) => void
createEdge: (edge: Edge) => void
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
@ -22,11 +24,13 @@ export const TypebotContext = ({
typebot,
apiHost,
isPreview,
onNewLog,
}: {
children: ReactNode
typebot: PublicTypebot
apiHost: string
isPreview: boolean
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
}) => {
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
@ -63,6 +67,7 @@ export const TypebotContext = ({
isPreview,
updateVariableValue,
createEdge,
onNewLog,
}}
>
{children}

View File

@ -1,3 +1,4 @@
import { Log } from 'db'
import {
IntegrationStep,
IntegrationStepType,
@ -18,7 +19,6 @@ import {
import { stringify } from 'qs'
import { sendRequest } from 'utils'
import { sendGaEvent } from '../../lib/gtag'
import { sendErrorMessage, sendInfoMessage } from './postMessage'
import { parseVariables, parseVariablesInObject } from './variable'
const safeEval = eval
@ -33,6 +33,7 @@ type IntegrationContext = {
resultValues: ResultValues
blocks: Block[]
updateVariableValue: (variableId: string, value: string) => void
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
}
export const executeIntegration = ({
@ -87,10 +88,17 @@ const executeGoogleSheetIntegration = async (
const insertRowInGoogleSheets = async (
options: GoogleSheetsInsertRowOptions,
{ variables, apiHost }: IntegrationContext
{ variables, apiHost, onNewLog }: IntegrationContext
) => {
if (!options.cellsToInsert) return
return sendRequest({
if (!options.cellsToInsert) {
onNewLog({
status: 'warning',
description: 'Cells to insert are undefined',
details: null,
})
return
}
const { error } = await sendRequest({
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
method: 'POST',
body: {
@ -98,14 +106,21 @@ const insertRowInGoogleSheets = async (
values: parseCellValues(options.cellsToInsert, variables),
},
})
onNewLog(
parseLog(
error,
'Succesfully inserted a row in the sheet',
'Failed to insert a row in the sheet'
)
)
}
const updateRowInGoogleSheets = async (
options: GoogleSheetsUpdateRowOptions,
{ variables, apiHost }: IntegrationContext
{ variables, apiHost, onNewLog }: IntegrationContext
) => {
if (!options.cellsToUpsert || !options.referenceCell) return
return sendRequest({
const { error } = await sendRequest({
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
method: 'PATCH',
body: {
@ -117,11 +132,18 @@ const updateRowInGoogleSheets = async (
},
},
})
onNewLog(
parseLog(
error,
'Succesfully updated a row in the sheet',
'Failed to update a row in the sheet'
)
)
}
const getRowFromGoogleSheets = async (
options: GoogleSheetsGetOptions,
{ variables, updateVariableValue, apiHost }: IntegrationContext
{ variables, updateVariableValue, apiHost, onNewLog }: IntegrationContext
) => {
if (!options.referenceCell || !options.cellsToExtract) return
const queryParams = stringify(
@ -135,10 +157,17 @@ const getRowFromGoogleSheets = async (
},
{ indices: false }
)
const { data } = await sendRequest<{ [key: string]: string }>({
const { data, error } = await sendRequest<{ [key: string]: string }>({
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${queryParams}`,
method: 'GET',
})
onNewLog(
parseLog(
error,
'Succesfully fetched data from sheet',
'Failed to fetch data from sheet'
)
)
if (!data) return
options.cellsToExtract.forEach((cell) =>
updateVariableValue(cell.variableId ?? '', data[cell.column ?? ''])
@ -167,7 +196,7 @@ const executeWebhook = async (
typebotId,
apiHost,
resultValues,
isPreview,
onNewLog,
}: IntegrationContext
) => {
const { data, error } = await sendRequest({
@ -178,8 +207,15 @@ const executeWebhook = async (
resultValues,
},
})
console.error(error)
if (isPreview && error) sendErrorMessage(`Webhook failed: ${error.message}`)
const statusCode = (data as Record<string, string>).statusCode.toString()
const isError = statusCode.startsWith('4') || statusCode.startsWith('5')
onNewLog({
status: error ? 'error' : isError ? 'warning' : 'success',
description: isError
? 'Webhook returned an error'
: 'Webhook successfuly executed',
details: JSON.stringify(error ?? data, null, 2).substring(0, 1000),
})
step.options.responseVariableMapping.forEach((varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return
const value = safeEval(`(${JSON.stringify(data)}).${varMapping?.bodyPath}`)
@ -190,10 +226,16 @@ const executeWebhook = async (
const sendEmail = async (
step: SendEmailStep,
{ variables, apiHost, isPreview }: IntegrationContext
{ variables, apiHost, isPreview, onNewLog }: IntegrationContext
) => {
if (isPreview) sendInfoMessage('Emails are not sent in preview mode')
if (isPreview) return step.outgoingEdgeId
if (isPreview) {
onNewLog({
status: 'info',
description: 'Emails are not sent in preview mode',
details: null,
})
return step.outgoingEdgeId
}
const { options } = step
const { error } = await sendRequest({
url: `${apiHost}/api/integrations/email`,
@ -207,6 +249,18 @@ const sendEmail = async (
bcc: (options.bcc ?? []).map(parseVariables(variables)),
},
})
console.error(error)
onNewLog(
parseLog(error, 'Succesfully sent an email', 'Failed to send an email')
)
return step.outgoingEdgeId
}
const parseLog = (
error: Error | undefined,
successMessage: string,
errorMessage: string
): Omit<Log, 'id' | 'createdAt' | 'resultId'> => ({
status: error ? 'error' : 'success',
description: error ? errorMessage : successMessage,
details: (error && JSON.stringify(error, null, 2).substring(0, 1000)) ?? null,
})

View File

@ -1,7 +0,0 @@
export const sendInfoMessage = (typebotInfo: string) => {
parent.postMessage({ typebotInfo })
}
export const sendErrorMessage = (typebotError: string) => {
parent.postMessage({ typebotError })
}

View File

@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "Log" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"resultId" TEXT NOT NULL,
"status" TEXT NOT NULL,
"description" TEXT NOT NULL,
"details" TEXT,
CONSTRAINT "Log_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Log" ADD CONSTRAINT "Log_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "Result"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -129,7 +129,7 @@ model Typebot {
customDomain String? @unique
collaborators CollaboratorsOnTypebots[]
invitations Invitation[]
webhooks Webhook[]
webhooks Webhook[]
@@unique([id, ownerId])
}
@ -184,6 +184,17 @@ model Result {
answers Answer[]
prefilledVariables Json[]
isCompleted Boolean
logs Log[]
}
model Log {
id String @id @default(cuid())
createdAt DateTime @default(now())
resultId String
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
status String
description String
details String?
}
model Answer {