diff --git a/frontend/__snapshots__/scenes-app-surveys--new-survey.png b/frontend/__snapshots__/scenes-app-surveys--new-survey.png
index 5671de85e8c52..9033d58fe25e7 100644
Binary files a/frontend/__snapshots__/scenes-app-surveys--new-survey.png and b/frontend/__snapshots__/scenes-app-surveys--new-survey.png differ
diff --git a/frontend/src/scenes/surveys/Survey.tsx b/frontend/src/scenes/surveys/Survey.tsx
index 100177211fb7b..10a67d4e96aea 100644
--- a/frontend/src/scenes/surveys/Survey.tsx
+++ b/frontend/src/scenes/surveys/Survey.tsx
@@ -5,12 +5,14 @@ import { Form, Group } from 'kea-forms'
import { PageHeader } from 'lib/components/PageHeader'
import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton'
import {
+ LemonBanner,
LemonButton,
LemonCheckbox,
LemonCollapse,
LemonDivider,
LemonInput,
LemonSelect,
+ LemonTabs,
LemonTextArea,
Link,
} from '@posthog/lemon-ui'
@@ -36,6 +38,7 @@ import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic'
import { defaultSurveyFieldValues, defaultSurveyAppearance, NewSurvey, SurveyUrlMatchTypeLabels } from './constants'
import { FEATURE_FLAGS } from 'lib/constants'
import { FeatureFlagReleaseConditions } from 'scenes/feature-flags/FeatureFlagReleaseConditions'
+import { CodeEditor } from 'lib/components/CodeEditors'
import { NotFound } from 'lib/components/NotFound'
export const scene: SceneExport = {
@@ -68,9 +71,16 @@ export function SurveyComponent({ id }: { id?: string } = {}): JSX.Element {
}
export function SurveyForm({ id }: { id: string }): JSX.Element {
- const { survey, surveyLoading, isEditingSurvey, hasTargetingFlag, urlMatchTypeValidationError } =
- useValues(surveyLogic)
- const { loadSurvey, editingSurvey, setSurveyValue, setDefaultForQuestionType } = useActions(surveyLogic)
+ const {
+ survey,
+ surveyLoading,
+ isEditingSurvey,
+ hasTargetingFlag,
+ urlMatchTypeValidationError,
+ writingHTMLDescription,
+ } = useValues(surveyLogic)
+ const { loadSurvey, editingSurvey, setSurveyValue, setDefaultForQuestionType, setWritingHTMLDescription } =
+ useActions(surveyLogic)
const { featureFlags } = useValues(enabledFeaturesLogic)
return (
@@ -153,7 +163,7 @@ export function SurveyForm({ id }: { id: string }): JSX.Element {
),
content: (
- <>
+
{
@@ -216,7 +226,72 @@ export function SurveyForm({ id }: { id: string }): JSX.Element {
)}
-
+ {({ value, onChange }) => (
+ <>
+
+ setWritingHTMLDescription(key === 'html')
+ }
+ tabs={[
+ {
+ key: 'text',
+ label: (
+ Text
+ ),
+ content: (
+ onChange(v)}
+ />
+ ),
+ },
+ {
+ key: 'html',
+ label: (
+ HTML
+ ),
+ content: (
+
+
+ onChange(v ?? '')
+ }
+ height={150}
+ options={{
+ minimap: { enabled: false },
+ wordWrap: 'on',
+ scrollBeyondLastLine: false,
+ automaticLayout: true,
+ fixedOverflowWidgets: true,
+ lineNumbers: 'off',
+ glyphMargin: false,
+ folding: false,
+ }}
+ />
+
+ ),
+ },
+ ]}
+ />
+ {question.description &&
+ question.description
+ ?.toLowerCase()
+ .includes('check?"},
+ {
+ "type": "link",
+ "link": "bazinga.com",
+ "question": "What do you think of the new notebooks feature?",
+ },
+ ],
+ },
+ format="json",
+ )
+ response_data = response.json()
+ assert response.status_code == status.HTTP_201_CREATED, response_data
+ assert Survey.objects.filter(id=response_data["id"]).exists()
+ assert response_data["name"] == "Notebooks beta release survey"
+ assert response_data["description"] == "Get feedback on the new notebooks feature"
+ assert response_data["type"] == "popover"
+ assert response_data["questions"] == [
+ {"type": "open", "question": "What up?", "description": "check?"},
+ {
+ "type": "link",
+ "link": "bazinga.com",
+ "question": "What do you think of the new notebooks feature?",
+ },
+ ]
+ assert response_data["created_by"]["id"] == self.user.id
+
+ def test_update_basic_survey_question_validation(self):
+
+ basic_survey = self.client.post(
+ f"/api/projects/{self.team.id}/surveys/",
+ data={
+ "name": "survey without targeting",
+ "type": "popover",
+ },
+ format="json",
+ ).json()
+
+ response = self.client.patch(
+ f"/api/projects/{self.team.id}/surveys/{basic_survey['id']}/",
+ data={
+ "name": "Notebooks beta release survey",
+ "description": "Get feedback on the new notebooks feature",
+ "type": "popover",
+ "questions": [
+ {"type": "open", "question": "What up?", "description": "check?"},
+ {
+ "type": "link",
+ "link": "bazinga.com",
+ "question": "What do you think of the new notebooks feature?",
+ },
+ ],
+ },
+ format="json",
+ )
+ response_data = response.json()
+ assert response.status_code == status.HTTP_200_OK, response_data
+ assert Survey.objects.filter(id=response_data["id"]).exists()
+ assert response_data["name"] == "Notebooks beta release survey"
+ assert response_data["description"] == "Get feedback on the new notebooks feature"
+ assert response_data["type"] == "popover"
+ assert response_data["questions"] == [
+ {"type": "open", "question": "What up?", "description": "check?"},
+ {
+ "type": "link",
+ "link": "bazinga.com",
+ "question": "What do you think of the new notebooks feature?",
+ },
+ ]
+ assert response_data["created_by"]["id"] == self.user.id
+
+ def test_cleaning_empty_questions(self):
+ response = self.client.post(
+ f"/api/projects/{self.team.id}/surveys/",
+ data={
+ "name": "Notebooks beta release survey",
+ "description": "Get feedback on the new notebooks feature",
+ "type": "popover",
+ "questions": [],
+ },
+ format="json",
+ )
+ response_data = response.json()
+ assert response.status_code == status.HTTP_201_CREATED, response_data
+ assert Survey.objects.filter(id=response_data["id"]).exists()
+ assert response_data["name"] == "Notebooks beta release survey"
+ assert response_data["questions"] == []
+
+ def test_validate_question_with_missing_text(self):
+ response = self.client.post(
+ f"/api/projects/{self.team.id}/surveys/",
+ data={
+ "name": "Notebooks beta release survey",
+ "description": "Get feedback on the new notebooks feature",
+ "type": "popover",
+ "questions": [{"type": "open"}],
+ },
+ format="json",
+ )
+ response_data = response.json()
+ assert response.status_code == status.HTTP_400_BAD_REQUEST, response_data
+ assert response_data["detail"] == "Question text is required"
+
+ def test_validate_malformed_questions(self):
+ response = self.client.post(
+ f"/api/projects/{self.team.id}/surveys/",
+ data={
+ "name": "Notebooks beta release survey",
+ "description": "Get feedback on the new notebooks feature",
+ "type": "popover",
+ "questions": "",
+ },
+ format="json",
+ )
+ response_data = response.json()
+ assert response.status_code == status.HTTP_400_BAD_REQUEST, response_data
+ assert response_data["detail"] == "Questions must be a list of objects"
+
+ def test_validate_malformed_questions_as_string(self):
+ response = self.client.post(
+ f"/api/projects/{self.team.id}/surveys/",
+ data={
+ "name": "Notebooks beta release survey",
+ "description": "Get feedback on the new notebooks feature",
+ "type": "popover",
+ "questions": "this is my question",
+ },
+ format="json",
+ )
+ response_data = response.json()
+ assert response.status_code == status.HTTP_400_BAD_REQUEST, response_data
+ assert response_data["detail"] == "Questions must be a list of objects"
+
+ def test_validate_malformed_questions_as_array_of_strings(self):
+ response = self.client.post(
+ f"/api/projects/{self.team.id}/surveys/",
+ data={
+ "name": "Notebooks beta release survey",
+ "description": "Get feedback on the new notebooks feature",
+ "type": "popover",
+ "questions": ["this is my question"],
+ },
+ format="json",
+ )
+ response_data = response.json()
+ assert response.status_code == status.HTTP_400_BAD_REQUEST, response_data
+ assert response_data["detail"] == "Questions must be a list of objects"
+
+
class TestSurveysAPIList(BaseTest, QueryMatchingTest):
def setUp(self):
cache.clear()
diff --git a/posthog/models/feature_flag/flag_matching.py b/posthog/models/feature_flag/flag_matching.py
index 2d2f76bb41595..0ee0deb83fb6e 100644
--- a/posthog/models/feature_flag/flag_matching.py
+++ b/posthog/models/feature_flag/flag_matching.py
@@ -496,6 +496,7 @@ def hashed_identifier(self, feature_flag: FeatureFlag) -> Optional[str]:
return self.hash_key_overrides[feature_flag.key]
return self.distinct_id
else:
+ # TODO: Don't use the cache if self.groups is empty, since that means no groups provided anyway
# :TRICKY: If aggregating by groups
group_type_name = self.cache.group_type_index_to_name.get(feature_flag.aggregation_group_type_index)
group_key = self.groups.get(group_type_name) # type: ignore
diff --git a/requirements.in b/requirements.in
index 673f7e045a34e..fbad668ecd89f 100644
--- a/requirements.in
+++ b/requirements.in
@@ -86,3 +86,4 @@ more-itertools==9.0.0
django-two-factor-auth==1.14.0
phonenumberslite==8.13.6
openai==0.27.8
+nh3==0.2.14
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index beb9261497bb4..ca2dc7c9389be 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -301,6 +301,8 @@ multidict==6.0.2
# yarl
mypy-boto3-s3==1.26.127
# via boto3-stubs
+nh3==0.2.14
+ # via -r requirements.in
numpy==1.23.3
# via
# -r requirements.in