Skip to content

web: add in-app feedback widget and usage events - #527

Merged
InfinityBowman merged 2 commits into
mainfrom
feedback-widget
Jul 6, 2026
Merged

web: add in-app feedback widget and usage events#527
InfinityBowman merged 2 commits into
mainfrom
feedback-widget

Conversation

@InfinityBowman

@InfinityBowman InfinityBowman commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Closes #525

Summary

Adds an in-app feedback widget for early-access users plus a small usage-analytics pass. No new vendors: feedback is stored in D1 and emailed via the existing contact queue, bug reports link to existing Sentry session replays, and usage tracking extends the self-hosted Plausible custom events.

Feedback widget

  • feedback table (migration 0004) with FeedbackId brand; status column (new/reviewed) is dormant until a triage view is needed.
  • submitFeedback server function following the contact pattern: auth middleware, Zod validation, insert, notification email. No rate-limiting infra - a single COUNT on the feedback table caps submissions at 5/user/hour purely to protect the notification inbox. Email failure after insert is logged, not surfaced, since the feedback is already saved.
  • FeedbackDialog mounted once in the root layout, opened via a new feedbackStore: Bug/Idea/Other chips, textarea, success state with auto-close. Auto-captures route, user agent, viewport; bug reports silently attach the Sentry replay id (disclosed in small print).
  • Entry points: "Give feedback" in the AppNavbar user dropdown, and the EarlyAccessBanner feedback text is now a real link (opens the dialog when signed in, routes to /contact when signed out on public pages).

Analytics

  • track(event, props) util wrapping window.plausible; the Window type declaration moved out of CreateLocalChecklist.tsx and existing call sites migrated.
  • New events: Project:Created, Checklist:Created (project checklist action funnel), Checklist:Completed (reconciled checklist finalized), Collaborator:Invited (email/direct prop), Feedback:Submitted (category prop).
  • No Signup event: user counts are already in the admin dashboard and a client-side event would miss OAuth signups.

Testing

  • 6 new server tests for submitFeedback (storage, context handling, email-failure tolerance, hourly cap, cap window, per-user isolation); full suite green (329 tests).
  • Typecheck and lint clean across the workspace; production build succeeds.
  • Manually smoke-tested locally: migration applied, dialog works from both entry points.

Summary by CodeRabbit

  • New Features

    • Added an in-app feedback dialog available from the app shell and navbar.
    • Users can send feedback with categories like bug, idea, or other, and include optional context.
    • Feedback now shows a success or error state after submission.
  • Bug Fixes

    • Feedback submissions are now rate-limited per user to reduce spam.
    • Feedback content is saved even if notification delivery fails.

…h per-user hourly email guard, dialog with bug/idea/other categories and Sentry replay id on bug reports, navbar dropdown and early-access banner entry points, track() util wrapping plausible with Project/Checklist/Collaborator/Feedback events

Closes #525
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces an in-app feedback feature: a feedback database table with migration, a validated server function enforcing per-user hourly rate limits and sending notification emails, a FeedbackDialog UI wired to a Zustand store, and entry points from the navbar and banner. It also adds a shared track() analytics helper replacing direct window.plausible calls across several components, plus minor unrelated formatting tweaks.

Changes

In-app Feedback Feature

Layer / File(s) Summary
Feedback data model and migration
packages/db/src/schema.ts, packages/shared/src/ids.ts, packages/web/migrations/0004_lying_frog_thor.sql, packages/web/migrations/meta/0004_snapshot.json, packages/web/migrations/meta/_journal.json, packages/web/src/__tests__/server/helpers.ts, packages/web/src/__tests__/server/migration-sql.js
Adds a FeedbackId branded type, feedback Drizzle table with FK/index and inferred types, matching SQL migration, full snapshot/journal entries, and updates test DB fixtures.
Feedback submission server function
packages/web/src/server/functions/feedback.functions.ts, packages/web/src/server/functions/feedback.server.ts, packages/web/src/config/sentry.ts
Adds a validated, auth-guarded submitFeedback server function enforcing an hourly per-user rate limit, inserting rows, sending notification emails, and a getSentryReplayId() helper for bug reports.
Feedback store and dialog UI
packages/web/src/stores/feedbackStore.ts, packages/web/src/components/feedback/FeedbackDialog.tsx
Adds a Zustand useFeedbackStore and a FeedbackDialog component with category selection, submission flow, success/error states, and analytics tracking.
Feedback dialog entry points
packages/web/src/components/EarlyAccessBanner.tsx, packages/web/src/components/layout/AppNavbar.tsx, packages/web/src/routes/__root.tsx
Wires the banner and navbar dropdown to open the feedback dialog, and mounts FeedbackDialog in the root layout.
Feedback server tests
packages/web/src/server/functions/__tests__/feedback.server.test.ts
Adds tests covering storage, email notification, missing context, email failure resilience, and per-user hourly rate limiting.

