Skip to content

Commit f2e2cb6

Browse files
authored
feat: read usage from cache priority (#963)
* feat: read usage from cache priority * feat: remove log * feat: conditionally fetch workspace usage
1 parent c1fd338 commit f2e2cb6

File tree

6 files changed

+125
-105
lines changed

6 files changed

+125
-105
lines changed

frontend/app/project/[projectId]/layout.tsx

Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import "@/app/globals.css";
22

3-
import { eq } from "drizzle-orm";
43
import { cookies } from "next/headers";
54
import { redirect } from "next/navigation";
65
import { getServerSession } from "next-auth";
@@ -13,79 +12,11 @@ import ProjectUsageBanner from "@/components/project/usage-banner";
1312
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
1413
import { ProjectContextProvider } from "@/contexts/project-context";
1514
import { UserContextProvider } from "@/contexts/user-context";
15+
import { getProjectDetails } from "@/lib/actions/project";
1616
import { getProjectsByWorkspace } from "@/lib/actions/projects";
17-
import { getWorkspaceInfo, getWorkspaceUsage } from "@/lib/actions/workspace";
17+
import { getWorkspaceInfo } from "@/lib/actions/workspace";
1818
import { authOptions } from "@/lib/auth";
19-
import { db } from "@/lib/db/drizzle";
20-
import { projects, subscriptionTiers, workspaces } from "@/lib/db/migrations/schema";
2119
import { Feature, isFeatureEnabled } from "@/lib/features/features";
22-
import { GetProjectResponse } from "@/lib/workspaces/types";
23-
24-
async function getProjectDetails(projectId: string): Promise<GetProjectResponse> {
25-
const projectResult = await db
26-
.select({
27-
id: projects.id,
28-
name: projects.name,
29-
workspaceId: projects.workspaceId,
30-
})
31-
.from(projects)
32-
.where(eq(projects.id, projectId))
33-
.limit(1);
34-
35-
if (projectResult.length === 0) {
36-
throw new Error("Project not found");
37-
}
38-
const project = projectResult[0];
39-
40-
const workspaceResult = await db
41-
.select({
42-
id: workspaces.id,
43-
tierId: workspaces.tierId,
44-
})
45-
.from(workspaces)
46-
.where(eq(workspaces.id, project.workspaceId))
47-
.limit(1);
48-
49-
if (workspaceResult.length === 0) {
50-
throw new Error("Workspace not found for project");
51-
}
52-
const workspace = workspaceResult[0];
53-
const usageResult = await getWorkspaceUsage(project.workspaceId);
54-
55-
const tierResult = await db
56-
.select({
57-
name: subscriptionTiers.name,
58-
stepsLimit: subscriptionTiers.steps,
59-
bytesLimit: subscriptionTiers.bytesIngested,
60-
})
61-
.from(subscriptionTiers)
62-
.where(eq(subscriptionTiers.id, workspace.tierId))
63-
.limit(1);
64-
65-
if (tierResult.length === 0) {
66-
throw new Error("Subscription tier not found for workspace");
67-
}
68-
const tier = tierResult[0];
69-
70-
// Convert bytes to GB (1 GB = 1024^3 bytes)
71-
const bytesToGB = (bytes: number): number => bytes / (1024 * 1024 * 1024);
72-
73-
const gbUsedThisMonth = bytesToGB(
74-
Number(
75-
usageResult.spansBytesIngested + usageResult.browserSessionEventsBytesIngested + usageResult.eventsBytesIngested
76-
)
77-
);
78-
const gbLimit = bytesToGB(Number(tier.bytesLimit));
79-
80-
return {
81-
id: project.id,
82-
name: project.name,
83-
workspaceId: project.workspaceId,
84-
gbUsedThisMonth,
85-
gbLimit,
86-
isFreeTier: tier.name.toLowerCase().trim() === "free",
87-
};
88-
}
8920

9021
export default async function ProjectIdLayout(props: { children: ReactNode; params: Promise<{ projectId: string }> }) {
9122
const params = await props.params;

frontend/lib/actions/project/index.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { eq } from "drizzle-orm";
22
import { z } from "zod/v4";
33

4+
import { getWorkspaceUsage } from "@/lib/actions/workspace";
45
import { cache, PROJECT_API_KEY_CACHE_KEY, PROJECT_CACHE_KEY } from "@/lib/cache";
56
import { clickhouseClient } from "@/lib/clickhouse/client";
67
import { db } from "@/lib/db/drizzle";
7-
import { projectApiKeys, projects } from "@/lib/db/migrations/schema";
8+
import { projectApiKeys, projects, subscriptionTiers, workspaces } from "@/lib/db/migrations/schema";
89

910
export const DeleteProjectSchema = z.object({
1011
projectId: z.uuid(),
@@ -162,3 +163,87 @@ async function deleteProjectWorkspaceInfoFromCache(projectId: string) {
162163
const cacheKey = `${PROJECT_CACHE_KEY}:${projectId}`;
163164
await cache.remove(cacheKey);
164165
}
166+
167+
export const getProjectDetails = async (
168+
projectId: string
169+
): Promise<{
170+
id: string;
171+
name: string;
172+
workspaceId: string;
173+
gbUsedThisMonth: number;
174+
gbLimit: number;
175+
isFreeTier: boolean;
176+
}> => {
177+
const projectResult = await db
178+
.select({
179+
id: projects.id,
180+
name: projects.name,
181+
workspaceId: projects.workspaceId,
182+
})
183+
.from(projects)
184+
.where(eq(projects.id, projectId))
185+
.limit(1);
186+
187+
if (projectResult.length === 0) {
188+
throw new Error("Project not found");
189+
}
190+
191+
const project = projectResult[0];
192+
193+
const workspaceResult = await db
194+
.select({
195+
id: workspaces.id,
196+
tierId: workspaces.tierId,
197+
})
198+
.from(workspaces)
199+
.where(eq(workspaces.id, project.workspaceId))
200+
.limit(1);
201+
202+
if (workspaceResult.length === 0) {
203+
throw new Error("Workspace not found for project");
204+
}
205+
const workspace = workspaceResult[0];
206+
207+
const tierResult = await db
208+
.select({
209+
name: subscriptionTiers.name,
210+
stepsLimit: subscriptionTiers.steps,
211+
bytesLimit: subscriptionTiers.bytesIngested,
212+
})
213+
.from(subscriptionTiers)
214+
.where(eq(subscriptionTiers.id, workspace.tierId))
215+
.limit(1);
216+
217+
if (tierResult.length === 0) {
218+
throw new Error("Subscription tier not found for workspace");
219+
}
220+
const tier = tierResult[0];
221+
const isFreeTier = tier.name.toLowerCase().trim() === "free";
222+
223+
const bytesToGB = (bytes: number): number => bytes / (1024 * 1024 * 1024);
224+
const gbLimit = bytesToGB(Number(tier.bytesLimit));
225+
226+
if (!isFreeTier) {
227+
return {
228+
id: project.id,
229+
name: project.name,
230+
workspaceId: project.workspaceId,
231+
// not used in ui
232+
gbUsedThisMonth: 0,
233+
gbLimit,
234+
isFreeTier,
235+
};
236+
}
237+
238+
const usageResult = await getWorkspaceUsage(project.workspaceId);
239+
const gbUsedThisMonth = bytesToGB(usageResult.totalBytesIngested);
240+
241+
return {
242+
id: project.id,
243+
name: project.name,
244+
workspaceId: project.workspaceId,
245+
gbUsedThisMonth,
246+
gbLimit,
247+
isFreeTier,
248+
};
249+
};

frontend/lib/actions/workspace/index.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { z } from "zod/v4";
55
import { deleteProject } from "@/lib/actions/project";
66
import { checkUserWorkspaceRole } from "@/lib/actions/workspace/utils";
77
import { completeMonthsElapsed } from "@/lib/actions/workspaces/utils";
8+
import { cache, WORKSPACE_BYTES_USAGE_CACHE_KEY } from "@/lib/cache";
89
import { clickhouseClient } from "@/lib/clickhouse/client";
910
import { db } from "@/lib/db/drizzle";
1011
import { membersOfWorkspaces, projects, subscriptionTiers, users, workspaces } from "@/lib/db/migrations/schema";
@@ -129,13 +130,6 @@ export const getWorkspaceInfo = async (workspaceId: string): Promise<Workspace>
129130
};
130131

131132
export const getWorkspaceUsage = async (workspaceId: string): Promise<WorkspaceUsage> => {
132-
const projectIds = await db.query.projects.findMany({
133-
where: eq(projects.workspaceId, workspaceId),
134-
columns: {
135-
id: true,
136-
},
137-
});
138-
139133
const resetTime = await db.query.workspaces.findFirst({
140134
where: eq(workspaces.id, workspaceId),
141135
columns: {
@@ -147,18 +141,38 @@ export const getWorkspaceUsage = async (workspaceId: string): Promise<WorkspaceU
147141
throw new Error("Workspace not found");
148142
}
149143

144+
const resetTimeDate = new Date(resetTime.resetTime);
145+
const latestResetTime = addMonths(resetTimeDate, completeMonthsElapsed(resetTimeDate, new Date()));
146+
147+
const cacheKey = `${WORKSPACE_BYTES_USAGE_CACHE_KEY}:${workspaceId}`;
148+
try {
149+
const cachedUsage = await cache.get<number>(cacheKey);
150+
if (cachedUsage !== null) {
151+
return {
152+
totalBytesIngested: Number(cachedUsage),
153+
resetTime: latestResetTime,
154+
};
155+
}
156+
} catch (error) {
157+
// If cache fails, continue to ClickHouse query
158+
console.error("Error reading from cache:", error);
159+
}
160+
161+
// Cache miss - query ClickHouse for the actual breakdown
162+
const projectIds = await db.query.projects.findMany({
163+
where: eq(projects.workspaceId, workspaceId),
164+
columns: {
165+
id: true,
166+
},
167+
});
168+
150169
if (projectIds.length === 0) {
151170
return {
152-
spansBytesIngested: 0,
153-
browserSessionEventsBytesIngested: 0,
154-
eventsBytesIngested: 0,
155-
resetTime: new Date(resetTime.resetTime),
171+
totalBytesIngested: 0,
172+
resetTime: latestResetTime,
156173
};
157174
}
158175

159-
const resetTimeDate = new Date(resetTime.resetTime);
160-
161-
const latestResetTime = addMonths(resetTimeDate, completeMonthsElapsed(resetTimeDate, new Date()));
162176
const query = `WITH spans_bytes_ingested AS (
163177
SELECT
164178
SUM(spans.size_bytes) as spans_bytes_ingested
@@ -205,10 +219,13 @@ export const getWorkspaceUsage = async (workspaceId: string): Promise<WorkspaceU
205219
throw new Error("Error getting workspace usage");
206220
}
207221

222+
const totalBytesIngested =
223+
Number(result[0].spans_bytes_ingested) +
224+
Number(result[0].browser_session_events_bytes_ingested) +
225+
Number(result[0].events_bytes_ingested);
226+
208227
return {
209-
spansBytesIngested: Number(result[0].spans_bytes_ingested),
210-
browserSessionEventsBytesIngested: Number(result[0].browser_session_events_bytes_ingested),
211-
eventsBytesIngested: Number(result[0].events_bytes_ingested),
228+
totalBytesIngested,
212229
resetTime: latestResetTime,
213230
};
214231
};

frontend/lib/cache.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,6 @@ export const PROJECT_API_KEY_CACHE_KEY = "project_api_key";
118118
export const PROJECT_EVALUATORS_BY_PATH_CACHE_KEY = "project_evaluators_by_path";
119119
export const PROJECT_CACHE_KEY = "project";
120120
export const WORKSPACE_LIMITS_CACHE_KEY = "workspace_limits";
121+
export const WORKSPACE_BYTES_USAGE_CACHE_KEY = "workspace_bytes_usage";
121122
export const TRACE_CHATS_CACHE_KEY = "trace_chats";
122123
export const TRACE_SUMMARIES_CACHE_KEY = "trace_summaries";

frontend/lib/usage/workspace-stats.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ export async function getWorkspaceStats(workspaceId: string): Promise<WorkspaceS
3838

3939
const usage = await getWorkspaceUsage(workspaceId);
4040

41-
const gbUsedThisMonth = bytesToGB(
42-
Number(usage.spansBytesIngested + usage.browserSessionEventsBytesIngested + usage.eventsBytesIngested)
43-
);
41+
const gbUsedThisMonth = bytesToGB(usage.totalBytesIngested);
4442
const gbLimit = bytesToGB(Number(limits.bytesLimit));
4543

4644
// Calculate GB overages

frontend/lib/workspaces/types.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,13 @@ export interface WorkspaceInvitation {
4141
createdAt: string;
4242
}
4343

44-
export type GetProjectResponse = {
45-
id: string;
46-
name: string;
47-
workspaceId: string;
48-
// New GB-based usage fields
49-
gbUsedThisMonth: number;
50-
gbLimit: number;
51-
isFreeTier: boolean;
52-
};
53-
5444
export interface ProjectStats {
5545
datasetsCount: number;
5646
spansCount: number;
5747
evaluationsCount: number;
5848
}
5949

6050
export interface WorkspaceUsage {
61-
spansBytesIngested: number;
62-
browserSessionEventsBytesIngested: number;
63-
eventsBytesIngested: number;
51+
totalBytesIngested: number;
6452
resetTime: Date;
6553
}

0 commit comments

Comments
 (0)