mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Start querying analytics data (#15)
This commit is contained in:
parent
81d3e375bd
commit
f45e93b9ec
19 changed files with 78 additions and 244 deletions
0
migrations/.gitkeep
Normal file
0
migrations/.gitkeep
Normal 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
|
||||
);
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
31
src/actions/analytics.ts
Normal 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());
|
||||
};
|
||||
28
src/app/(authenticated)/dashboard/Dashboard.tsx
Normal file
28
src/app/(authenticated)/dashboard/Dashboard.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -14,7 +14,6 @@ const AnalyticsPageContainer = () => {
|
|||
title={t('title_bar')}
|
||||
description={t('title_bar_description')}
|
||||
/>
|
||||
|
||||
<AnalyticsPage />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
3
src/components/dashboard/index.ts
Normal file
3
src/components/dashboard/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { TitleBar } from './TitleBar';
|
||||
export { UsageAnalyticsCard } from './UsageAnalyticsCard';
|
||||
export { AuditLogCard } from './AuditLogCard';
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
export { Navbar } from './Navbar';
|
||||
export { Footer } from './Footer';
|
||||
export { Section } from './Section';
|
||||
export { LocaleSwitcher } from './LocaleSwitcher';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
*/
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue