2
0

feat(results): ️ Improve logs details

This commit is contained in:
Baptiste Arnaud
2022-04-19 14:10:22 -07:00
parent 9fbe1cc34c
commit 54a757b21b
17 changed files with 370 additions and 255 deletions

View File

@ -51,7 +51,7 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
if (isDefined(user) || isNotDefined(session)) return
const parsedUser = session.user as User
setUser(parsedUser)
setSentryUser({ id: parsedUser.id })
if (parsedUser?.id) setSentryUser({ id: parsedUser.id })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session])

View File

@ -1,10 +1,9 @@
import { TypebotViewer } from 'bot-engine'
import { Log } from 'db'
import { Answer, PublicTypebot, VariableWithValue } from 'models'
import React, { useEffect, useState } from 'react'
import { upsertAnswer } from 'services/answer'
import { SEO } from '../components/Seo'
import { createLog, createResult, updateResult } from '../services/result'
import { createResult, updateResult } from '../services/result'
import { ErrorPage } from './ErrorPage'
export type TypebotPageProps = {
@ -74,14 +73,6 @@ export const TypebotPage = ({
if (error) setError(error)
}
const handleNewLog = async (
log: Omit<Log, 'id' | 'createdAt' | 'resultId'>
) => {
if (!resultId) return setError(new Error('Result was not created'))
const { error } = await createLog(resultId, log)
if (error) setError(error)
}
if (error) {
return <ErrorPage error={error} />
}
@ -95,11 +86,11 @@ export const TypebotPage = ({
{showTypebot && (
<TypebotViewer
typebot={typebot}
resultId={resultId}
predefinedVariables={predefinedVariables}
onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted}
onVariablesUpdated={handleNewVariables}
onNewLog={handleNewLog}
/>
)}
</div>

View File

@ -2,10 +2,11 @@ import prisma from 'libs/prisma'
import { SendEmailOptions, SmtpCredentialsData } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { createTransport, getTestMessageUrl } from 'nodemailer'
import { decrypt, initMiddleware } from 'utils'
import { decrypt, initMiddleware, methodNotAllowed } from 'utils'
import Cors from 'cors'
import { withSentry } from '@sentry/nextjs'
import { saveErrorLog, saveSuccessLog } from 'services/api/utils'
const cors = initMiddleware(Cors())
@ -27,6 +28,7 @@ const defaultFrom = {
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res)
if (req.method === 'POST') {
const resultId = req.query.resultId as string | undefined
const { credentialsId, recipients, body, subject, cc, bcc, replyTo } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as SendEmailOptions
@ -36,7 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!from)
return res.status(404).send({ message: "Couldn't find credentials" })
const transporter = createTransport({
const transportConfig = {
host,
port,
secure: isTlsEnabled ?? undefined,
@ -44,8 +46,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
user: username,
pass: password,
},
})
const info = await transporter.sendMail({
}
const transporter = createTransport(transportConfig)
const email = {
from: `"${from.name}" <${from.email}>`,
cc,
bcc,
@ -53,14 +56,26 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
replyTo,
subject,
text: body,
})
res.status(200).send({
}
try {
const info = await transporter.sendMail(email)
await saveSuccessLog(resultId, 'Email successfully sent')
return res.status(200).send({
message: 'Email sent!',
info,
previewUrl: getTestMessageUrl(info),
})
} catch (err) {
await saveErrorLog(resultId, 'Email not sent', {
transportConfig,
email,
})
return res.status(500).send({
message: `Email not sent. Error: ${err}`,
})
}
}
return methodNotAllowed(res)
}
const getEmailInfo = async (

View File

@ -5,10 +5,12 @@ import { getAuthenticatedGoogleClient } from 'libs/google-sheets'
import { Cell } from 'models'
import Cors from 'cors'
import { withSentry } from '@sentry/nextjs'
import { saveErrorLog, saveSuccessLog } from 'services/api/utils'
const cors = initMiddleware(Cors())
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res)
const resultId = req.query.resultId as string | undefined
if (req.method === 'GET') {
const spreadsheetId = req.query.spreadsheetId.toString()
const sheetId = req.query.sheetId.toString()
@ -29,17 +31,27 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
doc.useOAuth2Client(client)
await doc.loadInfo()
const sheet = doc.sheetsById[sheetId]
try {
const rows = await sheet.getRows()
const row = rows.find(
(row) => row[referenceCell.column as string] === referenceCell.value
)
if (!row) return res.status(404).send({ message: "Couldn't find row" })
return res.send({
if (!row) {
await saveErrorLog(resultId, "Couldn't find reference cell")
return res.status(404).send({ message: "Couldn't find row" })
}
const response = {
...extractingColumns.reduce(
(obj, column) => ({ ...obj, [column]: row[column] }),
{}
),
})
}
await saveSuccessLog(resultId, 'Succesfully fetched spreadsheet data')
return res.send(response)
} catch (err) {
await saveErrorLog(resultId, "Couldn't fetch spreadsheet data", err)
return res.status(500).send(err)
}
}
if (req.method === 'POST') {
const spreadsheetId = req.query.spreadsheetId.toString()
@ -55,10 +67,16 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!auth)
return res.status(404).send("Couldn't find credentials in database")
doc.useOAuth2Client(auth)
try {
await doc.loadInfo()
const sheet = doc.sheetsById[sheetId]
await sheet.addRow(values)
await saveSuccessLog(resultId, 'Succesfully inserted row')
return res.send({ message: 'Success' })
} catch (err) {
await saveErrorLog(resultId, "Couldn't fetch spreadsheet data", err)
return res.status(500).send(err)
}
}
if (req.method === 'PATCH') {
const spreadsheetId = req.query.spreadsheetId.toString()
@ -75,6 +93,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!auth)
return res.status(404).send("Couldn't find credentials in database")
doc.useOAuth2Client(auth)
try {
await doc.loadInfo()
const sheet = doc.sheetsById[sheetId]
const rows = await sheet.getRows()
@ -87,7 +106,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
rows[updatingRowIndex][key] = values[key]
}
await rows[updatingRowIndex].save()
await saveSuccessLog(resultId, 'Succesfully updated row')
return res.send({ message: 'Success' })
} catch (err) {
await saveErrorLog(resultId, "Couldn't fetch spreadsheet data", err)
return res.status(500).send(err)
}
}
return methodNotAllowed(res)
}

