Skip to content

Errors (versions)#3187

Draft
matt-aitken wants to merge 26 commits intomainfrom
errors-add-version-filter
Draft

Errors (versions)#3187
matt-aitken wants to merge 26 commits intomainfrom
errors-add-version-filter

Conversation

@matt-aitken
Copy link
Member

  • Added versions filtering on the Errors list and page
  • Added errors stacked bars to the graph on the individual error page

@changeset-bot
Copy link

changeset-bot bot commented Mar 5, 2026

⚠️ No Changeset found

Latest commit: 3688a9f

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

@matt-aitken matt-aitken marked this pull request as draft March 5, 2026 23:24
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

Adds end-to-end error-group versioning, filtering, and alerting support. Frontend: new LogsVersionFilter, exported VersionsDropdown, status/version filters, chart legend class prop, ConfigureErrorAlerts UI, alert configuration routes, and storybook UnorderedList. Backend: presenters accept versions/statuses, ErrorGroupState types, ErrorGroupActions, DeliverErrorGroupAlertService, ErrorAlertEvaluator, alert worker tasks, and alert-channel presenter. ClickHouse: new per-version and "since" query builders. Database: Prisma/schema and SQL migrations for ErrorGroupState and alert config. Email template and email client integration for error-group alerts.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~180 minutes

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and incomplete, missing most required sections from the template including issue reference, checklist, testing details, and screenshots. Complete the PR description by filling out all sections from the template: add issue reference, confirm checklist items, describe testing steps, add changelog details, and include screenshots where applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 17.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Errors (versions)' is vague and generic, using non-descriptive phrasing that doesn't clearly convey the main changes to someone scanning commit history. Provide a more specific title that describes the primary change, such as 'Add version filtering to errors list and stacked charts' or similar descriptive phrasing.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch errors-add-version-filter

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.

devin-ai-integration[bot]

This comment was marked as resolved.

@matt-aitken matt-aitken force-pushed the errors-add-version-filter branch from cd9b5ea to e609e8b Compare March 16, 2026 23:30
coderabbitai[bot]

This comment was marked as resolved.

@matt-aitken matt-aitken force-pushed the errors-add-version-filter branch from 6875ac8 to 497f301 Compare March 20, 2026 15:05
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts (1)

79-89: ⚠️ Potential issue | 🔴 Critical

Initialize state in getSummary() now that it is required.

ErrorGroupSummary now requires state, but getSummary() still returns an object without that field. This breaks the file’s TypeScript contract before call() gets a chance to overwrite summary.state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts` around lines 79
- 89, getSummary() currently returns an object missing the required state field
of ErrorGroupSummary; update getSummary() to include a default state value
(e.g., ErrorGroupState.Open or the appropriate ErrorGroupState enum member) in
the returned summary object so the returned type satisfies ErrorGroupSummary
before call() may overwrite summary.state; locate the getSummary function in
ErrorGroupPresenter.server.ts and add the state property to the returned object
referencing ErrorGroupState.
♻️ Duplicate comments (2)
apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts (1)

133-148: ⚠️ Potential issue | 🟠 Major

State lookup still needs taskIdentifier.

ErrorGroupState is keyed by (environmentId, taskIdentifier, errorFingerprint), but this code still fetches it with only (environmentId, errorFingerprint) and does so before summary.taskIdentifier is known. If the same fingerprint exists under multiple tasks, the page can surface the wrong resolved/ignored state. Fetch summary first, then look up state via the full composite key (findUnique fits here).

Also applies to: 324-332

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts` around lines 133
- 148, The state lookup is being called before the summary and without
taskIdentifier; change the flow in ErrorGroupPresenter.server.ts so you await
this.getSummary(...) first, extract summary.taskIdentifier, then fetch state
using the composite key (environmentId, taskIdentifier, fingerprint) — replace
the current this.getState(environmentId, fingerprint) call with a lookup that
uses the full composite key (use the repository's findUnique or equivalent to
query by environmentId, taskIdentifier, and errorFingerprint). Apply the same
fix to the other occurrence mentioned (lines around the second getState call).
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx (1)

80-98: ⚠️ Potential issue | 🟠 Major

Reject empty task identifiers and non-positive ignore thresholds server-side.

These branches still allow an empty taskIdentifier and zero/negative ignore thresholds through to ErrorGroupActions. That lets crafted POSTs create ErrorGroupState rows the UI cannot reliably match, or ignore conditions that never behave as intended. Tighten this schema with a trimmed non-empty identifier and positive numeric validators.

As per coding guidelines, "Use zod for validation in packages/core and apps/webapp".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
around lines 80 - 98, The discriminated union actionSchema allows empty
taskIdentifier and non-positive ignore thresholds; update the schema so
taskIdentifier uses a trimmed non-empty string (e.g., z.string().trim().min(1))
in every branch (resolve, ignore, unresolve) and make the numeric ignore fields
(duration, occurrenceRate, totalOccurrences) use coercion plus positive checks
(e.g., z.coerce.number().positive().optional()) so zero/negative values are
rejected; keep resolvedInVersion and reason optional but consider trimming them
as well for consistency.
🧹 Nitpick comments (1)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx (1)

820-845: Normalize dotted version strings used as chart series keys.

Version strings like 20260304.1 are used directly as object keys in the chart config (cfg[versions[i]]). While JavaScript bracket notation handles this correctly and recharts typically checks for direct properties before nested path traversal, this pattern is fragile. Recharts' accessor implementation could change, or future maintainers might not realize why this works. Normalize to internal series IDs (e.g., array indices) and use version strings only as labels:

cfg[i] = {
  label: versions[i],
  color: getSeriesColor(i),
}

