📈 Send onboarding replies to PostHog
This commit is contained in:
@ -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()
|
||||
},
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
@ -51,6 +51,7 @@ model User {
|
||||
image String? @db.VarChar(1000)
|
||||
company String?
|
||||
onboardingCategories Json
|
||||
referral String?
|
||||
graphNavigation GraphNavigation?
|
||||
preferredAppAppearance String?
|
||||
accounts Account[]
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "referral" TEXT;
|
@ -47,6 +47,7 @@ model User {
|
||||
image String?
|
||||
company String?
|
||||
onboardingCategories Json
|
||||
referral String?
|
||||
graphNavigation GraphNavigation?
|
||||
preferredAppAppearance String?
|
||||
accounts Account[]
|
||||
|
@ -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>
|
||||
|
@ -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(),
|
||||
|
Reference in New Issue
Block a user