📈 Send onboarding replies to PostHog
This commit is contained in:
@@ -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()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -306,9 +306,9 @@ 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>
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "referral" TEXT;
|
||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user