Also applies to: 890-899

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
around lines 820 - 845, The chart currently uses dotted version strings as
object keys (cfg[versions[i]]) which can be fragile for recharts accessors; in
ActivityChart replace those string keys with numeric series IDs (use i as the
key) so cfg[i] = { label: versions[i], color: getSeriesColor(i) } and update the
data transformation so each activity point maps its series value to the same
numeric ID instead of the dotted version string; make sure to keep versions[i]
as the human-readable label and apply the same change to the other chart config
block that builds cfg (the second occurrence that also references cfg, versions,
getSeriesColor, and activity/data).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts`:
- Around line 79-89: getSummary() currently returns an object missing the
required state field of ErrorGroupSummary; update getSummary() to include a
default state value (e.g., ErrorGroupState.Open or the appropriate
ErrorGroupState enum member) in the returned summary object so the returned type
satisfies ErrorGroupSummary before call() may overwrite summary.state; locate
the getSummary function in ErrorGroupPresenter.server.ts and add the state
property to the returned object referencing ErrorGroupState.

---

Duplicate comments:
In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts`:
- Around line 133-148: The state lookup is being called before the summary and
without taskIdentifier; change the flow in ErrorGroupPresenter.server.ts so you
await this.getSummary(...) first, extract summary.taskIdentifier, then fetch
state using the composite key (environmentId, taskIdentifier, fingerprint) —
replace the current this.getState(environmentId, fingerprint) call with a lookup
that uses the full composite key (use the repository's findUnique or equivalent
to query by environmentId, taskIdentifier, and errorFingerprint). Apply the same
fix to the other occurrence mentioned (lines around the second getState call).

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx:
- Around line 80-98: The discriminated union actionSchema allows empty
taskIdentifier and non-positive ignore thresholds; update the schema so
taskIdentifier uses a trimmed non-empty string (e.g., z.string().trim().min(1))
in every branch (resolve, ignore, unresolve) and make the numeric ignore fields
(duration, occurrenceRate, totalOccurrences) use coercion plus positive checks
(e.g., z.coerce.number().positive().optional()) so zero/negative values are
rejected; keep resolvedInVersion and reason optional but consider trimming them
as well for consistency.

---

Nitpick comments:
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx:
- Around line 820-845: The chart currently uses dotted version strings as object
keys (cfg[versions[i]]) which can be fragile for recharts accessors; in
ActivityChart replace those string keys with numeric series IDs (use i as the
key) so cfg[i] = { label: versions[i], color: getSeriesColor(i) } and update the
data transformation so each activity point maps its series value to the same
numeric ID instead of the dotted version string; make sure to keep versions[i]
as the human-readable label and apply the same change to the other chart config
block that builds cfg (the second occurrence that also references cfg, versions,
getSeriesColor, and activity/data).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d9f30fbf-5e21-4716-a3ce-d6e0559ba25e

📥 Commits

Reviewing files that changed from the base of the PR and between e91c887 and dbe89dd.

📒 Files selected for processing (2)
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📜 Review details
⏰ 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). (16)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{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

**/*.{ts,tsx}: Use task export syntax: export const myTask = task({ id: 'my-task', run: async (payload) => { ... } })
Use Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work - avoid DEPRECATED zodworker (Graphile-worker wrapper)
Prisma 6.14.0 client and schema use PostgreSQL in internal-packages/database - import only from Prisma client

Files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/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/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
**/*.{ts,tsx,js,jsx}

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

Use function declarations instead of default exports

Files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/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

Files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/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/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.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/presenters/v3/ErrorGroupPresenter.server.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
apps/webapp/**/*.server.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Access environment variables via the env export from app/env.server.ts, never use process.env directly

Files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
apps/{webapp,supervisor}/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

When modifying only server components (apps/webapp/, apps/supervisor/) with no package changes, add a .server-changes/ file instead of a changeset

Files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js}: Always import from @trigger.dev/sdk for Trigger.dev tasks - never use @trigger.dev/sdk/v3 or deprecated client.defineJob
Import subpaths only from @trigger.dev/core, never import from root
Add crumbs as you write code using // @crumbs comments or // #region @crumbs blocks for agentcrumbs debug tracing

Files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
apps/webapp/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Remix 2.1.0 is used in apps/webapp for the main API, dashboard, and orchestration with Express server

Files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
🧠 Learnings (22)
📚 Learning: 2026-01-08T15:57:09.323Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/otel-metrics.mdc:0-0
Timestamp: 2026-01-08T15:57:09.323Z
Learning: Applies to **/*.ts : 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)

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2026-03-04T16:40:11.313Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3172
File: apps/webapp/app/utils/semver.ts:19-33
Timestamp: 2026-03-04T16:40:11.313Z
Learning: In `apps/webapp/app/utils/semver.ts`, the versioning scheme used is date-based (e.g. `20260304.1`, `20260304.2`), not semantic versioning. Pre-release suffix handling (e.g. `-beta`, `-rc`) is not a concern for this file.

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2026-03-13T13:37:49.562Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T13:37:49.562Z
Learning: Applies to **/*.{ts,tsx} : Prisma 6.14.0 client and schema use PostgreSQL in internal-packages/database - import only from Prisma client

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 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 internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2025-07-12T18:06:04.133Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2264
File: apps/webapp/app/services/runsRepository.server.ts:172-174
Timestamp: 2025-07-12T18:06:04.133Z
Learning: In apps/webapp/app/services/runsRepository.server.ts, the in-memory status filtering after fetching runs from Prisma is intentionally used as a workaround for ClickHouse data delays. This approach is acceptable because the result set is limited to a maximum of 100 runs due to pagination, making the performance impact negligible.

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/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: Leverage the PostgreSQL database through the `trigger.dev/database` Prisma 6.14.0 client in the webapp for all data access patterns

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2026-02-06T19:53:38.843Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts:233-237
Timestamp: 2026-02-06T19:53:38.843Z
Learning: When constructing Vercel dashboard URLs from deployment IDs, always strip the dpl_ prefix from the ID. Implement this by transforming the ID with .replace(/^dpl_/, "") before concatenating into the URL: https://vercel.com/${teamSlug}/${projectName}/${cleanedDeploymentId}. Consider centralizing this logic in a small helper (e.g., getVercelDeploymentId(id) or a URL builder) and add tests to verify both prefixed and non-prefixed inputs.

Applied to files:

  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