View File

@ -20,12 +20,14 @@ import {
initMiddleware,
methodNotAllowed,
notFound,
omit,
parseAnswers,
} from 'utils'
import { stringify } from 'qs'
import { withSentry } from '@sentry/nextjs'
import Cors from 'cors'
import { parseSampleResult } from 'services/api/webhooks'
import { saveErrorLog, saveSuccessLog } from 'services/api/utils'
const cors = initMiddleware(Cors())
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@ -33,6 +35,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const typebotId = req.query.typebotId.toString()
const stepId = req.query.blockId.toString()
const resultId = req.query.resultId as string | undefined
const { resultValues, variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as {
@ -57,7 +60,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
preparedWebhook,
variables,
step.blockId,
resultValues
resultValues,
resultId
)
return res.status(200).send(result)
}
@ -76,13 +80,14 @@ const prepareWebhookAttributes = (
return webhook
}
const executeWebhook =
export const executeWebhook =
(typebot: Typebot) =>
async (
webhook: Webhook,
variables: Variable[],
blockId: string,
resultValues?: ResultValues
resultValues?: ResultValues,
resultId?: string
): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method)
return {
@ -120,12 +125,10 @@ const executeWebhook =
blockId,
})
: undefined
try {
const response = await got(
parseVariables(variables)(
const request = {
url: parseVariables(variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
),
{
method: webhook.method as Method,
headers,
...basicAuth,
@ -138,23 +141,39 @@ const executeWebhook =
? JSON.parse(parseVariables(variables)(body))
: undefined,
}
)
try {
const response = await got(request.url, omit(request, 'url'))
await saveSuccessLog(resultId, 'Webhook successfuly executed.', {
statusCode: response.statusCode,
request,
response: parseBody(response.body),
})
return {
statusCode: response.statusCode,
data: parseBody(response.body),
}
} catch (error) {
if (error instanceof HTTPError) {
return {
const response = {
statusCode: error.response.statusCode,
data: parseBody(error.response.body as string),
}
await saveErrorLog(resultId, 'Webhook returned an error', {
request,
response,
})
return response
}
console.error(error)
return {
const response = {
statusCode: 500,
data: { message: `Error from Typebot server: ${error}` },
}
console.error(error)
await saveErrorLog(resultId, 'Webhook failed to execute', {
request,
response,
})
return response
}
}

View File

@ -1,31 +1,18 @@
import prisma from 'libs/prisma'
import {
defaultWebhookAttributes,
HttpMethod,
KeyValue,
PublicTypebot,
ResultValues,
Typebot,
Variable,
Webhook,
WebhookOptions,
WebhookResponse,
WebhookStep,
} from 'models'
import { parseVariables } from 'bot-engine'
import { NextApiRequest, NextApiResponse } from 'next'
import got, { Method, Headers, HTTPError } from 'got'
import {
byId,
initMiddleware,
methodNotAllowed,
notFound,
parseAnswers,
} from 'utils'
import { stringify } from 'qs'
import { byId, initMiddleware, methodNotAllowed, notFound } from 'utils'
import { withSentry } from '@sentry/nextjs'
import Cors from 'cors'
import { parseSampleResult } from 'services/api/webhooks'
import { executeWebhook } from '../../executeWebhook'
const cors = initMiddleware(Cors())
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@ -34,6 +21,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const typebotId = req.query.typebotId.toString()
const blockId = req.query.blockId.toString()
const stepId = req.query.stepId.toString()
const resultId = req.query.resultId as string | undefined
const { resultValues, variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as {
@ -58,7 +46,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
preparedWebhook,
variables,
blockId,
resultValues
resultValues,
resultId
)
return res.status(200).send(result)
}
@ -77,129 +66,4 @@ const prepareWebhookAttributes = (
return webhook
}
const executeWebhook =
(typebot: Typebot) =>
async (
webhook: Webhook,
variables: Variable[],
blockId: string,
resultValues?: ResultValues
): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method)
return {
statusCode: 400,
data: { message: `Webhook doesn't have url or method` },
}
const basicAuth: { username?: string; password?: string } = {}
const basicAuthHeaderIdx = webhook.headers.findIndex(
(h) =>
h.key?.toLowerCase() === 'authorization' &&
h.value?.toLowerCase()?.includes('basic')
)
const isUsernamePasswordBasicAuth =
basicAuthHeaderIdx !== -1 &&
webhook.headers[basicAuthHeaderIdx].value?.includes(':')
if (isUsernamePasswordBasicAuth) {
const [username, password] =
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
basicAuth.username = username
basicAuth.password = password
webhook.headers.splice(basicAuthHeaderIdx, 1)
}
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
| Headers
| undefined
const queryParams = stringify(
convertKeyValueTableToObject(webhook.queryParams, variables)
)
const contentType = headers ? headers['Content-Type'] : undefined
const body =
webhook.method !== HttpMethod.GET
? getBodyContent(typebot)({
body: webhook.body,
resultValues,
blockId,
})
: undefined
try {
const response = await got(
parseVariables(variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
),
{
method: webhook.method as Method,
headers,
...basicAuth,
json:
contentType !== 'x-www-form-urlencoded' && body
? JSON.parse(parseVariables(variables)(body))
: undefined,
form:
contentType === 'x-www-form-urlencoded' && body
? JSON.parse(parseVariables(variables)(body))
: undefined,
}
)
return {
statusCode: response.statusCode,
data: parseBody(response.body),
}
} catch (error) {
if (error instanceof HTTPError) {
return {
statusCode: error.response.statusCode,
data: parseBody(error.response.body as string),
}
}
console.error(error)
return {
statusCode: 500,
data: { message: `Error from Typebot server: ${error}` },
}
}
}
const getBodyContent =
(typebot: Pick<Typebot | PublicTypebot, 'blocks' | 'variables' | 'edges'>) =>
({
body,
resultValues,
blockId,
}: {
body?: string | null
resultValues?: ResultValues
blockId: string
}): string | undefined => {
if (!body) return
return body === '{{state}}'
? JSON.stringify(
resultValues
? parseAnswers(typebot)(resultValues)
: parseSampleResult(typebot)(blockId)
)
: body
}
const parseBody = (body: string) => {
try {
return JSON.parse(body)
} catch (err) {
return body
}
}
const convertKeyValueTableToObject = (
keyValues: KeyValue[] | undefined,
variables: Variable[]
) => {
if (!keyValues) return
return keyValues.reduce((object, item) => {
if (!item.key) return {}
return {
...object,
[item.key]: parseVariables(variables)(item.value ?? ''),
}
}, {})
}
export default withSentry(handler)

View File

@ -1,20 +0,0 @@
import { withSentry } from '@sentry/nextjs'
import { Log } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { badRequest, methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const resultId = req.query.resultId as string
const log = req.body as
| Omit<Log, 'id' | 'createdAt' | 'resultId'>
| undefined
if (!log) return badRequest(res)
const createdLog = await prisma.log.create({ data: { ...log, resultId } })
return res.send(createdLog)
}
methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -0,0 +1,130 @@
{
"id": "cl26li8fl0407iez0w2tlw8fn",
"createdAt": "2022-04-19T20:25:30.417Z",
"updatedAt": "2022-04-19T20:40:48.366Z",
"icon": null,
"name": "My typebot",
"ownerId": "proUser",
"publishedTypebotId": null,
"folderId": null,
"blocks": [
{
"id": "cl26li8fj0000iez05x7razkg",
"steps": [
{
"id": "cl26li8fj0001iez0bqfraw9h",
"type": "start",
"label": "Start",
"blockId": "cl26li8fj0000iez05x7razkg",
"outgoingEdgeId": "cl26liqj6000g2e6ed2cwkvse"
}
],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
},
{
"id": "cl26lidjz000a2e6etf4v03hv",
"steps": [
{
"id": "cl26lidk4000b2e6es2fos0nl",
"type": "choice input",
"items": [
{
"id": "cl26lidk5000c2e6e39wyc7wq",
"type": 0,
"stepId": "cl26lidk4000b2e6es2fos0nl",
"content": "Send success webhook"
}
],
"blockId": "cl26lidjz000a2e6etf4v03hv",
"options": { "buttonLabel": "Send", "isMultipleChoice": false }
},
{
"id": "cl26lip76000e2e6ebmph843a",
"type": "Webhook",
"blockId": "cl26lidjz000a2e6etf4v03hv",
"options": {
"isCustomBody": false,
"isAdvancedConfig": false,
"variablesForTest": [],
"responseVariableMapping": []
},
"webhookId": "success-webhook"
},
{
"id": "cl26m0pdz00042e6ebjdoclaa",
"blockId": "cl26lidjz000a2e6etf4v03hv",
"type": "choice input",
"options": { "buttonLabel": "Send", "isMultipleChoice": false },
"items": [
{
"id": "cl26m0pdz00052e6ecmxwfz44",
"stepId": "cl26m0pdz00042e6ebjdoclaa",
"type": 0,
"content": "Send failed webhook"
}
]
},
{
"id": "cl26m0w9b00072e6eld1ei291",
"blockId": "cl26lidjz000a2e6etf4v03hv",
"type": "Webhook",
"options": {
"responseVariableMapping": [],
"variablesForTest": [],
"isAdvancedConfig": false,
"isCustomBody": false
},
"webhookId": "failed-webhook"
}
],
"title": "Group #1",
"graphCoordinates": { "x": 386, "y": 117 }
}
],
"variables": [
{ "id": "vcl26lzmg100012e6e9rn57c3o", "name": "var1" },
{ "id": "vcl26lzo7q00022e6edw3pe7lf", "name": "var2" },
{ "id": "vcl26lzq6s00032e6ecuhh80qz", "name": "var3" }
],
"edges": [
{
"id": "cl26liqj6000g2e6ed2cwkvse",
"to": { "blockId": "cl26lidjz000a2e6etf4v03hv" },
"from": {
"stepId": "cl26li8fj0001iez0bqfraw9h",
"blockId": "cl26li8fj0000iez05x7razkg"
}
}
],
"theme": {
"chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostAvatar": {
"url": "https://avatars.githubusercontent.com/u/16015833?v=4",
"isEnabled": true
},
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
},
"settings": {
"general": {
"isBrandingEnabled": true,
"isInputPrefillEnabled": true,
"isNewResultOnRefreshEnabled": false
},
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
},
"publicId": null,
"customDomain": null
}

View File

@ -6,6 +6,7 @@ import {
SmtpCredentialsData,
Step,
Typebot,
Webhook,
} from 'models'
import { PrismaClient } from 'db'
import { readFileSync } from 'fs'
@ -36,12 +37,13 @@ export const createUser = () =>
},
})
export const createWebhook = (typebotId: string) =>
export const createWebhook = (typebotId: string, webhook?: Partial<Webhook>) =>
prisma.webhook.create({
data: {
id: 'webhook1',
typebotId: typebotId,
typebotId,
method: 'GET',
...webhook,
},
})

