Skip to content

Commit 7f2224d

Browse files
carderneTrigger.dev RepoOps
authored andcommitted
fix(webapp,llm-model-catalog): bound realtime and analytics inputs
Mono-RevId: 3064956970461fff4c7221c24116e20fbeab7685
1 parent 2c720f6 commit 7f2224d

14 files changed

Lines changed: 179 additions & 239 deletions

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type ClickHouse, msToClickHouseInterval } from "@internal/clickhouse";
2-
import { TimeGranularity } from "~/utils/timeGranularity";
2+
import { isBoundedChartRange, TimeGranularity } from "~/utils/timeGranularity";
33
import { ErrorId } from "@trigger.dev/core/v3/isomorphic";
44
import { type ErrorGroupStatus, type PrismaClientOrTransaction } from "@trigger.dev/database";
55
import { timeFilterFromTo } from "~/components/runs/v3/SharedFilters";
@@ -187,6 +187,9 @@ export class ErrorGroupPresenter extends BasePresenter {
187187
versions: string[];
188188
}> {
189189
const granularityMs = errorGroupGranularity.getTimeGranularityMs(from, to);
190+
if (!isBoundedChartRange(from, to, granularityMs)) {
191+
return { data: [], versions: [] };
192+
}
190193
const intervalExpr = msToClickHouseInterval(granularityMs);
191194

192195
const queryBuilder =

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from "zod";
22
import { type ClickHouse, msToClickHouseInterval } from "@internal/clickhouse";
3-
import { TimeGranularity } from "~/utils/timeGranularity";
3+
import { isBoundedChartRange, TimeGranularity } from "~/utils/timeGranularity";
44

55
const errorsListGranularity = new TimeGranularity([
66
{ max: "2h", granularity: "1m" },
@@ -358,6 +358,9 @@ export class ErrorsListPresenter extends BasePresenter {
358358
}
359359

360360
const granularityMs = errorsListGranularity.getTimeGranularityMs(from, to);
361+
if (!isBoundedChartRange(from, to, granularityMs)) {
362+
return { data: {} };
363+
}
361364
const intervalExpr = msToClickHouseInterval(granularityMs);
362365

363366
const queryBuilder = this.clickhouse.errors.createOccurrencesQueryBuilder(intervalExpr);

‎apps/webapp/app/routes/tests.sse.stream.ts‎

Lines changed: 0 additions & 67 deletions
This file was deleted.

‎apps/webapp/app/routes/tests.sse.tsx‎

Lines changed: 0 additions & 37 deletions
This file was deleted.

‎apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Request as ExpressRequest, Response as ExpressResponse, NextFuncti
66
import { createHash } from "node:crypto";
77
import { z } from "zod";
88
import type { RedisWithClusterOptions } from "~/redis.server";
9+
import { getRouterPath } from "~/utils/sanitizeHttpUrl";
910
import { logger } from "./logger.server";
1011
import type { Duration, Limiter } from "./rateLimiter.server";
1112
import { createRedisRateLimitClient, RateLimiter } from "./rateLimiter.server";
@@ -229,9 +230,10 @@ export function authorizationRateLimitMiddleware({
229230
return next();
230231
}
231232

232-
//first check if any of the pathMatchers match the request path
233-
const path = req.path;
233+
// Use the same normalized pathname that the application router receives.
234+
const path = getRouterPath(req);
234235
if (
236+
!path ||
235237
!pathMatchers.some((matcher) =>
236238
matcher instanceof RegExp ? matcher.test(path) : path === matcher
237239
)

‎apps/webapp/app/services/engineRateLimit.server.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ export const engineRateLimiter = authorizationRateLimitMiddleware({
2323
stale: 60_000 * 20, // Date is stale after 20 minutes
2424
maxItems: 1000,
2525
},
26-
pathMatchers: [/^\/engine/],
27-
// Regex allow any path starting with /engine/v1/worker-actions/
28-
pathWhiteList: [/^\/engine\/v1\/worker-actions\/.*/],
26+
pathMatchers: [/^\/engine(?:\/|$)/],
27+
pathWhiteList: [/^\/engine\/v1\/worker-actions(?:\/|$)/],
2928
log: {
3029
rejections: env.RUN_ENGINE_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
3130
requests: env.RUN_ENGINE_RATE_LIMIT_REQUEST_LOGS_ENABLED === "1",

‎apps/webapp/app/services/realtime/redisRealtimeStreams.server.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ type StreamChunk =
2222
| { type: "data"; redisId: string; data: string }
2323
| { type: "legacy-data"; redisId: string; data: string };
2424

25+
function formatSseDataEvent(redisId: string, data: string): string {
26+
const withoutDelimiterCarriageReturn = data.endsWith("\r") ? data.slice(0, -1) : data;
27+
const normalized = withoutDelimiterCarriageReturn.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
28+
const dataFields = normalized
29+
.split("\n")
30+
.map((part) => `data: ${part}`)
31+
.join("\n");
32+
33+
return `id: ${redisId}\n${dataFields}\n\n`;
34+
}
35+
2536
// Class implementing both interfaces
2637
export class RedisRealtimeStreams implements StreamIngestor, StreamResponder {
2738
private logger: Logger;
@@ -277,8 +288,7 @@ export class RedisRealtimeStreams implements StreamIngestor, StreamResponder {
277288
if (chunk.type === "ping") {
278289
controller.enqueue(`: ping\n\n`);
279290
} else if ((chunk.type === "data" || chunk.type === "legacy-data") && chunk.line) {
280-
// Use Redis stream ID as SSE event ID
281-
controller.enqueue(`id: ${chunk.redisId}\ndata: ${chunk.line}\n\n`);
291+
controller.enqueue(formatSseDataEvent(chunk.redisId, chunk.line));
282292
}
283293
},
284294
})

‎apps/webapp/app/utils/sanitizeHttpUrl.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
11
const AUTHORIZATION_CODE_PATH_PREFIX = "/account/authorization-code/";
22
const REDACTED_AUTHORIZATION_CODE_PATH = "/account/authorization-code/[redacted]";
3+
const ROUTER_PATH = Symbol.for("trigger.routerPath");
4+
5+
type RequestTarget = {
6+
originalUrl?: string;
7+
url?: string;
8+
[ROUTER_PATH]?: string | null;
9+
};
10+
11+
export function getRouterPath(request: RequestTarget): string | undefined {
12+
const cached = request[ROUTER_PATH];
13+
if (cached !== undefined) {
14+
return cached ?? undefined;
15+
}
16+
17+
const target = request.originalUrl ?? request.url;
18+
if (!target) {
19+
request[ROUTER_PATH] = null;
20+
return undefined;
21+
}
22+
23+
try {
24+
const pathname = new URL(target, "http://localhost").pathname;
25+
request[ROUTER_PATH] = pathname;
26+
return pathname;
27+
} catch {
28+
request[ROUTER_PATH] = null;
29+
return undefined;
30+
}
31+
}
32+
33+
export function pathHasPrefix(pathname: string, prefix: string): boolean {
34+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
35+
}
336

437
export function sanitizeHttpUrl(value: string): string {
538
const queryIndex = value.indexOf("?");

‎apps/webapp/app/utils/sse.server.ts‎

Lines changed: 0 additions & 87 deletions
This file was deleted.

‎apps/webapp/app/utils/timeGranularity.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,21 @@ export class TimeGranularity {
5959
return this.parsed[this.parsed.length - 1].granularityMs;
6060
}
6161
}
62+
63+
const MAX_CHART_BUCKETS = 10_000;
64+
65+
/**
66+
* Rejects user-supplied chart ranges before they reach a query or a bucket
67+
* loop: non-finite or inverted dates, or a span that would generate an
68+
* unbounded number of buckets at the chosen granularity.
69+
*/
70+
export function isBoundedChartRange(from: Date, to: Date, granularityMs: number): boolean {
71+
const fromMs = from.getTime();
72+
const toMs = to.getTime();
73+
return (
74+
Number.isFinite(fromMs) &&
75+
Number.isFinite(toMs) &&
76+
fromMs <= toMs &&
77+
(toMs - fromMs) / granularityMs <= MAX_CHART_BUCKETS
78+
);
79+
}

0 commit comments

Comments
 (0)