Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

515 lines
20 KiB
TypeScript

import { MakeLogicType, actions, afterMount, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import insights from 'insights-js'
import api from 'lib/api'
import { integrationsLogic } from 'lib/integrations/integrationsLogic'
import { toast } from 'lib/elements/Toast/Toast'
import { deleteWithUndo } from 'lib/utils/deleteWithUndo'
import { projectLogic } from 'scenes/projectLogic'
import { InsightsFunctionType, IntegrationType } from '~/types'
import { alertsTestDeliveryCreate } from 'products/alerts/frontend/generated/api'
import type { AlertTestDeliveryResponseApi } from 'products/alerts/frontend/generated/api.schemas'
import {
ALERT_NOTIFICATION_TYPE_DISCORD,
ALERT_NOTIFICATION_TYPE_MICROSOFT_TEAMS,
ALERT_NOTIFICATION_TYPE_SLACK,
ALERT_NOTIFICATION_TYPE_WEBHOOK,
AlertNotificationType,
PendingAlertNotification,
buildAlertFilterConfig,
buildInsightsFunctionPayload,
notificationTypeFromTemplateId,
} from 'products/alerts/frontend/logic/alertNotifications'
import type { UserBasicType } from '../../../../frontend/src/types'
export const ALERT_NOTIFICATION_TYPE_OPTIONS = [
{ label: 'Slack', value: ALERT_NOTIFICATION_TYPE_SLACK },
{ label: 'Discord', value: ALERT_NOTIFICATION_TYPE_DISCORD },
{ label: 'Microsoft Teams', value: ALERT_NOTIFICATION_TYPE_MICROSOFT_TEAMS },
{ label: 'Webhook', value: ALERT_NOTIFICATION_TYPE_WEBHOOK },
]
export interface AlertNotificationLogicProps {
alertId?: string
loadDestinations?: boolean
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface alertNotificationLogicValues {
integrationsLoading: boolean // integrationsLogic
slackIntegrations: IntegrationType[] | undefined // integrationsLogic
currentProjectId: number | null // projectLogic
existingInsightsFunctions: InsightsFunctionType[]
existingInsightsFunctionsLoading: boolean
integrationsFailed: boolean
pendingNotifications: PendingAlertNotification[]
selectedSlackIntegration: IntegrationType | undefined
selectedSlackIntegrationId: number | null
selectedType: AlertNotificationType
slackChannelValue: string | null
testDeliveryResult: AlertTestDeliveryResponseApi | null
testDeliveryResultLoading: boolean
webhookUrl: string
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface alertNotificationLogicActions {
loadIntegrations: () => any // integrationsLogic
loadIntegrationsFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
} // integrationsLogic
loadIntegrationsSuccess: (
integrations: {
config: any
created_at: string
created_by?: UserBasicType | null | undefined
display_name: string
errors?: string | undefined
icon_url: any
id: number
kind:
| 'apns'
| 'aws-s3'
| 'azure-blob'
| 'bing-ads'
| 'clickup'
| 'customerio-app'
| 'customerio-track'
| 'customerio-webhook'
| 'databricks'
| 'email'
| 'firebase'
| 'github'
| 'gitlab'
| 'google-ads'
| 'google-analytics'
| 'google-cloud-service-account'
| 'google-cloud-storage'
| 'google-pubsub'
| 'google-search-console'
| 'google-sheets'
| 'hubspot'
| 'intercom'
| 'jira'
| 'linear'
| 'linkedin-ads'
| 'meta-ads'
| 'pardot'
| 'pinterest-ads'
| 'postgresql'
| 'reddit-ads'
| 's3-compatible'
| 'salesforce'
| 'slack'
| 'snapchat'
| 'snowflake'
| 'stripe'
| 'tiktok-ads'
| 'twilio'
| 'vercel'
}[],
payload?: any
) => {
integrations: {
config: any
created_at: string
created_by?: UserBasicType | null | undefined
display_name: string
errors?: string | undefined
icon_url: any
id: number
kind:
| 'apns'
| 'aws-s3'
| 'azure-blob'
| 'bing-ads'
| 'clickup'
| 'customerio-app'
| 'customerio-track'
| 'customerio-webhook'
| 'databricks'
| 'email'
| 'firebase'
| 'github'
| 'gitlab'
| 'google-ads'
| 'google-analytics'
| 'google-cloud-service-account'
| 'google-cloud-storage'
| 'google-pubsub'
| 'google-search-console'
| 'google-sheets'
| 'hubspot'
| 'intercom'
| 'jira'
| 'linear'
| 'linkedin-ads'
| 'meta-ads'
| 'pardot'
| 'pinterest-ads'
| 'postgresql'
| 'reddit-ads'
| 's3-compatible'
| 'salesforce'
| 'slack'
| 'snapchat'
| 'snowflake'
| 'stripe'
| 'tiktok-ads'
| 'twilio'
| 'vercel'
}[]
payload?: any
} // integrationsLogic
addPendingNotification: (notification: PendingAlertNotification) => {
notification: PendingAlertNotification
}
clearPendingNotifications: () => {
value: true
}
createPendingInsightsFunctions: (
alertId: string,
alertName?: string
) => {
alertId: string
alertName: string | undefined
}
deleteExistingInsightsFunction: (insightsFunction: InsightsFunctionType) => {
insightsFunction: InsightsFunctionType
}
loadExistingInsightsFunctions: () => any
loadExistingInsightsFunctionsFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadExistingInsightsFunctionsSuccess: (
existingInsightsFunctions: InsightsFunctionType[],
payload?: any
) => {
existingInsightsFunctions: InsightsFunctionType[]
payload?: any
}
removePendingNotification: (index: number) => {
index: number
}
sendTestDelivery: () => any
sendTestDeliveryFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
sendTestDeliverySuccess: (
testDeliveryResult: AlertTestDeliveryResponseApi,
payload?: any
) => {
testDeliveryResult: AlertTestDeliveryResponseApi
payload?: any
}
setPendingNotifications: (notifications: PendingAlertNotification[]) => {
notifications: PendingAlertNotification[]
}
setSelectedSlackIntegrationId: (selectedSlackIntegrationId: number | null) => {
selectedSlackIntegrationId: number | null
}
setSelectedType: (selectedType: AlertNotificationType) => {
selectedType: AlertNotificationType
}
setSlackChannelValue: (slackChannelValue: string | null) => {
slackChannelValue: string | null
}
setWebhookUrl: (webhookUrl: string) => {
webhookUrl: string
}
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface alertNotificationLogicMeta {
key: string
__keaTypeGenInternalSelectorTypes: {
selectedSlackIntegration: (
slackIntegrations: IntegrationType[] | undefined,
selectedSlackIntegrationId: number | null
) => IntegrationType | undefined
}
}
export type alertNotificationLogicType = MakeLogicType<
alertNotificationLogicValues,
alertNotificationLogicActions,
AlertNotificationLogicProps,
alertNotificationLogicMeta
>
export const alertNotificationLogic = kea<alertNotificationLogicType>([
path(['lib', 'components', 'Alerts', 'alertNotificationLogic']),
props({} as AlertNotificationLogicProps),
key(({ alertId, loadDestinations = true }) => `${alertId ?? 'new'}-${loadDestinations ? 'full' : 'test-only'}`),
connect({
values: [projectLogic, ['currentProjectId'], integrationsLogic, ['integrationsLoading', 'slackIntegrations']],
actions: [integrationsLogic, ['loadIntegrations', 'loadIntegrationsFailure', 'loadIntegrationsSuccess']],
}),
actions({
addPendingNotification: (notification: PendingAlertNotification) => ({ notification }),
removePendingNotification: (index: number) => ({ index }),
clearPendingNotifications: true,
setPendingNotifications: (notifications: PendingAlertNotification[]) => ({ notifications }),
deleteExistingInsightsFunction: (insightsFunction: InsightsFunctionType) => ({ insightsFunction }),
createPendingInsightsFunctions: (alertId: string, alertName?: string) => ({ alertId, alertName }),
setSelectedType: (selectedType: AlertNotificationType) => ({ selectedType }),
setSelectedSlackIntegrationId: (selectedSlackIntegrationId: number | null) => ({
selectedSlackIntegrationId,
}),
setSlackChannelValue: (slackChannelValue: string | null) => ({ slackChannelValue }),
setWebhookUrl: (webhookUrl: string) => ({ webhookUrl }),
}),
reducers({
pendingNotifications: [
[] as PendingAlertNotification[],
{
addPendingNotification: (state, { notification }) => [...state, notification],
removePendingNotification: (state, { index }) => state.filter((_, i) => i !== index),
clearPendingNotifications: () => [],
setPendingNotifications: (_, { notifications }) => notifications,
},
],
integrationsFailed: [
false,
{
loadIntegrations: () => false,
loadIntegrationsFailure: () => true,
loadIntegrationsSuccess: () => false,
},
],
slackChannelValue: [
null as string | null,
{
setSlackChannelValue: (_, { slackChannelValue }) => slackChannelValue,
// Reset the input when switching destination type so stale values don't carry over.
setSelectedType: () => null,
setSelectedSlackIntegrationId: () => null,
},
],
selectedSlackIntegrationId: [
null as number | null,
{
setSelectedSlackIntegrationId: (_, { selectedSlackIntegrationId }) => selectedSlackIntegrationId,
},
],
webhookUrl: [
'' as string,
{
setWebhookUrl: (_, { webhookUrl }) => webhookUrl,
setSelectedType: () => '',
},
],
selectedType: [
ALERT_NOTIFICATION_TYPE_SLACK as AlertNotificationType,
{
setSelectedType: (_, { selectedType }) => selectedType,
},
],
existingInsightsFunctions: [
[] as InsightsFunctionType[],
{
// Optimistic removal so the item disappears immediately
deleteExistingInsightsFunction: (state, { insightsFunction }) => state.filter((hf) => hf.id !== insightsFunction.id),
},
],
}),
selectors({
selectedSlackIntegration: [
(s) => [s.slackIntegrations, s.selectedSlackIntegrationId],
(
slackIntegrations: IntegrationType[] | undefined,
selectedSlackIntegrationId: number | null
): IntegrationType | undefined =>
slackIntegrations?.find((integration) => integration.id === selectedSlackIntegrationId) ??
slackIntegrations?.[0],
],
}),
loaders(({ props, values }) => ({
existingInsightsFunctions: [
[] as InsightsFunctionType[],
{
loadExistingInsightsFunctions: async () => {
if (!props.alertId) {
return []
}
const response = await api.insightsFunctions.list({
types: ['internal_destination'],
filter_groups: [buildAlertFilterConfig(props.alertId)],
full: true,
})
return response.results
},
},
],
testDeliveryResult: [
null as AlertTestDeliveryResponseApi | null,
{
sendTestDelivery: async () => {
if (!props.alertId || values.currentProjectId === null) {
throw new Error('Save the alert before sending a test.')
}
return alertsTestDeliveryCreate(String(values.currentProjectId), props.alertId)
},
},
],
})),
listeners(({ actions, values, props }) => ({
loadIntegrationsSuccess: ({ integrations }) => {
const availableSlackIntegrations = integrations.filter((integration) => integration.kind === 'slack')
const selectedIntegrationIsAvailable = availableSlackIntegrations.some(
(integration) => integration.id === values.selectedSlackIntegrationId
)
if (!selectedIntegrationIsAvailable) {
actions.setSelectedSlackIntegrationId(availableSlackIntegrations[0]?.id ?? null)
}
if (availableSlackIntegrations.length === 0) {
actions.setSelectedType(ALERT_NOTIFICATION_TYPE_WEBHOOK)
}
// Drop any staged Slack notifications pointing at a workspace that's no longer
// connected — saving them would create a destination referencing a dead integration.
const availableIds = new Set(availableSlackIntegrations.map((integration) => integration.id))
const stillValidPending = values.pendingNotifications.filter(
(notification) =>
notification.type !== ALERT_NOTIFICATION_TYPE_SLACK ||
availableIds.has(notification.slackWorkspaceId)
)
if (stillValidPending.length !== values.pendingNotifications.length) {
actions.setPendingNotifications(stillValidPending)
}
},
deleteExistingInsightsFunction: async ({ insightsFunction }) => {
// Resolve the type from the closure up front — after the delete the InsightsFunction is
// removed from state, so we can't derive it later.
const type = notificationTypeFromTemplateId(insightsFunction.template_id)
await deleteWithUndo({
endpoint: `projects/${values.currentProjectId}/insights_functions`,
object: {
id: insightsFunction.id,
name: insightsFunction.name,
},
callback: (undo) => {
if (undo) {
actions.loadExistingInsightsFunctions()
return
}
// Skip on undo and on unrecognized templates so this only counts confirmed deletes with a known type.
if (type) {
insights.capture('insight alert destination deleted', {
alert_id: props.alertId,
type,
})
}
},
})
},
createPendingInsightsFunctions: async ({ alertId, alertName }) => {
const pending = values.pendingNotifications
if (pending.length === 0) {
return
}
const results = await Promise.allSettled(
pending.map((notification) => {
const payload = buildInsightsFunctionPayload(alertId, alertName, notification)
return api.insightsFunctions.create(payload)
})
)
// No `alert created`/`alert updated` event captures destination type, so this is emitted per created destination instead.
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
insights.capture('insight alert destination created', {
alert_id: alertId,
type: pending[i].type,
})
}
})
const failures = results
.map((result, i) => ({ result, notification: pending[i] }))
.filter(
(item): item is { result: PromiseRejectedResult; notification: PendingAlertNotification } =>
item.result.status === 'rejected'
)
if (failures.length > 0) {
const labelForType = (type: AlertNotificationType): string =>
ALERT_NOTIFICATION_TYPE_OPTIONS.find((option) => option.value === type)?.label ?? type
failures.forEach(({ result, notification }) => {
console.error(
`Failed to create ${labelForType(notification.type)} alert notification`,
result.reason
)
})
const failedTypes = Array.from(
new Set(failures.map(({ notification }) => labelForType(notification.type)))
)
toast.error(
`Alert saved, but failed to create: ${failedTypes.join(', ')}. Reopen the alert to add them again.`
)
actions.setPendingNotifications(failures.map(({ notification }) => notification))
} else {
if (results.length > 0) {
toast.success(`${results.length} notification destination(s) created.`)
}
actions.clearPendingNotifications()
}
actions.loadExistingInsightsFunctions()
},
sendTestDeliverySuccess: ({ testDeliveryResult }) => {
const destinationCount = testDeliveryResult.destination_count
const emailCount = testDeliveryResult.email_recipient_count
const deliveryTargets: string[] = []
if (emailCount > 0) {
deliveryTargets.push(`${emailCount} email ${emailCount === 1 ? 'recipient' : 'recipients'}`)
}
if (destinationCount > 0) {
deliveryTargets.push(`${destinationCount} ${destinationCount === 1 ? 'destination' : 'destinations'}`)
}
if (testDeliveryResult.failed_delivery_channels.length > 0) {
toast.warning(
`Test delivery started for ${deliveryTargets.join(' and ')}, but failed for ${testDeliveryResult.failed_delivery_channels.join(' and ')}.`
)
return
}
toast.success(`Test delivery started for ${deliveryTargets.join(' and ')}.`)
},
sendTestDeliveryFailure: ({ errorObject }) => {
const detail = errorObject?.detail
toast.error(typeof detail === 'string' ? detail : 'Unable to send the test. Try again.')
},
})),
afterMount(({ actions, props }) => {
if (props.alertId && props.loadDestinations !== false) {
actions.loadExistingInsightsFunctions()
}
}),
])