feat: implement PostHog event tracking plan
Add trackEvent() calls for all user actions in the tracking plan: - Vault: vault_opened (with has_git, note_count), vault_switched - Notes: note_created (with has_type, creation_path), note_deleted, note_trashed, note_archived, note_favorited, note_unfavorited - Git: commit_made, sync_triggered - Search: search_used - Editor: raw_mode_toggled, wikilink_inserted - Types & Views: type_created, view_created - Telemetry: telemetry_opted_in, telemetry_opted_out All events gated by PostHog initialization (only fires when opted in). No PII in any event properties — counts, booleans, and enums only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
import type { ViewMode } from './useViewMode'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
|
||||
interface KeyboardActions {
|
||||
onQuickOpen: () => void
|
||||
@@ -104,6 +105,7 @@ export function useAppKeyboard({
|
||||
// Cmd+Shift+F: full-text search (distinct from Cmd+F browser find)
|
||||
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'f') {
|
||||
e.preventDefault()
|
||||
trackEvent('search_used')
|
||||
onSearch()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke } from '../mock-tauri'
|
||||
import type { GitPullResult, GitPushResult, GitRemoteStatus, LastCommitInfo, SyncStatus } from '../types'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
|
||||
const DEFAULT_INTERVAL_MS = 5 * 60_000
|
||||
|
||||
@@ -219,5 +220,10 @@ export function useAutoSync({
|
||||
const pausePull = useCallback(() => { pauseRef.current = true }, [])
|
||||
const resumePull = useCallback(() => { pauseRef.current = false }, [])
|
||||
|
||||
return { syncStatus, lastSyncTime, conflictFiles, lastCommitInfo, remoteStatus, triggerSync: performPull, pullAndPush, pausePull, resumePull, handlePushRejected }
|
||||
const triggerSync = useCallback(() => {
|
||||
trackEvent('sync_triggered')
|
||||
performPull()
|
||||
}, [performPull])
|
||||
|
||||
return { syncStatus, lastSyncTime, conflictFiles, lastCommitInfo, remoteStatus, triggerSync, pullAndPush, pausePull, resumePull, handlePushRejected }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { GitPushResult } from '../types'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
|
||||
interface CommitFlowConfig {
|
||||
savePending: () => Promise<void | boolean>
|
||||
@@ -25,6 +26,7 @@ export function useCommitFlow({ savePending, loadModifiedFiles, commitAndPush, s
|
||||
await savePending()
|
||||
const result = await commitAndPush(message)
|
||||
if (result.status === 'ok') {
|
||||
trackEvent('commit_made')
|
||||
setToastMessage('Committed and pushed')
|
||||
} else if (result.status === 'rejected') {
|
||||
setToastMessage('Committed, but push rejected — remote has new commits. Pull first.')
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke } from '../mock-tauri'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
|
||||
interface ConfirmDeleteState {
|
||||
title: string
|
||||
@@ -50,7 +51,10 @@ export function useDeleteActions({
|
||||
onConfirm: async () => {
|
||||
setConfirmDelete(null)
|
||||
const ok = await deleteNoteFromDisk(path)
|
||||
if (ok) setToastMessage('Note permanently deleted')
|
||||
if (ok) {
|
||||
trackEvent('note_deleted')
|
||||
setToastMessage('Note permanently deleted')
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [deleteNoteFromDisk, setToastMessage])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { VaultEntry } from '../types'
|
||||
import type { FrontmatterOpOptions } from './frontmatterOps'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
|
||||
interface EntryActionsConfig {
|
||||
entries: VaultEntry[]
|
||||
@@ -32,6 +33,7 @@ export function useEntryActions({
|
||||
// Optimistic: update UI immediately, write to disk async with rollback on failure
|
||||
const trashedAt = Date.now() / 1000
|
||||
updateEntry(path, { trashed: true, trashedAt })
|
||||
trackEvent('note_trashed')
|
||||
setToastMessage('Note moved to trash')
|
||||
const now = new Date().toISOString().slice(0, 10)
|
||||
try {
|
||||
@@ -64,6 +66,7 @@ export function useEntryActions({
|
||||
await onBeforeAction?.(path)
|
||||
// Optimistic: update UI immediately, write to disk async with rollback on failure
|
||||
updateEntry(path, { archived: true })
|
||||
trackEvent('note_archived')
|
||||
setToastMessage('Note archived')
|
||||
try {
|
||||
await handleUpdateFrontmatter(path, '_archived', true, { silent: true })
|
||||
@@ -129,6 +132,7 @@ export function useEntryActions({
|
||||
const entry = entries.find((e) => e.path === path)
|
||||
if (!entry) return
|
||||
if (entry.favorite) {
|
||||
trackEvent('note_unfavorited')
|
||||
updateEntry(path, { favorite: false, favoriteIndex: null })
|
||||
try {
|
||||
await handleDeleteProperty(path, '_favorite', { silent: true })
|
||||
@@ -139,6 +143,7 @@ export function useEntryActions({
|
||||
setToastMessage('Failed to unfavorite — rolled back')
|
||||
}
|
||||
} else {
|
||||
trackEvent('note_favorited')
|
||||
const maxIndex = entries.filter((e) => e.favorite).reduce((max, e) => Math.max(max, e.favoriteIndex ?? 0), 0)
|
||||
const newIndex = maxIndex + 1
|
||||
updateEntry(path, { favorite: true, favoriteIndex: newIndex })
|
||||
|
||||
@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, addMockEntry } from '../mock-tauri'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { resolveEntry } from '../utils/wikilink'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
|
||||
export interface NewEntryParams {
|
||||
path: string
|
||||
@@ -250,6 +251,7 @@ export function useNoteCreation(config: NoteCreationConfig, tabDeps: CreationTab
|
||||
const handleCreateNote = useCallback((title: string, type: string) => {
|
||||
const template = resolveTemplate(entries, type)
|
||||
persistNew(resolveNewNote(title, type, config.vaultPath, template))
|
||||
trackEvent('note_created', { has_type: type !== 'Note' ? 1 : 0, creation_path: 'plus_button' })
|
||||
}, [entries, persistNew, config.vaultPath])
|
||||
|
||||
const handleCreateNoteImmediate = useCallback((type?: string) => {
|
||||
@@ -257,6 +259,7 @@ export function useNoteCreation(config: NoteCreationConfig, tabDeps: CreationTab
|
||||
entries, vaultPath: config.vaultPath, pendingNames: pendingNamesRef.current,
|
||||
openTabWithContent, addEntry, trackUnsaved: config.trackUnsaved, markContentPending: config.markContentPending,
|
||||
}, type)
|
||||
trackEvent('note_created', { has_type: type ? 1 : 0, creation_path: type ? 'type_section' : 'cmd_n' })
|
||||
}, [entries, openTabWithContent, addEntry, config.vaultPath, config.trackUnsaved, config.markContentPending])
|
||||
|
||||
const handleCreateNoteForRelationship = useCallback((title: string): Promise<boolean> => {
|
||||
@@ -269,7 +272,10 @@ export function useNoteCreation(config: NoteCreationConfig, tabDeps: CreationTab
|
||||
|
||||
const handleOpenDailyNote = useCallback(() => openDailyNote(entries, handleSelectNote, persistNew, config.vaultPath), [entries, handleSelectNote, persistNew, config.vaultPath])
|
||||
|
||||
const handleCreateType = useCallback((typeName: string) => persistNew(resolveNewType(typeName, config.vaultPath)), [persistNew, config.vaultPath])
|
||||
const handleCreateType = useCallback((typeName: string) => {
|
||||
persistNew(resolveNewType(typeName, config.vaultPath))
|
||||
trackEvent('type_created')
|
||||
}, [persistNew, config.vaultPath])
|
||||
|
||||
/** Create a Type entry file silently (no tab opened). Adds to state and persists to disk. */
|
||||
const createTypeEntrySilent = useCallback(async (typeName: string): Promise<VaultEntry> => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { getVaultConfig, updateVaultConfigField, subscribeVaultConfig } from '../utils/vaultConfigStore'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
|
||||
interface UseRawModeParams {
|
||||
activeTabPath: string | null
|
||||
@@ -32,6 +33,7 @@ export function useRawMode({ activeTabPath, onFlushPending, onBeforeRawEnd }: Us
|
||||
const rawMode = rawEnabled && activeTabPath !== null
|
||||
|
||||
const handleToggleRaw = useCallback(async () => {
|
||||
trackEvent('raw_mode_toggled')
|
||||
if (rawEnabled) {
|
||||
onBeforeRawEnd?.()
|
||||
setRawEnabled(false)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isTauri, mockInvoke } from '../mock-tauri'
|
||||
import { pickFolder } from '../utils/vault-dialog'
|
||||
import { loadVaultList, saveVaultList } from '../utils/vaultListStore'
|
||||
import type { VaultOption } from '../components/StatusBar'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
|
||||
export type { PersistedVaultList } from '../utils/vaultListStore'
|
||||
|
||||
@@ -91,6 +92,7 @@ export function useVaultSwitcher({ onSwitch, onToast }: UseVaultSwitcherOptions)
|
||||
}, [])
|
||||
|
||||
const switchVault = useCallback((path: string) => {
|
||||
trackEvent('vault_switched')
|
||||
setVaultPath(path)
|
||||
onSwitchRef.current()
|
||||
}, [])
|
||||
|
||||
Reference in New Issue
Block a user