Skip to content

Commit 1e41c20

Browse files
cursoragentclaude
authored andcommitted
fix(operations): throttle the public exportTraces endpoint
exportTraces was the only export operation that enqueued an email export without passing through enforceExportRequestRateLimit, so an API caller could enqueue unbounded CSV export jobs and emails per (org, project, recipient) while exportSignals and exportDatasetRows were both capped at 10/hour in production. Adds the same throttle its siblings use and documents the resulting 429 in the OpenAPI contract, which required swapping typedResponses (success status only, by design) for the explicit responses object both siblings already use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 25249fd commit 1e41c20

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

apps/api/openapi.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14674,6 +14674,16 @@
1467414674
}
1467514675
}
1467614676
}
14677+
},
14678+
"429": {
14679+
"description": "Export rate limit exceeded",
14680+
"content": {
14681+
"application/json": {
14682+
"schema": {
14683+
"$ref": "#/components/schemas/Error"
14684+
}
14685+
}
14686+
}
1467714687
}
1467814688
}
1467914689
}

packages/operations/src/operations/traces.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BadRequestError, cuidSchema, OrganizationId, ProjectId, SessionId, Span
77
import { getTraceAnalyticsUseCase, SpanRepository, TraceRepository } from "@domain/spans"
88
import { createRoute, z } from "@hono/zod-openapi"
99
import { AIEmbedLive, withAi } from "@platform/ai"
10+
import { enforceExportRequestRateLimit } from "@platform/cache-redis"
1011
import { MemoryRepositoryLive, SpanRepositoryLive, TraceRepositoryLive, withClickHouse } from "@platform/db-clickhouse"
1112
import {
1213
MembershipRepositoryLive,
@@ -39,7 +40,9 @@ import {
3940
import { TraceAnalyticsResponseSchema, toTraceAnalyticsResponse } from "../openapi/entities/trace-analytics.ts"
4041
import { Paginated, PaginatedQueryParamsSchema } from "../openapi/pagination.ts"
4142
import {
43+
errorResponse,
4244
jsonBody,
45+
jsonResponse,
4346
PROTECTED_SECURITY,
4447
ProjectParamsSchema,
4548
spanIdSchema,
@@ -558,7 +561,13 @@ const exportTraces = traceEndpoint({
558561
params: ProjectParamsSchema,
559562
body: jsonBody(ExportBodySchema),
560563
},
561-
responses: typedResponses({ status: 202, schema: ExportResponseSchema, description: "Export enqueued" }),
564+
responses: {
565+
202: jsonResponse(ExportResponseSchema, "Export enqueued"),
566+
400: errorResponse("Validation error"),
567+
401: errorResponse("Unauthorized"),
568+
404: errorResponse("Not found"),
569+
429: errorResponse("Export rate limit exceeded"),
570+
},
562571
}),
563572
access: "write",
564573
rateLimitTier: "ultra",
@@ -578,6 +587,17 @@ const exportTraces = traceEndpoint({
578587
})
579588
}
580589

590+
yield* Effect.tryPromise({
591+
try: () =>
592+
enforceExportRequestRateLimit({
593+
redis: ctx.redis,
594+
organizationId: ctx.organization.id as string,
595+
projectId: project.id as string,
596+
recipientEmail: body.recipient,
597+
}),
598+
catch: (cause) => cause,
599+
})
600+
581601
yield* ctx.queuePublisher.publish("exports", "generate", {
582602
kind: "traces",
583603
organizationId: ctx.organization.id as string,

0 commit comments

Comments
 (0)