Skip to content

Commit

Permalink
feat(alerts): Relative alerts (#25423)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
anirudhpillai and github-actions[bot] authored Oct 17, 2024
1 parent ef24aad commit 41bbc38
Show file tree
Hide file tree
Showing 28 changed files with 1,839 additions and 531 deletions.
37 changes: 34 additions & 3 deletions cypress/e2e/alerts.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@ describe('Alerts', () => {
const createAlert = (
name: string = 'Alert name',
lowerThreshold: string = '100',
upperThreshold: string = '200'
upperThreshold: string = '200',
condition?: string
): void => {
cy.get('[data-attr=more-button]').click()
cy.contains('Manage alerts').click()
cy.contains('New alert').click()

cy.get('[data-attr=alertForm-name]').clear().type(name)
cy.get('[data-attr=subscribed-users').click().type('{downarrow}{enter}')

if (condition) {
cy.get('[data-attr=alertForm-condition').click()
cy.contains(condition).click()
cy.contains('%').click()
}

cy.get('[data-attr=alertForm-lower-threshold').clear().type(lowerThreshold)
cy.get('[data-attr=alertForm-upper-threshold').clear().type(upperThreshold)
cy.contains('Create alert').click()
Expand All @@ -39,7 +47,6 @@ describe('Alerts', () => {
cy.get('[data-attr=insight-edit-button]').click()
cy.get('[data-attr=chart-filter]').click()
cy.contains(displayType).click()
cy.get('.insight-empty-state').should('not.exist')
cy.get('[data-attr=insight-save-button]').contains('Save').click()
cy.url().should('not.include', '/edit')
}
Expand Down Expand Up @@ -69,7 +76,7 @@ describe('Alerts', () => {
})

it('Should warn about an alert deletion', () => {
setInsightDisplayTypeAndSave('Number')
setInsightDisplayTypeAndSave('Area chart')

createAlert('Alert to be deleted because of a changed insight')

Expand All @@ -90,4 +97,28 @@ describe('Alerts', () => {
cy.contains('Manage alerts').click()
cy.contains('Alert to be deleted because of a changed insight').should('not.exist')
})

it('Should allow create and delete a relative alert', () => {
cy.get('[data-attr=more-button]').click()
// Alerts should be disabled for trends represented with graphs
cy.get('[data-attr=manage-alerts-button]').should('have.attr', 'aria-disabled', 'true')

setInsightDisplayTypeAndSave('Bar chart')

createAlert('Alert name', '10', '20', 'increases by')
cy.reload()

// Check the alert has the same values as when it was created
cy.get('[data-attr=more-button]').click()
cy.contains('Manage alerts').click()
cy.get('[data-attr=alert-list-item]').contains('Alert name').click()
cy.get('[data-attr=alertForm-name]').should('have.value', 'Alert name')
cy.get('[data-attr=alertForm-lower-threshold').should('have.value', '10')
cy.get('[data-attr=alertForm-upper-threshold').should('have.value', '20')
cy.contains('Delete alert').click()
cy.wait(2000)

cy.reload()
cy.contains('Alert name').should('not.exist')
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions frontend/src/lib/components/Alerts/SnoozeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { dayjs } from 'lib/dayjs'
import { formatDate } from 'lib/utils'

import { DateFilter } from '../DateFilter/DateFilter'

const DATETIME_FORMAT = 'MMM D - HH:mm'

interface SnoozeButtonProps {
onChange: (snoonzeUntil: string) => void
value?: string
}

export function SnoozeButton({ onChange, value }: SnoozeButtonProps): JSX.Element {
return (
<DateFilter
dateFrom={value ?? null}
onChange={(snoozeUntil) => {
snoozeUntil && onChange(snoozeUntil)
}}
placeholder="Snooze until"
max={31}
isFixedDateMode
showRollingRangePicker={false}
allowedRollingDateOptions={['days', 'weeks', 'months', 'years']}
showCustom
dateOptions={[
{
key: 'Tomorrow',
values: ['+1d'],
getFormattedDate: (date: dayjs.Dayjs): string => formatDate(date.add(1, 'd'), DATETIME_FORMAT),
defaultInterval: 'day',
},
{
key: 'One week from now',
values: ['+1w'],
getFormattedDate: (date: dayjs.Dayjs): string => formatDate(date.add(1, 'w'), DATETIME_FORMAT),
defaultInterval: 'day',
},
]}
size="medium"
/>
)
}
36 changes: 29 additions & 7 deletions frontend/src/lib/components/Alerts/alertFormLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { forms } from 'kea-forms'
import api from 'lib/api'
import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'

import { AlertCalculationInterval } from '~/queries/schema'
import { AlertCalculationInterval, AlertConditionType, InsightThresholdType } from '~/queries/schema'
import { QueryBasedInsightModel } from '~/types'

import type { alertFormLogicType } from './alertFormLogicType'
import { AlertType, AlertTypeWrite } from './types'

export type AlertFormType = Pick<
AlertType,
'name' | 'enabled' | 'created_at' | 'threshold' | 'subscribed_users' | 'checks' | 'config'
'name' | 'enabled' | 'created_at' | 'threshold' | 'condition' | 'subscribed_users' | 'checks' | 'config'
> & {
id?: AlertType['id']
created_by?: AlertType['created_by'] | null
Expand All @@ -31,6 +31,8 @@ export const alertFormLogic = kea<alertFormLogicType>([

actions({
deleteAlert: true,
snoozeAlert: (snoozeUntil: string) => ({ snoozeUntil }),
clearSnooze: true,
}),

forms(({ props }) => ({
Expand All @@ -47,10 +49,9 @@ export const alertFormLogic = kea<alertFormLogicType>([
type: 'TrendsAlertConfig',
series_index: 0,
},
threshold: {
configuration: {
absoluteThreshold: {},
},
threshold: { configuration: { type: InsightThresholdType.ABSOLUTE, bounds: {} } },
condition: {
type: AlertConditionType.ABSOLUTE_VALUE,
},
subscribed_users: [],
checks: [],
Expand All @@ -61,12 +62,17 @@ export const alertFormLogic = kea<alertFormLogicType>([
name: !name ? 'You need to give your alert a name' : undefined,
}),
submit: async (alert) => {
const payload: Partial<AlertTypeWrite> = {
const payload: AlertTypeWrite = {
...alert,
subscribed_users: alert.subscribed_users?.map(({ id }) => id),
insight: props.insightId,
}

// absolute value alert can only have absolute threshold
if (payload.condition.type === AlertConditionType.ABSOLUTE_VALUE) {
payload.threshold.configuration.type = InsightThresholdType.ABSOLUTE
}

try {
if (alert.id === undefined) {
const updatedAlert: AlertType = await api.alerts.create(payload)
Expand Down Expand Up @@ -101,5 +107,21 @@ export const alertFormLogic = kea<alertFormLogicType>([
await api.alerts.delete(values.alertForm.id)
props.onEditSuccess()
},
snoozeAlert: async ({ snoozeUntil }) => {
// resolution only allowed on created alert (which will have alertId)
if (!values.alertForm.id) {
throw new Error("Cannot resolve alert that doesn't exist")
}
await api.alerts.update(values.alertForm.id, { snoozed_until: snoozeUntil })
props.onEditSuccess()
},
clearSnooze: async () => {
// resolution only allowed on created alert (which will have alertId)
if (!values.alertForm.id) {
throw new Error("Cannot resolve alert that doesn't exist")
}
await api.alerts.update(values.alertForm.id, { snoozed_until: null })
props.onEditSuccess()
},
})),
])
20 changes: 13 additions & 7 deletions frontend/src/lib/components/Alerts/insightAlertsLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { loaders } from 'kea-loaders'
import api from 'lib/api'
import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic'

import { GoalLine } from '~/queries/schema'
import { GoalLine, InsightThresholdType } from '~/queries/schema'
import { getBreakdown, isInsightVizNode, isTrendsQuery } from '~/queries/utils'
import { InsightLogicProps } from '~/types'

Expand Down Expand Up @@ -65,21 +65,27 @@ export const insightAlertsLogic = kea<insightAlertsLogicType>([
(s) => [s.alerts],
(alerts: AlertType[]): GoalLine[] =>
alerts.flatMap((alert) => {
const thresholds = []
if (
alert.threshold.configuration.type !== InsightThresholdType.ABSOLUTE ||
!alert.threshold.configuration.bounds
) {
return []
}

const absoluteThreshold = alert.threshold.configuration.absoluteThreshold
const bounds = alert.threshold.configuration.bounds

if (absoluteThreshold?.upper !== undefined) {
const thresholds = []
if (bounds?.upper !== undefined) {
thresholds.push({
label: `${alert.name} Upper Threshold`,
value: absoluteThreshold?.upper,
value: bounds?.upper,
})
}

if (absoluteThreshold?.lower !== undefined) {
if (bounds?.lower !== undefined) {
thresholds.push({
label: `${alert.name} Lower Threshold`,
value: absoluteThreshold?.lower,
value: bounds?.lower,
})
}

Expand Down
5 changes: 4 additions & 1 deletion frontend/src/lib/components/Alerts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type AlertConfig = TrendsAlertConfig
export interface AlertTypeBase {
name: string
condition: AlertCondition
threshold: { configuration: InsightThreshold }
enabled: boolean
insight: QueryBasedInsightModel
config: AlertConfig
Expand All @@ -20,6 +21,7 @@ export interface AlertTypeBase {
export interface AlertTypeWrite extends Omit<AlertTypeBase, 'insight'> {
subscribed_users: number[]
insight: number
snoozed_until?: string | null
}

export interface AlertCheck {
Expand All @@ -33,12 +35,13 @@ export interface AlertCheck {
export interface AlertType extends AlertTypeBase {
id: string
subscribed_users: UserBasicType[]
threshold: { configuration: InsightThreshold }
condition: AlertCondition
created_by: UserBasicType
created_at: string
state: AlertState
last_notified_at: string
last_checked_at: string
checks: AlertCheck[]
calculation_interval: AlertCalculationInterval
snoozed_until?: string
}
Loading

0 comments on commit 41bbc38

Please sign in to comment.