Skip to content

Conversation

@samejr
Copy link
Member

@samejr samejr commented Jan 14, 2026

CleanShot 2026-01-14 at 13 41 02

@changeset-bot
Copy link

changeset-bot bot commented Jan 14, 2026

⚠️ No Changeset found

Latest commit: a6d6c52

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

Walkthrough

Adds a Limits feature end-to-end: a new SideMenu entry linking to a Limits page; a server-side LimitsPresenter that aggregates rate limits, quotas, features, and plan data (including fetching remaining rate-limit tokens via Redis); a Remix route and UI rendering sections for Current Plan, Concurrency, Rate Limits, Quotas, and Features; a shared createLimiterFromConfig helper used where limiters are constructed; a new limitsPath helper; Tailwind color tokens for concurrency/limits/regions; and an icon dependency bump.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is largely incomplete, containing only a screenshot with no textual explanation of changes, testing steps, or changelog details required by the template. Provide a detailed description following the template: explain the changes, document testing steps, include a changelog entry, and ensure all checklist items are addressed.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a new limits page to the webapp, which is confirmed by the file summaries.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx (3)

87-91: Consider differentiating error status codes.

All presenter errors currently return HTTP 400 (Bad Request), but this status implies a client error. If the presenter fails due to internal issues (e.g., Redis unavailable), a 500 status would be more appropriate.

♻️ Suggested improvement
   if (error) {
-    throw new Response(error.message, {
-      status: 400,
-    });
+    // Re-throw unexpected errors to let Remix handle them as 500s
+    throw error;
   }

Alternatively, if you want to keep custom error responses, consider checking the error type to return appropriate status codes.


392-408: Avoid magic string comparison for business logic.

Using info.name === "Batch rate limit" couples display text to business logic. If the name changes, this conditional silently breaks. Consider adding an identifier field to RateLimitInfo for conditional rendering.

♻️ Suggested approach

In LimitsPresenter.server.ts, add a key or id field to RateLimitInfo:

type RateLimitInfo = {
  key: "api" | "batch";  // Add identifier
  name: string;
  // ...
};

Then in this component:

