Skip to content

Commit 64e75ea

Browse files
committed
review fixes
merged master
1 parent d830dbc commit 64e75ea

File tree

8 files changed

+79
-37
lines changed

8 files changed

+79
-37
lines changed

apps/webapp/app/components/logs/LogDetailView.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,30 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet
6767
const environment = useEnvironment();
6868
const fetcher = useTypedFetcher<typeof logDetailLoader>();
6969
const [activeTab, setActiveTab] = useState<TabType>("details");
70+
const [error, setError] = useState<string | null>(null);
7071

7172
// Fetch full log details when logId changes
7273
useEffect(() => {
7374
if (!logId) return;
7475

76+
setError(null);
7577
fetcher.load(
7678
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/logs/${encodeURIComponent(logId)}`
7779
);
7880
// eslint-disable-next-line react-hooks/exhaustive-deps
7981
}, [organization.slug, project.slug, environment.slug, logId]);
8082

83+
// Handle fetch errors
84+
useEffect(() => {
85+
if (fetcher.data && typeof fetcher.data === "object" && "error" in fetcher.data) {
86+
setError(fetcher.data.error as string);
87+
} else if (fetcher.state === "idle" && fetcher.data === null && !initialLog) {
88+
setError("Failed to load log details");
89+
} else {
90+
setError(null);
91+
}
92+
}, [fetcher.data, initialLog, fetcher.state]);
93+
8194
const isLoading = fetcher.state === "loading";
8295
const log = fetcher.data ?? initialLog;
8396

@@ -110,7 +123,7 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet
110123
</Button>
111124
</div>
112125
<div className="flex flex-1 items-center justify-center">
113-
<Paragraph className="text-text-dimmed">Log not found</Paragraph>
126+
<Paragraph className="text-text-dimmed">{error ?? "Log not found"}</Paragraph>
114127
</div>
115128
</div>
116129
);
@@ -255,20 +268,18 @@ function RunTab({ log, runPath }: { log: LogEntry; runPath: string }) {
255268
const project = useProject();
256269
const environment = useEnvironment();
257270
const fetcher = useTypedFetcher<RunContextData>();
258-
const [requested, setRequested] = useState(false);
259271

260272
// Fetch run details when tab is active
261273
useEffect(() => {
262274
if (!log.runId) return;
263275

264-
setRequested(true);
265276
fetcher.load(
266277
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/logs/${encodeURIComponent(log.id)}/run?runId=${encodeURIComponent(log.runId)}`
267278
);
268279
// eslint-disable-next-line react-hooks/exhaustive-deps
269280
}, [organization.slug, project.slug, environment.slug, log.id, log.runId]);
270281

271-
const isLoading = !requested || fetcher.state === "loading";
282+
const isLoading = fetcher.state === "loading";
272283
const runData = fetcher.data?.run;
273284

