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

18 KiB
Raw Blame History

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.tsmodify: extend uiFrontmatterSchema with optional bugReport block.
  • src/world/schema.test.tsmodify: add accept/reject cases for bugReport.
  • src/world/types.tsmodify: extend UiConfig to expose bugReport.
  • src/world/ui.mdmodify: add bugReport frontmatter.
  • src/ui/error-tracking.tscreate: reads body data attrs, calls Sentry.init. No-op when DSN missing.
  • src/ui/bug-report.tscreate: wires footer button click to lazy-load Bugpin widget, then call BugPin.open(). No-op when config missing.
  • src/pages/index.astromodify: render the footer button conditionally, attach data attrs to <body>, import the two new modules.
  • package.json / package-lock.jsonmodify: add @sentry/browser.
  • src/world/TODOs.md:45modify: 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):

  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:

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:

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:

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
// 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
// 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"

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:

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:

<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>:

{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):

<script>
  import '../ui/terminal.ts'
  import '../ui/theme.ts'
</script>

with:

<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):

.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:

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:

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].