From 80847e1e6f8c7a39579d819e13d485662ec695f4 Mon Sep 17 00:00:00 2001 From: Ben White Date: Fri, 21 Jun 2024 11:54:18 +0200 Subject: [PATCH] feat(cdp): Native slack integration (#23103) --- ...s-slack--slack-integration-added--dark.png | Bin 20732 -> 24216 bytes ...-slack--slack-integration-added--light.png | Bin 20608 -> 24091 bytes .../Subscriptions/subscriptionLogic.ts | 10 +- .../lib/components/Subscriptions/utils.tsx | 30 +--- .../Subscriptions/views/EditSubscription.tsx | 75 ++------ .../integrations/SlackIntegrationHelpers.tsx | 130 ++++++++++++++ .../integrations}/integrationsLogic.ts | 42 +---- .../lib/integrations/slackIntegrationLogic.ts | 48 +++++ .../LemonInputSelect/LemonInputSelect.tsx | 3 + .../IntegrationsRedirect.tsx | 2 +- .../hogfunctions/HogFunctionInputs.tsx | 21 ++- .../HogFunctionInputIntegration.tsx | 64 +++++++ .../HogFunctionInputIntegrationField.tsx | 62 +++++++ .../hogfunctions/integrations/types.ts | 10 ++ .../settings/project/SlackIntegration.tsx | 154 ++++++++-------- frontend/src/types.ts | 5 +- plugin-server/src/cdp/cdp-consumers.ts | 12 +- plugin-server/src/cdp/hog-executor.ts | 2 +- plugin-server/src/cdp/hog-function-manager.ts | 165 ++++++++++++------ plugin-server/src/cdp/types.ts | 33 +++- plugin-server/tests/cdp/fixtures.ts | 30 +++- .../tests/cdp/hog-function-manager.test.ts | 88 +++++++++- posthog/cdp/templates/slack/template_slack.py | 102 ++++++----- .../templates/slack/test_template_slack.py | 48 +++-- posthog/cdp/validation.py | 10 +- 25 files changed, 800 insertions(+), 346 deletions(-) create mode 100644 frontend/src/lib/integrations/SlackIntegrationHelpers.tsx rename frontend/src/{scenes/settings/project => lib/integrations}/integrationsLogic.ts (80%) create mode 100644 frontend/src/lib/integrations/slackIntegrationLogic.ts create mode 100644 frontend/src/scenes/pipeline/hogfunctions/integrations/HogFunctionInputIntegration.tsx create mode 100644 frontend/src/scenes/pipeline/hogfunctions/integrations/HogFunctionInputIntegrationField.tsx create mode 100644 frontend/src/scenes/pipeline/hogfunctions/integrations/types.ts diff --git a/frontend/__snapshots__/components-integrations-slack--slack-integration-added--dark.png b/frontend/__snapshots__/components-integrations-slack--slack-integration-added--dark.png index dd30ef4e40c81a8707ad28cdcfb833823d114a09..b0e1bd7e3e1d0372756982437968e52ab6ee537f 100644 GIT binary patch literal 24216 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 E0GzRpk^lez literal 20608 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 Udj};FTqy`~bvl80h(43~AL4SXApigX diff --git a/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts b/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts index ab5108074621bf..a3b328435be0f0 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionLogic.ts @@ -7,7 +7,6 @@ import { dayjs } from 'lib/dayjs' import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' import { isEmail, isURL } from 'lib/utils' import { getInsightId } from 'scenes/insights/utils' -import { integrationsLogic } from 'scenes/settings/project/integrationsLogic' import { SubscriptionType } from '~/types' @@ -33,7 +32,6 @@ export const subscriptionLogic = kea([ key(({ id, insightShortId, dashboardId }) => `${insightShortId || dashboardId}-${id ?? 'new'}`), connect(({ insightShortId, dashboardId }: SubscriptionsLogicProps) => ({ actions: [subscriptionsLogic({ insightShortId, dashboardId }), ['loadSubscriptions']], - values: [integrationsLogic, ['isMemberOfSlackChannel']], })), loaders(({ props }) => ({ @@ -48,7 +46,7 @@ export const subscriptionLogic = kea([ }, })), - forms(({ props, actions, values }) => ({ + forms(({ props, actions }) => ({ subscription: { defaults: {} as unknown as SubscriptionType, errors: ({ frequency, interval, target_value, target_type, title, start_date }) => ({ @@ -76,12 +74,6 @@ export const subscriptionLogic = kea([ ? 'Must be a valid URL' : undefined : undefined, - memberOfSlackChannel: - target_type == 'slack' - ? target_value && !values.isMemberOfSlackChannel(target_value) - ? 'Please add the PostHog Slack App to the selected channel' - : undefined - : undefined, }), submit: async (subscription, breakpoint) => { const insightId = props.insightShortId ? await getInsightId(props.insightShortId) : undefined diff --git a/frontend/src/lib/components/Subscriptions/utils.tsx b/frontend/src/lib/components/Subscriptions/utils.tsx index 370c6855d6d1f0..f76ce50535693f 100644 --- a/frontend/src/lib/components/Subscriptions/utils.tsx +++ b/frontend/src/lib/components/Subscriptions/utils.tsx @@ -1,11 +1,10 @@ import { IconLetter } from '@posthog/icons' import { LemonSelectOptions } from '@posthog/lemon-ui' -import { IconSlack, IconSlackExternal } from 'lib/lemon-ui/icons' -import { LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect' +import { IconSlack } from 'lib/lemon-ui/icons' import { range } from 'lib/utils' import { urls } from 'scenes/urls' -import { InsightShortId, SlackChannelType } from '~/types' +import { InsightShortId } from '~/types' export interface SubscriptionBaseProps { dashboardId?: number @@ -80,28 +79,3 @@ export const timeOptions: LemonSelectOptions = range(0, 24).map((x) => ( value: String(x), label: `${String(x).padStart(2, '0')}:00`, })) - -export const getSlackChannelOptions = ( - value: string, - slackChannels?: SlackChannelType[] | null -): LemonInputSelectOption[] => { - return slackChannels - ? slackChannels.map((x) => ({ - key: `${x.id}|#${x.name}`, - labelComponent: ( - - {x.is_private ? `🔒${x.name}` : `#${x.name}`} - {x.is_ext_shared ? : null} - - ), - label: `${x.id} #${x.name}`, - })) - : value - ? [ - { - key: value, - label: value?.split('|')?.pop() || value, - }, - ] - : [] -} diff --git a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx index c47d0435703842..3a8077ad6c4ee7 100644 --- a/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx +++ b/frontend/src/lib/components/Subscriptions/views/EditSubscription.tsx @@ -4,19 +4,19 @@ import { Form } from 'kea-forms' import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' import { usersLemonSelectOptions } from 'lib/components/UserSelectItem' import { dayjs } from 'lib/dayjs' +import { integrationsLogic } from 'lib/integrations/integrationsLogic' +import { SlackChannelPicker } from 'lib/integrations/SlackIntegrationHelpers' import { IconChevronLeft } from 'lib/lemon-ui/icons' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonField } from 'lib/lemon-ui/LemonField' -import { LemonInputSelect, LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect' +import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect' import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel' import { LemonModal } from 'lib/lemon-ui/LemonModal' import { LemonSelect } from 'lib/lemon-ui/LemonSelect' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { useEffect, useMemo } from 'react' import { membersLogic } from 'scenes/organization/membersLogic' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { integrationsLogic } from 'scenes/settings/project/integrationsLogic' import { urls } from 'scenes/urls' import { subscriptionLogic } from '../subscriptionLogic' @@ -25,7 +25,6 @@ import { bysetposOptions, frequencyOptionsPlural, frequencyOptionsSingular, - getSlackChannelOptions, intervalOptions, monthlyWeekdayOptions, SubscriptionBaseProps, @@ -59,15 +58,14 @@ export function EditSubscription({ }) const { meFirstMembers, membersLoading } = useValues(membersLogic) - const { subscription, subscriptionLoading, isSubscriptionSubmitting, subscriptionChanged, isMemberOfSlackChannel } = - useValues(logic) + const { subscription, subscriptionLoading, isSubscriptionSubmitting, subscriptionChanged } = useValues(logic) const { preflight, siteUrlMisconfigured } = useValues(preflightLogic) const { deleteSubscription } = useActions(subscriptionslogic) - const { slackChannels, slackChannelsLoading, slackIntegration, addToSlackButtonUrl } = useValues(integrationsLogic) - const { loadSlackChannels } = useActions(integrationsLogic) + const { slackIntegrations, addToSlackButtonUrl } = useValues(integrationsLogic) + // TODO: Fix this so that we use the appropriate config... + const firstSlackIntegration = slackIntegrations?.[0] const emailDisabled = !preflight?.email_service_available - const slackDisabled = !slackIntegration const _onDelete = (): void => { if (id !== 'new') { @@ -76,23 +74,6 @@ export function EditSubscription({ } } - useEffect(() => { - if (subscription?.target_type === 'slack' && slackIntegration) { - loadSlackChannels() - } - }, [subscription?.target_type, slackIntegration]) - - // If slackChannels aren't loaded, make sure we display only the channel name and not the actual underlying value - const slackChannelOptions: LemonInputSelectOption[] = useMemo( - () => getSlackChannelOptions(subscription?.target_value, slackChannels), - [slackChannels, subscription?.target_value] - ) - - const showSlackMembershipWarning = - subscription.target_value && - subscription.target_type === 'slack' && - !isMemberOfSlackChannel(subscription.target_value) - const formatter = new Intl.DateTimeFormat('en-US', { timeZoneName: 'shortGeneric' }) const parts = formatter.formatToParts(new Date()) const currentTimezone = parts?.find((part) => part.type === 'timeZoneName')?.value @@ -222,7 +203,7 @@ export function EditSubscription({ {subscription.target_type === 'slack' ? ( <> - {slackDisabled ? ( + {!firstSlackIntegration ? ( <> {addToSlackButtonUrl() ? ( @@ -278,45 +259,13 @@ export function EditSubscription({ } > {({ value, onChange }) => ( - onChange(val[0] ?? null)} - value={value ? [value] : []} - disabled={slackDisabled} - mode="single" - data-attr="select-slack-channel" - placeholder="Select a channel..." - options={slackChannelOptions} - loading={slackChannelsLoading} + )} - - {showSlackMembershipWarning ? ( - - -
- - The PostHog Slack App is not in this channel. Please add it - to the channel otherwise Subscriptions will fail to be - delivered.{' '} - - See the Docs for more information - - - - Check again - -
-
-
- ) : null} )} diff --git a/frontend/src/lib/integrations/SlackIntegrationHelpers.tsx b/frontend/src/lib/integrations/SlackIntegrationHelpers.tsx new file mode 100644 index 00000000000000..cd435a811a9ce9 --- /dev/null +++ b/frontend/src/lib/integrations/SlackIntegrationHelpers.tsx @@ -0,0 +1,130 @@ +import { LemonBanner, LemonButton, LemonInputSelect, LemonInputSelectOption, Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' +import { IconSlack, IconSlackExternal } from 'lib/lemon-ui/icons' +import { useMemo } from 'react' + +import { IntegrationType, SlackChannelType } from '~/types' + +import { slackIntegrationLogic } from './slackIntegrationLogic' + +const getSlackChannelOptions = (slackChannels?: SlackChannelType[] | null): LemonInputSelectOption[] | null => { + return slackChannels + ? slackChannels.map((x) => ({ + key: `${x.id}|#${x.name}`, + labelComponent: ( + + {x.is_private ? `🔒${x.name}` : `#${x.name}`} + {x.is_ext_shared ? : null} + + ), + label: `${x.id} #${x.name}`, + })) + : null +} + +export type SlackChannelPickerProps = { + integration: IntegrationType + value?: string + onChange?: (value: string | null) => void + disabled?: boolean +} + +export function SlackChannelPicker({ onChange, value, integration, disabled }: SlackChannelPickerProps): JSX.Element { + const { slackChannels, slackChannelsLoading, isMemberOfSlackChannel } = useValues( + slackIntegrationLogic({ id: integration.id }) + ) + const { loadSlackChannels } = useActions(slackIntegrationLogic({ id: integration.id })) + + // If slackChannels aren't loaded, make sure we display only the channel name and not the actual underlying value + const slackChannelOptions = useMemo(() => getSlackChannelOptions(slackChannels), [slackChannels]) + const showSlackMembershipWarning = value && isMemberOfSlackChannel(value) === false + + // Sometimes the parent will only store the channel ID and not the name, so we need to handle that + + const modifiedValue = useMemo(() => { + if (value?.split('|').length === 1) { + const channel = slackChannels?.find((x) => x.id === value) + + if (channel) { + return `${channel.id}|#${channel.name}` + } + } + + return value + }, [value, slackChannels]) + + return ( + <> + onChange?.(val[0] ?? null)} + value={modifiedValue ? [modifiedValue] : []} + onFocus={() => !slackChannels && !slackChannelsLoading && loadSlackChannels()} + disabled={disabled} + mode="single" + data-attr="select-slack-channel" + placeholder="Select a channel..." + options={ + slackChannelOptions ?? + (modifiedValue + ? [ + { + key: modifiedValue, + label: modifiedValue?.split('|')[1] ?? modifiedValue, + }, + ] + : []) + } + loading={slackChannelsLoading} + /> + + {showSlackMembershipWarning ? ( + +
+ + The PostHog Slack App is not in this channel. Please add it to the channel otherwise + Subscriptions will fail to be delivered.{' '} + + See the Docs for more information + + + + Check again + +
+
+ ) : null} + + ) +} + +export function SlackIntegrationView({ + integration, + suffix, +}: { + integration: IntegrationType + suffix?: JSX.Element +}): JSX.Element { + return ( +
+
+ +
+
+ Connected to {integration.config.team.name} workspace +
+ {integration.created_by ? ( + + ) : null} +
+
+ + {suffix} +
+ ) +} diff --git a/frontend/src/scenes/settings/project/integrationsLogic.ts b/frontend/src/lib/integrations/integrationsLogic.ts similarity index 80% rename from frontend/src/scenes/settings/project/integrationsLogic.ts rename to frontend/src/lib/integrations/integrationsLogic.ts index 55c90ac929b797..464c3901dd8ef2 100644 --- a/frontend/src/scenes/settings/project/integrationsLogic.ts +++ b/frontend/src/lib/integrations/integrationsLogic.ts @@ -6,18 +6,18 @@ import api from 'lib/api' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' import { urls } from 'scenes/urls' -import { IntegrationType, SlackChannelType } from '~/types' +import { IntegrationType } from '~/types' import type { integrationsLogicType } from './integrationsLogicType' // NOTE: Slack enforces HTTPS urls so to aid local dev we change to https so the redirect works. // Just means we have to change it back to http once redirected. -export const getSlackRedirectUri = (next: string = ''): string => +const getSlackRedirectUri = (next: string = ''): string => `${window.location.origin.replace('http://', 'https://')}/integrations/slack/redirect${ next ? '?next=' + encodeURIComponent(next) : '' }` -export const getSlackEventsUri = (): string => +const getSlackEventsUri = (): string => `${window.location.origin.replace('http://', 'https://')}/api/integrations/slack/events` // Modified version of https://app.slack.com/app-settings/TSS5W8YQZ/A03KWE2FJJ2/app-manifest to match current instance @@ -57,7 +57,7 @@ export const getSlackAppManifest = (): any => ({ }) export const integrationsLogic = kea([ - path(['scenes', 'project', 'Settings', 'integrationsLogic']), + path(['lib', 'integrations', 'integrationsLogic']), connect({ values: [preflightLogic, ['siteUrlMisconfigured', 'preflight']], }), @@ -67,7 +67,7 @@ export const integrationsLogic = kea([ deleteIntegration: (id: number) => ({ id }), }), - loaders(({ values }) => ({ + loaders(() => ({ integrations: [ null as IntegrationType[] | null, { @@ -77,20 +77,6 @@ export const integrationsLogic = kea([ }, }, ], - - slackChannels: [ - null as SlackChannelType[] | null, - { - loadSlackChannels: async () => { - if (!values.slackIntegration) { - return null - } - - const res = await api.integrations.slackChannels(values.slackIntegration.id) - return res.channels - }, - }, - ], })), listeners(({ actions }) => ({ handleRedirect: async ({ kind, searchParams }) => { @@ -142,27 +128,13 @@ export const integrationsLogic = kea([ }, })), selectors({ - slackIntegration: [ + slackIntegrations: [ (s) => [s.integrations], (integrations) => { - return integrations?.find((x) => x.kind == 'slack') + return integrations?.filter((x) => x.kind == 'slack') }, ], - isMemberOfSlackChannel: [ - (s) => [s.slackChannels], - (slackChannels) => { - return (channel: string) => { - if (!slackChannels) { - return null - } - - const [channelId] = channel.split('|') - - return slackChannels.find((x) => x.id === channelId)?.is_member - } - }, - ], addToSlackButtonUrl: [ (s) => [s.preflight], (preflight) => { diff --git a/frontend/src/lib/integrations/slackIntegrationLogic.ts b/frontend/src/lib/integrations/slackIntegrationLogic.ts new file mode 100644 index 00000000000000..729dc33378f411 --- /dev/null +++ b/frontend/src/lib/integrations/slackIntegrationLogic.ts @@ -0,0 +1,48 @@ +import { actions, connect, kea, key, path, props, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' + +import { SlackChannelType } from '~/types' + +import type { slackIntegrationLogicType } from './slackIntegrationLogicType' + +export const slackIntegrationLogic = kea([ + props({} as { id: number }), + key((props) => props.id), + path((key) => ['lib', 'integrations', 'slackIntegrationLogic', key]), + connect({ + values: [preflightLogic, ['siteUrlMisconfigured', 'preflight']], + }), + actions({ + loadSlackChannels: true, + }), + + loaders(({ props }) => ({ + slackChannels: [ + null as SlackChannelType[] | null, + { + loadSlackChannels: async () => { + const res = await api.integrations.slackChannels(props.id) + return res.channels + }, + }, + ], + })), + selectors({ + isMemberOfSlackChannel: [ + (s) => [s.slackChannels], + (slackChannels) => { + return (channel: string) => { + if (!slackChannels) { + return null + } + + const [channelId] = channel.split('|') + + return slackChannels.find((x) => x.id === channelId)?.is_member ?? false + } + }, + ], + }), +]) diff --git a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx index 0bef3e097d6c7e..c6c67170f851ef 100644 --- a/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx +++ b/frontend/src/lib/lemon-ui/LemonInputSelect/LemonInputSelect.tsx @@ -33,6 +33,7 @@ export type LemonInputSelectProps = Pick< allowCustomValues?: boolean onChange?: (newValue: string[]) => void onBlur?: () => void + onFocus?: () => void onInputChange?: (newValue: string) => void 'data-attr'?: string popoverClassName?: string @@ -45,6 +46,7 @@ export function LemonInputSelect({ loading, onChange, onInputChange, + onFocus, onBlur, mode, disabled, @@ -189,6 +191,7 @@ export function LemonInputSelect({ } const _onFocus = (): void => { + onFocus?.() setShowPopover(true) popoverFocusRef.current = true } diff --git a/frontend/src/scenes/IntegrationsRedirect/IntegrationsRedirect.tsx b/frontend/src/scenes/IntegrationsRedirect/IntegrationsRedirect.tsx index 3a1cb65525eb66..b59afbff98a64e 100644 --- a/frontend/src/scenes/IntegrationsRedirect/IntegrationsRedirect.tsx +++ b/frontend/src/scenes/IntegrationsRedirect/IntegrationsRedirect.tsx @@ -1,6 +1,6 @@ +import { integrationsLogic } from 'lib/integrations/integrationsLogic' import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' import { SceneExport } from 'scenes/sceneTypes' -import { integrationsLogic } from 'scenes/settings/project/integrationsLogic' export const scene: SceneExport = { component: IntegrationsRedirect, diff --git a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputs.tsx b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputs.tsx index 6961e09250b4f9..92a068b7fb6bed 100644 --- a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputs.tsx +++ b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputs.tsx @@ -23,6 +23,8 @@ import { useEffect, useMemo, useState } from 'react' import { groupsModel } from '~/models/groupsModel' import { HogFunctionInputSchemaType } from '~/types' +import { HogFunctionInputIntegration } from './integrations/HogFunctionInputIntegration' +import { HogFunctionInputIntegrationField } from './integrations/HogFunctionInputIntegrationField' import { pipelineHogFunctionConfigurationLogic } from './pipelineHogFunctionConfigurationLogic' export type HogFunctionInputProps = { @@ -36,7 +38,7 @@ export type HogFunctionInputWithSchemaProps = { schema: HogFunctionInputSchemaType } -const typeList = ['string', 'boolean', 'dictionary', 'choice', 'json'] as const +const typeList = ['string', 'boolean', 'dictionary', 'choice', 'json', 'integration'] as const function useAutocompleteOptions(): languages.CompletionItem[] { const { groupTypes } = useValues(groupsModel) @@ -257,12 +259,14 @@ export function HogFunctionInputRenderer({ value, onChange, schema, disabled }: case 'boolean': return onChange?.(checked)} disabled={disabled} /> + case 'integration': + return + case 'integration_field': + return default: return ( Unknown field type "{schema.type}". -
- You may need to upgrade PostHog!
) } @@ -354,6 +358,17 @@ function HogFunctionInputSchemaControls({ value, onChange, onDone }: HogFunction )} + {value.type === 'integration' && ( + + _onChange({ integration })} + options={[{ label: 'Slack', value: 'slack' }]} + placeholder="Choose kind" + /> + + )} + void +} + +export type HogFunctionInputIntegrationProps = HogFunctionInputIntegrationConfigureProps & { + schema: HogFunctionInputSchemaType +} + +export function HogFunctionInputIntegration({ schema, ...props }: HogFunctionInputIntegrationProps): JSX.Element { + if (schema.integration === 'slack') { + return + } + return ( +
+

Unsupported integration type: {schema.integration}

+
+ ) +} + +export function HogFunctionIntegrationSlackConnection({ + onChange, + value, +}: HogFunctionInputIntegrationConfigureProps): JSX.Element { + const { integrationsLoading, slackIntegrations, addToSlackButtonUrl } = useValues(integrationsLogic) + + const integration = slackIntegrations?.find((integration) => integration.id === value) + + if (integrationsLoading) { + return + } + + const button = ( + ({ + icon: , + onClick: () => onChange?.(integration.id), + label: integration.config.team.name, + })) || []), + { + to: addToSlackButtonUrl(window.location.pathname + '?target_type=slack') || '', + label: 'Add to different Slack workspace', + }, + ]} + > + {integration ? ( + Change + ) : ( + Choose Slack connection + )} + + ) + + return <>{integration ? : button} +} diff --git a/frontend/src/scenes/pipeline/hogfunctions/integrations/HogFunctionInputIntegrationField.tsx b/frontend/src/scenes/pipeline/hogfunctions/integrations/HogFunctionInputIntegrationField.tsx new file mode 100644 index 00000000000000..ca348c2f7fb2d8 --- /dev/null +++ b/frontend/src/scenes/pipeline/hogfunctions/integrations/HogFunctionInputIntegrationField.tsx @@ -0,0 +1,62 @@ +import { LemonSkeleton } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { integrationsLogic } from 'lib/integrations/integrationsLogic' +import { SlackChannelPicker } from 'lib/integrations/SlackIntegrationHelpers' + +import { HogFunctionInputSchemaType } from '~/types' + +import { pipelineHogFunctionConfigurationLogic } from '../pipelineHogFunctionConfigurationLogic' + +export type HogFunctionInputIntegrationFieldProps = { + schema: HogFunctionInputSchemaType + value?: any + onChange?: (value: any) => void +} + +export function HogFunctionInputIntegrationField({ + schema, + value, + onChange, +}: HogFunctionInputIntegrationFieldProps): JSX.Element { + const { configuration } = useValues(pipelineHogFunctionConfigurationLogic) + const { integrationsLoading, integrations } = useValues(integrationsLogic) + + if (integrationsLoading) { + return + } + + const relatedSchemaIntegration = configuration.inputs_schema?.find((input) => input.key === schema.integration_key) + + if (!relatedSchemaIntegration) { + return ( +
+ Bad configuration: integration key {schema.integration_key} not found in schema +
+ ) + } + + const integrationId = configuration.inputs?.[relatedSchemaIntegration.key]?.value + const integration = integrations?.find((integration) => integration.id === integrationId) + + if (!integration) { + return ( +
+ Configure {relatedSchemaIntegration.label} to continue +
+ ) + } + if (schema.integration_field === 'slack_channel') { + return ( + onChange?.(x?.split('|')[0])} + integration={integration} + /> + ) + } + return ( +
+

Unsupported integration type: {schema.integration}

+
+ ) +} diff --git a/frontend/src/scenes/pipeline/hogfunctions/integrations/types.ts b/frontend/src/scenes/pipeline/hogfunctions/integrations/types.ts new file mode 100644 index 00000000000000..a44f2b301eca9c --- /dev/null +++ b/frontend/src/scenes/pipeline/hogfunctions/integrations/types.ts @@ -0,0 +1,10 @@ +import { HogFunctionInputSchemaType } from '~/types' + +export type HogFunctionInputIntegrationConfigureProps = { + value?: any + onChange?: (value: string | null) => void +} + +export type HogFunctionInputIntegrationProps = HogFunctionInputIntegrationConfigureProps & { + schema: HogFunctionInputSchemaType +} diff --git a/frontend/src/scenes/settings/project/SlackIntegration.tsx b/frontend/src/scenes/settings/project/SlackIntegration.tsx index 58c479c6832679..31297bbd689ba9 100644 --- a/frontend/src/scenes/settings/project/SlackIntegration.tsx +++ b/frontend/src/scenes/settings/project/SlackIntegration.tsx @@ -2,32 +2,30 @@ import { IconTrash } from '@posthog/icons' import { LemonButton, Link } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator' -import { IconSlack } from 'lib/lemon-ui/icons' +import { getSlackAppManifest, integrationsLogic } from 'lib/integrations/integrationsLogic' +import { SlackIntegrationView } from 'lib/integrations/SlackIntegrationHelpers' import { LemonDialog } from 'lib/lemon-ui/LemonDialog' import { useState } from 'react' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { getSlackAppManifest, integrationsLogic } from './integrationsLogic' - export function SlackIntegration(): JSX.Element { - const { slackIntegration, addToSlackButtonUrl } = useValues(integrationsLogic) + const { slackIntegrations, addToSlackButtonUrl } = useValues(integrationsLogic) const { deleteIntegration } = useActions(integrationsLogic) const [showSlackInstructions, setShowSlackInstructions] = useState(false) const { user } = useValues(userLogic) - const onDeleteClick = (): void => { + const onDeleteClick = (id: number): void => { LemonDialog.open({ title: `Do you want to disconnect from Slack?`, description: - 'This cannot be undone. PostHog resources configured to use Slack will remain but will stop working.', + 'This cannot be undone. PostHog resources configured to use this Slack workspace will remain but will stop working.', primaryButton: { children: 'Yes, disconnect', status: 'danger', onClick: () => { - if (slackIntegration?.id) { - deleteIntegration(slackIntegration.id) + if (id) { + deleteIntegration(id) } }, }, @@ -49,79 +47,75 @@ export function SlackIntegration(): JSX.Element { .

