fix(surveys): re-translate displayed surveys when user language changes - #4195
fix(surveys): re-translate displayed surveys when user language changes#4195nandinitiw wants to merge 5 commits into
Conversation
|
Hey @adboio @lucasheriques — would love a review on this when you get a chance! It closes the TODO in |
|
@ioannisj related to what you’re doing with survey as well? |
There was a problem hiding this comment.
Left a few inline comments. The main ones is that the survey events don't follow the same language rule as the display after a mid-survey switch, and maybe a miss in survey style when switching language. everything else is smaller. Thanks for picking up that TODO!
We'll also need to add a changeset entry for this
|
Thanks for the review! Pushed fixes for all three:
On the answer-time language question: since Preact's |
| } | ||
| } | ||
|
|
||
| private _onLanguageChange(): void { |
There was a problem hiding this comment.
Hey, nice fix. One small thing on the delayed path: for a survey with surveyPopupDelaySeconds > 0, _addSurveyToFocus sets _surveyInFocus before the actual render is deferred into the setTimeout (can you please validate if this is the case).
So if a languagechange or an onFeatureFlags reload fires during that delay window with a different language (e.g. from identify()), _onLanguageChange sees the survey in focus and renders it before the delay elapses and without the _shouldDisplaySurvey recheck the timeout does.
That recheck is there specifically because identify() can flip targeting off mid-delay, and onFeatureFlags is exactly the hook this PR subscribes to, so the same reload can both make the survey ineligible and force it on screen early.
Could we gate _onLanguageChange so it only re-renders a survey that's already on screen?
| 'language', | ||
| 'response_format', | ||
| 'timestamp_granularities', | ||
| 'service_tier', |
There was a problem hiding this comment.
Looks unrelated. Probably need to merge latest main
5a6924b to
3280076
Compare
|
Addressed both comments from the latest review: Delayed render race (re: Unrelated |
|
Implemented answer-time language snapshotting to align with the iOS behavior:
|
ioannisj
left a comment
There was a problem hiding this comment.
LG, just this #4195 (comment) here (probably left over from #4072
4d5a179 to
c0ada3c
Compare
lucasheriques
left a comment
There was a problem hiding this comment.
the re-translate path holds up. checked whether a language switch wipes in-progress answers and it does not: retrieveSurveyShadow returns the existing shadow root and nothing re-keys, so preact patches in place.
four red checks, three are one-liners:
- lint: prettier at
surveys.test.ts:1682and:1696, sopnpm lint:fix - mangled names: 6 new private fields, so
cd packages/browser && pnpm write-mangled-property-namesand commit the file - unit tests:
questionSnapshotsnow rides along in thesendSurveyEventcall, and 2 pre-existing tests insurvey-popup.test.tsxpin the exact object, so they need the key added - hygiene: the script itself passed and its warning is real (
@posthog/corechanged, wants"@posthog/core": patch). the red x is ours though: that job posts a pr comment and fork prs get a read-only token, so it 403s on every external contribution. ignore it.
worth adding questionSnapshots to the description too, since it is why core changed.
one note inline.
| instance.onFeatureFlags = jest.fn().mockReturnValue(() => {}) | ||
| }) | ||
|
|
||
| it('updates _currentLanguage and re-renders when languagechange fires and language differs', () => { |
There was a problem hiding this comment.
these set _surveyInFocus / _currentLanguage / _surveyIsRendered directly and stub _translateSurveyForRendering, so they assert internal state rather than what the user sees. they would pass even if the popup never re-rendered.
the claim that answers survive the switch has no test. one that renders, types into a question, fires languagechange, then asserts the label changed and the typed value did not would cover it. __tests__/extensions/surveys.test.ts:261 has a describe('SurveyManager') with fixtures if that is easier.
…g#4195 - Fix prettier failures in surveys.test.ts (pnpm lint:fix) - Regenerate terser-mangled-names.json for the 7 new private fields introduced by the language re-translation and answer-time snapshotting work (_currentLanguage, _languageChangeListener, _onLanguageChange, _surveyIsRendered, _surveyPopupProps, _unsubscribeFeatureFlags, and the pre-existing _surveyInFocus entry) - Add questionSnapshots to the two survey-popup.test.tsx assertions that pin the exact sendSurveyEvent call shape - Add @posthog/core to the changeset (questionSnapshots flows through buildSurveyResponseProperties there) and mention it in the description - Replace the internal-state-only language-change test with one that exercises the real render path: renders a popup via the unmocked handlePopoverSurvey, types an answer, fires a real languagechange event with navigator.language changed, and asserts both that the rendered question text updates and that the typed answer survives the re-render - Add an afterEach to destroy() the SurveyManager between tests in the SurveyManager describe block — without it, each test's construction left a 'languagechange' window listener attached, so later real window.dispatchEvent(new Event('languagechange')) calls (including the new test) hit stale instances whose mockPostHog no longer had a get_property mock, throwing during event dispatch
|
Thanks for the thorough review! Addressed everything:
One thing I found along the way: the existing |
|
thanks fro the improvementes on the tests! @nandinitiw can you fix the last conflicts so we can get this merged? |
When identify() sets a new 'language' person property, or the browser fires a languagechange event, any popover survey currently on screen stays in the old language until dismissed. This wires up both triggers to re-detect the resolved language and re-render in place when it differs from what was originally shown. Closes PostHog#4174
- Use addEventListener() wrapper (fixes CI lint rule) - Snapshot style/properties/isSurveyCompleted on initial render and restore them on language-change re-render, so NextToTrigger position and custom event properties are preserved - Add changeset entry - Update tests to no longer rely on renderPopover mock (now renders inline)
Prevents _onLanguageChange from rendering a survey early when it is queued with surveyPopupDelaySeconds > 0. The survey was gaining focus before the delay timer fired, so a concurrent languagechange event could bypass the delay and skip the eligibility recheck.
Ensures $survey_questions[].question and $survey_language in sent/dismissed events reflect the language the user saw when they answered, not whatever language is active at event-fire time after a mid-session switch. - buildSurveyResponseProperties accepts questionSnapshots (id → displayed question text) and prefers those over the current survey question text - InProgressSurveyState gains questionSnapshots, persisted to localStorage - onNextButtonClick snapshots currentQuestion.question before advancing - dismissedSurveyEvent prefers inProgressSurvey.surveyLanguage over the current display prop; falls back to the prop only when no in-progress state exists (dismissed before answering anything)
…g#4195 - Fix prettier failures in surveys.test.ts (pnpm lint:fix) - Regenerate terser-mangled-names.json for the 7 new private fields introduced by the language re-translation and answer-time snapshotting work (_currentLanguage, _languageChangeListener, _onLanguageChange, _surveyIsRendered, _surveyPopupProps, _unsubscribeFeatureFlags, and the pre-existing _surveyInFocus entry) - Add questionSnapshots to the two survey-popup.test.tsx assertions that pin the exact sendSurveyEvent call shape - Add @posthog/core to the changeset (questionSnapshots flows through buildSurveyResponseProperties there) and mention it in the description - Replace the internal-state-only language-change test with one that exercises the real render path: renders a popup via the unmocked handlePopoverSurvey, types an answer, fires a real languagechange event with navigator.language changed, and asserts both that the rendered question text updates and that the typed answer survives the re-render - Add an afterEach to destroy() the SurveyManager between tests in the SurveyManager describe block — without it, each test's construction left a 'languagechange' window listener attached, so later real window.dispatchEvent(new Event('languagechange')) calls (including the new test) hit stale instances whose mockPostHog no longer had a get_property mock, throwing during event dispatch
308990c to
028f008
Compare
|
Rebased on latest main and resolved the conflict in |
|
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, post a comment or remove the |
|
This PR was closed due to lack of activity. Feel free to reopen if it's still relevant. |
|
Reopening — this was auto-closed by the stale bot, not for cause. lucasheriques's last review was positive; picking this back up. |
|
This was auto-closed by the stale bot after a week of inactivity, not for cause — the last review from @lucasheriques was positive and all requested changes had already been addressed. GitHub's reopen API is failing with an opaque validation error I couldn't resolve, so I've opened a fresh PR from the same branch with the identical diff: #4626 |
…g#4195 - Fix prettier failures in surveys.test.ts (pnpm lint:fix) - Regenerate terser-mangled-names.json for the 7 new private fields introduced by the language re-translation and answer-time snapshotting work (_currentLanguage, _languageChangeListener, _onLanguageChange, _surveyIsRendered, _surveyPopupProps, _unsubscribeFeatureFlags, and the pre-existing _surveyInFocus entry) - Add questionSnapshots to the two survey-popup.test.tsx assertions that pin the exact sendSurveyEvent call shape - Add @posthog/core to the changeset (questionSnapshots flows through buildSurveyResponseProperties there) and mention it in the description - Replace the internal-state-only language-change test with one that exercises the real render path: renders a popup via the unmocked handlePopoverSurvey, types an answer, fires a real languagechange event with navigator.language changed, and asserts both that the rendered question text updates and that the typed answer survives the re-render - Add an afterEach to destroy() the SurveyManager between tests in the SurveyManager describe block — without it, each test's construction left a 'languagechange' window listener attached, so later real window.dispatchEvent(new Event('languagechange')) calls (including the new test) hit stale instances whose mockPostHog no longer had a get_property mock, throwing during event dispatch
…g#4195 - Fix prettier failures in surveys.test.ts (pnpm lint:fix) - Regenerate terser-mangled-names.json for the 7 new private fields introduced by the language re-translation and answer-time snapshotting work (_currentLanguage, _languageChangeListener, _onLanguageChange, _surveyIsRendered, _surveyPopupProps, _unsubscribeFeatureFlags, and the pre-existing _surveyInFocus entry) - Add questionSnapshots to the two survey-popup.test.tsx assertions that pin the exact sendSurveyEvent call shape - Add @posthog/core to the changeset (questionSnapshots flows through buildSurveyResponseProperties there) and mention it in the description - Replace the internal-state-only language-change test with one that exercises the real render path: renders a popup via the unmocked handlePopoverSurvey, types an answer, fires a real languagechange event with navigator.language changed, and asserts both that the rendered question text updates and that the typed answer survives the re-render - Add an afterEach to destroy() the SurveyManager between tests in the SurveyManager describe block — without it, each test's construction left a 'languagechange' window listener attached, so later real window.dispatchEvent(new Event('languagechange')) calls (including the new test) hit stale instances whose mockPostHog no longer had a get_property mock, throwing during event dispatch
What
When a survey popover is on screen, it was never re-translated if the user's language changed mid-session. This happened in two scenarios:
posthog.identify()/setPersonPropertiesForFlags()sets a newlanguageperson propertylanguagechangeevent (OS/browser language switch)The TODO comment in
survey-translations.tsalready called this out as a known gap.Also fixes a gap this surfaced:
$survey_questions[].questionand$survey_languagewere read at capture time, not answer time — if the language changed mid-survey, submitted events showed the new language's question text even for questions the user answered under the old language.How
In
SurveyManager:window.languagechangelistener in the constructorposthog.onFeatureFlags()(which fires afteridentify()reloads flags)_onLanguageChange(), which:surveyPopupDelaySeconds— being forced on screen early by a mid-delay reload, skipping the eligibility recheck the delay timeout does)_currentLanguageand re-renders the popover viarenderPopover()destroy()_currentLanguageis cleared when a survey is dismissedquestionSnapshots:onNextButtonClicknow snapshots the currently-displayed question text intoquestionSnapshots: Record<string, string>(question id → text) before advancing, and persists it toInProgressSurveyStatein localStorage.buildSurveyResponsePropertiesin@posthog/corenow accepts this optional param and prefers snapshot text over the current (possibly re-translated) survey question text, so$survey_questionsreflects what the user actually saw when they answered.$survey_languageondismissednow similarly prefers the in-progress language over the current display-time language.The iOS reference implementation in posthog-ios #686 follows the same pattern.
Tests
Unit tests covering:
_currentLanguageon dismisslanguagechangelistener ondestroy()questionSnapshotsrecorded and used in place of current question texthandlePopoverSurvey, types an answer, fires a reallanguagechangeevent, and asserts both that the rendered question text updates and that the typed answer survives the re-renderCloses #4174