Skip to content

fix(feedback): stop the Sentry report button covering "Next" on mobile - #577

Merged
guillermoscript merged 2 commits into
masterfrom
worktree-fix-mobile-sentry-feedback-button
Jul 28, 2026
Merged

fix(feedback): stop the Sentry report button covering "Next" on mobile#577
guillermoscript merged 2 commits into
masterfrom
worktree-fix-mobile-sentry-feedback-button

Conversation

@guillermoscript

@guillermoscript guillermoscript commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What

Sentry's feedback widget no longer plants a fixed button in the bottom-right corner, where it sat on top of the lesson footer's Next button on mobile. The trigger is ours now: a 44px puck that rests out of the way, can be dragged anywhere, and remembers where you put it. A "Report a problem" item was also added to the user menu.

Closes #

Why

Sentry.feedbackIntegration() defaults to autoInject: true, which injects a 50px actor button at bottom-right. At 390×844 that lands on the lesson footer's primary action:

element rect
Sentry actor (324, 778, 50×50)
Lesson Next (338, 796, 40×40)
intersection 72% of the Next button's area

So tapping to advance a lesson opened a bug report instead. Reporting a bug is a rare action and shouldn't hold prime thumb real estate at all.

Making it draggable alone wouldn't have been enough — that just delegates the collision to the user, on every device, forever. So this changes the default and adds drag:

  • Default position is the right edge at 62% height, clear of both the dashboard header and any bottom action bar (measured clearance from Next: 259px).
  • Draggable, snaps to the nearest edge, clamps inside env(safe-area-inset-*) (read as pixels off a zero-size probe element, so notches and the iOS home indicator are handled without device guesses), and persists position in localStorage as a fraction of the range so rotation and resize keep the relative spot.
  • Keyboard-operable: opening lives on click, not pointerup, so Enter and Space work; a drag sets a flag the trailing click consumes, and that flag is cleared on every pointerdown so a browser that skips click can't swallow the next tap. Arrow keys reposition it, since dragging is pointer-only.
  • Menu entry in components/user-nav.tsx is the discoverable path, at zero screen cost.
  • Position moves via transform only, never a layout property. The transitioned property list drops transform mid-drag so the puck tracks the finger exactly and eases (ease-out-quart, 200ms) only when snapping home. prefers-reduced-motion honoured.

Two adjacent bugs fixed in files already being touched:

  • The dialog was English-only for Spanish tenants. It's built outside React so it can't reach next-intl, but <html lang> is server-rendered and readable at init.
  • The dialog followed the OS colour scheme, not the app's — wrong whenever a user had overridden the theme in-app. It now syncs to resolvedTheme.

How to QA

Mobile viewport (390×844) is the point; the collision doesn't appear on desktop.

  1. npm run db:reset && npm run dev, then open default.lvh.me:3000 and log in as student@e2etest.com / password123.
  2. Go to Dashboard → your course → Lesson 1 (/dashboard/student/courses/1001/lessons/1001) with the viewport at 390×844.
  3. The Next arrow at bottom-right is fully visible and tappable. The feedback puck is the faint bug icon on the right edge, about two-thirds down.
  4. Drag the puck to the left side and release — it snaps to the left edge. Navigate to another lesson: it's still where you left it.
  5. Tap the puck (without dragging) — the Sentry form opens, titled "Report a problem".
  6. Keyboard: Tab to the puck, press Enter (form opens), Esc, then arrow keys to move it between edges and up/down.
  7. Open the avatar menu → "Report a problem" opens the same form.
  8. Switch to /es/... and repeat step 5 — the form should read "Reportar un problema" / "Enviar reporte".
  9. Toggle the app's light/dark switch while the OS is on the opposite scheme, then open the form — it should match the app, not the OS.

Screenshots / GIF

390×844, lesson page. The before frame reproduces master exactly: autoInject flipped back on and our own puck hidden, so only Sentry's actor is present.

Before — Sentry's actor covering Next After — Next clear, puck parked out of the way

Checklist

  • npm run typecheck and npm run test:unit pass (611 tests, 49 files)
  • npm run build passes
  • Every new tenant-scoped query filters by tenant_id (or the table genuinely has no such column) — n/a, no queries added
  • Tested with every relevant role (student / teacher / admin) — verified as student; the puck and menu entry are role-independent and mount in the shared [locale] layout / user nav
  • Loading and error states handled — the puck doesn't render until the integration is live, and a failed createForm returns false instead of appearing broken
  • New UI strings added to both messages/en.json and messages/es.json
  • Migration (if any) applies cleanly — n/a, no migration

Verification notes

  • Bug reproduced with autoInject flipped back on (overlaps: true, 72% by intersection math), gone after (overlaps: false, 259px clearance), Sentry's actor absent from the DOM.
  • Puck lands at the computed spot: x=334 (390−44−12), y=493 (12 + 0.62×776), 44×44 touch target.
  • Drag → snaps to left edge (x=12), persists {"side":"left","yFraction":0.723}, survives navigation.
  • Tap → full native pointerdown → mousedown → pointerup → mouseup → click, dialog created and opened.
  • Keyboard → ArrowRight/Left switch edges, ArrowUp/Down move 62px per press, persisted.
  • 0 console errors, no hydration warnings. Availability and reduced-motion read through useSyncExternalStore with a false server snapshot, so the puck can't appear in the first client render where the server rendered nothing.

Not addressed here

The SDK also logs ACTION REQUIRED: export an onRouterTransitionStart hook from instrumentation-client. Pre-existing and unrelated to this collision, so I left it rather than widen the diff. One line to add if you want it here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FJ4dqwXuyKUPtg11ni5BSb

Sentry's feedback integration ran with `autoInject: true`, which plants a
fixed 50px actor button in the bottom-right corner. On a 390x844 viewport
that landed on top of the lesson footer's primary action: the actor at
(324,778,50x50) covered roughly 72% of the "Next" button at (338,796,40x40),
so tapping to advance a lesson opened a bug report instead.

Reporting a bug is a rare action, so it no longer holds prime thumb real
estate. `autoInject` is off and the trigger is ours:

- `components/shared/feedback-button.tsx` renders a 44px icon-only puck that
  rests dimmed (opacity .45) on the right edge at 62% height, clear of both
  the dashboard header and any bottom action bar. Measured clearance from
  "Next" is 259px.
- It can be dragged anywhere, snaps to the nearest edge, clamps inside the
  viewport (respecting `env(safe-area-inset-*)` via a probe element), and
  remembers where it was parked in localStorage.
- Opening lives on `click`, not `pointerup`, so Enter and Space work; a drag
  sets a flag that suppresses the trailing click. Arrow keys reposition it,
  since dragging is pointer-only.
- Position moves via `transform` only, never a layout property, and the
  transition list drops `transform` mid-drag so the puck tracks the finger
  exactly and eases only when snapping home. Reduced motion is honoured.
- `components/user-nav.tsx` gains a "Report a problem" item: the
  discoverable entry point, at zero screen cost.

Availability and reduced-motion are read through `useSyncExternalStore` with
a `false` server snapshot, so the puck can't appear in the first client
render where the server rendered nothing.

The dialog itself was English-only for Spanish tenants; it's built outside
React so it can't reach next-intl, but `<html lang>` is server-rendered and
readable at init, so the form strings are now localized too. The dialog also
follows the app's resolved theme rather than the OS, which was wrong whenever
a user had overridden the theme in-app.

Verified in Chrome at 390x844 against the seeded student on a real lesson
page: bug reproduced with `autoInject` flipped back on (overlap true), then
gone after (overlap false); drag snaps and persists across navigation;
keyboard nudges move it 62px per press; dialog opens and reads correctly in
both en and es. Build, typecheck and lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJ4dqwXuyKUPtg11ni5BSb
@guillermoscript
guillermoscript marked this pull request as ready for review July 27, 2026 21:27
@guillermoscript guillermoscript added the bug Something isn't working label Jul 27, 2026
@guillermoscript guillermoscript self-assigned this Jul 27, 2026
Captured at 390x844 on /dashboard/student/courses/1001/lessons/1001 as the
seeded student. The before frame reproduces master exactly: `autoInject`
flipped back on and our own puck hidden, so only Sentry's actor is present.
Intersection math on that frame puts 72% of the "Next" button under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJ4dqwXuyKUPtg11ni5BSb
@guillermoscript
guillermoscript merged commit c280432 into master Jul 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant