Claude for Enterprise provides comprehensive audit logs that enable organizations to gain valuable insights into their Claude usage patterns. This guide will walk you through creating an interactive analytics dashboard using your organization's audit log data.
Prerequisites
Access to Claude for Enterprise
Primary Owner privileges to access Recorded Events in audit logs (available in CSV or JSON format)
A Claude project for analytics
Step-by-Step Implementation
1. Create a Dedicated Analytics Project
Begin by creating a new Claude project specifically for usage analytics:
Navigate to your Claude workspace
Create a new project named "Usage Analytics"
Open the project settings to add custom instructions
2. Configure Project Instructions
Add the custom instructions that are pasted at the end of this document to your Usage Analytics project. These instructions will enable Claude to create consistent, interactive dashboards whenever you upload audit log data.
The dashboard will provide:
Key Metrics Display
Total unique users
Total conversations
Total projects
Total uploaded files
Interactive Features
Overview tab showing daily active users trend
Conversations tab with detailed metrics:
Period summaries (7-day, 30-day, all-time)
Per-user conversation statistics
User engagement leaderboard
Daily conversation trends
3. Generate Analytics
Once your project is configured:
Download your organization's audit log in CSV format (Instructions to access audit logs)
Start a new chat in your Usage Analytics project
Upload the audit log CSV file
Ask Claude: "Can you create an interactive dashboard for me using the project custom instructions?"
4. Explore and Customize
The resulting dashboard provides various ways to analyze your organization's Claude usage:
Track user adoption through the daily active users chart
Monitor conversation volumes across different time periods
Identify usage patterns
Analyze file and project volumes
You can further customize the dashboard by asking Claude to modify specific aspects or add new visualizations based on your needs.
Custom Instructions for Your Project
Copy and paste the following instructions into your ‘Usage Analytics’ project.
(NOTE: You only need to do this once and then can simply upload your most recent audit log CSV into the chat to have it pull from these custom instructions)
Custom instructions
Custom instructions
Our goal is to produce simple analysis and visualization of usage patterns.
When I upload an audit logs CSV, do NOT use the analysis tool - you don't need it.
Instead, you should create a React data visualization Artifact with
a) Summary Cards showing:
- Total unique users
- Total conversations
- Total projects
- Total files
b) Tab Navigation:
- Overview: Daily active users trend
- Conversations:
* Summary metrics for 7D/30D/All-time periods
* Per-user conversation counts for each period (7D/30D/All-time)
* User conversation leaderboard with last seen dates
* Daily conversations trend
c) Tables & Charts:
- Use standard HTML tables with Tailwind styling
- Use Recharts for visualizations
- Ensure all tables are sortable
- Include last seen dates where relevant
Technical Requirements:
- Use window.fs.readFile for file access
- Handle all JSON parsing errors gracefully
- Focus on conversation metrics as primary KPI
Here's some starter Artifact code that might help:
```tsx
import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Users, MessageSquare, FolderKanban, FileText } from 'lucide-react';
import Papa from 'papaparse';
import _ from 'lodash';
const MetricCard = ({ title, value, icon: Icon }) => (
<div className="bg-white rounded-lg shadow p-4 flex flex-col">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-medium text-gray-600">{title}</h3>
<Icon className="h-4 w-4 text-gray-400" />
</div>
<div className="text-2xl font-bold">{value}</div>
</div>
);
const SortableTable = ({ data, columns }) => {
const [sortConfig, setSortConfig] = useState({ key: '', direction: 'asc' });
const sortedData = React.useMemo(() => {
if (!sortConfig.key) return data;
return [...data].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
return 0;
});
}, [data, sortConfig]);
const requestSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
};
return (
<div className="relative overflow-x-auto shadow-md rounded-lg">
<table className="w-full text-sm text-left text-gray-700">
<thead className="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
{columns.map((column) => (
<th
key={column.key}
onClick={() => requestSort(column.key)}
className="px-6 py-3 cursor-pointer hover:bg-gray-100"
>
{column.label}
{sortConfig.key === column.key && (
<span>{sortConfig.direction === 'asc' ? ' ↑' : ' ↓'}</span>
)}
</th>
))}
</tr>
</thead>
<tbody>
{sortedData.map((row, i) => (
<tr key={i} className="bg-white border-b hover:bg-gray-50">
{columns.map((column) => (
<td key={column.key} className="px-6 py-4">
{typeof column.format === 'function'
? column.format(row[column.key])
: row[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
};
const Dashboard = () => {
const [activeTab, setActiveTab] = useState('overview');
const [data, setData] = useState(null);
const [metrics, setMetrics] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
try {
const response = await window.fs.readFile('audit_logs.csv', { encoding: 'utf8' });
const result = Papa.parse(response, {
header: true,
dynamicTyping: true,
skipEmptyLines: 'greedy'
});
const processedData = result.data.map(row => {
const actorInfo = typeof row.actor_info === 'string' ?
JSON.parse(row.actor_info.replace(/'/g, '"')) : row.actor_info;
return {
...row,
email: actorInfo?.metadata?.email_address || 'Unknown',
userName: actorInfo?.name || 'Unknown',
date: new Date(row.created_at),
dateStr: new Date(row.created_at).toISOString().split('T')[0]
};
});
const userMetrics = {};
processedData.forEach(row => {
if (!userMetrics[row.email]) {
userMetrics[row.email] = {
name: row.userName,
email: row.email,
totalActions: 0,
conversations: 0,
projects: 0,
files: 0,
lastSeen: row.date
};
}
userMetrics[row.email].totalActions++;
if (row.event === 'conversation_created') userMetrics[row.email].conversations++;
if (row.event === 'project_created') userMetrics[row.email].projects++;
if (row.event === 'file_uploaded') userMetrics[row.email].files++;
if (row.date > userMetrics[row.email].lastSeen) {
userMetrics[row.email].lastSeen = row.date;
}
});
const dailyUsers = _.chain(processedData)
.groupBy('dateStr')
.map((rows, date) => ({
date,
activeUsers: _.uniqBy(rows, 'email').length
}))
.orderBy(['date'], ['asc'])
.value();
const dailyConversations = _.chain(processedData)
.filter({ event: 'conversation_created' })
.groupBy('dateStr')
.map((rows, date) => ({
date,
conversations: rows.length
}))
.orderBy(['date'], ['asc'])
.value();
setData(processedData);
setMetrics({
userMetrics: Object.values(userMetrics),
dailyUsers,
dailyConversations,
totalUsers: _.uniqBy(processedData, 'email').length,
totalConversations: processedData.filter(d => d.event === 'conversation_created').length,
totalProjects: processedData.filter(d => d.event === 'project_created').length,
totalFiles: processedData.filter(d => d.event === 'file_uploaded').length,
});
setIsLoading(false);
} catch (error) {
console.error('Error loading data:', error);
setIsLoading(false);
}
};
loadData();
}, []);
if (isLoading || !metrics) {
return <div className="p-4">Loading...</div>;
}
return (
<div className="p-4 max-w-7xl mx-auto">
<h1 className="text-2xl font-bold mb-6">Usage Analytics Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<MetricCard
title="Total Users"
value={metrics.totalUsers}
icon={Users}
/>
<MetricCard
title="Total Conversations"
value={metrics.totalConversations}
icon={MessageSquare}
/>
<MetricCard
title="Total Projects"
value={metrics.totalProjects}
icon={FolderKanban}
/>
<MetricCard
title="Total Files"
value={metrics.totalFiles}
icon={FileText}
/>
</div>
<div className="mb-6">
<div className="border-b border-gray-200">
<nav className="flex space-x-4" aria-label="Tabs">
{['overview', 'conversations'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`${
activeTab === tab
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
} whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm capitalize`}
>
{tab}
</button>
))}
</nav>
</div>
</div>
<div className="space-y-6">
{activeTab === 'overview' && (
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-lg font-medium mb-4">Daily Active Users</h3>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={metrics.dailyUsers}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="activeUsers" stroke="#3b82f6" name="Active Users" />
</LineChart>
</ResponsiveContainer>
</div>
</div>
)}
{activeTab === 'conversations' && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-600 mb-2">Last 7 Days</h3>
<div className="text-2xl font-bold">
{data.filter(row =>
row.date >= new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) &&
row.event === 'conversation_created'
).length}
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-600 mb-2">Last 30 Days</h3>
<div className="text-2xl font-bold">
{data.filter(row =>
row.date >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) &&
row.event === 'conversation_created'
).length}
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-600 mb-2">All Time</h3>
<div className="text-2xl font-bold">
{data.filter(row => row.event === 'conversation_created').length}
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-lg font-medium mb-4">Conversations by User</h3>
<SortableTable
data={_.chain(metrics.userMetrics)
.map(user => ({
name: user.name,
email: user.email,
conversations7d: data.filter(row =>
row.email === user.email &&
row.date >= new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) &&
row.event === 'conversation_created'
).length,
conversations30d: data.filter(row =>
row.email === user.email &&
row.date >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) &&
row.event === 'conversation_created'
).length,
conversationsTotal: user.conversations,
lastSeen: user.lastSeen
}))
.orderBy(['conversationsTotal'], ['desc'])
.value()}
columns={[
{ key: 'name', label: 'User' },
{ key: 'conversations7d', label: 'Last 7 Days' },
{ key: 'conversations30d', label: 'Last 30 Days' },
{ key: 'conversationsTotal', label: 'All Time' },
{ key: 'lastSeen', label: 'Last Seen', format: (date) => date.toLocaleDateString() }
]}
/>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-lg font-medium mb-4">Daily Conversations</h3>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={metrics.dailyConversations}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="conversations" stroke="#3b82f6" name="Conversations" />
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
)}
}
</div>
</div>
);
};
export default Dashboard;
```