From ad4e2fc0d20ec37fa4d36842791fbf5ac0adefa4 Mon Sep 17 00:00:00 2001 From: neonwatty Date: Fri, 30 Jan 2026 03:41:16 -0700 Subject: [PATCH 1/2] feat: add feedback categories (Bug, Feature, Question) - Add category selector UI with radio buttons (Bug, Feature, Question) - Map categories to GitHub labels (bug, enhancement, question) - Add E2E tests for category selector - Update README and CHANGELOG documentation --- CHANGELOG.md | 27 ++++++++++- README.md | 26 ++++++++++- e2e/widget.spec.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++ src/routes/api.ts | 14 +++++- src/types.ts | 3 ++ src/widget/index.ts | 28 +++++++++++ 6 files changed, 204 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ada5ef..ceea711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to BugDrop will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.10.0] - 2026-01-30 + +### Added +- **Feedback categories**: Users can now select a category (Bug 🐛, Feature ✨, or Question ❓) when submitting feedback. Categories are mapped to GitHub labels (`bug`, `enhancement`, `question`) for easy triage. + +## [1.9.0] - 2026-01-30 + +### Added +- **Automatic browser/OS detection**: The widget now automatically captures and parses browser name/version and OS name/version from the user agent string. This information is displayed in a collapsible "System Info" section on the GitHub issue for easier debugging. +- **Enhanced system metadata**: Issues now include viewport size with device pixel ratio, browser language, and cleaner formatting in a markdown table. + +### Changed +- **URL privacy**: URLs are now automatically redacted to remove query parameters and hash fragments, protecting potentially sensitive data while still providing useful page context. + +## [1.8.1] - 2026-01-29 + +### Fixed +- Fixed animation timing issue where dismiss animation could be interrupted. + ## [1.8.0] - 2026-01-29 ### Added @@ -96,6 +115,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Minor versions** (v1.0 → v1.1): New features, backwards compatible - **Patch versions** (v1.0.0 → v1.0.1): Bug fixes only -[Unreleased]: https://github.com/neonwatty/bugdrop/compare/v1.1.0...HEAD +[Unreleased]: https://github.com/neonwatty/bugdrop/compare/v1.10.0...HEAD +[1.10.0]: https://github.com/neonwatty/bugdrop/compare/v1.9.0...v1.10.0 +[1.9.0]: https://github.com/neonwatty/bugdrop/compare/v1.8.1...v1.9.0 +[1.8.1]: https://github.com/neonwatty/bugdrop/compare/v1.8.0...v1.8.1 +[1.8.0]: https://github.com/neonwatty/bugdrop/compare/v1.7.0...v1.8.0 +[1.7.0]: https://github.com/neonwatty/bugdrop/compare/v1.6.0...v1.7.0 +[1.6.0]: https://github.com/neonwatty/bugdrop/compare/v1.1.0...v1.6.0 [1.1.0]: https://github.com/neonwatty/bugdrop/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/neonwatty/bugdrop/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 422d24a..d405eab 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BugDrop 🐛 [![CI](https://github.com/neonwatty/bugdrop/actions/workflows/ci.yml/badge.svg)](https://github.com/neonwatty/bugdrop/actions/workflows/ci.yml) -[![Version](https://img.shields.io/badge/version-1.1.0-14b8a6)](./CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-1.10.0-14b8a6)](./CHANGELOG.md) [![Security Policy](https://img.shields.io/badge/Security-Policy-blue)](./SECURITY.md) [![Live Demo](https://img.shields.io/badge/Demo-Try_It_Live-ff9e64)](https://neonwatty.github.io/feedback-widget-test/) @@ -117,6 +117,30 @@ When enabled, hovering over the button reveals an X icon. Clicking it hides the With `data-dismiss-duration="7"`, users who dismiss the button will see it again after 7 days. Without this attribute, the button stays hidden forever (until localStorage is cleared). +### Feedback Categories + +When users submit feedback, they can select a category: + +| Category | Emoji | GitHub Label | +|----------|-------|--------------| +| Bug | 🐛 | `bug` | +| Feature | ✨ | `enhancement` | +| Question | ❓ | `question` | + +The selected category is automatically mapped to a GitHub label on the created issue, making it easy to filter and triage feedback. Bug is selected by default. + +### Automatic System Info + +Each feedback submission automatically includes: + +- **Browser** name and version (e.g., Chrome 120) +- **OS** name and version (e.g., macOS 14.2) +- **Viewport** size with device pixel ratio +- **Language** preference +- **Page URL** (with query params redacted for privacy) + +This information appears in a collapsible "System Info" section on the GitHub issue. + ### JavaScript API BugDrop exposes a JavaScript API for programmatic control, useful when you want to trigger feedback from your own UI instead of (or in addition to) the floating button. diff --git a/e2e/widget.spec.ts b/e2e/widget.spec.ts index 1e6b687..d4dfa29 100644 --- a/e2e/widget.spec.ts +++ b/e2e/widget.spec.ts @@ -1434,3 +1434,113 @@ test.describe('API-Only Mode (data-button="false")', () => { await expect(trigger).not.toBeAttached(); }); }); + +test.describe('Feedback Categories', () => { + test('category selector is visible on feedback form', async ({ page }) => { + // Mock installation check + await page.route('**/api/check/**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ installed: true }), + }); + }); + + await page.goto('/test/index.html'); + + const trigger = page.locator('#bugdrop-host').locator('css=.bd-trigger'); + await expect(trigger).toBeVisible({ timeout: 5000 }); + + // Click to open modal + await trigger.click(); + await page.waitForTimeout(300); + + // Click continue on welcome screen + const continueBtn = page.locator('#bugdrop-host').locator('css=[data-action="continue"]'); + await continueBtn.click(); + + // Wait for form to appear by checking for title input + const titleInput = page.locator('#bugdrop-host').locator('css=#title'); + await expect(titleInput).toBeVisible({ timeout: 5000 }); + + // Category selector should be visible + const categorySelector = page.locator('#bugdrop-host').locator('css=.bd-category-selector'); + await expect(categorySelector).toBeVisible(); + + // All three options should be present + const bugOption = page.locator('#bugdrop-host').locator('css=input[name="category"][value="bug"]'); + const featureOption = page.locator('#bugdrop-host').locator('css=input[name="category"][value="feature"]'); + const questionOption = page.locator('#bugdrop-host').locator('css=input[name="category"][value="question"]'); + + await expect(bugOption).toBeAttached(); + await expect(featureOption).toBeAttached(); + await expect(questionOption).toBeAttached(); + }); + + test('bug category is selected by default', async ({ page }) => { + // Mock installation check + await page.route('**/api/check/**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ installed: true }), + }); + }); + + await page.goto('/test/index.html'); + + const trigger = page.locator('#bugdrop-host').locator('css=.bd-trigger'); + await trigger.click(); + await page.waitForTimeout(300); + + const continueBtn = page.locator('#bugdrop-host').locator('css=[data-action="continue"]'); + await continueBtn.click(); + + // Wait for form to appear + const titleInput = page.locator('#bugdrop-host').locator('css=#title'); + await expect(titleInput).toBeVisible({ timeout: 5000 }); + + // Bug should be checked by default + const bugOption = page.locator('#bugdrop-host').locator('css=input[name="category"][value="bug"]'); + await expect(bugOption).toBeChecked(); + }); + + test('can select different categories', async ({ page }) => { + // Mock installation check + await page.route('**/api/check/**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ installed: true }), + }); + }); + + await page.goto('/test/index.html'); + + const trigger = page.locator('#bugdrop-host').locator('css=.bd-trigger'); + await trigger.click(); + await page.waitForTimeout(300); + + const continueBtn = page.locator('#bugdrop-host').locator('css=[data-action="continue"]'); + await continueBtn.click(); + + // Wait for form to appear + const titleInput = page.locator('#bugdrop-host').locator('css=#title'); + await expect(titleInput).toBeVisible({ timeout: 5000 }); + + // Select feature + const featureOption = page.locator('#bugdrop-host').locator('css=input[name="category"][value="feature"]'); + await featureOption.click(); + await expect(featureOption).toBeChecked(); + + // Bug should no longer be checked + const bugOption = page.locator('#bugdrop-host').locator('css=input[name="category"][value="bug"]'); + await expect(bugOption).not.toBeChecked(); + + // Select question + const questionOption = page.locator('#bugdrop-host').locator('css=input[name="category"][value="question"]'); + await questionOption.click(); + await expect(questionOption).toBeChecked(); + await expect(featureOption).not.toBeChecked(); + }); +}); diff --git a/src/routes/api.ts b/src/routes/api.ts index f10d330..fa02431 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -127,14 +127,24 @@ api.post('/feedback', async (c) => { // Check repo visibility (for UI to decide whether to show issue link) const isPublic = await isRepoPublic(token, owner, repo); - // Create issue + // Map category to GitHub label + const categoryLabels: Record = { + bug: 'bug', + feature: 'enhancement', + question: 'question', + }; + const categoryLabel = payload.category + ? categoryLabels[payload.category] || 'bug' + : 'bug'; + + // Create issue with category label const issue = await createIssue( token, owner, repo, payload.title, body, - ['bug', 'bugdrop'] + [categoryLabel, 'bugdrop'] ); return c.json({ diff --git a/src/types.ts b/src/types.ts index dd63f5d..47d494a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,10 +13,13 @@ export interface Env { ASSETS: Fetcher; } +export type FeedbackCategory = 'bug' | 'feature' | 'question'; + export interface FeedbackPayload { repo: string; // "owner/repo" format title: string; description: string; + category?: FeedbackCategory; // Feedback type (maps to GitHub labels) screenshot?: string; // base64 data URL annotations?: string; // base64 annotated image submitter?: { // Optional submitter info (configured per widget) diff --git a/src/widget/index.ts b/src/widget/index.ts index c80ee3b..44c1e63 100644 --- a/src/widget/index.ts +++ b/src/widget/index.ts @@ -47,6 +47,7 @@ declare global { interface FeedbackData { title: string; description: string; + category: FeedbackCategory; screenshot: string | null; elementSelector: string | null; name?: string; @@ -486,6 +487,7 @@ async function openFeedbackFlow(root: HTMLElement, config: WidgetConfig) { await submitFeedback(root, config, { title: formResult.title, description: formResult.description, + category: formResult.category, name: formResult.name, email: formResult.email, screenshot, @@ -634,9 +636,12 @@ function showWelcomeScreen(root: HTMLElement): Promise { }); } +type FeedbackCategory = 'bug' | 'feature' | 'question'; + interface FeedbackFormResult { title: string; description: string; + category: FeedbackCategory; name?: string; email?: string; includeScreenshot: boolean; @@ -678,6 +683,23 @@ function showFeedbackFormWithScreenshotOption( +
+ +
+ + + +
+
@@ -737,10 +759,15 @@ function showFeedbackFormWithScreenshotOption( return; } + // Get selected category + const categoryInput = modal.querySelector('input[name="category"]:checked') as HTMLInputElement; + const category = (categoryInput?.value || 'bug') as FeedbackCategory; + modal.remove(); resolve({ title: titleInput.value.trim(), description: descInput.value.trim(), + category, name: nameInput?.value.trim() || undefined, email: emailInput?.value.trim() || undefined, includeScreenshot: screenshotCheckbox.checked, @@ -895,6 +922,7 @@ async function submitFeedback( repo: config.repo, title: data.title, description: data.description, + category: data.category, screenshot: data.screenshot, submitter, metadata: { From d7173feb5435108dbdd4472198ec237c7b4b1986 Mon Sep 17 00:00:00 2001 From: neonwatty Date: Fri, 30 Jan 2026 03:42:50 -0700 Subject: [PATCH 2/2] fix: remove unused export from FeedbackCategory type --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 47d494a..874cd19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,7 +13,7 @@ export interface Env { ASSETS: Fetcher; } -export type FeedbackCategory = 'bug' | 'feature' | 'question'; +type FeedbackCategory = 'bug' | 'feature' | 'question'; export interface FeedbackPayload { repo: string; // "owner/repo" format