-          {info.name === "Batch rate limit" ? (
+          {info.key === "batch" ? (

568-573: Same magic string pattern for quota conditional logic.

Similar to the rate limits section, using quota.name === "Log retention" and quota.name === "Projects" (line 610) ties display text to business logic. Consider using identifier fields in QuotaInfo for these conditionals.


📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26c82c5 and a6d6c52.

📒 Files selected for processing (2)
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Every Trigger.dev task must be exported; use task() function with unique id and run async function

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

In webapp development, access environment variables via env export from apps/webapp/app/env.server.ts; never use process.env directly

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
**/*.{js,ts,jsx,tsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js}: Import from @trigger.dev/core using subpaths only; never import from root of @trigger.dev/core
Always import task definitions from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or deprecated client.defineJob pattern

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
🧠 Learnings (10)
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : When importing from `trigger.dev/core` in the webapp, use subpath exports from the package.json instead of importing from the root path

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2026-01-12T11:01:34.792Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-12T11:01:34.792Z
Learning: Applies to **/*.{ts,tsx,js} : Import from trigger.dev/core using subpaths only; never import from root of trigger.dev/core

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2026-01-12T11:01:34.792Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-12T11:01:34.792Z
Learning: Applies to **/*.{ts,tsx,js} : Always import task definitions from trigger.dev/sdk, never from trigger.dev/sdk/v3 or deprecated client.defineJob pattern

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2026-01-12T11:01:34.792Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-12T11:01:34.792Z
Learning: Applies to **/*.{ts,tsx} : Every Trigger.dev task must be exported; use task() function with unique id and run async function

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `tasks.trigger()` with type-only imports to trigger tasks from backend code without importing the task implementation

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/app/**/*.{ts,tsx} : Access all environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env` in the Trigger.dev webapp

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: claude-review
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx (3)

102-179: LGTM!

The page component and section structure are well-organized with proper use of hooks, conditional rendering, and auto-revalidation setup matching the queues page pattern.


799-822: LGTM!

Well-documented utility with clear JSDoc explaining the percentage scale and mode semantics. The bidirectional logic correctly handles both quota usage (higher = worse) and rate limit tokens (lower = worse).


824-852: LGTM!

Clean badge component implementation with centralized variant configuration.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Jan 14, 2026

PR Review: New Limits Page

Overview

This PR adds a new "Limits" page to the webapp that displays rate limits, quotas, and plan features for an organization. The implementation follows existing patterns in the codebase (similar to the concurrency page).

Code Quality and Best Practices

Strengths:

  • Well-structured presenter pattern following existing conventions (LimitsPresenter extends BasePresenter)
  • Good separation of concerns between presenter (data fetching) and route (UI rendering)
  • Consistent use of existing UI components (Table, Badge, InfoIconTooltip, etc.)
  • Type safety with proper TypeScript types exported from the presenter
  • Auto-revalidation using the established useAutoRevalidate hook pattern

Minor Improvements:

  1. Unused userId parameter (line 85-86 in LimitsPresenter.server.ts): The userId is passed to the presenter but never used within it. While authorization is properly handled at the route level via findProjectBySlug, consider either using it for tracing/logging or removing it from the parameter list to avoid confusion.

  2. Unused projectId parameter: Similarly, projectId is passed but never used in the presenter. The org-level queries don't need it.

Potential Issues

  1. Singleton Redis client initialization (lines 14-24 in LimitsPresenter.server.ts): The Redis client is created as a singleton at module load time. If the rate limit Redis environment variables aren't set, this could throw an error during module initialization rather than gracefully handling missing config. Consider lazy initialization or adding null checks.

  2. Rate limit token calculation assumptions: The getRateLimitRemainingTokens function makes assumptions about the internal storage format of @upstash/ratelimit ("tokens:timestamp" format). If the library's internal format changes, this would silently return incorrect values. The comment documents this well, but it's worth noting this is a fragile integration.

Security Considerations

  • Authorization is properly enforced at the route level via findProjectBySlug and findEnvironmentBySlug, which verify the user is a member of the organization
  • API key handling: The environment API key is passed to query Redis for rate limit tokens. The key is hashed with SHA-256 before being used as a Redis key (same as the rate limiter itself), which is appropriate

Performance Considerations

  1. Multiple sequential database queries: The presenter makes 4 separate count() queries (schedules, alerts, branches, org). These could potentially be parallelized with Promise.all() for better performance, though the impact may be minimal given they're hitting the replica.

  2. Auto-reload interval: 5 second polling interval is reasonable for this type of dashboard. Consider whether this is needed at all since limit data doesn't change frequently.

Test Coverage

No tests are included with this PR. Given the CLAUDE.md guidance about using testcontainers for tests, consider adding:

  • Unit tests for durationToMs function
  • Integration tests for LimitsPresenter using postgresTest and redisTest
  • Tests for edge cases like missing plan data or null limits

Minor Suggestions

  1. Line 121: isOnTopPlan is hardcoded to check for "v3_pro_1". Consider making this configurable or documenting why this specific plan code is the "top" plan.

  2. Lines 272, 282, 292, 298 in presenter: Comments like "Would need to query Redis/realtime service for this" indicate incomplete functionality. Consider either implementing these or removing the currentUsage field for these quotas to avoid showing misleading zeros.

  3. Tailwind config: The new limits color is defined as colors.purple[500] which is the same as queues. Consider whether a distinct color would help differentiate the menu items.

Overall Assessment

This is a well-structured feature that follows established patterns in the codebase. The main areas for improvement are adding test coverage and potentially parallelizing the database queries. The security model is sound and the UI implementation is clean and consistent with existing pages.

Copy link
Collaborator

@nicktrn nicktrn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a test

Copy link
Collaborator

@nicktrn nicktrn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also a test, ignore this

@claude
Copy link

claude bot commented Jan 14, 2026

PR Review: New Limits Page

This PR adds a comprehensive "Limits" page to the webapp that displays rate limits, quotas, and plan features to users. Overall, this is a well-structured feature with good separation between the presenter and the UI components.

Strengths

  1. Good architecture: The LimitsPresenter follows the existing presenter pattern and properly separates data fetching from rendering
  2. Reuses existing infrastructure: Correctly uses createLimiterFromConfig from the auth middleware and follows established patterns for Redis client singletons
  3. Thoughtful UX: Auto-revalidation with useAutoRevalidate, environment selector for rate limits, clear upgrade paths based on plan tier
  4. Consistent styling: Uses existing component primitives and extends tailwind config appropriately

Issues and Suggestions

1. Potential Performance Issue - Sequential Database Queries

File: apps/webapp/app/presenters/v3/LimitsPresenter.server.ts:136-169

The presenter makes 4 sequential database queries (organization, schedules, alerts, branches). These could be parallelized with Promise.all:

const [organization, scheduleCount, alertChannelCount, activeBranchCount] = await Promise.all([
  this._replica.organization.findUniqueOrThrow({...}),
  this._replica.taskSchedule.count({...}),
  this._replica.projectAlertChannel.count({...}),
  this._replica.runtimeEnvironment.count({...}),
]);

This would reduce latency, especially important since this page auto-refreshes every 5 seconds.

2. Unused Parameter

File: apps/webapp/app/presenters/v3/LimitsPresenter.server.ts:86

The userId and projectId parameters are passed to call() but never used. Either remove them or document why they might be needed for future authorization checks.

3. Hardcoded Plan Code

File: apps/webapp/app/presenters/v3/LimitsPresenter.server.ts:122

const isOnTopPlan = currentPlan?.v3Subscription?.plan?.code === "v3_pro_1";

Consider extracting this plan code to a constant or using a property from the plan itself to determine if it's the top tier, to avoid breaking if plan codes change.

4. Duplicate Rate Limit Config Resolution Logic

File: apps/webapp/app/presenters/v3/LimitsPresenter.server.ts:333-371

The resolveBatchRateLimitConfig function is nearly identical to the one in batchLimits.server.ts. Consider consolidating to avoid drift between the two implementations.

5. Missing Error Handling for Redis Client

File: apps/webapp/app/presenters/v3/LimitsPresenter.server.ts:16-25

The singleton Redis client creation doesn't handle the case where Redis environment variables might be undefined. If RATE_LIMIT_REDIS_HOST is not set, this could fail silently or throw an unclear error. The getRateLimitRemainingTokens function does handle errors gracefully (line 424-429), which is good.

6. Consider Adding Test Coverage

Per the CLAUDE.md guidelines, tests should go next to source files. Consider adding:

  • LimitsPresenter.server.test.ts using postgresTest and redisTest from testcontainers
  • Tests for the config resolution functions

7. Minor UI Considerations

  • Line 129: The docs link points to /limits which may not exist yet - ensure the docs page is created
  • Line 569-571: The percentage calculation could divide by zero if quota.limit is 0 (though the > 0 check prevents this)

Security

  • No security concerns identified
  • The API key hashing (createHash("sha256")) matches the middleware implementation
  • No sensitive data is exposed in the UI that shouldn't be

Overall

This is a solid implementation that follows project conventions well. The main areas for improvement are performance (parallel queries) and code duplication (rate limit config resolution). The feature provides valuable visibility into system limits for users.

Recommendation: Approve with minor changes to parallelize the database queries and consolidate the rate limit config resolution logic.

@claude
Copy link

claude bot commented Jan 14, 2026

PR Review: feat(webapp): New limits page (2)

This PR adds a new Limits page to the webapp that displays rate limits, quotas, and plan features for organizations. Overall, this is a well-structured addition with clean separation between the presenter and route components.

Strengths

1. Clean Architecture

  • Good separation of concerns with LimitsPresenter handling data fetching and the route handling UI
  • Proper use of the existing BasePresenter pattern
  • Uses the replica database for read operations, which is the correct pattern

2. Thoughtful UX

  • Auto-revalidation every 5 seconds with useAutoRevalidate
  • Environment selector for viewing rate limits per environment
  • Clear visual indicators with color-coded usage percentages
  • Helpful tooltips explaining each limit type

3. Security Considerations

  • API keys are hashed using SHA-256 before querying Redis (matching the middleware pattern)
  • No sensitive data exposed in the UI

Potential Issues & Suggestions

1. Redis Client Error Handling (LimitsPresenter.server.ts:16-25)

The singleton Redis client is created immediately at module load time. If Redis is unavailable during startup, this could cause issues.

const rateLimitRedisClient = singleton("rateLimitQueryRedisClient", () =>
  createRedisRateLimitClient({...})
);

Consider wrapping this in a lazy initialization pattern or adding error handling around the Redis client creation.

2. Unused userId Parameter (LimitsPresenter.server.ts:86)

The call method accepts a userId parameter but never uses it. Either remove it or consider if authorization checks should use it:

public async call({
  userId,  // <-- Never used
  projectId,
  organizationId,
  environmentApiKey,
}: {...})

3. Hardcoded Plan Detection (LimitsPresenter.server.ts:122)

The "top plan" check is hardcoded to a specific plan code:

const isOnTopPlan = currentPlan?.v3Subscription?.plan?.code === "v3_pro_1";

This could become a maintenance burden if plan codes change. Consider adding a property like isTopTier to the plan definition itself.

4. Unused projectId Parameter (LimitsPresenter.server.ts:88)

Similar to userId, the projectId parameter is accepted but unused in the presenter.

5. Missing Error Boundary (route.tsx)

The route catches errors via tryCatch and throws a 400 response, but there is no error boundary component for better UX when things fail. Consider adding an ErrorBoundary export.

6. Reference Project Import Path (rateLimitStress.ts:1)

The stress test file imports from @trigger.dev/sdk/v3 instead of @trigger.dev/sdk:

import { logger, task, tasks, RateLimitError } from "@trigger.dev/sdk/v3";

Per CLAUDE.md, the correct import is @trigger.dev/sdk (the /v3 suffix is deprecated).

7. Rate Limit Query Creates New Ratelimit Instance (LimitsPresenter.server.ts:410-418)

Each call to getRateLimitRemainingTokens creates a new Ratelimit instance:

const ratelimit = new Ratelimit({
  redis: rateLimitRedisClient,
  limiter,
  ephemeralCache: new Map(),  // <-- New cache each time
  analytics: false,
  prefix: `ratelimit:${keyPrefix}`,
});

Since this page auto-revalidates every 5 seconds, creating new ephemeral caches each time is wasteful. Consider caching the Ratelimit instances.


Performance Considerations

1. Multiple Sequential Database Queries (LimitsPresenter.server.ts:136-169)

The presenter makes four separate count queries:

  • taskSchedule.count
  • projectAlertChannel.count
  • runtimeEnvironment.count
  • Organization with _count

Consider using Promise.all to run these in parallel:

const [scheduleCount, alertChannelCount, activeBranchCount] = await Promise.all([
  this._replica.taskSchedule.count({...}),
  this._replica.projectAlertChannel.count({...}),
  this._replica.runtimeEnvironment.count({...}),
]);

2. Rate Limit Redis Queries (LimitsPresenter.server.ts:171-181)

Similarly, the two getRateLimitRemainingTokens calls could be parallelized:

const [apiRateLimitTokens, batchRateLimitTokens] = await Promise.all([
  getRateLimitRemainingTokens("api", environmentApiKey, apiRateLimitConfig),
  getRateLimitRemainingTokens("batch", environmentApiKey, batchRateLimitConfig),
]);

Test Coverage

No tests were added for LimitsPresenter. Given the complexity of the presenter (rate limit queries, plan resolution, config parsing), adding tests would be valuable. The presenter could be tested using the existing testcontainers pattern mentioned in CLAUDE.md.


Minor Nits

  1. tailwind.config.js - The new limits and regions color variables are defined but line 351-352 in SideMenu.tsx updates concurrency and regions icons to use them while still using text-orange-500 directly for ConcurrencyIcon in route.tsx:225

  2. Duplicate resolution functions - resolveBatchRateLimitConfig exists in both LimitsPresenter.server.ts and batchLimits.server.ts. Consider extracting to a shared utility.


Summary

This is a solid PR that adds valuable visibility into organization limits. The main areas for improvement are:

  1. Parallelize database and Redis queries for better performance
  2. Add tests for the presenter
  3. Fix the SDK import path in the reference project
  4. Consider removing unused parameters or adding error boundaries

The UI looks clean and the component structure is well-organized. Good work!

@claude
Copy link

claude bot commented Jan 14, 2026

PR Review: feat(webapp): New limits page (2)

This is a well-structured addition of a new Limits page that provides users visibility into their rate limits, quotas, and plan features. The implementation follows existing patterns in the codebase and includes good refactoring of shared code.

Overall Assessment: ✅ Looks Good

The PR is well-organized and ready for merge with a few minor suggestions.


Strengths

  1. Good code reuse: The createLimiterFromConfig function was extracted and shared between authorizationRateLimitMiddleware.server.ts and batchLimits.server.ts, eliminating duplicate code.

  2. Consistent patterns: The presenter/route pattern follows existing conventions in the codebase (LimitsPresenter extending BasePresenter).

  3. User-friendly UI: The page includes helpful tooltips, color-coded usage indicators, and clear upgrade paths (View plans vs Contact us).

  4. Auto-refresh: Using useAutoRevalidate with a 5-second interval ensures users see current rate limit token counts.

  5. Environment-aware: Rate limits are tracked per API key, and the environment selector allows users to view limits for different environments.


Suggestions

1. Unused userId parameter in presenter

LimitsPresenter.server.ts:85-88

The userId parameter is passed but never used in the presenter. Consider removing it if not needed:

public async call({
  userId,  // <-- Not used anywhere in the method
  projectId,
  organizationId,
  environmentApiKey,
}: {...})

2. Import path in reference test file

references/hello-world/src/trigger/rateLimitStress.ts:1

The import uses @trigger.dev/sdk/v3 which is flagged as deprecated in CLAUDE.md:

import { logger, task, tasks, RateLimitError } from "@trigger.dev/sdk/v3";

Should this be @trigger.dev/sdk instead?

3. Consider parallel database queries

LimitsPresenter.server.ts:136-169

The three count queries (scheduleCount, alertChannelCount, activeBranchCount) are executed sequentially. Since they're independent, they could be parallelized with Promise.all for better performance:

const [scheduleCount, alertChannelCount, activeBranchCount] = await Promise.all([
  this._replica.taskSchedule.count({...}),
  this._replica.projectAlertChannel.count({...}),
  this._replica.runtimeEnvironment.count({...}),
]);

4. Hardcoded plan code for "top plan" detection

LimitsPresenter.server.ts:122

const isOnTopPlan = currentPlan?.v3Subscription?.plan?.code === "v3_pro_1";

This hardcoded plan code may become stale if plan codes change. Consider whether this should be derived from plan metadata (e.g., a tier field) or kept centralized in a constants file.

5. Minor: Type assertion in resolveBatchConcurrencyConfig

LimitsPresenter.server.ts:384-387

The type assertion could be replaced with proper Zod validation for consistency with the other resolve functions:

const config = batchConcurrencyConfig as Record<string, unknown>;

Questions

  1. Rate limit stress test file: Is references/hello-world/src/trigger/rateLimitStress.ts intended to be included in this PR, or should it be in a separate testing/tooling PR? It's useful for testing but adds 257 lines to a feature PR.

  2. Missing test coverage: There don't appear to be any unit tests for LimitsPresenter. Given the complexity of resolving configs and querying rate limit tokens, consider adding tests using testcontainers.


Security

No security concerns identified. The API key is hashed before being used as a rate limit key, consistent with existing patterns. Sensitive data (API keys) are not exposed in the UI.


Summary

This is a clean implementation that provides valuable visibility into rate limits and quotas. The minor suggestions above are non-blocking. Nice work on the UI polish with color-coded usage indicators and comprehensive tooltips!

@samejr samejr marked this pull request as ready for review January 14, 2026 16:29
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx:
- Line 5: The import of tryCatch currently comes from "@trigger.dev/core";
update the import to use the utils subpath export by importing tryCatch from
"@trigger.dev/core/utils" instead so it follows the webapp subpath export
guideline; modify the import statement referencing tryCatch in the file where
it's currently imported to point to the utils subpath.
🧹 Nitpick comments (5)
apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts (1)

133-143: Good addition of shared limiter factory.

The helper function centralizes limiter creation logic. The nested ternary handles all three discriminated union cases correctly.

🔧 Optional: Consider a switch statement for readability

A switch statement could be slightly more readable, especially if more limiter types are added in the future:

 export function createLimiterFromConfig(config: RateLimiterConfig): Limiter {
-  return config.type === "fixedWindow"
-    ? Ratelimit.fixedWindow(config.tokens, config.window)
-    : config.type === "tokenBucket"
-    ? Ratelimit.tokenBucket(config.refillRate, config.interval, config.maxTokens)
-    : Ratelimit.slidingWindow(config.tokens, config.window);
+  switch (config.type) {
+    case "fixedWindow":
+      return Ratelimit.fixedWindow(config.tokens, config.window);
+    case "tokenBucket":
+      return Ratelimit.tokenBucket(config.refillRate, config.interval, config.maxTokens);
+    case "slidingWindow":
+      return Ratelimit.slidingWindow(config.tokens, config.window);
+  }
 }

This is a minor style preference - the current implementation is correct.

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx (1)

392-411: Consider using a discriminator field instead of string comparison.

Using info.name === "Batch rate limit" for conditional logic is fragile. If the display name changes in LimitsPresenter, this logic will silently break.

Consider adding an id or type field to RateLimitInfo for reliable discrimination:

Example approach
// In LimitsPresenter.server.ts, add an id field:
 export type RateLimitInfo = {
+  id: "api" | "batch";
   name: string;
   // ...
 };

// Then in the route:
-          {info.name === "Batch rate limit" ? (
+          {info.id === "batch" ? (
apps/webapp/app/presenters/v3/LimitsPresenter.server.ts (3)

277-302: Several quotas display currentUsage: 0 as placeholders.

The quotas for realtimeConnections, devQueueSize, and deployedQueueSize show currentUsage: 0 because the actual values require querying external services. While this is documented in comments, users may find these zero values misleading.

Consider either:

  • Hiding the "Current" column for these quotas in the UI
  • Using null to indicate unavailable data (and showing "–" in the UI)

119-123: Consider extracting the hardcoded plan code to a constant or adding an isTopPlan flag in the plan definition.

The comparison plan?.code === "v3_pro_1" on line 122 is isolated to this one location, but hardcoding the plan code makes it fragile if plan codes change in the platform service. Since the plan code is sourced from an external service (getCurrentPlan), any coordinated changes to the plan naming could silently break this check without compiler warnings.

Extracting this to a shared constant or adding an isTopPlan property directly to the plan definition would improve maintainability and make future plan changes less error-prone.


333-351: defaultConfig bypasses RateLimiterConfig validation; consider adding duration format validation.

The as Duration cast on line 337 (and similarly line 357 for batch config) creates a default configuration that's returned without validation against the RateLimiterConfig schema. While env.API_RATE_LIMIT_REFILL_INTERVAL is validated as a string with a sensible default, the schema doesn't validate the duration format itself—only that the value is a string.

To improve consistency and catch misconfigurations earlier, consider adding a duration format validator to the environment schema (e.g., validating patterns like "10s", "1m", etc.) so invalid values are rejected at startup rather than failing at runtime.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5339a1 and 26c82c5.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • references/hello-world/src/trigger/rateLimitStress.ts is excluded by !references/**
📒 Files selected for processing (8)
  • apps/webapp/app/components/navigation/SideMenu.tsx
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/package.json
  • apps/webapp/tailwind.config.js
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,ts,jsx,tsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier

Files:

  • apps/webapp/package.json
  • apps/webapp/tailwind.config.js
  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/components/navigation/SideMenu.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/tailwind.config.js
  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/components/navigation/SideMenu.tsx
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js}: Import from @trigger.dev/core using subpaths only; never import from root of @trigger.dev/core
Always import task definitions from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or deprecated client.defineJob pattern

Files:

  • apps/webapp/tailwind.config.js
  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/components/navigation/SideMenu.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Every Trigger.dev task must be exported; use task() function with unique id and run async function

Files:

  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/components/navigation/SideMenu.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/components/navigation/SideMenu.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

In webapp development, access environment variables via env export from apps/webapp/app/env.server.ts; never use process.env directly

Files:

  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/components/navigation/SideMenu.tsx
apps/webapp/app/services/**/*.server.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Separate testable services from configuration files; follow the pattern of realtimeClient.server.ts (testable service) and realtimeClientGlobal.server.ts (configuration) in the webapp

Files:

  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/components/navigation/SideMenu.tsx
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/utils/pathBuilder.ts
🧠 Learnings (8)
📚 Learning: 2026-01-12T17:18:09.451Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2870
File: apps/webapp/app/services/redisConcurrencyLimiter.server.ts:56-66
Timestamp: 2026-01-12T17:18:09.451Z
Learning: In `apps/webapp/app/services/redisConcurrencyLimiter.server.ts`, the query concurrency limiter will not be deployed with Redis Cluster mode, so multi-key operations (keyKey and globalKey in different hash slots) are acceptable and will function correctly in standalone Redis mode.

Applied to files:

  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
  • apps/webapp/app/runEngine/concerns/batchLimits.server.ts
📚 Learning: 2026-01-12T11:01:34.792Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-12T11:01:34.792Z
Learning: Applies to apps/webapp/app/**/*.test.{ts,tsx} : In testable code, never import env.server.ts in test files; pass configuration as options instead

Applied to files:

  • apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/app/v3/presenters/**/*.server.{ts,tsx} : Organize presenters in the webapp following the pattern `app/v3/presenters/*/*.server.ts` to move complex loader code into classes

Applied to files:

  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
📚 Learning: 2025-06-06T23:55:01.933Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 2155
File: hosting/docker/.env.example:4-7
Timestamp: 2025-06-06T23:55:01.933Z
Learning: In the trigger.dev project, .env.example files should contain actual example secret values rather than placeholders, as these help users understand the expected format. The files include clear warnings about not using these defaults in production and instructions for generating proper secrets.

Applied to files:

  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
📚 Learning: 2025-06-25T14:14:11.965Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 2195
File: hosting/k8s/helm/values-production-example.yaml:95-102
Timestamp: 2025-06-25T14:14:11.965Z
Learning: In the Trigger.dev Helm chart production examples, the maintainer prefers to include initial/bootstrap credentials with clear warnings that they should be changed after first login, rather than requiring external secret references that could complicate initial setup. This follows their pattern of providing working examples with explicit security guidance.

Applied to files:

  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
📚 Learning: 2025-11-14T16:03:06.917Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2681
File: apps/webapp/app/services/platform.v3.server.ts:258-302
Timestamp: 2025-11-14T16:03:06.917Z
Learning: In `apps/webapp/app/services/platform.v3.server.ts`, the `getDefaultEnvironmentConcurrencyLimit` function intentionally throws an error (rather than falling back to org.maximumConcurrencyLimit) when the billing client returns undefined plan limits. This fail-fast behavior prevents users from receiving more concurrency than their plan entitles them to. The org.maximumConcurrencyLimit fallback is only for self-hosted deployments where no billing client exists.

Applied to files:

  • apps/webapp/app/presenters/v3/LimitsPresenter.server.ts
  • apps/webapp/app/utils/pathBuilder.ts
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
🧬 Code graph analysis (4)
apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts (1)
apps/webapp/app/services/rateLimiter.server.ts (1)
  • Limiter (16-16)
apps/webapp/app/runEngine/concerns/batchLimits.server.ts (1)
apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts (1)
  • createLimiterFromConfig (137-143)
apps/webapp/app/presenters/v3/LimitsPresenter.server.ts (2)
apps/webapp/app/services/rateLimiter.server.ts (2)
  • createRedisRateLimitClient (74-105)
  • Duration (17-17)
apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts (5)
  • RateLimiterConfig (46-50)
  • RateLimiterConfig (52-52)
  • RateLimitTokenBucketConfig (37-42)
  • RateLimitTokenBucketConfig (44-44)
  • createLimiterFromConfig (137-143)
apps/webapp/app/components/navigation/SideMenu.tsx (2)
apps/webapp/app/utils/pathBuilder.ts (3)
  • concurrencyPath (502-508)
  • regionsPath (518-524)
  • limitsPath (510-516)
apps/webapp/app/components/navigation/SideMenuItem.tsx (1)
  • SideMenuItem (7-53)
🔇 Additional comments (19)
apps/webapp/package.json (1)

114-114: No action required. The codebase uses @tabler/icons-react icons in a straightforward manner—imported as named exports and rendered as JSX components—which is fully compatible with v3. There are no usages of TablerIconProps or explicit type annotations that would be affected by v3's breaking changes to internal types and signatures.

Likely an incorrect or invalid review comment.

apps/webapp/tailwind.config.js (2)

163-165: LGTM!

The new color tokens follow the established pattern for icon colors. The naming is consistent with other semantic tokens in the file.


241-244: LGTM!

The new color tokens are properly exposed in the theme's color mapping.

apps/webapp/app/utils/pathBuilder.ts (1)

510-516: LGTM!

The new limitsPath function follows the established pattern used by concurrencyPath and regionsPath, maintaining consistency in the codebase.

apps/webapp/app/components/navigation/SideMenu.tsx (3)

354-357: Good refactor to semantic color tokens.

Using text-concurrency instead of hardcoded text-amber-500 improves maintainability by centralizing color definitions in the Tailwind config.


362-365: LGTM!

Consistent with the semantic color token refactor.


367-373: No action needed—Limits is intentionally available to all users. Unlike Concurrency, which is a managed-cloud-only feature, the Limits route has no managed cloud guards and the LimitsPresenter provides data for all plans. Leaving the menu item unconditional is correct.

apps/webapp/app/runEngine/concerns/batchLimits.server.ts (2)

4-7: Good refactor to centralize limiter creation.

Importing createLimiterFromConfig from the middleware module eliminates code duplication and ensures consistent limiter construction across the codebase.


35-46: LGTM!

The refactored createOrganizationRateLimiter is cleaner and delegates limiter creation to the shared helper function.

apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts (2)

10-10: LGTM!

Adding the Limiter type import enables proper typing for the new helper function.


264-264: LGTM!

The middleware now uses the shared helper function, maintaining consistency with the rest of the codebase.

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx (5)

59-102: LGTM!

The loader follows proper Remix conventions with authentication, parameter validation using Zod, appropriate 404 responses, and structured error handling with tryCatch.


104-181: LGTM!

Clean component structure with proper auto-revalidation, context hooks usage, and well-organized section rendering.


560-682: LGTM with minor note.

The component logic is sound with proper handling for retention quotas and upgrade rendering. The string comparisons for quota types (Lines 570, 612) share the same fragility concern noted for rate limits, but the implementation is otherwise clean and well-organized.


732-799: LGTM!

Clean implementation with proper handling of different feature value types and upgrade options.


807-854: LGTM!

Well-documented utility function with clear mode handling, and clean variant-based badge rendering.

apps/webapp/app/presenters/v3/LimitsPresenter.server.ts (3)

1-25: LGTM!

Proper imports with environment variables accessed through env.server.ts, and appropriate singleton pattern for Redis client reuse.


27-82: LGTM!

Well-structured type definitions using type instead of interface as per coding guidelines.


398-431: LGTM!

The SHA256 hashing here is for rate limit key identification (matching the middleware's approach), not for password storage—the security scanner alerts are a false positive for this context. Error handling with graceful degradation to null is appropriate.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

import { type MetaFunction } from "@remix-run/react";
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { IconCardsFilled, IconDiamondFilled, IconTallymark4 } from "@tabler/icons-react";
import { tryCatch } from "@trigger.dev/core";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the exports configuration in `@trigger.dev/core`
cat packages/core/package.json | jq '.exports'

Repository: triggerdotdev/trigger.dev

Length of output: 10911


🏁 Script executed:

# Find where tryCatch is defined in packages/core
rg "tryCatch" packages/core --type ts --type tsx -B 2 -A 5

Repository: triggerdotdev/trigger.dev

Length of output: 96


🏁 Script executed:

# Check the current file to understand the context
head -20 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx

Repository: triggerdotdev/trigger.dev

Length of output: 193


🏁 Script executed:

# Search for tryCatch definition in packages/core
rg "export.*tryCatch" packages/core -t ts

Repository: triggerdotdev/trigger.dev

Length of output: 434


🏁 Script executed:

# Find the limits route file (path may have different formatting)
find apps/webapp/app/routes -name "*limits*" -o -name "*route.tsx" | grep -i limits | head -5

Repository: triggerdotdev/trigger.dev

Length of output: 268


🏁 Script executed:

# Search for all tryCatch imports in the webapp
rg "tryCatch" apps/webapp --type ts

Repository: triggerdotdev/trigger.dev

Length of output: 16698


Use the @trigger.dev/core/utils subpath export for tryCatch.

Per coding guidelines, imports from @trigger.dev/core in the webapp must use subpath exports. Since tryCatch is exported from the utils subpath, change the import to:

import { tryCatch } from "@trigger.dev/core/utils";
🤖 Prompt for AI Agents
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
at line 5, The import of tryCatch currently comes from "@trigger.dev/core";
update the import to use the utils subpath export by importing tryCatch from
"@trigger.dev/core/utils" instead so it follows the webapp subpath export
guideline; modify the import statement referencing tryCatch in the file where
it's currently imported to point to the utils subpath.

: "default";

// Get schedule count for this org
const scheduleCount = await this._replica.taskSchedule.count({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this worries me, really not sure if this will be fast

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI found a more performant way to do this query which i've committed:

The current query goes through instances -> environment -> organizationId which requires multiple joins with no direct indexes.
Better approach: TaskSchedule has a projectId, and Project has organizationId. Query through project instead:

const scheduleCount = await this._replica.taskSchedule.count({  
   where: {   
       project: {    
           organizationId,    
       },
    },
});

This is more efficient because:
TaskSchedule has @@index([projectId])
Avoids the extra join through TaskScheduleInstance and RuntimeEnvironment
Direct path: TaskSchedule → Project (by projectId) → filter by organizationId
The current query requires: TaskSchedule → TaskScheduleInstance → RuntimeEnvironment → Organization (3 joins, no helpful indexes on the instance table).

});

// Get alert channel count for this org
const alertChannelCount = await this._replica.projectAlertChannel.count({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, performance could be an issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI response to this:

This query is actually okay, though not perfectly optimized:
Current state:

  • ProjectAlertChannel has @@unique([projectId, deduplicationKey]) - composite index with projectId as leading column ✓
  • Projecthas NO @@index([organizationId]) ✗

How the query executes:
Join ProjectAlertChannelProject via projectId (uses composite unique index) ✓
Filter Project by organizationId (no index, requires scan) ✗

However, this is probably fine in practice because:

  • The Project table is small (most orgs have 1-10 projects)
  • The join to ProjectAlertChannel uses the index efficiently

If you wanted to optimize further, you'd add an index to the Project model:

model Project {
   // ... fields  
   @@index([organizationId])
}

This would help all queries that filter projects by organization. But for a low-volume page like limits, the current query is acceptable.

});

// Get active branches count for this org
const activeBranchCount = await this._replica.runtimeEnvironment.count({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same, possible perf issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one can be improved. I've update it:

RuntimeEnvironment has its own organizationId field AND an index on it (@@index([organizationId])). No need to go through the project relation:

// Current: goes through project (no index on project.organizationId)
project: { organizationId }

// Better: direct field with index
organizationId  // @@index([organizationId]) exists

Changed to:

const activeBranchCount = await this._replica.runtimeEnvironment.count({
  where: {
    organizationId,
    branchName: {
      not: null,
    },
    archivedAt: null,
  },
});

@samejr samejr changed the title feat(webapp): New limits page (2) feat(webapp): New limits page Jan 14, 2026
@claude
Copy link

claude bot commented Jan 14, 2026

PR Review: feat(webapp): New limits page

Overall Assessment

This is a well-structured PR that adds a comprehensive limits dashboard showing rate limits, quotas, and plan features. The code follows the existing patterns in the codebase and provides a useful feature for users to understand their limits. Here is detailed feedback:


Code Quality and Best Practices

Strengths:

  • Clean separation of concerns: presenter handles data fetching, route handles presentation
  • Good use of existing components (Table, Badge, InfoIconTooltip, etc.)
  • Proper extraction of createLimiterFromConfig to avoid code duplication (previously duplicated in batchLimits.server.ts and authorizationRateLimitMiddleware.server.ts)
  • Well-typed with proper TypeScript types exported from the presenter
  • Good use of useAutoRevalidate for real-time updates

Suggestions:

  1. Import consistency in reference file (line 1): The rateLimitStress.ts reference file imports from @trigger.dev/sdk/v3 but per CLAUDE.md, you should "Always import from @trigger.dev/sdk. Never use @trigger.dev/sdk/v3"

  2. Minor duplication: The resolveApiRateLimitConfig and resolveBatchRateLimitConfig functions in LimitsPresenter.server.ts are nearly identical. Consider a generic helper, though this may be intentional for clarity.


Potential Bugs or Issues

  1. Rate limit prefix inconsistency (LimitsPresenter.server.ts:407): The presenter uses "api" and "batch" as key prefixes when querying remaining tokens. However, the actual middleware uses different prefixes set in the server configuration. You should verify that these prefixes match exactly what the rate limit middleware uses, otherwise the getRemaining call will query the wrong keys and return incorrect (or null) values.

  2. Missing limitsPath import in SideMenu.tsx: The import is added but I would verify this does not break when the route does not exist (e.g., during navigation). Looks fine based on how other paths are handled.

  3. Hardcoded "top plan" check (LimitsPresenter.server.ts:118): The hardcoded plan code check (v3_pro_1) could become stale if plan codes change. Consider adding a comment explaining this or using a constant.


Performance Considerations

  1. Multiple sequential database queries (LimitsPresenter.server.ts:132-158): The presenter makes several sequential count() queries for taskSchedule, projectAlertChannel, and runtimeEnvironment. These could potentially be parallelized with Promise.all() for better performance.

  2. Rate limit token queries: Similarly, getRateLimitRemainingTokens is called twice sequentially. These could also be parallelized.

  3. 5-second auto-refresh interval: The page polls every 5 seconds which is reasonable, but consider if this frequency is necessary for data that does not change rapidly (quotas do not change often).


Security Concerns

  1. API key passed to presenter: The presenter receives environmentApiKey to query rate limits. This is handled appropriately - it is hashed before being used as a Redis key identifier, matching the existing pattern in the rate limit middleware. No security issues identified.

  2. No sensitive data exposure: The page only shows limits/quotas, not actual API keys or secrets.


Test Coverage

Missing tests: There do not appear to be any tests added for:

  • LimitsPresenter.server.ts
  • The route loader

Per the codebase conventions, tests should be added using testcontainers (not mocks). Consider adding unit tests for the presenter and integration tests for the loader.

The reference rateLimitStress.ts file is appropriately a manual testing tool and does not need automated tests.


Minor Issues

  1. Unused import warning potential: The Limiter type is now exported from rateLimiter.server.ts but verify the import in authorizationRateLimitMiddleware.server.ts includes it.

  2. Tabler icons upgrade: The PR upgrades @tabler/icons-react from 2.39.0 to 3.36.1 which is a major version bump. Verify this does not cause any visual regressions or API breaking changes in other parts of the codebase using these icons.


Summary

This is a solid PR that adds valuable functionality. The main items to address:

  • Required: Fix the SDK import in rateLimitStress.ts (@trigger.dev/sdk not @trigger.dev/sdk/v3)
  • Recommended: Verify rate limit key prefixes match actual middleware configuration
  • Recommended: Parallelize database queries for better performance
  • Nice to have: Add test coverage for the presenter

Overall, good work on the implementation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants