Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(surveys): auto opt in and out surveys for users #18080

Merged
merged 17 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified frontend/__snapshots__/scenes-app-surveys--surveys-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 0 additions & 20 deletions frontend/src/scenes/ingestion/panels/SuperpowersPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
const { completeOnboarding } = useActions(ingestionLogic)
const [sessionRecordingsChecked, setSessionRecordingsChecked] = useState(true)
const [autocaptureChecked, setAutocaptureChecked] = useState(true)
const [surveysChecked, setSurveysChecked] = useState(true)

return (
<CardContainer
Expand All @@ -23,7 +22,7 @@
capture_console_log_opt_in: sessionRecordingsChecked,
capture_performance_opt_in: sessionRecordingsChecked,
autocapture_opt_out: !autocaptureChecked,
surveys_opt_in: surveysChecked,

Check failure on line 25 in frontend/src/scenes/ingestion/panels/SuperpowersPanel.tsx

View workflow job for this annotation

GitHub Actions / Code quality checks

Cannot find name 'surveysChecked'.
})
if (!showBillingStep) {
completeOnboarding()
Expand Down Expand Up @@ -84,25 +83,6 @@
directly in your code snippet.
</p>
</div>
<div>
<LemonSwitch
data-attr="opt-in-surveys-switch"
onChange={(checked) => {
setSurveysChecked(checked)
}}
label="Get qualitative feedback from your users"
fullWidth={true}
labelClassName={'text-base font-semibold'}
checked={surveysChecked}
/>
<p className="prompt-text ml-0">
Collect feedback from your users directly in your product.{' '}
<Link to={'https://posthog.com/docs/surveys'} target="blank">
Learn more
</Link>{' '}
about Surveys.
</p>
</div>
</CardContainer>
)
}
37 changes: 24 additions & 13 deletions frontend/src/scenes/surveys/Surveys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ export function Surveys(): JSX.Element {

const { user } = useValues(userLogic)
const { currentTeam } = useValues(teamLogic)
const surveysPopupDisabled = currentTeam && !currentTeam?.surveys_opt_in
const { updateCurrentTeam } = useActions(teamLogic)
const surveysPopupDisabled =
liyiy marked this conversation as resolved.
Show resolved Hide resolved
currentTeam && !currentTeam?.surveys_opt_in && surveys.some((s) => s.start_date && !s.end_date)
liyiy marked this conversation as resolved.
Show resolved Hide resolved

const [tab, setSurveyTab] = useState(SurveysTabs.Active)
const shouldShowEmptyState = !surveysLoading && surveys.length === 0
Expand Down Expand Up @@ -97,13 +99,6 @@ export function Surveys(): JSX.Element {
>
New survey
</LemonButtonWithSideAction>
<LemonButton
type="secondary"
icon={<IconSettings />}
onClick={() => openSurveysSettingsDialog()}
>
Configure
</LemonButton>
</>
}
caption={
Expand Down Expand Up @@ -146,7 +141,8 @@ export function Surveys(): JSX.Element {
}}
className="mb-2"
>
Survey popovers are currently disabled for this project.
Survey popovers are currently disabled for this project but there are active surveys running.
Re-enable them in the settings.
liyiy marked this conversation as resolved.
Show resolved Hide resolved
liyiy marked this conversation as resolved.
Show resolved Hide resolved
</LemonBanner>
) : null}
</div>
Expand Down Expand Up @@ -306,14 +302,26 @@ export function Surveys(): JSX.Element {
<LemonButton
status="stealth"
fullWidth
onClick={() =>
onClick={() => {
updateSurvey({
id: survey.id,
updatePayload: {
end_date: dayjs().toISOString(),
},
})
}
if (currentTeam && currentTeam.surveys_opt_in) {
const activeSurveys = surveys.filter(
(s) => s.start_date && !s.end_date
)
if (
activeSurveys.filter(
(s) => s.id !== survey.id
).length === 0
) {
updateCurrentTeam({ surveys_opt_in: false })
}
}
}}
liyiy marked this conversation as resolved.
Show resolved Hide resolved
>
Stop survey
</LemonButton>
Expand All @@ -322,12 +330,15 @@ export function Surveys(): JSX.Element {
<LemonButton
status="stealth"
fullWidth
onClick={() =>
onClick={() => {
if (currentTeam && !currentTeam.surveys_opt_in) {
updateCurrentTeam({ surveys_opt_in: true })
}
updateSurvey({
id: survey.id,
updatePayload: { end_date: null },
})
}
}}
liyiy marked this conversation as resolved.
Show resolved Hide resolved
>
Resume survey
</LemonButton>
Expand Down
27 changes: 25 additions & 2 deletions frontend/src/scenes/surveys/surveyLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
NewSurvey,
} from './constants'
import { sanitize } from 'dompurify'
import { teamLogic } from 'scenes/teamLogic'

export enum SurveyEditSection {
Steps = 'steps',
Expand Down Expand Up @@ -94,6 +95,8 @@ export const surveyLogic = kea<surveyLogicType>([
actions: [
surveysLogic,
['loadSurveys'],
teamLogic,
['updateCurrentTeam'],
eventUsageLogic,
[
'reportSurveyCreated',
Expand All @@ -105,7 +108,14 @@ export const surveyLogic = kea<surveyLogicType>([
'reportSurveyViewed',
],
],
values: [enabledFlagLogic, ['featureFlags as enabledFlags']],
values: [
enabledFlagLogic,
['featureFlags as enabledFlags'],
surveysLogic,
['surveys'],
teamLogic,
['currentTeam'],
],
})),
actions({
setSurveyMissing: true,
Expand All @@ -130,6 +140,7 @@ export const surveyLogic = kea<surveyLogicType>([
setSelectedQuestion: (idx: number | null) => ({ idx }),
setSelectedSection: (section: SurveyEditSection | null) => ({ section }),
resetTargeting: true,
setSurveysOptIn: (optIn: boolean) => ({ optIn }),
}),
loaders(({ props, actions, values }) => ({
survey: {
Expand Down Expand Up @@ -389,7 +400,7 @@ export const surveyLogic = kea<surveyLogicType>([
},
},
})),
listeners(({ actions }) => ({
listeners(({ actions, values }) => ({
createSurveySuccess: ({ survey }) => {
lemonToast.success(<>Survey {survey.name} created</>)
actions.loadSurveys()
Expand All @@ -405,10 +416,19 @@ export const surveyLogic = kea<surveyLogicType>([
launchSurveySuccess: ({ survey }) => {
lemonToast.success(<>Survey {survey.name} launched</>)
actions.loadSurveys()
if (!values.currentTeam?.surveys_opt_in) {
actions.setSurveysOptIn(true)
}
liyiy marked this conversation as resolved.
Show resolved Hide resolved
actions.reportSurveyLaunched(survey)
},
stopSurveySuccess: ({ survey }) => {
actions.loadSurveys()
if (values.currentTeam?.surveys_opt_in) {
const activeSurveys = values.surveys.filter((s) => s.start_date && !s.end_date)
if (activeSurveys.filter((s) => s.id !== survey.id).length === 0) {
actions.setSurveysOptIn(false)
}
}
actions.reportSurveyStopped(survey)
},
resumeSurveySuccess: ({ survey }) => {
Expand All @@ -430,6 +450,9 @@ export const surveyLogic = kea<surveyLogicType>([
actions.setSurveyValue('conditions', NEW_SURVEY.conditions)
actions.setSurveyValue('remove_targeting_flag', true)
},
setSurveysOptIn: ({ optIn }) => {
actions.updateCurrentTeam({ surveys_opt_in: optIn })
},
liyiy marked this conversation as resolved.
Show resolved Hide resolved
})),
reducers({
isEditingSurvey: [
Expand Down
37 changes: 37 additions & 0 deletions posthog/api/test/test_survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,43 @@ def test_updating_survey_name_validates(self):
updated_survey_deletes_targeting_flag.json()["detail"] == "There is already another survey with this name."
)

def test_enable_surveys_opt_in(self):
Survey.objects.create(
team=self.team,
created_by=self.user,
name="Survey 1",
type="popover",
questions=[{"type": "open", "question": "What's a survey?"}],
start_date=datetime.now() - timedelta(days=2),
end_date=datetime.now() - timedelta(days=1),
)
assert self.team.surveys_opt_in is None
Survey.objects.create(
team=self.team,
created_by=self.user,
name="Survey 2",
type="popover",
questions=[{"type": "open", "question": "What's a hedgehog?"}],
start_date=datetime.now() - timedelta(days=2),
)
assert self.team.surveys_opt_in is True

def test_disable_surveys_opt_in(self):
liyiy marked this conversation as resolved.
Show resolved Hide resolved
survey = Survey.objects.create(
team=self.team,
created_by=self.user,
name="Survey 2",
type="popover",
questions=[{"type": "open", "question": "What's a hedgehog?"}],
start_date=datetime.now() - timedelta(days=2),
)
assert self.team.surveys_opt_in is True
self.client.patch(
f"/api/projects/{self.team.id}/surveys/{survey.id}/",
data={"end_date": datetime.now() - timedelta(days=1)},
)
assert self.team.surveys_opt_in is False
liyiy marked this conversation as resolved.
Show resolved Hide resolved


class TestSurveyQuestionValidation(APIBaseTest):
def test_create_basic_survey_question_validation(self):
Expand Down
27 changes: 15 additions & 12 deletions posthog/models/feedback/survey.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.db import models
from django.db.models.signals import post_save
from posthog.models.signals import mutable_receiver
from posthog.models.utils import UUIDModel


Expand Down Expand Up @@ -35,21 +37,9 @@ class Meta:
related_query_name="survey_targeting_flag",
)
type: models.CharField = models.CharField(max_length=40, choices=SurveyType.choices)

# { url: 'posthog.com/feature', selector: null, triggers: [{}] #similar to cohort behavioral filters for now?}
conditions: models.JSONField = models.JSONField(blank=True, null=True)

# class SurveyQuestionType(models.TextChoices):
# OPEN = "open"
# MULTIPLE_CHOICE_SINGLE = "multiple"
# NPS = "nps"
# RATING = "rating"
# [ { type: 'open', question: "leave feedback plz?"}, { type: 'rating', question: null}, { type: 'multiple_choice', question: "multiple choice question?", choices: ["choice 1", "choice 2"]} ]
questions: models.JSONField = models.JSONField(blank=True, null=True)

# { background_color: "white", button_color: "orange", text_color: "", position, etc... }
appearance: models.JSONField = models.JSONField(blank=True, null=True)

created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
created_by: models.ForeignKey = models.ForeignKey(
"posthog.User",
Expand All @@ -62,3 +52,16 @@ class Meta:
end_date: models.DateTimeField = models.DateTimeField(null=True)
updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)
archived: models.BooleanField = models.BooleanField(default=False)


@mutable_receiver(post_save, sender=Survey)
liyiy marked this conversation as resolved.
Show resolved Hide resolved
def update_surveys_opt_in(sender, instance, **kwargs):
active_surveys_count = Survey.objects.filter(
team_id=instance.team_id, start_date__isnull=False, end_date__isnull=True, archived=False
).count()
if active_surveys_count > 0 and instance.team.surveys_opt_in is (False or None):
liyiy marked this conversation as resolved.
Show resolved Hide resolved
instance.team.surveys_opt_in = True
instance.team.save()
liyiy marked this conversation as resolved.
Show resolved Hide resolved
elif active_surveys_count == 0 and instance.team.surveys_opt_in is True:
instance.team.surveys_opt_in = False
instance.team.save()
Loading