View File

@ -42,4 +42,7 @@ test('should send an email', async ({ page }) => {
await expect(
page.locator('text="<baptiste.arnaud95@gmail.com>" >> nth=0')
).toBeVisible()
await page.goto(`http://localhost:3000/typebots/${typebotId}/results`)
await page.click('text="See logs"')
await expect(page.locator('text="Email successfully sent"')).toBeVisible()
})

View File

@ -0,0 +1,45 @@
import test, { expect } from '@playwright/test'
import { createWebhook, importTypebotInDatabase } from '../services/database'
import cuid from 'cuid'
import path from 'path'
import { typebotViewer } from '../services/selectorUtils'
import { HttpMethod } from 'models'
test('should execute webhooks properly', async ({ page }) => {
const typebotId = cuid()
await importTypebotInDatabase(
path.join(__dirname, '../fixtures/typebots/webhook.json'),
{ id: typebotId, publicId: `${typebotId}-public` }
)
await createWebhook(typebotId, {
id: 'success-webhook',
url: 'https://webhook.site/912bafb0-b92f-4be8-ae6a-186b5879a17a',
method: HttpMethod.POST,
})
await createWebhook(typebotId, {
id: 'failed-webhook',
url: 'https://webhook.site/8be94c01-141e-4792-b3c6-cf45137481d6',
method: HttpMethod.POST,
})
await page.goto(`/${typebotId}-public`)
await typebotViewer(page).locator('text=Send success webhook').click()
await page.waitForResponse(
(resp) =>
resp.request().url().includes(`/api/typebots/${typebotId}/blocks`) &&
resp.status() === 200
)
await typebotViewer(page).locator('text=Send failed webhook').click()
await page.waitForResponse(
async (resp) =>
resp.request().url().includes(`/api/typebots/${typebotId}/blocks`) &&
resp.status() === 200 &&
(await resp.json()).statusCode === 500
)
await page.goto(`http://localhost:3000/typebots/${typebotId}/results`)
await page.click('text="See logs"')
await expect(
page.locator('text="Webhook successfuly executed."')
).toBeVisible()
await expect(page.locator('text="Webhook returned an error"')).toBeVisible()
})

