From ab068e2ba57867733702cd66086ce52f1563125b Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Fri, 29 May 2026 19:54:13 +0000 Subject: [PATCH] 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 --- apps/web/app/(app)/onboarding/page.tsx | 29 ++++++++++++++-- apps/web/components/initial-header.tsx | 2 -- apps/web/components/integrations-view.tsx | 42 +++++++++++++++++++---- apps/web/lib/analytics.ts | 42 ++++++++++++----------- 4 files changed, 84 insertions(+), 31 deletions(-) diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index 2c5eaf07..3d67b3ba 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -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 = { + 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(null) const pollingRef = useRef | null>(null) const skippingRef = useRef(false) + const completedTrackedRef = useRef(false) const [isSkipping, setIsSkipping] = useState(false) const [spotlightCategory, setSpotlightCategory] = useState("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) diff --git a/apps/web/components/initial-header.tsx b/apps/web/components/initial-header.tsx index 831fb77c..95bc9628 100644 --- a/apps/web/components/initial-header.tsx +++ b/apps/web/components/initial-header.tsx @@ -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 ?? "/") } diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index e602f070..5a5a7161 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -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 ( 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 ( addConnectionMutation.mutate(item.provider)} + onClick={() => { + trackCard(item) + addConnectionMutation.mutate(item.provider) + }} disabled={!!connectingProvider} > {busy ? ( @@ -1415,6 +1431,7 @@ export function IntegrationsView() { return ( { + trackCard(item) window.open( (item.action as { type: "external"; href: string }).href, "_blank", @@ -1431,12 +1448,13 @@ export function IntegrationsView() { } return ( + onClick={() => { + trackCard(item) setViewMode( (item.action as { type: "view"; viewMode: ViewParamValue }) .viewMode, ) - } + }} > Connect @@ -1444,13 +1462,23 @@ export function IntegrationsView() { } case "mcp-client": return ( - openMcpClient(item.clientKey)}> + { + trackCard(item) + openMcpClient(item.clientKey) + }} + > Connect ) case "import": return ( - setViewMode(item.viewMode)}> + { + trackCard(item) + setViewMode(item.viewMode) + }} + > Connect ) diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index d762654e..132ebeb3 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -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: {