feat(web): add an "other" answer to the post-export survey - #7074
feat(web): add an "other" answer to the post-export survey#7074open-design-crew[bot] wants to merge 3 commits into
Conversation
The eight choices cover what users already tell us most often, but they cannot cover what we have not heard yet β and the people whose problem is not on the list are exactly the ones worth hearing from. Picking "Something else" opens a text field instead of finishing the survey. The cost is paid only by the people who need it: everyone whose answer is on the list still finishes in one tap. This is the open-ended question we cut earlier, put back behind a door rather than in everyone way. Wire shape follows PostHog open-choice convention β the response recorded is the text they typed. Picking "Something else" and typing nothing still reports the choice itself, because "none of these fit" is an answer and dropping it would turn those people into non-responders. Skip disappears once the choice is made; it would only have been a second way to submit.
|
Quick heads-up: this PR has user-visible survey flow changes, so it needs a manual QA pass before merge β please hold off merging it until it's merge-ready and QA has been looped in. Also, could you add a |
PerishCode
left a comment
There was a problem hiding this comment.
I found one merge-safe layout regression in the new Other-answer submit state. The analytics wire behavior and locale coverage otherwise look consistent, and the focused tests, web typecheck, and repository guard pass.
π Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.| would only be a second way to submit. Submit replaces it, and an | ||
| empty field still reports "none of these fit". */} | ||
| {picked === OTHER_INDEX ? ( | ||
| <Button variant="primary" className={styles.submit} onClick={submitOther}> |
There was a problem hiding this comment.
Keep Send in the right-aligned action slot that Skip occupied. This Button uses styles.submit, but that rule only changes sizing; the replaced styles.skip rule is what supplied margin-left: auto. Consequently, choosing Something else moves the primary action next to the 2/2 counter instead of replacing Skip at the right edge, producing a visible layout jump in the new state. Add margin-left: auto to .submit (or put the auto margin on the footer's action slot) so both branches retain the same alignment.
The green ring competed with the brand fill on the selected chip directly above it β two green things one under the other, neither reading as primary. A neutral border marks focus without joining that fight, and the inset fill still separates the field from the card.
PerishCode
left a comment
There was a problem hiding this comment.
The open-choice analytics behavior, locale coverage, focused tests, web typecheck, and repository guard all check out. One merge-safe layout regression from the prior head remains unresolved on this head.
π Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.| would only be a second way to submit. Submit replaces it, and an | ||
| empty field still reports "none of these fit". */} | ||
| {picked === OTHER_INDEX ? ( | ||
| <Button variant="primary" className={styles.submit} onClick={submitOther}> |
There was a problem hiding this comment.
Keep Send in the same right-aligned footer action slot as Skip. This branch replaces the element carrying styles.skip, whose CSS supplies margin-left: auto, with styles.submit, which only controls sizing. As a result, selecting Something else moves the action beside the 2/2 counter instead of replacing Skip at the right edge, causing a visible layout jump. Add margin-left: auto to .submit, or move that margin to a shared footer action wrapper so both branches align identically.
Submit replaces Skip in the footer, but only Skip carried the margin-left:auto that pushes it right β so the moment the text field opened, the primary action collapsed back against the step counter and jumped from the right edge to the left. Measured from the card edge, both now sit at the same 17px gap.
PerishCode
left a comment
There was a problem hiding this comment.
The open-choice analytics shape, empty-answer fallback, locale coverage, and footer-alignment follow-up are consistent on this head. I found one merge-safe accessibility gap in the newly added free-text interaction. Local focused tests and typecheck could not be rerun because this prepared worktree has no installed workspace dependencies.
π Powered by Looper Β· runner=reviewer Β· agent=codex Β· An autonomous AI dev team for your GitHub repos.| className={styles.textarea} | ||
| value={otherText} | ||
| onChange={(event) => setOtherText(event.target.value)} | ||
| placeholder={t('experienceSurvey.otherPlaceholder')} |
There was a problem hiding this comment.
Give the new free-text control a programmatic accessible name. The textarea is auto-focused as soon as Something else is selected, but its only description is placeholder; placeholder text disappears once typing starts and is not a persistent label for assistive technology, so screen-reader users can land on an unnamed multiline edit control without knowing what response it accepts. Associate a visible <label> via htmlFor/id, or at minimum add an aria-label/aria-labelledby using the localized prompt, and add a focused component assertion that the textbox has that accessible name.
xxiaoxiong
left a comment
There was a problem hiding this comment.
Well-executed "Other" escape hatch. The PostHog open-choice convention is the right call β reporting the typed text as the response itself rather than "Other" + sidecar text, and falling back to "Other" for empty input preserves "none of these fit" as a real answer instead of silently understating it. Three tests nail the three cases (typed-text, empty-fallback, typed-over-stale-index), and all 13 i18n locales + the Dict type are kept in lockstep. Approved.
β¦add an "other" answer (nexu-io#7117) * feat(web): add an "other" answer with a free-text field to the survey The eight choices cover what users already tell us most often, but they cannot cover what we have not heard yet β and the people whose problem is not on the list are exactly the ones worth hearing from. Picking "Something else" opens a text field instead of finishing the survey. The cost is paid only by the people who need it: everyone whose answer is on the list still finishes in one tap. This is the open-ended question we cut earlier, put back behind a door rather than in everyone way. Wire shape follows PostHog open-choice convention β the response recorded is the text they typed. Picking "Something else" and typing nothing still reports the choice itself, because "none of these fit" is an answer and dropping it would turn those people into non-responders. Skip disappears once the choice is made; it would only have been a second way to submit. * style(web): use a neutral focus border on the survey text field The green ring competed with the brand fill on the selected chip directly above it β two green things one under the other, neither reading as primary. A neutral border marks focus without joining that fight, and the inset fill still separates the field from the card. * fix(web): keep the survey submit button on the footer right edge Submit replaces Skip in the footer, but only Skip carried the margin-left:auto that pushes it right β so the moment the text field opened, the primary action collapsed back against the step counter and jumped from the right edge to the left. Measured from the card edge, both now sit at the same 17px gap. * feat(web): ask the experience survey after a delivered artifact, not an export The post-export survey could only reach people who exported. Over 30 days ~13k users exported while ~37k produced an artifact, so roughly two thirds of the people who got real work out of the product were never asked β and the ones who tried it, got something, and left without exporting are the ones worth hearing from most. The card is now armed by a delivered design run (resolveDesignDeliveryOutcome === 'delivered'), from the second such delivery on. The first delivery measures a first impression rather than an experience, and lands at the one moment the user most wants to look at what they just got; waiting for the second costs almost no reach because deliveries repeat (~13 per user per 30 days). Because a delivery is usually mid-session rather than end-of-session, the 3s delay now bails out if the user starts writing their next prompt. 'beforeinput' rather than 'keydown' so IME composition counts as typing. A dropped chance is taken by the next delivery, so the card lands after the turn the user actually stopped on. Export no longer arms anything; the three FileViewer call sites are gone. Every survey event carries trigger: 'post_generation' so the two regimes stay distinguishable in the events table. * feat(web): arm the experience survey on the first delivered artifact Drops the two-delivery threshold to one. Waiting for a second run bought a more considered opinion at the cost of everyone who produces one artifact and leaves β the users we understand least, and the ones this survey was widened to reach in the first place. Asking on the first delivery is affordable because the card is not an interruption: the component still waits out the delay and still drops the chance entirely if the user starts typing, so an early ask that lands badly costs a dropped chance rather than a derailed session. The delivery counter stays. It is the seam the policy is made of, and it is what makes an unwritable store read as "not yet qualified" β a store that cannot write cannot record a dismissal either, so without it the card would come back after every run with no way to stop it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): retire the experience survey when it is shown, not when it is answered One ask per user was the intent, but `retireSurvey()` only ran from `finish` and `close`. Ignoring the card β the most common way to decline a prompt β left the flag unset, so the card returned after the user's next artifact, and the one after that. `exposedRef` did not cover this: it is a ref, so it resets on the next page load. Retiring on exposure makes the rule what it claims to be: a user sees this card at most once, whatever they do with it. A chance dropped by the typing bail-out is still retried, because a card the user never saw did not spend their one ask. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(web): drop the typing bail-out from the experience survey The card no longer cancels itself when the user starts typing during the three-second delay. With one ask per user, a dropped chance is the whole ask for anyone whose habit is to type straight into the next prompt β and that habit belongs to the engaged users this survey most wants to hear from. Removes the `beforeinput` listener and the IME reasoning that went with it. The delay stays: it keeps the card from animating in on top of the artifact, which was always its other job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(web): give the survey's free-text field an accessible name Review finding from nexu-io#7074, folded in with that branch. The textarea is auto-focused the instant "Something else" is picked, so its accessible name is the only thing telling a screen-reader user what the field is for β and it had none. A placeholder is not a label and disappears on the first keystroke. Reuses the placeholder string rather than adding a twentieth translation of the same sentence, so the visible and announced names cannot drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: ensure PR CI --------- Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com> Co-authored-by: Eli <qiongyu1999@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: open-design-crew[bot] <4197780+open-design-crew[bot]@users.noreply.github.com>
β¦add an "other" answer (#7117) (#7127) * feat(web): add an "other" answer with a free-text field to the survey The eight choices cover what users already tell us most often, but they cannot cover what we have not heard yet β and the people whose problem is not on the list are exactly the ones worth hearing from. Picking "Something else" opens a text field instead of finishing the survey. The cost is paid only by the people who need it: everyone whose answer is on the list still finishes in one tap. This is the open-ended question we cut earlier, put back behind a door rather than in everyone way. Wire shape follows PostHog open-choice convention β the response recorded is the text they typed. Picking "Something else" and typing nothing still reports the choice itself, because "none of these fit" is an answer and dropping it would turn those people into non-responders. Skip disappears once the choice is made; it would only have been a second way to submit. * style(web): use a neutral focus border on the survey text field The green ring competed with the brand fill on the selected chip directly above it β two green things one under the other, neither reading as primary. A neutral border marks focus without joining that fight, and the inset fill still separates the field from the card. * fix(web): keep the survey submit button on the footer right edge Submit replaces Skip in the footer, but only Skip carried the margin-left:auto that pushes it right β so the moment the text field opened, the primary action collapsed back against the step counter and jumped from the right edge to the left. Measured from the card edge, both now sit at the same 17px gap. * feat(web): ask the experience survey after a delivered artifact, not an export The post-export survey could only reach people who exported. Over 30 days ~13k users exported while ~37k produced an artifact, so roughly two thirds of the people who got real work out of the product were never asked β and the ones who tried it, got something, and left without exporting are the ones worth hearing from most. The card is now armed by a delivered design run (resolveDesignDeliveryOutcome === 'delivered'), from the second such delivery on. The first delivery measures a first impression rather than an experience, and lands at the one moment the user most wants to look at what they just got; waiting for the second costs almost no reach because deliveries repeat (~13 per user per 30 days). Because a delivery is usually mid-session rather than end-of-session, the 3s delay now bails out if the user starts writing their next prompt. 'beforeinput' rather than 'keydown' so IME composition counts as typing. A dropped chance is taken by the next delivery, so the card lands after the turn the user actually stopped on. Export no longer arms anything; the three FileViewer call sites are gone. Every survey event carries trigger: 'post_generation' so the two regimes stay distinguishable in the events table. * feat(web): arm the experience survey on the first delivered artifact Drops the two-delivery threshold to one. Waiting for a second run bought a more considered opinion at the cost of everyone who produces one artifact and leaves β the users we understand least, and the ones this survey was widened to reach in the first place. Asking on the first delivery is affordable because the card is not an interruption: the component still waits out the delay and still drops the chance entirely if the user starts typing, so an early ask that lands badly costs a dropped chance rather than a derailed session. The delivery counter stays. It is the seam the policy is made of, and it is what makes an unwritable store read as "not yet qualified" β a store that cannot write cannot record a dismissal either, so without it the card would come back after every run with no way to stop it. * fix(web): retire the experience survey when it is shown, not when it is answered One ask per user was the intent, but `retireSurvey()` only ran from `finish` and `close`. Ignoring the card β the most common way to decline a prompt β left the flag unset, so the card returned after the user's next artifact, and the one after that. `exposedRef` did not cover this: it is a ref, so it resets on the next page load. Retiring on exposure makes the rule what it claims to be: a user sees this card at most once, whatever they do with it. A chance dropped by the typing bail-out is still retried, because a card the user never saw did not spend their one ask. * refactor(web): drop the typing bail-out from the experience survey The card no longer cancels itself when the user starts typing during the three-second delay. With one ask per user, a dropped chance is the whole ask for anyone whose habit is to type straight into the next prompt β and that habit belongs to the engaged users this survey most wants to hear from. Removes the `beforeinput` listener and the IME reasoning that went with it. The delay stays: it keeps the card from animating in on top of the artifact, which was always its other job. * fix(web): give the survey's free-text field an accessible name Review finding from #7074, folded in with that branch. The textarea is auto-focused the instant "Something else" is picked, so its accessible name is the only thing telling a screen-reader user what the field is for β and it had none. A placeholder is not a label and disappears on the first keystroke. Reuses the placeholder string rather than adding a twentieth translation of the same sentence, so the visible and announced names cannot drift. * chore: ensure PR CI --------- (cherry picked from commit 713caf0) Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com> Co-authored-by: elifive555555 <296440099+elifive555555@users.noreply.github.com> Co-authored-by: Eli <qiongyu1999@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: open-design-crew[bot] <4197780+open-design-crew[bot]@users.noreply.github.com>













































Why
The post-export survey (#6999) asks which one thing to fix first, and offers eight choices. Those eight came from what users already report most often in the per-run feedback β so by construction they cover what we have already heard, and nothing else.
The people whose problem is not on that list are the ones worth hearing from, and right now they have two options: pick the least-wrong answer, or skip. Both destroy the signal.
What users will see
The second question gains a ninth option at the end: Something else.
Picking it does not finish the survey β a text box opens underneath, already focused, and the Skip link is replaced by Send. Everyone whose answer is on the list still finishes in one tap; the extra step is paid for only by the people who need it.
Picking Something else and sending nothing still counts as an answer. "None of these fit" is information, and treating it as a skip would quietly overstate how well the eight choices cover the ground.
Surface area
apps/webScreenshots
Attached in the review thread: the second question with the new option, and the state after picking it.
To see it without waiting for a real export, run the web app and open
/?survey=preview(dev builds only).Notes for review
The wire shape follows PostHog's open-choice convention: when a respondent picks the open choice, the recorded response is the text they typed, not the word "Other" with the text stored beside it.
tests/experience-survey-response.test.tspins this, including the empty-text case and the case where a stale choice index is present alongside the text.The PostHog survey definition was updated in the same breath β the question now carries a ninth choice with
hasOpenChoice: true. As the contract file warns, editing questions in PostHog regenerates nothing in the repo, so the two have to move together.One consequence for reporting: free-text answers each become their own value in the improvement distribution, so that tile will grow a long tail of single-count rows once this ships. That is the point of the option, but the tile will want a "known choice vs free text" split before the tail gets long. Not in this PR.