# Bug reporting: Bugpin (user) + Bugsink (auto)

**Status:** Approved design, ready for implementation plan.
**Date:** 2026-05-17

## Goal

Let visitors report bugs in the Halfstreet site with a screenshot, and capture uncaught JavaScript errors automatically. Reports should flow to systems already running on the `half.st` host so there is no new infrastructure to manage.

## Two distinct flows

| | Bugpin | Bugsink |
|---|---|---|
| Trigger | Visitor clicks "Report a Bug" in footer | Uncaught JS error |
| Capture | Screenshot, annotations, free-text note, console/network metadata | Stack trace, breadcrumbs |
| Destination | Bugpin portal → auto-creates a GitHub Issue in the halfstreet repo | Bugsink portal only |
| Library | `bugpin.half.st/widget.js` (lazy-loaded on click) | `@sentry/browser` npm package (loads on every page) |

The two are intentionally separate. Bugpin owns user-facing reports; Bugsink owns automatic exception capture. Sentry's user-feedback widget is **not** used — that would overlap Bugpin and muddle the responsibilities.

## Components

### 1. Footer link

`src/pages/index.astro`

Add "Report a Bug" to the footer chain after the existing links. Render as a `<button>` styled to match the surrounding `<a>` elements so it can dispatch JS without a hash-link navigation.

Final order:
`© 2026 Ethan J Lewis | GNU 3.0 | Build #<n> | Source Code | Report a Bug`

The button only renders when `world.ui.bugReport?.enabled` is true and a Bugpin server URL is configured. If either is missing, the button is omitted (local dev stays clean).

### 2. Config

`src/world/ui.md` — add a `bugReport` frontmatter block:

```yaml
bugReport:
  enabled: true
  label: "Report a Bug"
  bugpin:
    serverUrl: "https://bugpin.half.st"
    apiKey: "proj_07df4bf91f12445b8ef8c723e865ed7b"
  bugsink:
    enabled: true
    dsn: "https://231ef18b6b4f426ca249778cfddf821c@bugsink.half.st/1"
```

The Bugpin project API key and the Bugsink DSN are both designed to ship to clients (they are public credentials that authenticate the project, not the operator), so checking them into the repo is acceptable.

`src/world/schema.ts` — extend `uiFrontmatterSchema` with an optional `bugReport` object:

```ts
bugReport: z.object({
  enabled: z.boolean().default(false),
  label: z.string().trim().min(1).default('Report a Bug'),
  bugpin: z.object({
    serverUrl: z.url(),
    apiKey: z.string().trim().min(1),
  }).optional(),
  bugsink: z.object({
    enabled: z.boolean().default(true),
    dsn: z.url(),
  }).optional(),
}).optional(),
```

`src/world/types.ts` — extend `UiConfig` with the matching TypeScript shape.

### 3. Bugpin lazy loader

New module: `src/ui/bug-report.ts`

Responsibilities:
- Read Bugpin config from a `data-*` attribute on the footer button (so the module stays free of Astro imports).
- On first click, inject `<script src="${serverUrl}/widget.js" data-api-key="${apiKey}" async>` into `<head>`. Hide Bugpin's built-in floating button using whatever option the widget exposes (likely `data-hidden="true"` or a CSS rule scoped via the widget's Shadow DOM hook — confirm against the install guide during implementation).
- Once the script resolves, call `window.BugPin.open()`.
- Cache the loaded state on a module-level flag; subsequent clicks just call `open()` again.
- If the script fails to load (offline, server down), log to the console and surface a minimal inline message ("Couldn't open bug reporter — try refreshing"). Do not break the page.

Wire-up in `src/pages/index.astro`:
- The footer button gets `data-bug-report-trigger`, `data-bugpin-server`, `data-bugpin-key` attributes.
- A new `<script>` block imports `../ui/bug-report.ts` alongside the existing terminal/theme imports.

The lazy-load is deliberate: ~150 KB of widget code stays off the cold load for visitors who never click.

### 4. Bugsink init

New module: `src/ui/error-tracking.ts`

Responsibilities:
- Add `@sentry/browser` as a dependency.
- Read the DSN from a `data-bugsink-dsn` attribute set on `<body>` (or a meta tag) so the module is server-render-agnostic.
- On import, call:
  ```ts
  Sentry.init({
    dsn,
    tracesSampleRate: 0,
    replaysSessionSampleRate: 0,
    replaysOnErrorSampleRate: 0,
    integrations: [],
  })
  ```
- If `dsn` is missing or `bugsink.enabled` is false, no-op.

Error-capture only — no performance tracing, no session replay. Keeps the bundle small and avoids accidentally shipping a replay tool we didn't design for.

Wire-up in `src/pages/index.astro`:
- A new `<script>` block imports `../ui/error-tracking.ts` near the existing imports.

### 5. Bugpin GitHub wiring

Out of scope for the code change, but recorded here so it isn't lost during implementation:

1. In the halfstreet GitHub repo, create a classic personal access token (or fine-grained equivalent) with `repo` scope.
2. In Bugpin admin (https://bugpin.half.st/portal): Projects → halfstreet → Integrations → GitHub. Paste the token, select the halfstreet repo, set default labels (e.g., `bug`, `user-report`).
3. Verify by submitting a test report from the footer link and confirming an issue appears in GitHub.

## Out of scope

- **Markdown files under `src/world/bugs/`.** Original TODO #45 proposed this; explicitly dropped — Bugpin's portal + GitHub Issues replace that storage path.
- **Gitea mirroring.** Bugpin does not natively support Gitea. Deferred until there's clear value in a Bugpin → Gitea bridge. The front-end design does not preclude this; it would be a server-side addition on the Bugpin host.
- **Sentry user-feedback widget.** Bugpin owns user reports; Bugsink stays auto-only.
- **PII redaction policy.** Bugpin's widget includes a built-in blur tool; Bugsink only captures stack traces, not request bodies. No additional scrubbing layer.

## Risks / things to verify during implementation

- **Bugpin "hide default button" mechanism.** Docs mention a floating launcher; the exact opt-out attribute needs to be confirmed against the install guide or the widget source. If no opt-out exists, fall back to CSS that hides the widget's launcher selector inside its Shadow DOM root (or wrap with a custom-element rule).
- **Bugsink Sentry SDK compatibility.** Bugsink claims Sentry-SDK compatibility but tested feature surface varies. Verify with `@sentry/browser` v9 (current stable as of 2026-05) — if a specific minor version is needed, pin it.
- **Cold-start cost.** Bugsink adds ~30–40 KB gzipped to the page. Acceptable for an authored, low-traffic site; flag if budget gets tight.

## TODOs.md update

Replace line 45 with:

```
- [ ] Add a "Report a Bug" footer link backed by Bugpin (widget at bugpin.half.st → forwards to GitHub Issues). Add Bugsink (@sentry/browser → bugsink.half.st) for automatic JS error capture.
```

Mark complete once the implementation lands and a test report has appeared in both Bugpin's portal and GitHub.

## Files touched (preview)

- `src/world/ui.md` — add `bugReport` block.
- `src/world/schema.ts` — extend `uiFrontmatterSchema`.
- `src/world/types.ts` — extend `UiConfig`.
- `src/pages/index.astro` — render the button conditionally, wire data attributes, import the two new UI modules.
- `src/ui/bug-report.ts` — **new**, lazy loads the Bugpin widget on click.
- `src/ui/error-tracking.ts` — **new**, initializes Sentry SDK against Bugsink DSN.
- `package.json` / `package-lock.json` — add `@sentry/browser`.
- `src/world/TODOs.md` — rewrite line 45.
