2
0

📈 Send onboarding replies to PostHog

This commit is contained in:
Baptiste Arnaud
2024-02-02 11:58:32 +01:00
parent ce79e897a7
commit fd4867f3ae
12 changed files with 99 additions and 36 deletions

View File

@@ -85,13 +85,13 @@ export const UserProvider = ({ children }: { children: ReactNode }) => {
if (isNotDefined(user)) return if (isNotDefined(user)) return
const newUser = { ...user, ...updates } const newUser = { ...user, ...updates }
setUser(newUser) setUser(newUser)
saveUser(newUser) saveUser(updates)
} }
const saveUser = useDebouncedCallback( const saveUser = useDebouncedCallback(
async (newUser?: Partial<User>) => { async (updates: Partial<User>) => {
if (isNotDefined(user)) return if (isNotDefined(user)) return
const { error } = await updateUserQuery(user.id, { ...user, ...newUser }) const { error } = await updateUserQuery(user.id, updates)
if (error) showToast({ title: error.name, description: error.message }) if (error) showToast({ title: error.name, description: error.message })
await refreshUser() await refreshUser()
}, },

View File

@@ -1,7 +1,6 @@
import { import {
Stack, Stack,
Heading, Heading,
useColorMode,
Menu, Menu,
MenuButton, MenuButton,
MenuList, MenuList,
@@ -34,7 +33,6 @@ export const UserPreferencesForm = () => {
const { getLanguage } = useTolgee() const { getLanguage } = useTolgee()
const router = useRouter() const router = useRouter()
const { t } = useTranslate() const { t } = useTranslate()
const { colorMode } = useColorMode()
const { user, updateUser } = useUser() const { user, updateUser } = useUser()
useEffect(() => { useEffect(() => {
@@ -117,7 +115,7 @@ export const UserPreferencesForm = () => {
defaultValue={ defaultValue={
user?.preferredAppAppearance user?.preferredAppAppearance
? user.preferredAppAppearance ? user.preferredAppAppearance
: colorMode : 'system'
} }
onChange={changeAppearance} onChange={changeAppearance}
/> />

View File

@@ -1,9 +1,9 @@
import { User } from '@typebot.io/prisma'
import { sendRequest } from '@typebot.io/lib' import { sendRequest } from '@typebot.io/lib'
import { User } from '@typebot.io/schemas'
export const updateUserQuery = async (id: string, user: User) => export const updateUserQuery = async (id: string, user: Partial<User>) =>
sendRequest({ sendRequest({
url: `/api/users/${id}`, url: `/api/users/${id}`,
method: 'PUT', method: 'PATCH',
body: user, body: user,
}) })

View File

@@ -25,6 +25,12 @@ export const OnboardingPage = () => {
const confettiCanon = useRef<confetti.CreateTypes>() const confettiCanon = useRef<confetti.CreateTypes>()
const { user, updateUser } = useUser() const { user, updateUser } = useUser()
const [currentStep, setCurrentStep] = useState<number>(user?.name ? 2 : 1) const [currentStep, setCurrentStep] = useState<number>(user?.name ? 2 : 1)
const [onboardingReplies, setOnboardingReplies] = useState<{
name?: string
company?: string
onboardingCategories?: string[]
referral?: string
}>({})
const isNewUser = const isNewUser =
user && user &&
@@ -49,30 +55,58 @@ export const OnboardingPage = () => {
}) })
} }
const updateUserInfo = async (answer: { const setOnboardingAnswer = async (answer: {
message: string message: string
blockId: string blockId: string
}) => { }) => {
const isOtherItem = [
'cl126pv7n001o2e6dajltc4qz',
'saw904bfzgspmt0l24achtiy',
].includes(answer.blockId)
if (isOtherItem) return
const isName = answer.blockId === 'cl126820m000g2e6dfleq78bt' const isName = answer.blockId === 'cl126820m000g2e6dfleq78bt'
const isCompany = answer.blockId === 'cl126jioz000v2e6dwrk1f2cb' const isCompany = answer.blockId === 'cl126jioz000v2e6dwrk1f2cb'
const isCategories = answer.blockId === 'cl126lb8v00142e6duv5qe08l' const isCategories = answer.blockId === 'cl126lb8v00142e6duv5qe08l'
if (isName) updateUser({ name: answer.message }) const isOtherCategory = answer.blockId === 'cl126pv7n001o2e6dajltc4qz'
const isReferral = answer.blockId === 'phcb0s1e9qgp0f8l2amcu7xr'
const isOtherReferral = answer.blockId === 'saw904bfzgspmt0l24achtiy'
if (isName)
setOnboardingReplies((prev) => ({ ...prev, name: answer.message }))
if (isCategories) { if (isCategories) {
const onboardingCategories = answer.message.split(', ') const onboardingCategories = answer.message.split(', ')
updateUser({ onboardingCategories }) setOnboardingReplies((prev) => ({
...prev,
onboardingCategories,
}))
} }
if (isOtherCategory)
setOnboardingReplies((prev) => ({
...prev,
categories: prev.onboardingCategories
? [...prev.onboardingCategories, answer.message]
: [answer.message],
}))
if (isCompany) { if (isCompany) {
updateUser({ company: answer.message }) setOnboardingReplies((prev) => ({ ...prev, company: answer.message }))
if (confettiCanon.current) shootConfettis(confettiCanon.current) if (confettiCanon.current) shootConfettis(confettiCanon.current)
} }
if (isReferral)
setOnboardingReplies((prev) => ({ ...prev, referral: answer.message }))
if (isOtherReferral)
setOnboardingReplies((prev) => ({ ...prev, referral: answer.message }))
setCurrentStep((prev) => prev + 1) setCurrentStep((prev) => prev + 1)
} }
const skipOnboarding = () => {
updateUser(onboardingReplies)
replace({ pathname: '/typebots', query })
}
const updateUserAndProceedToTypebotCreation = () => {
updateUser(onboardingReplies)
setTimeout(() => {
replace({
pathname: '/typebots',
query: { ...query, isFirstBot: true },
})
}, 2000)
}
if (!isNewUser) return null if (!isNewUser) return null
return ( return (
<VStack h="100vh" flexDir="column" justifyContent="center" spacing="10"> <VStack h="100vh" flexDir="column" justifyContent="center" spacing="10">
@@ -83,7 +117,7 @@ export const OnboardingPage = () => {
right="5" right="5"
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => replace({ pathname: '/typebots', query })} onClick={skipOnboarding}
> >
{t('skip')} {t('skip')}
</Button> </Button>
@@ -93,15 +127,8 @@ export const OnboardingPage = () => {
typebot={env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID} typebot={env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID}
style={{ borderRadius: '1rem' }} style={{ borderRadius: '1rem' }}
prefilledVariables={{ Name: user?.name, Email: user?.email }} prefilledVariables={{ Name: user?.name, Email: user?.email }}
onEnd={() => { onEnd={updateUserAndProceedToTypebotCreation}
setTimeout(() => { onAnswer={setOnboardingAnswer}
replace({
pathname: '/typebots',
query: { ...query, isFirstBot: true },
})
}, 2000)
}}
onAnswer={updateUserInfo}
/> />
</Flex> </Flex>
<chakra.canvas <chakra.canvas

View File

@@ -2,17 +2,19 @@ import prisma from '@typebot.io/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api' import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
import { Prisma, User } from '@typebot.io/prisma' import { User } from '@typebot.io/schemas'
import { trackEvents } from '@typebot.io/lib/telemetry/trackEvents'
import { Prisma } from '@typebot.io/prisma'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const user = await getAuthenticatedUser(req, res) const user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res) if (!user) return notAuthenticated(res)
const id = req.query.userId as string const id = req.query.userId as string
if (req.method === 'PUT') { if (req.method === 'PATCH') {
const data = ( const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as User ) as Partial<User>
const typebots = await prisma.user.update({ const typebots = await prisma.user.update({
where: { id }, where: { id },
data: { data: {
@@ -22,6 +24,19 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
data.displayedInAppNotifications ?? Prisma.DbNull, data.displayedInAppNotifications ?? Prisma.DbNull,
}, },
}) })
if (data.onboardingCategories || data.referral || data.company || data.name)
await trackEvents([
{
name: 'User updated',
userId: user.id,
data: {
name: data.name ?? undefined,
onboardingCategories: data.onboardingCategories ?? undefined,
referral: data.referral ?? undefined,
company: data.company ?? undefined,
},
},
])
return res.send({ typebots }) return res.send({ typebots })
} }
return methodNotAllowed(res) return methodNotAllowed(res)

View File

@@ -307,8 +307,8 @@ The related environment variables are listed here but you are probably not inter
<Accordion title="PostHog"> <Accordion title="PostHog">
| Parameter | Default | Description | | Parameter | Default | Description |
| ---------------------------- | ----------------------- | ---------------- | | ------------------------ | ----------------------- | ---------------- |
| NEXT_PUBLIC_POSTHOG_API_KEY | | PostHog API Key | | NEXT_PUBLIC_POSTHOG_KEY | | PostHog API Key |
| NEXT_PUBLIC_POSTHOG_API_HOST | https://app.posthog.com | PostHog API Host | | NEXT_PUBLIC_POSTHOG_HOST | https://app.posthog.com | PostHog API Host |
</Accordion> </Accordion>

View File

@@ -45,7 +45,12 @@ export const trackEvents = async (events: TelemetryEvent[]) => {
client.capture({ client.capture({
distinctId: event.userId, distinctId: event.userId,
event: event.name, event: event.name,
properties: 'data' in event ? event.data : undefined, properties:
event.name === 'User updated'
? { $set: event.data }
: 'data' in event
? event.data
: undefined,
groups, groups,
}) })
}) })

View File

@@ -51,6 +51,7 @@ model User {
image String? @db.VarChar(1000) image String? @db.VarChar(1000)
company String? company String?
onboardingCategories Json onboardingCategories Json
referral String?
graphNavigation GraphNavigation? graphNavigation GraphNavigation?
preferredAppAppearance String? preferredAppAppearance String?
accounts Account[] accounts Account[]

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "referral" TEXT;

View File

@@ -47,6 +47,7 @@ model User {
image String? image String?
company String? company String?
onboardingCategories Json onboardingCategories Json
referral String?
graphNavigation GraphNavigation? graphNavigation GraphNavigation?
preferredAppAppearance String? preferredAppAppearance String?
accounts Account[] accounts Account[]

View File

@@ -37,6 +37,18 @@ const userCreatedEventSchema = userEvent.merge(
}) })
) )
const userUpdatedEventSchema = userEvent.merge(
z.object({
name: z.literal('User updated'),
data: z.object({
name: z.string().optional(),
onboardingCategories: z.array(z.string()).optional(),
referral: z.string().optional(),
company: z.string().optional(),
}),
})
)
const typebotCreatedEventSchema = typebotEvent.merge( const typebotCreatedEventSchema = typebotEvent.merge(
z.object({ z.object({
name: z.literal('Typebot created'), name: z.literal('Typebot created'),
@@ -129,6 +141,7 @@ export const eventSchema = z.discriminatedUnion('name', [
subscriptionAutoUpdatedEventSchema, subscriptionAutoUpdatedEventSchema,
workspacePastDueEventSchema, workspacePastDueEventSchema,
workspaceNotPastDueEventSchema, workspaceNotPastDueEventSchema,
userUpdatedEventSchema,
]) ])
export type TelemetryEvent = z.infer<typeof eventSchema> export type TelemetryEvent = z.infer<typeof eventSchema>

View File

@@ -14,6 +14,7 @@ export const userSchema = z.object({
image: z.string().nullable(), image: z.string().nullable(),
company: z.string().nullable(), company: z.string().nullable(),
onboardingCategories: z.array(z.string()), onboardingCategories: z.array(z.string()),
referral: z.string().nullable(),
graphNavigation: z.nativeEnum(GraphNavigation), graphNavigation: z.nativeEnum(GraphNavigation),
preferredAppAppearance: z.string().nullable(), preferredAppAppearance: z.string().nullable(),
displayedInAppNotifications: displayedInAppNotificationsSchema.nullable(), displayedInAppNotifications: displayedInAppNotificationsSchema.nullable(),