Feat/docs feedback widget - #422
Conversation
…-page link DocItem/Footer --wrap swizzle appends a 'Was this page helpful?' widget on every doc page. Vote -> docs_page_feedback; free-text comment -> docs_page_feedback_comment (the actionable part). Uses the existing PostHog capture helper; one vote per session; client-only, no new deps. Also enables editUrl so pages get an edit/PR link (repo is private, so useful for contributors with access). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps the per-page feedback widget; drops only the edit link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
moritzhartmeier
left a comment
There was a problem hiding this comment.
Reviewed the diff. Net change is 3 new files (+252/-0) — the second commit reverts the editUrl addition, so docusaurus.config.ts ends up untouched. All three CI checks pass.
Overall this is clean and well-scoped. No new dependencies, reuses the existing capturePostHogEvent helper, snake_case event properties consistent with docs_search_performed / docs_search_result_click, single-quote TSX style matching SkillsCallout, and English-only strings are fine given locales: ["en"]. I also verified the wiring actually takes effect: the existing ejected swizzle at src/theme/DocItem/Layout/index.js:53 renders <DocItemFooter />, so the --wrap swizzle is picked up rather than silently bypassed.
Comments inline. The three I'd want addressed before merge are the unbounded votes, the silent drop when PostHog is unavailable, and the dead vote buttons after sending — the rest are nits.
Two questions that aren't really code review:
- Who actually watches
docs_page_feedback_commentin PostHog? The UI copy promises the docs team reads it. Is there an alert, a dashboard, or a routine — or will these accumulate unread? This determines whether the widget is worth its footprint on every page. - Requiring a vote before the comment box appears is deliberate per the docstring. Is that the intended funnel, or should a reader be able to leave a comment without voting first? Right now the actionable signal is gated behind the less actionable one.
| const vote = (isHelpful: boolean) => { | ||
| setHelpful(isHelpful); | ||
| capturePostHogEvent('docs_page_feedback', { ...base(), helpful: isHelpful }); | ||
| try { | ||
| window.sessionStorage.setItem(storeKey, isHelpful ? 'up' : 'down'); | ||
| } catch { | ||
| /* ignore */ | ||
| } | ||
| }; |
There was a problem hiding this comment.
Votes are unbounded — the anti-spam claim above isn't true.
The docstring at line 17 says "One vote per browser session per page (sessionStorage), so it can't be spammed." But sessionStorage here only restores the button state on mount (the effect at lines 30-38); vote() has no guard, so alternating 👍/👎 fires one docs_page_feedback event per click, indefinitely.
Two things to decide:
- Add the guard (read the stored value, return early or only capture when the vote actually changes) — or fix the comment so it doesn't promise something the code doesn't do.
- Either way, the helpful-rate KPI will need a distinct-on-(person, url) in PostHog rather than a raw event count. Worth agreeing on that before the dashboard gets built, otherwise flip-flopping readers quietly skew the metric.
| </div> | ||
| )} | ||
|
|
||
| {sent && <p className={styles.thanks}>Thanks for the detail — it goes straight to the docs team.</p>} |
There was a problem hiding this comment.
Feedback is silently dropped when PostHog isn't available, but this copy claims delivery.
capturePostHogEvent no-ops when window.posthog is absent — ad blocker, consent declined, script blocked, PostHog outage. The comment is described as the actionable part of this widget, so silently losing it while telling the reader it reached the docs team is the worst failure mode here.
Suggestion: have the helper return a boolean and branch on it, and soften the copy — at minimum drop the "straight to the docs team" promise unless you can confirm it was sent.
| </div> | ||
| )} | ||
|
|
||
| {sent && <p className={styles.thanks}>Thanks for the detail — it goes straight to the docs team.</p>} |
There was a problem hiding this comment.
a11y: no live region on the confirmation.
Submitting removes the textarea and swaps in this paragraph. Screen-reader users get no announcement that anything happened. role="status" on the <p> would fix it.
| </div> | ||
| </div> | ||
|
|
||
| {helpful !== null && !sent && ( |
There was a problem hiding this comment.
After the comment is sent, the vote buttons stay live but the panel never comes back.
Once sent is true, this condition is false forever. The 👍/👎 buttons remain enabled and still fire docs_page_feedback events, but clicking them produces no visible response at all — the comment panel doesn't reopen.
Either disable the vote buttons once sent, or reset sent on a vote change so the panel reopens for the new vote.
| const submit = () => { | ||
| const c = comment.trim(); | ||
| if (!c) return; | ||
| capturePostHogEvent('docs_page_feedback_comment', { ...base(), helpful: helpful === true, comment: c }); |
There was a problem hiding this comment.
helpful: helpful === true coerces null → false. Unreachable today, since the panel only renders when helpful !== null — but if that guard ever changes (e.g. allowing a comment without a vote), this silently records "not helpful" instead of "no vote". helpful ?? null is safer and self-documenting.
| const c = comment.trim(); | ||
| if (!c) return; | ||
| capturePostHogEvent('docs_page_feedback_comment', { ...base(), helpful: helpful === true, comment: c }); | ||
| setSent(true); |
There was a problem hiding this comment.
sent isn't persisted to sessionStorage the way the vote is. Navigate away and back within the same session and the reader gets a fresh comment box (with their vote still highlighted), so the same comment can be sent repeatedly. Minor, but inconsistent with the vote's persistence — worth storing alongside it.
| const permalink = window.location.pathname; | ||
| const title = document.title; | ||
| const storeKey = `docs-feedback:${permalink}`; |
There was a problem hiding this comment.
Prefer useLocation() / useDoc() over the raw DOM globals.
Not a bug as written — I checked, and docs routes do remount on client-side navigation (react-router-config's renderRoutes keys routes by array index), so the []-dep effect below and its eslint-disable are safe and state resets between pages.
Two reasons to switch anyway:
- Consistency:
src/theme/DocItem/Content/index.tsxalready usesuseLocation()for exactly this. document.titleincludes the site-title suffix, so thetitleproperty on every event is noisier than the frontmatter titleuseDoc()would give you.
It also makes the component robust if the remount behaviour ever changes underneath you, instead of relying on it.
| className={styles.textarea} | ||
| rows={2} | ||
| value={comment} | ||
| placeholder="Tell us more (optional, but the most useful part)…" |
There was a problem hiding this comment.
PII risk in the free-text box.
Customer-facing docs + a free-text field + the text stored as a PostHog event property. Readers will paste log excerpts, license keys, and customer names in here.
Two asks:
- Add a short note to the placeholder or label about not including confidential data.
- Confirm with whoever owns the PostHog instance that free-text properties are acceptable there (retention, access, deletion requests).
| .on { | ||
| border-color: var(--ifm-color-success); | ||
| background: var(--ifm-color-success-contrast-background); | ||
| color: var(--ifm-color-success-darker); | ||
| } | ||
|
|
||
| .bad { | ||
| border-color: var(--ifm-color-warning); | ||
| background: var(--ifm-color-warning-contrast-background); | ||
| color: var(--ifm-color-warning-darker); | ||
| } |
There was a problem hiding this comment.
--ifm-color-success-* and --ifm-color-warning-* aren't overridden anywhere in src/css/custom.scss, so the pressed states fall back to Infima's default green/yellow rather than Scandit brand colors. Probably worth a glance from design before this ships on every doc page.
No description provided.