Internal bug reporting platform for staging / UAT environments.
Stage Bug Reporter lets approved testers report bugs from inside a stage web app — with optional screenshots, structured repro details and technical metadata — and submits each report as a GitHub Issue through a secure, server-side backend.
GitHub credentials (personal access token / GitHub App private key) must never be exposed to a browser. By hosting a dedicated backend service (e.g. https://bugreport.betait.no) we:
- Keep all GitHub auth server-side.
- Apply server-level rate limiting and input validation.
- Centralise screenshot / attachment handling in one place.
- Serve multiple stage apps from a single backend deployment.
An in-app widget (injected via a small SDK) gives testers the full browser context at the time of the bug: the current URL, viewport, locale, console errors, etc. A separate popup page would lose that context and require the tester to copy details manually.
| Role | URL |
|---|---|
| Backend API | https://bugreport.betait.no |
| Widget consumer | https://stage.reginor.events |
The stage.reginor.events app imports @bugreport/widget-sdk, calls initBugReporter({ apiBaseUrl: 'https://bugreport.betait.no', ... }) and the widget handles the rest.
/
apps/
bugreport-api/ ← Express backend – the only service with GitHub creds
packages/
widget-sdk/ ← Browser SDK (DOM-based, no React dependency)
shared-types/ ← Shared TypeScript contracts (request/response shapes)
scripts/ ← Helper scripts (label creation, etc.)
docs/ ← Architecture, API contract, issue format, roadmap
.github/ ← CI workflow & GitHub issue templates
┌─────────────────────────┐ HTTPS POST /api/reports/bug
│ stage.reginor.events │ ──────────────────────────────────────▶ bugreport.betait.no
│ (widget-sdk embedded) │ │
└─────────────────────────┘ │ Octokit
▼
GitHub Issues API
- Tester clicks the floating bug-report button inside the stage app.
widget-sdkcollects browser context (URL, viewport, browser, locale, console errors, failed network requests).- Tester fills in the form, optionally captures a full-page screenshot or attaches a file.
- SDK posts
multipart/form-datato the backend. - Backend validates the payload (Zod), stores the screenshot (S3/R2 or local disk), then creates a GitHub Issue via Octokit.
- Backend returns the new issue URL to the widget.
| Tool | Minimum version |
|---|---|
| Node.js | 20+ |
| pnpm | 8+ |
GitHub CLI (gh) |
any (only for label creation script) |
pnpm installcp apps/bugreport-api/.env.example apps/bugreport-api/.envOpen apps/bugreport-api/.env and fill in the required values:
| Variable | Description |
|---|---|
GITHUB_OWNER |
GitHub organisation or user that owns the target repo |
GITHUB_REPO |
Repository where issues will be created |
GITHUB_TOKEN |
Personal access token or GitHub App installation token (minimum scope: issues:write) |
All other variables have sensible defaults for local development.
The bug reporter expects specific labels in the target repository. Run the helper script once:
./scripts/create-github-labels.sh <owner> <repo>This is idempotent — re-running it skips labels that already exist.
pnpm --filter bugreport-api devThe API starts at http://localhost:3001 by default.
import { initBugReporter } from '@bugreport/widget-sdk';
initBugReporter({
apiBaseUrl: 'http://localhost:3001',
environment: 'staging',
enabled: true,
appVersion: '1.0.0',
commitSha: process.env.VITE_COMMIT_SHA,
currentUser: {
id: 'tester-001',
email: 'tester@example.com',
role: 'qa',
},
});The widget auto-injects a floating "🐛 Report bug" button and handles everything else.
By default, screenshots are saved to the local uploads/ directory (development only).
For production, set up S3-compatible storage:
STORAGE_PROVIDER=s3 # or "r2" for Cloudflare R2
STORAGE_BUCKET=bugreport-screenshots
STORAGE_REGION=eu-north-1
STORAGE_ACCESS_KEY=AKIA...
STORAGE_SECRET_KEY=...
# AWS S3: leave STORAGE_ENDPOINT empty
# Cloudflare R2: set to https://<account_id>.r2.cloudflarestorage.com
STORAGE_ENDPOINT=
# Public URL base (optional). If set, screenshots are served from this domain.
# If not set, pre-signed GET URLs are generated instead.
STORAGE_PUBLIC_URL_BASE=https://cdn.bugreport.betait.no
# Pre-signed URL expiry (seconds). Default: 7 days. Only used when STORAGE_PUBLIC_URL_BASE is not set.
STORAGE_SIGNED_URL_EXPIRES=604800Per-tester rate limiting works out of the box with an in-memory sliding-window counter. This is fine for single-process deployments.
For multi-process / horizontally-scaled deployments, set REDIS_URL:
REDIS_URL=redis://localhost:6379
# or with auth: redis://:password@host:6379/0When REDIS_URL is set, the rate limiter uses Redis INCR + EXPIRE for atomic, distributed counting. It fails open — if Redis goes down, requests are allowed (the IP limiter still applies).
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_PER_USER_PER_HOUR |
20 |
Max bug reports per tester per hour |
RATE_LIMIT_PER_IP_PER_HOUR |
50 |
Max bug reports per IP per hour |
REDIS_URL |
(empty) | Redis connection URL for distributed rate limiting |
The widget includes a "📸 Capture screen" button that captures the current page using html2canvas. The library is lazy-loaded from a CDN the first time the button is clicked — zero impact on initial page load.
No setup required. The feature works automatically. If html2canvas fails to load (e.g. CSP restrictions), the user can still attach a screenshot file manually.
To bundle html2canvas yourself instead of loading from CDN:
npm install html2canvas
# The SDK will detect window.html2canvas and use it directly.The SDK automatically hooks console.error when the widget is enabled, keeping a rolling buffer of the last 20 errors. These are serialised as JSON and attached to every bug report as optionalClientErrors.
No setup required. Captured automatically by initBugReporter().
The SDK intercepts fetch() and XMLHttpRequest to record any requests that return 4xx/5xx status codes or fail entirely. A PerformanceObserver catches additional edge cases. Results are attached as failedNetworkRequests.
No setup required. Captured automatically by initBugReporter().
The API creates issues with these labels:
| Label | Purpose |
|---|---|
bug |
Standard GitHub bug label |
stage |
Marks the issue as coming from a staging environment |
needs-triage |
Signals that it needs human review |
severity:blocker / severity:high / severity:medium / severity:low |
Severity level |
Create them with:
./scripts/create-github-labels.sh <owner> <repo>The repository includes .github/workflows/ci.yml which runs:
- Typecheck —
pnpm typecheck - Tests —
pnpm test - Lint —
pnpm lint(continue-on-error)
Triggered on pushes to main and copilot/** branches, and on pull requests to main.
| Command | Description |
|---|---|
pnpm install |
Install all workspace dependencies |
pnpm dev |
Start API + widget in watch mode (parallel) |
pnpm --filter bugreport-api dev |
Start only the API |
pnpm typecheck |
Type-check all packages |
pnpm test |
Run all tests (67 tests across 11 files) |
pnpm lint |
Lint all packages |
pnpm format |
Format all files with Prettier |
See apps/bugreport-api/.env.example for the full list with descriptions.
| Variable | Required | Default | Description |
|---|---|---|---|
GITHUB_OWNER |
✅ | — | GitHub org/user |
GITHUB_REPO |
✅ | — | Target repository |
GITHUB_TOKEN |
✅ | — | PAT or GitHub App token |
PORT |
3001 |
API port | |
ALLOWED_ORIGINS |
http://localhost:3000 |
CORS origins (comma-separated) | |
STORAGE_PROVIDER |
local |
local, s3, or r2 |
|
STORAGE_BUCKET |
— | S3 bucket name | |
STORAGE_REGION |
— | AWS region | |
STORAGE_ENDPOINT |
— | Custom S3 endpoint (R2/MinIO) | |
STORAGE_ACCESS_KEY |
— | S3 access key | |
STORAGE_SECRET_KEY |
— | S3 secret key | |
STORAGE_PUBLIC_URL_BASE |
— | Public URL base for screenshots | |
STORAGE_SIGNED_URL_EXPIRES |
604800 |
Pre-signed URL TTL (seconds) | |
REDIS_URL |
— | Redis URL for distributed rate limiting | |
RATE_LIMIT_PER_USER_PER_HOUR |
20 |
Per-tester limit | |
RATE_LIMIT_PER_IP_PER_HOUR |
50 |
Per-IP limit | |
MAX_UPLOAD_MB |
5 |
Max screenshot size (MB) |
See docs/ROADMAP.md for the full feature backlog.