Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/content/docs/analytics-dashboard/alerts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 4 additions & 3 deletions docs/content/docs/sitesettings/get-traffic-alerts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<img alt="Alerts screen" src="/docs/img/sitesettings/set_up_alert.png" />

Expand All @@ -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.
27 changes: 0 additions & 27 deletions web/app/api/api.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Alerts } from '~/lib/models/Alerts'
import { Auth } from '~/lib/models/Auth'
import { User } from '~/lib/models/User'
import {
Expand Down Expand Up @@ -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<ServerFetchResult<AlertsResponse>> {
const params = new URLSearchParams({
take: String(take),
skip: String(skip),
})

return serverFetch<AlertsResponse>(
request,
`alert/project/${projectId}?${params.toString()}`,
)
}

// ============================================================================
// MARK: Funnels API (Deferred Loading)
// ============================================================================
Expand Down
1 change: 0 additions & 1 deletion web/app/lib/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,6 @@ const PRODUCTION_PROJECT_TABS = {
goals: 'goals',
experiments: 'experiments',
featureFlags: 'featureFlags',
alerts: 'alerts',
captcha: 'captcha',
ai: 'ai',
} as const
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,17 +389,15 @@ const ProjectAlertsSettings = ({
showSearchButton={false}
showRefreshButton={false}
showPeriodSelector={false}
leftContent={
<Text
as='h2'
size='xl'
weight='bold'
className='wrap-break-word break-all'
>
{title}
</Text>
}
/>
<Text
as='h2'
size='xl'
weight='bold'
className='wrap-break-word break-all'
>
{title}
</Text>
<fetcher.Form
method='POST'
action={`/projects/${projectId}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,25 @@ import {
FileTextIcon,
BellSlashIcon,
} from '@phosphor-icons/react'
import { useMemo, useState, useEffect, useRef, Suspense, use } from 'react'
import { useMemo, useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import {
Link,
useSearchParams,
useFetcher,
useLoaderData,
useRevalidator,
} from 'react-router'
import { Link, useSearchParams, useFetcher } from 'react-router'
import { toast } from 'sonner'

import type { AlertsResponse } from '~/api/api.server'
import { QUERY_METRIC, PLAN_LIMITS, DEFAULT_ALERTS_TAKE } from '~/lib/constants'
import { Alerts } from '~/lib/models/Alerts'
import PaidFeature from '~/modals/PaidFeature'
import { useAuth } from '~/providers/AuthProvider'
import { useCurrentProject } from '~/providers/CurrentProjectProvider'
import type {
ProjectLoaderData,
ProjectViewActionData,
} from '~/routes/projects.$id'
import type { ProjectViewActionData } from '~/routes/projects.$id'
import { Badge, type BadgeProps } from '~/ui/Badge'
import Button from '~/ui/Button'
import LoadingBar from '~/ui/LoadingBar'
import Modal from '~/ui/Modal'
import Pagination from '~/ui/Pagination'
import StatusPage from '~/ui/StatusPage'
import { Text } from '~/ui/Text'
import routes from '~/utils/routes'

import ProjectAlertsSettings from './ProjectAlertsSettings'
import { LoaderView } from '../../View/components/LoaderView'

const NoNotificationChannelSet = () => {
const { t } = useTranslation('common')
Expand Down Expand Up @@ -255,64 +242,37 @@ const AlertRow = ({
)
}

interface DeferredAlertsData {
alertsData: AlertsResponse | null
}

function AlertsDataResolver({
children,
}: {
children: (data: DeferredAlertsData) => React.ReactNode
}) {
const { alertsData: alertsDataPromise } = useLoaderData<ProjectLoaderData>()
const alertsData = alertsDataPromise ? use(alertsDataPromise) : null
return <>{children({ alertsData })}</>
}

function ProjectAlertsWrapper() {
return (
<Suspense fallback={<LoaderView />}>
<AlertsDataResolver>
{(deferredData) => <ProjectAlertsInner deferredData={deferredData} />}
</AlertsDataResolver>
</Suspense>
)
}

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<ProjectViewActionData>()
const lastHandledData = useRef<ProjectViewActionData | null>(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<Alerts[]>(
() => deferredData.alertsData?.results || [],
)
const [total, setTotal] = useState(0)
const [alerts, setAlerts] = useState<Alerts[] | null>(null)
const [page, setPage] = useState(1)

const [error, setError] = useState<string | null>(null)

const isLoading = revalidator.state === 'loading' || fetcher.state !== 'idle'
const isLoading = fetcher.state !== 'idle'

Comment thread
Blaumaus marked this conversation as resolved.
Resolved
const pageAmount = Math.ceil(total / DEFAULT_ALERTS_TAKE)

const limits = PLAN_LIMITS[user?.planCode || 'trial']
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')
Expand All @@ -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)
Expand All @@ -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)
}
Expand All @@ -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(() => {
Expand All @@ -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<string, string> = useMemo(() => {
const values = _values(QUERY_METRIC)
Expand Down Expand Up @@ -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) => {
Expand All @@ -474,12 +410,7 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => {

if (!canManageAlerts) {
return (
<div
className={cx('flex flex-col bg-gray-50 dark:bg-slate-950', {
'min-h-including-header': !isEmbedded,
'min-h-screen': isEmbedded,
})}
>
<div className='flex flex-col'>
<div className='mt-5 rounded-lg bg-slate-700 p-5 dark:bg-slate-900'>
<div className='flex items-center text-gray-50'>
<BellRingingIcon className='mr-2 h-8 w-8' />
Expand Down Expand Up @@ -534,15 +465,9 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => {

return (
<>
{isLoading && !_isEmpty(alerts) ? <LoadingBar /> : null}
<div className='mt-4'>
{_isEmpty(alerts) ? (
<div
className={cx('flex flex-col bg-gray-50 dark:bg-slate-950', {
'min-h-including-header': !isEmbedded,
'min-h-screen': isEmbedded,
})}
>
{alerts === null ? null : alerts.length === 0 ? (
<div className='flex flex-col'>
<div className='mt-5 rounded-lg bg-slate-700 p-5 dark:bg-slate-900'>
<div className='flex items-center text-gray-50'>
<BellRingingIcon className='mr-2 h-8 w-8' />
Expand Down Expand Up @@ -609,6 +534,6 @@ const ProjectAlertsInner = ({ deferredData }: ProjectAlertsInnerProps) => {
)
}

const ProjectAlerts = ProjectAlertsWrapper
const ProjectAlerts = ProjectAlertsInner

export default ProjectAlerts
Loading
Loading