Start querying analytics data (#15)

This commit is contained in:
Chris Estreich 2025-05-16 00:44:52 -07:00 committed by GitHub
parent 81d3e375bd
commit f45e93b9ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 78 additions and 244 deletions

0
migrations/.gitkeep Normal file
View file

View file

@ -1,8 +0,0 @@
CREATE TABLE "events" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "events_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"type" text NOT NULL,
"timestamp" bigint NOT NULL,
"properties" json NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);

View file

@ -1,81 +0,0 @@
{
"id": "5ab333f5-a980-4564-a919-3d15608505d0",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.events": {
"name": "events",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "events_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timestamp": {
"name": "timestamp",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"properties": {
"name": "properties",
"type": "json",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -1,13 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1747204609794,
"tag": "0000_hesitant_kylun",
"breakpoints": true
}
]
}

31
src/actions/analytics.ts Normal file
View file

@ -0,0 +1,31 @@
import { z } from 'zod';
import { client } from '@/lib/server/analytics';
const usageSchema = z.array(
z.object({
userId: z.string(),
count: z.coerce.number(),
}),
);
export type Usage = z.infer<typeof usageSchema>;
export const getUsage = async (orgId?: string | null): Promise<Usage> => {
if (!orgId) {
return [];
}
const resultSet = await client.query({
query: `
SELECT userId, COUNT(1) as count
FROM "events"
WHERE orgId = {orgId: String}
GROUP BY 1;
`,
format: 'JSONEachRow',
query_params: { orgId },
});
return usageSchema.parse(await resultSet.json());
};

View file

@ -0,0 +1,28 @@
'use client';
import { useAuth } from '@clerk/nextjs';
import { useTranslations } from 'next-intl';
import type { Usage } from '@/actions/analytics';
import {
TitleBar,
UsageAnalyticsCard,
AuditLogCard,
} from '@/components/dashboard';
export const Dashboard = ({ usage }: { usage: Usage }) => {
const { userId, orgId } = useAuth();
const t = useTranslations('DashboardIndex');
return (
<>
<TitleBar title={t('title_bar')} />
<div>Org: {orgId}</div>
<div>User: {userId}</div>
<pre>Usage: {JSON.stringify(usage, null, 2)}</pre>
<UsageAnalyticsCard />
<AuditLogCard />
</>
);
};

View file

@ -14,7 +14,6 @@ const AnalyticsPageContainer = () => {
title={t('title_bar')}
description={t('title_bar_description')}
/>
<AnalyticsPage />
</>
);

View file

