Skip to content

Commit bbbed41

Browse files
committed
respond to devin comments
1 parent a02be44 commit bbbed41

12 files changed

Lines changed: 221 additions & 409 deletions

File tree

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ServiceValidationError } from "~/v3/services/baseService.server";
1818
import {
1919
escapeClickHouseLike,
2020
hasMinimumLogsSearchLength,
21+
logsSearchExpansionPeriod,
2122
LOGS_SEARCH_RETRY_OVERFETCH_FACTOR,
2223
MIN_LOGS_SEARCH_LENGTH,
2324
normalizeLogsSearchTerm,
@@ -302,7 +303,7 @@ export class LogsListPresenter extends BasePresenter {
302303
});
303304
} else {
304305
queryBuilder.where(
305-
"(lowerUTF8(message) LIKE {searchPattern: String} OR lowerUTF8(attributes_text) LIKE {searchPattern: String})",
306+
"(lower(message) LIKE {searchPattern: String} OR lower(attributes_text) LIKE {searchPattern: String})",
306307
{ searchPattern: `%${searchTerm}%` }
307308
);
308309
}
@@ -433,6 +434,11 @@ export class LogsListPresenter extends BasePresenter {
433434
};
434435
});
435436

437+
const searchExpansion =
438+
searchTerm !== undefined && time.isDefault && transformedLogs.length === 0
439+
? logsSearchExpansionPeriod(effectiveFrom, clampedTo, retentionLimitDays)
440+
: undefined;
441+
436442
return {
437443
logs: transformedLogs,
438444
pagination: {
@@ -455,10 +461,7 @@ export class LogsListPresenter extends BasePresenter {
455461
hasFilters,
456462
hasAnyLogs: transformedLogs.length > 0,
457463
searchTerm: search,
458-
searchExpansion:
459-
searchTerm !== undefined && time.isDefault && transformedLogs.length === 0
460-
? { nextPeriod: `${Math.min(retentionLimitDays ?? 7, 7)}d` }
461-
: undefined,
464+
searchExpansion: searchExpansion ? { nextPeriod: searchExpansion } : undefined,
462465
retention:
463466
retentionLimitDays !== undefined
464467
? {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ import { MIN_LOGS_SEARCH_LENGTH, normalizeLogsSearchTerm } from "~/utils/logSear
5050
// Valid log levels for filtering
5151
const validLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
5252

53+
function formatSearchPeriod(period: string): string {
54+
const days = Number(period.replace("d", ""));
55+
return days === 1 ? "day" : `${days} days`;
56+
}
57+
5358
function parseLevelsFromUrl(url: URL): LogLevel[] | undefined {
5459
const levelParams = url.searchParams.getAll("levels").filter((v) => v.length > 0);
5560
if (levelParams.length === 0) return undefined;
@@ -448,7 +453,7 @@ function LogsList({
448453
className="m-2 mb-0"
449454
cta={
450455
<Button variant="tertiary/small" onClick={expandSearch}>
451-
Search last {list.searchExpansion.nextPeriod.replace("d", " days")}
456+
Search last {formatSearchPeriod(list.searchExpansion.nextPeriod)}
452457
</Button>
453458
}
454459
>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,9 @@ export class LogsSearchProjector {
205205
}
206206

207207
async pause(): Promise<LogsSearchProjectorStatus> {
208-
if (!(await this.stateStore.find())) return uninitializedProjectorStatus();
208+
if (!(await this.stateStore.find())) {
209+
throw new LogsSearchProjectorConflictError("Logs search projector is not initialized");
210+
}
209211
await this.stateStore.pause();
210212
return this.readStatus(false);
211213
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { prisma } from "~/db.server";
12
import { env } from "~/env.server";
23
import { getLogsSearchProjectorClickhouseClient } from "~/services/clickhouse/clickhouseFactory.server";
34
import { LogsSearchProjector } from "~/services/logsSearchProjector.server";
@@ -28,7 +29,7 @@ function initializeLogsSearchProjector() {
2829
maxBackfillRangeMs: env.LOGS_SEARCH_PROJECTOR_MAX_BACKFILL_RANGE_DAYS * 24 * 60 * 60 * 1000,
2930
maxBackfillAgeMs: env.LOGS_SEARCH_PROJECTOR_MAX_BACKFILL_AGE_DAYS * 24 * 60 * 60 * 1000,
3031
},
31-
new PrismaLogsSearchProjectorStateStore(),
32+
new PrismaLogsSearchProjectorStateStore(prisma),
3233
async (window) => {
3334
const [error, result] = await clickhouse.taskEventsSearch.projectV2Window(window, limits);
3435
if (error) throw error;

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { prisma } from "~/db.server";
1+
import type { PrismaClient } from "@trigger.dev/database";
22
import {
33
LOGS_SEARCH_PROJECTOR_STATE_ID,
44
type LogsSearchProjectorState,
55
type LogsSearchProjectorStateStore,
66
} from "~/services/logsSearchProjector.server";
77

8+
type LogsSearchProjectorDatabase = Pick<PrismaClient, "logsSearchProjectorState" | "$executeRaw">;
9+
810
export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorStateStore {
11+
constructor(private readonly database: LogsSearchProjectorDatabase) {}
12+
913
async initialize(boundary: Date): Promise<LogsSearchProjectorState> {
10-
return prisma.logsSearchProjectorState.upsert({
14+
return this.database.logsSearchProjectorState.upsert({
1115
where: { id: LOGS_SEARCH_PROJECTOR_STATE_ID },
1216
create: {
1317
id: LOGS_SEARCH_PROJECTOR_STATE_ID,
@@ -19,7 +23,7 @@ export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorS
1923
}
2024

2125
async find(): Promise<LogsSearchProjectorState | null> {
22-
return prisma.logsSearchProjectorState.findFirst({
26+
return this.database.logsSearchProjectorState.findFirst({
2327
where: { id: LOGS_SEARCH_PROJECTOR_STATE_ID },
2428
});
2529
}
@@ -31,7 +35,7 @@ export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorS
3135
}
3236

3337
async acquireLease(token: string, leaseDurationMs: number): Promise<boolean> {
34-
const count = await prisma.$executeRaw`
38+
const count = await this.database.$executeRaw`
3539
UPDATE "LogsSearchProjectorState"
3640
SET
3741
"leaseToken" = ${token},
@@ -49,7 +53,7 @@ export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorS
4953
}
5054

5155
async renewLease(token: string, leaseDurationMs: number): Promise<boolean> {
52-
const count = await prisma.$executeRaw`
56+
const count = await this.database.$executeRaw`
5357
UPDATE "LogsSearchProjectorState"
5458
SET
5559
"leaseExpiresAt" = CURRENT_TIMESTAMP + (${leaseDurationMs} * INTERVAL '1 millisecond'),
@@ -62,14 +66,14 @@ export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorS
6266
}
6367

6468
async releaseLease(token: string): Promise<void> {
65-
await prisma.logsSearchProjectorState.updateMany({
69+
await this.database.logsSearchProjectorState.updateMany({
6670
where: { id: LOGS_SEARCH_PROJECTOR_STATE_ID, leaseToken: token },
6771
data: { leaseToken: null, leaseExpiresAt: null },
6872
});
6973
}
7074

7175
async advanceLive(token: string, expected: Date, next: Date): Promise<boolean> {
72-
const result = await prisma.logsSearchProjectorState.updateMany({
76+
const result = await this.database.logsSearchProjectorState.updateMany({
7377
where: {
7478
id: LOGS_SEARCH_PROJECTOR_STATE_ID,
7579
paused: false,
@@ -87,7 +91,7 @@ export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorS
8791
next: Date,
8892
expectedTarget: Date
8993
): Promise<boolean> {
90-
const result = await prisma.logsSearchProjectorState.updateMany({
94+
const result = await this.database.logsSearchProjectorState.updateMany({
9195
where: {
9296
id: LOGS_SEARCH_PROJECTOR_STATE_ID,
9397
paused: false,
@@ -104,21 +108,21 @@ export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorS
104108
}
105109

106110
async pause(): Promise<void> {
107-
await prisma.logsSearchProjectorState.update({
111+
await this.database.logsSearchProjectorState.update({
108112
where: { id: LOGS_SEARCH_PROJECTOR_STATE_ID },
109113
data: { paused: true },
110114
});
111115
}
112116

113117
async resume(): Promise<void> {
114-
await prisma.logsSearchProjectorState.update({
118+
await this.database.logsSearchProjectorState.update({
115119
where: { id: LOGS_SEARCH_PROJECTOR_STATE_ID },
116120
data: { paused: false },
117121
});
118122
}
119123

120124
async setBackfillTarget(expectedHistorical: Date, target: Date): Promise<boolean> {
121-
const result = await prisma.logsSearchProjectorState.updateMany({
125+
const result = await this.database.logsSearchProjectorState.updateMany({
122126
where: {
123127
id: LOGS_SEARCH_PROJECTOR_STATE_ID,
124128
historicalWatermark: expectedHistorical,
@@ -130,7 +134,7 @@ export class PrismaLogsSearchProjectorStateStore implements LogsSearchProjectorS
130134
}
131135

132136
async cancelBackfill(): Promise<void> {
133-
await prisma.logsSearchProjectorState.update({
137+
await this.database.logsSearchProjectorState.update({
134138
where: { id: LOGS_SEARCH_PROJECTOR_STATE_ID },
135139
data: { backfillTarget: null },
136140
});

apps/webapp/app/utils/logSearch.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import {
33
escapeClickHouseLike,
44
hasMinimumLogsSearchLength,
5+
logsSearchExpansionPeriod,
56
normalizeLogsSearchTerm,
67
prepareLogsSearchPage,
78
} from "./logSearch";
@@ -10,7 +11,9 @@ describe("log search normalization", () => {
1011
it("normalizes punctuation while preserving unicode, paths, and ids", () => {
1112
expect(
1213
normalizeLogsSearchTerm("TypeError: Zahlungsübersicht failed, retrying (/api/orders/42)")
13-
).toBe("typeerror: zahlungsübersicht failed retrying /api/orders/42");
14+
).toBe("typeerror:zahlungsübersicht failed retrying /api/orders/42");
15+
expect(normalizeLogsSearchTerm('"status_code": 500')).toBe("status_code:500");
16+
expect(normalizeLogsSearchTerm("status_code:500")).toBe("status_code:500");
1417
});
1518

1619
it("escapes LIKE wildcards without escaping path separators", () => {
@@ -24,6 +27,14 @@ describe("log search normalization", () => {
2427
expect(hasMinimumLogsSearchLength("日本語")).toBe(true);
2528
});
2629

30+
it("only offers a strictly wider retained search range", () => {
31+
const to = new Date("2026-08-14T12:00:00.000Z");
32+
33+
expect(logsSearchExpansionPeriod(new Date("2026-08-14T11:00:00.000Z"), to, 1)).toBe("1d");
34+
expect(logsSearchExpansionPeriod(new Date("2026-08-13T12:00:00.000Z"), to, 1)).toBeUndefined();
35+
expect(logsSearchExpansionPeriod(new Date("2026-08-13T12:00:00.000Z"), to, 7)).toBe("7d");
36+
});
37+
2738
it("removes projector retry copies after bounded overfetch", () => {
2839
const row = (fingerprint: string) => ({
2940
projection_fingerprint_string: fingerprint,

apps/webapp/app/utils/logSearch.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
export const MIN_LOGS_SEARCH_LENGTH = 3;
22
export const LOGS_SEARCH_RETRY_OVERFETCH_FACTOR = 4;
3+
const DAY_MS = 24 * 60 * 60 * 1000;
4+
const RANGE_COMPARISON_TOLERANCE_MS = 1000;
5+
6+
export function logsSearchExpansionPeriod(
7+
from: Date | undefined,
8+
to: Date,
9+
retentionLimitDays: number | undefined
10+
): string | undefined {
11+
if (!from) return undefined;
12+
13+
const candidateDays = Math.min(retentionLimitDays ?? 7, 7);
14+
const currentRangeMs = Math.max(0, to.getTime() - from.getTime());
15+
if (candidateDays * DAY_MS <= currentRangeMs + RANGE_COMPARISON_TOLERANCE_MS) {
16+
return undefined;
17+
}
18+
19+
return `${candidateDays}d`;
20+
}
321

422
type ProjectedLogIdentity = {
523
projection_fingerprint_string?: string;
@@ -38,10 +56,11 @@ export function escapeClickHouseLike(value: string): string {
3856
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
3957
}
4058

41-
// Must match the normalization in ClickHouse migration 038.
59+
// Must match the scheduled ClickHouse projector normalization.
4260
export function normalizeLogsSearchTerm(value: string): string {
4361
return value
4462
.toLocaleLowerCase()
4563
.replace(/[^\p{L}\p{N}_./:@+-]+/gu, " ")
64+
.replace(/\s*:\s*/g, ":")
4665
.trim();
4766
}

0 commit comments

Comments
 (0)