Analytics Tracking Pass

Layer / File(s) Summary
Analytics helper module
packages/web/src/lib/analytics.ts
Adds a typed window.plausible global and the track() helper function.
Analytics instrumentation across features
packages/web/src/components/checklist/CreateLocalChecklist.tsx, .../LocalChecklistView.tsx, packages/web/src/components/project/CreateProjectModal.tsx, .../overview-tab/AddMemberModal.tsx, .../reconcile-tab/ReconciliationWrapper.tsx, packages/web/src/project/actions.ts
Replaces window.plausible calls with track() and adds new tracking calls for project creation, collaborator invitation, checklist completion, and checklist creation.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FeedbackDialog
  participant submitFeedback
  participant Database
  participant EmailQueue

  User->>FeedbackDialog: select category, enter message, submit
  FeedbackDialog->>submitFeedback: submit(category, message, context)
  submitFeedback->>Database: count recent submissions (last hour)
  Database-->>submitFeedback: submission count
  alt limit exceeded
    submitFeedback-->>FeedbackDialog: throw rate-limit error
    FeedbackDialog-->>User: show error state
  else within limit
    submitFeedback->>Database: insert feedback row
    submitFeedback->>EmailQueue: send notification email
    submitFeedback-->>FeedbackDialog: success response
    FeedbackDialog-->>User: show success state, auto-close
  end
Loading

Estimated code review effort

Estimated code review effort: 3 (Moderate) | ~30 minutes

Poem

A rabbit hops with feedback in tow,
"Bug or idea?" — pick one and go!
A table, a dialog, an email in flight,
Rate-limited hops keep the inbox light.
Plausible tracks each click and each cheer —
Hooray for feedback, the rabbit's ear!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes unrelated files, including formatting-only edits and a subscription index addition that aren't part of the feedback/analytics scope. Remove unrelated formatting-only edits and the subscription index change, leaving only feedback and analytics work tied to the issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an in-app feedback widget and related usage events.
Linked Issues check ✅ Passed The PR covers the feedback dialog, D1 storage/email flow, context capture, replay IDs, entry points, and the requested analytics events.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feedback-widget

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@InfinityBowman
InfinityBowman merged commit 094bf1e into main Jul 6, 2026
4 of 5 checks passed
@InfinityBowman
InfinityBowman deleted the feedback-widget branch July 6, 2026 20:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/web/src/server/functions/feedback.server.ts (1)

70-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type-unsafe env cast.

