Repository: keephq/keep
Pull request: feat: Added dashboard for Alert Quality
<!-- 
Thanks for creating this pull request 🤗

Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
-->

<!-- If this pull request closes an issue, please mention the issue number below -->
Closes #1779
/claim #1779 
## 📑 Description
<!-- Add a brief description of the pr -->

<!-- You can also choose to add a list of changes and if they have been completed or not by using the markdown to-do list syntax
- [ ] Not Completed
- [x] Completed
-->

## ✅ Checks
<!-- Make sure your pr passes the CI checks and do check the following fields as needed - -->
- [ ] My pull request adheres to the code style of this project
- [ ] My code requires changes to the documentation
- [ ] I have updated the documentation as required
- [ ] All the tests have passed

## ℹ Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->


Problem statement:
[➕ Feature]: Alert quality widgets for a dashboard
Users connect a lot of providers for Keep, and we should provide some analytics about the quality of events coming out of those providers.

I propose to add a few widgets, and an auto-provisioned dashboard called "Alert quality". 

Imagine a use-case when users wants to make sure that all alert providers provide "severity", "department", "product" and "affected_customer" from the sources. This dashboard should help them.

Widgets:
- A table with a per-provider alert quality. Rows: connected providers. Columns: provider name, amount of alerts received, % of alerts correlated to incidents, % of alerts having a "field" filled. Where field is a configurable set of fields. 
- % of alerts correlated to incidents
- % of alerts having a "field" filled

Hints from issue discussion:
/bounty 200
## 💎 $200 bounty [• Keep (YC W23)](https://console.algora.io/org/keephq)

