mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
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:
parent
fc08bd891f
commit
ab068e2ba5
4 changed files with 84 additions and 31 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ?? "/")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue