Skip to content

Commit 6268d51

Browse files
committed
Errors are now by task
1 parent 4e40640 commit 6268d51

File tree

6 files changed

+305
-67
lines changed

6 files changed

+305
-67
lines changed

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Squares2X2Icon,
2525
TableCellsIcon,
2626
UsersIcon,
27+
BugAntIcon,
2728
} from "@heroicons/react/20/solid";
2829
import { Link, useFetcher, useNavigation } from "@remix-run/react";
2930
import { LayoutGroup, motion } from "framer-motion";
@@ -477,9 +478,9 @@ export function SideMenu({
477478
)}
478479
<SideMenuItem
479480
name="Errors"
480-
icon={ExclamationTriangleIcon}
481-
activeIconColor="text-rose-500"
482-
inactiveIconColor="text-rose-500"
481+
icon={BugAntIcon}
482+
activeIconColor="text-amber-500"
483+
inactiveIconColor="text-amber-500"
483484
to={v3ErrorsPath(organization, project, environment)}
484485
data-action="errors"
485486
isCollapsed={isCollapsed}

apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ const DEFAULT_PAGE_SIZE = 50;
4646
export type ErrorsList = Awaited<ReturnType<ErrorsListPresenter["call"]>>;
4747
export type ErrorGroup = ErrorsList["errorGroups"][0];
4848
export type ErrorsListAppliedFilters = ErrorsList["filters"];
49+
export type ErrorHourlyOccurrences = Awaited<
50+
ReturnType<ErrorsListPresenter["getHourlyOccurrences"]>
51+
>;
52+
export type ErrorHourlyActivity = ErrorHourlyOccurrences[string];
4953

5054
// Cursor for error groups pagination
5155
type ErrorGroupCursor = {
@@ -76,6 +80,15 @@ function decodeCursor(cursor: string): ErrorGroupCursor | null {
7680
}
7781
}
7882

83+
function parseClickHouseDateTime(value: string): Date {
84+
const asNum = Number(value);
85+
if (!isNaN(asNum) && asNum > 1e12) {
86+
return new Date(asNum);
87+
}
88+
// ClickHouse returns 'YYYY-MM-DD HH:mm:ss.SSS' in UTC
89+
return new Date(value.replace(" ", "T") + "Z");
90+
}
91+
7992
function escapeClickHouseString(val: string): string {
8093
return val.replace(/\\/g, "\\\\").replace(/\//g, "\\/").replace(/%/g, "\\%").replace(/_/g, "\\_");
8194
}
@@ -156,18 +169,19 @@ export class ErrorsListPresenter extends BasePresenter {
156169
queryBuilder.where("project_id = {projectId: String}", { projectId });
157170
queryBuilder.where("environment_id = {environmentId: String}", { environmentId });
158171

159-
// Group by error_fingerprint to merge partial aggregations
160-
queryBuilder.groupBy("error_fingerprint");
161-
162-
// Apply HAVING filters (filters on aggregated columns)
163-
// Time range filter - use last_seen_date regular column instead of aggregate
164-
queryBuilder.having("max(last_seen_date) >= now() - INTERVAL {days: Int64} DAY", { days: daysAgo });
165-
166-
// Task filter
172+
// Task filter (task_identifier is part of the key, so use WHERE)
167173
if (tasks && tasks.length > 0) {
168-
queryBuilder.having("anyMerge(sample_task_identifier) IN {tasks: Array(String)}", { tasks });
174+
queryBuilder.where("task_identifier IN {tasks: Array(String)}", { tasks });
169175
}
170176

177+
// Group by key columns to merge partial aggregations
178+
queryBuilder.groupBy("error_fingerprint, task_identifier");
179+
180+
// Time range filter
181+
queryBuilder.having("max(last_seen_date) >= now() - INTERVAL {days: Int64} DAY", {
182+
days: daysAgo,
183+
});
184+
171185
// Search filter - searches in error type and message
172186
if (search && search.trim() !== "") {
173187
const searchTerm = escapeClickHouseString(search.trim()).toLowerCase();
@@ -219,13 +233,12 @@ export class ErrorsListPresenter extends BasePresenter {
219233
errorType: error.error_type,
220234
errorMessage: error.error_message,
221235
fingerprint: error.error_fingerprint,
222-
firstSeen: new Date(parseInt(error.first_seen) * 1000),
223-
lastSeen: new Date(parseInt(error.last_seen) * 1000),
236+
taskIdentifier: error.task_identifier,
237+
firstSeen: parseClickHouseDateTime(error.first_seen),
238+
lastSeen: parseClickHouseDateTime(error.last_seen),
224239
count: error.occurrence_count,
225-
affectedTasks: error.affected_tasks,
226240
sampleRunId: error.sample_run_id,
227241
sampleFriendlyId: error.sample_friendly_id,
228-
sampleTaskIdentifier: error.sample_task_identifier,
229242
}));
230243

231244
return {
@@ -244,4 +257,59 @@ export class ErrorsListPresenter extends BasePresenter {
244257
},
245258
};
246259
}
260+
261+
public async getHourlyOccurrences(
262+
organizationId: string,
263+
projectId: string,
264+
environmentId: string,
265+
fingerprints: string[]
266+
): Promise<Record<string, Array<{ date: Date; count: number }>>> {
267+
if (fingerprints.length === 0) {
268+
return {};
269+
}
270+
271+
const hours = 24;
272+
273+
const [queryError, records] = await this.clickhouse.errors.getHourlyOccurrences({
274+
organizationId,
275+
projectId,
276+
environmentId,
277+
fingerprints,
278+
hours,
279+
});
280+
281+
if (queryError) {
282+
throw queryError;
283+
}
284+
285+
// Build 24 hourly buckets as epoch seconds (UTC, floored to hour)
286+
const buckets: number[] = [];
287+
const nowMs = Date.now();
288+
for (let i = hours - 1; i >= 0; i--) {
289+
const hourStart = Math.floor((nowMs - i * 3_600_000) / 3_600_000) * 3_600;
290+
buckets.push(hourStart);
291+
}
292+
293+
// Index ClickHouse results by fingerprint → epoch → count
294+
const grouped = new Map<string, Map<number, number>>();
295+
for (const row of records ?? []) {
296+
let byHour = grouped.get(row.error_fingerprint);
297+
if (!byHour) {
298+
byHour = new Map();
299+
grouped.set(row.error_fingerprint, byHour);
300+
}
301+
byHour.set(row.hour_epoch, row.count);
302+
}
303+
304+
const result: Record<string, Array<{ date: Date; count: number }>> = {};
305+
for (const fp of fingerprints) {
306+
const byHour = grouped.get(fp);
307+
result[fp] = buckets.map((epoch) => ({
308+
date: new Date(epoch * 1000),
309+
count: byHour?.get(epoch) ?? 0,
310+
}));
311+
}
312+
313+
return result;
314+
}
247315
}

0 commit comments

Comments
 (0)