@@ -112,19 +105,41 @@ export function Surveys(): JSX.Element {
/>
{
- setSurveyTab(newTab)
- setSurveysFilters({ ...filters, archived: newTab === SurveysTabs.Archived })
- }}
+ onChange={(newTab) => setTab(newTab as SurveysTabs)}
tabs={[
{ key: SurveysTabs.Active, label: 'Active' },
{ key: SurveysTabs.Archived, label: 'Archived' },
+ showLinkedHogFunctions ? { key: SurveysTabs.Notifications, label: 'Notifications' } : null,
{ key: SurveysTabs.History, label: 'History' },
]}
/>
{tab === SurveysTabs.History ? (
+ ) : tab === SurveysTabs.Notifications ? (
+ <>
+ Get notified whenever a survey result is submitted
+
+ >
) : (
<>
diff --git a/frontend/src/scenes/surveys/surveysLogic.tsx b/frontend/src/scenes/surveys/surveysLogic.tsx
index a817fcdc9bb7b..806379d9e441a 100644
--- a/frontend/src/scenes/surveys/surveysLogic.tsx
+++ b/frontend/src/scenes/surveys/surveysLogic.tsx
@@ -2,7 +2,7 @@ import { lemonToast } from '@posthog/lemon-ui'
import Fuse from 'fuse.js'
import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
-import { router } from 'kea-router'
+import { actionToUrl, router, urlToAction } from 'kea-router'
import api from 'lib/api'
import { Scene } from 'scenes/sceneTypes'
import { teamLogic } from 'scenes/teamLogic'
@@ -13,6 +13,14 @@ import { AvailableFeature, Breadcrumb, ProgressStatus, Survey, SurveyType } from
import type { surveysLogicType } from './surveysLogicType'
+export enum SurveysTabs {
+ Active = 'active',
+ Yours = 'yours',
+ Archived = 'archived',
+ Notifications = 'notifications',
+ History = 'history',
+}
+
export function getSurveyStatus(survey: Survey): ProgressStatus {
if (!survey.start_date) {
return ProgressStatus.Draft
@@ -37,6 +45,7 @@ export const surveysLogic = kea([
actions({
setSearchTerm: (searchTerm: string) => ({ searchTerm }),
setSurveysFilters: (filters: Partial, replace?: boolean) => ({ filters, replace }),
+ setTab: (tab: SurveysTabs) => ({ tab }),
}),
loaders(({ values }) => ({
surveys: {
@@ -63,6 +72,12 @@ export const surveysLogic = kea([
},
})),
reducers({
+ tab: [
+ SurveysTabs.Active as SurveysTabs,
+ {
+ setTab: (_, { tab }) => tab,
+ },
+ ],
searchTerm: {
setSearchTerm: (_, { searchTerm }) => searchTerm,
},
@@ -79,7 +94,7 @@ export const surveysLogic = kea([
},
],
}),
- listeners(({ actions }) => ({
+ listeners(({ actions, values }) => ({
deleteSurveySuccess: () => {
lemonToast.success('Survey deleted')
router.actions.push(urls.surveys())
@@ -95,6 +110,9 @@ export const surveysLogic = kea([
loadSurveysSuccess: () => {
actions.loadCurrentTeam()
},
+ setTab: ({ tab }) => {
+ actions.setSurveysFilters({ ...values.filters, archived: tab === SurveysTabs.Archived })
+ },
})),
selectors({
searchedSurveys: [
@@ -179,6 +197,18 @@ export const surveysLogic = kea([
},
],
}),
+ actionToUrl(({ values }) => ({
+ setTab: () => {
+ return [router.values.location.pathname, { ...router.values.searchParams, tab: values.tab }]
+ },
+ })),
+ urlToAction(({ actions }) => ({
+ [urls.surveys()]: (_, { tab }) => {
+ if (tab) {
+ actions.setTab(tab)
+ }
+ },
+ })),
afterMount(({ actions }) => {
actions.loadSurveys()
actions.loadResponsesCount()
diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts
index f1b2ad553a24f..f12b5017a9014 100644
--- a/frontend/src/scenes/urls.ts
+++ b/frontend/src/scenes/urls.ts
@@ -21,6 +21,7 @@ import {
import { OnboardingStepKey } from './onboarding/onboardingLogic'
import { SettingId, SettingLevelId, SettingSectionId } from './settings/types'
+import { SurveysTabs } from './surveys/surveysLogic'
/**
* To add a new URL to the front end:
@@ -137,7 +138,7 @@ export const urls = {
errorTracking: (): string => '/error_tracking',
errorTrackingGroup: (fingerprint: string): string =>
`/error_tracking/${fingerprint === ':fingerprint' ? fingerprint : encodeURIComponent(fingerprint)}`,
- surveys: (): string => '/surveys',
+ surveys: (tab?: SurveysTabs): string => `/surveys${tab ? `?tab=${tab}` : ''}`,
/** @param id A UUID or 'new'. ':id' for routing. */
survey: (id: string): string => `/surveys/${id}`,
surveyTemplates: (): string => '/survey_templates',
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index d66b0beb1e788..7a8fddeded785 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -4332,9 +4332,9 @@ export type HogFunctionMasking = {
// subset of EntityFilter
export interface HogFunctionFilterBase {
id: string
- name: string | null
- order: number
- properties: (EventPropertyFilter | PersonPropertyFilter | ElementPropertyFilter)[]
+ name?: string | null
+ order?: number
+ properties?: (EventPropertyFilter | PersonPropertyFilter | ElementPropertyFilter)[]
}
export interface HogFunctionFilterEvents extends HogFunctionFilterBase {
@@ -4375,27 +4375,36 @@ export type HogFunctionType = {
hog: string
inputs_schema?: HogFunctionInputSchemaType[]
- inputs?: Record
+ inputs?: Record | null
masking?: HogFunctionMasking | null
filters?: HogFunctionFiltersType | null
template?: HogFunctionTemplateType
status?: HogFunctionStatus
}
+export type HogFunctionTemplateStatus = 'alpha' | 'beta' | 'stable' | 'free' | 'deprecated'
+export type HogFunctionSubTemplateIdType = 'early_access_feature_enrollment' | 'survey_response'
+
export type HogFunctionConfigurationType = Omit<
HogFunctionType,
- 'created_at' | 'created_by' | 'updated_at' | 'status' | 'hog'
+ 'id' | 'created_at' | 'created_by' | 'updated_at' | 'status' | 'hog'
> & {
hog?: HogFunctionType['hog'] // In the config it can be empty if using a template
+ sub_template_id?: HogFunctionSubTemplateIdType
}
-export type HogFunctionTemplateStatus = 'alpha' | 'beta' | 'stable' | 'free' | 'deprecated'
+export type HogFunctionSubTemplateType = Pick & {
+ id: HogFunctionSubTemplateIdType
+ name: string
+ description: string | null
+}
export type HogFunctionTemplateType = Pick<
HogFunctionType,
- 'id' | 'name' | 'description' | 'hog' | 'inputs_schema' | 'filters' | 'icon_url'
+ 'id' | 'name' | 'description' | 'hog' | 'inputs_schema' | 'filters' | 'icon_url' | 'masking'
> & {
status: HogFunctionTemplateStatus
+ sub_templates?: HogFunctionSubTemplateType[]
}
export type HogFunctionIconResponse = {
diff --git a/posthog/api/hog_function_template.py b/posthog/api/hog_function_template.py
index 6d04fa69ced6c..c2becf5ab4afe 100644
--- a/posthog/api/hog_function_template.py
+++ b/posthog/api/hog_function_template.py
@@ -7,7 +7,7 @@
from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.cdp.templates import HOG_FUNCTION_TEMPLATES
-from posthog.cdp.templates.hog_function_template import HogFunctionTemplate
+from posthog.cdp.templates.hog_function_template import HogFunctionTemplate, HogFunctionSubTemplate
from posthog.models.hog_functions.hog_function import HogFunction
from posthog.permissions import PostHogFeatureFlagPermission
from rest_framework_dataclasses.serializers import DataclassSerializer
@@ -16,7 +16,14 @@
logger = structlog.get_logger(__name__)
+class HogFunctionSubTemplateSerializer(DataclassSerializer):
+ class Meta:
+ dataclass = HogFunctionSubTemplate
+
+
class HogFunctionTemplateSerializer(DataclassSerializer):
+ sub_templates = HogFunctionSubTemplateSerializer(many=True, required=False)
+
class Meta:
dataclass = HogFunctionTemplate
diff --git a/posthog/api/test/test_hog_function.py b/posthog/api/test/test_hog_function.py
index f62a96714a311..1fce31f85e6b9 100644
--- a/posthog/api/test/test_hog_function.py
+++ b/posthog/api/test/test_hog_function.py
@@ -171,6 +171,8 @@ def test_creates_with_template_id(self, *args):
"inputs_schema": template_webhook.inputs_schema,
"hog": template_webhook.hog,
"filters": None,
+ "masking": None,
+ "sub_templates": response.json()["template"]["sub_templates"],
}
def test_deletes_via_update(self, *args):
diff --git a/posthog/cdp/templates/hog_function_template.py b/posthog/cdp/templates/hog_function_template.py
index 9bfc7faf7cb13..ba7ee16e34027 100644
--- a/posthog/cdp/templates/hog_function_template.py
+++ b/posthog/cdp/templates/hog_function_template.py
@@ -1,5 +1,6 @@
import dataclasses
-from typing import Literal, Optional, TYPE_CHECKING
+from typing import Literal, Optional, get_args, TYPE_CHECKING
+
if TYPE_CHECKING:
from posthog.models.plugin import PluginConfig
@@ -7,6 +8,21 @@
PluginConfig = None
+SubTemplateId = Literal["early_access_feature_enrollment", "survey_response"]
+
+SUB_TEMPLATE_ID: tuple[SubTemplateId, ...] = get_args(SubTemplateId)
+
+
+@dataclasses.dataclass(frozen=True)
+class HogFunctionSubTemplate:
+ id: SubTemplateId
+ name: str
+ description: Optional[str] = None
+ filters: Optional[dict] = None
+ masking: Optional[dict] = None
+ inputs: Optional[dict] = None
+
+
@dataclasses.dataclass(frozen=True)
class HogFunctionTemplate:
status: Literal["alpha", "beta", "stable", "free"]
@@ -15,7 +31,9 @@ class HogFunctionTemplate:
description: str
hog: str
inputs_schema: list[dict]
+ sub_templates: Optional[list[HogFunctionSubTemplate]] = None
filters: Optional[dict] = None
+ masking: Optional[dict] = None
icon_url: Optional[str] = None
@@ -26,3 +44,32 @@ class HogFunctionTemplateMigrator:
def migrate(cls, obj: PluginConfig) -> dict:
# Return a dict for the template of a new HogFunction
raise NotImplementedError()
+
+
+SUB_TEMPLATE_COMMON: dict[SubTemplateId, HogFunctionSubTemplate] = {
+ "survey_response": HogFunctionSubTemplate(
+ id="survey_response",
+ name="Survey Response",
+ filters={
+ "events": [
+ {
+ "id": "survey sent",
+ "type": "events",
+ "properties": [
+ {
+ "key": "$survey_response",
+ "type": "event",
+ "value": "is_set",
+ "operator": "is_set",
+ },
+ ],
+ }
+ ]
+ },
+ ),
+ "early_access_feature_enrollment": HogFunctionSubTemplate(
+ id="early_access_feature_enrollment",
+ name="Early Access Feature Enrollment",
+ filters={"events": [{"id": "$feature_enrollment_update", "type": "events"}]},
+ ),
+}
diff --git a/posthog/cdp/templates/slack/template_slack.py b/posthog/cdp/templates/slack/template_slack.py
index dce9872bc3b89..68d4f93274090 100644
--- a/posthog/cdp/templates/slack/template_slack.py
+++ b/posthog/cdp/templates/slack/template_slack.py
@@ -1,4 +1,4 @@
-from posthog.cdp.templates.hog_function_template import HogFunctionTemplate
+from posthog.cdp.templates.hog_function_template import HogFunctionTemplate, HogFunctionSubTemplate, SUB_TEMPLATE_COMMON
template: HogFunctionTemplate = HogFunctionTemplate(
status="free",
@@ -102,4 +102,68 @@
"required": False,
},
],
+ sub_templates=[
+ HogFunctionSubTemplate(
+ id="early_access_feature_enrollment",
+ name="Post to Slack on feature enrollment",
+ description="Posts a message to Slack when a user enrolls or un-enrolls in an early access feature",
+ filters=SUB_TEMPLATE_COMMON["early_access_feature_enrollment"].filters,
+ inputs={
+ "text": "*{person.name}* {event.properties.$feature_enrollment ? 'enrolled in' : 'un-enrolled from'} the early access feature for '{event.properties.$feature_flag}'",
+ "blocks": [
+ {
+ "text": {
+ "text": "*{person.name}* {event.properties.$feature_enrollment ? 'enrolled in' : 'un-enrolled from'} the early access feature for '{event.properties.$feature_flag}'",
+ "type": "mrkdwn",
+ },
+ "type": "section",
+ },
+ {
+ "type": "actions",
+ "elements": [
+ {
+ "url": "{person.url}",
+ "text": {"text": "View Person in PostHog", "type": "plain_text"},
+ "type": "button",
+ },
+ # NOTE: It would be nice to have a link to the EAF but the event needs more info
+ ],
+ },
+ ],
+ },
+ ),
+ HogFunctionSubTemplate(
+ id="survey_response",
+ name="Post to Slack on survey response",
+ description="Posts a message to Slack when a user responds to a survey",
+ filters=SUB_TEMPLATE_COMMON["survey_response"].filters,
+ inputs={
+ "text": "*{person.name}* responded to survey *{event.properties.$survey_name}*",
+ "blocks": [
+ {
+ "text": {
+ "text": "*{person.name}* responded to survey *{event.properties.$survey_name}*",
+ "type": "mrkdwn",
+ },
+ "type": "section",
+ },
+ {
+ "type": "actions",
+ "elements": [
+ {
+ "url": "{project.url}/surveys/{event.properties.$survey_id}",
+ "text": {"text": "View Survey", "type": "plain_text"},
+ "type": "button",
+ },
+ {
+ "url": "{person.url}",
+ "text": {"text": "View Person", "type": "plain_text"},
+ "type": "button",
+ },
+ ],
+ },
+ ],
+ },
+ ),
+ ],
)
diff --git a/posthog/cdp/templates/webhook/template_webhook.py b/posthog/cdp/templates/webhook/template_webhook.py
index b99e7f9f31f79..cf2acd665619a 100644
--- a/posthog/cdp/templates/webhook/template_webhook.py
+++ b/posthog/cdp/templates/webhook/template_webhook.py
@@ -1,4 +1,4 @@
-from posthog.cdp.templates.hog_function_template import HogFunctionTemplate
+from posthog.cdp.templates.hog_function_template import SUB_TEMPLATE_COMMON, HogFunctionSubTemplate, HogFunctionTemplate
template: HogFunctionTemplate = HogFunctionTemplate(
@@ -82,4 +82,16 @@
"default": False,
},
],
+ sub_templates=[
+ HogFunctionSubTemplate(
+ id="early_access_feature_enrollment",
+ name="HTTP Webhook on feature enrollment",
+ filters=SUB_TEMPLATE_COMMON["early_access_feature_enrollment"].filters,
+ ),
+ HogFunctionSubTemplate(
+ id="survey_response",
+ name="HTTP Webhook on survey response",
+ filters=SUB_TEMPLATE_COMMON["survey_response"].filters,
+ ),
+ ],
)