-
+
+
}>
}>
- {(result) =>
- result.data.length > 0 ? (
-
- ) : (
-
- )
- }
+ {(result) => {
+ if (result.data.length > 0 && result.versions.length > 0) {
+ return
;
+ }
+ return
;
+ }}
@@ -369,10 +380,10 @@ function ErrorGroupDetail({
{runList ? (
0}
filters={{
tasks: [],
- versions: [],
+ versions: selectedVersions,
statuses: [],
from: undefined,
to: undefined,
@@ -392,14 +403,24 @@ function ErrorGroupDetail({
);
}
-const activityChartConfig: ChartConfig = {
- count: {
- label: "Occurrences",
- color: "#6366F1",
- },
-};
+function ActivityChart({
+ activity,
+ versions,
+}: {
+ activity: ErrorGroupActivity;
+ versions: ErrorGroupActivityVersions;
+}) {
+ const chartConfig = useMemo(() => {
+ const cfg: ChartConfig = {};
+ for (let i = 0; i < versions.length; i++) {
+ cfg[versions[i]] = {
+ label: versions[i],
+ color: getSeriesColor(i),
+ };
+ }
+ return cfg;
+ }, [versions]);
-function ActivityChart({ activity }: { activity: ErrorGroupActivity }) {
const data = useMemo(
() =>
activity.map((d) => ({
@@ -453,13 +474,14 @@ function ActivityChart({ activity }: { activity: ErrorGroupActivity }) {
return (
{
const url = new URL(request.url);
const tasks = url.searchParams.getAll("tasks").filter((t) => t.length > 0);
+ const versions = url.searchParams.getAll("versions").filter((v) => v.length > 0);
const search = url.searchParams.get("search") ?? undefined;
const period = url.searchParams.get("period") ?? undefined;
const fromStr = url.searchParams.get("from");
@@ -101,6 +103,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
userId,
projectId: project.id,
tasks: tasks.length > 0 ? tasks : undefined,
+ versions: versions.length > 0 ? versions : undefined,
search,
period,
from,
@@ -239,6 +242,7 @@ function FiltersBar({
const searchParams = new URLSearchParams(location.search);
const hasFilters =
searchParams.has("tasks") ||
+ searchParams.has("versions") ||
searchParams.has("search") ||
searchParams.has("period") ||
searchParams.has("from") ||
@@ -250,6 +254,7 @@ function FiltersBar({
{list ? (
<>
+
+
{hasFilters && (
@@ -373,6 +379,9 @@ function ErrorGroupRow({
if (period) carry.set("period", period);
if (from) carry.set("from", from);
if (to) carry.set("to", to);
+ for (const v of searchParams.getAll("versions")) {
+ if (v) carry.append("versions", v);
+ }
const qs = carry.toString();
return qs ? `${base}?${qs}` : base;
}, [organizationSlug, projectParam, envParam, errorGroup.fingerprint, searchParams.toString()]);
diff --git a/internal-packages/clickhouse/src/errors.ts b/internal-packages/clickhouse/src/errors.ts
index c93efbcaf1..77ac05e0a2 100644
--- a/internal-packages/clickhouse/src/errors.ts
+++ b/internal-packages/clickhouse/src/errors.ts
@@ -314,3 +314,39 @@ export function createErrorOccurrencesQueryBuilder(
settings
);
}
+
+export const ErrorOccurrencesByVersionQueryResult = z.object({
+ error_fingerprint: z.string(),
+ task_version: z.string(),
+ bucket_epoch: z.number(),
+ count: z.number(),
+});
+
+export type ErrorOccurrencesByVersionQueryResult = z.infer<
+ typeof ErrorOccurrencesByVersionQueryResult
+>;
+
+/**
+ * Creates a query builder for bucketed error occurrence counts grouped by task_version.
+ * Used for stacked-by-version activity charts on the error detail page.
+ */
+export function createErrorOccurrencesByVersionQueryBuilder(
+ ch: ClickhouseReader,
+ intervalExpr: string,
+ settings?: ClickHouseSettings
+): ClickhouseQueryBuilder {
+ return new ClickhouseQueryBuilder(
+ "getErrorOccurrencesByVersion",
+ `
+ SELECT
+ error_fingerprint,
+ task_version,
+ toUnixTimestamp(toStartOfInterval(minute, ${intervalExpr})) as bucket_epoch,
+ sum(count) as count
+ FROM trigger_dev.error_occurrences_v1
+ `,
+ ch,
+ ErrorOccurrencesByVersionQueryResult,
+ settings
+ );
+}
diff --git a/internal-packages/clickhouse/src/index.ts b/internal-packages/clickhouse/src/index.ts
index b6fbd92177..0432b0625d 100644
--- a/internal-packages/clickhouse/src/index.ts
+++ b/internal-packages/clickhouse/src/index.ts
@@ -34,6 +34,7 @@ import {
getErrorHourlyOccurrences,
getErrorOccurrencesListQueryBuilder,
createErrorOccurrencesQueryBuilder,
+ createErrorOccurrencesByVersionQueryBuilder,
getErrorAffectedVersionsQueryBuilder,
} from "./errors.js";
export { msToClickHouseInterval } from "./intervals.js";
@@ -251,6 +252,8 @@ export class ClickHouse {
occurrencesListQueryBuilder: getErrorOccurrencesListQueryBuilder(this.reader),
createOccurrencesQueryBuilder: (intervalExpr: string) =>
createErrorOccurrencesQueryBuilder(this.reader, intervalExpr),
+ createOccurrencesByVersionQueryBuilder: (intervalExpr: string) =>
+ createErrorOccurrencesByVersionQueryBuilder(this.reader, intervalExpr),
};
}
}