Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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/)

Expand Down Expand Up @@ -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.
Expand Down
110 changes: 110 additions & 0 deletions e2e/widget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
14 changes: 12 additions & 2 deletions src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
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({
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ export interface Env {
ASSETS: Fetcher;
}

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)
Expand Down
28 changes: 28 additions & 0 deletions src/widget/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
interface FeedbackData {
title: string;
description: string;
category: FeedbackCategory;
screenshot: string | null;
elementSelector: string | null;
name?: string;
Expand Down Expand Up @@ -486,6 +487,7 @@
await submitFeedback(root, config, {
title: formResult.title,
description: formResult.description,
category: formResult.category,
name: formResult.name,
email: formResult.email,
screenshot,
Expand Down Expand Up @@ -634,9 +636,12 @@
});
}

type FeedbackCategory = 'bug' | 'feature' | 'question';

interface FeedbackFormResult {
title: string;
description: string;
category: FeedbackCategory;
name?: string;
email?: string;
includeScreenshot: boolean;
Expand Down Expand Up @@ -678,6 +683,23 @@
<label class="bd-label" for="title">Title *</label>
<input type="text" id="title" class="bd-input" required placeholder="Brief description of the issue or suggestion" />
</div>
<div class="bd-form-group">
<label class="bd-label">Category</label>
<div class="bd-category-selector" style="display: flex; gap: 8px; margin-top: 6px;">
<label class="bd-category-option" style="flex: 1; display: flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px solid var(--bd-border); border-radius: 6px; cursor: pointer; transition: all 0.15s ease;">
<input type="radio" name="category" value="bug" checked style="accent-color: var(--bd-primary);" />
<span style="font-size: 0.9rem;">πŸ› Bug</span>
</label>
<label class="bd-category-option" style="flex: 1; display: flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px solid var(--bd-border); border-radius: 6px; cursor: pointer; transition: all 0.15s ease;">
<input type="radio" name="category" value="feature" style="accent-color: var(--bd-primary);" />
<span style="font-size: 0.9rem;">✨ Feature</span>
</label>
<label class="bd-category-option" style="flex: 1; display: flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px solid var(--bd-border); border-radius: 6px; cursor: pointer; transition: all 0.15s ease;">
<input type="radio" name="category" value="question" style="accent-color: var(--bd-primary);" />
<span style="font-size: 0.9rem;">❓ Question</span>
</label>
</div>
</div>
<div class="bd-form-group">
<label class="bd-label" for="description">Description</label>
<textarea id="description" class="bd-textarea" placeholder="Provide additional details, steps to reproduce, or context..."></textarea>
Expand Down Expand Up @@ -737,10 +759,15 @@
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,
Expand Down Expand Up @@ -831,7 +858,7 @@
} else if (tool) {
toolButtons.forEach((b) => b.classList.remove('active'));
target.classList.add('active');
annotator.setTool(tool as any);

Check warning on line 861 in src/widget/index.ts

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, Knip

Unexpected any. Specify a different type
}
});
});
Expand Down Expand Up @@ -895,6 +922,7 @@
repo: config.repo,
title: data.title,
description: data.description,
category: data.category,
screenshot: data.screenshot,
submitter,
metadata: {
Expand Down
Loading