mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
feat: added advanced analytics events (#702)
added advanced analytics events
This commit is contained in:
parent
e7ea26442a
commit
134861de3d
18 changed files with 293 additions and 39 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue