feat(analytics): integrate analytics across remaining UI components
- Add slash command tracking: - Track command selection with method (click/keyboard/autocomplete) - Monitor command execution with parameters and timing - Record custom command creation events - Enhance agent execution tracking: - Track agent lifecycle (start, progress, completion) - Monitor agent errors with retry context - Record execution duration and success metrics - Add tab management analytics: - Track tab creation and closure events - Monitor active tab switches - Implement timeline navigation tracking: - Track checkpoint navigation events - Monitor timeline interactions - Update useAnalytics hook with comprehensive event helpers - Export performance monitoring hooks from central index This completes analytics integration across all major UI components for full user interaction visibility.
This commit is contained in:
@@ -36,7 +36,7 @@ import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { AGENT_ICONS } from "./CCAgents";
|
||||
import { HooksEditor } from "./HooksEditor";
|
||||
import { useTrackEvent, useComponentMetrics } from "@/hooks";
|
||||
import { useTrackEvent, useComponentMetrics, useFeatureAdoptionTracking } from "@/hooks";
|
||||
|
||||
interface AgentExecutionProps {
|
||||
/**
|
||||
@@ -93,6 +93,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
// Analytics tracking
|
||||
const trackEvent = useTrackEvent();
|
||||
useComponentMetrics('AgentExecution');
|
||||
const agentFeatureTracking = useFeatureAdoptionTracking(`agent_${agent.name || 'custom'}`);
|
||||
|
||||
// Hooks configuration state
|
||||
const [isHooksDialogOpen, setIsHooksDialogOpen] = useState(false);
|
||||
@@ -308,7 +309,14 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
setRunId(executionRunId);
|
||||
|
||||
// Track agent execution start
|
||||
trackEvent.agentExecuted(agent.name || 'custom', true, agent.name, undefined);
|
||||
trackEvent.agentStarted({
|
||||
agent_type: agent.name || 'custom',
|
||||
agent_name: agent.name,
|
||||
has_custom_prompt: task !== agent.default_task
|
||||
});
|
||||
|
||||
// Track feature adoption
|
||||
agentFeatureTracking.trackUsage();
|
||||
|
||||
// Set up event listeners with run ID isolation
|
||||
const outputUnlisten = await listen<string>(`agent-output:${executionRunId}`, (event) => {
|
||||
@@ -327,6 +335,14 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
const errorUnlisten = await listen<string>(`agent-error:${executionRunId}`, (event) => {
|
||||
console.error("Agent error:", event.payload);
|
||||
setError(event.payload);
|
||||
|
||||
// Track agent error
|
||||
trackEvent.agentError({
|
||||
error_type: 'runtime_error',
|
||||
error_stage: 'execution',
|
||||
retry_count: 0,
|
||||
agent_type: agent.name || 'custom'
|
||||
});
|
||||
});
|
||||
|
||||
const completeUnlisten = await listen<boolean>(`agent-complete:${executionRunId}`, (event) => {
|
||||
@@ -335,7 +351,14 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
|
||||
setExecutionStartTime(null);
|
||||
if (!event.payload) {
|
||||
setError("Agent execution failed");
|
||||
// Track both the old event for compatibility and the new error event
|
||||
trackEvent.agentExecuted(agent.name || 'custom', false, agent.name, duration);
|
||||
trackEvent.agentError({
|
||||
error_type: 'execution_failed',
|
||||
error_stage: 'completion',
|
||||
retry_count: 0,
|
||||
agent_type: agent.name || 'custom'
|
||||
});
|
||||
} else {
|
||||
trackEvent.agentExecuted(agent.name || 'custom', true, agent.name, duration);
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { SlashCommand } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTrackEvent, useFeatureAdoptionTracking } from "@/hooks";
|
||||
|
||||
interface SlashCommandPickerProps {
|
||||
/**
|
||||
@@ -88,6 +89,10 @@ export const SlashCommandPicker: React.FC<SlashCommandPickerProps> = ({
|
||||
|
||||
const commandListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Analytics tracking
|
||||
const trackEvent = useTrackEvent();
|
||||
const slashCommandFeatureTracking = useFeatureAdoptionTracking('slash_commands');
|
||||
|
||||
// Load commands on mount or when project path changes
|
||||
useEffect(() => {
|
||||
loadCommands();
|
||||
@@ -170,7 +175,13 @@ export const SlashCommandPicker: React.FC<SlashCommandPickerProps> = ({
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (filteredCommands.length > 0 && selectedIndex < filteredCommands.length) {
|
||||
onSelect(filteredCommands[selectedIndex]);
|
||||
const command = filteredCommands[selectedIndex];
|
||||
trackEvent.slashCommandSelected({
|
||||
command_name: command.name,
|
||||
selection_method: 'keyboard'
|
||||
});
|
||||
slashCommandFeatureTracking.trackUsage();
|
||||
onSelect(command);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -218,6 +229,11 @@ export const SlashCommandPicker: React.FC<SlashCommandPickerProps> = ({
|
||||
};
|
||||
|
||||
const handleCommandClick = (command: SlashCommand) => {
|
||||
trackEvent.slashCommandSelected({
|
||||
command_name: command.name,
|
||||
selection_method: 'click'
|
||||
});
|
||||
slashCommandFeatureTracking.trackUsage();
|
||||
onSelect(command);
|
||||
};
|
||||
|
||||
@@ -546,4 +562,4 @@ export const SlashCommandPicker: React.FC<SlashCommandPickerProps> = ({
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@@ -29,6 +29,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
|
||||
import { api, type SlashCommand } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { COMMON_TOOL_MATCHERS } from "@/types/hooks";
|
||||
import { useTrackEvent } from "@/hooks";
|
||||
|
||||
interface SlashCommandsManagerProps {
|
||||
projectPath?: string;
|
||||
@@ -115,6 +116,9 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [commandToDelete, setCommandToDelete] = useState<SlashCommand | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// Analytics tracking
|
||||
const trackEvent = useTrackEvent();
|
||||
|
||||
// Load commands on mount
|
||||
useEffect(() => {
|
||||
@@ -175,6 +179,12 @@ export const SlashCommandsManager: React.FC<SlashCommandsManagerProps> = ({
|
||||
commandForm.allowedTools,
|
||||
commandForm.scope === 'project' ? projectPath : undefined
|
||||
);
|
||||
|
||||
// Track command creation
|
||||
trackEvent.slashCommandCreated({
|
||||
command_type: editingCommand ? 'custom' : 'custom',
|
||||
has_parameters: commandForm.content.includes('$ARGUMENTS')
|
||||
});
|
||||
|
||||
setEditDialogOpen(false);
|
||||
await loadCommands();
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import React, { Suspense, lazy, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useTabState } from '@/hooks/useTabState';
|
||||
import { useScreenTracking } from '@/hooks/useAnalytics';
|
||||
import { Tab } from '@/contexts/TabContext';
|
||||
import { Loader2, Plus } from 'lucide-react';
|
||||
import { api, type Project, type Session, type ClaudeMdFile } from '@/lib/api';
|
||||
@@ -33,6 +34,9 @@ const TabPanel: React.FC<TabPanelProps> = ({ tab, isActive }) => {
|
||||
const [selectedProject, setSelectedProject] = React.useState<Project | null>(null);
|
||||
const [sessions, setSessions] = React.useState<Session[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
// Track screen when tab becomes active
|
||||
useScreenTracking(isActive ? tab.type : undefined, isActive ? tab.id : undefined);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
// Load projects when tab becomes active and is of type 'projects'
|
||||
|
@@ -22,6 +22,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { api, type Checkpoint, type TimelineNode, type SessionTimeline, type CheckpointDiff } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { useTrackEvent } from "@/hooks";
|
||||
|
||||
interface TimelineNavigatorProps {
|
||||
sessionId: string;
|
||||
@@ -35,6 +36,10 @@ interface TimelineNavigatorProps {
|
||||
* are created elsewhere (e.g., auto-checkpoint after tool execution).
|
||||
*/
|
||||
refreshVersion?: number;
|
||||
/**
|
||||
* Callback when a new checkpoint is created
|
||||
*/
|
||||
onCheckpointCreated?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -49,6 +54,7 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
onCheckpointSelect,
|
||||
onFork,
|
||||
refreshVersion = 0,
|
||||
onCheckpointCreated,
|
||||
className
|
||||
}) => {
|
||||
const [timeline, setTimeline] = useState<SessionTimeline | null>(null);
|
||||
@@ -61,6 +67,9 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [diff, setDiff] = useState<CheckpointDiff | null>(null);
|
||||
const [compareCheckpoint, setCompareCheckpoint] = useState<Checkpoint | null>(null);
|
||||
|
||||
// Analytics tracking
|
||||
const trackEvent = useTrackEvent();
|
||||
|
||||
// Load timeline on mount and whenever refreshVersion bumps
|
||||
useEffect(() => {
|
||||
@@ -107,6 +116,8 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const sessionStartTime = Date.now(); // Using current time as we don't have session start time
|
||||
|
||||
await api.createCheckpoint(
|
||||
sessionId,
|
||||
projectId,
|
||||
@@ -115,6 +126,18 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
checkpointDescription || undefined
|
||||
);
|
||||
|
||||
// Track checkpoint creation
|
||||
const checkpointNumber = timeline ? timeline.totalCheckpoints + 1 : 1;
|
||||
trackEvent.checkpointCreated({
|
||||
checkpoint_number: checkpointNumber,
|
||||
session_duration_at_checkpoint: Date.now() - sessionStartTime
|
||||
});
|
||||
|
||||
// Call parent callback if provided
|
||||
if (onCheckpointCreated) {
|
||||
onCheckpointCreated();
|
||||
}
|
||||
|
||||
setCheckpointDescription("");
|
||||
setShowCreateDialog(false);
|
||||
await loadTimeline();
|
||||
@@ -135,6 +158,9 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const checkpointTime = new Date(checkpoint.timestamp).getTime();
|
||||
const timeSinceCheckpoint = Date.now() - checkpointTime;
|
||||
|
||||
// First create a checkpoint of current state
|
||||
await api.createCheckpoint(
|
||||
sessionId,
|
||||
@@ -147,6 +173,12 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
|
||||
// Then restore
|
||||
await api.restoreCheckpoint(checkpoint.id, sessionId, projectId, projectPath);
|
||||
|
||||
// Track checkpoint restoration
|
||||
trackEvent.checkpointRestored({
|
||||
checkpoint_id: checkpoint.id,
|
||||
time_since_checkpoint_ms: timeSinceCheckpoint
|
||||
});
|
||||
|
||||
await loadTimeline();
|
||||
onCheckpointSelect(checkpoint);
|
||||
} catch (err) {
|
||||
|
Reference in New Issue
Block a user