View File

@ -15,3 +15,40 @@ const authenticateByToken = async (
const extractBearerToken = (req: NextApiRequest) =>
req.headers['authorization']?.slice(7)
export const saveErrorLog = (
resultId: string | undefined,
message: string,
details?: any
) => saveLog('error', resultId, message, details)
export const saveSuccessLog = (
resultId: string | undefined,
message: string,
details?: any
) => saveLog('success', resultId, message, details)
const saveLog = (
status: 'error' | 'success',
resultId: string | undefined,
message: string,
details?: any
) => {
if (!resultId) return
return prisma.log.create({
data: {
resultId,
status,
description: message,
details: formatDetails(details),
},
})
}
const formatDetails = (details: any) => {
try {
return JSON.stringify(details, null, 2).substring(0, 1000)
} catch {
return details
}
}

View File

@ -14,13 +14,3 @@ export const updateResult = async (resultId: string, result: Partial<Result>) =>
method: 'PATCH',
body: result,
})
export const createLog = (
resultId: string,
log: Omit<Log, 'id' | 'createdAt' | 'resultId'>
) =>
sendRequest<Result>({
url: `/api/typebots/t/results/${resultId}/logs`,
method: 'POST',
body: log,
})

View File

@ -54,7 +54,7 @@ export const ChatBlock = ({
setCurrentTypebotId,
pushEdgeIdInLinkedTypebotQueue,
} = useTypebot()
const { resultValues, updateVariables } = useAnswers()
const { resultValues, updateVariables, resultId } = useAnswers()
const [processedSteps, setProcessedSteps] = useState<Step[]>([])
const [displayedChunks, setDisplayedChunks] = useState<ChatDisplayChunk[]>([])
@ -134,6 +134,7 @@ export const ChatBlock = ({
resultValues,
blocks: typebot.blocks,
onNewLog,
resultId,
},
})
nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()