274285
if (isLoading) {
@@ -388,14 +399,12 @@ function RunTab({ log, runPath }: { log: LogEntry; runPath: string }) {
388399
</Property.Value>
389400
</Property.Item>
390401

391-
{environment && (
392-
<Property.Item>
393-
<Property.Label>Environment</Property.Label>
394-
<Property.Value>
395-
<EnvironmentCombo environment={environment} />
396-
</Property.Value>
397-
</Property.Item>
398-
)}
402+
<Property.Item>
403+
<Property.Label>Environment</Property.Label>
404+
<Property.Value>
405+
<EnvironmentCombo environment={environment} />
406+
</Property.Value>
407+
</Property.Item>
399408

400409
<Property.Item>
401410
<Property.Label>Queue</Property.Label>

apps/webapp/app/components/logs/LogsTable.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Button } from "~/components/primitives/Buttons";
55
import { useEnvironment } from "~/hooks/useEnvironment";
66
import { useOrganization } from "~/hooks/useOrganizations";
77
import { useProject } from "~/hooks/useProject";
8-
import type { LogEntry, LogsListAppliedFilters } from "~/presenters/v3/LogsListPresenter.server";
8+
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
99
import { getLevelColor, highlightSearchText } from "~/utils/logUtils";
1010
import { v3RunSpanPath } from "~/utils/pathBuilder";
1111
import { DateTime } from "../primitives/DateTime";
@@ -131,7 +131,7 @@ export function LogsTable({
131131
{!isLoading && <NoLogs title="No logs found" />}
132132
</TableBlankRow>
133133
) : logs.length === 0 ? (
134-
<BlankState isLoading={isLoading} />
134+
<BlankState isLoading={isLoading} onRefresh={() => window.location.reload()} />
135135
) : (
136136
logs.map((log) => {
137137
const isSelected = selectedLogId === log.id;
@@ -222,9 +222,11 @@ function NoLogs({ title }: { title: string }) {
222222
);
223223
}
224224

225-
function BlankState({ isLoading }: { isLoading?: boolean }) {
225+
function BlankState({ isLoading, onRefresh }: { isLoading?: boolean; onRefresh?: () => void }) {
226226
if (isLoading) return <TableBlankRow colSpan={6}></TableBlankRow>;
227227

228+
const handleRefresh = onRefresh ?? (() => window.location.reload());
229+
228230
return (
229231
<TableBlankRow colSpan={6}>
230232
<div className="flex flex-col items-center justify-center gap-6">
@@ -235,9 +237,7 @@ function BlankState({ isLoading }: { isLoading?: boolean }) {
235237
<Button
236238
LeadingIcon={ArrowPathIcon}
237239
variant="tertiary/medium"
238-
onClick={() => {
239-
window.location.reload();
240-
}}
240+
onClick={handleRefresh}
241241
>
242242
Refresh
243243
</Button>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,8 @@ type RunFiltersProps = {
326326
hasFilters: boolean;
327327
/** Hide the AI search input (useful when replacing with a custom search component) */
328328
hideSearch?: boolean;
329+
/** Custom default period for the time filter (e.g., "1h", "7d") */
330+
defaultPeriod?: string;
329331
};
330332

331333
export function RunsFilters(props: RunFiltersProps) {
@@ -348,7 +350,7 @@ export function RunsFilters(props: RunFiltersProps) {
348350
<FilterMenu {...props} />
349351
{!props.hideSearch && <AIFilterInput />}
350352
<RootOnlyToggle defaultValue={props.rootOnlyDefault} />
351-
<TimeFilter />
353+
<TimeFilter defaultPeriod={props.defaultPeriod} />
352354
<AppliedFilters {...props} />
353355
{hasFilters && (
354356
<Form className="h-6">

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,18 @@ export function timeFilterRenderValues({
260260
return { label, valueLabel, rangeType };
261261
}
262262

263-
export function TimeFilter() {
263+
export interface TimeFilterProps {
264+
defaultPeriod?: string;
265+
}
266+
267+
export function TimeFilter({ defaultPeriod }: TimeFilterProps = {}) {
264268
const { value, del } = useSearchParams();
265269

266270
const { period, from, to, label, valueLabel } = timeFilters({
267271
period: value("period"),
268272
from: value("from"),
269273
to: value("to"),
274+
defaultPeriod,
270275
});
271276

272277
return (
@@ -287,6 +292,7 @@ export function TimeFilter() {
287292
period={period}
288293
from={from}
289294
to={to}
295+
defaultPeriod={defaultPeriod}
290296
/>
291297
)}
292298
</FilterMenuProvider>

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import {
1010
} from "@trigger.dev/database";
1111

1212
// Create a schema that validates TaskRunStatus enum values
13-
const TaskRunStatusSchema = z.array(z.string()).refine(
14-
(val) => val.every((v) => Object.values(TaskRunStatusEnum).includes(v)),
15-
{ message: "Invalid TaskRunStatus values" }
16-
) as unknown as z.ZodSchema<TaskRunStatus[]>;
13+
const TaskRunStatusSchema = z.array(z.nativeEnum(TaskRunStatusEnum));
1714
import parseDuration from "parse-duration";
1815
import { type Direction } from "~/components/ListPagination";
1916
import { timeFilters } from "~/components/runs/v3/SharedFilters";
@@ -25,7 +22,7 @@ import {
2522
convertDateToClickhouseDateTime,
2623
convertClickhouseDateTime64ToJsDate,
2724
} from "~/v3/eventRepository/clickhouseEventRepository.server";
28-
import { kindToLevel, type LogLevel } from "~/utils/logUtils";
25+
import { kindToLevel, type LogLevel, LogLevelSchema } from "~/utils/logUtils";
2926

3027
export type { LogLevel };
3128

@@ -56,6 +53,7 @@ export type LogsListOptions = {
5653
queues?: string[];
5754
machines?: MachinePresetName[];
5855
levels?: LogLevel[];
56+
defaultPeriod?: string;
5957
// search
6058
search?: string;
6159
includeDebugLogs?: boolean;
@@ -82,8 +80,9 @@ export const LogsListOptionsSchema = z.object({
8280
batchId: z.string().optional(),
8381
runId: z.array(z.string()).optional(),
8482
queues: z.array(z.string()).optional(),
85-
machines: z.array(z.string()).optional(),
86-
levels: z.array(z.enum(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CANCELLED"])).optional(),
83+
machines: z.array(MachinePresetName).optional(),
84+
levels: z.array(LogLevelSchema).optional(),
85+
defaultPeriod: z.string().optional(),
8786
search: z.string().max(1000).optional(),
8887
includeDebugLogs: z.boolean().optional(),
8988
direction: z.enum(["forward", "backward"]).optional(),
@@ -106,14 +105,26 @@ type LogCursor = {
106105
runId: string;
107106
};
108107

108+
const LogCursorSchema = z.object({
109+
startTime: z.string(),
110+
traceId: z.string(),
111+
spanId: z.string(),
112+
runId: z.string(),
113+
});
114+
109115
function encodeCursor(cursor: LogCursor): string {
110116
return Buffer.from(JSON.stringify(cursor)).toString("base64");
111117
}
112118

113119
function decodeCursor(cursor: string): LogCursor | null {
114120
try {
115121
const decoded = Buffer.from(cursor, "base64").toString("utf-8");
116-
return JSON.parse(decoded) as LogCursor;
122+
const parsed = JSON.parse(decoded);
123+
const validated = LogCursorSchema.safeParse(parsed);
124+
if (!validated.success) {
125+
return null;
126+
}
127+
return validated.data;
117128
} catch {
118129
return null;
119130
}
@@ -189,12 +200,14 @@ export class LogsListPresenter {
189200
cursor,
190201
pageSize = DEFAULT_PAGE_SIZE,
191202
includeDebugLogs = true,
203+
defaultPeriod,
192204
}: LogsListOptions
193205
) {
194206
const time = timeFilters({
195207
period,
196208
from,
197209
to,
210+
defaultPeriod,
198211
});
199212

200213
let effectiveFrom = time.from;
@@ -418,7 +431,7 @@ export class LogsListPresenter {
418431

419432
if (levels && levels.length > 0) {
420433
const conditions: string[] = [];
421-
const params: Record<string, any> = {};
434+
const params: Record<string, string[]> = {};
422435
const hasErrorOrCancelledLevel = levels.includes("ERROR") || levels.includes("CANCELLED");
423436

424437
for (const level of levels) {
@@ -451,7 +464,7 @@ export class LogsListPresenter {
451464
}
452465

453466
if (conditions.length > 0) {
454-
queryBuilder.where(`(${conditions.join(" OR ")})`, params as any);
467+
queryBuilder.where(`(${conditions.join(" OR ")})`, params);
455468
}
456469
}
457470

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
9393
search,
9494
levels,
9595
includeDebugLogs: isAdmin && showDebug,
96+
defaultPeriod: "1h",
9697
});
9798

9899
const session = await setRootOnlyFilterPreference(filters.rootOnly, request);
@@ -105,6 +106,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
105106
filters,
106107
isAdmin,
107108
showDebug,
109+
defaultPeriod: "1h",
108110
},
109111
{
110112
headers: {
@@ -115,7 +117,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
115117
};
116118

117119
export default function Page() {
118-
const { data, rootOnlyDefault, isAdmin, showDebug } = useTypedLoaderData<typeof loader>();
120+
const { data, rootOnlyDefault, isAdmin, showDebug, defaultPeriod } = useTypedLoaderData<typeof loader>();
119121

120122
return (
121123
<PageContainer>
@@ -154,6 +156,7 @@ export default function Page() {
154156
rootOnlyDefault={rootOnlyDefault}
155157
isAdmin={isAdmin}
156158
showDebug={showDebug}
159+
defaultPeriod={defaultPeriod}
157160
/>
158161
);
159162
}}
@@ -169,11 +172,13 @@ function LogsList({
169172
rootOnlyDefault,
170173
isAdmin,
171174
showDebug,
175+
defaultPeriod,
172176
}: {
173177
list: Awaited<UseDataFunctionReturn<typeof loader>["data"]>;
174178
rootOnlyDefault: boolean;
175179
isAdmin: boolean;
176180
showDebug: boolean;
181+
defaultPeriod?: string;
177182
}) {
178183
const navigation = useNavigation();
179184
const location = useLocation();
@@ -284,6 +289,7 @@ function LogsList({
284289
hasFilters={list.hasFilters}
285290
rootOnlyDefault={rootOnlyDefault}
286291
hideSearch
292+
defaultPeriod={defaultPeriod}
287293
/>
288294
<LogsLevelFilter showDebug={showDebug} />
289295
<LogsSearchInput />
@@ -302,7 +308,6 @@ function LogsList({
302308
<LogsTable
303309
logs={accumulatedLogs}
304310
hasFilters={list.hasFilters}
305-
filters={list.filters}
306311
searchTerm={list.searchTerm}
307312
isLoading={isLoading}
308313
isLoadingMore={fetcher.state === "loading"}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
5353
cursor,
5454
levels,
5555
includeDebugLogs: isAdmin && showDebug,
56+
defaultPeriod: "1h",
5657
}) as any; // Validated by LogsListOptionsSchema at runtime
5758

5859
const presenter = new LogsListPresenter($replica, clickhouseClient);

apps/webapp/app/utils/logUtils.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { createElement, Fragment, type ReactNode } from "react";
2+
import { z } from "zod";
23

3-
export type LogLevel = "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR" | "CANCELLED";
4+
export const LogLevelSchema = z.enum(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CANCELLED"]);
5+
export type LogLevel = z.infer<typeof LogLevelSchema>;
6+
7+
export const validLogLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CANCELLED"];
48

59
// Default styles for search highlighting
610
const DEFAULT_HIGHLIGHT_STYLES: React.CSSProperties = {
@@ -29,6 +33,12 @@ export function highlightSearchText(
2933
return text;
3034
}
3135

36+
// Defense in depth: limit search term length to prevent ReDoS and performance issues
37+
const MAX_SEARCH_LENGTH = 500;
38+
if (searchTerm.length > MAX_SEARCH_LENGTH) {
39+
return text;
40+
}
41+
3242
// Escape special regex characters in search term
3343
const escapedSearch = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3444
const regex = new RegExp(escapedSearch, "gi");
@@ -130,13 +140,9 @@ export function getKindLabel(kind: string): string {
130140
case "SPAN_EVENT":
131141
return "Event";
132142
case "LOG_DEBUG":
133-
return "Log";
134143
case "LOG_INFO":
135-
return "Log";
136144
case "LOG_WARN":
137-
return "Log";
138145
case "LOG_ERROR":
139-
return "Log";
140146
case "LOG_LOG":
141147
return "Log";
142148
case "DEBUG_EVENT":

0 commit comments

Comments
 (0)