166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
import { type ErrorGroupListItem } from "@trigger.dev/core/v3";
|
|
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
|
|
import {
|
|
type ErrorGroupStatus,
|
|
type Project,
|
|
type RuntimeEnvironment,
|
|
} from "@trigger.dev/database";
|
|
import { z } from "zod";
|
|
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
|
|
import { getCurrentPlan } from "~/services/platform.v3.server";
|
|
import { CoercedDate } from "~/utils/zod";
|
|
import { ErrorsListPresenter, type ErrorsListOptions } from "./ErrorsListPresenter.server";
|
|
import { BasePresenter } from "./basePresenter.server";
|
|
|
|
// API status (lowercase) <-> DB ErrorGroupState.status (uppercase).
|
|
const API_STATUS_TO_DB: Record<string, ErrorGroupStatus> = {
|
|
unresolved: "UNRESOLVED",
|
|
resolved: "RESOLVED",
|
|
ignored: "IGNORED",
|
|
};
|
|
|
|
const DB_STATUS_TO_API: Record<ErrorGroupStatus, ErrorGroupListItem["status"]> = {
|
|
UNRESOLVED: "unresolved",
|
|
RESOLVED: "resolved",
|
|
IGNORED: "ignored",
|
|
};
|
|
|
|
export const ApiErrorListSearchParams = z.object({
|
|
"page[size]": z.coerce.number().int().positive().min(1).max(100).optional(),
|
|
"page[after]": z.string().optional(),
|
|
"page[before]": z.string().optional(),
|
|
"filter[taskIdentifier]": z
|
|
.string()
|
|
.optional()
|
|
.transform((value) => (value ? value.split(",") : undefined)),
|
|
"filter[version]": z
|
|
.string()
|
|
.optional()
|
|
.transform((value) => (value ? value.split(",") : undefined)),
|
|
"filter[status]": z
|
|
.string()
|
|
.optional()
|
|
.transform((value, ctx) => {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
|
|
const statuses = value.split(",");
|
|
// hasOwnProperty, not `in`: `in` walks the prototype chain, so
|
|
// `filter[status]=toString` would pass and map to a function.
|
|
const invalid = statuses.filter(
|
|
(status) => !Object.prototype.hasOwnProperty.call(API_STATUS_TO_DB, status)
|
|
);
|
|
|
|
if (invalid.length > 0) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `Invalid status values: ${invalid.join(
|
|
", "
|
|
)}. Allowed: unresolved, resolved, ignored.`,
|
|
});
|
|
return z.NEVER;
|
|
}
|
|
|
|
return Array.from(new Set(statuses.map((status) => API_STATUS_TO_DB[status])));
|
|
}),
|
|
"filter[search]": z.string().max(1000).optional(),
|
|
"filter[period]": z.string().optional(),
|
|
"filter[from]": CoercedDate,
|
|
"filter[to]": CoercedDate,
|
|
});
|
|
|
|
type ApiErrorListSearchParams = z.infer<typeof ApiErrorListSearchParams>;
|
|
|
|
export class ApiErrorListPresenter extends BasePresenter {
|
|
public async call(
|
|
project: Pick<Project, "id" | "organizationId">,
|
|
environment: Pick<RuntimeEnvironment, "id" | "organizationId">,
|
|
searchParams: ApiErrorListSearchParams
|
|
): Promise<{
|
|
data: ErrorGroupListItem[];
|
|
pagination: { next?: string; previous?: string };
|
|
}> {
|
|
return this.trace("call", async () => {
|
|
const options: ErrorsListOptions = {
|
|
projectId: project.id,
|
|
defaultPeriod: "1d",
|
|
};
|
|
|
|
if (searchParams["page[size]"]) {
|
|
options.pageSize = searchParams["page[size]"];
|
|
}
|
|
|
|
if (searchParams["page[after]"]) {
|
|
options.cursor = searchParams["page[after]"];
|
|
options.direction = "forward";
|
|
}
|
|
|
|
if (searchParams["page[before]"]) {
|
|
options.cursor = searchParams["page[before]"];
|
|
options.direction = "backward";
|
|
}
|
|
|
|
if (searchParams["filter[taskIdentifier]"]) {
|
|
options.tasks = searchParams["filter[taskIdentifier]"];
|
|
}
|
|
|
|
if (searchParams["filter[version]"]) {
|
|
options.versions = searchParams["filter[version]"];
|
|
}
|
|
|
|
if (searchParams["filter[status]"]) {
|
|
options.statuses = searchParams["filter[status]"];
|
|
}
|
|
|
|
if (searchParams["filter[search]"]) {
|
|
options.search = searchParams["filter[search]"];
|
|
}
|
|
|
|
if (searchParams["filter[period]"]) {
|
|
options.period = searchParams["filter[period]"];
|
|
}
|
|
|
|
if (searchParams["filter[from]"]) {
|
|
options.from = searchParams["filter[from]"].getTime();
|
|
}
|
|
|
|
if (searchParams["filter[to]"]) {
|
|
options.to = searchParams["filter[to]"].getTime();
|
|
}
|
|
|
|
const organizationId = environment.organizationId;
|
|
|
|
const plan = await getCurrentPlan(organizationId);
|
|
options.retentionLimitDays = plan?.v3Subscription?.plan?.limits.logRetentionDays.number ?? 30;
|
|
|
|
// The errors data lives in the "logs" ClickHouse client, matching the
|
|
// dashboard list loader.
|
|
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
|
|
organizationId,
|
|
"logs"
|
|
);
|
|
|
|
const presenter = new ErrorsListPresenter(this._replica, clickhouse);
|
|
const result = await presenter.call(organizationId, environment.id, options);
|
|
|
|
return {
|
|
data: result.errorGroups.map((group) => ({
|
|
id: ErrorId.toFriendlyId(group.fingerprint),
|
|
fingerprint: group.fingerprint,
|
|
taskIdentifier: group.taskIdentifier,
|
|
errorType: group.errorType,
|
|
errorMessage: group.errorMessage,
|
|
status: DB_STATUS_TO_API[group.status],
|
|
count: group.count,
|
|
firstSeen: group.firstSeen,
|
|
lastSeen: group.lastSeen,
|
|
resolvedAt: group.resolvedAt,
|
|
ignoredUntil: group.ignoredUntil,
|
|
})),
|
|
pagination: result.pagination,
|
|
};
|
|
});
|
|
}
|
|
}
|