Skip to content

EDU role survey: the sidebar panel - #7581

Open
gmorador-tribu wants to merge 3 commits into
mainfrom
feature/survey-panel
Open

gmorador-tribu wants to merge 3 commits into
mainfrom
feature/survey-panel

Conversation

@gmorador-tribu

Copy link
Copy Markdown
Contributor

First of the two client PRs for the EDU role survey (issue
hypothesis/product-backlog#1751). Adds the panel that asks the question and
records the answer. The sidebar-blocking behaviour is not here -- greying
the sidebar out, pinning the panel and switching the adder off are increment
B, in a PR stacked on this one.

Needs the H side first: hypothesis/h#10196 (migration), hypothesis/h#10198
(model) and the session PR above it. Safe to merge before them, and safe to
deploy: with instructor_survey off, H never sends show_instructor_survey,
the store selector stays false and nothing renders.

What's here

  • src/types/api.ts -- InstructorSurveyResponse and the two profile
    preference keys.
  • src/sidebar/helpers/session.ts -- shouldShowInstructorSurvey().
  • src/sidebar/store/modules/session.ts -- isInstructorSurveyPending.
  • src/sidebar/services/session.ts -- submitInstructorSurveyResponse().
  • src/sidebar/services/api.ts -- the client.survey.instructor_role.shown
    analytics event.
  • src/sidebar/components/InstructorSurveyPanel.tsx -- the panel.
  • src/sidebar/components/HypothesisApp.tsx -- renders it in the sidebar.

Add the two profile preferences the EDU role survey rides on, the predicate
that reads them, the store selector, and the method that records an answer.
Nothing renders yet; the panel lands next.

The client is told *whether* to ask, never why. `show_instructor_survey` is
true only while H considers the user part of the survey's audience and they
haven't answered, and the whole audience rule -- the feature flag, the
first-party authority, the educational email domain -- stays in H. That is
what lets the audience change without a client release, which would otherwise
have to propagate through the CDN and the browser extension before it took
effect.

`shouldShowInstructorSurvey` takes the effective feature flags rather than
`SidebarSettings` so that FrameSyncService can call it unchanged when the
sidebar-blocking work needs the same answer. `isInstructorSurveyPending`
wraps it in the store as the single source of truth for both.

`submitInstructorSurveyResponse` sends `instructor_survey_response` on its
own, following the error handling of `dismissYoutubeDisclaimer` -- toast and
rethrow -- rather than `dismissSidebarTutorial`, which silently swallows
failures. One method covers all three answers, dismissal included: H records a
dismissal like any other answer, and any of them stops the survey being shown.
The PATCH deliberately carries only that one key, because H emits
`show_instructor_survey` but does not accept it back, so echoing the whole
preferences object would be rejected and take the answer down with it.
Ask the question. InstructorSurveyPanel renders above the sidebar's content
with Yes, No and a dismiss "x", and HypothesisApp shows it whenever the store
says the survey is pending and we are in the sidebar -- not the notebook,
profile or stream.

Shaped after YouTubeDisclaimerBanner, which is the closest end-to-end
precedent in the repo: a full-bleed banner under the top bar, its state read
from the profile, its answer written through the session service. The panel
needs no "answered" state of its own, because the answer comes back in the
profile and unmounts it.

Both effects wait for the sidebar to have been opened. The sidebar iframe is
created when the page loads and starts collapsed, so the panel mounts as soon
as the profile arrives whether or not anyone will ever look at it: tracking
the impression at mount would count every page load of a user who never opens
the sidebar, and the PRD asks who *saw* the survey. Focusing at mount has the
matching problem -- a no-op while the frame is hidden, and a theft of focus
from whatever the reader was doing if it weren't. `hasSidebarOpened` is
sticky, so each effect runs once, on the first open; SidebarView uses it the
same way to hold off connecting the streamer.

Focus moves to the panel the way SidebarPanel does it, but without
`role="dialog"`: that would claim the top bar is inert, and it isn't. The
three controls are disabled while an answer is in flight, and re-enabled
afterwards whatever happened -- on success the panel is about to unmount, but
if a response ever came back still reporting the survey as pending, leaving
them disabled would strand the user with a banner whose dismiss button no
longer works.
@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.62%. Comparing base (b4d085a) to head (a4fc7cb).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7581   +/-   ##
=======================================
  Coverage   99.62%   99.62%           
=======================================
  Files         285      286    +1     
  Lines       11971    12007   +36     
  Branches     2920     2925    +5     
=======================================
+ Hits        11926    11962   +36     
  Misses         45       45           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Render the survey as a card inside the sidebar's `.container`, next to the
Help, Search and Share panels, instead of a full-bleed banner under the top
bar. With the tutorial open at the same time -- which is what a newly
registered instructor gets -- the banner ran wider than the Help card below
it, had no border, and its dismiss "x" was drawn at CloseButton's default
size, bigger than the one on the Help panel.

The outer section now takes SidebarPanel's shape (`mb-4`, rounded focus
ring) and the content sits in a Card, so the two line up. The dismiss button
uses the same size and colours as TabHeader's close button, which is what
the Help and Share panels show.
@gmorador-tribu
gmorador-tribu added this pull request to stack #7583 September 24, 2026 19:06

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant