Skip to content

[Ready for Review] Add Report Issue dialog UI and wire it to Settings menu with Bugsink configured#164

Open
eddie-wq07 wants to merge 2 commits intomainfrom
report-issue-dialog
Open

[Ready for Review] Add Report Issue dialog UI and wire it to Settings menu with Bugsink configured#164
eddie-wq07 wants to merge 2 commits intomainfrom
report-issue-dialog

Conversation

@eddie-wq07
Copy link

What this PR does

  • Adds a new ReportIssueDialog UI component
  • Wires the dialog into the Settings dropdown (replacing the previous disabled item)
  • Provides basic form fields (title, description, optional contact email)
  • Submit currently closes the dialog (no backend yet)

Why

This sets up the user-facing UI for reporting issues so we can get early
feedback on wording, layout, and interaction before adding backend support.

What’s not included yet

  • Backend submission (tRPC mutation + Bugsink integration)
  • Validation beyond basic form handling

How to test

  1. Run the app locally
  2. Open the Settings dropdown
  3. Click "Report Issue"
  4. Verify the dialog opens and submit closes it

Notes

Opening this PR early for UI/UX feedback before proceeding with backend work.

<DropdownMenuItem onSelect={() => NiceModal.show(ReportIssueDialog)}>
<Flag />
<span>
Report a Bug <i>(Coming soon)</i>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should do "Report an Issue" maybe, otherwise great work on the frontend

@eddie-wq07
Copy link
Author

Backend Integration - Bug Report to Bugsink

What this does:

  • Adds Bugsink container to docker-compose.dev.yaml for local bug tracking
  • Creates tRPC mutation bugReport.submit that validates and forwards reports to Bugsink
  • Integrates Sentry SDK (@sentry/nextjs) configured to send to local Bugsink instance
  • Wires frontend ReportIssueDialog to call the backend mutation
  • Adds SENTRY_DSN environment variable for Bugsink connection

Why:

  • Provides self-hosted bug tracking without external dependencies or paid services
  • Bugsink is Sentry-compatible, so we use the mature Sentry SDK ecosystem
  • All user-reported issues are centralized in one place for the dev team to review

What's included:

  • src/server/api/routers/bug-report-router.ts - tRPC router with validation
  • src/instrumentation.ts - Initializes Sentry SDK on server startup
  • Updated docker-compose.dev.yaml with Bugsink service on port 8000
  • Updated src/env.js to validate SENTRY_DSN
  • Updated frontend to call mutation with loading/success/error states

How to test:

  1. Run pnpm dev:up to start Bugsink container
  2. Access Bugsink UI at http://localhost:8000 login: admin/admin (Poor security, can change later)
  3. Run pnpm dev and open the app
  4. Submit a bug report via Settings → Report Issue
  5. Check Bugsink UI to see the report appear

Notes:

  • DSN points to localhost for development; needs production Bugsink deployment later
  • Reports sent via Sentry.captureMessage() with metadata (description, email) in extras
image

@eddie-wq07 eddie-wq07 marked this pull request as ready for review March 13, 2026 02:34
@eddie-wq07 eddie-wq07 changed the title WIP: Add Report Issue dialog UI and wire it to Settings menu [Ready for Review] Add Report Issue dialog UI and wire it to Settings menu with Bugsink configured Mar 13, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR adds the UI and partial backend wiring for a "Report Issue" feature — a ReportIssueDialog component hooked into the Settings dropdown that submits bug reports to a self-hosted Bugsink instance via the Sentry SDK. The UI integration is clean and the component follows established project patterns (NiceModal, react-hook-form, tRPC), but there are two issues that need to be resolved before merging:

  • Unauthenticated API endpoint: bugReportRouter.submit uses publicProcedure, meaning any unauthenticated caller can POST arbitrary data to the bug tracker. It should use authorizedProcedure to match the access model enforced by the UI.
  • Required SENTRY_DSN breaks dev startup: Adding SENTRY_DSN as a mandatory env var will prevent the app from starting for any developer who hasn't yet stood up a local Bugsink instance, which is a regression for contributors not working on this feature.

Additional style suggestions:

  • Wrap Sentry.captureMessage() in a try/catch to surface friendlier errors to the user.
  • Export the shared Zod schema from the router and import it in the dialog to avoid schema duplication.
  • Reset the form when the dialog is closed without submitting, so stale input isn't shown on next open.
  • Lower tracesSampleRate from 1.0 for production to avoid excessive telemetry volume.

Confidence Score: 2/5

  • Not safe to merge — the unauthenticated bug-report endpoint and the required SENTRY_DSN env var are blocking issues.
  • Two logic-level issues need to be fixed before this can land: the publicProcedure on the submit endpoint allows unauthenticated abuse of the issue tracker, and making SENTRY_DSN a required env var will break app startup for every contributor without a local Bugsink instance. The UI wiring itself is solid, but these backend issues lower confidence significantly.
  • Pay close attention to src/server/api/routers/bug-report-router.ts (unauthenticated endpoint) and src/env.js (required SENTRY_DSN).

Important Files Changed

Filename Overview
src/server/api/routers/bug-report-router.ts New tRPC router for bug report submission — uses publicProcedure (unauthenticated), no error handling around Sentry.captureMessage, and duplicates the Zod input schema from the frontend.
src/env.js Adds required SENTRY_DSN env var — marking it required will break app startup for any developer without a local Bugsink/Sentry instance configured.
src/components/report-issue-dialog.tsx New NiceModal dialog with react-hook-form and Zod validation — form state is not cleared on cancel/dismiss, and the Zod schema is duplicated from the backend router.
src/components/settings/settings-dropdown.tsx Wires the new ReportIssueDialog into the Settings dropdown via NiceModal.show — straightforward integration, no issues.
src/instrumentation.ts New Next.js instrumentation hook that initialises Sentry on Node.js runtime — uses a 100% trace sample rate which will be expensive in production, and doesn't guard against an unset SENTRY_DSN.
src/server/api/root.ts Registers the new bugReportRouter — straightforward addition, no issues.
docker-compose.dev.yaml Adds the Bugsink container for local development — hardcoded SECRET_KEY is acceptable in this dev-only file given the inline comment, but worth confirming it is never promoted to production.
next.config.js Enables the instrumentationHook experimental flag required by Sentry — correct and expected change.

Last reviewed commit: d0028dc

});

export const bugReportRouter = createTRPCRouter({
submit: publicProcedure
Copy link
Contributor

Choose a reason for hiding this comment

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

Unauthenticated endpoint accepts arbitrary bug reports

The submit procedure uses publicProcedure, which means any unauthenticated caller — including bots or external scripts — can POST to this endpoint and inject arbitrary data into your Sentry/Bugsink instance. This opens the door to spam flooding the issue tracker and exhausting any API rate limits on the Sentry DSN.

The rest of the app uses authorizedProcedure for actions that should require a logged-in user. Since the dialog is only accessible from the Settings dropdown (which is already behind auth in the UI), using authorizedProcedure here aligns with that intent and closes the API-layer bypass:

Suggested change
submit: publicProcedure
submit: authorizedProcedure

You'll also need to add the import at the top of the file:

import { authorizedProcedure } from "@/server/api/procedures";

MINIO_USE_SSL: z.enum(["true", "false"]).transform((val) => val === "true"),

// Sentry / Bugsink
SENTRY_DSN: z.string().url(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Required SENTRY_DSN will break environments without Sentry configured

SENTRY_DSN is validated as a required z.string().url(), so the app will throw an env-validation error on startup for any developer who hasn't set up a Bugsink/Sentry instance locally. This is a regression — the app previously started fine without this variable.

Since the PR description explicitly notes that "backend submission is not included yet" and dev setups may not always have Bugsink running, this env var should be optional:

Suggested change
SENTRY_DSN: z.string().url(),
SENTRY_DSN: z.string().url().optional(),

The instrumentation.ts init will need a corresponding guard:

if (process.env.NEXT_RUNTIME === "nodejs" && env.SENTRY_DSN) {
  Sentry.init({ dsn: env.SENTRY_DSN, ... });
}

Comment on lines +15 to +28
.mutation(async ({ input }) => {
// Send to Bugsink via Sentry SDK
Sentry.captureMessage(input.title, {
level: "info",
tags: {
type: "user_bug_report",
},
extra: {
description: input.description,
email: input.email || "not provided",
},
});

return { success: true };
Copy link
Contributor

Choose a reason for hiding this comment

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

No error handling around Sentry.captureMessage

Sentry.captureMessage() can throw (e.g., if the SDK was never initialised because SENTRY_DSN is unset). Currently, any exception will bubble up as an opaque tRPC internal-server error, and the client's onError toast will show a confusing message. Wrapping the call in a try/catch lets you surface a friendlier error:

try {
  Sentry.captureMessage(input.title, {
    level: "info",
    tags: { type: "user_bug_report" },
    extra: {
      description: input.description,
      email: input.email || "not provided",
    },
  });
} catch (err) {
  throw new TRPCError({
    code: "INTERNAL_SERVER_ERROR",
    message: "Failed to submit bug report. Please try again later.",
    cause: err,
  });
}

Comment on lines +23 to +27
const ReportIssueSchema = z.object({
title: z.string().min(1, "Title is required"),
description: z.string().min(1, "Description is required"),
email: z.string().email("Invalid email").optional().or(z.literal("")),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicated Zod schema between frontend and backend

ReportIssueSchema in the dialog is an exact copy of SubmitBugReportInput in bug-report-router.ts. If the validation rules ever change (e.g., a minimum length for the description), they'll need to be updated in two places and can easily drift.

Consider exporting the schema from the router file (or a shared schemas/ location) and importing it here:

// src/server/api/routers/bug-report-router.ts
export const SubmitBugReportInput = z.object({ ... });

// src/components/report-issue-dialog.tsx
import { SubmitBugReportInput } from "@/server/api/routers/bug-report-router";
type ReportIssueSchemaType = z.infer<typeof SubmitBugReportInput>;

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +48 to +56
onSuccess: () => {
toast.success("Issue reported successfully. Thank you!");
modal.hide();
form.reset();
},
onError: (error) => {
toast.error(`Failed to report issue: ${error.message}`);
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Form data is not cleared when the dialog is dismissed without submitting

form.reset() is called only inside onSuccess. If a user partially fills the form and then closes the dialog via the Cancel button or the × icon, their draft input persists. The next time they open the dialog from the settings menu they'll see leftover text.

To clear state on every close, reset the form in the onOpenChange handler:

Suggested change
onSuccess: () => {
toast.success("Issue reported successfully. Thank you!");
modal.hide();
form.reset();
},
onError: (error) => {
toast.error(`Failed to report issue: ${error.message}`);
},
});
const submitBugReport = clientApi.bugReport.submit.useMutation({
onSuccess: () => {
toast.success("Issue reported successfully. Thank you!");
modal.hide();
form.reset();
},
onError: (error) => {
toast.error(`Failed to report issue: ${error.message}`);
},
});

And update the onOpenChange callback:

onOpenChange={(open) => {
  if (!open) {
    form.reset();
    modal.hide();
  } else {
    modal.show();
  }
}}

if (process.env.NEXT_RUNTIME === "nodejs") {
Sentry.init({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
Copy link
Contributor

Choose a reason for hiding this comment

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

tracesSampleRate: 1.0 is too high for production

A sample rate of 1.0 means every single request is traced, which can generate a large volume of data and incur significant costs on Sentry or storage overhead on Bugsink once real traffic flows through. Consider lowering this for production or making it configurable:

Suggested change
tracesSampleRate: 1.0,
tracesSampleRate: env.NODE_ENV === "production" ? 0.1 : 1.0,

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

2 participants