timerise-ai/in-app-bug-reports

in-app-bug-reports

Build an in-app bug report module in a Next.js App Router app that files every report as a GitHub issue: members of any tenant report with a title, markdown and attachments, the issue's state comes back as the report's status, the team answers with /reply c…

View source
Original skill document

Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.

In-app bug reports bridged to GitHub Issues

Any member of any tenant reports a bug from inside the app; the product team works it in GitHub Issues; the member sees the status and the team's replies without leaving the app.

The idea the design turns on: the local row is the record, the tracker follows it, and the tracker owns the status. A report is saved under the tenant's policies first and reaches GitHub through an outbox, so a GitHub outage delays it and loses nothing. Status is written only from GitHub's own state, so the app and the tracker can never disagree about whether a bug is fixed.

Written by the engineer who has shipped this module, audited against the earlier implementation, the bug report bridge of a multi-tenant console on Next.js 16, Vercel and Supabase. The templates hold what such a bridge must: one issue per report however often a job retries, no team comment shown to a tenant without an explicit opt-in, no attachment reachable without a sign-in, and workers that run on any host. provenance.md has the record.

When to use

  • A product wants its users to report bugs from inside the app, into the repository the team uses.
  • The team wants to answer and close reports in GitHub and have the app reflect it.
  • An assistant in the app should help write a report without being able to send one.

When NOT to use

  • Crash and exception capture: an error monitoring service. This module files what a person writes.
  • Help articles and search: the help-center-markdown skill; link its pages to this form.
  • Customer support conversations: a support inbox. Reports here go to engineers, not to agents.
  • Assistants in Slack: the slack-ai-bot skill, which carries its own approval round trip.

Architecture

  member ---> /bug-reports/new ---> createReport ---> bug_reports (RLS, insert only)
                |  files: prepareUpload -> signed URL -> private bucket -> claimed on save
                v
             bug_report_jobs (outbox) <--- after() now, cron every 5 min --- processDueJobs
                |  create_issue / post_comment, marker de-dup on retry
                v
           GitHub App (installation token: one repo, issues only) ---> issue #123
                                                                         |
  member <--- notifications <--- applyStatus / mirror /reply <--- POST /api/github/webhook
                                     ^                                (HMAC, ledger, inline)
                                     +--- reconcileStatuses (cron backstop, cursor)
  operator ---> /operator/bug-reports: every tenant, bridge health, retry, reply as the team
  assistant ---> draft_bug_report (reads nothing, writes nothing) ---> card ---> createReport

Critical facts

  1. Vercel Cron calls the deployment's own `.vercel.app` host.** A proxy that resolves the tenant

from the host answers every cron 404, and no worker ever runs. List the cron paths in CRON_PATHS and pass them before tenant resolution with the block in outbox.md as written; the test keeps the list equal to vercel.json. Only the webhook may join that bypass, beside it.

  1. The webhook URL must be a host the proxy serves without a tenant. On a tenant-resolving proxy

an unconfigured operator host answers "Domain not configured" and GitHub's deliveries all 404.

  1. GitHub does not redeliver a failed webhook by itself. Process the delivery inline and answer

500 on failure, so it shows in the App's delivery log; the ledger makes "Redeliver" safe.

  1. Search does not index HTML comments. Find a report's marker by listing the label's issues;

a search for the marker returns nothing and the retry opens a duplicate.

  1. `state_reason` is what says resolved. closed plus not_planned or duplicate is not a fix.
  2. A Server Action body stops at 1 MB. Files go from the browser to a signed upload URL; the

action only reserves a row and later claims it, after checking the object really landed.

  1. GitHub's image proxy cannot fetch a login-gated URL. Attachments are links, not inline images.
  2. An ignored delivery still answers 200, or GitHub marks the hook failing. Record why it was

ignored, so a misconfigured installation shows on the health view instead of looking healthy.

  1. The installation token is requested narrowed to one repository and to issues, so it cannot

touch code even if the App is later granted more.

Hard rules

