fix(web): restore Nova onboarding analytics events (#1023)

The onboarding rework moved the flow to app/(app)/onboarding and dropped the funnel tracking, so onboarding_completed stopped firing on 2026-05-02.

- Wire onboarding_step_viewed across idle/processing/done/error transitions
- Fire onboarding_completed on real completion (status=done) with source + memories_count
- onboarding_profile_submitted now carries { source }
- Add onboarding_skipped { from_step }
- Remove dead onboardingCompleted() from InitialHeader and unused name/relatable helpers
This commit is contained in:
MaheshtheDev 2026-05-29 19:54:13 +00:00
parent fc08bd891f
commit ab068e2ba5
4 changed files with 84 additions and 31 deletions

View file

@ -41,7 +41,7 @@ import {
CheckCircle2,
Loader2,
} from "lucide-react"
import { analytics } from "@/lib/analytics"
import { analytics, type OnboardingStep } from "@/lib/analytics"
import { consumePendingConnectUrl } from "@/lib/constants"
type DetectedSource = "x" | "linkedin" | "resume" | null
@ -378,6 +378,13 @@ function isAccountSource(source: DetectedSource): source is "x" | "linkedin" {
return source === "x" || source === "linkedin"
}
const STATUS_TO_STEP: Record<Status, OnboardingStep> = {
idle: "profile_input",
processing: "processing",
done: "done",
error: "error",
}
function useSpotlightAutoRotation(
status: Status,
pauseSpotlight: boolean,
@ -555,6 +562,7 @@ export default function OnboardingPage() {
const fileRef = useRef<HTMLInputElement>(null)
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
const skippingRef = useRef(false)
const completedTrackedRef = useRef(false)
const [isSkipping, setIsSkipping] = useState(false)
const [spotlightCategory, setSpotlightCategory] =
useState<SpotlightCategoryId>("productivity")
@ -591,6 +599,21 @@ export default function OnboardingPage() {
usePollingCleanup(pollingRef)
useDoneAnimation(status, setStampLanded, setVisibleSnippets)
// biome-ignore lint/correctness/useExhaustiveDependencies: fire per status transition only
useEffect(() => {
analytics.onboardingStepViewed({
step: STATUS_TO_STEP[status],
trigger: "auto",
})
if (status === "done" && !completedTrackedRef.current) {
completedTrackedRef.current = true
analytics.onboardingCompleted({
source: isAccountSource(detected) ? detected : undefined,
memories_count: memoriesCount,
})
}
}, [status])
const handleChange = (v: string) => {
setValue(v)
setDetected(detectSource(v))
@ -619,6 +642,7 @@ export default function OnboardingPage() {
if (skippingRef.current) return
skippingRef.current = true
setIsSkipping(true)
analytics.onboardingSkipped({ from_step: STATUS_TO_STEP[status] })
try {
await ensureOrg()
const pendingPath = consumePendingConnectUrl()
@ -628,7 +652,7 @@ export default function OnboardingPage() {
skippingRef.current = false
setIsSkipping(false)
}
}, [ensureOrg, router])
}, [ensureOrg, router, status])
const pollDocument = useCallback((docId: string) => {
const maxAttempts = 60
@ -688,6 +712,7 @@ export default function OnboardingPage() {
const handleSubmit = useCallback(
async (source: "x" | "linkedin" | "resume", resumeFileOverride?: File) => {
analytics.onboardingProfileSubmitted({ source })
setStatus("processing")
setSpotlightCategory("productivity")
setPauseSpotlight(false)

View file

@ -4,7 +4,6 @@ import { Logo } from "@ui/assets/Logo"
import { Button } from "@ui/components/button"
import { useRouter } from "next/navigation"
import { useOrgOnboarding } from "@hooks/use-org-onboarding"
import { analytics } from "@/lib/analytics"
import { consumePendingConnectUrl } from "@/lib/constants"
import { cn } from "@lib/utils"
@ -23,7 +22,6 @@ export function InitialHeader({
const handleSkip = () => {
markOrgOnboarded()
analytics.onboardingCompleted()
const pendingPath = consumePendingConnectUrl()
router.push(pendingPath ?? "/")
}

View file

@ -1135,7 +1135,10 @@ export function IntegrationsView() {
}
throw new Error(response.error?.message || "Failed to connect")
},
onMutate: (provider) => setConnectingProvider(provider),
onMutate: (provider) => {
setConnectingProvider(provider)
analytics.connectionAuthStarted({ provider })
},
onError: (err) => {
setConnectingProvider(null)
toast.error("Failed to connect", {
@ -1349,6 +1352,13 @@ export function IntegrationsView() {
return true
})
const trackCard = (item: Item) =>
analytics.integrationCardClicked({
kind: item.kind,
id: item.id,
name: item.name,
})
const renderRight = (item: Item): ReactNode => {
switch (item.kind) {
case "plugin": {
@ -1370,7 +1380,10 @@ export function IntegrationsView() {
const busy = connectingPlugin === item.pluginId
return (
<PillButton
onClick={() => createPluginKeyMutation.mutate(item.pluginId)}
onClick={() => {
trackCard(item)
createPluginKeyMutation.mutate(item.pluginId)
}}
disabled={!!connectingPlugin}
>
{busy ? (
@ -1397,7 +1410,10 @@ export function IntegrationsView() {
const busy = connectingProvider === item.provider
return (
<PillButton
onClick={() => addConnectionMutation.mutate(item.provider)}
onClick={() => {
trackCard(item)
addConnectionMutation.mutate(item.provider)
}}
disabled={!!connectingProvider}
>
{busy ? (
@ -1415,6 +1431,7 @@ export function IntegrationsView() {
return (
<PillButton
onClick={() => {
trackCard(item)
window.open(
(item.action as { type: "external"; href: string }).href,
"_blank",
@ -1431,12 +1448,13 @@ export function IntegrationsView() {
}
return (
<PillButton
onClick={() =>
onClick={() => {
trackCard(item)
setViewMode(
(item.action as { type: "view"; viewMode: ViewParamValue })
.viewMode,
)
}
}}
>
Connect
</PillButton>
@ -1444,13 +1462,23 @@ export function IntegrationsView() {
}
case "mcp-client":
return (
<PillButton onClick={() => openMcpClient(item.clientKey)}>
<PillButton
onClick={() => {
trackCard(item)
openMcpClient(item.clientKey)
}}
>
Connect
</PillButton>
)
case "import":
return (
<PillButton onClick={() => setViewMode(item.viewMode)}>
<PillButton
onClick={() => {
trackCard(item)
setViewMode(item.viewMode)
}}
>
Connect
</PillButton>
)

View file

@ -1,5 +1,8 @@
import posthog from "posthog-js"
export type OnboardingStep = "profile_input" | "processing" | "done" | "error"
export type OnboardingSource = "x" | "linkedin" | "resume"
// Helper function to safely capture events
const safeCapture = (
eventName: string,
@ -40,12 +43,13 @@ export const analytics = {
upgradeCompleted: () => safeCapture("upgrade_completed"),
billingPortalOpened: () => safeCapture("billing_portal_opened"),
connectionAdded: (provider: string) =>
safeCapture("connection_added", { provider }),
connectionDeleted: () => safeCapture("connection_deleted"),
connectionAuthStarted: () => safeCapture("connection_auth_started"),
connectionAuthCompleted: () => safeCapture("connection_auth_completed"),
connectionAuthFailed: () => safeCapture("connection_auth_failed"),
connectionAuthStarted: (props: { provider: string }) =>
safeCapture("connection_auth_started", props),
// integrations surface (main Nova page)
integrationCardClicked: (props: { kind: string; id: string; name: string }) =>
safeCapture("integration_card_clicked", props),
nextAppResearchCtaDismissed: () =>
safeCapture("next_app_research_cta_dismissed"),
@ -72,21 +76,13 @@ export const analytics = {
addDocumentModalOpened: () => safeCapture("add_document_modal_opened"),
// onboarding analytics
onboardingStepViewed: (props: { step: string; trigger: "user" | "auto" }) =>
safeCapture("onboarding_step_viewed", props),
onboardingStepViewed: (props: {
step: OnboardingStep
trigger: "user" | "auto"
}) => safeCapture("onboarding_step_viewed", props),
onboardingNameSubmitted: (props: { name_length: number }) =>
safeCapture("onboarding_name_submitted", props),
onboardingProfileSubmitted: (props: {
has_twitter: boolean
has_linkedin: boolean
other_links_count: number
description_length: number
}) => safeCapture("onboarding_profile_submitted", props),
onboardingRelatableSelected: (props: { options: string[] }) =>
safeCapture("onboarding_relatable_selected", props),
onboardingProfileSubmitted: (props: { source: OnboardingSource }) =>
safeCapture("onboarding_profile_submitted", props),
onboardingIntegrationClicked: (props: { integration: string }) =>
safeCapture("onboarding_integration_clicked", props),
@ -100,7 +96,13 @@ export const analytics = {
onboardingXBookmarksDetailOpened: () =>
safeCapture("onboarding_x_bookmarks_detail_opened"),
onboardingCompleted: () => safeCapture("onboarding_completed"),
onboardingSkipped: (props: { from_step: OnboardingStep }) =>
safeCapture("onboarding_skipped", props),
onboardingCompleted: (props?: {
source?: OnboardingSource
memories_count?: number
}) => safeCapture("onboarding_completed", props),
// main app analytics
searchOpened: (props: {