View File

@ -28,6 +28,7 @@ export type TypebotViewerProps = {
apiHost?: string
style?: CSSProperties
predefinedVariables?: { [key: string]: string | undefined }
resultId?: string
onNewBlockVisible?: (edge: Edge) => void
onNewAnswer?: (answer: Answer) => void
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
@ -40,6 +41,7 @@ export const TypebotViewer = ({
apiHost = process.env.NEXT_PUBLIC_VIEWER_URL?.split(',')[0],
isPreview = false,
style,
resultId,
predefinedVariables,
onNewLog,
onNewBlockVisible,
@ -95,6 +97,7 @@ export const TypebotViewer = ({
onNewLog={handleNewLog}
>
<AnswersContext
resultId={resultId}
onNewAnswer={handleNewAnswer}
onVariablesUpdated={onVariablesUpdated}
>

View File

@ -2,6 +2,7 @@ import { Answer, ResultValues, VariableWithValue } from 'models'
import React, { createContext, ReactNode, useContext, useState } from 'react'
const answersContext = createContext<{
resultId?: string
resultValues: ResultValues
addAnswer: (answer: Answer) => void
updateVariables: (variables: VariableWithValue[]) => void
@ -11,9 +12,11 @@ const answersContext = createContext<{
export const AnswersContext = ({
children,
resultId,
onNewAnswer,
onVariablesUpdated,
}: {
resultId?: string
onNewAnswer: (answer: Answer) => void
onVariablesUpdated?: (variables: VariableWithValue[]) => void
children: ReactNode
@ -45,6 +48,7 @@ export const AnswersContext = ({
return (
<answersContext.Provider
value={{
resultId,
resultValues,
addAnswer,
updateVariables,

View File

@ -33,6 +33,7 @@ type IntegrationContext = {
variables: Variable[]
resultValues: ResultValues
blocks: Block[]
resultId?: string
updateVariables: (variables: VariableWithValue[]) => void
updateVariableValue: (variableId: string, value: string) => void
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
@ -92,7 +93,7 @@ const executeGoogleSheetIntegration = async (
const insertRowInGoogleSheets = async (
options: GoogleSheetsInsertRowOptions,
{ variables, apiHost, onNewLog }: IntegrationContext
{ variables, apiHost, onNewLog, resultId }: IntegrationContext
) => {
if (!options.cellsToInsert) {
onNewLog({
@ -102,8 +103,9 @@ const insertRowInGoogleSheets = async (
})
return
}
const params = stringify({ resultId })
const { error } = await sendRequest({
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${params}`,
method: 'POST',
body: {
credentialsId: options.credentialsId,
@ -121,11 +123,12 @@ const insertRowInGoogleSheets = async (
const updateRowInGoogleSheets = async (
options: GoogleSheetsUpdateRowOptions,
{ variables, apiHost, onNewLog }: IntegrationContext
{ variables, apiHost, onNewLog, resultId }: IntegrationContext
) => {
if (!options.cellsToUpsert || !options.referenceCell) return
const params = stringify({ resultId })
const { error } = await sendRequest({
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${params}`,
method: 'PATCH',
body: {
credentialsId: options.credentialsId,
@ -153,6 +156,7 @@ const getRowFromGoogleSheets = async (
updateVariables,
apiHost,
onNewLog,
resultId,
}: IntegrationContext
) => {
if (!options.referenceCell || !options.cellsToExtract) return
@ -164,6 +168,7 @@ const getRowFromGoogleSheets = async (
value: parseVariables(variables)(options.referenceCell.value ?? ''),
},
columns: options.cellsToExtract.map((cell) => cell.column),
resultId,
},
{ indices: false }
)
@ -222,10 +227,12 @@ const executeWebhook = async (
apiHost,
resultValues,
onNewLog,
resultId,
}: IntegrationContext
) => {
const params = stringify({ resultId })
const { data, error } = await sendRequest({
url: `${apiHost}/api/typebots/${typebotId}/blocks/${blockId}/steps/${stepId}/executeWebhook`,
url: `${apiHost}/api/typebots/${typebotId}/blocks/${blockId}/steps/${stepId}/executeWebhook?${params}`,
method: 'POST',
body: {
variables,
@ -266,7 +273,7 @@ const executeWebhook = async (
const sendEmail = async (
step: SendEmailStep,
{ variables, apiHost, isPreview, onNewLog }: IntegrationContext
{ variables, apiHost, isPreview, onNewLog, resultId }: IntegrationContext
) => {
if (isPreview) {
onNewLog({
@ -279,7 +286,7 @@ const sendEmail = async (
const { options } = step
const replyTo = parseVariables(variables)(options.replyTo)
const { error } = await sendRequest({
url: `${apiHost}/api/integrations/email`,
url: `${apiHost}/api/integrations/email?resultId=${resultId}`,
method: 'POST',
body: {
credentialsId: options.credentialsId,