docs: implementation plan for bug reporting

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 23:02:46 -05:00
parent 4f6460297f
commit a51bb6f86f
@@ -0,0 +1,653 @@
# Bug Reporting Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a "Report a Bug" footer link that opens the Bugpin screenshot widget (forwards to GitHub Issues), plus auto error capture via Bugsink (Sentry-compatible) on every page.
**Architecture:** Two independent flows. Bugpin = user-initiated, lazy-loaded `<script>` widget from `bugpin.half.st`, triggered by a footer `<button>`. Bugsink = background, `@sentry/browser` SDK initialised on page load against a DSN at `bugsink.half.st`. Both credentials are public project tokens and live in `src/world/ui.md`. Both flows are no-ops when not configured.
**Tech Stack:** Astro 6, TypeScript, Zod 4, Vitest 4 (node env, no jsdom), `@sentry/browser` (new dep), Bugpin widget script.
**Spec:** `docs/superpowers/specs/2026-05-17-bug-reporting-design.md`
---
## File map
- `src/world/schema.ts`**modify**: extend `uiFrontmatterSchema` with optional `bugReport` block.
- `src/world/schema.test.ts`**modify**: add accept/reject cases for `bugReport`.
- `src/world/types.ts`**modify**: extend `UiConfig` to expose `bugReport`.
- `src/world/ui.md`**modify**: add `bugReport` frontmatter.
- `src/ui/error-tracking.ts`**create**: reads body data attrs, calls `Sentry.init`. No-op when DSN missing.
- `src/ui/bug-report.ts`**create**: wires footer button click to lazy-load Bugpin widget, then call `BugPin.open()`. No-op when config missing.
- `src/pages/index.astro`**modify**: render the footer button conditionally, attach data attrs to `<body>`, import the two new modules.
- `package.json` / `package-lock.json`**modify**: add `@sentry/browser`.
- `src/world/TODOs.md:45`**modify**: rewrite to reflect actual approach.
No DOM tests for the two new UI modules: the project's Vitest config runs in node with no jsdom. Coverage for those modules is `astro check` (type safety) + a manual smoke step at the end of the plan. Schema changes get full unit test coverage via the existing test file.
---
## Task 1: Extend `uiFrontmatterSchema` with `bugReport` (TDD)
**Files:**
- Modify: `src/world/schema.ts`
- Modify: `src/world/schema.test.ts`
- [ ] **Step 1: Add the failing tests**
Append these inside the existing `describe('uiFrontmatterSchema', ...)` block in `src/world/schema.test.ts` (just before the closing `})` on line 120):
```ts
it('accepts a bugReport block with bugpin and bugsink subconfigs', () => {
const data = {
pageTitle: 'Halfstreet',
description: 'A gothic mystery.',
footer: {
copyright: '© 2026 Ethan J Lewis',
links: [],
},
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',
},
},
}
expect(() => uiFrontmatterSchema.parse(data)).not.toThrow()
})
it('accepts ui config with no bugReport block at all', () => {
const data = {
pageTitle: 'Halfstreet',
description: 'A gothic mystery.',
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
}
expect(() => uiFrontmatterSchema.parse(data)).not.toThrow()
})
it('rejects a bugpin block with a non-url serverUrl', () => {
const data = {
pageTitle: 'Halfstreet',
description: 'A gothic mystery.',
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
bugReport: {
enabled: true,
bugpin: { serverUrl: 'not-a-url', apiKey: 'proj_x' },
},
}
expect(() => uiFrontmatterSchema.parse(data)).toThrow()
})
it('rejects a bugsink block with a non-url dsn', () => {
const data = {
pageTitle: 'Halfstreet',
description: 'A gothic mystery.',
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
bugReport: {
enabled: true,
bugsink: { dsn: 'whatever' },
},
}
expect(() => uiFrontmatterSchema.parse(data)).toThrow()
})
```
- [ ] **Step 2: Run tests, confirm they fail**
```
npm test -- src/world/schema.test.ts
```
Expected: the four new tests fail because `bugReport` is currently unknown (zod by default strips extras, so the "accepts" cases probably pass — but the "rejects" cases will fail because zod will accept anything for unknown fields). Either way, confirm the test runner reports at least the two "rejects" cases as failing.
- [ ] **Step 3: Add the schema fields**
In `src/world/schema.ts`, find `uiFrontmatterSchema` (starts at line 48). Add a `bugReport` field at the end of the object (after `features`, before the closing `})`). Final block:
```ts
export const uiFrontmatterSchema = z.object({
pageTitle: z.string().trim().min(1),
description: z.string().trim().min(1),
robots: z.string().trim().min(1).default('noindex'),
themeColor: z.string().trim().min(1).default('#1a0d00'),
footer: z.object({
copyright: z.string().trim().min(1),
copyrightHref: z.url().optional(),
buildLabel: z.string().trim().min(1).default('Build #'),
showBuild: z.boolean().default(true),
links: z.array(z.object({
label: z.string().trim().min(1),
href: z.url(),
})).default([]),
}),
features: z.object({
chips: z.boolean().default(true),
lightMeter: z.boolean().default(true),
typedEffect: z.boolean().default(true),
roomScroll: z.boolean().default(true),
}).default({
chips: true,
lightMeter: true,
typedEffect: true,
roomScroll: true,
}),
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(),
})
```
- [ ] **Step 4: Run tests, confirm pass**
```
npm test -- src/world/schema.test.ts
```
Expected: all `uiFrontmatterSchema` tests pass (the original two + the four new ones).
- [ ] **Step 5: Commit**
```
git add src/world/schema.ts src/world/schema.test.ts
git commit -m "feat(world): add bugReport block to ui schema"
```
---
## Task 2: Surface `bugReport` on the `UiConfig` type
**Files:**
- Modify: `src/world/types.ts:64-85`
- [ ] **Step 1: Extend the interface**
In `src/world/types.ts`, replace the existing `UiConfig` interface (currently lines 6485) with:
```ts
export interface UiConfig {
pageTitle: string
description: string
robots: string
themeColor: string
footer: {
copyright: string
copyrightHref?: string
buildLabel: string
showBuild: boolean
links: Array<{
label: string
href: string
}>
}
features: {
chips: boolean
lightMeter: boolean
typedEffect: boolean
roomScroll: boolean
}
bugReport?: {
enabled: boolean
label: string
bugpin?: {
serverUrl: string
apiKey: string
}
bugsink?: {
enabled: boolean
dsn: string
}
}
}
```
- [ ] **Step 2: Verify type check passes**
```
npx astro check
```
Expected: 0 errors, 0 warnings (or at least no new errors introduced by this change).
- [ ] **Step 3: Commit**
```
git add src/world/types.ts
git commit -m "feat(world): expose bugReport on UiConfig"
```
---
## Task 3: Add real config to `ui.md`
**Files:**
- Modify: `src/world/ui.md`
- [ ] **Step 1: Edit frontmatter**
Insert this block in `src/world/ui.md` between the `features:` block and the closing `---`, so the frontmatter ends like:
```yaml
features:
chips: true
lightMeter: true
typedEffect: true
roomScroll: true
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"
---
```
- [ ] **Step 2: Verify the world loads**
```
npm test -- src/world/buildWorld.test.ts
npm test -- src/world/loader.test.ts
```
Expected: all tests pass — the new frontmatter is valid.
- [ ] **Step 3: Commit**
```
git add src/world/ui.md
git commit -m "feat(world): wire bugpin + bugsink credentials in ui.md"
```
---
## Task 4: Install `@sentry/browser`
**Files:**
- Modify: `package.json`, `package-lock.json`
- [ ] **Step 1: Install**
```
npm install @sentry/browser
```
Expected: a single new dependency added under `"dependencies"` in `package.json`; lockfile updates.
- [ ] **Step 2: Verify**
```
node -e "require('@sentry/browser')"
```
Expected: no output and exit code 0.
- [ ] **Step 3: Commit**
```
git add package.json package-lock.json
git commit -m "chore: add @sentry/browser for bugsink integration"
```
---
## Task 5: Create the Bugsink init module
**Files:**
- Create: `src/ui/error-tracking.ts`
No unit test — Vitest runs in node with no jsdom, and this module only does work in a browser. Coverage is `astro check` (types) + the manual smoke at the end.
- [ ] **Step 1: Write the module**
```ts
// src/ui/error-tracking.ts
import * as Sentry from '@sentry/browser'
const dsn = document.body?.dataset.bugsinkDsn ?? ''
if (dsn) {
Sentry.init({
dsn,
tracesSampleRate: 0,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
integrations: [],
})
}
```
- [ ] **Step 2: Type-check**
```
npx astro check
```
Expected: 0 new errors.
- [ ] **Step 3: Commit**
```
git add src/ui/error-tracking.ts
git commit -m "feat(ui): initialise sentry browser sdk against bugsink dsn"
```
---
## Task 6: Create the Bugpin trigger module
**Files:**
- Create: `src/ui/bug-report.ts`
Module reads config from the footer button's `data-*` attributes, lazy-loads the Bugpin widget on first click, then calls `BugPin.open()`. Subsequent clicks just call `open()` again.
- [ ] **Step 1: Write the module**
```ts
// src/ui/bug-report.ts
interface BugPinAPI {
open: () => void
close: () => void
}
declare global {
interface Window {
BugPin?: BugPinAPI
}
}
const button = document.querySelector<HTMLButtonElement>('[data-bug-report-trigger]')
if (button) {
const serverUrl = button.dataset.bugpinServer
const apiKey = button.dataset.bugpinKey
if (serverUrl && apiKey) {
let loadPromise: Promise<void> | null = null
const ensureLoaded = (): Promise<void> => {
if (window.BugPin) return Promise.resolve()
if (loadPromise) return loadPromise
loadPromise = new Promise<void>((resolve, reject) => {
const script = document.createElement('script')
script.src = `${serverUrl.replace(/\/$/, '')}/widget.js`
script.async = true
script.dataset.apiKey = apiKey
script.addEventListener('load', () => resolve())
script.addEventListener('error', () => {
loadPromise = null
reject(new Error(`Failed to load Bugpin widget from ${script.src}`))
})
document.head.appendChild(script)
})
return loadPromise
}
button.addEventListener('click', async (event) => {
event.preventDefault()
button.disabled = true
try {
await ensureLoaded()
window.BugPin?.open()
} catch (err) {
console.error('[bug-report]', err)
} finally {
button.disabled = false
}
})
}
}
```
- [ ] **Step 2: Type-check**
```
npx astro check
```
Expected: 0 new errors.
- [ ] **Step 3: Commit**
```
git add src/ui/bug-report.ts
git commit -m "feat(ui): lazy-load bugpin widget on footer click"
```
---
## Task 7: Wire footer button and module imports in `index.astro`
**Files:**
- Modify: `src/pages/index.astro`
- [ ] **Step 1: Add config bindings to the frontmatter script**
In the `---` block at the top of `src/pages/index.astro`, after `const remainingFooterLinks = footerLinks.slice(1)`, add:
```ts
const bugReport = ui?.bugReport
const bugpinConfig = bugReport?.enabled ? bugReport.bugpin : undefined
const bugsinkDsn =
bugReport?.enabled && bugReport.bugsink?.enabled ? bugReport.bugsink.dsn : ''
```
- [ ] **Step 2: Add the `data-bugsink-dsn` attribute to `<body>`**
Replace the existing `<body>` opening tag with:
```astro
<body data-bugsink-dsn={bugsinkDsn}>
```
- [ ] **Step 3: Add the footer button**
In the `<footer class="mystery-footer">` block (currently lines 89109), append this AFTER the existing `{remainingFooterLinks.map(...)}` block, before the closing `</footer>`:
```astro
{bugpinConfig && (
<>
<span aria-hidden="true">|</span>
<button
type="button"
class="mystery-footer-bug-report"
data-bug-report-trigger
data-bugpin-server={bugpinConfig.serverUrl}
data-bugpin-key={bugpinConfig.apiKey}
>{bugReport?.label ?? 'Report a Bug'}</button>
</>
)}
```
- [ ] **Step 4: Import the two new modules**
Replace the existing module-import script block (currently lines 130133):
```astro
<script>
import '../ui/terminal.ts'
import '../ui/theme.ts'
</script>
```
with:
```astro
<script>
import '../ui/terminal.ts'
import '../ui/theme.ts'
import '../ui/error-tracking.ts'
import '../ui/bug-report.ts'
</script>
```
- [ ] **Step 5: Add CSS so the button matches the surrounding links**
Open `src/ui/crt.css` and grep for `.mystery-footer` to find the existing footer styles. Add a rule that makes the button look identical to the footer anchors. Append this block at the end of the existing `.mystery-footer` rules (search for `.mystery-footer a` and add directly after that selector's rule):
```css
.mystery-footer-bug-report {
background: none;
border: none;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
text-decoration: underline;
cursor: pointer;
}
.mystery-footer-bug-report:hover,
.mystery-footer-bug-report:focus-visible {
text-decoration: none;
}
.mystery-footer-bug-report:disabled {
opacity: 0.5;
cursor: progress;
}
```
If `.mystery-footer a` styles its anchors differently (e.g. specific color, no underline by default), match those properties instead. Look at the actual `a` rule and copy its `color`/`text-decoration` declarations into `.mystery-footer-bug-report` so the two render visually identically.
- [ ] **Step 6: Run the full type check and build**
```
npx astro check
npm run build
```
Expected: 0 errors, build succeeds.
- [ ] **Step 7: Commit**
```
git add src/pages/index.astro src/ui/crt.css
git commit -m "feat(ui): add Report a Bug footer button and wire bug tracking modules"
```
---
## Task 8: Update TODOs
**Files:**
- Modify: `src/world/TODOs.md:45`
- [ ] **Step 1: Rewrite the TODO line**
Replace line 45 of `src/world/TODOs.md`:
```
- [ ] Set up BugPin as a self-hosted visual bug reporter for the site, then have incoming reports create markdown files under `src/world/bugs/` via a webhook or API bridge so bugs can be tracked in git alongside the game content. Include screenshot/annotation metadata in the markdown and decide whether these bug docs stay outside the world loader or get their own loader later.
```
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 after manual verification: a test report appears in the Bugpin portal AND a GitHub issue is created AND a thrown error appears in the Bugsink portal.
```
- [ ] **Step 2: Commit**
```
git add src/world/TODOs.md
git commit -m "docs: rewrite TODO 45 to reflect Bugpin/Bugsink approach"
```
---
## Task 9: Manual smoke test
**Pre-req:** the Bugpin GitHub integration must be configured server-side before user reports will create issues. See spec §5. Steps below test the widget plumbing regardless.
- [ ] **Step 1: Start dev server**
```
npm run dev
```
Open the URL it prints (default `http://localhost:4321`).
- [ ] **Step 2: Verify the footer button renders**
In the rendered footer at the bottom of the page, confirm "Report a Bug" appears after "Source Code", styled like the surrounding links.
- [ ] **Step 3: Verify the widget lazy-loads**
Open DevTools → Network. Filter for `widget.js`. Confirm NO request to `bugpin.half.st/widget.js` on cold page load.
Click "Report a Bug". Confirm:
- A single GET to `https://bugpin.half.st/widget.js` (status 200).
- The Bugpin dialog opens.
- Take a screenshot, write "smoke test" in the note, submit.
- The dialog closes / shows a success state.
Re-click "Report a Bug". Confirm NO second `widget.js` request — only the dialog re-opens.
- [ ] **Step 4: Verify Bugpin received the report**
Open `https://bugpin.half.st/portal`. Confirm the "smoke test" report appears with the screenshot attached.
If the GitHub integration is configured: confirm a matching issue appears in the halfstreet GitHub repo.
- [ ] **Step 5: Verify Bugsink captures an uncaught error**
In the dev server's running page, open DevTools console and run:
```js
setTimeout(() => { throw new Error('bugsink smoke test') }, 0)
```
Open `https://bugsink.half.st`. Confirm a "bugsink smoke test" event appears within ~30 seconds.
- [ ] **Step 6: Verify the launcher styling is acceptable**
After step 3, the Bugpin widget's built-in floating launcher may now also be visible on the page. If its presence is visually objectionable, add a CSS rule to `src/ui/crt.css` targeting the Bugpin host element (inspect the DOM to find the selector — Bugpin uses Shadow DOM with a custom-element host like `<bugpin-widget>` or similar). Example:
```css
bugpin-widget,
[data-bugpin-launcher] {
display: none !important;
}
```
If this turns out to be needed, commit the CSS as `style(ui): hide bugpin built-in launcher (we have our own trigger)`. If the launcher's appearance is fine as-is, skip this step.
- [ ] **Step 7: Mark TODO #45 complete**
Once steps 15 all pass, edit `src/world/TODOs.md` line 45, change `- [ ]` to `- [x]`, and commit:
```
git add src/world/TODOs.md
git commit -m "chore: mark TODO 45 complete"
```
---
## Done criteria
All of the following are true:
- `npm test` passes.
- `npx astro check` reports 0 errors.
- `npm run build` succeeds.
- Manual smoke (Task 9 steps 15) all pass.
- TODO #45 is `[x]`.