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
const newUser = { ...user, ...updates }
setUser(newUser)
saveUser(newUser)
saveUser(updates)
}
const saveUser = useDebouncedCallback(
async (newUser?: Partial<User>) => {
async (updates: Partial<User>) => {
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 })
await refreshUser()
},

View File

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

View File

@ -1,9 +1,9 @@
import { User } from '@typebot.io/prisma'
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({
url: `/api/users/${id}`,
method: 'PUT',
method: 'PATCH',
body: user,
})

View File

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

View File

@ -2,17 +2,19 @@ import prisma from '@typebot.io/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
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 user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res)
const id = req.query.userId as string
if (req.method === 'PUT') {
if (req.method === 'PATCH') {
const data = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as User
) as Partial<User>
const typebots = await prisma.user.update({
where: { id },
data: {
@ -22,6 +24,19 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
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 methodNotAllowed(res)

View File

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

View File

@ -45,7 +45,12 @@ export const trackEvents = async (events: TelemetryEvent[]) => {
client.capture({
distinctId: event.userId,
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,
})
})

View File

@ -51,6 +51,7 @@ model User {
image String? @db.VarChar(1000)
company String?
onboardingCategories Json
referral String?
graphNavigation GraphNavigation?
preferredAppAppearance String?
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?
company String?
onboardingCategories Json
referral String?
graphNavigation GraphNavigation?
preferredAppAppearance String?
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(
z.object({
name: z.literal('Typebot created'),
@ -129,6 +141,7 @@ export const eventSchema = z.discriminatedUnion('name', [
subscriptionAutoUpdatedEventSchema,
workspacePastDueEventSchema,
workspaceNotPastDueEventSchema,
userUpdatedEventSchema,
])
export type TelemetryEvent = z.infer<typeof eventSchema>

View File

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