Skip to content

Commit 7f30042

Browse files
committed
Rework ErrorGroupState queries/table for indexes
1 parent d13f8f5 commit 7f30042

File tree

7 files changed

+118
-36
lines changed

7 files changed

+118
-36
lines changed

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

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,20 +168,47 @@ export class ErrorsListPresenter extends BasePresenter {
168168
(tasks !== undefined && tasks.length > 0) ||
169169
(versions !== undefined && versions.length > 0) ||
170170
(search !== undefined && search !== "") ||
171-
(statuses !== undefined && statuses.length > 0) ||
172-
!time.isDefault;
171+
(statuses !== undefined && statuses.length > 0);
173172

174173
const possibleTasksAsync = getAllTaskIdentifiers(this.replica, environmentId);
175174

176-
const [possibleTasks, displayableEnvironment] = await Promise.all([
175+
// Pre-filter by status: since status lives in Postgres (ErrorGroupState) and the error
176+
// list comes from ClickHouse, we resolve inclusion/exclusion sets upfront so that
177+
// ClickHouse pagination operates on the correctly filtered dataset.
178+
const statusFilterAsync = this.resolveStatusFilter(environmentId, statuses);
179+
180+
const [possibleTasks, displayableEnvironment, statusFilter] = await Promise.all([
177181
possibleTasksAsync,
178182
findDisplayableEnvironment(environmentId, userId),
183+
statusFilterAsync,
179184
]);
180185

181186
if (!displayableEnvironment) {
182187
throw new ServiceValidationError("No environment found");
183188
}
184189

190+
if (statusFilter.empty) {
191+
return {
192+
errorGroups: [],
193+
pagination: {
194+
next: undefined,
195+
previous: undefined,
196+
},
197+
filters: {
198+
tasks,
199+
versions,
200+
statuses,
201+
search,
202+
period: time,
203+
from: effectiveFrom,
204+
to: effectiveTo,
205+
hasFilters,
206+
possibleTasks,
207+
wasClampedByRetention,
208+
},
209+
};
210+
}
211+
185212
// Query the per-minute error_occurrences_v1 table for time-scoped counts
186213
const queryBuilder = this.clickhouse.errors.occurrencesListQueryBuilder();
187214

@@ -205,6 +232,19 @@ export class ErrorsListPresenter extends BasePresenter {
205232
queryBuilder.where("task_version IN {versions: Array(String)}", { versions });
206233
}
207234

235+
if (statusFilter.includeKeys) {
236+
queryBuilder.where(
237+
"concat(task_identifier, '::', error_fingerprint) IN ({statusIncludeKeys: Array(String)})",
238+
{ statusIncludeKeys: statusFilter.includeKeys }
239+
);
240+
}
241+
if (statusFilter.excludeKeys) {
242+
queryBuilder.where(
243+
"concat(task_identifier, '::', error_fingerprint) NOT IN ({statusExcludeKeys: Array(String)})",
244+
{ statusExcludeKeys: statusFilter.excludeKeys }
245+
);
246+
}
247+
208248
queryBuilder.groupBy("error_fingerprint, task_identifier");
209249

210250
// Text search via HAVING (operates on aggregated values)
@@ -292,12 +332,6 @@ export class ErrorsListPresenter extends BasePresenter {
292332
};
293333
});
294334

295-
if (statuses && statuses.length > 0) {
296-
transformedErrorGroups = transformedErrorGroups.filter((g) =>
297-
statuses.includes(g.status as ErrorGroupStatus)
298-
);
299-
}
300-
301335
return {
302336
errorGroups: transformedErrorGroups,
303337
pagination: {
@@ -393,6 +427,61 @@ export class ErrorsListPresenter extends BasePresenter {
393427
return { data };
394428
}
395429

430+
/**
431+
* Determines which (task, fingerprint) pairs to include or exclude from the ClickHouse
432+
* query based on the requested status filter. Since status lives in Postgres and errors
433+
* live in ClickHouse, we resolve the filter set here so ClickHouse pagination is correct.
434+
*
435+
* - UNRESOLVED is the default (no ErrorGroupState row), so filtering FOR it means
436+
* excluding groups with non-matching explicit statuses.
437+
* - RESOLVED/IGNORED are explicit, so filtering for them means including only matching groups.
438+
*/
439+
private async resolveStatusFilter(
440+
environmentId: string,
441+
statuses?: ErrorGroupStatus[]
442+
): Promise<{
443+
includeKeys?: string[];
444+
excludeKeys?: string[];
445+
empty: boolean;
446+
}> {
447+
if (!statuses || statuses.length === 0) {
448+
return { empty: false };
449+
}
450+
451+
const allStatuses: ErrorGroupStatus[] = ["UNRESOLVED", "RESOLVED", "IGNORED"];
452+
const excludedStatuses = allStatuses.filter((s) => !statuses.includes(s));
453+
454+
if (excludedStatuses.length === 0) {
455+
return { empty: false };
456+
}
457+
458+
if (statuses.includes("UNRESOLVED")) {
459+
const excluded = await this.replica.errorGroupState.findMany({
460+
where: { environmentId, status: { in: excludedStatuses } },
461+
select: { taskIdentifier: true, errorFingerprint: true },
462+
});
463+
if (excluded.length === 0) {
464+
return { empty: false };
465+
}
466+
return {
467+
excludeKeys: excluded.map((g) => `${g.taskIdentifier}::${g.errorFingerprint}`),
468+
empty: false,
469+
};
470+
}
471+
472+
const included = await this.replica.errorGroupState.findMany({
473+
where: { environmentId, status: { in: statuses } },
474+
select: { taskIdentifier: true, errorFingerprint: true },
475+
});
476+
if (included.length === 0) {
477+
return { empty: true };
478+
}
479+
return {
480+
includeKeys: included.map((g) => `${g.taskIdentifier}::${g.errorFingerprint}`),
481+
empty: false,
482+
};
483+
}
484+
396485
/**
397486
* Batch-fetch ErrorGroupState rows from Postgres for the given ClickHouse error groups.
398487
* Returns a map keyed by `${taskIdentifier}:${errorFingerprint}`.

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,10 +408,7 @@ function FiltersBar({
408408
searchParams.has("status") ||
409409
searchParams.has("tasks") ||
410410
searchParams.has("versions") ||
411-
searchParams.has("search") ||
412-
searchParams.has("period") ||
413-
searchParams.has("from") ||
414-
searchParams.has("to");
411+
searchParams.has("search");
415412

416413
return (
417414
<div className="flex items-start justify-between gap-x-2 border-b border-grid-bright p-2">

apps/webapp/app/v3/services/alerts/errorAlertEvaluator.server.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export class ErrorAlertEvaluator {
9393
return;
9494
}
9595

96-
const states = await this.getErrorGroupStates(projectId, activeErrors, envIds);
96+
const states = await this.getErrorGroupStates(activeErrors);
9797
const stateMap = this.buildStateMap(states);
9898

9999
const occurrenceCounts = await this.getOccurrenceCountsSince(projectId, envIds, scheduledAt);
@@ -322,18 +322,17 @@ export class ErrorAlertEvaluator {
322322
}
323323

324324
private async getErrorGroupStates(
325-
projectId: string,
326-
activeErrors: ActiveErrorsSinceQueryResult[],
327-
envIds: string[]
325+
activeErrors: ActiveErrorsSinceQueryResult[]
328326
): Promise<ErrorGroupState[]> {
329-
const fingerprints = [...new Set(activeErrors.map((e) => e.error_fingerprint))];
330-
if (fingerprints.length === 0) return [];
327+
if (activeErrors.length === 0) return [];
331328

332329
return this._replica.errorGroupState.findMany({
333330
where: {
334-
projectId,
335-
errorFingerprint: { in: fingerprints },
336-
environmentId: { in: envIds },
331+
OR: activeErrors.map((e) => ({
332+
environmentId: e.environment_id,
333+
taskIdentifier: e.task_identifier,
334+
errorFingerprint: e.error_fingerprint,
335+
})),
337336
},
338337
});
339338
}

internal-packages/database/prisma/migrations/20260306102053_error_group_state/migration.sql

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
CREATE TYPE "public"."ErrorGroupStatus" AS ENUM ('UNRESOLVED', 'RESOLVED', 'IGNORED');
33

44
-- AlterEnum
5-
ALTER TYPE "public"."ProjectAlertType" ADD VALUE 'ERROR_GROUP';
5+
ALTER TYPE "public"."ProjectAlertType" ADD VALUE IF NOT EXISTS 'ERROR_GROUP';
66

77
-- CreateTable
88
CREATE TABLE
@@ -17,6 +17,7 @@ CREATE TABLE
1717
"ignoredUntil" TIMESTAMP(3),
1818
"ignoredUntilOccurrenceRate" INTEGER,
1919
"ignoredUntilTotalOccurrences" INTEGER,
20+
"ignoredAtOccurrenceCount" BIGINT,
2021
"ignoredAt" TIMESTAMP(3),
2122
"ignoredReason" TEXT,
2223
"ignoredByUserId" TEXT,
@@ -28,24 +29,25 @@ CREATE TABLE
2829
CONSTRAINT "ErrorGroupState_pkey" PRIMARY KEY ("id")
2930
);
3031

31-
-- CreateIndex
32-
CREATE INDEX "ErrorGroupState_status_idx" ON "public"."ErrorGroupState" ("status");
33-
34-
-- CreateIndex
35-
CREATE INDEX "ErrorGroupState_ignoredUntil_idx" ON "public"."ErrorGroupState" ("ignoredUntil");
36-
3732
-- CreateIndex
3833
CREATE UNIQUE INDEX "ErrorGroupState_environmentId_taskIdentifier_errorFingerpri_key" ON "public"."ErrorGroupState" (
3934
"environmentId",
4035
"taskIdentifier",
4136
"errorFingerprint"
4237
);
4338

39+
-- CreateIndex
40+
CREATE INDEX "ErrorGroupState_environmentId_status_idx" ON "public"."ErrorGroupState" ("environmentId", "status");
41+
4442
-- AddForeignKey
4543
ALTER TABLE "public"."ErrorGroupState" ADD CONSTRAINT "ErrorGroupState_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
4644

4745
-- AddForeignKey
4846
ALTER TABLE "public"."ErrorGroupState" ADD CONSTRAINT "ErrorGroupState_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "public"."Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
4947

5048
-- AddForeignKey
51-
ALTER TABLE "public"."ErrorGroupState" ADD CONSTRAINT "ErrorGroupState_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "public"."RuntimeEnvironment" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
49+
ALTER TABLE "public"."ErrorGroupState" ADD CONSTRAINT "ErrorGroupState_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "public"."RuntimeEnvironment" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
50+
51+
-- AlterTable
52+
ALTER TABLE "public"."ProjectAlertChannel"
53+
ADD COLUMN "errorAlertConfig" JSONB;

internal-packages/database/prisma/migrations/20260308181657_add_error_alert_config_to_project_alert_channel/migration.sql

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

internal-packages/database/prisma/migrations/20260320115950_add_ignored_at_occurrence_count_to_error_group_state/migration.sql

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

internal-packages/database/prisma/schema.prisma

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2717,6 +2717,5 @@ model ErrorGroupState {
27172717
updatedAt DateTime @updatedAt
27182718
27192719
@@unique([environmentId, taskIdentifier, errorFingerprint])
2720-
@@index([status])
2721-
@@index([ignoredUntil])
2720+
@@index([environmentId, status])
27222721
}

0 commit comments

Comments
 (0)