@ -17,7 +17,6 @@ const AuditLogsPage = () => {
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Get filtered logs based on selected time period
const logs = getFilteredLogs(Number(timePeriod));
const handleLogClick = (log: AuditLog) => {
@ -35,7 +34,6 @@ const AuditLogsPage = () => {
title="Audit Logs"
description="View all organization activity and changes"
/>
<div className="w-2/3 rounded-md bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">All Activity</h3>
@ -43,8 +41,6 @@ const AuditLogsPage = () => {
Showing all audit logs for your organization
</p>
</div>
{/* Time period toggle */}
<div className="mb-4 flex space-x-2">
<Button
variant={timePeriod === '7' ? 'default' : 'outline'}
@ -68,8 +64,6 @@ const AuditLogsPage = () => {
Last 90 days
</Button>
</div>
{/* Log entries */}
<div className="space-y-1">
{logs.length > 0 ? (
logs.map((log: AuditLog) => (
@ -82,8 +76,6 @@ const AuditLogsPage = () => {
)}
</div>
</div>
{/* Drawer for log details */}
<Drawer
isOpen={isDrawerOpen}
onClose={handleCloseDrawer}

View file

@ -1,20 +1,11 @@
import { useTranslations } from 'next-intl';
import { auth } from '@clerk/nextjs/server';
import { TitleBar } from '@/components/dashboard/TitleBar';
import { AuditLogCard } from '@/components/dashboard/AuditLogCard';
import { UsageAnalyticsCard } from '@/components/dashboard/UsageAnalyticsCard';
import { getUsage } from '@/actions/analytics';
export default function Page() {
const t = useTranslations('DashboardIndex');
import { Dashboard } from './Dashboard';
return (
<>
<TitleBar
title={t('title_bar')}
description={t('title_bar_description')}
/>
<UsageAnalyticsCard />
<AuditLogCard />
</>
);
export default async function Page() {
const { orgId } = await auth();
const usage = await getUsage(orgId);
return <Dashboard usage={usage} />;
}

View file

@ -1,15 +1,15 @@
import { currentUser } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
import { auth as clerkAuth } from '@clerk/nextjs/server';
export default async function AuthenticatedLayout({
children,
}: {
children: React.ReactNode;
}) {
const auth = await clerkAuth();
const user = await currentUser();
if (!auth.userId) {
redirect('/');
if (!user) {
return redirect('/sign-in');
}
return children;

View file

@ -1,7 +1,7 @@
import { redirect } from 'next/navigation';
import { auth as clerkAuth } from '@clerk/nextjs/server';
import { currentUser } from '@clerk/nextjs/server';
export default async function Page() {
const { userId } = await clerkAuth();
redirect(userId ? '/dashboard' : '/sign-in');
const user = await currentUser();
redirect(user ? '/dashboard' : '/sign-in');
}

View file

@ -0,0 +1,3 @@
export { TitleBar } from './TitleBar';
export { UsageAnalyticsCard } from './UsageAnalyticsCard';
export { AuditLogCard } from './AuditLogCard';

View file

@ -1,41 +0,0 @@
'use client';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui';
import { Section, LocaleSwitcher, ThemeSwitcher } from '@/components/layout';
import { Logo } from './Logo';
export const Navbar = () => {
const t = useTranslations('Navbar');
return (
<Section className="py-6">
<div className="flex flex-wrap items-center justify-between">
<Link href="/">
<Logo />
</Link>
<ul className="flex flex-row items-center gap-x-1.5 text-lg font-medium [&_li[data-fade]:hover]:opacity-100 [&_li[data-fade]]:opacity-60">
<li data-fade>
<LocaleSwitcher />
</li>
<li data-fade>
<ThemeSwitcher />
</li>
<li>
<Button variant="ghost" asChild>
<Link href="/sign-in">{t('sign_in')}</Link>
</Button>
</li>
<li>
<Button asChild>
<Link href="/sign-up">{t('sign_up')}</Link>
</Button>
</li>
</ul>
</div>
</Section>
);
};

View file

@ -1,4 +1,3 @@
export { Navbar } from './Navbar';
export { Footer } from './Footer';
export { Section } from './Section';
export { LocaleSwitcher } from './LocaleSwitcher';

View file

@ -1,38 +0,0 @@
/*
CREATE TABLE default.events
(
-- Shared
`id` UUID,
`orgId` String,
`userId` String,
`timestamp` Int32,
`type` String,
-- App
`appVersion` String,
`vscodeVersion` String,
`platform` String,
`editorName` String,
`language` String,
`mode` String,
-- Task
`taskId` Nullable(String),
`apiProvider` Nullable(String),
`modelId` Nullable(String),
`diffStrategy` Nullable(String),
`isSubtask` Nullable(Bool),
-- Completion
`inputTokens` Nullable(Int32),
`outputTokens` Nullable(Int32),
`cacheReadTokens` Nullable(Int32),
`cacheWriteTokens` Nullable(Int32),
`cost` Nullable(Float32)
)
ENGINE = SharedMergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')
ORDER BY (id, type, timestamp)
SETTINGS index_granularity = 8192;
*/

View file

@ -1,11 +1 @@
import { pgTable, text, timestamp, integer } from 'drizzle-orm/pg-core';
export const usersTable = pgTable('users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
authenticationId: text('authentication_id').notNull(),
createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { mode: 'date' })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
});
// TODO

View file

@ -3,10 +3,6 @@
"meta_title": "Roo Code Cloud",
"meta_description": "Cloud management for Roo Code."
},
"Navbar": {
"sign_in": "Sign In",
"sign_up": "Sign Up"
},
"Footer": {
"terms_of_service": "Terms Of Service",
"privacy_policy": "Privacy Policy"
@ -21,11 +17,6 @@
},
"DashboardIndex": {
"title_bar": "Dashboard",
"title_bar_description": "Welcome to your dashboard",
"message_state_title": "Let's get started",
"message_state_description": "You can customize this page by editing the file at <code>dashboard/page.tsx</code>",
"message_state_button": "Star on GitHub",
"message_state_alternative": "Want more features using the same stack? Try <url></url>.",
"analytics_title": "Usage Analytics",
"analytics_description": "Organization usage metrics and statistics",
"analytics_period_7_days": "Last 7 days",

View file

@ -3,10 +3,6 @@
"meta_title": "Roo Code Cloud",
"meta_description": "Gestion cloud pour Roo Code."
},
"Navbar": {
"sign_in": "Se connecter",
"sign_up": "S'inscrire"
},
"Footer": {
"terms_of_service": "Conditions d'utilisation",
"privacy_policy": "Politique de confidentialité"
@ -21,11 +17,6 @@
},
"DashboardIndex": {
"title_bar": "Tableau de bord",
"title_bar_description": "Bienvenue sur votre tableau de bord",
"message_state_title": "C'est parti",
"message_state_description": "Vous pouvez personnaliser cette page en modifiant le fichier dans <code>dashboard/page.tsx</code>",
"message_state_button": "Mettez une étoile sur GitHub",
"message_state_alternative": "Vous voulez plus de fonctionnalités en utilisant la même stack ? Essayez <url></url>.",
"analytics_title": "Analytiques d'utilisation",
"analytics_description": "Métriques et statistiques d'utilisation de l'organisation",
"analytics_period_7_days": "7 derniers jours",

View file

@ -3,7 +3,7 @@ import { createClient } from '@clickhouse/client';
import { CloudEvent } from '@/schemas';
import { Env } from './env';
const client = createClient({
export const client = createClient({
url: Env.CLICKHOUSE_URL,
username: Env.CLICKHOUSE_USERNAME,
password: Env.CLICKHOUSE_PASSWORD,