Skip to main content

Overview

Claude Analytics is a Next.js 16 application that visualizes usage data from Claude Code’s local ~/.claude/ directory. The application operates in two distinct modes:

Local Mode

Server component reads ~/.claude/ directly via Node.js fs module and passes data to client components

Upload Mode

User exports data with scripts/export.mjs, then uploads the JSON file. All processing happens client-side in the browser
Privacy First: No data is ever sent to external servers. There is no database. All analytics processing happens locally or in your browser.

Data Flow Architecture

Component Flow

  1. Data Loading Layer (src/app/page.tsx)
    • Server-side component that calls loadAllData() from src/lib/load-data.ts
    • Reads all necessary files from ~/.claude/ directory
    • Passes initial data to client components
  2. State Management Layer (ClientPage)
    • Manages multi-profile state
    • Handles file uploads and profile switching
    • Renders either UploadZone or Dashboard
  3. Presentation Layer (Dashboard + child components)
    • Project filtering and tab navigation
    • Data visualization components
    • Interactive charts and tables

Key Architecture Decisions

Client-Side Rendering

All UI components use "use client" directive. The server component (page.tsx) only reads files from disk — all chart rendering and interaction happens client-side. Benefits:
  • Works as a static export with uploaded files
  • Full interactivity without server round-trips
  • Seamless transition between local and upload modes
src/app/page.tsx
import { loadAllData } from "@/lib/load-data";
import { ClientPage } from "@/components/client-page";

export const dynamic = "force-dynamic";

export default function Home() {
  const data = loadAllData();
  const hasData = data.stats !== null || data.sessions.length > 0;

  return <ClientPage initialData={hasData ? data : null} />;
}

Multi-Profile State Management

ClientPage manages an array of Profile objects, each containing:
id
string
Unique identifier for the profile
name
string
Display name (“Local” or generated from account UUID)
data
DashboardData
Complete analytics data for the profile
Profiles are additive — uploading a new file doesn’t replace existing profiles, allowing comparison between different exports or accounts.
Profile Structure
interface Profile {
  id: string;
  name: string;
  data: DashboardData;
}

Project Filtering Strategy

Dashboard component extracts unique project_path values from sessions and provides a dropdown filter:
  • Filtered: sessions and history arrays are filtered by selected project
  • Unfiltered: Stats, heatmap, and hour chart remain unfiltered because they use pre-aggregated data from stats-cache.json that can’t be split by project
Project Filtering Logic
const filteredSessions = useMemo(
  () =>
    selectedProject === "all"
      ? data.sessions
      : data.sessions.filter((s) => s.project_path === selectedProject),
  [data.sessions, selectedProject]
);

Next.js App Router Structure

src/
├── app/                    # Next.js App Router
│   ├── layout.tsx          # Root HTML shell (fonts, dark class)
│   ├── page.tsx            # Server entry: loads data, renders ClientPage
│   ├── globals.css         # Tailwind + neomorphic theme
│   └── api/
│       └── session-messages/
│           └── route.ts    # GET endpoint for full session transcripts
├── components/
│   ├── client-page.tsx     # Profile management, upload/dashboard toggle
│   ├── dashboard.tsx       # Tab layout, project filter, profile switcher
│   ├── upload-zone.tsx     # File upload UI with instructions
│   ├── stats-overview.tsx  # 7 stat cards
│   ├── activity-heatmap.tsx
│   ├── hour-chart.tsx
│   ├── project-breakdown.tsx
│   ├── daily-tokens-chart.tsx
│   ├── model-breakdown.tsx
│   ├── session-table.tsx
│   ├── tool-usage-chart.tsx
│   ├── prompt-history.tsx
│   └── ui/                 # shadcn primitives
├── lib/
│   ├── types.ts            # All TypeScript interfaces
│   ├── utils.ts            # cn(), formatTokens(), etc.
│   └── load-data.ts        # Server-side fs readers

Root Layout

The root layout (src/app/layout.tsx) establishes:
  • Font Stack: Geist Sans + Geist Mono from next/font/google
  • Dark Mode: <html className="dark"> for persistent dark theme
  • Metadata: Title and description for SEO
src/app/layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className="dark">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

API Routes

Session Messages Endpoint

GET /api/session-messages

Fetches full conversation transcript for a specific session
Query Parameters:
session_id
string
required
The unique session identifier
project_path
string
required
Absolute path to the project directory
Response:
{
  "messages": [
    {
      "role": "user",
      "text": "Help me refactor this component",
      "timestamp": "2026-03-04T10:30:00Z",
      "toolUse": null
    },
    {
      "role": "assistant",
      "text": "I'll help you refactor that component...",
      "timestamp": "2026-03-04T10:30:05Z",
      "toolUse": [
        { "name": "Read", "id": "toolu_123" },
        { "name": "Edit", "id": "toolu_456" }
      ]
    }
  ]
}
This endpoint only works in local mode — it requires filesystem access to ~/.claude/projects/
Implementation Details:
src/app/api/session-messages/route.ts
const encoded = projectPath.replace(/\//g, "-");
const jsonlPath = path.join(CLAUDE_DIR, "projects", encoded, `${sessionId}.jsonl`);

// Deduplicate by uuid (assistant messages can span multiple lines)
const uuid = entry.uuid;
if (uuid && seen.has(uuid)) continue;
if (uuid) seen.add(uuid);

Design Philosophy

Neomorphic Dark Theme

The application uses a custom neomorphic design system defined in globals.css:
  • Base Background: #0a0a0a (near-black)
  • Card Background: #1a1a1a with subtle inner shadows
  • Borders: White with 5-10% opacity
  • Text Hierarchy: White → Gray 200 → Gray 400 → Gray 600
All shadcn components are styled via [data-slot="..."] selectors with consistent neomorphic shadows.

Performance Considerations

Uses Recharts library with responsive containers. All charts are client-rendered for interactivity.
Uses React useMemo to prevent unnecessary recalculations during project filtering.
Fetches message transcripts on-demand via API route only when user expands a session.
Limits display to first 200 prompts to prevent rendering slowdown.

Technology Stack

Next.js 16

App Router framework with server/client components

React 19

UI library with modern hooks

Recharts

Area, Bar, and Pie charts

Radix UI

Headless primitives (Tabs, Select, ScrollArea)

Tailwind CSS 4

Utility-first styling

TypeScript

Type-safe development

Lucide React

Icon system

date-fns

Date formatting utilities

Security & Privacy

The application never transmits data to external servers. All processing happens:
  • Server-side: Reading local ~/.claude/ files (local mode)
  • Client-side: Processing uploaded JSON files (upload mode)
No analytics, tracking, or telemetry is included. Your Claude Code usage data stays entirely on your machine or in your browser memory.

Build docs developers (and LLMs) love