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:
Vivek R
2025-07-31 14:22:47 +05:30
parent f08764c6ea
commit db1efc2831
7 changed files with 584 additions and 8 deletions

View File

@@ -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);
}

View File

@@ -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>
);
};
};

View File

@@ -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();

View File

@@ -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'

View File

@@ -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) {