📚 Learning: 2026-03-02T12:42:56.114Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/app/routes/**/*.ts : Use Remix flat-file route convention with dot-separated segments (e.g., `api.v1.tasks.$taskId.trigger.ts` for `/api/v1/tasks/:taskId/trigger`)

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 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.errors.$fingerprint/route.tsx
📚 Learning: 2026-02-04T16:34:48.876Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/vercel.connect.tsx:13-27
Timestamp: 2026-02-04T16:34:48.876Z
Learning: In apps/webapp/app/routes/vercel.connect.tsx, configurationId may be absent for "dashboard" flows but must be present for "marketplace" flows. Enforce this with a Zod superRefine and pass installationId to repository methods only when configurationId is defined (omit the field otherwise).

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-03-13T13:37:49.562Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-13T13:37:49.562Z
Learning: Applies to apps/webapp/**/*.{ts,tsx,jsx,js} : Remix 2.1.0 is used in apps/webapp for the main API, dashboard, and orchestration with Express server

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-03-13T13:45:39.411Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3213
File: apps/webapp/app/routes/admin.llm-models.missing.$model.tsx:19-21
Timestamp: 2026-03-13T13:45:39.411Z
Learning: In `apps/webapp/app/routes/admin.llm-models.missing.$model.tsx`, the `decodeURIComponent(params.model ?? "")` call is intentionally unguarded. Remix route params are decoded at the routing layer before reaching the loader, so malformed percent-encoding is rejected upstream. The page is also admin-only, so the risk is minimal and no try-catch is warranted.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/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.errors.$fingerprint/route.tsx
📚 Learning: 2026-02-11T16:50:14.167Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx:126-131
Timestamp: 2026-02-11T16:50:14.167Z
Learning: In apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx, MetricsDashboard entities are intentionally scoped to the organization level, not the project level. The dashboard lookup should filter by organizationId only (not projectId), allowing dashboards to be accessed across projects within the same organization. The optional projectId field on MetricsDashboard serves other purposes and should not be used as an authorization constraint.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-03-13T13:42:59.104Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3213
File: apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts:40-43
Timestamp: 2026-03-13T13:42:59.104Z
Learning: In `apps/webapp/app/routes/admin.api.v1.llm-models.$modelId.ts` and `apps/webapp/app/routes/admin.api.v1.llm-models.ts`, the `startDate` field in `UpdateModelSchema` and `CreateModelSchema` intentionally uses `z.string().optional()` (or `.nullable().optional()`) without strict ISO datetime validation. Invalid date strings are rejected at the Prisma/DB layer. This is acceptable because these are admin-only API routes protected by Personal Access Token (PAT) authentication and are not user-facing.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/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 `schemaTask()` from `trigger.dev/sdk/v3` with Zod schema for payload validation

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/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/core,apps/webapp}/**/*.{ts,tsx} : Use zod for validation in packages/core and apps/webapp

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/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} : Attach metadata to task runs using the metadata option when triggering, and access/update it inside runs using metadata functions

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2025-07-12T18:00:06.163Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2264
File: apps/webapp/app/utils/searchParams.ts:16-18
Timestamp: 2025-07-12T18:00:06.163Z
Learning: The `objectToSearchParams` function in `apps/webapp/app/utils/searchParams.ts` is used to generate URL parameters from objects and is separate from code that parses incoming search parameters. Changes to this function only affect places where it's used to create URLs, not places that parse search parameters from external sources.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-01-28T14:15:15.011Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 2953
File: apps/webapp/app/components/runs/v3/SharedFilters.tsx:441-452
Timestamp: 2026-01-28T14:15:15.011Z
Learning: In apps/webapp/app/components/runs/v3/SharedFilters.tsx, the maxPeriodDays limit for date ranges should only check the from date (via dateRangeToDays(fromValue)) because it enforces data retention limits—how far back in history queries can reach. The to date is irrelevant for retention-based limits.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.

Applied to files:

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

devin-ai-integration[bot]

This comment was marked as resolved.

@matt-aitken matt-aitken force-pushed the errors-add-version-filter branch from 7f30042 to c31d639 Compare March 23, 2026 11:12
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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts (1)

79-89: ⚠️ Potential issue | 🔴 Critical

getSummary() return type mismatch: missing required state field.

The return statement at lines 283–292 omits the required state: ErrorGroupState property declared in the ErrorGroupSummary type. TypeScript will report a type error here. Either initialize state in getSummary() using the fallback state object already defined at lines 153–166, or mark state optional in the type definition and require it only after call() enriches the summary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts` around lines 79
- 89, getSummary() currently returns an object missing the required state
property from the ErrorGroupSummary type; either set the state field on the
returned summary to the existing fallback state object (the fallback
ErrorGroupState defined near the top of the file) by adding state:
fallbackErrorGroupState to the summary returned by getSummary(), or change the
ErrorGroupSummary type to make state optional (state?: ErrorGroupState) so
callers that enrich via call() can provide it later—locate getSummary(),
ErrorGroupSummary, and the fallback ErrorGroupState symbol names to implement
the chosen fix.
🧹 Nitpick comments (6)
apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts (1)

6-32: Use a type alias for ErrorGroupAlertData.

This exported payload shape should follow the repo’s TS rule to prefer type over interface.

As per coding guidelines, **/*.{ts,tsx}: Use types over interfaces for TypeScript.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts` around lines
6 - 32, Replace the exported interface ErrorGroupAlertData with an equivalent
exported type alias named ErrorGroupAlertData; keep the exact same property
shape (including nested error, organization, project, and dashboardUrl) but use
the type ... = { ... } syntax to satisfy the repo rule preferring types over
interfaces.
apps/webapp/test/slackErrorAlerts.test.ts (1)

160-383: These cases still only assert “no throw”.

Every test ends with expect(true).toBe(true), so regressions in block composition, truncation, or wording will still pass as long as Slack accepts the request. If these are meant to stay manual visual checks, mark them that way; otherwise assert on the generated Slack payload before delivery.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/test/slackErrorAlerts.test.ts` around lines 160 - 383, Tests
currently only assert no-throw (expect(true).toBe(true)); update each case to
either mark them as manual/visual (e.g., use test.skip or add a clear comment)
or add assertions against the generated Slack payload by stubbing/mocking the
delivery path: capture the payload produced by
DeliverErrorGroupAlertService.call (or the underlying method that formats the
Slack message) when invoked with createMockErrorPayload inputs and assert
expected fields (presence of classification text, stack truncation notice for
long messages, correct taskIdentifier/errorType, and special character
preservation) before the actual send; reference DeliverErrorGroupAlertService,
createMockErrorPayload and the formatter/delivery method to locate where to
intercept and assert.
apps/webapp/test/webhookErrorAlerts.test.ts (1)

1-5: Keep this as a colocated unit test and drop the Prisma setup.

The assertions only exercise generateErrorGroupWebhookPayload() and Webhook.parse(), so the org/project setup is just DB churn. Fixed ids/slugs are enough here, and this suite will be easier to maintain next to the webhook generator it covers.

As per coding guidelines, Test files should live beside the files under test and use descriptive describe and it blocks.

Also applies to: 92-139

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/test/webhookErrorAlerts.test.ts` around lines 1 - 5, This test
should be converted into a pure colocated unit test: remove all Prisma
setup/teardown and any imports/usage of prisma or getSecretStore, keep the file
next to the webhook generator, and replace dynamic org/project DB setup with
fixed test values (fixed ids/slugs) used as inputs to
DeliverErrorGroupAlertService.generateErrorGroupWebhookPayload() and
Webhook.parse(); ensure the test only imports and exercises
generateErrorGroupWebhookPayload() and Webhook.parse(), and rename the
describe/test blocks to a descriptive "describe/it" style that reflects it’s a
unit test for the webhook payload generator.
apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts (2)

