♻️ Introduce typebot v6 with events (#1013)

Closes #885
This commit is contained in:
Baptiste Arnaud
2023-11-08 15:34:16 +01:00
committed by GitHub
parent 68e4fc71fb
commit 35300eaf34
634 changed files with 58971 additions and 31449 deletions

View File

@@ -1,12 +1,13 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { PublicTypebot } from '@typebot.io/schemas'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
import { parseGroups } from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/lib'
export const getTotalAnswersInBlocks = authenticatedProcedure
export const getTotalAnswers = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
@@ -21,7 +22,7 @@ export const getTotalAnswersInBlocks = authenticatedProcedure
typebotId: z.string(),
})
)
.output(z.object({ totalAnswersInBlocks: z.array(totalAnswersInBlock) }))
.output(z.object({ totalAnswers: z.array(totalAnswersSchema) }))
.query(async ({ input: { typebotId }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
@@ -33,17 +34,17 @@ export const getTotalAnswersInBlocks = authenticatedProcedure
message: 'Published typebot not found',
})
const publishedTypebot = typebot.publishedTypebot as PublicTypebot
const totalAnswersPerBlock = await prisma.answer.groupBy({
by: ['itemId', 'blockId'],
by: ['blockId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
},
blockId: {
in: publishedTypebot.groups.flatMap((group) =>
group.blocks.map((block) => block.id)
in: parseGroups(typebot.publishedTypebot.groups, {
typebotVersion: typebot.publishedTypebot.version,
}).flatMap((group) =>
group.blocks.filter(isInputBlock).map((block) => block.id)
),
},
},
@@ -51,10 +52,9 @@ export const getTotalAnswersInBlocks = authenticatedProcedure
})
return {
totalAnswersInBlocks: totalAnswersPerBlock.map((answer) => ({
blockId: answer.blockId,
itemId: answer.itemId ?? undefined,
total: answer._count._all,
totalAnswers: totalAnswersPerBlock.map((a) => ({
blockId: a.blockId,
total: a._count._all,
})),
}
})

View File

@@ -0,0 +1,55 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalVisitedEdgesSchema } from '@typebot.io/schemas'
export const getTotalVisitedEdges = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/typebots/{typebotId}/analytics/totalVisitedEdges',
protect: true,
summary: 'List total edges used in results',
tags: ['Analytics'],
},
})
.input(
z.object({
typebotId: z.string(),
})
)
.output(
z.object({
totalVisitedEdges: z.array(totalVisitedEdgesSchema),
})
)
.query(async ({ input: { typebotId }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { id: true },
})
if (!typebot?.id)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Published typebot not found',
})
const edges = await prisma.visitedEdge.groupBy({
by: ['edgeId'],
where: {
result: {
typebotId: typebot.id,
},
},
_count: { resultId: true },
})
return {
totalVisitedEdges: edges.map((e) => ({
edgeId: e.edgeId,
total: e._count.resultId,
})),
}
})

View File

@@ -1,6 +1,8 @@
import { router } from '@/helpers/server/trpc'
import { getTotalAnswersInBlocks } from './getTotalAnswersInBlocks'
import { getTotalAnswers } from './getTotalAnswers'
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
export const analyticsRouter = router({
getTotalAnswersInBlocks,
getTotalAnswers,
getTotalVisitedEdges,
})

View File

@@ -15,20 +15,25 @@ import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoor
import { useTranslate } from '@tolgee/react'
import { trpc } from '@/lib/trpc'
import { isDefined } from '@typebot.io/lib'
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure()
const { typebot, publishedTypebot } = useTypebot()
const { data } = trpc.analytics.getTotalAnswersInBlocks.useQuery(
const { data } = trpc.analytics.getTotalAnswers.useQuery(
{
typebotId: typebot?.id as string,
},
{ enabled: isDefined(publishedTypebot) }
)
const { data: edgesData } = trpc.analytics.getTotalVisitedEdges.useQuery(
{
typebotId: typebot?.id as string,
},
{ enabled: isDefined(publishedTypebot) }
)
const startBlockId = publishedTypebot?.groups
.find((group) => group.blocks.at(0)?.type === 'start')
?.blocks.at(0)?.id
return (
<Flex
@@ -44,28 +49,18 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
h="full"
justifyContent="center"
>
{publishedTypebot &&
data?.totalAnswersInBlocks &&
stats &&
startBlockId ? (
{publishedTypebot && stats ? (
<GraphProvider isReadOnly>
<GroupsCoordinatesProvider groups={publishedTypebot?.groups}>
<Graph
flex="1"
typebot={publishedTypebot}
onUnlockProPlanClick={onOpen}
totalAnswersInBlocks={
startBlockId
? [
{
blockId: startBlockId,
total: stats.totalViews,
},
...data.totalAnswersInBlocks,
]
: []
}
/>
<EventsCoordinatesProvider events={publishedTypebot?.events}>
<Graph
flex="1"
typebot={publishedTypebot}
onUnlockProPlanClick={onOpen}
totalAnswers={data?.totalAnswers}
totalVisitedEdges={edgesData?.totalVisitedEdges}
/>
</EventsCoordinatesProvider>
</GroupsCoordinatesProvider>
</GraphProvider>
) : (

View File

@@ -1,96 +0,0 @@
import { isInputBlock } from '@typebot.io/lib'
import { PublicTypebot } from '@typebot.io/schemas'
import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
export const computePreviousTotalAnswers = (
publishedTypebot: PublicTypebot,
currentBlockId: string,
totalAnswersInBlocks: TotalAnswersInBlock[],
visitedBlocks: string[] = []
): number => {
let totalAnswers = 0
const allBlocks = publishedTypebot.groups.flatMap((group) => group.blocks)
const currentGroup = publishedTypebot.groups.find((group) =>
group.blocks.find((block) => block.id === currentBlockId)
)
if (!currentGroup) return 0
const currentBlockIndex = currentGroup.blocks.findIndex(
(block) => block.id === currentBlockId
)
const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1)
for (const block of previousBlocks.reverse()) {
if (visitedBlocks.includes(block.id)) continue
if (
currentBlockId !== block.id &&
(isInputBlock(block) || block.type === 'start')
) {
visitedBlocks.push(block.id)
return (
totalAnswersInBlocks.find(
(totalAnswersInBlock) =>
totalAnswersInBlock.blockId === block.id &&
totalAnswersInBlock.itemId === undefined
)?.total ?? 0
)
}
const connectedEdges = publishedTypebot.edges.filter(
(edge) => edge.to.blockId === block.id
)
if (connectedEdges.length) {
for (const connectedEdge of connectedEdges) {
const connectedBlock = allBlocks.find(
(block) => block.id === connectedEdge.from.blockId
)
if (connectedBlock && !visitedBlocks.includes(connectedBlock.id)) {
if (isInputBlock(connectedBlock) || connectedBlock.type === 'start') {
visitedBlocks.push(connectedBlock.id)
totalAnswers +=
totalAnswersInBlocks.find(
(totalAnswersInBlock) =>
totalAnswersInBlock.blockId === connectedEdge.from.blockId &&
totalAnswersInBlock.itemId === connectedEdge.from.itemId
)?.total ?? 0
} else {
totalAnswers += computePreviousTotalAnswers(
publishedTypebot,
connectedBlock.id,
totalAnswersInBlocks,
visitedBlocks
)
}
}
}
}
}
const edgesConnectedToGroup = publishedTypebot.edges.filter(
(edge) => edge.to.groupId === currentGroup.id
)
if (edgesConnectedToGroup.length) {
for (const connectedEdge of edgesConnectedToGroup) {
const connectedBlock = allBlocks.find(
(block) => block.id === connectedEdge.from.blockId
)
if (connectedBlock && !visitedBlocks.includes(connectedBlock.id)) {
if (isInputBlock(connectedBlock) || connectedBlock.type === 'start') {
visitedBlocks.push(connectedBlock.id)
totalAnswers +=
totalAnswersInBlocks.find(
(totalAnswersInBlock) =>
totalAnswersInBlock.blockId === connectedEdge.from.blockId &&
totalAnswersInBlock.itemId === connectedEdge.from.itemId
)?.total ?? 0
} else {
totalAnswers += computePreviousTotalAnswers(
publishedTypebot,
connectedBlock.id,
totalAnswersInBlocks,
visitedBlocks
)
}
}
}
}
return totalAnswers
}

View File

@@ -0,0 +1,60 @@
import { isInputBlock, isNotDefined } from '@typebot.io/lib'
import { PublicTypebotV6 } from '@typebot.io/schemas'
import {
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas/features/analytics'
export const computeTotalUsersAtBlock = (
currentBlockId: string,
{
publishedTypebot,
totalVisitedEdges,
totalAnswers,
}: {
publishedTypebot: PublicTypebotV6
totalVisitedEdges: TotalVisitedEdges[]
totalAnswers: TotalAnswers[]
}
): number => {
let totalUsers = 0
const currentGroup = publishedTypebot.groups.find((group) =>
group.blocks.find((block) => block.id === currentBlockId)
)
if (!currentGroup) return 0
const currentBlockIndex = currentGroup.blocks.findIndex(
(block) => block.id === currentBlockId
)
const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1)
for (const block of previousBlocks.reverse()) {
if (currentBlockId !== block.id && isInputBlock(block))
return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0
const incomingEdges = publishedTypebot.edges.filter(
(edge) => edge.to.blockId === block.id
)
if (!incomingEdges.length) continue
totalUsers += incomingEdges.reduce(
(acc, incomingEdge) =>
acc +
(totalVisitedEdges.find(
(totalEdge) => totalEdge.edgeId === incomingEdge.id
)?.total ?? 0),
0
)
}
const edgesConnectedToGroup = publishedTypebot.edges.filter(
(edge) =>
edge.to.groupId === currentGroup.id && isNotDefined(edge.to.blockId)
)
totalUsers += edgesConnectedToGroup.reduce(
(acc, connectedEdge) =>
acc +
(totalVisitedEdges.find(
(totalEdge) => totalEdge.edgeId === connectedEdge.id
)?.total ?? 0),
0
)
return totalUsers
}

View File

@@ -0,0 +1,20 @@
import { byId } from '@typebot.io/lib'
import { PublicTypebotV6 } from '@typebot.io/schemas'
import { TotalAnswers } from '@typebot.io/schemas/features/analytics'
export const getTotalAnswersAtBlock = (
currentBlockId: string,
{
publishedTypebot,
totalAnswers,
}: {
publishedTypebot: PublicTypebotV6
totalAnswers: TotalAnswers[]
}
): number => {
const block = publishedTypebot.groups
.flatMap((g) => g.blocks)
.find(byId(currentBlockId))
if (!block) throw new Error(`Block ${currentBlockId} not found`)
return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0
}