diff options
Diffstat (limited to 'apps')
18 files changed, 293 insertions, 39 deletions
diff --git a/apps/web/app/new/onboarding/setup/layout.tsx b/apps/web/app/new/onboarding/setup/layout.tsx index 3a958469..6694f5fd 100644 --- a/apps/web/app/new/onboarding/setup/layout.tsx +++ b/apps/web/app/new/onboarding/setup/layout.tsx @@ -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, diff --git a/apps/web/app/new/onboarding/welcome/layout.tsx b/apps/web/app/new/onboarding/welcome/layout.tsx index 427907c2..07d3c4b3 100644 --- a/apps/web/app/new/onboarding/welcome/layout.tsx +++ b/apps/web/app/new/onboarding/welcome/layout.tsx @@ -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], diff --git a/apps/web/app/new/onboarding/welcome/page.tsx b/apps/web/app/new/onboarding/welcome/page.tsx index 706cd40a..a09f93b5 100644 --- a/apps/web/app/new/onboarding/welcome/page.tsx +++ b/apps/web/app/new/onboarding/welcome/page.tsx @@ -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) } diff --git a/apps/web/app/new/page.tsx b/apps/web/app/new/page.tsx index 31fab182..61c573f3 100644 --- a/apps/web/app/new/page.tsx +++ b/apps/web/app/new/page.tsx @@ -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}`} diff --git a/apps/web/app/new/settings/page.tsx b/apps/web/app/new/settings/page.tsx index f1972bb9..b2e35b7f 100644 --- a/apps/web/app/new/settings/page.tsx +++ b/apps/web/app/new/settings/page.tsx @@ -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", diff --git a/apps/web/components/new/add-space-modal.tsx b/apps/web/components/new/add-space-modal.tsx index 63ee5e96..fdc5347f 100644 --- a/apps/web/components/new/add-space-modal.tsx +++ b/apps/web/components/new/add-space-modal.tsx @@ -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() }, }, diff --git a/apps/web/components/new/chat/index.tsx b/apps/web/components/new/chat/index.tsx index 22a77812..82bba2bc 100644 --- a/apps/web/components/new/chat/index.tsx +++ b/apps/web/components/new/chat/index.tsx @@ -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) }} /> diff --git a/apps/web/components/new/chat/model-selector.tsx b/apps/web/components/new/chat/model-selector.tsx index c1952bea..db77a5c7 100644 --- a/apps/web/components/new/chat/model-selector.tsx +++ b/apps/web/components/new/chat/model-selector.tsx @@ -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) } diff --git a/apps/web/components/new/document-cards/tweet-preview.tsx b/apps/web/components/new/document-cards/tweet-preview.tsx index 2d9baacf..c9a30ecd 100644 --- a/apps/web/components/new/document-cards/tweet-preview.tsx +++ b/apps/web/components/new/document-cards/tweet-preview.tsx @@ -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 />} diff --git a/apps/web/components/new/highlights-card.tsx b/apps/web/components/new/highlights-card.tsx index eb3e0b44..8ca07637 100644 --- a/apps/web/components/new/highlights-card.tsx +++ b/apps/web/components/new/highlights-card.tsx @@ -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]) diff --git a/apps/web/components/new/onboarding/setup/integrations-step.tsx b/apps/web/components/new/onboarding/setup/integrations-step.tsx index 75e0d782..03102b14 100644 --- a/apps/web/components/new/onboarding/setup/integrations-step.tsx +++ b/apps/web/components/new/onboarding/setup/integrations-step.tsx @@ -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) } }} diff --git a/apps/web/components/new/onboarding/setup/relatable-question.tsx b/apps/web/components/new/onboarding/setup/relatable-question.tsx index 5edb4a99..b65bdab4 100644 --- a/apps/web/components/new/onboarding/setup/relatable-question.tsx +++ b/apps/web/components/new/onboarding/setup/relatable-question.tsx @@ -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") } diff --git a/apps/web/components/new/onboarding/welcome/profile-step.tsx b/apps/web/components/new/onboarding/welcome/profile-step.tsx index 34393dad..8d05997a 100644 --- a/apps/web/components/new/onboarding/welcome/profile-step.tsx +++ b/apps/web/components/new/onboarding/welcome/profile-step.tsx @@ -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") }} diff --git a/apps/web/components/new/settings/integrations.tsx b/apps/web/components/new/settings/integrations.tsx index deccf5e4..f28695e7 100644 --- a/apps/web/components/new/settings/integrations.tsx +++ b/apps/web/components/new/settings/integrations.tsx @@ -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() } diff --git a/apps/web/components/new/space-selector.tsx b/apps/web/components/new/space-selector.tsx index be2fcb50..45b51512 100644 --- a/apps/web/components/new/space-selector.tsx +++ b/apps/web/components/new/space-selector.tsx @@ -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) } diff --git a/apps/web/hooks/use-document-mutations.ts b/apps/web/hooks/use-document-mutations.ts index ea3c3783..3480c247 100644 --- a/apps/web/hooks/use-document-mutations.ts +++ b/apps/web/hooks/use-document-mutations.ts @@ -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"], diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index 84eda62b..6d8c00eb 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -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), } diff --git a/apps/web/stores/quick-note-draft.ts b/apps/web/stores/quick-note-draft.ts index afd2bc6d..31efa1d7 100644 --- a/apps/web/stores/quick-note-draft.ts +++ b/apps/web/stores/quick-note-draft.ts @@ -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) |