Never let the app write a status. No UPDATE policy for members, no close button for anyone. GitHub's state is the only writer, through the webhook and the reconcile pass.
Never show a tracker comment to a tenant unless it opts in. A comment is visible only when its first line starts with /reply and its author is on the team. Mirroring everything except an /internal marker leaks the first time someone forgets it.
Never call the tracker inside the request that saves the report. Save, enqueue, return; the outbox sends. A report refused because GitHub is down is a report lost.
Never publish an attachment. Private bucket, a 404 for everyone but an operator or the report's tenant, a 60-second signed URL, no SVG or HTML.
Never let the assistant send. The draft tool reads nothing and writes nothing; the member's click on the card runs the same Server Action as the form.

Quick start

Copy each code block verbatim to the path on its first line. You write server/bug-reports/host.ts bodies, components/bug-reports/host-ui.tsx, the two SQL functions in adaptation.md, the renames and the strings. A template that looks redundant is not trimmed: provenance.md says why it is there. Nor is one extended: no polling, no dynamic export, no config route for the NEXT_PUBLIC_ values, no check or retry it does not ship. Install what the host lacks of zod, @supabase/supabase-js, @supabase/ssr, server-only and vitest (ai only with an assistant) with its package manager. The package registry is not an external service, and no dependency is replaced by hand-written code or dropped from a template.

  1. Probe the host and fill in the seams, see adaptation.md.
  2. Create the tables, policies, outbox and bucket, see data-model.md.
  3. Add the client, the tracker and .env.example with all eleven names, see github-app.md.
  4. Add the outbox, the worker, the cron and the proxy bypass, see outbox.md.
  5. Add the webhook, see webhook.md.
  6. Add uploads and the attachment route, see attachments.md.
  7. Add the read models and the actions, see actions.md.
  8. Build the form, the list and the thread, see member-ui.md.
  9. Wire the notifications, see notifications.md.
  10. Add the assistant's draft tool, see assistant-tool.md.
  11. Run the nine suites unmodified under vitest, reporting 50, see testing.md.
  12. Add the operator view and walk the go-live list, see operations.md.

End by telling the operator three things: the bridge is off, and reports are saved and wait, until the first six variables in github-app.md are set; the migration, the two app functions and the GitHub App are theirs to apply and register; and while currentMember and currentOperator answer null, every page is a 404.

Reference directory

ScenarioTrigger keywordsReference
Fitting it to a host appseam, BugReportsHost, host.ts, host-ui, rename, tenant, member, operator, i18n, stringsadaptation.md
Tables and policiesbug_reports, RLS, insert only, per-tenant number, advisory lock, outbox, bucketdata-model.md
The GitHub sideGitHub App, env, .env.example, JWT, RS256, installation token, 401, 422, 429, rate limit, IssueTracker, marker, issue bodygithub-app.md
Sending and retryingoutbox, claim, lease, MAXATTEMPTS, duplicate issue, reconcile, cursor, Vercel Cron, 404, proxy, CRONPATHSoutbox.md
Hearing backwebhook, X-Hub-Signature-256, ping, X-GitHub-Delivery, redelivery, state_reason, /reply, echo, Domain not configuredwebhook.md
Filesupload, 1 MB, createSignedUploadUrl, uploadToSignedUrl, paste screenshot, signed URL, 404 not 403, SVGattachments.md
Reads and writesServer Action, ActionResult, keyset pagination, cursor, rate limit, createReport, addComment, retrySyncactions.md
The screensform, markdown preview, list, thread, help page button, Not a menu section, privacy hintmember-ui.md
Telling peoplebell, push, notifyMembers, tag, mark read, reporter, commentersnotifications.md
The assistantdraftbugreport, AI SDK tool, tool part, output-available, confirm card, sessionStorage, prompt injectionassistant-tool.md
Proving itvitest, tests, npm test, exclude, server-only stub, fake tracker, integrationtesting.md
Running itsetup, env, preview label, one App per project, health, stale worker, failed sync, rotate keyoperations.md
What the audit changedaudit, fixed, kept deliberately, added, fix orderprovenance.md

Part of the Timerise Skills index, which lists the sibling skills.