feat: added advanced analytics events (#702)

added advanced analytics events
This commit is contained in:
MaheshtheDev 2026-01-25 00:56:19 +00:00
parent e7ea26442a
commit 134861de3d
18 changed files with 293 additions and 39 deletions

View file

@ -1,8 +1,16 @@
"use client"
import { createContext, useContext, useCallback, type ReactNode } from "react"
import {
createContext,
useContext,
useCallback,
useEffect,
useRef,
type ReactNode,
} from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useOnboardingContext, type MemoryFormData } from "../layout"
import { analytics } from "@/lib/analytics"
export const SETUP_STEPS = ["relatable", "integrations"] as const
export type SetupStep = (typeof SETUP_STEPS)[number]
@ -34,9 +42,11 @@ export default function SetupLayout({ children }: { children: ReactNode }) {
const currentStep: SetupStep = SETUP_STEPS.includes(stepParam as SetupStep)
? (stepParam as SetupStep)
: "relatable"
const hasTrackedInitialStep = useRef(false)
const goToStep = useCallback(
(step: SetupStep) => {
analytics.onboardingStepViewed({ step, trigger: "user" })
router.push(`/new/onboarding/setup?step=${step}`)
},
[router],
@ -54,6 +64,13 @@ export default function SetupLayout({ children }: { children: ReactNode }) {
router.push("/new")
}, [router, resetOnboarding])
useEffect(() => {
if (!hasTrackedInitialStep.current) {
analytics.onboardingStepViewed({ step: currentStep, trigger: "user" })
hasTrackedInitialStep.current = true
}
}, [currentStep])
const contextValue: SetupContextValue = {
memoryFormData,
currentStep,

View file

@ -11,6 +11,7 @@ import {
} from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { useOnboardingContext, type MemoryFormData } from "../layout"
import { analytics } from "@/lib/analytics"
export const WELCOME_STEPS = [
"input",
@ -61,6 +62,7 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
const [isSubmitting, setIsSubmitting] = useState(false)
const [showWelcomeContent, setShowWelcomeContent] = useState(false)
const isMountedRef = useRef(true)
const hasTrackedInitialStep = useRef(false)
useEffect(() => {
isMountedRef.current = true
@ -89,6 +91,7 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
timers.push(
setTimeout(() => {
if (isMountedRef.current) {
analytics.onboardingStepViewed({ step: "welcome", trigger: "auto" })
router.replace("/new/onboarding/welcome?step=welcome")
}
}, 2000),
@ -97,6 +100,10 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
timers.push(
setTimeout(() => {
if (isMountedRef.current) {
analytics.onboardingStepViewed({
step: "username",
trigger: "auto",
})
router.replace("/new/onboarding/welcome?step=username")
}
}, 2000),
@ -108,8 +115,19 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) {
}
}, [currentStep, router])
useEffect(() => {
if (!hasTrackedInitialStep.current) {
analytics.onboardingStepViewed({
step: currentStep,
trigger: "user",
})
hasTrackedInitialStep.current = true
}
}, [currentStep])
const goToStep = useCallback(
(step: WelcomeStep) => {
analytics.onboardingStepViewed({ step, trigger: "user" })
router.push(`/new/onboarding/welcome?step=${step}`)
},
[router],

View file

@ -19,6 +19,7 @@ import {
} from "./layout"
import { gapVariants, orbVariants } from "@/lib/variants"
import { authClient } from "@lib/auth"
import { analytics } from "@/lib/analytics"
function UserSupermemory({ name }: { name: string }) {
return (
@ -88,6 +89,7 @@ export default function WelcomePage() {
console.error("Failed to update displayUsername:", error)
}
analytics.onboardingNameSubmitted({ name_length: name.trim().length })
goToStep("greeting")
setIsSubmitting(false)
}

View file

@ -16,7 +16,10 @@ import { useHotkeys } from "react-hotkeys-hook"
import { AnimatePresence } from "motion/react"
import { useIsMobile } from "@hooks/use-mobile"
import { useProject } from "@/stores"
import { useQuickNoteDraftReset } from "@/stores/quick-note-draft"
import {
useQuickNoteDraftReset,
useQuickNoteDraft,
} from "@/stores/quick-note-draft"
import { analytics } from "@/lib/analytics"
import { useDocumentMutations } from "@/hooks/use-document-mutations"
import { useQuery } from "@tanstack/react-query"
@ -42,6 +45,7 @@ export default function NewPage() {
const [searchPrefill, setSearchPrefill] = useState("")
const resetDraft = useQuickNoteDraftReset(selectedProject)
const { draft: quickNoteDraft } = useQuickNoteDraft(selectedProject || "")
const { noteMutation } = useDocumentMutations({
onClose: () => {
@ -92,11 +96,15 @@ export default function NewPage() {
})
useHotkeys("mod+k", (e) => {
e.preventDefault()
analytics.searchOpened({ source: "hotkey" })
setIsSearchOpen(true)
})
const [isChatOpen, setIsChatOpen] = useState(!isMobile)
const handleOpenDocument = useCallback((document: DocumentWithMemories) => {
if (document.id) {
analytics.documentModalOpened({ document_id: document.id })
}
setSelectedDocument(document)
setIsDocumentModalOpen(true)
}, [])
@ -104,28 +112,50 @@ export default function NewPage() {
const handleQuickNoteSave = useCallback(
(content: string) => {
if (content.trim()) {
noteMutation.mutate({ content, project: selectedProject })
const hadPreviousContent = quickNoteDraft.trim().length > 0
noteMutation.mutate(
{ content, project: selectedProject },
{
onSuccess: () => {
if (hadPreviousContent) {
analytics.quickNoteEdited()
} else {
analytics.quickNoteCreated()
}
},
},
)
}
},
[selectedProject, noteMutation],
[selectedProject, noteMutation, quickNoteDraft],
)
const handleFullScreenSave = useCallback(
(content: string) => {
if (content.trim()) {
noteMutation.mutate({ content, project: selectedProject })
const hadInitialContent = fullscreenInitialContent.trim().length > 0
noteMutation.mutate(
{ content, project: selectedProject },
{
onSuccess: () => {
if (hadInitialContent) {
analytics.quickNoteEdited()
} else {
analytics.quickNoteCreated()
}
},
},
)
}
},
[selectedProject, noteMutation],
[selectedProject, noteMutation, fullscreenInitialContent],
)
const handleMaximize = useCallback(
(content: string) => {
setFullscreenInitialContent(content)
setIsFullScreenNoteOpen(true)
},
[],
)
const handleMaximize = useCallback((content: string) => {
analytics.fullscreenNoteModalOpened()
setFullscreenInitialContent(content)
setIsFullScreenNoteOpen(true)
}, [])
const handleHighlightsChat = useCallback((seed: string) => {
setQueuedChatSeed(seed)
@ -133,6 +163,7 @@ export default function NewPage() {
}, [])
const handleHighlightsShowRelated = useCallback((query: string) => {
analytics.searchOpened({ source: "highlight_related" })
setSearchPrefill(query)
setIsSearchOpen(true)
}, [])
@ -154,7 +185,10 @@ export default function NewPage() {
setIsMCPModalOpen(true)
}}
onOpenChat={() => setIsChatOpen(true)}
onOpenSearch={() => setIsSearchOpen(true)}
onOpenSearch={() => {
analytics.searchOpened({ source: "header" })
setIsSearchOpen(true)
}}
/>
<main
key={`main-container-${isChatOpen}`}

View file

@ -13,6 +13,7 @@ import ConnectionsMCP from "@/components/new/settings/connections-mcp"
import Support from "@/components/new/settings/support"
import { useRouter } from "next/navigation"
import { useIsMobile } from "@hooks/use-mobile"
import { analytics } from "@/lib/analytics"
const TABS = ["account", "integrations", "connections", "support"] as const
type SettingsTab = (typeof TABS)[number]
@ -158,6 +159,7 @@ export default function SettingsPage() {
const hash = window.location.hash
const tab = parseHashToTab(hash)
setActiveTab(tab)
analytics.settingsTabChanged({ tab })
// If no hash or invalid hash, push #account
if (!hash || !TABS.includes(hash.replace("#", "") as SettingsTab)) {
@ -169,6 +171,7 @@ export default function SettingsPage() {
const handleHashChange = () => {
const tab = parseHashToTab(window.location.hash)
setActiveTab(tab)
analytics.settingsTabChanged({ tab })
}
window.addEventListener("hashchange", handleHashChange)
@ -230,6 +233,7 @@ export default function SettingsPage() {
onClick={() => {
window.location.hash = item.id
setActiveTab(item.id)
analytics.settingsTabChanged({ tab: item.id })
}}
className={cn(
"rounded-xl transition-colors flex items-start gap-3 shrink-0",

View file

@ -9,6 +9,7 @@ import { XIcon, Loader2 } from "lucide-react"
import { Button } from "@ui/components/button"
import { useProjectMutations } from "@/hooks/use-project-mutations"
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
import { analytics } from "@/lib/analytics"
const EMOJI_LIST = [
"📁",
@ -87,6 +88,7 @@ export function AddSpaceModal({
{ name: trimmedName, emoji: emoji || undefined },
{
onSuccess: () => {
analytics.spaceCreated()
handleClose()
},
},

View file

@ -362,6 +362,7 @@ export function ChatSidebar({
const handleSend = () => {
if (!input.trim() || status === "submitted" || status === "streaming")
return
analytics.chatMessageSent({ source: "typed" })
sendMessage({ text: input })
setInput("")
scrollToBottom()
@ -379,27 +380,50 @@ export function ChatSidebar({
}
const handleCopyMessage = useCallback((messageId: string, text: string) => {
analytics.chatMessageCopied({ message_id: messageId })
navigator.clipboard.writeText(text)
setCopiedMessageId(messageId)
setTimeout(() => setCopiedMessageId(null), 2000)
}, [])
const handleLikeMessage = useCallback((messageId: string) => {
setMessageFeedback((prev) => ({
...prev,
[messageId]: prev[messageId] === "like" ? null : "like",
}))
}, [])
const handleLikeMessage = useCallback(
(messageId: string) => {
const wasLiked = messageFeedback[messageId] === "like"
setMessageFeedback((prev) => ({
...prev,
[messageId]: prev[messageId] === "like" ? null : "like",
}))
if (!wasLiked) {
analytics.chatMessageLiked({ message_id: messageId })
}
},
[messageFeedback],
)
const handleDislikeMessage = useCallback((messageId: string) => {
setMessageFeedback((prev) => ({
...prev,
[messageId]: prev[messageId] === "dislike" ? null : "dislike",
}))
}, [])
const handleDislikeMessage = useCallback(
(messageId: string) => {
const wasDisliked = messageFeedback[messageId] === "dislike"
setMessageFeedback((prev) => ({
...prev,
[messageId]: prev[messageId] === "dislike" ? null : "dislike",
}))
if (!wasDisliked) {
analytics.chatMessageDisliked({ message_id: messageId })
}
},
[messageFeedback],
)
const handleToggleMemories = useCallback((messageId: string) => {
setExpandedMemories((prev) => (prev === messageId ? null : messageId))
setExpandedMemories((prev) => {
const isExpanding = prev !== messageId
if (isExpanding) {
analytics.chatMemoryExpanded({ message_id: messageId })
} else {
analytics.chatMemoryCollapsed({ message_id: messageId })
}
return prev === messageId ? null : messageId
})
}, [])
const handleNewChat = useCallback(() => {
@ -454,6 +478,7 @@ export function ChatSidebar({
)
setMessages(uiMessages)
setConversation(threadId, uiMessages) // persist messages to store
analytics.chatThreadLoaded({ thread_id: threadId })
setIsHistoryOpen(false)
setConfirmingDeleteId(null)
}
@ -472,6 +497,7 @@ export function ChatSidebar({
{ method: "DELETE", credentials: "include" },
)
if (response.ok) {
analytics.chatThreadDeleted({ thread_id: threadId })
setThreads((prev) => prev.filter((t) => t.id !== threadId))
if (currentChatId === threadId) {
handleNewChat()
@ -524,6 +550,7 @@ export function ChatSidebar({
status !== "submitted" &&
status !== "streaming"
) {
analytics.chatMessageSent({ source: "highlight" })
sendMessage({ text: queuedMessage })
onConsumeQueuedMessage?.()
}
@ -667,7 +694,8 @@ export function ChatSidebar({
variant="headers"
className="rounded-full text-base gap-2 h-10! border-[#73737333] bg-[#0D121A] cursor-pointer"
style={{
boxShadow: "1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
boxShadow:
"1.5px 1.5px 4.5px 0 rgba(0, 0, 0, 0.70) inset",
}}
>
<HistoryIcon className="size-4 text-[#737373]" />
@ -833,6 +861,8 @@ export function ChatSidebar({
{messages.length === 0 && (
<ChatEmptyStatePlaceholder
onSuggestionClick={(suggestion) => {
analytics.chatSuggestedQuestionClicked()
analytics.chatMessageSent({ source: "suggested" })
sendMessage({ text: suggestion })
}}
suggestions={emptyStateSuggestions}
@ -883,6 +913,10 @@ export function ChatSidebar({
onDislike={handleDislikeMessage}
onToggleMemories={handleToggleMemories}
onQuestionClick={(question) => {
analytics.chatFollowUpClicked({
thread_id: currentChatId || undefined,
})
analytics.chatMessageSent({ source: "follow_up" })
setInput(question)
}}
/>

View file

@ -6,6 +6,7 @@ import { Button } from "@ui/components/button"
import { dmSansClassName } from "@/lib/fonts"
import { ChevronDownIcon } from "lucide-react"
import { models, type ModelId, modelNames } from "@/lib/models"
import { analytics } from "@/lib/analytics"
interface ChatModelSelectorProps {
selectedModel?: ModelId
@ -28,6 +29,7 @@ export default function ChatModelSelector({
} else {
setInternalModel(modelId)
}
analytics.modelChanged({ model: modelId })
setIsOpen(false)
}

View file

@ -60,12 +60,12 @@ function CustomTweetHeader({
</div>
<div className="flex flex-col items-start">
<div className="flex gap-0.5 items-center">
<p
className={cn(
"font-semibold leading-tight overflow-hidden text-[#fafafa] text-[12px] truncate tracking-[-0.12px]",
dmSansClassName(),
)}
>
<p
className={cn(
"font-semibold leading-tight overflow-hidden text-[#fafafa] text-[12px] truncate tracking-[-0.12px]",
dmSansClassName(),
)}
>
{user.name}
</p>
{isVerified && <VerifiedBadge />}

View file

@ -12,6 +12,7 @@ import {
Link2,
} from "lucide-react"
import { Logo } from "@ui/assets/Logo"
import { analytics } from "@/lib/analytics"
export type HighlightFormat = "paragraph" | "bullets" | "quote" | "one_liner"
@ -83,12 +84,20 @@ export function HighlightsCard({
const handleChat = useCallback(() => {
if (!currentItem) return
analytics.highlightClicked({
highlight_id: currentItem.id,
action: "chat",
})
const seed = `Tell me more about "${currentItem.title}"`
onChat(seed)
}, [currentItem, onChat])
const handleShowRelated = useCallback(() => {
if (!currentItem) return
analytics.highlightClicked({
highlight_id: currentItem.id,
action: "related",
})
onShowRelated(currentItem.query || currentItem.title)
}, [currentItem, onShowRelated])

View file

@ -8,6 +8,7 @@ import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { useOnboardingStorage } from "@hooks/use-onboarding-storage"
import { analytics } from "@/lib/analytics"
const integrationCards = [
{
@ -63,6 +64,7 @@ export function IntegrationsStep() {
const { markOnboardingCompleted } = useOnboardingStorage()
const handleContinue = () => {
analytics.onboardingCompleted()
markOnboardingCompleted()
router.push("/new")
}
@ -108,11 +110,22 @@ export function IntegrationsStep() {
)}
onClick={() => {
if (card.title === "Capture") {
analytics.onboardingChromeExtensionClicked({
source: "onboarding",
})
window.open(
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
"_blank",
)
} else {
analytics.onboardingIntegrationClicked({
integration: card.title,
})
if (card.title === "Connect to AI") {
analytics.onboardingMcpDetailOpened()
} else if (card.title === "Import") {
analytics.onboardingXBookmarksDetailOpened()
}
setSelectedCard(card.title)
}
}}

View file

@ -6,6 +6,7 @@ import { Button } from "@ui/components/button"
import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { analytics } from "@/lib/analytics"
const relatableOptions = [
{
@ -35,6 +36,10 @@ export function RelatableQuestion() {
const [selectedOptions, setSelectedOptions] = useState<number[]>([])
const handleContinueOrSkip = () => {
const selectedTexts = selectedOptions.map(
(idx) => relatableOptions[idx]?.text || "",
)
analytics.onboardingRelatableSelected({ options: selectedTexts })
router.push("/new/onboarding/setup?step=integrations")
}

View file

@ -10,6 +10,7 @@ import {
toXProfileUrl,
toLinkedInProfileUrl,
} from "@/lib/url-helpers"
import { analytics } from "@/lib/analytics"
interface ProfileStepProps {
onSubmit: (data: {
@ -291,6 +292,12 @@ export function ProfileStep({ onSubmit }: ProfileStepProps) {
description: description,
otherLinks: otherLinks.filter((l) => l.trim()),
}
analytics.onboardingProfileSubmitted({
has_twitter: !!twitterHandle.trim(),
has_linkedin: !!linkedinProfile.trim(),
other_links_count: otherLinks.filter((l) => l.trim()).length,
description_length: description.trim().length,
})
onSubmit(formData)
router.push("/new/onboarding/setup?step=relatable")
}}

View file

@ -283,6 +283,7 @@ export default function Integrations() {
"_blank",
"noopener,noreferrer",
)
analytics.onboardingChromeExtensionClicked({ source: "settings" })
analytics.extensionInstallClicked()
}
@ -310,6 +311,7 @@ export default function Integrations() {
const handleRaycastInstall = () => {
window.open(RAYCAST_EXTENSION_URL, "_blank")
analytics.onboardingChromeExtensionClicked({ source: "settings" })
analytics.extensionInstallClicked()
}

View file

@ -27,6 +27,7 @@ import {
SelectValue,
} from "@repo/ui/components/select"
import { Button } from "@repo/ui/components/button"
import { analytics } from "@/lib/analytics"
export interface SpaceSelectorProps {
value: string
@ -98,6 +99,9 @@ export function SpaceSelector({
const selectedProjectEmoji = selectedProject.emoji || "📁"
const handleSelect = (containerTag: string) => {
if (containerTag !== value) {
analytics.spaceSwitched({ space_id: containerTag })
}
onValueChange(containerTag)
setIsOpen(false)
}

View file

@ -359,7 +359,8 @@ export function useDocumentMutations({
return response.data
},
onSuccess: () => {
onSuccess: (_data, variables) => {
analytics.documentEdited({ document_id: variables.documentId })
toast.success("Document saved successfully!")
queryClient.invalidateQueries({
queryKey: ["documents-with-memories"],
@ -451,7 +452,8 @@ export function useDocumentMutations({
description: _error instanceof Error ? _error.message : "Unknown error",
})
},
onSuccess: () => {
onSuccess: (_data, variables) => {
analytics.documentDeleted({ document_id: variables.documentId })
toast.success("Document deleted successfully!")
queryClient.invalidateQueries({
queryKey: ["documents-with-memories"],

View file

@ -62,4 +62,105 @@ export const analytics = {
mcpModalOpened: () => safeCapture("mcp_modal_opened"),
addDocumentModalOpened: () => safeCapture("add_document_modal_opened"),
// onboarding analytics
onboardingStepViewed: (props: { step: string; 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),
onboardingIntegrationClicked: (props: { integration: string }) =>
safeCapture("onboarding_integration_clicked", props),
onboardingChromeExtensionClicked: (props: {
source: "onboarding" | "settings"
}) => safeCapture("onboarding_chrome_extension_clicked", props),
onboardingMcpDetailOpened: () => safeCapture("onboarding_mcp_detail_opened"),
onboardingXBookmarksDetailOpened: () =>
safeCapture("onboarding_x_bookmarks_detail_opened"),
onboardingCompleted: () => safeCapture("onboarding_completed"),
// main app analytics
searchOpened: (props: {
source: "hotkey" | "header" | "highlight_related"
}) => safeCapture("search_opened", props),
documentModalOpened: (props: { document_id: string }) =>
safeCapture("document_modal_opened", props),
fullscreenNoteModalOpened: () => safeCapture("fullscreen_note_modal_opened"),
highlightClicked: (props: {
highlight_id: string
action: "chat" | "related"
}) => safeCapture("highlight_clicked", props),
// chat analytics
chatMessageSent: (props: {
source: "typed" | "suggested" | "highlight" | "follow_up"
}) => safeCapture("chat_message_sent", props),
chatFollowUpClicked: (props: { thread_id?: string }) =>
safeCapture("chat_follow_up_clicked", props),
chatSuggestedQuestionClicked: () =>
safeCapture("chat_suggested_question_clicked"),
chatMessageLiked: (props: { message_id: string }) =>
safeCapture("chat_message_liked", props),
chatMessageDisliked: (props: { message_id: string }) =>
safeCapture("chat_message_disliked", props),
chatMessageCopied: (props: { message_id: string }) =>
safeCapture("chat_message_copied", props),
chatMemoryExpanded: (props: { message_id: string }) =>
safeCapture("chat_memory_expanded", props),
chatMemoryCollapsed: (props: { message_id: string }) =>
safeCapture("chat_memory_collapsed", props),
chatThreadLoaded: (props: { thread_id: string }) =>
safeCapture("chat_thread_loaded", props),
chatThreadDeleted: (props: { thread_id: string }) =>
safeCapture("chat_thread_deleted", props),
modelChanged: (props: { model: string }) =>
safeCapture("model_changed", props),
// settings / spaces / docs analytics
settingsTabChanged: (props: {
tab: "account" | "integrations" | "connections" | "support"
}) => safeCapture("settings_tab_changed", props),
spaceCreated: () => safeCapture("space_created"),
spaceSwitched: (props: { space_id: string }) =>
safeCapture("space_switched", props),
quickNoteCreated: () => safeCapture("quick_note_created"),
quickNoteEdited: () => safeCapture("quick_note_edited"),
documentDeleted: (props: { document_id: string }) =>
safeCapture("document_deleted", props),
documentEdited: (props: { document_id: string }) =>
safeCapture("document_edited", props),
}

View file

@ -35,9 +35,7 @@ export const useQuickNoteDraftStore = create<QuickNoteDraftState>()(
)
export function useQuickNoteDraft(projectId: string) {
const draft = useQuickNoteDraftStore(
(s) => s.draftByProject[projectId] ?? "",
)
const draft = useQuickNoteDraftStore((s) => s.draftByProject[projectId] ?? "")
const setDraft = useQuickNoteDraftStore((s) => s.setDraft)
const resetDraft = useQuickNoteDraftStore((s) => s.resetDraft)