Skip to content

feat(feedback): in-app quick feedback widget (#5662)#7143

Open
MarkusNeusinger wants to merge 2 commits into
mainfrom
claude/fix-anyplot-5662-1wuVb
Open

feat(feedback): in-app quick feedback widget (#5662)#7143
MarkusNeusinger wants to merge 2 commits into
mainfrom
claude/fix-anyplot-5662-1wuVb

Conversation

@MarkusNeusinger
Copy link
Copy Markdown
Owner

Summary

  • Add a global floating Feedback button (FAB, bottom-right) wired into RootLayout. Clicking opens a compact popover with a 500-char message field, optional reaction (👍 👎 🐛 💡 ❤️), optional email, and a "Send" button. After submit users see a "Thanks!" state that auto-closes.
  • New backend route POST /feedback persists entries to a new feedback table via FeedbackRepository. Guards: honeypot field, message length cap, reaction allow-list, per-IP rate limit (5/min using a SHA-256 IP hash).
  • Add Plausible events feedback_opened and feedback_submitted via the existing useAnalytics hook; documented in docs/reference/plausible.md.

Resolves the three open questions in the issue:

  • Where: global (visible on every page).
  • Persistence/triage: dedicated DB table, manual triage. No auto-issue and no Slack/Discord webhook in this PR.
  • Analytics: yes — open + submit fire Plausible events.

Files

  • Backend: core/database/models.py (Feedback model + FEEDBACK_REACTIONS), core/database/repositories.py (FeedbackRepository), core/database/__init__.py (exports), alembic/versions/c5d7e9f1a3b2_add_feedback_table.py (new migration off the current head 3a7e1b5c0c4f), api/schemas.py (FeedbackRequest/Response), api/routers/feedback.py, api/routers/__init__.py, api/main.py.
  • Frontend: app/src/components/FeedbackWidget.tsx, app/src/components/RootLayout.tsx (mounts widget).
  • Tests: unit tests/unit/api/test_feedback_router.py (7), integration tests/integration/api/test_api_endpoints.py::TestFeedbackEndpoint (8) and tests/integration/test_repositories.py::TestFeedbackRepository (2), frontend app/src/components/FeedbackWidget.test.tsx (6).
  • Docs: docs/reference/plausible.md adds the two new events.

Test plan

  • uv run ruff check . && uv run ruff format --check .
  • uv run pytest tests/unit tests/integration — 1505 passed
  • yarn tsc --noEmit (clean)
  • yarn test — 465 passed across 53 files
  • Manual smoke (against a deployed env): open the FAB, send feedback, verify a row lands in the feedback table; trigger 6 submissions in a row from one IP and confirm the 6th returns 429; verify the honeypot path doesn't write.
  • Run the alembic migration round-trip against Postgres before the first deploy (alembic upgrade head then downgrade -1 then upgrade head).

https://claude.ai/code/session_01D5QExULwc4wAGZyHeTiC89


Generated by Claude Code

Add a global floating widget so non-coder users can send a short note
without opening a GitHub issue. Submissions go to a new `feedback` table
behind `POST /feedback`, guarded by a honeypot, length cap, reaction
allow-list, and per-IP rate limit. Open/submit events fire through the
existing Plausible analytics hook.

https://claude.ai/code/session_01D5QExULwc4wAGZyHeTiC89
Copilot AI review requested due to automatic review settings May 17, 2026 21:35
Comment thread app/src/components/FeedbackWidget.tsx Fixed
CodeQL flagged Math.random() in the newSessionId fallback. The id is an
opaque correlation handle (not a credential), but there's no reason to
reach for Math.random when crypto.getRandomValues is universally available
in any browser that has window.crypto.

https://claude.ai/code/session_01D5QExULwc4wAGZyHeTiC89
@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

❌ Patch coverage is 87.07865% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
app/src/components/FeedbackWidget.tsx 74.44% 23 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an in-app quick feedback flow spanning the React app, FastAPI backend, database schema, tests, and Plausible analytics documentation.

Changes:

  • Adds a global floating feedback widget mounted in RootLayout.
  • Adds POST /feedback, persistence via a new Feedback model/repository, and Alembic migration.
  • Adds backend/frontend tests and documents the new Plausible events.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
app/src/components/FeedbackWidget.tsx Implements the floating feedback FAB, popover form, submission, and analytics events.
app/src/components/FeedbackWidget.test.tsx Adds component tests for widget rendering, submission, errors, and honeypot accessibility.
app/src/components/RootLayout.tsx Mounts the feedback widget globally.
api/routers/feedback.py Adds the feedback submission endpoint, validation, rate limiting, and persistence.
api/routers/__init__.py Exports the new feedback router.
api/main.py Registers the feedback router with the FastAPI app.
api/schemas.py Adds feedback request/response schemas.
core/database/models.py Adds the Feedback model and allowed reaction constants.
core/database/repositories.py Adds FeedbackRepository.
core/database/__init__.py Re-exports feedback model/repository symbols.
alembic/versions/c5d7e9f1a3b2_add_feedback_table.py Adds the feedback table migration and indexes.
tests/unit/api/test_feedback_router.py Adds isolated unit coverage for feedback endpoint guard logic.
tests/integration/api/test_api_endpoints.py Adds integration coverage for /feedback.
tests/integration/test_repositories.py Adds integration coverage for FeedbackRepository.
docs/reference/plausible.md Documents feedback analytics events.

"""Create the feedback table plus indexes for recent-first browsing and rate-limit lookups."""
op.create_table(
"feedback",
sa.Column("id", sa.String(36), primary_key=True, nullable=False),
Comment thread api/routers/feedback.py
Comment on lines +74 to +75
since = datetime.now(timezone.utc) - RATE_LIMIT_WINDOW
recent = await repo.count_recent_by_ip(ip_hash, since)
Comment thread api/routers/feedback.py
Comment on lines +32 to +36
"""Resolve the client IP, preferring x-forwarded-for (Cloud Run + CF)."""
forwarded = request.headers.get("x-forwarded-for", "")
if forwarded:
# x-forwarded-for can be a comma-separated chain; the first entry is the original client.
return forwarded.split(",")[0].strip()
Comment thread api/routers/feedback.py
Comment on lines +73 to +77
if ip_hash:
since = datetime.now(timezone.utc) - RATE_LIMIT_WINDOW
recent = await repo.count_recent_by_ip(ip_hash, since)
if recent >= RATE_LIMIT_MAX:
raise HTTPException(status_code=429, detail="Too many feedback submissions, please slow down")
Comment on lines +72 to +75

const ensureSessionId = (): string => {
if (sessionId) return sessionId;
const fresh = newSessionId();
model = Feedback
updatable_fields = frozenset()

async def count_recent_by_ip(self, ip_hash: str, since) -> int:
Comment thread api/routers/feedback.py
Comment on lines +40 to +42
def _hash_ip(ip: str) -> str:
"""SHA-256 of the IP, hex. Used for rate-limit lookups only — never reversed."""
return hashlib.sha256(ip.encode("utf-8")).hexdigest() if ip else ""
Comment on lines +33 to +34
if (RESERVED_TOP_LEVEL.has(segments[0])) return undefined;
return segments[0];
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.

4 participants