Skip to content

Commit 1f58e42

Browse files
committed
Runs list filtering by error id
1 parent cad27fb commit 1f58e42

File tree

9 files changed

+168
-11
lines changed

9 files changed

+168
-11
lines changed

apps/webapp/app/components/BulkActionFilterSummary.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,18 @@ export function BulkActionFilterSummary({
228228
/>
229229
);
230230
}
231+
case "errorId": {
232+
return (
233+
<AppliedFilter
234+
variant="minimal/medium"
235+
key={key}
236+
label={"Error ID"}
237+
icon={filterIcon(key)}
238+
value={value}
239+
removable={false}
240+
/>
241+
);
242+
}
231243
default: {
232244
assertNever(typedKey);
233245
}

apps/webapp/app/components/runs/v3/RunFilters.tsx

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as Ariakit from "@ariakit/react";
22
import {
3+
BugAntIcon,
34
CalendarIcon,
45
ClockIcon,
56
FingerPrintIcon,
@@ -181,6 +182,7 @@ export const TaskRunListSearchFilters = z.object({
181182
machines: MachinePresetOrMachinePresetArray.describe(
182183
`Machine presets to filter by (${machines.join(", ")})`
183184
),
185+
errorId: z.string().optional().describe("Error ID to filter runs by (e.g. error_abc123)"),
184186
});
185187

186188
export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>;
@@ -220,6 +222,8 @@ export function filterTitle(filterKey: string) {
220222
return "Machine";
221223
case "versions":
222224
return "Version";
225+
case "errorId":
226+
return "Error ID";
223227
default:
224228
return filterKey;
225229
}
@@ -258,6 +262,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
258262
return <MachineDefaultIcon className="size-4" />;
259263
case "versions":
260264
return <IconRotateClockwise2 className="size-4" />;
265+
case "errorId":
266+
return <BugAntIcon className="size-4" />;
261267
default:
262268
return undefined;
263269
}
@@ -304,6 +310,7 @@ export function getRunFiltersFromSearchParams(
304310
searchParams.getAll("versions").filter((v) => v.length > 0).length > 0
305311
? searchParams.getAll("versions")
306312
: undefined,
313+
errorId: searchParams.get("errorId") ?? undefined,
307314
};
308315

309316
const parsed = TaskRunListSearchFilters.safeParse(params);
@@ -344,7 +351,8 @@ export function RunsFilters(props: RunFiltersProps) {
344351
searchParams.has("scheduleId") ||
345352
searchParams.has("queues") ||
346353
searchParams.has("machines") ||
347-
searchParams.has("versions");
354+
searchParams.has("versions") ||
355+
searchParams.has("errorId");
348356

349357
return (
350358
<div className="flex flex-row flex-wrap items-center gap-1">
@@ -380,6 +388,7 @@ const filterTypes = [
380388
{ name: "batch", title: "Batch ID", icon: <Squares2X2Icon className="size-4" /> },
381389
{ name: "schedule", title: "Schedule ID", icon: <ClockIcon className="size-4" /> },
382390
{ name: "bulk", title: "Bulk action", icon: <ListCheckedIcon className="size-4" /> },
391+
{ name: "error", title: "Error ID", icon: <BugAntIcon className="size-4" /> },
383392
] as const;
384393

385394
type FilterType = (typeof filterTypes)[number]["name"];
@@ -434,6 +443,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) {
434443
<AppliedBatchIdFilter />
435444
<AppliedScheduleIdFilter />
436445
<AppliedBulkActionsFilter bulkActions={bulkActions} />
446+
<AppliedErrorIdFilter />
437447
</>
438448
);
439449
}
@@ -470,6 +480,8 @@ function Menu(props: MenuProps) {
470480
return <ScheduleIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
471481
case "versions":
472482
return <VersionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
483+
case "error":
484+
return <ErrorIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
473485
}
474486
}
475487

@@ -1740,3 +1752,121 @@ function AppliedScheduleIdFilter() {
17401752
</FilterMenuProvider>
17411753
);
17421754
}
1755+
1756+
function ErrorIdDropdown({
1757+
trigger,
1758+
clearSearchValue,
1759+
searchValue,
1760+
onClose,
1761+
}: {
1762+
trigger: ReactNode;
1763+
clearSearchValue: () => void;
1764+
searchValue: string;
1765+
onClose?: () => void;
1766+
}) {
1767+
const [open, setOpen] = useState<boolean | undefined>();
1768+
const { value, replace } = useSearchParams();
1769+
const errorIdValue = value("errorId");
1770+
1771+
const [errorId, setErrorId] = useState(errorIdValue);
1772+
1773+
const apply = useCallback(() => {
1774+
clearSearchValue();
1775+
replace({
1776+
cursor: undefined,
1777+
direction: undefined,
1778+
errorId: errorId === "" ? undefined : errorId?.toString(),
1779+
});
1780+
1781+
setOpen(false);
1782+
}, [errorId, replace]);
1783+
1784+
let error: string | undefined = undefined;
1785+
if (errorId) {
1786+
if (!errorId.startsWith("error_")) {
1787+
error = "Error IDs start with 'error_'";
1788+
}
1789+
}
1790+
1791+
return (
1792+
<SelectProvider virtualFocus={true} open={open} setOpen={setOpen}>
1793+
{trigger}
1794+
<SelectPopover
1795+
hideOnEnter={false}
1796+
hideOnEscape={() => {
1797+
if (onClose) {
1798+
onClose();
1799+
return false;
1800+
}
1801+
1802+
return true;
1803+
}}
1804+
className="max-w-[min(32ch,var(--popover-available-width))]"
1805+
>
1806+
<div className="flex flex-col gap-4 p-3">
1807+
<div className="flex flex-col gap-1">
1808+
<Label>Error ID</Label>
1809+
<Input
1810+
placeholder="error_"
1811+
value={errorId ?? ""}
1812+
onChange={(e) => setErrorId(e.target.value)}
1813+
variant="small"
1814+
className="w-[29ch] font-mono"
1815+
spellCheck={false}
1816+
/>
1817+
{error ? <FormError>{error}</FormError> : null}
1818+
</div>
1819+
<div className="flex justify-between gap-1 border-t border-grid-dimmed pt-3">
1820+
<Button variant="tertiary/small" onClick={() => setOpen(false)}>
1821+
Cancel
1822+
</Button>
1823+
<Button
1824+
disabled={error !== undefined || !errorId}
1825+
variant="secondary/small"
1826+
shortcut={{
1827+
modifiers: ["mod"],
1828+
key: "Enter",
1829+
enabledOnInputElements: true,
1830+
}}
1831+
onClick={() => apply()}
1832+
>
1833+
Apply
1834+
</Button>
1835+
</div>
1836+
</div>
1837+
</SelectPopover>
1838+
</SelectProvider>
1839+
);
1840+
}
1841+
1842+
function AppliedErrorIdFilter() {
1843+
const { value, del } = useSearchParams();
1844+
1845+
if (value("errorId") === undefined) {
1846+
return null;
1847+
}
1848+
1849+
const errorId = value("errorId");
1850+
1851+
return (
1852+
<FilterMenuProvider>
1853+
{(search, setSearch) => (
1854+
<ErrorIdDropdown
1855+
trigger={
1856+
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
1857+
<AppliedFilter
1858+
label="Error ID"
1859+
icon={filterIcon("errorId")}
1860+
value={errorId}
1861+
onRemove={() => del(["errorId", "cursor", "direction"])}
1862+
variant="secondary/small"
1863+
/>
1864+
</Ariakit.Select>
1865+
}
1866+
searchValue={search}
1867+
clearSearchValue={() => setSearch("")}
1868+
/>
1869+
)}
1870+
</FilterMenuProvider>
1871+
);
1872+
}

apps/webapp/app/components/runs/v3/TaskRunsTable.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type RunsTableProps = {
6969
allowSelection?: boolean;
7070
variant?: TableVariant;
7171
disableAdjacentRows?: boolean;
72+
additionalTableState?: Record<string, string>;
7273
};
7374

7475
export function TaskRunsTable({
@@ -81,6 +82,7 @@ export function TaskRunsTable({
8182
isLoading = false,
8283
allowSelection = false,
8384
variant = "dimmed",
85+
additionalTableState,
8486
}: RunsTableProps) {
8587
const organization = useOrganization();
8688
const project = useProject();
@@ -90,7 +92,14 @@ export function TaskRunsTable({
9092
const { value } = useSearchParams();
9193
const location = useOptimisticLocation();
9294
const rootOnly = value("rootOnly") ? `` : `rootOnly=${rootOnlyDefault}`;
93-
const search = rootOnly ? `${rootOnly}&${location.search}` : location.search;
95+
let search = rootOnly ? `${rootOnly}&${location.search}` : location.search;
96+
if (additionalTableState) {
97+
const params = new URLSearchParams(search);
98+
for (const [key, val] of Object.entries(additionalTableState)) {
99+
params.set(key, val);
100+
}
101+
search = params.toString();
102+
}
94103
/** TableState has to be encoded as a separate URI component, so it's merged under one, 'tableState' param */
95104
const tableStateParam = disableAdjacentRows ? '' : encodeURIComponent(search);
96105

apps/webapp/app/presenters/RunFilters.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function getRunFiltersFromRequest(request: Request): Promise<Filter
3535
scheduleId,
3636
queues,
3737
machines,
38+
errorId,
3839
} = TaskRunListSearchFilters.parse(s);
3940

4041
return {
@@ -54,5 +55,6 @@ export async function getRunFiltersFromRequest(request: Request): Promise<Filter
5455
cursor: cursor,
5556
queues,
5657
machines,
58+
errorId,
5759
};
5860
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const errorGroupGranularity = new TimeGranularity([
1010
{ max: "45d", granularity: "1w" },
1111
{ max: "Infinity", granularity: "30d" },
1212
]);
13+
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
1314
import { type PrismaClientOrTransaction } from "@trigger.dev/database";
1415
import { timeFilterFromTo } from "~/components/runs/v3/SharedFilters";
1516
import { type Direction, DirectionSchema } from "~/components/ListPagination";
@@ -286,7 +287,7 @@ export class ErrorGroupPresenter extends BasePresenter {
286287
const result = await runListPresenter.call(organizationId, environmentId, {
287288
userId: options.userId,
288289
projectId: options.projectId,
289-
errorFingerprint: options.fingerprint,
290+
errorId: ErrorId.toFriendlyId(options.fingerprint),
290291
pageSize: options.pageSize,
291292
from: options.from,
292293
to: options.to,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export type RunListOptions = {
3333
runId?: string[];
3434
queues?: string[];
3535
machines?: MachinePresetName[];
36-
errorFingerprint?: string;
36+
errorId?: string;
3737
//pagination
3838
direction?: Direction;
3939
cursor?: string;
@@ -71,7 +71,7 @@ export class NextRunListPresenter {
7171
runId,
7272
queues,
7373
machines,
74-
errorFingerprint,
74+
errorId,
7575
from,
7676
to,
7777
direction = "forward",
@@ -184,7 +184,7 @@ export class NextRunListPresenter {
184184
bulkId,
185185
queues,
186186
machines,
187-
errorFingerprint,
187+
errorId,
188188
page: {
189189
size: pageSize,
190190
cursor,

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export default function Page() {
190190
organizationSlug={organizationSlug}
191191
projectParam={projectParam}
192192
envParam={envParam}
193+
fingerprint={fingerprint}
193194
/>
194195
);
195196
}}
@@ -207,13 +208,15 @@ function ErrorGroupDetail({
207208
organizationSlug,
208209
projectParam,
209210
envParam,
211+
fingerprint,
210212
}: {
211213
errorGroup: ErrorGroupSummary | undefined;
212214
runList: NextRunList | undefined;
213215
activity: Promise<ErrorGroupOccurrences>;
214216
organizationSlug: string;
215217
projectParam: string;
216218
envParam: string;
219+
fingerprint: string;
217220
}) {
218221
if (!errorGroup) {
219222
return (
@@ -324,7 +327,7 @@ function ErrorGroupDetail({
324327
runs={runList.runs}
325328
isLoading={false}
326329
variant="dimmed"
327-
disableAdjacentRows
330+
additionalTableState={{ errorId: ErrorId.toFriendlyId(fingerprint) }}
328331
/>
329332
) : (
330333
<Paragraph variant="small" className="p-4 text-text-dimmed">

apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type ClickhouseQueryBuilder } from "@internal/clickhouse";
2-
import { RunId } from "@trigger.dev/core/v3/isomorphic";
2+
import { ErrorId, RunId } from "@trigger.dev/core/v3/isomorphic";
33
import {
44
type FilterRunsOptions,
55
type IRunsRepository,
@@ -329,9 +329,9 @@ function applyRunFiltersToQueryBuilder<T>(
329329
});
330330
}
331331

332-
if (options.errorFingerprint) {
332+
if (options.errorId) {
333333
queryBuilder.where("error_fingerprint = {errorFingerprint: String}", {
334-
errorFingerprint: options.errorFingerprint,
334+
errorFingerprint: ErrorId.toId(options.errorId),
335335
});
336336
}
337337
}

apps/webapp/app/services/runsRepository/runsRepository.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const RunListInputOptionsSchema = z.object({
4444
bulkId: z.string().optional(),
4545
queues: z.array(z.string()).optional(),
4646
machines: MachinePresetName.array().optional(),
47-
errorFingerprint: z.string().optional(),
47+
errorId: z.string().optional(),
4848
});
4949

5050
export type RunListInputOptions = z.infer<typeof RunListInputOptionsSchema>;

0 commit comments

Comments
 (0)