feat(surveys): add slider question type#62158
Conversation
Adds a slider question type with configurable min, max, step, and optional prefix/suffix. Widget rendering requires a companion posthog-js update before the live preview works. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
| class SurveySliderQuestionSchemaSerializer(SurveyBaseQuestionSchemaSerializer): | ||
| type = serializers.ChoiceField(choices=["slider"], required=True) | ||
| min = serializers.FloatField(required=True, help_text="Minimum value of the slider.") | ||
| max = serializers.FloatField(required=True, help_text="Maximum value of the slider.") | ||
| step = serializers.FloatField(required=False, help_text="Step size for the slider.") | ||
| prefix = serializers.CharField(required=False, help_text="Prefix for the slider value (e.g., '$').") | ||
| suffix = serializers.CharField(required=False, help_text="Suffix for the slider value (e.g., '%').") |
There was a problem hiding this comment.
Bound labels silently dropped on save
lowerBoundLabel and upperBoundLabel are not declared in SurveySliderQuestionSchemaSerializer. The validation path calls question_serializer.data to build cleaned_question, which only returns declared fields. Any bound label values the user sets in the editor UI will be stripped before being persisted, so they will never be stored or returned by the API. The generated API schema (SurveySliderQuestionSchemaApi) confirms neither field is included. Contrast with SurveyRatingQuestionSchemaSerializer, which explicitly declares both fields.
Prompt To Fix With AI
This is a comment left during a code review.
Path: products/surveys/backend/api/survey.py
Line: 494-500
Comment:
**Bound labels silently dropped on save**
`lowerBoundLabel` and `upperBoundLabel` are not declared in `SurveySliderQuestionSchemaSerializer`. The validation path calls `question_serializer.data` to build `cleaned_question`, which only returns declared fields. Any bound label values the user sets in the editor UI will be stripped before being persisted, so they will never be stored or returned by the API. The generated API schema (`SurveySliderQuestionSchemaApi`) confirms neither field is included. Contrast with `SurveyRatingQuestionSchemaSerializer`, which explicitly declares both fields.
How can I resolve this? If you propose a fix, please make it concise.| type = serializers.ChoiceField(choices=["slider"], required=True) | ||
| min = serializers.FloatField(required=True, help_text="Minimum value of the slider.") | ||
| max = serializers.FloatField(required=True, help_text="Maximum value of the slider.") | ||
| step = serializers.FloatField(required=False, help_text="Step size for the slider.") |
There was a problem hiding this comment.
step is declared required=False here and therefore appears as optional in every generated API schema (step?: number). However, the validation code in validate_questions immediately raises "Slider questions require 'step'." when it is absent. Callers who follow the schema and omit step will receive a 400 error — the API contract is broken.
| step = serializers.FloatField(required=False, help_text="Step size for the slider.") | |
| step = serializers.FloatField(required=True, help_text="Step size for the slider.") |
Prompt To Fix With AI
This is a comment left during a code review.
Path: products/surveys/backend/api/survey.py
Line: 498
Comment:
`step` is declared `required=False` here and therefore appears as optional in every generated API schema (`step?: number`). However, the validation code in `validate_questions` immediately raises `"Slider questions require 'step'."` when it is absent. Callers who follow the schema and omit `step` will receive a 400 error — the API contract is broken.
```suggestion
step = serializers.FloatField(required=True, help_text="Step size for the slider.")
```
How can I resolve this? If you propose a fix, please make it concise.|
|
||
| const isSliderQuestion = (question: SurveyQuestion): question is SliderSurveyQuestion => |
There was a problem hiding this comment.
isSliderQuestion is also exported from questionTypeGuards.ts (added in the same PR). The local definition here is identical. SurveyEditQuestionRow.tsx could import isSliderQuestion from questionTypeGuards.ts instead, keeping the guard in one place — consistent with the existing isRatingQuestion and isLinkQuestion guards that exist in both files as pre-existing debt.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx
Line: 53-54
Comment:
**Duplicate type guard**
`isSliderQuestion` is also exported from `questionTypeGuards.ts` (added in the same PR). The local definition here is identical. `SurveyEditQuestionRow.tsx` could import `isSliderQuestion` from `questionTypeGuards.ts` instead, keeping the guard in one place — consistent with the existing `isRatingQuestion` and `isLinkQuestion` guards that exist in both files as pre-existing debt.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Reviews (2): Last reviewed commit: "Merge branch 'master' into feat/survey-s..." | Re-trigger Greptile |
|
Reviews (3): Last reviewed commit: "Merge branch 'master' into feat/survey-s..." | Re-trigger Greptile |
|
This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, please remove the |
Problem
Survey creators want to ask users for a numeric value across a configurable range — e.g. "what price would you pay?" — without the endpoint bias that rating scales and multiple choice introduce. The existing question types push respondents toward the lowest or highest option, which doesn't reflect what they actually think.
Closes #60079
Changes
Adds a new
sliderquestion type with min, max, step, and optional prefix/suffix (e.g.$,%) and lower/upper bound labels.SurveySliderQuestionSchemaSerializerand validation invalidate_questionsenforcingmin < max, positivestep, andstep <= range.IconTuning. Config UI lets creators set min/max/step/prefix/suffix and bound labels.SliderQuestionVizshows average/median/min/max stat cards plus a value histogram. Responses are bucketed viafloor(value / step) * stepat query time and cast throughtoFloat64OrNull.The widget rendering itself depends on the companion PostHog/posthog-js#3774. Until that ships and the dependency is bumped, the in-editor live preview is empty for slider questions — the editor and results views work end-to-end on their own.
How did you test this code?
I'm an agent (Claude Opus 4.7) — only listing automated tests I actually ran.
products/surveys/backend/api/test/test_survey.py::TestSurveyQuestionValidation— slider acceptance + validation rejection cases (parameterized for min/max/step edge cases)frontend/src/scenes/surveys/surveyLogic.test.ts—setDefaultForQuestionTypeapplies slider defaultsfrontend/src/scenes/surveys/utils.test.ts—buildAggregateQueryemits the expected slider branch (Float64 cast, floor-bucket grouping)No manual UI testing was performed — the live preview is blocked on posthog-js anyway.
Automatic notifications
Docs update
🤖 Agent context
Built with Claude Code (Opus 4.7) using a TDD workflow — failing test, minimal implementation, repeat — across backend validation, kea logic, editor UI, results query, and visualization phases.
Two notable decisions: (1) slider responses are cast to
Float64at query time rather than introducing a typed column, avoiding a ClickHouse migration; (2) results visualization is its own component rather than reusing rating's, since slider has continuous numeric data with summary stats rather than fixed buckets with branching/NPS.Branching, validation rules, and SDK widget rendering are intentionally out of scope — branching can be added later if needed, and the widget rendering ships in the companion PostHog/posthog-js#3774.