-
- {slackIntegration ? ( -
-
- -
-
- Connected to {slackIntegration.config.team.name} workspace -
- {slackIntegration.created_by ? ( - - ) : null} -
-
- - }> - Disconnect - -
- ) : addToSlackButtonUrl() ? ( - - Add to Slack - - ) : user?.is_staff ? ( - !showSlackInstructions ? ( - <> - setShowSlackInstructions(true)}> - Show Instructions +
+ {slackIntegrations?.map((integration) => ( + onDeleteClick(integration.id)} + icon={} + > + Disconnect - - ) : ( - <> -
To get started
-

-

    -
  1. Copy the below Slack App Template
  2. -
  3. - Go to{' '} - - Slack Apps - -
  4. -
  5. Create an App using the provided template
  6. -
  7. - Go to Instance Settings and update the{' '} - "SLACK_" properties using the values from the{' '} - App Credentials section of your Slack Apps -
  8. -
+ } + /> + ))} - - {JSON.stringify(getSlackAppManifest(), null, 2)} - -

- - ) - ) : ( -

- This PostHog instance is not configured for Slack. Please contact the instance owner to - configure it. -

- )} +
+ {addToSlackButtonUrl() ? ( + + Connect to Slack workspace + + ) : user?.is_staff ? ( + !showSlackInstructions ? ( + <> + setShowSlackInstructions(true)}> + Show Instructions + + + ) : ( + <> +
To get started
+

+

    +
  1. Copy the below Slack App Template
  2. +
  3. + Go to{' '} + + Slack Apps + +
  4. +
  5. Create an App using the provided template
  6. +
  7. + Go to Instance Settings and update + the "SLACK_" properties using the values from the{' '} + App Credentials section of your Slack Apps +
  8. +
+ + + {JSON.stringify(getSlackAppManifest(), null, 2)} + +

+ + ) + ) : ( +

+ This PostHog instance is not configured for Slack. Please contact the instance owner to + configure it. +

+ )} +
) diff --git a/frontend/src/types.ts b/frontend/src/types.ts index afb1c56f1b7dab..7e3996c6ffc93f 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -4183,7 +4183,7 @@ export type OnboardingProduct = { } export type HogFunctionInputSchemaType = { - type: 'string' | 'boolean' | 'dictionary' | 'choice' | 'json' + type: 'string' | 'boolean' | 'dictionary' | 'choice' | 'json' | 'integration' | 'integration_field' key: string label: string choices?: { value: string; label: string }[] @@ -4191,6 +4191,9 @@ export type HogFunctionInputSchemaType = { default?: any secret?: boolean description?: string + integration?: string + integration_key?: string + integration_field?: 'slack_channel' } export type HogFunctionType = { diff --git a/plugin-server/src/cdp/cdp-consumers.ts b/plugin-server/src/cdp/cdp-consumers.ts index 2a8891807d7942..efcda79763198e 100644 --- a/plugin-server/src/cdp/cdp-consumers.ts +++ b/plugin-server/src/cdp/cdp-consumers.ts @@ -427,9 +427,15 @@ export class CdpFunctionCallbackConsumer extends CdpConsumerBase { } // We use the provided config if given, otherwise the function's config - const functionConfiguration: HogFunctionType = configuration ?? hogFunction + const compoundConfiguration: HogFunctionType = { + ...hogFunction, + ...(configuration ?? {}), + } + + // TODO: Type the configuration better so we don't make mistakes here + await this.hogFunctionManager.enrichWithIntegrations([compoundConfiguration]) - let response = this.hogExecutor.execute(functionConfiguration, invocation) + let response = this.hogExecutor.execute(compoundConfiguration, invocation) while (response.asyncFunctionRequest) { const asyncFunctionRequest = response.asyncFunctionRequest @@ -456,7 +462,7 @@ export class CdpFunctionCallbackConsumer extends CdpConsumerBase { // Clear it so we can't ever end up in a loop delete response.asyncFunctionRequest - response = this.hogExecutor.execute(functionConfiguration, response, asyncFunctionRequest.vmState) + response = this.hogExecutor.execute(compoundConfiguration, response, asyncFunctionRequest.vmState) } res.json({ diff --git a/plugin-server/src/cdp/hog-executor.ts b/plugin-server/src/cdp/hog-executor.ts index f86ed7aba528b8..f9628f9960a613 100644 --- a/plugin-server/src/cdp/hog-executor.ts +++ b/plugin-server/src/cdp/hog-executor.ts @@ -307,7 +307,7 @@ export class HogExecutor { buildHogFunctionGlobals(hogFunction: HogFunctionType, invocation: HogFunctionInvocation): Record { const builtInputs: Record = {} - Object.entries(hogFunction.inputs).forEach(([key, item]) => { + Object.entries(hogFunction.inputs ?? {}).forEach(([key, item]) => { builtInputs[key] = item.value if (item.bytecode) { diff --git a/plugin-server/src/cdp/hog-function-manager.ts b/plugin-server/src/cdp/hog-function-manager.ts index 4adbec4ab81ccb..b8522d1f388edc 100644 --- a/plugin-server/src/cdp/hog-function-manager.ts +++ b/plugin-server/src/cdp/hog-function-manager.ts @@ -4,11 +4,13 @@ import { PluginsServerConfig, Team } from '../types' import { PostgresRouter, PostgresUse } from '../utils/db/postgres' import { PubSub } from '../utils/pubsub' import { status } from '../utils/status' -import { HogFunctionType } from './types' +import { HogFunctionType, IntegrationType } from './types' export type HogFunctionMap = Record export type HogFunctionCache = Record +const HOG_FUNCTION_FIELDS = ['id', 'team_id', 'name', 'enabled', 'inputs', 'inputs_schema', 'filters', 'bytecode'] + export class HogFunctionManager { private started: boolean private ready: boolean @@ -67,13 +69,49 @@ export class HogFunctionManager { } public async reloadAllHogFunctions(): Promise { - this.cache = await fetchAllHogFunctionsGroupedByTeam(this.postgres) + const items = ( + await this.postgres.query( + PostgresUse.COMMON_READ, + ` + SELECT ${HOG_FUNCTION_FIELDS.join(', ')} + FROM posthog_hogfunction + WHERE deleted = FALSE AND enabled = TRUE + `, + [], + 'fetchAllHogFunctions' + ) + ).rows + + await this.enrichWithIntegrations(items) + + const cache: HogFunctionCache = {} + for (const item of items) { + if (!cache[item.team_id]) { + cache[item.team_id] = {} + } + + cache[item.team_id][item.id] = item + } + + this.cache = cache status.info('🍿', 'Fetched all hog functions from DB anew') } public async reloadHogFunctions(teamId: Team['id'], ids: HogFunctionType['id'][]): Promise { status.info('🍿', `Reloading hog functions ${ids} from DB`) - const items = await fetchEnabledHogFunctions(this.postgres, ids) + + const items: HogFunctionType[] = ( + await this.postgres.query( + PostgresUse.COMMON_READ, + `SELECT ${HOG_FUNCTION_FIELDS.join(', ')} + FROM posthog_hogfunction + WHERE id = ANY($1) AND deleted = FALSE AND enabled = TRUE`, + [ids], + 'fetchEnabledHogFunctions' + ) + ).rows + + await this.enrichWithIntegrations(items) if (!this.cache[teamId]) { this.cache[teamId] = {} @@ -89,66 +127,77 @@ export class HogFunctionManager { } } - public fetchHogFunction(id: HogFunctionType['id']): Promise { - return fetchHogFunction(this.postgres, id) + public async fetchHogFunction(id: HogFunctionType['id']): Promise { + const items: HogFunctionType[] = ( + await this.postgres.query( + PostgresUse.COMMON_READ, + `SELECT ${HOG_FUNCTION_FIELDS.join(', ')} + FROM posthog_hogfunction + WHERE id = $1 AND deleted = FALSE`, + [id], + 'fetchHogFunction' + ) + ).rows + await this.enrichWithIntegrations(items) + return items[0] ?? null } -} - -const HOG_FUNCTION_FIELDS = ['id', 'team_id', 'name', 'enabled', 'inputs', 'filters', 'bytecode'] -async function fetchAllHogFunctionsGroupedByTeam(client: PostgresRouter): Promise { - const items = ( - await client.query( - PostgresUse.COMMON_READ, - ` - SELECT ${HOG_FUNCTION_FIELDS.join(', ')} - FROM posthog_hogfunction - WHERE deleted = FALSE AND enabled = TRUE - `, - [], - 'fetchAllHogFunctions' - ) - ).rows + public async enrichWithIntegrations(items: HogFunctionType[]): Promise { + const integrationIds: number[] = [] + + items.forEach((item) => { + item.inputs_schema?.forEach((schema) => { + if (schema.type === 'integration') { + const input = item.inputs?.[schema.key] + if (input && typeof input.value === 'number') { + integrationIds.push(input.value) + } + } + }) + }) - const cache: HogFunctionCache = {} - for (const item of items) { - if (!cache[item.team_id]) { - cache[item.team_id] = {} + if (!items.length) { + return } - cache[item.team_id][item.id] = item - } - - return cache -} - -async function fetchEnabledHogFunctions( - client: PostgresRouter, - ids: HogFunctionType['id'][] -): Promise { - const items: HogFunctionType[] = ( - await client.query( - PostgresUse.COMMON_READ, - `SELECT ${HOG_FUNCTION_FIELDS.join(', ')} - FROM posthog_hogfunction - WHERE id = ANY($1) AND deleted = FALSE AND enabled = TRUE`, - [ids], - 'fetchEnabledHogFunctions' + const integrations: IntegrationType[] = ( + await this.postgres.query( + PostgresUse.COMMON_READ, + `SELECT id, team_id, kind, config, sensitive_config + FROM posthog_integration + WHERE id = ANY($1)`, + [integrationIds], + 'fetchIntegrations' + ) + ).rows + + const integrationConfigsByTeamAndId: Record> = integrations.reduce( + (acc, integration) => { + return { + ...acc, + [`${integration.team_id}:${integration.id}`]: { + ...integration.config, + ...integration.sensitive_config, + }, + } + }, + {} ) - ).rows - return items -} -async function fetchHogFunction(client: PostgresRouter, id: HogFunctionType['id']): Promise { - const items: HogFunctionType[] = ( - await client.query( - PostgresUse.COMMON_READ, - `SELECT ${HOG_FUNCTION_FIELDS.join(', ')} - FROM posthog_hogfunction - WHERE id = $1 AND deleted = FALSE`, - [id], - 'fetchHogFunction' - ) - ).rows - return items[0] ?? null + items.forEach((item) => { + item.inputs_schema?.forEach((schema) => { + if (schema.type === 'integration') { + const input = item.inputs?.[schema.key] + if (!input) { + return + } + const integrationId = input.value + const integrationConfig = integrationConfigsByTeamAndId[`${item.team_id}:${integrationId}`] + if (integrationConfig) { + input.value = integrationConfig + } + } + }) + }) + } } diff --git a/plugin-server/src/cdp/types.ts b/plugin-server/src/cdp/types.ts index 83c30a4344d141..f961229d966fe5 100644 --- a/plugin-server/src/cdp/types.ts +++ b/plugin-server/src/cdp/types.ts @@ -182,7 +182,7 @@ export type HogFunctionMessageToQueue = { // Mostly copied from frontend types export type HogFunctionInputSchemaType = { - type: 'string' | 'number' | 'boolean' | 'dictionary' | 'choice' | 'json' + type: 'string' | 'boolean' | 'dictionary' | 'choice' | 'json' | 'integration' | 'integration_field' key: string label?: string choices?: { value: string; label: string }[] @@ -190,6 +190,9 @@ export type HogFunctionInputSchemaType = { default?: any secret?: boolean description?: string + integration?: string + integration_key?: string + integration_field?: 'slack_channel' } export type HogFunctionType = { @@ -199,13 +202,25 @@ export type HogFunctionType = { enabled: boolean hog: string bytecode: HogBytecode - inputs_schema: HogFunctionInputSchemaType[] - inputs: Record< - string, - { - value: any - bytecode?: HogBytecode | object - } - > + inputs_schema?: HogFunctionInputSchemaType[] + inputs?: Record filters?: HogFunctionFilters | null } + +export type HogFunctionInputType = { + value: any + bytecode?: HogBytecode | object +} + +export type IntegrationType = { + id: number + team_id: number + kind: 'slack' + config: Record + sensitive_config: Record + + // Fields we don't load but need for seeding data + errors?: string + created_at?: string + created_by_id?: number +} diff --git a/plugin-server/tests/cdp/fixtures.ts b/plugin-server/tests/cdp/fixtures.ts index 9147d043a7468c..b70af8efe3c394 100644 --- a/plugin-server/tests/cdp/fixtures.ts +++ b/plugin-server/tests/cdp/fixtures.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'crypto' import { Message } from 'node-rdkafka' -import { HogFunctionInvocationGlobals, HogFunctionType } from '../../src/cdp/types' +import { HogFunctionInvocationGlobals, HogFunctionType, IntegrationType } from '../../src/cdp/types' import { ClickHouseTimestamp, RawClickHouseEvent, Team } from '../../src/types' import { PostgresRouter } from '../../src/utils/db/postgres' import { insertRow } from '../helpers/sql' @@ -23,6 +23,18 @@ export const createHogFunction = (hogFunction: Partial) => { return item } +export const createIntegration = (integration: Partial) => { + const item: IntegrationType = { + team_id: 1, + errors: '', + created_at: new Date().toISOString(), + created_by_id: 1001, + ...integration, + } + + return item +} + export const createIncomingEvent = (teamId: number, data: Partial): RawClickHouseEvent => { return { team_id: teamId, @@ -67,6 +79,22 @@ export const insertHogFunction = async ( return res } +export const insertIntegration = async ( + postgres: PostgresRouter, + team_id: Team['id'], + integration: Partial = {} +): Promise => { + const res = await insertRow( + postgres, + 'posthog_integration', + createIntegration({ + ...integration, + team_id: team_id, + }) + ) + return res +} + export const createHogExecutionGlobals = ( data: Partial = {} ): HogFunctionInvocationGlobals => { diff --git a/plugin-server/tests/cdp/hog-function-manager.test.ts b/plugin-server/tests/cdp/hog-function-manager.test.ts index a3245b4ef97c1f..05bb8debea75c7 100644 --- a/plugin-server/tests/cdp/hog-function-manager.test.ts +++ b/plugin-server/tests/cdp/hog-function-manager.test.ts @@ -1,10 +1,10 @@ import { HogFunctionManager } from '../../src/cdp/hog-function-manager' -import { HogFunctionType } from '../../src/cdp/types' +import { HogFunctionType, IntegrationType } from '../../src/cdp/types' import { Hub } from '../../src/types' import { createHub } from '../../src/utils/db/hub' import { PostgresUse } from '../../src/utils/db/postgres' import { createTeam, resetTestDatabase } from '../helpers/sql' -import { insertHogFunction } from './fixtures' +import { insertHogFunction, insertIntegration } from './fixtures' describe('HogFunctionManager', () => { let hub: Hub @@ -12,6 +12,7 @@ describe('HogFunctionManager', () => { let manager: HogFunctionManager let hogFunctions: HogFunctionType[] + let integrations: IntegrationType[] let teamId1: number let teamId2: number @@ -27,15 +28,53 @@ describe('HogFunctionManager', () => { teamId2 = await createTeam(hub.db.postgres, team!.organization_id) hogFunctions = [] + integrations = [] + + integrations.push( + await insertIntegration(hub.postgres, teamId1, { + kind: 'slack', + config: { team: 'foobar' }, + sensitive_config: { access_token: 'token' }, + }) + ) + hogFunctions.push( await insertHogFunction(hub.postgres, teamId1, { name: 'Test Hog Function team 1', + inputs_schema: [ + { + type: 'integration', + key: 'slack', + }, + ], + inputs: { + slack: { + value: integrations[0].id, + }, + normal: { + value: integrations[0].id, + }, + }, }) ) hogFunctions.push( await insertHogFunction(hub.postgres, teamId2, { name: 'Test Hog Function team 2', + inputs_schema: [ + { + type: 'integration', + key: 'slack', + }, + ], + inputs: { + slack: { + value: integrations[0].id, + }, + normal: { + value: integrations[0].id, + }, + }, }) ) @@ -55,9 +94,25 @@ describe('HogFunctionManager', () => { team_id: teamId1, name: 'Test Hog Function team 1', enabled: true, - inputs: null, bytecode: null, filters: null, + inputs_schema: [ + { + key: 'slack', + type: 'integration', + }, + ], + inputs: { + slack: { + value: { + access_token: 'token', + team: 'foobar', + }, + }, + normal: { + value: integrations[0].id, + }, + }, }, }) @@ -98,4 +153,31 @@ describe('HogFunctionManager', () => { expect(functionsMap).not.toHaveProperty(hogFunctions[0].id) }) + + it('enriches integration inputs if found and belonging to the team', () => { + const function1Inputs = manager.getTeamHogFunctions(teamId1)[hogFunctions[0].id].inputs + const function2Inputs = manager.getTeamHogFunctions(teamId2)[hogFunctions[1].id].inputs + + // Only the right team gets the integration inputs enriched + expect(function1Inputs).toEqual({ + slack: { + value: { + access_token: 'token', + team: 'foobar', + }, + }, + normal: { + value: integrations[0].id, + }, + }) + + expect(function2Inputs).toEqual({ + slack: { + value: integrations[0].id, + }, + normal: { + value: integrations[0].id, + }, + }) + }) }) diff --git a/posthog/cdp/templates/slack/template_slack.py b/posthog/cdp/templates/slack/template_slack.py index 69851527577ff7..d00cf01c8a748a 100644 --- a/posthog/cdp/templates/slack/template_slack.py +++ b/posthog/cdp/templates/slack/template_slack.py @@ -1,65 +1,89 @@ from posthog.cdp.templates.hog_function_template import HogFunctionTemplate -# NOTE: Slack template is essentially just a webhook template with limited options - template: HogFunctionTemplate = HogFunctionTemplate( status="beta", id="template-slack", - name="Slack webhook", - description="Sends a webhook templated by the incoming event data", + name="Post a Slack message", + description="Sends a message to a slack channel", icon_url="/api/projects/@current/hog_functions/icon/?id=slack.com", hog=""" -fetch(inputs.url, { - 'body': inputs.body, +let res := fetch('https://slack.com/api/chat.postMessage', { + 'body': { + 'channel': inputs.channel, + 'icon_emoji': inputs.icon_emoji, + 'username': inputs.username, + 'blocks': inputs.blocks, + 'text': inputs.text + }, 'method': 'POST', 'headers': { + 'Authorization': f'Bearer {inputs.slack_workspace.access_token}', 'Content-Type': 'application/json' } }); + +if (res.status != 200 or not res.body.ok) { + print('Non-ok response:', res) +} """.strip(), inputs_schema=[ { - "key": "url", - "type": "string", - "label": "Slack webhook URL", - "description": "Create a slack webhook URL in your (see https://api.slack.com/messaging/webhooks)", - "placeholder": "https://hooks.slack.com/services/XXX/YYY", + "key": "slack_workspace", + "type": "integration", + "integration": "slack", + "label": "Slack workspace", + "secret": False, + "required": True, + }, + { + "key": "channel", + "type": "integration_field", + "integration_key": "slack_workspace", + "integration_field": "slack_channel", + "label": "Channel to post to", + "description": "Select the channel to post to (e.g. #general). The PostHog app must be installed in the workspace.", "secret": False, "required": True, }, + {"key": "icon_emoji", "type": "string", "label": "Emoji icon", "default": ":hedgehog:", "required": False}, + {"key": "username", "type": "string", "label": "Bot name", "defaukt": "PostHog", "required": False}, { - "key": "body", + "key": "blocks", "type": "json", - "label": "Message", - "description": "Message to send to Slack (see https://api.slack.com/block-kit/building)", - "default": { - "blocks": [ - { - "text": { - "text": "*{person.name}* triggered event: '{event.name}'", - "type": "mrkdwn", - }, - "type": "section", - }, - { - "type": "actions", - "elements": [ - { - "url": "{person.url}", - "text": {"text": "View Person in PostHog", "type": "plain_text"}, - "type": "button", - }, - { - "url": "{source.url}", - "text": {"text": "Message source", "type": "plain_text"}, - "type": "button", - }, - ], + "label": "Blocks", + "description": "(see https://api.slack.com/block-kit/building)", + "default": [ + { + "text": { + "text": "*{person.name}* triggered event: '{event.name}'", + "type": "mrkdwn", }, - ] - }, + "type": "section", + }, + { + "type": "actions", + "elements": [ + { + "url": "{person.url}", + "text": {"text": "View Person in PostHog", "type": "plain_text"}, + "type": "button", + }, + { + "url": "{source.url}", + "text": {"text": "Message source", "type": "plain_text"}, + "type": "button", + }, + ], + }, + ], "secret": False, "required": False, }, + { + "key": "text", + "type": "string", + "label": "Plain text message", + "description": "Optional fallback message if blocks are not provided or supported", + }, ], ) diff --git a/posthog/cdp/templates/slack/test_template_slack.py b/posthog/cdp/templates/slack/test_template_slack.py index 15d3f17804b187..9b8bab8cd19529 100644 --- a/posthog/cdp/templates/slack/test_template_slack.py +++ b/posthog/cdp/templates/slack/test_template_slack.py @@ -5,25 +5,51 @@ class TestTemplateSlack(BaseHogFunctionTemplateTest): template = template_slack + def _inputs(self, **kwargs): + inputs = { + "slack_workspace": { + "access_token": "xoxb-1234", + }, + "icon_emoji": ":hedgehog:", + "username": "PostHog", + "channel": "channel", + "blocks": [], + } + inputs.update(kwargs) + return inputs + def test_function_works(self): - res = self.run_function( - inputs={ - "url": "https://webhooks.slack.com/1234", - "body": { - "blocks": [], - }, - } - ) + self.mock_fetch_response = lambda *args: {"status": 200, "body": {"ok": True}} # type: ignore + res = self.run_function(self._inputs()) assert res.result is None assert self.get_mock_fetch_calls()[0] == ( - "https://webhooks.slack.com/1234", + "https://slack.com/api/chat.postMessage", { + "body": { + "channel": "channel", + "icon_emoji": ":hedgehog:", + "username": "PostHog", + "blocks": [], + "text": None, + }, + "method": "POST", "headers": { + "Authorization": "Bearer xoxb-1234", "Content-Type": "application/json", }, - "body": {"blocks": []}, - "method": "POST", }, ) + + assert self.get_mock_print_calls() == [] + + def test_function_prints_warning_on_bad_status(self): + self.mock_fetch_response = lambda *args: {"status": 400, "body": {"ok": True}} # type: ignore + self.run_function(self._inputs()) + assert self.get_mock_print_calls() == [("Non-ok response:", {"status": 400, "body": {"ok": True}})] + + def test_function_prints_warning_on_bad_body(self): + self.mock_fetch_response = lambda *args: {"status": 200, "body": {"ok": False}} # type: ignore + self.run_function(self._inputs()) + assert self.get_mock_print_calls() == [("Non-ok response:", {"status": 200, "body": {"ok": False}})] diff --git a/posthog/cdp/validation.py b/posthog/cdp/validation.py index 4a38523947de74..48944fa2ba1889 100644 --- a/posthog/cdp/validation.py +++ b/posthog/cdp/validation.py @@ -24,7 +24,9 @@ def generate_template_bytecode(obj: Any) -> Any: class InputsSchemaItemSerializer(serializers.Serializer): - type = serializers.ChoiceField(choices=["string", "boolean", "dictionary", "choice", "json"]) + type = serializers.ChoiceField( + choices=["string", "boolean", "dictionary", "choice", "json", "integration", "integration_field"] + ) key = serializers.CharField() label = serializers.CharField(required=False) # type: ignore choices = serializers.ListField(child=serializers.DictField(), required=False) @@ -32,6 +34,9 @@ class InputsSchemaItemSerializer(serializers.Serializer): default = serializers.JSONField(required=False) secret = serializers.BooleanField(default=False) description = serializers.CharField(required=False) + integration = serializers.CharField(required=False) + integration_key = serializers.CharField(required=False) + integration_field = serializers.ChoiceField(choices=["slack_channel"], required=False) # TODO Validate choices if type=choice @@ -71,6 +76,9 @@ def validate(self, attrs): elif item_type == "dictionary": if not isinstance(value, dict): raise serializers.ValidationError({"inputs": {name: f"Value must be a dictionary."}}) + elif item_type == "integration": + if not isinstance(value, int): + raise serializers.ValidationError({"inputs": {name: f"Value must be an Integration ID."}}) try: if value: