diff --git a/docs/content/docs/analytics-dashboard/alerts.mdx b/docs/content/docs/analytics-dashboard/alerts.mdx index f14c89d2c..35bcb058b 100644 --- a/docs/content/docs/analytics-dashboard/alerts.mdx +++ b/docs/content/docs/analytics-dashboard/alerts.mdx @@ -31,7 +31,7 @@ To configure these, go to your **Account Settings** and follow the instructions ## Creating an Alert -1. Navigate to your project's dashboard and click on the **Alerts** tab. +1. Navigate to your project's **Settings** and click on the **Alerts** tab. 2. Click the **Add Alert** button. 3. Configure the alert settings: - **Name**: A descriptive name for your alert (e.g., "High Traffic Warning"). @@ -54,7 +54,7 @@ You can create alerts based on the following metrics: ## Managing Alerts -Once created, your alerts will appear in a list on the Alerts tab. +Once created, your alerts will appear in a list on the Alerts tab in your project settings. - **Edit**: Click the **Pencil** icon to modify the alert's threshold or settings. - **Delete**: Click the **Trash** icon to remove the alert permanently. diff --git a/docs/content/docs/sitesettings/embed-your-analytics-dashboard-into-your-website.mdx b/docs/content/docs/sitesettings/embed-your-analytics-dashboard-into-your-website.mdx index 6ff1dd00a..8207ed49b 100644 --- a/docs/content/docs/sitesettings/embed-your-analytics-dashboard-into-your-website.mdx +++ b/docs/content/docs/sitesettings/embed-your-analytics-dashboard-into-your-website.mdx @@ -42,7 +42,7 @@ Here is a list of all available options: - `theme`: `light` or `dark` - the theme of the dashboard. - `lng`: language to display the dashboard in. Supported languages: `en`, `uk`, `pl`, `de`, `fr`. Defaults to user's browser language. -- `tab`: `traffic`, `performance`, `profiles`, `funnels`, `sessions`, `errors`, `goals`, `experiments`, `featureFlags`, `alerts`, `captcha`, `ai` - the default tab to show. By default, the traffic tab is shown. -- `tabs`: `traffic`, `performance`, `profiles`, `funnels`, `sessions`, `errors`, `goals`, `experiments`, `featureFlags`, `alerts`, `captcha`, `ai` - a comma-separated list of tabs that should be accessible in dashboard. Example: `tabs=traffic,performance,sessions` will only show the traffic, performance and sessions tabs. By default, all tabs are shown. +- `tab`: `traffic`, `performance`, `profiles`, `funnels`, `sessions`, `errors`, `goals`, `experiments`, `featureFlags`, `captcha`, `ai` - the default tab to show. By default, the traffic tab is shown. +- `tabs`: `traffic`, `performance`, `profiles`, `funnels`, `sessions`, `errors`, `goals`, `experiments`, `featureFlags`, `captcha`, `ai` - a comma-separated list of tabs that should be accessible in dashboard. Example: `tabs=traffic,performance,sessions` will only show the traffic, performance and sessions tabs. By default, all tabs are shown. - `embedded` - if set to `true`, header, footer and any marketing CTAs will be hidden. - `password` - if your dashboard is password-protected, you can pass the password as a query parameter. Example: `password=yourpassword`, this way the dashboard will be unlocked without the need to enter the password. diff --git a/docs/content/docs/sitesettings/get-traffic-alerts.mdx b/docs/content/docs/sitesettings/get-traffic-alerts.mdx index 855d5551c..3f75ebc00 100644 --- a/docs/content/docs/sitesettings/get-traffic-alerts.mdx +++ b/docs/content/docs/sitesettings/get-traffic-alerts.mdx @@ -22,8 +22,9 @@ After you connected your integration, all the alerts will be sent there. You can To set up an alert, 1. Go to your website's analytics page. -2. Navigate to the "Alerts" tab. -3. Click on the "Add alert" button. +2. Open the project **Settings** (gear icon in the sidebar). +3. Navigate to the **Alerts** tab. +4. Click on the "Add alert" button. Alerts screen @@ -36,4 +37,4 @@ When you click on the "Add alert" button, you will see a screen where you can cr - when the number of users on your website is less than 5 during the last 24 hours - ... and more -From the same "Alerts" tab you can also see the list of all the alerts you have set up and the last time they triggered. You can edit or delete them by clicking on the "Edit" or "Delete" buttons. +From the same "Alerts" tab in project settings you can also see the list of all the alerts you have set up and the last time they triggered. You can edit or delete them by clicking on the "Edit" or "Delete" buttons. diff --git a/web/app/api/api.server.ts b/web/app/api/api.server.ts index 0b3452878..ed0fb78bf 100644 --- a/web/app/api/api.server.ts +++ b/web/app/api/api.server.ts @@ -1,4 +1,3 @@ -import { Alerts } from '~/lib/models/Alerts' import { Auth } from '~/lib/models/Auth' import { User } from '~/lib/models/User' import { @@ -1419,32 +1418,6 @@ export async function getGoalChartServer( ) } -// ============================================================================ -// MARK: Alerts API (Deferred Loading) -// ============================================================================ - -export interface AlertsResponse { - results: Alerts[] - total: number -} - -export async function getProjectAlertsServer( - request: Request, - projectId: string, - take = 25, - skip = 0, -): Promise> { - const params = new URLSearchParams({ - take: String(take), - skip: String(skip), - }) - - return serverFetch( - request, - `alert/project/${projectId}?${params.toString()}`, - ) -} - // ============================================================================ // MARK: Funnels API (Deferred Loading) // ============================================================================ diff --git a/web/app/lib/constants/index.ts b/web/app/lib/constants/index.ts index 233e4c746..a5a761ad1 100644 --- a/web/app/lib/constants/index.ts +++ b/web/app/lib/constants/index.ts @@ -506,7 +506,6 @@ const PRODUCTION_PROJECT_TABS = { goals: 'goals', experiments: 'experiments', featureFlags: 'featureFlags', - alerts: 'alerts', captcha: 'captcha', ai: 'ai', } as const diff --git a/web/app/pages/Project/tabs/Alerts/ProjectAlertsSettings.tsx b/web/app/pages/Project/Settings/Alerts/ProjectAlertsSettings.tsx similarity index 98% rename from web/app/pages/Project/tabs/Alerts/ProjectAlertsSettings.tsx rename to web/app/pages/Project/Settings/Alerts/ProjectAlertsSettings.tsx index 23118ccaf..b71a9c5b0 100644 --- a/web/app/pages/Project/tabs/Alerts/ProjectAlertsSettings.tsx +++ b/web/app/pages/Project/Settings/Alerts/ProjectAlertsSettings.tsx @@ -389,17 +389,15 @@ const ProjectAlertsSettings = ({ showSearchButton={false} showRefreshButton={false} showPeriodSelector={false} - leftContent={ - - {title} - - } /> + + {title} + { const { t } = useTranslation('common') @@ -255,56 +242,29 @@ const AlertRow = ({ ) } -interface DeferredAlertsData { - alertsData: AlertsResponse | null -} - -function AlertsDataResolver({ - children, -}: { - children: (data: DeferredAlertsData) => React.ReactNode -}) { - const { alertsData: alertsDataPromise } = useLoaderData() - const alertsData = alertsDataPromise ? use(alertsDataPromise) : null - return <>{children({ alertsData })} -} - -function ProjectAlertsWrapper() { - return ( - }> - - {(deferredData) => } - - - ) -} - -interface ProjectAlertsInnerProps { - deferredData: DeferredAlertsData +interface ProjectAlertsProps { + projectId: string + projectRole?: string } -const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { - const { id, project } = useCurrentProject() - const revalidator = useRevalidator() +const ProjectAlertsInner = ({ projectId, projectRole }: ProjectAlertsProps) => { + const id = projectId const { t } = useTranslation() const { user, isAuthenticated } = useAuth() const [isPaidFeatureOpened, setIsPaidFeatureOpened] = useState(false) const [searchParams, setSearchParams] = useSearchParams() - const isEmbedded = searchParams.get('embedded') === 'true' const fetcher = useFetcher() const lastHandledData = useRef(null) // Track if we're in pagination mode const [isSearchMode, setIsSearchMode] = useState(false) - const [total, setTotal] = useState(() => deferredData.alertsData?.total || 0) - const [alerts, setAlerts] = useState( - () => deferredData.alertsData?.results || [], - ) + const [total, setTotal] = useState(0) + const [alerts, setAlerts] = useState(null) const [page, setPage] = useState(1) const [error, setError] = useState(null) - const isLoading = revalidator.state === 'loading' || fetcher.state !== 'idle' + const isLoading = fetcher.state !== 'idle' const pageAmount = Math.ceil(total / DEFAULT_ALERTS_TAKE) @@ -312,7 +272,7 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { const isLimitReached = isAuthenticated && total >= limits?.maxAlerts // Check if user has permission to view alerts - const canManageAlerts = project?.role === 'owner' && isAuthenticated + const canManageAlerts = projectRole === 'owner' && isAuthenticated // Get active alert from URL params const activeAlertId = searchParams.get('alertId') @@ -326,18 +286,6 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { return newSearchParams.toString() }, [searchParams]) - // Sync state when loader provides new data - useEffect(() => { - if ( - deferredData.alertsData && - revalidator.state === 'idle' && - !isSearchMode - ) { - setAlerts(deferredData.alertsData.results || []) - setTotal(deferredData.alertsData.total || 0) - } - }, [revalidator.state, deferredData.alertsData, isSearchMode]) - const loadAlerts = (take: number, skip: number) => { if (fetcher.state !== 'idle') return setIsSearchMode(true) @@ -363,9 +311,11 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { setTotal(result.total) } else if (intent === 'delete-alert') { toast.success(t('alertsSettings.alertDeleted')) - if (page === 1) { - setIsSearchMode(false) - revalidator.revalidate() + + if (page > 1 && alerts?.length === 1) { + const newPage = page - 1 + setPage(newPage) + loadAlerts(DEFAULT_ALERTS_TAKE, (newPage - 1) * DEFAULT_ALERTS_TAKE) } else { loadAlerts(DEFAULT_ALERTS_TAKE, (page - 1) * DEFAULT_ALERTS_TAKE) } @@ -374,7 +324,7 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { setError(fetcherError) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fetcher.state, fetcher.data, t, page, revalidator]) + }, [fetcher.state, fetcher.data, t, page]) // Handle page changes - use fetcher for pagination useEffect(() => { @@ -385,23 +335,15 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, canManageAlerts]) - // Fallback load for client-side tab switches where deferred data is not present yet. useEffect(() => { if (!canManageAlerts) return - if (deferredData.alertsData) return if (isSearchMode) return if (fetcher.state !== 'idle') return - if (!_isEmpty(alerts)) return + if (alerts !== null) return loadAlerts(DEFAULT_ALERTS_TAKE, 0) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - canManageAlerts, - deferredData.alertsData, - isSearchMode, - fetcher.state, - alerts, - ]) + }, [canManageAlerts, isSearchMode, fetcher.state, alerts]) const queryMetricTMapping: Record = useMemo(() => { const values = _values(QUERY_METRIC) @@ -454,13 +396,7 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { const handleAlertSaved = () => { closeAlertSettings() - // Reload alerts after saving - if (page === 1) { - setIsSearchMode(false) - revalidator.revalidate() - } else { - loadAlerts(DEFAULT_ALERTS_TAKE, (page - 1) * DEFAULT_ALERTS_TAKE) - } + loadAlerts(DEFAULT_ALERTS_TAKE, (page - 1) * DEFAULT_ALERTS_TAKE) } const onDelete = (alertId: string) => { @@ -474,12 +410,7 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { if (!canManageAlerts) { return ( -
+
@@ -534,15 +465,9 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { return ( <> - {isLoading && !_isEmpty(alerts) ? : null}
- {_isEmpty(alerts) ? ( -
+ {alerts === null ? null : alerts.length === 0 ? ( +
@@ -609,6 +534,6 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => { ) } -const ProjectAlerts = ProjectAlertsWrapper +const ProjectAlerts = ProjectAlertsInner export default ProjectAlerts diff --git a/web/app/pages/Project/Settings/ProjectSettings.tsx b/web/app/pages/Project/Settings/ProjectSettings.tsx index 6977b7585..a70015d6e 100644 --- a/web/app/pages/Project/Settings/ProjectSettings.tsx +++ b/web/app/pages/Project/Settings/ProjectSettings.tsx @@ -22,6 +22,7 @@ import { ShieldCheckIcon, CurrencyDollarIcon, DownloadIcon, + BellRingingIcon, } from '@phosphor-icons/react' import React, { useState, useEffect, useMemo, useRef } from 'react' import { Trans, useTranslation } from 'react-i18next' @@ -65,6 +66,7 @@ import DangerZone from './tabs/DangerZone' import General from './tabs/General' import Revenue from './tabs/Revenue' import Shields from './tabs/Shields' +import ProjectAlerts from './Alerts/ProjectAlertsView' import DataImportTab from './components/DataImportTab' const MAX_NAME_LENGTH = 50 @@ -353,6 +355,7 @@ const ProjectSettings = () => { | 'access' | 'captcha' | 'integrations' + | 'alerts' | 'revenue' | 'emails' | 'people' @@ -403,6 +406,14 @@ const ProjectSettings = () => { iconColor: 'text-purple-500', visible: !isSelfhosted, }, + { + id: 'alerts', + label: t('project.settings.tabs.alerts'), + description: t('project.settings.tabs.alertsDesc'), + icon: BellRingingIcon, + iconColor: 'text-cyan-500', + visible: !isSelfhosted && project?.role === 'owner', + }, { id: 'revenue', label: t('project.settings.tabs.revenue'), @@ -1225,6 +1236,18 @@ const ProjectSettings = () => {
) : null} + {activeTab === 'alerts' && activeTabConfig ? ( + <> + + + + ) : null} + {activeTab === 'captcha' && activeTabConfig ? (
{ label: t('dashboard.askAi'), icon: SparkleIcon, }, - { - id: PROJECT_TABS.alerts, - label: t('dashboard.alerts'), - icon: BellRingingIcon, - }, { id: PROJECT_TABS.experiments, label: t('dashboard.experiments'), @@ -1126,10 +1119,6 @@ const ViewProjectContent = () => { } if (pair.period === 'compare') { - if (activeTab === PROJECT_TABS.alerts) { - return - } - if (isActiveCompare) { compareDisable() } else { @@ -1535,9 +1524,6 @@ const ViewProjectContent = () => { {activeTab === PROJECT_TABS.funnels ? ( ) : null} - {activeTab === PROJECT_TABS.alerts ? ( - - ) : null} {activeTab === PROJECT_TABS.profiles ? ( ) : null} diff --git a/web/app/pages/Project/View/components/ProjectSidebar.tsx b/web/app/pages/Project/View/components/ProjectSidebar.tsx index 3277420b6..2163bcee9 100644 --- a/web/app/pages/Project/View/components/ProjectSidebar.tsx +++ b/web/app/pages/Project/View/components/ProjectSidebar.tsx @@ -64,7 +64,6 @@ const ICON_COLORS: Record = { traffic: 'text-blue-500', performance: 'text-amber-500', funnels: 'text-teal-500', - alerts: 'text-cyan-500', // Product Analytics profiles: 'text-fuchsia-500', sessions: 'text-indigo-500', @@ -399,12 +398,7 @@ const ProjectSidebar: React.FC = ({ const groups: TabGroup[] = [] const webAnalyticsTabs = tabs.filter((tab) => - [ - PROJECT_TABS.traffic, - PROJECT_TABS.performance, - PROJECT_TABS.funnels, - PROJECT_TABS.alerts, - ] + [PROJECT_TABS.traffic, PROJECT_TABS.performance, PROJECT_TABS.funnels] .filter(Boolean) .includes(tab.id as any), ) diff --git a/web/app/routes/projects.$id.tsx b/web/app/routes/projects.$id.tsx index e724719ea..d861e90f9 100644 --- a/web/app/routes/projects.$id.tsx +++ b/web/app/routes/projects.$id.tsx @@ -25,7 +25,6 @@ import { getErrorOverviewServer, getProjectFeatureFlagsServer, getProjectGoalsServer, - getProjectAlertsServer, getFunnelDataServer, type TrafficLogResponse, type OverallObject, @@ -38,7 +37,6 @@ import { type ErrorOverviewResponse, type FeatureFlagsResponse, type GoalsResponse, - type AlertsResponse, type FunnelDataResponse, type AnalyticsFilter, } from '~/api/api.server' @@ -175,7 +173,6 @@ export const meta: MetaFunction = ({ data, location }) => { [PROJECT_TABS.featureFlags]: t('dashboard.featureFlags'), [PROJECT_TABS.captcha]: t('common.captcha'), [PROJECT_TABS.ai]: t('dashboard.askAi'), - [PROJECT_TABS.alerts]: t('dashboard.alerts'), [PROJECT_TABS.experiments]: t('dashboard.experiments'), settings: t('common.settings'), } @@ -310,8 +307,6 @@ export interface ProjectLoaderData { featureFlagsData?: Promise // Goals data goalsData?: Promise - // Alerts data - alertsData?: Promise // Funnels data funnelData?: Promise // Data import @@ -431,7 +426,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { let errorOverview: Promise | undefined let featureFlagsData: Promise | undefined let goalsData: Promise | undefined - let alertsData: Promise | undefined let funnelData: Promise | undefined let hasImportedData: Promise | undefined @@ -535,10 +529,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { goalsData = getProjectGoalsServer(request, projectId).then( (res) => res.data, ) - } else if (tab === PROJECT_TABS.alerts) { - alertsData = getProjectAlertsServer(request, projectId).then( - (res) => res.data, - ) } else if (tab === PROJECT_TABS.funnels) { // Fetch funnel data if funnelId is in URL const funnelId = url.searchParams.get('funnelId') @@ -591,7 +581,6 @@ export async function loader({ request, params }: LoaderFunctionArgs) { errorOverview, featureFlagsData, goalsData, - alertsData, funnelData, hasImportedData, }, diff --git a/web/public/locales/en.json b/web/public/locales/en.json index da9bbb826..a6b4abcbd 100644 --- a/web/public/locales/en.json +++ b/web/public/locales/en.json @@ -1734,6 +1734,8 @@ "dangerDesc": "Destructive actions that cannot be undone.", "integrations": "Integrations", "integrationsDesc": "Connect external services to enrich your analytics.", + "alerts": "Alerts", + "alertsDesc": "Get notified about traffic spikes, errors, and custom events.", "revenue": "Revenue", "revenueDesc": "Track revenue and attribute sales to campaigns.", "import": "Import data",