Files
halfstreet/docs/superpowers/plans/2026-05-17-bug-reporting.md
2026-05-17 23:02:46 -05:00

654 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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]`.