(env as unknown as Record<string, string | undefined>).CONTACT_EMAIL bypasses the Cloudflare Env typing entirely. Consider extending the generated Env type with CONTACT_EMAIL so this reads as env.CONTACT_EMAIL with proper typing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/server/functions/feedback.server.ts` around lines 70 - 71,
The contact email lookup in feedback.server.ts is using a type-unsafe double
cast on env, bypassing the Cloudflare Env typing. Update the generated Env type
to include CONTACT_EMAIL and then read it directly through the
feedback.server.ts logic in the contactEmail assignment, so the function uses
env.CONTACT_EMAIL with proper typing and keeps the fallback value.
packages/web/src/__tests__/server/migration-sql.js (1)

263-276: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Feedback table/index design looks solid.

FK cascade on userId and the composite (userId, createdAt) index correctly support the per-user hourly rate-limit COUNT query described in the PR objectives.

Separately: this hunk also folds in an unrelated subscription_referenceId_incomplete_uidx partial index into the same migration as the feedback table. Not blocking, but worth confirming that was intentional (e.g., a hotfix bundled in) rather than accidental migration-file conflation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/__tests__/server/migration-sql.js` around lines 263 - 276,
The feedback table and index changes look fine, but this migration hunk also
includes the unrelated subscription_referenceId_incomplete_uidx partial index.
Review the migration SQL assembly in migration-sql.js and confirm whether that
subscription index belongs in the same migration as the feedback table; if not,
move it to the correct migration or separate hunk so the feedback schema change
stays isolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/src/components/feedback/FeedbackDialog.tsx`:
- Around line 45-76: The pending submission in FeedbackDialog can still update
state after the dialog has been dismissed, causing stale sent state to leak into
a later session. Add a submission/session guard in handleSubmit and
handleOpenChange so closing the dialog invalidates any in-flight request before
it resolves. Use a ref-based submission id in FeedbackDialog, increment it when
starting submit and again inside handleOpenChange(false), then only run the
success path (setFormState('sent') and the auto-close timeout) if the id still
matches.

In `@packages/web/src/server/functions/feedback.server.ts`:
- Around line 35-52: The rate-limit logic in feedback.server.ts is non-atomic
because the existing count() query and db.insert(feedback) run as separate
steps, so concurrent requests can both pass MAX_SUBMISSIONS_PER_HOUR. Update the
submit flow in the feedback server function to use an atomic per-user guard or
serialization approach around the recent submission check and insert, keeping
the existing symbols db, feedback, recentCount, MAX_SUBMISSIONS_PER_HOUR, and
throwDomainError in view while changing the implementation so only one
submission decision can happen at a time for a user.

---

Nitpick comments:
In `@packages/web/src/__tests__/server/migration-sql.js`:
- Around line 263-276: The feedback table and index changes look fine, but this
migration hunk also includes the unrelated
subscription_referenceId_incomplete_uidx partial index. Review the migration SQL
assembly in migration-sql.js and confirm whether that subscription index belongs
in the same migration as the feedback table; if not, move it to the correct
migration or separate hunk so the feedback schema change stays isolated.

In `@packages/web/src/server/functions/feedback.server.ts`:
- Around line 70-71: The contact email lookup in feedback.server.ts is using a
type-unsafe double cast on env, bypassing the Cloudflare Env typing. Update the
generated Env type to include CONTACT_EMAIL and then read it directly through
the feedback.server.ts logic in the contactEmail assignment, so the function
uses env.CONTACT_EMAIL with proper typing and keeps the fallback value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8fd351e3-5ec7-4cdb-8cad-af03606d4b6b

📥 Commits

Reviewing files that changed from the base of the PR and between f0ed62a and 2141602.

📒 Files selected for processing (28)
  • packages/db/src/schema.ts
  • packages/shared/src/ids.ts
  • packages/web/migrations/0004_lying_frog_thor.sql
  • packages/web/migrations/meta/0004_snapshot.json
  • packages/web/migrations/meta/_journal.json
  • packages/web/src/__tests__/server/helpers.ts
  • packages/web/src/__tests__/server/migration-sql.js
  • packages/web/src/components/EarlyAccessBanner.tsx
  • packages/web/src/components/checklist/CreateLocalChecklist.tsx
  • packages/web/src/components/checklist/LocalChecklistView.tsx
  • packages/web/src/components/checklist/ROB2Checklist/ScoringSummary.tsx
  • packages/web/src/components/checklist/ROBINSIChecklist/ScoringSummary.tsx
  • packages/web/src/components/dashboard/ContactPrompt.tsx
  • packages/web/src/components/feedback/FeedbackDialog.tsx
  • packages/web/src/components/layout/AppNavbar.tsx
  • packages/web/src/components/project/CreateProjectModal.tsx
  • packages/web/src/components/project/overview-tab/AddMemberModal.tsx
  • packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx
  • packages/web/src/config/sentry.ts
  • packages/web/src/lib/analytics.ts
  • packages/web/src/lib/devPdfPool.ts
  • packages/web/src/project/actions.ts
  • packages/web/src/routes/__root.tsx
  • packages/web/src/server/functions/__tests__/feedback.server.test.ts
  • packages/web/src/server/functions/feedback.functions.ts
  • packages/web/src/server/functions/feedback.server.ts
  • packages/web/src/stores/feedbackStore.ts
  • packages/workers/src/lib/mock-templates.ts
💤 Files with no reviewable changes (2)
  • packages/web/src/components/checklist/ROB2Checklist/ScoringSummary.tsx
  • packages/web/src/components/checklist/ROBINSIChecklist/ScoringSummary.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: checks
🧰 Additional context used
📓 Path-based instructions (9)
**/*

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*: NEVER use emojis anywhere - not in code, comments, documentation, plan files, commit messages, or examples. This includes unicode symbols.
Never use emojis or unicode symbols - not in code, comments, docs, or commits

Files:

  • packages/web/src/routes/__root.tsx
  • packages/web/src/__tests__/server/migration-sql.js
  • packages/web/src/server/functions/feedback.functions.ts
  • packages/shared/src/ids.ts
  • packages/workers/src/lib/mock-templates.ts
  • packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx
  • packages/web/src/components/project/overview-tab/AddMemberModal.tsx
  • packages/web/src/stores/feedbackStore.ts
  • packages/web/src/project/actions.ts
  • packages/web/src/components/dashboard/ContactPrompt.tsx
  • packages/web/src/config/sentry.ts
  • packages/web/src/lib/devPdfPool.ts
  • packages/web/migrations/0004_lying_frog_thor.sql
  • packages/web/migrations/meta/_journal.json
  • packages/web/src/__tests__/server/helpers.ts
  • packages/web/src/components/project/CreateProjectModal.tsx
  • packages/web/src/server/functions/__tests__/feedback.server.test.ts
  • packages/web/src/components/feedback/FeedbackDialog.tsx
  • packages/web/src/components/checklist/LocalChecklistView.tsx
  • packages/web/src/components/checklist/CreateLocalChecklist.tsx
  • packages/web/src/lib/analytics.ts
  • packages/db/src/schema.ts
  • packages/web/src/server/functions/feedback.server.ts
  • packages/web/src/components/layout/AppNavbar.tsx
  • packages/web/src/components/EarlyAccessBanner.tsx
  • packages/web/migrations/meta/0004_snapshot.json
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

For UI icons, use lucide-react library or SVGs only (never emojis)

Files:

  • packages/web/src/routes/__root.tsx
  • packages/web/src/server/functions/feedback.functions.ts
  • packages/shared/src/ids.ts
  • packages/workers/src/lib/mock-templates.ts
  • packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx
  • packages/web/src/components/project/overview-tab/AddMemberModal.tsx
  • packages/web/src/stores/feedbackStore.ts
  • packages/web/src/project/actions.ts
  • packages/web/src/components/dashboard/ContactPrompt.tsx
  • packages/web/src/config/sentry.ts
  • packages/web/src/lib/devPdfPool.ts
  • packages/web/src/__tests__/server/helpers.ts
  • packages/web/src/components/project/CreateProjectModal.tsx
  • packages/web/src/server/functions/__tests__/feedback.server.test.ts
  • packages/web/src/components/feedback/FeedbackDialog.tsx
  • packages/web/src/components/checklist/LocalChecklistView.tsx
  • packages/web/src/components/checklist/CreateLocalChecklist.tsx
  • packages/web/src/lib/analytics.ts
  • packages/db/src/schema.ts
  • packages/web/src/server/functions/feedback.server.ts
  • packages/web/src/components/layout/AppNavbar.tsx
  • packages/web/src/components/EarlyAccessBanner.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx}: Use import aliases from tsconfig.json
Code comments should explain why something is being done or provide context, not repeat what the code is saying
Use TODO(agent) pattern for incomplete work or flagging items for future attention, with brief description and optional doc reference

Files:

  • packages/web/src/routes/__root.tsx
  • packages/web/src/__tests__/server/migration-sql.js
  • packages/web/src/server/functions/feedback.functions.ts
  • packages/shared/src/ids.ts
  • packages/workers/src/lib/mock-templates.ts
  • packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx
  • packages/web/src/components/project/overview-tab/AddMemberModal.tsx
  • packages/web/src/stores/feedbackStore.ts
  • packages/web/src/project/actions.ts
  • packages/web/src/components/dashboard/ContactPrompt.tsx
  • packages/web/src/config/sentry.ts
  • packages/web/src/lib/devPdfPool.ts
  • packages/web/src/__tests__/server/helpers.ts
  • packages/web/src/components/project/CreateProjectModal.tsx
  • packages/web/src/server/functions/__tests__/feedback.server.test.ts
  • packages/web/src/components/feedback/FeedbackDialog.tsx
  • packages/web/src/components/checklist/LocalChecklistView.tsx
  • packages/web/src/components/checklist/CreateLocalChecklist.tsx
  • packages/web/src/lib/analytics.ts
  • packages/db/src/schema.ts
  • packages/web/src/server/functions/feedback.server.ts
  • packages/web/src/components/layout/AppNavbar.tsx
  • packages/web/src/components/EarlyAccessBanner.tsx
packages/web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/**/*.{tsx,ts}: Use lucide-react for the icon library
Use TanStack Query for server state management (useQuery, useMutation)
Import Zustand stores directly from @/stores/ instead of prop-drilling shared state
Avoid useMemo or useCallback - let the React Compiler handle memoization
Prefer newer React primitives where possible, always on the latest version
Use useEffectEvent for stable event handler references inside effects
Use useLayoutEffect for DOM measurements before paint
Use startTransition / useTransition for non-urgent state updates
Use useDeferredValue for deferring expensive re-renders
Use useSyncExternalStore for external store subscriptions (e.g., Yjs awareness)
Use useId() for unique IDs on form elements (radio buttons, checkboxes)
Never prop-drill shared state - import Zustand stores directly

Files:

  • packages/web/src/routes/__root.tsx
  • packages/web/src/server/functions/feedback.functions.ts
  • packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx
  • packages/web/src/components/project/overview-tab/AddMemberModal.tsx
  • packages/web/src/stores/feedbackStore.ts
  • packages/web/src/project/actions.ts
  • packages/web/src/components/dashboard/ContactPrompt.tsx
  • packages/web/src/config/sentry.ts
  • packages/web/src/lib/devPdfPool.ts
  • packages/web/src/__tests__/server/helpers.ts
  • packages/web/src/components/project/CreateProjectModal.tsx
  • packages/web/src/server/functions/__tests__/feedback.server.test.ts
  • packages/web/src/components/feedback/FeedbackDialog.tsx
  • packages/web/src/components/checklist/LocalChecklistView.tsx
  • packages/web/src/components/checklist/CreateLocalChecklist.tsx
  • packages/web/src/lib/analytics.ts
  • packages/web/src/server/functions/feedback.server.ts
  • packages/web/src/components/layout/AppNavbar.tsx
  • packages/web/src/components/EarlyAccessBanner.tsx
packages/web/src/routes/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use TanStack Router with file-based routing (createFileRoute)

Files:

  • packages/web/src/routes/__root.tsx
packages/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Path aliases: @/ maps to packages/web/src/

Files:

  • packages/web/src/routes/__root.tsx
  • packages/web/src/server/functions/feedback.functions.ts
  • packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx
  • packages/web/src/components/project/overview-tab/AddMemberModal.tsx
  • packages/web/src/stores/feedbackStore.ts
  • packages/web/src/project/actions.ts
  • packages/web/src/components/dashboard/ContactPrompt.tsx
  • packages/web/src/config/sentry.ts
  • packages/web/src/lib/devPdfPool.ts
  • packages/web/src/__tests__/server/helpers.ts
  • packages/web/src/components/project/CreateProjectModal.tsx
  • packages/web/src/server/functions/__tests__/feedback.server.test.ts
  • packages/web/src/components/feedback/FeedbackDialog.tsx
  • packages/web/src/components/checklist/LocalChecklistView.tsx
  • packages/web/src/components/checklist/CreateLocalChecklist.tsx
  • packages/web/src/lib/analytics.ts
  • packages/web/src/server/functions/feedback.server.ts
  • packages/web/src/components/layout/AppNavbar.tsx
  • packages/web/src/components/EarlyAccessBanner.tsx
packages/workers/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/workers/**/*.{ts,tsx}: Use Zod for schema and input validation (backend)
Use Drizzle ORM for ALL database interactions and migrations
Use Better-Auth for authentication and user management
Never bypass Drizzle for database access

Files:

  • packages/workers/src/lib/mock-templates.ts
packages/web/src/components/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use shadcn/ui for UI components (Radix-based, in @/components/ui/)

Files:

  • packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx
  • packages/web/src/components/project/overview-tab/AddMemberModal.tsx
  • packages/web/src/components/dashboard/ContactPrompt.tsx
  • packages/web/src/components/project/CreateProjectModal.tsx
  • packages/web/src/components/feedback/FeedbackDialog.tsx
  • packages/web/src/components/checklist/LocalChecklistView.tsx
  • packages/web/src/components/checklist/CreateLocalChecklist.tsx
  • packages/web/src/components/layout/AppNavbar.tsx
  • packages/web/src/components/EarlyAccessBanner.tsx
packages/web/src/stores/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/web/src/stores/**/*.{tsx,ts}: Use Zustand for client state management (stores in @/stores/)
Shared state lives in Zustand stores under packages/web/src/stores/

Files:

  • packages/web/src/stores/feedbackStore.ts
🔇 Additional comments (25)
packages/workers/src/lib/mock-templates.ts (1)

1612-1613: LGTM!

packages/web/src/components/dashboard/ContactPrompt.tsx (1)

11-15: LGTM!

packages/web/src/lib/devPdfPool.ts (1)

41-41: LGTM!

packages/web/src/lib/analytics.ts (1)

1-16: LGTM!

packages/web/src/components/checklist/CreateLocalChecklist.tsx (1)

25-25: LGTM!

Also applies to: 83-86

packages/web/src/components/checklist/LocalChecklistView.tsx (1)

24-24: LGTM!

Also applies to: 106-106

packages/web/src/components/project/CreateProjectModal.tsx (1)

37-37: LGTM!

Also applies to: 101-101

packages/web/src/components/project/overview-tab/AddMemberModal.tsx (1)

35-35: LGTM!

Also applies to: 139-139

packages/web/src/components/project/reconcile-tab/ReconciliationWrapper.tsx (1)

26-26: LGTM!

Also applies to: 407-414

packages/web/src/project/actions.ts (1)

7-7: LGTM!

Also applies to: 48-48

packages/db/src/schema.ts (1)

15-15: LGTM!

Also applies to: 369-387, 412-412, 464-466

packages/shared/src/ids.ts (1)

64-66: LGTM!

packages/web/migrations/0004_lying_frog_thor.sql (1)

1-12: LGTM!

packages/web/migrations/meta/0004_snapshot.json (1)

126-202: LGTM!

packages/web/src/__tests__/server/helpers.ts (1)

125-126: LGTM!

packages/web/src/server/functions/feedback.functions.ts (1)

6-22: LGTM!

packages/web/src/server/functions/__tests__/feedback.server.test.ts (1)

1-153: LGTM!

packages/web/src/server/functions/feedback.server.ts (1)

90-94: 🔒 Security & Privacy

Strip CR/LF from the feedback email subject. data.message.slice(0, 60) still preserves embedded \r/\n, so a raw-header transport could turn this into extra Cc/Bcc headers. Normalize control characters before building subject, or have the mail layer reject newline-bearing values.

packages/web/migrations/meta/_journal.json (1)

32-39: LGTM!

packages/web/src/stores/feedbackStore.ts (1)

1-21: LGTM!

packages/web/src/components/feedback/FeedbackDialog.tsx (1)

9-149: LGTM on the rest of the component (category chips, textarea, bug-report disclosure text, success state) — matches PR objectives closely.

packages/web/src/components/EarlyAccessBanner.tsx (1)

2-29: LGTM!

packages/web/src/components/layout/AppNavbar.tsx (1)

10-10: LGTM!

Also applies to: 35-35, 76-76, 165-165

packages/web/src/routes/__root.tsx (1)

9-9: LGTM!

Also applies to: 224-228

packages/web/src/config/sentry.ts (1)

76-79: 🎯 Functional Correctness

No schema change needed for replayId
getReplayId() returns string | undefined in the JS SDK, and z.string().max(100).optional() already matches that shape.

			> Likely an incorrect or invalid review comment.

Comment on lines +45 to +76
const handleOpenChange = (open: boolean) => {
if (!open) {
close();
setMessage('');
setFormState('idle');
}
};

const handleSubmit = async () => {
if (!message.trim() || formState === 'sending') return;
setFormState('sending');

try {
await submitFeedback({
data: {
category,
message: message.trim(),
context: {
route: window.location.pathname,
userAgent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`,
replayId: category === 'bug' ? getSentryReplayId() : undefined,
},
},
});
track('Feedback:Submitted', { category });
setFormState('sent');
setTimeout(() => handleOpenChange(false), CLOSE_AFTER_SENT_MS);
} catch {
setFormState('error');
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stale response can leak into a later dialog session.

If the dialog is closed (Escape/click-outside) while submitFeedback is still in flight, handleOpenChange(false) resets state to idle. When the pending request later resolves, its .then unconditionally calls setFormState('sent') and schedules a new auto-close timeout — even though that submission attempt was already abandoned. Reopening the dialog in that window shows a stale "Thanks for the feedback" screen and then auto-closes on its own.

🔧 Proposed fix using a submission-id guard
+  const submitIdRef = useRef(0);
+
   const handleSubmit = async () => {
     if (!message.trim() || formState === 'sending') return;
     setFormState('sending');
+    const submitId = ++submitIdRef.current;

     try {
       await submitFeedback({
         data: {
           category,
           message: message.trim(),
           context: {
             route: window.location.pathname,
             userAgent: navigator.userAgent,
             viewport: `${window.innerWidth}x${window.innerHeight}`,
             replayId: category === 'bug' ? getSentryReplayId() : undefined,
           },
         },
       });
+      if (submitId !== submitIdRef.current) return; // superseded by a later open/close
       track('Feedback:Submitted', { category });
       setFormState('sent');
       setTimeout(() => handleOpenChange(false), CLOSE_AFTER_SENT_MS);
     } catch {
+      if (submitId !== submitIdRef.current) return;
       setFormState('error');
     }
   };

And bump submitIdRef.current inside handleOpenChange when closing, so any in-flight submission is invalidated:

   const handleOpenChange = (open: boolean) => {
     if (!open) {
       close();
       setMessage('');
       setFormState('idle');
+      submitIdRef.current++;
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleOpenChange = (open: boolean) => {
if (!open) {
close();
setMessage('');
setFormState('idle');
}
};
const handleSubmit = async () => {
if (!message.trim() || formState === 'sending') return;
setFormState('sending');
try {
await submitFeedback({
data: {
category,
message: message.trim(),
context: {
route: window.location.pathname,
userAgent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`,
replayId: category === 'bug' ? getSentryReplayId() : undefined,
},
},
});
track('Feedback:Submitted', { category });
setFormState('sent');
setTimeout(() => handleOpenChange(false), CLOSE_AFTER_SENT_MS);
} catch {
setFormState('error');
}
};
const submitIdRef = useRef(0);
const handleOpenChange = (open: boolean) => {
if (!open) {
close();
setMessage('');
setFormState('idle');
submitIdRef.current++;
}
};
const handleSubmit = async () => {
if (!message.trim() || formState === 'sending') return;
setFormState('sending');
const submitId = ++submitIdRef.current;
try {
await submitFeedback({
data: {
category,
message: message.trim(),
context: {
route: window.location.pathname,
userAgent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`,
replayId: category === 'bug' ? getSentryReplayId() : undefined,
},
},
});
if (submitId !== submitIdRef.current) return; // superseded by a later open/close
track('Feedback:Submitted', { category });
setFormState('sent');
setTimeout(() => handleOpenChange(false), CLOSE_AFTER_SENT_MS);
} catch {
if (submitId !== submitIdRef.current) return;
setFormState('error');
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/components/feedback/FeedbackDialog.tsx` around lines 45 -
76, The pending submission in FeedbackDialog can still update state after the
dialog has been dismissed, causing stale sent state to leak into a later
session. Add a submission/session guard in handleSubmit and handleOpenChange so
closing the dialog invalidates any in-flight request before it resolves. Use a
ref-based submission id in FeedbackDialog, increment it when starting submit and
again inside handleOpenChange(false), then only run the success path
(setFormState('sent') and the auto-close timeout) if the id still matches.

Comment on lines +35 to +52
const [{ count: recentCount }] = await db
.select({ count: count() })
.from(feedback)
.where(
and(eq(feedback.userId, userId), gt(feedback.createdAt, new Date(Date.now() - 3600_000))),
);
if (recentCount >= MAX_SUBMISSIONS_PER_HOUR) {
throwDomainError(SYSTEM_ERRORS.RATE_LIMITED);
}

const id = crypto.randomUUID() as FeedbackId;
await db.insert(feedback).values({
id,
userId,
category: data.category,
message: data.message,
context: data.context ? JSON.stringify(data.context) : null,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does Cloudflare D1 support db.transaction() with drizzle-orm, or is a workaround needed for check-then-insert patterns?

💡 Result:

Cloudflare D1 does not support standard SQL transactions (e.g., BEGIN TRANSACTION or SAVEPOINT), and attempting to use db.transaction() with Drizzle ORM on D1 will typically fail or result in errors [1][2][3][4]. Because D1 operates in auto-commit mode, it does not provide the ACID guarantees associated with traditional database transactions [5][3][6]. To handle atomic operations or check-then-insert patterns, you must use D1's native db.batch() API instead of db.transaction() [3][4][6][7]. Key Workarounds and Patterns: 1. Use db.batch: The batch API allows you to send multiple SQL statements in a single request [3][6][7]. D1 executes these sequentially and atomistically; if one statement fails, the entire batch sequence is aborted [3][4]. However, because batch is not a traditional transaction, you generally cannot use the results of one statement to influence the logic of subsequent statements within the same batch [4][6]. 2. Pre-generate IDs: Since you cannot rely on the result of a previous insert (e.g., a generated ID) within the same db.batch() call, the recommended practice is to generate IDs (such as UUIDs) on the client side before executing the batch [6][7]. 3. Leverage.returning: For single-statement operations, you can use .returning() (supported by D1) to retrieve inserted data immediately, which helps minimize the need for multi-step checks in some scenarios [6][7]. In summary, db.transaction() is not supported, and db.batch() is the required mechanism for grouping operations in Cloudflare D1 [5][3][4][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and surrounding server functions.
git ls-files 'packages/web/src/server/functions/feedback.server.ts' \
  'packages/web/src/server/functions/*.ts' \
  'packages/web/src/server/**/*.ts'

echo
echo "---- outline ----"
ast-grep outline packages/web/src/server/functions/feedback.server.ts --view expanded

echo
echo "---- targeted read ----"
sed -n '1,220p' packages/web/src/server/functions/feedback.server.ts

Repository: InfinityBowman/corates

Length of output: 9799


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- feedback test outline ----"
ast-grep outline packages/web/src/server/functions/__tests__/feedback.server.test.ts --view expanded

echo
echo "---- feedback tests ----"
sed -n '1,260p' packages/web/src/server/functions/__tests__/feedback.server.test.ts

echo
echo "---- feedback schema ----"
sed -n '1,260p' packages/web/src/server/lib/dbTables.ts

Repository: InfinityBowman/corates

Length of output: 6346


Rate-limit check needs atomicity
count() and insert() are separate statements, so two concurrent submissions from the same user can both pass the cap before either row is written. That can exceed the 5/hour limit and spam the inbox; on D1 this likely needs per-user serialization or a different atomic counter pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/server/functions/feedback.server.ts` around lines 35 - 52,
The rate-limit logic in feedback.server.ts is non-atomic because the existing
count() query and db.insert(feedback) run as separate steps, so concurrent
requests can both pass MAX_SUBMISSIONS_PER_HOUR. Update the submit flow in the
feedback server function to use an atomic per-user guard or serialization
approach around the recent submission check and insert, keeping the existing
symbols db, feedback, recentCount, MAX_SUBMISSIONS_PER_HOUR, and
throwDomainError in view while changing the implementation so only one
submission decision can happen at a time for a user.

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.

In-app feedback widget and usage-event pass

2 participants