### Steps to solve:
1. **Start working**: Comment `/attempt #1779` with your implementation plan
2. **Submit work**: Create a pull request including `/claim #1779` in the PR body to claim the bounty
3. **Receive payment**: 100% of the bounty is received 2-5 days post-reward. [Make sure you are eligible for payouts](https://docs.algora.io/bounties/payments#supported-countries-regions)

Thank you for contributing to keephq/keep!

**[Add a bounty](https://console.algora.io/org/keephq/bounties/community?fund=keephq%2Fkeep%231779)** • **[Share on socials](https://twitter.com/intent/tweet?text=%24200+bounty%21+%F0%9F%92%8E+https%3A%2F%2Fgithub.com%2Fkeephq%2Fkeep%2Fissues%2F1779&related=algoraio)**

<table>
<thead>
<tr>
<th>Attempt</th>
<th>Started (GMT+0)</th>
<th>Solution</th>
</tr>
</thead>
<tbody>
<tr>
<td>🟢 @vikashsprem</td>
<td>Sep 11, 2024, 9:15:28 AM</td>
<td><a href="https://github.com/keephq/keep/pull/1977">#1977</a></td>
</tr>
</tbody>
</table>
/attempt #1779


<div id="algora-attempt" />

| [Algora profile](https://console.algora.io/@/vikashsprem) | Completed bounties | Tech | Active attempts | Options |
| --- | --- | --- | --- | --- |
| @vikashsprem | 1 bounty from 1 project | <div align="center">TypeScript, JavaScript, <br />HTML & more</div> | <div align="center"></div> | [Cancel attempt](https://console.algora.io/api/bounties/cm0xn7wpz0004mg039ghk38iq/cancel-attempt) |
@Matvey-Kuk Could you pls assign me? I will start working on it.
@vikashsprem absolutely! Please keep us updated on the progress :)
@vikashsprem I would like to collaborate with you if that works for you. Please let me know how I can assist.
@Matvey-Kuk should I add the "Alert Quality" dashboard as a sub-option under one of the Alerts in the sidebar?

Is the widget that should look like this (A rough overview): 
# Alert Quality Dashboard

| Provider Name | Alerts Received | % Alerts Correlated to Incidents | % Alerts with "Severity" | % Alerts with "Department" | % Alerts with "Product" | % Alerts with "Affected Customer" |
|---------------|-----------------|---------------------------------|---------------------------|----------------------------|-------------------------|----------------------------------|
| Provider A    | 1200            | 85%                             | 95%                       | 90%                        | 88%                     | 92%                              |
| Provider B    | 980             | 78%                             | 92%                       | 85%                        | 83%                     | 89%                              |
| Provider C    | 450             | 65%                             | 88%                       | 82%                        | 79%                     | 85%                              |
| Provider D    | 300             | 72%                             | 91%                       | 87%                        | 84%                     | 90%                              |

@Matvey-Kuk @vikashsprem 
I noticed this open issue for the Alert Quality dashboard feature and I'm interested in contributing. Before I start working on it, I wanted to confirm a few things:

- Is this issue still available for contribution?
- As vikashsprem has already been assigned, are you open to having another contributor work on this as well? If so, would you prefer collaboration or should I wait to see if vikashsprem needs any assistance?
- If I am able to contribute, are there any specific guidelines or requirements I should be aware of before starting?
@vikashsprem you are absolutely right about the dashboard view :)
@vishalxtyagi it's up to @vikashsprem if he is open to the co-contributor :)

Commit message: Merge branch 'main' into main

Patch to review:
diff --git a/keep-ui/app/alerts/quality/alert-quality-table.tsx b/keep-ui/app/alerts/quality/alert-quality-table.tsx
new file mode 100644
index 0000000000..cc035466ed
--- /dev/null
+++ b/keep-ui/app/alerts/quality/alert-quality-table.tsx
@@ -0,0 +1,329 @@
+"use client"; // Add this line at the top to make this a Client Component
+
+import React, {
+  useState,
+  useEffect,
+  Dispatch,
+  SetStateAction,
+  useMemo,
+} from "react";
+import { GenericTable } from "@/components/table/GenericTable";
+import { useAlertQualityMetrics } from "utils/hooks/useAlertQuality";
+import { useProviders } from "utils/hooks/useProviders";
+import { Provider, ProvidersResponse } from "app/providers/providers";
+import { TabGroup, TabList, Tab } from "@tremor/react";
+import { GenericFilters } from "@/components/filters/GenericFilters";
+import { useSearchParams } from "next/navigation";
+import { AlertKnownKeys } from "../models";
+
+const tabs = [
+  { name: "All", value: "all" },
+  { name: "Installed", value: "installed" },
+];
+
+const ALERT_QUALITY_FILTERS = [
+  {
+    type: "date",
+    key: "time_stamp",
+    value: "",
+    name: "Last received",
+  },
+];
+
+export const FilterTabs = ({
+  tabs,
+  setTab,
+  tab,
+}: {
+  tabs: { name: string; value: string }[];
+  setTab: Dispatch<SetStateAction<number>>;
+  tab: number;
+}) => {
+  return (
+    <div className="max-w-lg space-y-12 pt-6">
+      <TabGroup
+        index={tab}
+        onIndexChange={(index: number) => {
+          setTab(index);
+        }}
+      >
+        <TabList variant="solid" color="black" className="bg-gray-300">
+          {tabs.map((tabItem) => (
+            <Tab key={tabItem.value}>{tabItem.name}</Tab>
+          ))}
+        </TabList>
+      </TabGroup>
+    </div>
+  );
+};
+
+interface AlertMetricQuality {
+  alertsReceived: number;
+  alertsCorrelatedToIncidentsPercentage: number;
+  alertsWithSeverityPercentage: number;
+  [key: string]: number;
+}
+
+type FinalAlertQuality = (Provider &
+  AlertMetricQuality & { provider_display_name: string })[];
+interface Pagination {
+  limit: number;
+  offset: number;
+}
+
+const QualityTable = ({
+  providersMeta,
+  alertsQualityMetrics,
+  isDashBoard,
+  setFields,
+  fieldsValue,
+}: {
+  providersMeta: ProvidersResponse | undefined;
+  alertsQualityMetrics: Record<string, Record<string, any>> | undefined;
+  isDashBoard?: boolean;
+  setFields: (fields: string | string[] | Record<string, string>) => void;
+  fieldsValue: string | string[] | Record<string, string>;
+}) => {
+  const [pagination, setPagination] = useState<Pagination>({
+    limit: 10,
+    offset: 0,
+  });
+  const customFieldFilter = {
+    type: "select",
+    key: "fields",
+    value: isDashBoard ? fieldsValue : "",
+    name: "Field",
+    options: AlertKnownKeys.map((key) => ({ value: key, label: key })),
+    // only_one: true,
+    searchParamsNotNeed: isDashBoard,
+    can_select: 3,
+    setFilter: setFields,
+  };
+  const searchParams = useSearchParams();
+  const entries = searchParams ? Array.from(searchParams.entries()) : [];
+  const params = entries.reduce((acc, [key, value]) => {
+    if (key in acc) {
+      if (Array.isArray(acc[key])) {
+        acc[key] = [...acc[key], value];
+        return acc;
+      } else {
+        acc[key] = [acc[key] as string, value];
+      }
+      return acc;
+    }
+    acc[key] = value;
+    return acc;
+  }, {} as Record<string, string | string[]>);
+  function toArray(value: string | string[]) {
+    if (!value) {
+      return [];
+    }
+
+    if (!Array.isArray(value) && value) {
+      return [value];
+    }
+
+    return value;
+  }
+  const fields = toArray(
+    params?.["fields"] || (fieldsValue as string | string[]) || []
+  ) as string[];
+  const [tab, setTab] = useState(1);
+
+  const handlePaginationChange = (newLimit: number, newOffset: number) => {
+    setPagination({ limit: newLimit, offset: newOffset });
+  };
+
+  useEffect(() => {
+    handlePaginationChange(10, 0);
+  }, [tab, searchParams?.toString()]);
+
+  // Construct columns based on the fields selected
+  const columns = useMemo(() => {
+    const baseColumns = [
+      {
+        header: "Provider Name",
+        accessorKey: "provider_display_name",
+      },
+      {
+        header: "Alerts Received",
+        accessorKey: "alertsReceived",
+      },
+      {
+        header: "% of Alerts Correlated to Incidents",
+        accessorKey: "alertsCorrelatedToIncidentsPercentage",
+        cell: (info: any) => `${info.getValue().toFixed(2)}%`,
+      },
+    ];
+
+    // Add dynamic columns based on the fields
+    const dynamicColumns = fields.map((field: string) => ({
+      header: `% of Alerts Having ${
+        field.charAt(0).toUpperCase() + field.slice(1)
+      }`,
+      accessorKey: `alertsWith${
+        field.charAt(0).toUpperCase() + field.slice(1)
+      }Percentage`,
+      cell: (info: any) => `${info.getValue().toFixed(2)}%`,
+    }));
+
+    return [...baseColumns, ...dynamicColumns];
+  }, [fields]);
+
+  // Process data and include dynamic fields
+  const finalData = useMemo(() => {
+    let providers: Provider[] | null = null;
+
+    if (!providersMeta || !alertsQualityMetrics) {
+      return null;
+    }
+
+    switch (tab) {
+      case 0:
+        providers = providersMeta?.providers || providers;
+        break;
+      case 1:
+        providers = providersMeta?.installed_providers || providers;
+        break;
+      default:
+        providers = providersMeta?.providers || providers;
+        break;
+    }
+
+    if (!providers) {
+      return null;
+    }
+
+    const groupedMetrics: { [key: string]: any } = {};
+
+    if (tab === 0) {
+      // Iterate over each provider in the alertsMetrics object
+      for (const provider in alertsQualityMetrics) {
+        const metrics = alertsQualityMetrics[provider];
+        const providerType = metrics.provider_type;
+
+        // If the provider_type doesn't exist in the result, initialize it
+        if (!groupedMetrics[providerType]) {
+          groupedMetrics[providerType] = {
+            total_alerts: 0,
+            correlated_alerts: 0,
+          };
+        }
+
+        // Aggregate the values for total_alerts, correlated_alerts, etc.
+        groupedMetrics[providerType].total_alerts += metrics.total_alerts;
+        groupedMetrics[providerType].correlated_alerts +=
+          metrics.correlated_alerts;
+
+        fields.forEach((field) => {
+          const key = `${field}_count`;
+          groupedMetrics[providerType][key] =
+            groupedMetrics[providerType][key] || 0;
+          groupedMetrics[providerType][key] += metrics[key];
+        });
+      }
+    }
+
+    const innerData: FinalAlertQuality = providers.map((provider) => {
+      const providerId = provider.id;
+      const providerType = provider.type;
+      const key =`${providerId}_${providerType}`;
+      const alertQuality = tab ===0 ? groupedMetrics[providerType] :  alertsQualityMetrics[key];
+      const totalAlertsReceived = alertQuality?.total_alerts ?? 0;
+      const correlated_alerts = alertQuality?.correlated_alerts ?? 0;
+      const correltedPert =
+        totalAlertsReceived && correlated_alerts
+          ? (correlated_alerts / totalAlertsReceived) * 100
+          : 0;
+      const severityPert = totalAlertsReceived
+        ? ((alertQuality?.severity_count ?? 0) / totalAlertsReceived) * 100
+        : 0;
+
+      // Calculate percentages for dynamic fields
+      const dynamicFieldPercentages = fields.reduce((acc, field: string) => {
+        acc[
+          `alertsWith${
+            field.charAt(0).toUpperCase() + field.slice(1)
+          }Percentage`
+        ] = totalAlertsReceived
+          ? ((alertQuality?.[`${field}_count`] ?? 0) / totalAlertsReceived) *
+            100
+          : 0;
+        return acc;
+      }, {} as Record<string, number>);
+
+      return {
+        ...provider,
+        alertsReceived: totalAlertsReceived,
+        alertsCorrelatedToIncidentsPercentage: correltedPert,
+        alertsWithSeverityPercentage: severityPert,
+        ...dynamicFieldPercentages, // Add dynamic field percentages here
+        provider_display_name:
+          provider?.details?.name ? `${provider.details.name} (${provider.display_name})` : provider.display_name || "",
+      } as FinalAlertQuality[number];
+    });
+
+    return innerData;
+  }, [tab, providersMeta, alertsQualityMetrics, fields]);
+
+  return (
+    <div
+      className={`flex flex-col gap-2 p-2 px-4 ${isDashBoard ? "h-[90%]" : ""}`}
+    >
+      <div>
+        {!isDashBoard && (
+          <h1 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4">
+            Alert Quality Dashboard
+          </h1>
+        )}
+        <div className="flex justify-end items-end mb-4">
+          {/* if we want to use tabs. we can enable it */}
+          {/* <FilterTabs tabs={tabs} setTab={setTab} tab={tab} /> */}
+          <GenericFilters
+            filters={
+              isDashBoard
+                ? [customFieldFilter]
+                : [...ALERT_QUALITY_FILTERS, customFieldFilter]
+            }
+          />
+        </div>
+      </div>
+      {finalData && (
+        <GenericTable
+          data={finalData}
+          columns={columns}
+          rowCount={finalData?.length}
+          offset={pagination.offset}
+          limit={pagination.limit}
+          onPaginationChange={handlePaginationChange}
+          dataFetchedAtOneGO={true}
+          onRowClick={(row) => {
+            console.log("Row clicked:", row);
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+const AlertQuality = ({ isDashBoard }: { isDashBoard?: boolean }) => {
+  const [fieldsValue, setFieldsValue] = useState<
+    string | string[] | Record<string, string>
+  >("severity");
+  const { data: providersMeta } = useProviders();
+  const { data: alertsQualityMetrics, error } = useAlertQualityMetrics(
+    isDashBoard ? (fieldsValue as string) : ""
+  );
+
+  return (
+    <QualityTable
+      providersMeta={providersMeta}
+      alertsQualityMetrics={alertsQualityMetrics}
+      isDashBoard={isDashBoard}
+      setFields={setFieldsValue}
+      fieldsValue={fieldsValue}
+    />
+  );
+};
+
+export default AlertQuality;
diff --git a/keep-ui/app/alerts/quality/page.tsx b/keep-ui/app/alerts/quality/page.tsx
new file mode 100644
index 0000000000..982361518c
--- /dev/null
+++ b/keep-ui/app/alerts/quality/page.tsx
@@ -0,0 +1,12 @@
+import QualityAlert from './alert-quality-table'; // Adjust the import path if needed
+
+const AlertQualityPage = () => {
+  return <QualityAlert />;
+};
+
+export default AlertQualityPage;
+
+export const metadata = {
+  title: "Alert Quality Dashboard",
+  description: "View and manage alert quality metrics.",
+};
diff --git a/keep-ui/app/dashboard/GridItem.tsx b/keep-ui/app/dashboard/GridItem.tsx
index 7488ab12bc..235f2af30b 100644
--- a/keep-ui/app/dashboard/GridItem.tsx
+++ b/keep-ui/app/dashboard/GridItem.tsx
@@ -2,6 +2,9 @@ import React from "react";
 import { Card } from "@tremor/react";
 import MenuButton from "./MenuButton";
 import { WidgetData } from "./types";
+import AlertQuality from "app/alerts/quality/alert-quality-table";
+import { useSearchParams } from "next/navigation";
+import { format, parseISO } from "date-fns";
 
 interface GridItemProps {
   item: WidgetData;
@@ -10,10 +13,18 @@ interface GridItemProps {
 }
 
 const GridItem: React.FC<GridItemProps> = ({ item, onEdit, onDelete }) => {
+  const searchParams = useSearchParams();
+  let timeStampParams = searchParams?.get("time_stamp") ?? "{}";
+  let timeStamp: { start?: string; end?: string } = {};
+  try {
+    timeStamp = JSON.parse(timeStampParams as string);
+  } catch (e) {
+    timeStamp = {};
+  }
   const getColor = () => {
-    let color = '#000000';
+    let color = "#000000";
     for (let i = item.thresholds.length - 1; i >= 0; i--) {
-      if (item.preset.alerts_count >= item.thresholds[i].value) {
+      if (item.preset && item.preset.alerts_count >= item.thresholds[i].value) {
         color = item.thresholds[i].color;
         break;
       }
@@ -21,18 +32,46 @@ const GridItem: React.FC<GridItemProps> = ({ item, onEdit, onDelete }) => {
     return color;
   };
 
+  function getGenericMterics(item: WidgetData) {
+    switch (item.genericMetrics) {
+      case "alert_quality":
+        return <AlertQuality isDashBoard={true} />;
+
+      default:
+        return null;
+    }
+  }
+
   return (
     <Card className="relative w-full h-full p-4">
       <div className="flex flex-col h-full">
-        <div className="flex-none h-1/5 p-2 flex items-center justify-between">
-          <span className="text-lg font-semibold truncate">{item.name}</span>
-          <MenuButton onEdit={() => onEdit(item.i)} onDelete={() => onDelete(item.i)} />
+        <div
+          className={`flex-none flex items-center justify-between p-2 ${
+            item.preset ? "h-1/5" : "h-[10%]"
+          }`}
+        >
+          {/* For table view we need intract with table filter and pagination.so we aare dragging the widget here */}
+          <span
+            className={`text-lg font-semibold truncate ${
+              item.preset ? "" : "grid-item__widget"
+            }`}
+          >
+            {item.name}
+          </span>
+          <MenuButton
+            onEdit={() => onEdit(item.i)}
+            onDelete={() => onDelete(item.i)}
+          />
         </div>
-        <div className="flex-1 h-4/5 flex items-center justify-center grid-item__widget">
-          <div className="text-4xl font-bold" style={{ color: getColor() }}>
-            {item.preset.alerts_count}
+        {item.preset && (
+          //We can remove drag and drop style and make it same as table view. if we want to maintain consistency.
+          <div className="flex-1 h-4/5 flex items-center justify-center grid-item__widget">
+            <div className="text-4xl font-bold" style={{ color: getColor() }}>
+              {item.preset.alerts_count}
+            </div>
           </div>
-        </div>
+        )}
+        <div className="w-full h-[90%]">{getGenericMterics(item)}</div>
       </div>
     </Card>
   );
diff --git a/keep-ui/app/dashboard/GridLayout.tsx b/keep-ui/app/dashboard/GridLayout.tsx
index 89ed7e4db5..ed318a82b5 100644
--- a/keep-ui/app/dashboard/GridLayout.tsx
+++ b/keep-ui/app/dashboard/GridLayout.tsx
@@ -16,41 +16,61 @@ interface GridLayoutProps {
   presets: Preset[];
 }
 
-const GridLayout: React.FC<GridLayoutProps> = ({ layout, onLayoutChange, data, onEdit, onDelete, presets }) => {
+const GridLayout: React.FC<GridLayoutProps> = ({
+  layout,
+  onLayoutChange,
+  data,
+  onEdit,
+  onDelete,
+  presets,
+}) => {
   const layouts = { lg: layout };
 
   return (
-    <ResponsiveGridLayout
-      className="layout"
-      layouts={layouts}
-      onLayoutChange={(currentLayout: Layout[]) => {
-        const updatedLayout = currentLayout.map(item => ({
-          ...item,
-          static: item.static ?? false // Ensure static is a boolean
-        }));
-        onLayoutChange(updatedLayout as LayoutItem[]);
-      }}
-      breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
-      cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
-      rowHeight={30}
-      containerPadding={[0, 0]}
-      margin={[10, 10]}
-      useCSSTransforms={true}
-      isDraggable={true}
-      isResizable={true}
-      compactType={null}
-      draggableHandle=".grid-item__widget"
-    >
-      {data.map((item) => {
+    <>
+      <ResponsiveGridLayout
+        className="layout"
+        layouts={layouts}
+        onLayoutChange={(currentLayout: Layout[]) => {
+          const updatedLayout = currentLayout.map((item) => ({
+            ...item,
+            static: item.static ?? false, // Ensure static is a boolean
+          }));
+          onLayoutChange(updatedLayout as LayoutItem[]);
+        }}
+        breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
+        cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
+        rowHeight={30}
+        containerPadding={[0, 0]}
+        margin={[10, 10]}
+        useCSSTransforms={true}
+        isDraggable={true}
+        isResizable={true}
+        compactType={null}
+        draggableHandle=".grid-item__widget"
+      >
+        {data.map((item) => {
           //Fixing the static hardcode db value.
-          const preset = presets?.find(p => p?.id === item?.preset?.id);
-          item.preset = { ...item.preset,alerts_count: preset?.alerts_count ?? 0};
-        return (
-          <div key={item.i} data-grid={item}>
-            <GridItemContainer item={item} onEdit={onEdit} onDelete={onDelete} />
-          </div>
-      )})}
-    </ResponsiveGridLayout>
+          if (item.preset) {
+            const preset = presets?.find((p) => p?.id === item?.preset?.id);
+            item.preset = {
+              ...item.preset,
+              alerts_count: preset?.alerts_count ?? 0,
+            };
+           
+          }
+          return (
+            <div key={item.i} data-grid={item}>
+              <GridItemContainer
+                key={item.i}
+                item={item}
+                onEdit={onEdit}
+                onDelete={onDelete}
+              />
+            </div>
+          );        })}
+      </ResponsiveGridLayout>
+    </>
   );
 };
 
diff --git a/keep-ui/app/dashboard/WidgetModal.tsx b/keep-ui/app/dashboard/WidgetModal.tsx
index d269c0b7d4..ce3f9691a1 100644
--- a/keep-ui/app/dashboard/WidgetModal.tsx
+++ b/keep-ui/app/dashboard/WidgetModal.tsx
@@ -5,22 +5,33 @@ import { Trashcan } from "components/icons";
 import { Threshold, WidgetData } from "./types";
 import { Preset } from "app/alerts/models";
 import { useForm, Controller, get } from "react-hook-form";
+import { Dashboard } from '../../utils/hooks/useDashboards';
+import { on } from "events";
 
 interface WidgetForm {
   widgetName: string;
   selectedPreset: string;
   thresholds: Threshold[];
+  selectedWidgetType: string;
+  selectedGenericMetrics: string;
 }
 
 interface WidgetModalProps {
   isOpen: boolean;
   onClose: () => void;
-  onAddWidget: (preset: Preset, thresholds: Threshold[], name: string) => void;
+  onAddWidget: (preset: Preset|null, thresholds: Threshold[], name: string, widgetType?: string, genericMetrics?: string) => void;
   onEditWidget: (updatedWidget: WidgetData) => void;
   presets: Preset[];
   editingItem?: WidgetData | null;
 }
 
+const GENERIC_METRICS = [
+  {
+    'key': 'alert_quality',
+    'name': 'Alert Quality'
+  }
+]
+
 const WidgetModal: React.FC<WidgetModalProps> = ({ isOpen, onClose, onAddWidget, onEditWidget, presets, editingItem }) => {
   const [thresholds, setThresholds] = useState<Threshold[]>([
     { value: 0, color: '#22c55e' }, // Green
@@ -32,19 +43,26 @@ const WidgetModal: React.FC<WidgetModalProps> = ({ isOpen, onClose, onAddWidget,
       widgetName: '',
       selectedPreset: '',
       thresholds: thresholds,
+      selectedWidgetType: '',
+      selectedGenericMetrics: ''
     }
   });
 
+  const [currentWidgetType, setCurrentWidgetType] = useState('');
+
   useEffect(() => {
     if (editingItem) {
       setValue('widgetName', editingItem.name);
-      setValue('selectedPreset', editingItem.preset.id);
+      setValue('selectedPreset', editingItem?.preset?.id ?? "");
+      setValue('selectedWidgetType', editingItem?.widgetType ?? "");
+      setValue('selectedGenericMetrics', editingItem?.genericMetrics ?? "");
       setThresholds(editingItem.thresholds);
     } else {
       reset({
         widgetName: '',
         selectedPreset: '',
         thresholds: thresholds,
+        selectedWidgetType: '',
       });
     }
   }, [editingItem, setValue, reset]);
@@ -76,23 +94,25 @@ const WidgetModal: React.FC<WidgetModalProps> = ({ isOpen, onClose, onAddWidget,
   };
 
   const onSubmit = (data: WidgetForm) => {
-    const preset = presets.find(p => p.id === data.selectedPreset);
-    if (preset) {
+    const preset = presets.find(p => p.id === data.selectedPreset) ?? null;
+    if (preset || data.selectedGenericMetrics) {
       const formattedThresholds = thresholds.map(t => ({
         ...t,
         value: parseInt(t.value.toString(), 10) || 0
       }));
 
       if (editingItem) {
-        const updatedWidget: WidgetData = {
+        let updatedWidget: WidgetData = {
           ...editingItem,
           name: data.widgetName,
+          widgetType: data.selectedWidgetType || 'preset',//backwards compatibility
           preset,
           thresholds: formattedThresholds,
+          genericMetrics: data.selectedGenericMetrics || ''
         };
         onEditWidget(updatedWidget);
       } else {
-        onAddWidget(preset, formattedThresholds, data.widgetName);
+        onAddWidget(preset, formattedThresholds, data.widgetName, data.selectedWidgetType, data.selectedGenericMetrics);
         // cleanup form
         setThresholds([
           { value: 0, color: '#22c55e' }, // Green
@@ -102,6 +122,8 @@ const WidgetModal: React.FC<WidgetModalProps> = ({ isOpen, onClose, onAddWidget,
           widgetName: '',
           selectedPreset: '',
           thresholds: thresholds,
+          selectedGenericMetrics: '',
+          selectedWidgetType: '',
         });
       }
       onClose();
@@ -128,6 +150,34 @@ const WidgetModal: React.FC<WidgetModalProps> = ({ isOpen, onClose, onAddWidget,
           />
         </div>
         <div className="mb-4 mt-2">
+          <Subtitle>Widget Type</Subtitle>
+          <Controller
+              name="selectedWidgetType"
+              control={control}
+              rules={{ required: { value: true, message: "Preset selection is required" } }}
+              render={({ field }) => {
+                setCurrentWidgetType(field.value);
+                return <Select
+                  {...field}
+                  placeholder="Select a Widget Type"
+                  error={!!get(errors, "selectedWidgetType.message")}
+                  errorMessage={get(errors, "selectedWidgetType.message")}
+                >
+                  {[
+                    { key: 'preset', value: 'Preset' },
+                    { key: 'generic_metrics', value: 'Generic Metrics' },
+                  ].map(({ key, value }) => (
+                    <SelectItem key={key} value={key}>
+                      {value}
+                    </SelectItem>
+                  ))}
+                </Select>
+              }}
+          />                    
+        </div>
+        {currentWidgetType === 'preset' ? (
+          <>
+           <div className="mb-4 mt-2">
           <Subtitle>Preset</Subtitle>
           <Controller
             name="selectedPreset"
@@ -182,6 +232,31 @@ const WidgetModal: React.FC<WidgetModalProps> = ({ isOpen, onClose, onAddWidget,
             ))}
           </div>
         </div>
+          </>
+        ): currentWidgetType === 'generic_metrics' && <>
+          <div className="mb-4 mt-2">
+          <Subtitle>Generic Metrics</Subtitle>
+          <Controller
+            name="selectedGenericMetrics"
+            control={control}
+            rules={{ required: { value: true, message: "Preset selection is required" } }}
+            render={({ field }) => (
+              <Select
+                {...field}
+                placeholder="Select a Generic Metrics"
+                error={!!get(errors, "selectedGenericMetrics.message")}
+                errorMessage={get(errors, "selectedGenericMetrics.message")}
+              >
+                {GENERIC_METRICS.map(metrics => (
+                  <SelectItem key={metrics.key} value={metrics.key}>
+                    {metrics.name}
+                  </SelectItem>
+                ))}
+              </Select>
+            )}
+          />
+          </div>
+        </>}       
         <Button color="orange" type="submit">{editingItem ? "Update Widget" : "Add Widget"}</Button>
       </form>
     </Modal>
diff --git a/keep-ui/app/dashboard/[id]/dashboard.tsx b/keep-ui/app/dashboard/[id]/dashboard.tsx
index f9d4009de1..23c2148032 100644
--- a/keep-ui/app/dashboard/[id]/dashboard.tsx
+++ b/keep-ui/app/dashboard/[id]/dashboard.tsx
@@ -54,16 +54,16 @@ const DashboardPage = () => {
   };
   const closeModal = () => setIsModalOpen(false);
 
-  const handleAddWidget = (preset: Preset, thresholds: Threshold[], name: string) => {
+  const handleAddWidget = (preset: Preset|null, thresholds: Threshold[], name: string, widgetType?: string, genericMetrics?: string) => {
     const uniqueId = `w-${Date.now()}`;
     const newItem: LayoutItem = {
       i: uniqueId,
       x: (layout.length % 12) * 2,
       y: Math.floor(layout.length / 12) * 2,
-      w: 3,
-      h: 3,
-      minW: 2,
-      minH: 2,
+      w: genericMetrics ? 12 : 3,
+      h: genericMetrics ? 20 : 3,
+      minW: genericMetrics ? 10 : 2,
+      minH: genericMetrics ? 15 : 2,
       static: false
     };
     const newWidget: WidgetData = {
@@ -71,6 +71,8 @@ const DashboardPage = () => {
       thresholds,
       preset,
       name,
+      widgetType: widgetType || 'preset',
+      genericMetrics: genericMetrics || '',
     };
     setLayout((prevLayout) => [...prevLayout, newItem]);
     setWidgetData((prevData) => [...prevData, newWidget]);
diff --git a/keep-ui/app/dashboard/types.tsx b/keep-ui/app/dashboard/types.tsx
index f21c53e146..d595d3efe8 100644
--- a/keep-ui/app/dashboard/types.tsx
+++ b/keep-ui/app/dashboard/types.tsx
@@ -12,8 +12,10 @@ export interface LayoutItem {
 
   export interface WidgetData extends LayoutItem {
     thresholds: Threshold[];
-    preset: Preset;
+    preset: Preset | null;
     name: string;
+    widgetType?:string;
+    genericMetrics?: string;
   }
 
   export interface Threshold {
diff --git a/keep-ui/components/filters/GenericFilters.tsx b/keep-ui/components/filters/GenericFilters.tsx
index 7d30e9f6c7..71dd59acd8 100644
--- a/keep-ui/components/filters/GenericFilters.tsx
+++ b/keep-ui/components/filters/GenericFilters.tsx
@@ -1,8 +1,6 @@
 import GenericPopover from "@/components/popover/GenericPopover";
-import { Textarea, Badge, Button, Tab, TabGroup, TabList } from "@tremor/react";
-import moment from "moment";
 import { usePathname, useSearchParams, useRouter } from "next/navigation";
-import { useRef, useState, useEffect, ChangeEvent } from "react";
+import { useRef, useState, useEffect, ChangeEvent, useMemo } from "react";
 import { GoPlusCircle } from "react-icons/go";
 import { DateRangePicker, DateRangePickerValue, Title } from "@tremor/react";
 import { MdOutlineDateRange } from "react-icons/md";
@@ -17,6 +15,10 @@ type Filter = {
   options?: { value: string; label: string }[];
   name: string;
   icon?: IconType;
+  only_one?: boolean;
+  searchParamsNotNeed?: boolean;
+  setFilter?: (value: string | string[] | Record<string, string>) => void;
+  can_select?: number;
 };
 
 interface FiltersProps {
@@ -27,6 +29,9 @@ interface PopoverContentProps {
   filterRef: React.MutableRefObject<Filter[]>;
   filterKey: string;
   type: string;
+  only_one?: boolean;
+  can_select?: number;
+  onApply?: () => void;
 }
 
 function toArray(value: string | string[]) {
@@ -41,49 +46,82 @@ function toArray(value: string | string[]) {
 // TODO: Testing is needed
 function CustomSelect({
   filter,
-  setLocalFilter,
+  only_one,
+  handleSelect,
+  can_select,
 }: {
   filter: Filter | null;
-  setLocalFilter: (value: string | string[]) => void;
+  handleSelect: (value: string | string[]) => void;
+  only_one?: boolean;
+  can_select?: number;
 }) {
   const filterKey = filter?.key || "";
   const [selectedOptions, setSelectedOptions] = useState<Set<string>>(
     new Set<string>()
   );
 
+  const [localFilter, setLocalFilter] = useState<Filter | null>(null);
+
   useEffect(() => {
     if (filter) {
       setSelectedOptions(new Set(toArray(filter.value as string | string[])));
+      setLocalFilter({ ...filter });
     }
-  }, [filter]);
+  }, [filter, filter?.value]);
 
   const handleCheckboxChange = (option: string, checked: boolean) => {
     setSelectedOptions((prev) => {
-      const updatedOptions = new Set(prev);
-      if (checked) {
+      let updatedOptions = new Set(prev);
+      if (only_one) {
+        updatedOptions.clear();
+      }
+      if (
+        checked &&
+        (!can_select || (can_select && updatedOptions.size < can_select))
+      ) {
         updatedOptions.add(option);
       } else {
         updatedOptions.delete(option);
       }
-      if (filter) {
-        setLocalFilter(Array.from(updatedOptions));
-        // setFilter((prev) => ({ ...prev, ...filter }));
-      }
+      let newValues = Array.from(updatedOptions);
+      setLocalFilter((prev) => {
+        if (prev) {
+          return {
+            ...prev,
+            value: newValues,
+          };
+        }
+        return prev;
+      });
+      handleSelect(newValues);
       return updatedOptions;
     });
   };
 
-  if (!filter) {
+  if (!localFilter) {
     return null;
   }
 
+  const name = `${filterKey?.charAt(0)?.toUpperCase() + filterKey?.slice(1)}`;
+
   return (
-    <>
+    <div>
       <span className="text-gray-400 text-sm">
-        Select {filterKey?.charAt(0)?.toUpperCase() + filterKey?.slice(1)}
+        Select {`${can_select ? `${can_select} ${name}` : name}`}
       </span>
+      {can_select && (
+        <span
+          className={`text-xs text-gray-400 ${
+            selectedOptions.size >= can_select
+              ? "text-red-500"
+              : "text-green-600"
+          }`}
+        >
+          ({selectedOptions.size}/{can_select})
+        </span>
+      )}
       <ul className="flex flex-col mt-3 max-h-96 overflow-auto">
-        {filter.options?.map((option) => (
+        {localFilter.options?.map((option) => (
           <li key={option.value}>
             <label className="cursor-pointer p-2 flex items-center">
               <input
@@ -99,7 +137,7 @@ function CustomSelect({
           </li>
         ))}
       </ul>
-    </>
+    </div>
   );
 }
 
@@ -137,7 +175,6 @@ function CustomDate({
     const endDate = end || start;
     const endOfDayDate = endDate ? endOfDay(endDate) : end;
 
-
     setDateRange({ from: start ?? undefined, to: endOfDayDate ?? undefined });
     handleDate(start, endOfDayDate);
   };
@@ -168,39 +205,27 @@ const PopoverContent: React.FC<PopoverContentProps> = ({
   filterRef,
   filterKey,
   type,
+  only_one,
+  can_select,
+  onApply,
 }) => {
   // Initialize local state for selected options
+  const filter =  filterRef.current?.find((f) => f.key === filterKey);
+  if (!filter) {
+    return null;
+  }
 
-  const filter = filterRef.current?.find((filter) => filter.key === filterKey);
-
-  const [localFilter, setLocalFilter] = useState<Filter | null>(null);
-
-  useEffect(() => {
-    if (filter) {
-      setLocalFilter({ ...filter });
-    }
-  }, []);
-
-  useEffect(() => {
-    if (localFilter && filter) {
-      filter.value = localFilter.value;
-    }
-  }, [localFilter?.value]);
-
-  const handleLocalFilter = (value: string | string[]) => {
-    if (filter) {
-      filter.value = value;
+  const handleSelect = (value: string | string[]) => {
+    if (filterRef.current) {
+      const updatedFilters = filterRef.current.map((f) =>
+        f.key === filterKey ? { ...f, value: value } : f
+      );
+      filterRef.current = updatedFilters;
     }
-    setLocalFilter((prev) => {
-      if (prev) {
-        return { ...prev, value };
-      }
-      return null;
-    });
   };
 
   const handleDate = (start?: Date, end?: Date) => {
-    let newValue = ""
+    let newValue = "";
     if (!start && !end) {
       newValue = "";
     } else {
@@ -209,25 +234,28 @@ const PopoverContent: React.FC<PopoverContentProps> = ({
         end: end || start,
       });
     }
-    if (filter) {
-      filter.value = newValue;
+    if (filterRef.current) {
+      const updatedFilters = filterRef.current.map((f) =>
+        f.key === filterKey ? { ...f, value: newValue } : f
+      );
+      filterRef.current = updatedFilters;
+      onApply?.();
     }
-    setLocalFilter((prev) => {
-      if (prev) {
-        return { ...prev, value: newValue };
-      }
-      return null;
-    });
   };
 
   // Return the appropriate content based on the selected type
   switch (type) {
     case "select":
       return (
-        <CustomSelect filter={localFilter} setLocalFilter={handleLocalFilter} />
+        <CustomSelect
+          filter={filter ?? null}
+          handleSelect={handleSelect}
+          only_one={only_one}
+          can_select={can_select}
+        />
       );
     case "date":
-      return <CustomDate filter={localFilter} handleDate={handleDate} />;
+      return <CustomDate filter={filter || null} handleDate={handleDate} />;
     default:
       return null;
   }
@@ -239,7 +267,7 @@ export const GenericFilters: React.FC<FiltersProps> = ({ filters }) => {
   const router = useRouter();
   const pathname = usePathname();
   const searchParams = useSearchParams();
-  const searchParamString = searchParams?.toString();
+  const searchParamString = searchParams?.toString() || "";
   const [apply, setApply] = useState(false);
 
   useEffect(() => {
@@ -250,6 +278,13 @@ export const GenericFilters: React.FC<FiltersProps> = ({ filters }) => {
       const keys = filterRef.current.map((filter) => filter.key);
       keys.forEach((key) => newParams.delete(key));
       for (const { key, value } of filterRef.current) {
+        const filter = filterRef.current.find(
+          (filter) => filter.key === key && filter.searchParamsNotNeed
+        );
+        if (filter) {
+          newParams.delete(key);
+          continue;
+        }
         if (Array.isArray(value)) {
           for (const item of value) {
             newParams.append(key, item);
@@ -262,8 +297,21 @@ export const GenericFilters: React.FC<FiltersProps> = ({ filters }) => {
           }
         }
       }
-
-      router.push(`${pathname}?${newParams.toString()}`);
+      for (const { key, value } of filterRef.current) {
+        const filter = filterRef.current.find(
+          (filter) => filter.key === key && filter.searchParamsNotNeed
+        );
+        if (filter && filter.type == 'select') {
+          let newValue = Array.isArray(value) && value.length == 0 ? "" : toArray(value as string | string[]);
+          if (filter.setFilter) {
+            filter.setFilter(newValue || "");
+          }
+          continue;
+        }
+      }
+      if ((newParams?.toString() || "") !== searchParamString) {
+        router.push(`${pathname}?${newParams.toString()}`);
+      }
       setApply(false); // Reset apply state
     }
   }, [apply]);
@@ -277,7 +325,7 @@ export const GenericFilters: React.FC<FiltersProps> = ({ filters }) => {
           if (Array.isArray(acc[key])) {
             acc[key] = [...acc[key], value];
             return acc;
-          }else {
+          } else {
             acc[key] = [acc[key] as string, value];
           }
           return acc;
@@ -289,7 +337,12 @@ export const GenericFilters: React.FC<FiltersProps> = ({ filters }) => {
       // Update filterRef.current with the new params
       filterRef.current = filters.map((filter) => ({
         ...filter,
-        value: params[filter.key] || "",
+        value: params[filter.key] || filter?.value || "",
+      }));
+    } else {
+      filterRef.current = filters.map((filter) => ({
+        ...filter,
+        value: filter.value || "",
       }));
     }
   }, [searchParamString, filters]);
@@ -312,23 +365,36 @@ export const GenericFilters: React.FC<FiltersProps> = ({ filters }) => {
   return (
     <div className="relative flex flex-col md:flex-row lg:flex-row gap-4 items-center">
       {filters &&
-        filters?.map(({ key, type, name, icon }) => {
+        filters?.map(({ key, type, name, icon, only_one, can_select }) => {
           //only type==select and date need popover i guess other text and textarea can be handled different. for now handling select and date
           icon = icon ?? type === "date" ? MdOutlineDateRange : GoPlusCircle;
           return (
             <div key={key} className="flex gap-4">
-              <GenericPopover
-                triggerText={name}
-                triggerIcon={icon}
-                content={
-                  <PopoverContent
-                    filterRef={filterRef}
-                    filterKey={key}
-                    type={type}
-                  />
-                }
-                onApply={() => setApply(true)}
-              />
+              {type !== "date" ? (
+                <GenericPopover
+                  triggerText={name}
+                  triggerIcon={icon}
+                  content={
+                    <PopoverContent
+                      filterRef={filterRef}
+                      filterKey={key}
+                      type={type}
+                      only_one={!!only_one}
+                      can_select={can_select}
+                    />
+                  }
+                  onApply={() => setApply(true)}
+                />
+              ) : (
+                <PopoverContent
+                  filterRef={filterRef}
+                  filterKey={key}
+                  type={type}
+                  only_one={!!only_one}
+                  can_select={can_select}
+                  onApply={() => setApply(true)}
+                />
+              )}
             </div>
           );
         })}
diff --git a/keep-ui/components/navbar/CustomPresetAlertLinks.tsx b/keep-ui/components/navbar/CustomPresetAlertLinks.tsx
index b81fdb5929..7219782b0f 100644
--- a/keep-ui/components/navbar/CustomPresetAlertLinks.tsx
+++ b/keep-ui/components/navbar/CustomPresetAlertLinks.tsx
@@ -188,7 +188,7 @@ export const CustomPresetAlertLinks = ({
           oldOrder.filter((p) => p.id !== presetId)
         );
 
-        router.push("/alerts/feed");
+        router.push("/alerts/feed"); // Redirect to feed
       }
     }
   };
diff --git a/keep-ui/components/navbar/Search.tsx b/keep-ui/components/navbar/Search.tsx
index 9c8c8d7786..63bfd6ad91 100644
--- a/keep-ui/components/navbar/Search.tsx
+++ b/keep-ui/components/navbar/Search.tsx
@@ -19,7 +19,7 @@ import {
 } from "@heroicons/react/24/outline";
 import { VscDebugDisconnect } from "react-icons/vsc";
 import { LuWorkflow } from "react-icons/lu";
-import { AiOutlineAlert } from "react-icons/ai";
+import { AiOutlineAlert, AiOutlineGroup } from "react-icons/ai";
 import { MdOutlineEngineering, MdOutlineSearchOff } from "react-icons/md";
 import KeepPng from "../../keep.png";
 
@@ -36,6 +36,12 @@ const NAVIGATION_OPTIONS = [
     shortcut: ["g"],
     navigate: "/alerts/feed",
   },
+  {
+    icon: AiOutlineGroup,
+    label: "Go to alert quality",
+    shortcut: ["q"],
+    navigate: "/alerts/quality",
+  },
   {
     icon: MdOutlineEngineering,
     label: "Go to alert groups",
diff --git a/keep-ui/components/table/GenericTable.tsx b/keep-ui/components/table/GenericTable.tsx
index 0c26077fe7..cc14683d21 100644
--- a/keep-ui/components/table/GenericTable.tsx
+++ b/keep-ui/components/table/GenericTable.tsx
@@ -26,6 +26,7 @@ interface GenericTableProps<T> {
     limit: number;
     onPaginationChange: ( limit: number, offset: number ) => void;
     onRowClick?: (row: T) => void;
+    dataFetchedAtOneGO?: boolean
 }
 
 export function GenericTable<T>({
@@ -36,6 +37,7 @@ export function GenericTable<T>({
     limit,
     onPaginationChange,
     onRowClick,
+    dataFetchedAtOneGO,
 }: GenericTableProps<T>) {
     const [expanded, setExpanded] = useState<ExpandedState>({});
     const [pagination, setPagination] = useState({
@@ -60,9 +62,11 @@ export function GenericTable<T>({
         }
     }, [pagination]);
 
+    const finalData = (dataFetchedAtOneGO ? data.slice(pagination.pageSize * pagination.pageIndex, pagination.pageSize * (pagination.pageIndex + 1)) : data) as T[]
+
     const table = useReactTable({
         columns,
-        data,
+        data: finalData,
         state: { expanded, pagination },
         getCoreRowModel: getCoreRowModel(),
         manualPagination: true,
@@ -76,7 +80,7 @@ export function GenericTable<T>({
 
     return (
         <div className="flex flex-col w-full h-full max-h-full">
-            <div className="overflow-auto h-1/2">
+            <div className="overflow-auto h-[85%]">
                 <TremorTable className="w-full rounded border border-tremor-border dark:border-dark-tremor-border">
                     <TableHead>
                         {table.getHeaderGroups().map((headerGroup) => (
@@ -115,7 +119,7 @@ export function GenericTable<T>({
                     </TableBody>
                 </TremorTable>
             </div>
-            <div className="mt-4">
+            <div className="mt-4 h-[15%]">
                 {pagination&&<Pagination
                     table={table}
                     isRefreshAllowed={false}
diff --git a/keep-ui/utils/hooks/useAlertQuality.ts b/keep-ui/utils/hooks/useAlertQuality.ts
new file mode 100644
index 0000000000..1a928fd09f
--- /dev/null
+++ b/keep-ui/utils/hooks/useAlertQuality.ts
@@ -0,0 +1,40 @@
+import { useSession } from "next-auth/react";
+import { getApiURL } from "../apiUrl";
+import { SWRConfiguration } from "swr";
+import { fetcher } from "../fetcher";
+import useSWRImmutable from "swr/immutable";
+import { useSearchParams } from "next/navigation";
+import { useMemo } from "react";
+
+export const useAlertQualityMetrics = (
+  fields: string | string[],
+  options: SWRConfiguration = {}
+) => {
+  const { data: session } = useSession();
+  const apiUrl = getApiURL();
+  const searchParams = useSearchParams();
+  ``;
+  let filters = useMemo(() => {
+    let params = searchParams?.toString();
+    if (fields) {
+      fields = Array.isArray(fields) ? fields : [fields];
+      let fieldParams = new URLSearchParams("");
+      fields.forEach((field) => {
+        fieldParams.append("fields", field);
+      });
+      params = params
+        ? `${params}&${fieldParams.toString()}`
+        : fieldParams.toString();
+    }
+    return params;
+  }, [fields?.toString(), searchParams?.toString()]);
+  // TODO: Proper type needs to be defined.
+  return useSWRImmutable<Record<string, Record<string, any>>>(
+    () =>
+      session
+        ? `${apiUrl}/alerts/quality/metrics${filters ? `?${filters}` : ""}`
+        : null,
+    (url) => fetcher(url, session?.accessToken),
+    options
+  );
+};
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 75ffb774a9..95cabebbfb 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -19,7 +19,7 @@
 import validators
 from dotenv import find_dotenv, load_dotenv
 from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
-from sqlalchemy import and_, case, desc, literal, null, union, update
+from sqlalchemy import and_, case, desc, literal, null, union, update, func, case
 from sqlalchemy.dialects.mysql import insert as mysql_insert
 from sqlalchemy.dialects.postgresql import insert as pg_insert
 from sqlalchemy.dialects.sqlite import insert as sqlite_insert
@@ -3529,7 +3529,6 @@ def get_workflow_executions_for_incident_or_alert(
         results = session.execute(final_query).all()
         return results, total_count
 
-
 def is_all_incident_alerts_resolved(incident: Incident, session: Optional[Session] = None) -> bool:
 
     if incident.alerts_count == 0:
@@ -3615,4 +3614,60 @@ def is_edge_incident_alert_resolved(incident: Incident, direction: Callable, ses
         return (
             enriched_status == AlertStatus.RESOLVED.value or
             (enriched_status is None and status == AlertStatus.RESOLVED.value)
-        )
\ No newline at end of file
+        )
+def get_alerts_metrics_by_provider(
+    tenant_id: str,
+    start_date: Optional[datetime] = None, 
+    end_date: Optional[datetime] = None,
+    fields: Optional[List[str]] = []
+) -> Dict[str, Dict[str, Any]]:
+    
+    dynamic_field_sums = [
+        func.sum(
+            case(
+                [
+                    (
+                        func.json_extract(Alert.event, f'$.{field}').isnot(None) & 
+                        (func.json_extract(Alert.event, f'$.{field}') != False), 
+                        1
+                    )
+                ], 
+                else_=0
+            )
+        ).label(f"{field}_count")
+        for field in fields
+    ]
+
+    with Session(engine) as session:
+        query = (
+            session.query(
+                Alert.provider_type,
+                Alert.provider_id,
+                func.count(Alert.id).label("total_alerts"),
+                func.sum(case([(AlertToIncident.alert_id.isnot(None), 1)], else_=0)).label("correlated_alerts"),
+                *dynamic_field_sums
+            )
+            .outerjoin(AlertToIncident, Alert.id == AlertToIncident.alert_id)
+            .filter(
+                Alert.tenant_id == tenant_id,
+            )
+        )
+
+        # Add timestamp filter only if both start_date and end_date are provided
+        if start_date and end_date:
+            query = query.filter(
+                Alert.timestamp >= start_date,
+                Alert.timestamp <= end_date
+            )
+
+        results = query.group_by(Alert.provider_id, Alert.provider_type).all()
+        
+    return {
+        f"{row.provider_id}_{row.provider_type}": {
+            "total_alerts": row.total_alerts,
+            "correlated_alerts": row.correlated_alerts,
+            "provider_type": row.provider_type,
+            **{f"{field}_count": getattr(row, f"{field}_count") for field in fields}  # Add field-specific counts
+        }
+        for row in results
+    }
diff --git a/keep/api/routes/alerts.py b/keep/api/routes/alerts.py
index ec02e80180..c14c092f99 100644
--- a/keep/api/routes/alerts.py
+++ b/keep/api/routes/alerts.py
@@ -4,7 +4,7 @@
 import json
 import logging
 import os
-from typing import Optional
+from typing import Optional, List
 
 import celpy
 from arq import ArqRedis
@@ -25,7 +25,7 @@
 from keep.api.consts import KEEP_ARQ_QUEUE_BASIC
 from keep.api.core.config import config
 from keep.api.core.db import get_alert_audit as get_alert_audit_db
-from keep.api.core.db import get_alerts_by_fingerprint, get_enrichment, get_last_alerts
+from keep.api.core.db import get_alerts_by_fingerprint, get_enrichment, get_last_alerts, get_alerts_metrics_by_provider
 from keep.api.core.dependencies import extract_generic_body, get_pusher_client
 from keep.api.core.elastic import ElasticClient
 from keep.api.models.alert import (
@@ -44,6 +44,8 @@
 from keep.identitymanager.identitymanagerfactory import IdentityManagerFactory
 from keep.providers.providers_factory import ProvidersFactory
 from keep.searchengine.searchengine import SearchEngine
+from keep.api.utils.time_stamp_helpers import get_time_stamp_filter
+from keep.api.models.time_stamp import TimeStampFilter
 
 router = APIRouter()
 logger = logging.getLogger(__name__)
@@ -756,3 +758,29 @@ def get_alert_audit(
 
     grouped_events = AlertAuditDto.from_orm_list(alert_audit)
     return grouped_events
+
+
+@router.get("/quality/metrics", description="Get alert quality")
+def get_alert_quality(
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["read:alert"])
+    ),
+    time_stamp: TimeStampFilter = Depends(get_time_stamp_filter),
+    fields: Optional[List[str]] = Query([]),
+):
+    logger.info(
+        "Fetching alert quality metrics per provider",
+        extra={
+            "tenant_id": authenticated_entity.tenant_id,
+            "fields": fields
+        },
+        
+    )
+    start_date = time_stamp.lower_timestamp if time_stamp else None
+    end_date = time_stamp.upper_timestamp if time_stamp else None
+    db_alerts_quality = get_alerts_metrics_by_provider(
+        tenant_id=authenticated_entity.tenant_id, start_date=start_date, end_date=end_date,
+        fields=fields
+    )
+    
+    return db_alerts_quality
diff --git a/keep/api/utils/time_stamp_helpers.py b/keep/api/utils/time_stamp_helpers.py
new file mode 100644
index 0000000000..8e39324e89
--- /dev/null
+++ b/keep/api/utils/time_stamp_helpers.py
@@ -0,0 +1,20 @@
+from keep.api.models.time_stamp import TimeStampFilter
+from fastapi import (
+    HTTPException,
+    Query
+)
+from typing import Optional
+import json
+
+def get_time_stamp_filter(
+    time_stamp: Optional[str] = Query(None)
+) -> TimeStampFilter:
+    if time_stamp:
+        try:
+            # Parse the JSON string
+            time_stamp_dict = json.loads(time_stamp)
+            # Return the TimeStampFilter object, Pydantic will map 'from' -> lower_timestamp and 'to' -> upper_timestamp
+            return TimeStampFilter(**time_stamp_dict)
+        except (json.JSONDecodeError, TypeError):
+            raise HTTPException(status_code=400, detail="Invalid time_stamp format")
+    return TimeStampFilter()
\ No newline at end of file


Code review comments:
diff_hunk: @@ -0,0 +1,12 @@
+import QualityAlert from './alert-quality-table'; // Adjust the import path if needed
+
+const AlertQualityPage = () => {
path: keep-ui/app/alerts/quality/page.tsx
text: @user1:
I guess we don't need this page anymore, right?

@user2:
yes

@user2:
resolved