diff --git a/apps/desktop/src/components/chat/input.tsx b/apps/desktop/src/components/chat/input.tsx index 1c55265949..c2f363035b 100644 --- a/apps/desktop/src/components/chat/input.tsx +++ b/apps/desktop/src/components/chat/input.tsx @@ -1,6 +1,7 @@ import { FullscreenIcon, MicIcon, PaperclipIcon, SendIcon } from "lucide-react"; import { useCallback, useEffect, useRef } from "react"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; import type { TiptapEditor } from "@hypr/tiptap/editor"; import Editor from "@hypr/tiptap/editor"; import { @@ -32,6 +33,7 @@ export function ChatMessageInput({ return; } + analyticsCommands.event({ event: "chat_message_sent" }); onSendMessage(text, [{ type: "text", text }]); editorRef.current?.editor?.commands.clearContent(); }, [disabled, onSendMessage]); diff --git a/apps/desktop/src/components/main/body/sessions/note-input/header.tsx b/apps/desktop/src/components/main/body/sessions/note-input/header.tsx index b0554568af..262d071dd9 100644 --- a/apps/desktop/src/components/main/body/sessions/note-input/header.tsx +++ b/apps/desktop/src/components/main/body/sessions/note-input/header.tsx @@ -1,6 +1,7 @@ import { AlertCircleIcon, PlusIcon, RefreshCcwIcon } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; import { commands as windowsCommands } from "@hypr/plugin-windows"; import { md2json } from "@hypr/tiptap/shared"; import { @@ -249,6 +250,11 @@ function CreateOtherFormatButton({ return; } + analyticsCommands.event({ + event: "template_summary_created", + template_id: templateId, + }); + handleTabChange({ type: "enhanced", id: enhancedNoteId }); setPendingNote({ id: enhancedNoteId, templateId }); }, @@ -454,6 +460,8 @@ function useEnhanceLogic(sessionId: string, enhancedNoteId: string) { setMissingModelError(null); + analyticsCommands.event({ event: "summary_generated", is_auto: false }); + await enhanceTask.start({ model, args: { diff --git a/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx b/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx index acf5b72d20..ac11a3acd4 100644 --- a/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx +++ b/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx @@ -1,5 +1,13 @@ -import { forwardRef, useEffect, useMemo, useState } from "react"; +import { + forwardRef, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; import NoteEditor, { type JSONContent, type TiptapEditor, @@ -41,7 +49,7 @@ export const RawEditor = forwardRef< } }, [store, sessionId]); - const handleChange = main.UI.useSetPartialRowCallback( + const persistChange = main.UI.useSetPartialRowCallback( "sessions", sessionId, (input: JSONContent) => ({ raw_md: JSON.stringify(input) }), @@ -49,6 +57,31 @@ export const RawEditor = forwardRef< main.STORE_ID, ); + const hasTrackedWriteRef = useRef(false); + + useEffect(() => { + hasTrackedWriteRef.current = false; + }, [sessionId]); + + const hasNonEmptyText = (node?: JSONContent): boolean => + !!node?.text?.trim() || + !!node?.content?.some((child) => hasNonEmptyText(child)); + + const handleChange = useCallback( + (input: JSONContent) => { + persistChange(input); + + if (!hasTrackedWriteRef.current) { + const hasContent = hasNonEmptyText(input); + if (hasContent) { + hasTrackedWriteRef.current = true; + analyticsCommands.event({ event: "note_written", has_content: true }); + } + } + }, + [persistChange], + ); + const mentionConfig = useMemo( () => ({ trigger: "@", diff --git a/apps/desktop/src/components/main/shared.ts b/apps/desktop/src/components/main/shared.ts index 466a9f304c..04286ae333 100644 --- a/apps/desktop/src/components/main/shared.ts +++ b/apps/desktop/src/components/main/shared.ts @@ -2,6 +2,8 @@ import { useRouteContext } from "@tanstack/react-router"; import { useCallback } from "react"; import { useShallow } from "zustand/shallow"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; + import { useTabs } from "../../store/zustand/tabs"; import { id } from "../../utils"; @@ -30,6 +32,8 @@ export function useNewNote({ title: "", }); + analyticsCommands.event({ event: "note_created", has_event_id: false }); + const ff = behavior === "new" ? openNew : openCurrent; ff({ type: "sessions", id: sessionId }); }, [persistedStore, internalStore, openNew, openCurrent, behavior]); diff --git a/apps/desktop/src/components/main/sidebar/timeline/item.tsx b/apps/desktop/src/components/main/sidebar/timeline/item.tsx index 94d793660f..1c45a8bd33 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/item.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/item.tsx @@ -1,5 +1,6 @@ import { memo, useCallback, useMemo } from "react"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; import { ContextMenuItem } from "@hypr/ui/components/ui/context-menu"; import { cn } from "@hypr/utils"; @@ -81,6 +82,7 @@ export const TimelineItemComponent = memo( title: title, created_at: new Date().toISOString(), }); + analyticsCommands.event({ event: "note_created", has_event_id: true }); const tab: TabInput = { id: sessionId, type: "sessions" }; if (openInNewTab) { openNew(tab); diff --git a/apps/desktop/src/contexts/search/ui.tsx b/apps/desktop/src/contexts/search/ui.tsx index ee45472574..ad72c71186 100644 --- a/apps/desktop/src/contexts/search/ui.tsx +++ b/apps/desktop/src/contexts/search/ui.tsx @@ -10,6 +10,8 @@ import { } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; + import type { SearchDocument, SearchEntityType, @@ -203,6 +205,7 @@ export function SearchUIProvider({ children }: { children: React.ReactNode }) { setIsSearching(true); try { + analyticsCommands.event({ event: "search_used" }); const hits = await search(searchQueryInput, searchFilters); setSearchHits(hits); setSearchQuery(searchQueryInput.trim()); diff --git a/apps/desktop/src/hooks/useAutoEnhance.ts b/apps/desktop/src/hooks/useAutoEnhance.ts index 79bd5b8c24..fc11bee371 100644 --- a/apps/desktop/src/hooks/useAutoEnhance.ts +++ b/apps/desktop/src/hooks/useAutoEnhance.ts @@ -1,6 +1,7 @@ import { usePrevious } from "@uidotdev/usehooks"; import { useCallback, useEffect, useRef, useState } from "react"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; import { md2json } from "@hypr/tiptap/shared"; import { useListener } from "../contexts/listener"; @@ -77,6 +78,7 @@ export function useAutoEnhance(tab: Extract) { !startedTasksRef.current.has(autoEnhancedNoteId) ) { startedTasksRef.current.add(autoEnhancedNoteId); + analyticsCommands.event({ event: "summary_generated", is_auto: true }); void enhanceTask.start({ model, args: { sessionId, enhancedNoteId: autoEnhancedNoteId }, diff --git a/apps/desktop/src/hooks/useStartListening.ts b/apps/desktop/src/hooks/useStartListening.ts index 32acd32da2..abf3b23b90 100644 --- a/apps/desktop/src/hooks/useStartListening.ts +++ b/apps/desktop/src/hooks/useStartListening.ts @@ -1,5 +1,7 @@ import { useCallback } from "react"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; + import { useConfigValue } from "../config/use-config"; import { useListener } from "../contexts/listener"; import * as main from "../store/tinybase/main"; @@ -35,6 +37,12 @@ export function useStartListening(sessionId: string) { started_at: startedAt, }); + const eventId = store.getCell("sessions", sessionId, "event_id"); + analyticsCommands.event({ + event: "recording_started", + has_calendar_event: !!eventId, + }); + const handlePersist: HandlePersistCallback = (words, hints) => { if (words.length === 0) { return; diff --git a/apps/desktop/src/routes/notification.tsx b/apps/desktop/src/routes/notification.tsx index c21cace719..9de861b054 100644 --- a/apps/desktop/src/routes/notification.tsx +++ b/apps/desktop/src/routes/notification.tsx @@ -1,5 +1,6 @@ import { createFileRoute, redirect } from "@tanstack/react-router"; +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; import type { NotificationSearch } from "@hypr/plugin-deeplink2"; export const Route = createFileRoute("/notification")({ @@ -10,6 +11,7 @@ export const Route = createFileRoute("/notification")({ }, beforeLoad: async ({ search }) => { console.log("notification deeplink received", search); + analyticsCommands.event({ event: "notification_clicked" }); throw redirect({ to: "/app/main" }); }, });