feat(results): ✨ Add logs in results
This commit is contained in:
@ -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()
|
||||
|
@ -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({
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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) => ({
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -1,7 +0,0 @@
|
||||
export const sendInfoMessage = (typebotInfo: string) => {
|
||||
parent.postMessage({ typebotInfo })
|
||||
}
|
||||
|
||||
export const sendErrorMessage = (typebotError: string) => {
|
||||
parent.postMessage({ typebotError })
|
||||
}
|
@ -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;
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user