282-285: Consider exposing Slack's retryAfter for smarter retry backoff.

The WebAPIRateLimitedError includes a retryAfter value (seconds). Exposing this in the error or as a custom error type could help the worker implement proper backoff rather than immediate retry.

♻️ Optional: Include retryAfter in error
 if (isWebAPIRateLimitedError(error)) {
-  throw new Error("Slack rate limited");
+  const rateLimitError = error as WebAPIRateLimitedError;
+  throw new Error(`Slack rate limited, retry after ${rateLimitError.retryAfter}s`);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts` around
lines 282 - 285, When handling Slack rate limit errors inside
deliverErrorGroupAlert (the catch that checks isWebAPIRateLimitedError(error)),
extract the retryAfter value from the WebAPIRateLimitedError and throw a custom
or enhanced error that includes that value (e.g., SlackRateLimitedError with a
retryAfter property or attach retryAfter to the thrown Error). Update the throw
in the isWebAPIRateLimitedError branch to include the numeric retryAfter seconds
from the Slack error so callers (the worker) can apply proper backoff based on
that field.

1-8: Remove unused imports.

WebAPIHTTPError and WebAPIRequestError are imported but not used anywhere in this file.

♻️ Remove unused imports
 import {
   type ChatPostMessageArguments,
   ErrorCode,
-  type WebAPIHTTPError,
   type WebAPIPlatformError,
   type WebAPIRateLimitedError,
-  type WebAPIRequestError,
 } from "@slack/web-api";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts` around
lines 1 - 8, The import list at the top currently includes unused symbols
WebAPIHTTPError and WebAPIRequestError; update the import statement that brings
in ChatPostMessageArguments, ErrorCode, WebAPIHTTPError, WebAPIPlatformError,
WebAPIRateLimitedError, WebAPIRequestError to remove WebAPIHTTPError and
WebAPIRequestError so only used types (e.g., ChatPostMessageArguments,
ErrorCode, WebAPIPlatformError, WebAPIRateLimitedError) remain; ensure no other
references to those removed types exist in functions like deliverErrorGroupAlert
or any helper functions before committing.
apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts (1)

394-420: Consider parallelizing state updates for better throughput.

The sequential await inside the loop works correctly but may become a bottleneck if many errors transition to UNRESOLVED in a single evaluation. Consider using Promise.all or a batch update if performance becomes a concern.

♻️ Optional: Parallelize updates
 private async updateErrorGroupStates(
   alertableErrors: AlertableError[],
   stateMap: Map<string, ErrorGroupState>
 ): Promise<void> {
-  for (const alertable of alertableErrors) {
+  const updates = alertableErrors.map(async (alertable) => {
     const key = `${alertable.error.environment_id}:${alertable.error.task_identifier}:${alertable.error.error_fingerprint}`;
     const state = stateMap.get(key);
-    if (!state) continue;
+    if (!state) return;

     await this._prisma.errorGroupState.update({
-        where: { id: state.id },
-        data: {
-          status: "UNRESOLVED",
-          ignoredUntil: null,
-          ignoredUntilOccurrenceRate: null,
-          ignoredUntilTotalOccurrences: null,
-          ignoredAtOccurrenceCount: null,
-          ignoredAt: null,
-          ignoredReason: null,
-          ignoredByUserId: null,
-          resolvedAt: null,
-          resolvedInVersion: null,
-          resolvedBy: null,
-        },
-      });
-  }
+      where: { id: state.id },
+      data: {
+        status: "UNRESOLVED",
+        ignoredUntil: null,
+        ignoredUntilOccurrenceRate: null,
+        ignoredUntilTotalOccurrences: null,
+        ignoredAtOccurrenceCount: null,
+        ignoredAt: null,
+        ignoredReason: null,
+        ignoredByUserId: null,
+        resolvedAt: null,
+        resolvedInVersion: null,
+        resolvedBy: null,
+      },
+    });
+  });
+
+  await Promise.all(updates);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts` around
lines 394 - 420, The loop in updateErrorGroupStates performs sequential awaits
on this._prisma.errorGroupState.update which can be slow; change it to collect
update promises for each matching alertable (use the same key logic with
alertableErrors and stateMap) and run them in parallel with await
Promise.all(promises) (or use a single batch/transaction where appropriate),
calling this._prisma.errorGroupState.update for each state but not awaiting
inside the for-loop so throughput improves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts`:
- Around line 235-245: The IN/NOT IN clauses built with queryBuilder are using
parentheses around the ClickHouse parameter placeholders which breaks runtime
substitution; in the code paths that check statusFilter.includeKeys and
statusFilter.excludeKeys (the queryBuilder.where calls referencing
"concat(task_identifier, '::', error_fingerprint)"), remove the surrounding
parentheses so the clauses use the same pattern as other IN usages (i.e., change
"IN ({statusIncludeKeys: Array(String)})" to "IN {statusIncludeKeys:
Array(String)}" and "NOT IN ({statusExcludeKeys: Array(String)})" to "NOT IN
{statusExcludeKeys: Array(String)}" while keeping the parameter objects
statusIncludeKeys/statusExcludeKeys).

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx:
- Around line 407-411: The hasFilters check (variable hasFilters) ignores
time-related query params so a custom period/from/to doesn't count as an active
filter; update the expression that computes hasFilters to also check
searchParams.has("period"), searchParams.has("from"), and searchParams.has("to")
(in the same boolean OR chain) so the "Clear all filters" UI appears when only
time params are set.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx:
- Around line 767-771: The dialog is being closed unconditionally in the
fetcher.Form onSubmit handler (setTimeout(onClose, 100)) which discards input
even when the POST returns validation errors; remove that onSubmit close and
instead watch fetcher.data for a successful response (check fetcher.data?.ok ===
true) and call onClose() only when that condition becomes true (e.g., use an
effect that depends on fetcher.data to call onClose()). Ensure the fetcher.Form
submit stays intact and reference the existing fetcher.Form, onSubmit handler,
onClose callback, and fetcher.data in the change.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx:
- Around line 97-127: The current action unconditionally creates email and Slack
channels from the posted emails[] and slackChannel even when those UI sections
are not editable; before creating channels in the emails loop and before the
slackChannel block, verify the corresponding availability/editable flag (the
same check used to render the UI, e.g., inspect editableTypes or the feature
availability predicate used elsewhere) and skip creation if that channel type is
not allowed; ensure the check happens prior to constructing
CreateAlertChannelOptions and calling service.call(project.externalRef, userId,
...), mirroring the UI logic so only permitted email (emails) and Slack
(slackChannel + slackIntegrationId) channels are upserted.

In `@apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts`:
- Around line 51-52: The code constructs dates using new
Date(data.error.firstSeen) and new Date(data.error.lastSeen) where
firstSeen/lastSeen are millisecond-formatted strings; convert those string
timestamps to numbers before calling Date to avoid Invalid Date/JSON nulls.
Update the new Date calls in errorGroupWebhook.server.ts (the places referencing
data.error.firstSeen and data.error.lastSeen) to use
Number(data.error.firstSeen) and Number(data.error.lastSeen) (or parseInt) so
the Date constructor receives numeric milliseconds.

In `@apps/webapp/test/slackErrorAlerts.test.ts`:
- Around line 93-107: The test creates a live secret and DB metadata (using
secretStore.setSecret with secretKey and prisma.secretReference.create) but
never cleans them up; add teardown logic in afterAll to remove both the
persisted secret and its DB record: call the secret store deletion for secretKey
via secretStore.deleteSecret (or the store's remove API) and delete the created
secretReference by id via prisma.secretReference.delete({ where: { id:
<the-created-id> } }) (ensure you capture the created secretReference id when
creating it in the test so teardown can reference it); apply the same cleanup
for the other instance noted at lines ~138-157.

---

Outside diff comments:
In `@apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts`:
- Around line 79-89: getSummary() currently returns an object missing the
required state property from the ErrorGroupSummary type; either set the state
field on the returned summary to the existing fallback state object (the
fallback ErrorGroupState defined near the top of the file) by adding state:
fallbackErrorGroupState to the summary returned by getSummary(), or change the
ErrorGroupSummary type to make state optional (state?: ErrorGroupState) so
callers that enrich via call() can provide it later—locate getSummary(),
ErrorGroupSummary, and the fallback ErrorGroupState symbol names to implement
the chosen fix.

---

Nitpick comments:
In `@apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts`:
- Around line 282-285: When handling Slack rate limit errors inside
deliverErrorGroupAlert (the catch that checks isWebAPIRateLimitedError(error)),
extract the retryAfter value from the WebAPIRateLimitedError and throw a custom
or enhanced error that includes that value (e.g., SlackRateLimitedError with a
retryAfter property or attach retryAfter to the thrown Error). Update the throw
in the isWebAPIRateLimitedError branch to include the numeric retryAfter seconds
from the Slack error so callers (the worker) can apply proper backoff based on
that field.
- Around line 1-8: The import list at the top currently includes unused symbols
WebAPIHTTPError and WebAPIRequestError; update the import statement that brings
in ChatPostMessageArguments, ErrorCode, WebAPIHTTPError, WebAPIPlatformError,
WebAPIRateLimitedError, WebAPIRequestError to remove WebAPIHTTPError and
WebAPIRequestError so only used types (e.g., ChatPostMessageArguments,
ErrorCode, WebAPIPlatformError, WebAPIRateLimitedError) remain; ensure no other
references to those removed types exist in functions like deliverErrorGroupAlert
or any helper functions before committing.

In `@apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts`:
- Around line 394-420: The loop in updateErrorGroupStates performs sequential
awaits on this._prisma.errorGroupState.update which can be slow; change it to
collect update promises for each matching alertable (use the same key logic with
alertableErrors and stateMap) and run them in parallel with await
Promise.all(promises) (or use a single batch/transaction where appropriate),
calling this._prisma.errorGroupState.update for each state but not awaiting
inside the for-loop so throughput improves.

In `@apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts`:
- Around line 6-32: Replace the exported interface ErrorGroupAlertData with an
equivalent exported type alias named ErrorGroupAlertData; keep the exact same
property shape (including nested error, organization, project, and dashboardUrl)
but use the type ... = { ... } syntax to satisfy the repo rule preferring types
over interfaces.

In `@apps/webapp/test/slackErrorAlerts.test.ts`:
- Around line 160-383: Tests currently only assert no-throw
(expect(true).toBe(true)); update each case to either mark them as manual/visual
(e.g., use test.skip or add a clear comment) or add assertions against the
generated Slack payload by stubbing/mocking the delivery path: capture the
payload produced by DeliverErrorGroupAlertService.call (or the underlying method
that formats the Slack message) when invoked with createMockErrorPayload inputs
and assert expected fields (presence of classification text, stack truncation
notice for long messages, correct taskIdentifier/errorType, and special
character preservation) before the actual send; reference
DeliverErrorGroupAlertService, createMockErrorPayload and the formatter/delivery
method to locate where to intercept and assert.

In `@apps/webapp/test/webhookErrorAlerts.test.ts`:
- Around line 1-5: This test should be converted into a pure colocated unit
test: remove all Prisma setup/teardown and any imports/usage of prisma or
getSecretStore, keep the file next to the webhook generator, and replace dynamic
org/project DB setup with fixed test values (fixed ids/slugs) used as inputs to
DeliverErrorGroupAlertService.generateErrorGroupWebhookPayload() and
Webhook.parse(); ensure the test only imports and exercises
generateErrorGroupWebhookPayload() and Webhook.parse(), and rename the
describe/test blocks to a descriptive "describe/it" style that reflects it’s a
unit test for the webhook payload generator.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1d4ea286-3ad8-4bf3-b751-ebc5f81426b2

📥 Commits

Reviewing files that changed from the base of the PR and between e91c887 and c31d639.

📒 Files selected for processing (36)
  • apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx
  • apps/webapp/app/components/layout/AppLayout.tsx
  • apps/webapp/app/components/logs/LogsVersionFilter.tsx
  • apps/webapp/app/components/primitives/Popover.tsx
  • apps/webapp/app/components/primitives/UnorderedList.tsx
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
  • apps/webapp/app/components/runs/v3/RunFilters.tsx
  • apps/webapp/app/models/projectAlert.server.ts
  • apps/webapp/app/presenters/v3/ErrorAlertChannelPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
  • apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
  • apps/webapp/app/routes/storybook.unordered-list/route.tsx
  • apps/webapp/app/routes/storybook/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/v3/alertsWorker.server.ts
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts
  • apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts
  • apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts
  • apps/webapp/app/v3/services/errorGroupActions.server.ts
  • apps/webapp/test/errorGroupWebhook.test.ts
  • apps/webapp/test/slackErrorAlerts.test.ts
  • apps/webapp/test/webhookErrorAlerts.test.ts
  • internal-packages/clickhouse/src/errors.ts
  • internal-packages/clickhouse/src/index.ts
  • internal-packages/database/prisma/migrations/20260306102053_error_group_state/migration.sql
  • internal-packages/database/prisma/schema.prisma
  • internal-packages/emails/emails/alert-error-group.tsx
  • internal-packages/emails/src/index.tsx
  • packages/core/src/v3/schemas/webhooks.ts
✅ Files skipped from review due to trivial changes (3)
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/routes/storybook.unordered-list/route.tsx
  • apps/webapp/app/models/projectAlert.server.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/webapp/app/routes/storybook/route.tsx
  • apps/webapp/app/utils/pathBuilder.ts
  • apps/webapp/app/components/layout/AppLayout.tsx
  • apps/webapp/app/v3/services/alerts/createAlertChannel.server.ts
  • apps/webapp/app/components/runs/v3/RunFilters.tsx
  • apps/webapp/app/components/logs/LogsVersionFilter.tsx
  • apps/webapp/app/components/primitives/charts/ChartRoot.tsx
  • internal-packages/emails/src/index.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.connect-to-slack.ts
  • internal-packages/clickhouse/src/index.ts
  • apps/webapp/app/v3/services/errorGroupActions.server.ts
  • internal-packages/database/prisma/migrations/20260306102053_error_group_state/migration.sql

Comment on lines +235 to +245
if (statusFilter.includeKeys) {
queryBuilder.where(
"concat(task_identifier, '::', error_fingerprint) IN ({statusIncludeKeys: Array(String)})",
{ statusIncludeKeys: statusFilter.includeKeys }
);
}
if (statusFilter.excludeKeys) {
queryBuilder.where(
"concat(task_identifier, '::', error_fingerprint) NOT IN ({statusExcludeKeys: Array(String)})",
{ statusExcludeKeys: statusFilter.excludeKeys }
);
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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

ClickHouse parameterized IN clause syntax with array parameters: is expr IN {ids:Array(String)}the correct form, and doesexpr IN ({ids:Array(String)}) change the semantics or break the query?

💡 Result:

No, expr IN {ids:Array(String)} is the correct syntax for ClickHouse parameterized queries with array parameters. The parameter {ids:Array(String)} is substituted as an array literal (e.g., ['a','b']), which the IN operator treats as a set of constants on the right side. expr IN ({ids:Array(String)}) adds unnecessary parentheses around the parameter. Since the parameter substitutes to an array literal [...] and IN expects a set or parenthesized list on the right, the extra parentheses would make it IN ([...]), which is invalid syntax and would break the query with a parsing error. For array parameters in IN: - Correct: column IN {ids:Array(String)} - Use has({ids:Array(String)}, column) if needing array membership check (left/right swapped). This syntax is confirmed in official docs for query parameters and GitHub issues validating IN ({param:Array(T)}) form (equivalent without extra parens).

Citations:


🏁 Script executed:

# Extract the specific lines from the file to verify the issue
sed -n '235,245p' apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts

Repository: triggerdotdev/trigger.dev

Length of output: 532


🏁 Script executed:

# Search for other IN clause usages in the same file to confirm the existing pattern
rg -A 2 'IN \{' apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts

Repository: triggerdotdev/trigger.dev

Length of output: 650


Fix ClickHouse IN clause syntax to match the existing pattern in this file.

The new IN ({statusIncludeKeys: Array(String)}) and NOT IN ({statusExcludeKeys: Array(String)}) clauses at lines 235-245 add unnecessary parentheses that break the query at runtime. All other IN clauses in this presenter use IN {param: Array(String)} without parentheses. Remove the parentheses to match the existing pattern and ensure the parameter substitution is valid.

🔧 Suggested fix
     if (statusFilter.includeKeys) {
       queryBuilder.where(
-        "concat(task_identifier, '::', error_fingerprint) IN ({statusIncludeKeys: Array(String)})",
+        "concat(task_identifier, '::', error_fingerprint) IN {statusIncludeKeys: Array(String)}",
         { statusIncludeKeys: statusFilter.includeKeys }
       );
     }
     if (statusFilter.excludeKeys) {
       queryBuilder.where(
-        "concat(task_identifier, '::', error_fingerprint) NOT IN ({statusExcludeKeys: Array(String)})",
+        "concat(task_identifier, '::', error_fingerprint) NOT IN {statusExcludeKeys: Array(String)}",
         { statusExcludeKeys: statusFilter.excludeKeys }
       );
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (statusFilter.includeKeys) {
queryBuilder.where(
"concat(task_identifier, '::', error_fingerprint) IN ({statusIncludeKeys: Array(String)})",
{ statusIncludeKeys: statusFilter.includeKeys }
);
}
if (statusFilter.excludeKeys) {
queryBuilder.where(
"concat(task_identifier, '::', error_fingerprint) NOT IN ({statusExcludeKeys: Array(String)})",
{ statusExcludeKeys: statusFilter.excludeKeys }
);
if (statusFilter.includeKeys) {
queryBuilder.where(
"concat(task_identifier, '::', error_fingerprint) IN {statusIncludeKeys: Array(String)}",
{ statusIncludeKeys: statusFilter.includeKeys }
);
}
if (statusFilter.excludeKeys) {
queryBuilder.where(
"concat(task_identifier, '::', error_fingerprint) NOT IN {statusExcludeKeys: Array(String)}",
{ statusExcludeKeys: statusFilter.excludeKeys }
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts` around lines 235
- 245, The IN/NOT IN clauses built with queryBuilder are using parentheses
around the ClickHouse parameter placeholders which breaks runtime substitution;
in the code paths that check statusFilter.includeKeys and
statusFilter.excludeKeys (the queryBuilder.where calls referencing
"concat(task_identifier, '::', error_fingerprint)"), remove the surrounding
parentheses so the clauses use the same pattern as other IN usages (i.e., change
"IN ({statusIncludeKeys: Array(String)})" to "IN {statusIncludeKeys:
Array(String)}" and "NOT IN ({statusExcludeKeys: Array(String)})" to "NOT IN
{statusExcludeKeys: Array(String)}" while keeping the parameter objects
statusIncludeKeys/statusExcludeKeys).

Comment on lines 407 to +411
const hasFilters =
searchParams.has("status") ||
searchParams.has("tasks") ||
searchParams.has("search") ||
searchParams.has("period") ||
searchParams.has("from") ||
searchParams.has("to");
searchParams.has("versions") ||
searchParams.has("search");
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

Include time params in hasFilters.

A custom period / from / to still changes the query, but this check only counts status/task/version/search. That makes “Clear all filters” disappear when time is the only active filter.

🔧 Suggested fix
   const hasFilters =
+    searchParams.has("period") ||
+    searchParams.has("from") ||
+    searchParams.has("to") ||
     searchParams.has("status") ||
     searchParams.has("tasks") ||
     searchParams.has("versions") ||
     searchParams.has("search");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
around lines 407 - 411, The hasFilters check (variable hasFilters) ignores
time-related query params so a custom period/from/to doesn't count as an active
filter; update the expression that computes hasFilters to also check
searchParams.has("period"), searchParams.has("from"), and searchParams.has("to")
(in the same boolean OR chain) so the "Clear all filters" UI appears when only
time params are set.

Comment on lines +767 to +771
<fetcher.Form
method="post"
onSubmit={() => {
setTimeout(onClose, 100);
}}
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

Close the dialog on success, not on submit.

This dismisses the form after 100 ms even when the action returns validation errors or the POST fails, so the user loses the input with no recovery path. Wait for fetcher.data?.ok === true before calling onClose().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
around lines 767 - 771, The dialog is being closed unconditionally in the
fetcher.Form onSubmit handler (setTimeout(onClose, 100)) which discards input
even when the POST returns validation errors; remove that onSubmit close and
instead watch fetcher.data for a successful response (check fetcher.data?.ok ===
true) and call onClose() only when that condition becomes true (e.g., use an
effect that depends on fetcher.data to call onClose()). Ensure the fetcher.Form
submit stays intact and reference the existing fetcher.Form, onSubmit handler,
onClose callback, and fetcher.data in the change.

Comment on lines +97 to +127
for (const email of emails) {
const options: CreateAlertChannelOptions = {
name: `Error alert to ${email}`,
alertTypes: ["ERROR_GROUP"],
environmentTypes,
deduplicationKey: `error-email:${email}:${environment.type}`,
channel: { type: "EMAIL", email },
};
const channel = await service.call(project.externalRef, userId, options);
processedChannelIds.add(channel.id);
}

if (slackChannel) {
const [channelId, channelName] = slackChannel.split("/");
if (channelId && channelName) {
const options: CreateAlertChannelOptions = {
name: `Error alert to #${channelName}`,
alertTypes: ["ERROR_GROUP"],
environmentTypes,
deduplicationKey: `error-slack:${environment.type}`,
channel: {
type: "SLACK",
channelId,
channelName,
integrationId: slackIntegrationId,
},
};
const channel = await service.call(project.externalRef, userId, options);
processedChannelIds.add(channel.id);
}
}
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 | 🟠 Major

Mirror the email/Slack availability checks in the action.

editableTypes only protects deletions. The create/update path still upserts email alerts whenever emails[] is posted and Slack alerts whenever slackChannel is posted, even if those sections were hidden or unavailable in the UI. A crafted POST can therefore persist channels the page intentionally disallows.

Also applies to: 141-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.errors/route.tsx
around lines 97 - 127, The current action unconditionally creates email and
Slack channels from the posted emails[] and slackChannel even when those UI
sections are not editable; before creating channels in the emails loop and
before the slackChannel block, verify the corresponding availability/editable
flag (the same check used to render the UI, e.g., inspect editableTypes or the
feature availability predicate used elsewhere) and skip creation if that channel
type is not allowed; ensure the check happens prior to constructing
CreateAlertChannelOptions and calling service.call(project.externalRef, userId,
...), mirroring the UI logic so only permitted email (emails) and Slack
(slackChannel + slackIntegrationId) channels are upserted.

Comment on lines +93 to +107
// Create secret reference for Slack token
const secretStore = getSecretStore("DATABASE");
const secretKey = `slack-test-token-${Date.now()}`;

await secretStore.setSecret(secretKey, {
botAccessToken: process.env.TEST_SLACK_BOT_TOKEN!,
});

const secretReference = await prisma.secretReference.create({
data: {
key: secretKey,
provider: "DATABASE",
},
});

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 | 🟠 Major

Delete the stored bot token in teardown.

This writes a live TEST_SLACK_BOT_TOKEN into the secret store and creates a secretReference, but afterAll never removes either one. Repeated runs will leave orphaned secret metadata and a persisted real credential behind.

Also applies to: 138-157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/test/slackErrorAlerts.test.ts` around lines 93 - 107, The test
creates a live secret and DB metadata (using secretStore.setSecret with
secretKey and prisma.secretReference.create) but never cleans them up; add
teardown logic in afterAll to remove both the persisted secret and its DB
record: call the secret store deletion for secretKey via
secretStore.deleteSecret (or the store's remove API) and delete the created
secretReference by id via prisma.secretReference.delete({ where: { id:
<the-created-id> } }) (ensure you capture the created secretReference id when
creating it in the test so teardown can reference it); apply the same cleanup
for the other instance noted at lines ~138-157.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +52 to +60
async evaluate(projectId: string, scheduledAt: number): Promise<void> {
const nextScheduledAt = Date.now();

const channels = await this.resolveChannels(projectId);
if (channels.length === 0) {
logger.info("[ErrorAlertEvaluator] No active ERROR_GROUP channels, self-terminating", {
projectId,
});
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Evaluator self-chain stops permanently if all retry attempts fail

The ErrorAlertEvaluator self-chains by enqueuing the next evaluation at the end of evaluate() (line 168). The job has maxAttempts: 3 (alertsWorker.server.ts:66). If all 3 retry attempts fail (e.g., due to a transient ClickHouse outage), selfChain() is never called, and the evaluator permanently stops for that project. It would only restart when a channel is created/enabled/updated via CreateAlertChannelService or the alerts page enable action. This is graceful degradation rather than a bug, but operators should be aware that extended ClickHouse outages could silently disable error alerting for all projects until channels are re-touched.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +146 to +162
if (submission.value.totalOccurrences) {
const qb = clickhouseClient.errors.listQueryBuilder();
qb.where("project_id = {projectId: String}", { projectId: project.id });
qb.where("environment_id = {environmentId: String}", {
environmentId: environment.id,
});
qb.where("error_fingerprint = {fingerprint: String}", { fingerprint });
qb.where("task_identifier = {taskIdentifier: String}", {
taskIdentifier: submission.value.taskIdentifier,
});
qb.groupBy("error_fingerprint, task_identifier");

const [err, results] = await qb.execute();
if (!err && results && results.length > 0) {
occurrenceCountAtIgnoreTime = results[0].occurrence_count;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Total occurrence ignore threshold silently degrades if ClickHouse query fails at ignore time

In the fingerprint route action at lines 146-162, occurrenceCountAtIgnoreTime is only fetched from ClickHouse when submission.value.totalOccurrences is set. If the ClickHouse query fails (error or empty results), occurrenceCountAtIgnoreTime remains undefined, resulting in ignoredAtOccurrenceCount: null in the DB. In the evaluator's isIgnoreBreached() at line 220, the total-occurrences check is skipped when ignoredAtOccurrenceCount is null. This means a user who sets a total occurrence threshold but experiences a transient ClickHouse failure will have that threshold effectively disabled forever. There's no user-visible indication of this degradation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 4 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +308 to +314
const qb = this._clickhouse.errors.activeErrorsSinceQueryBuilder();
qb.where("project_id = {projectId: String}", { projectId });
qb.where("environment_id IN {envIds: Array(String)}", { envIds });
qb.groupBy("environment_id, task_identifier, error_fingerprint");
qb.having("toInt64(last_seen) > {scheduledAt: Int64}", {
scheduledAt,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 activeErrorsSinceQueryBuilder omits organization_id filter unlike other error queries

The getActiveErrors method at apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts:308-314 filters only by project_id and environment_id, while every other error query in the codebase (e.g., ErrorGroupPresenter.getSummary, ErrorsListPresenter.call) also includes organization_id. Since project_id is globally unique this won't cause incorrect results, but if the errors_v1 table is partitioned or indexed by organization_id first, omitting it could impact query performance on large datasets.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +377 to +398
export function getActiveErrorsSinceQueryBuilder(
ch: ClickhouseReader,
settings?: ClickHouseSettings
) {
return ch.queryBuilder({
name: "getActiveErrorsSince",
baseQuery: `
SELECT
environment_id,
task_identifier,
error_fingerprint,
any(error_type) as error_type,
any(error_message) as error_message,
any(sample_stack_trace) as sample_stack_trace,
toString(toUnixTimestamp64Milli(min(first_seen))) as first_seen,
toString(toUnixTimestamp64Milli(max(last_seen))) as last_seen,
toUInt64(sumMerge(occurrence_count)) as occurrence_count
FROM trigger_dev.errors_v1
`,
schema: ActiveErrorsSinceQueryResult,
settings,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 activeErrorsSinceQueryBuilder references sample_stack_trace on errors_v1 without a visible ClickHouse migration

The ActiveErrorsSinceQueryResult schema at internal-packages/clickhouse/src/errors.ts:358-370 expects a sample_stack_trace field from errors_v1, and the query uses any(sample_stack_trace). However, other existing queries against errors_v1 (e.g., getErrorGroupsListQueryBuilder, getErrorGroups) don't reference this column, and no ClickHouse migration adding it appears in this PR. If the column doesn't exist, the alert evaluator query will fail at runtime. If it does exist, the use of any() (vs anyMerge() used for other aggregate columns like sample_run_id) should be verified against the column's actual type.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +324 to +337
private async getErrorGroupStates(
activeErrors: ActiveErrorsSinceQueryResult[]
): Promise<ErrorGroupState[]> {
if (activeErrors.length === 0) return [];

return this._replica.errorGroupState.findMany({
where: {
OR: activeErrors.map((e) => ({
environmentId: e.environment_id,
taskIdentifier: e.task_identifier,
errorFingerprint: e.error_fingerprint,
})),
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Unbounded Prisma OR query in evaluator could generate very large SQL for high-volume projects

The getErrorGroupStates method at apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts:329-337 builds a Prisma OR clause from all active errors. For projects with many distinct error fingerprints across multiple environments, this could generate a very large SQL query. The same pattern appears in ErrorsListPresenter.getErrorGroupStates at apps/webapp/app/presenters/v3/ErrorsListPresenter.server.ts:502-508 but is bounded by page size (25). The evaluator's query has no such bound — it processes ALL errors with last_seen > scheduledAt.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +77 to +78
firstSeen: new Date().toISOString(),
lastSeen: new Date().toISOString(),
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Test mock data uses ISO dates instead of millisecond timestamps

In apps/webapp/test/webhookErrorAlerts.test.ts:77-78, the createMockErrorPayload function sets firstSeen: new Date().toISOString() and lastSeen: new Date().toISOString(). However, generateErrorGroupWebhookPayload at apps/webapp/app/v3/services/alerts/errorGroupWebhook.server.ts:51-52 does new Date(Number(data.error.firstSeen)). Number("2024-01-01T00:00:00.000Z") produces NaN, leading to Invalid Date, which would cause Webhook.parse() to fail validation. The sibling test file errorGroupWebhook.test.ts correctly uses String(earlier) (millisecond timestamps). These tests require a database and may not run in standard CI, so this may not block the PR.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

2 participants