@fatagnus/convex-feedback
v0.2.18
Published
Bug reports and feedback collection component for Convex applications with AI analysis and email notifications
Maintainers
Readme
@fatagnus/convex-feedback
Bug reports and feedback collection component for Convex applications with AI analysis, email notifications, and a REST API.
Features
- 🐛 Bug Reports - Capture bug reports with browser diagnostics, console errors, and screenshots
- 💬 Feedback - Collect feature requests, change requests, and general feedback
- 🤖 AI Analysis - Automatic analysis of submissions using OpenRouter/Claude
- 💬 AI Interview Mode - Conversational AI interviews to help users articulate detailed bug reports and feedback
- 📧 Email Notifications - Send notifications to reporters and support teams via Resend
- 👥 Support Teams - Route notifications to the right teams based on type/severity
- 📸 Screenshots - Capture or upload screenshots with reports
- 🔄 Real-time - Leverages Convex for real-time updates
- 🎫 Ticket Numbers - Human-readable ticket IDs (BUG-2025-0001, FB-2025-0001)
- 🔑 API Keys - Secure API key management for external integrations
- 🌐 REST API - HTTP endpoints for external tools (Codebuff, CLI tools, etc.)
Installation
npm install @fatagnus/convex-feedbackPeer Dependencies
Required:
convex(>=1.17.0)react(>=18.0.0)
Optional (for React components):
@mantine/core(>=7.0.0)@mantine/form(>=7.0.0)@mantine/hooks(>=7.0.0)@mantine/notifications(>=7.0.0)@tabler/icons-react(>=3.0.0)html2canvas(>=1.4.0)
Optional (for AI analysis and interview mode):
@convex-dev/agent(>=0.1.0)@ai-sdk/openai-compatible(>=0.1.0)zod(>=3.0.0)
Optional (for email notifications):
resend(>=4.0.0)
Setup
1. Add the Component
Add the feedback component to your Convex app configuration:
// convex/convex.config.ts
import { defineApp } from "convex/server";
import feedback from "@fatagnus/convex-feedback/convex.config";
const app = defineApp();
app.use(feedback);
export default app;2. Register HTTP Routes (Optional)
To enable the REST API for external integrations:
// convex/http.ts
import { httpRouter } from "convex/server";
import { registerFeedbackRoutes } from "@fatagnus/convex-feedback";
const http = httpRouter();
// Register feedback API routes with optional prefix
registerFeedbackRoutes(http, { pathPrefix: "/feedback" });
// Your other routes...
export default http;3. Configure Environment Variables
Set these environment variables in your Convex dashboard:
| Variable | Required | Description |
|----------|----------|-------------|
| OPENROUTER_API_KEY | No | OpenRouter API key for AI analysis |
| RESEND_API_KEY | No | Resend API key for email notifications |
| RESEND_FROM_EMAIL | No | From email address (default: [email protected]) |
4. Add React Components
Wrap your app with the BugReportProvider and add the BugReportButton:
import { BugReportProvider, BugReportButton } from '@fatagnus/convex-feedback/react';
import { api } from './convex/_generated/api';
function App() {
const user = useCurrentUser(); // Your auth hook
return (
<BugReportProvider>
<YourApp />
{user && (
<BugReportButton
reporterType="staff"
reporterId={user.id}
reporterEmail={user.email}
reporterName={user.name}
bugReportApi={{
create: api.feedback.bugReports.create,
generateUploadUrl: api.feedback.bugReports.generateUploadUrl,
}}
feedbackApi={{
create: api.feedback.feedback.create,
generateUploadUrl: api.feedback.feedback.generateUploadUrl,
}}
/>
)}
</BugReportProvider>
);
}HTTP REST API
The REST API allows external tools (like Codebuff, CLI tools, CI/CD pipelines) to interact with bug reports and feedback.
Authentication
All endpoints require an API key in the Authorization header:
Authorization: Bearer fb_your_api_key_hereBase URL
Your Convex HTTP endpoint + the path prefix you configured:
https://your-deployment.convex.site/feedback/api/...Ticket Number Format
All tickets have human-readable IDs:
| Type | Format | Example |
|------|--------|----------|
| Bug Report | BUG-YYYY-NNNN | BUG-2025-0001 |
| Feedback | FB-YYYY-NNNN | FB-2025-0042 |
API Key Management
API keys are managed via Convex mutations. The full key is only shown once at creation time.
Create a Key
const result = await ctx.runMutation(api.feedback.apiKeys.create, {
name: "My Integration",
expiresAt: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days (optional)
});
console.log(result.key); // fb_a1b2c3d4e5f6g7h8... (save this!)
console.log(result.keyPrefix); // fb_a1b2c3d4List Keys
const keys = await ctx.runQuery(api.feedback.apiKeys.list, {});
// Returns array of { _id, keyPrefix, name, expiresAt, isRevoked, isExpired, createdAt }Revoke a Key
await ctx.runMutation(api.feedback.apiKeys.revoke, {
keyPrefix: "fb_a1b2c3d4",
});Rotate a Key
const newKey = await ctx.runMutation(api.feedback.apiKeys.rotate, {
keyPrefix: "fb_a1b2c3d4",
name: "Rotated Key", // optional
expiresAt: Date.now() + 90 * 24 * 60 * 60 * 1000, // optional
});Endpoints
GET /api/items
List bug reports and/or feedback items with optional filters.
Query Parameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| itemType | bug | feedback | all | all | Filter by item type |
| status | string | - | Filter by status |
| severity | low | medium | high | critical | - | Filter bugs by severity |
| priority | nice_to_have | important | critical | - | Filter feedback by priority |
| type | feature_request | change_request | general | - | Filter feedback by type |
| limit | number | 50 | Max items to return |
| includeArchived | true | false | false | Include archived items |
Bug Status Values: open, in-progress, resolved, closed
Feedback Status Values: open, under_review, planned, in_progress, completed, declined
Example:
# List all open bugs
curl -H "Authorization: Bearer fb_your_key" \
"https://your-site.convex.site/feedback/api/items?itemType=bug&status=open"Response:
{
"bugs": [
{
"_id": "abc123",
"ticketNumber": "BUG-2025-0001",
"title": "Login button not working",
"severity": "high",
"status": "open",
...
}
],
"feedback": [...]
}GET /api/items/{ticketNumber}
Fetch a single bug report or feedback item by ticket number.
Example:
curl -H "Authorization: Bearer fb_your_key" \
"https://your-site.convex.site/feedback/api/items/BUG-2025-0001"Response:
{
"type": "bug",
"_id": "abc123",
"ticketNumber": "BUG-2025-0001",
"title": "Login button not working",
"description": "When I click the login button, nothing happens",
"severity": "high",
"status": "open",
"aiSummary": "The login form submit handler is not properly bound",
"aiRootCauseAnalysis": "React event handler not attached correctly",
"aiSuggestedFix": "Check that the onClick handler is properly bound",
...
}GET /api/prompt/{ticketNumber}
Fetch an AI-generated prompt for fixing/implementing a ticket. Designed for AI coding assistants like Codebuff.
Query Parameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| template | fix | implement | analyze | codebuff | fix (bugs) / implement (feedback) | Prompt template |
Templates:
| Template | Use Case |
|----------|----------|
| fix | Bug fix instructions |
| implement | Feature implementation instructions |
| analyze | Deep analysis prompt |
| codebuff | Codebuff-optimized structured format |
Example:
# Get a Codebuff-optimized prompt
curl -H "Authorization: Bearer fb_your_key" \
"https://your-site.convex.site/feedback/api/prompt/BUG-2025-0001?template=codebuff"Response:
{
"ticketNumber": "BUG-2025-0001",
"type": "bug",
"template": "codebuff",
"prompt": "# Bug Fix Request: BUG-2025-0001\n\n## Title\nLogin button not working\n\n..."
}PATCH /api/items/{ticketNumber}/status
Update the status of a bug report or feedback item.
Request Body:
{ "status": "resolved" }Example:
curl -X PATCH \
-H "Authorization: Bearer fb_your_key" \
-H "Content-Type: application/json" \
-d '{"status": "resolved"}' \
"https://your-site.convex.site/feedback/api/items/BUG-2025-0001/status"Response:
{ "success": true, "ticketNumber": "BUG-2025-0001" }PATCH /api/items/{ticketNumber}/archive
Archive or unarchive a bug report or feedback item.
Request Body:
{ "archived": true }Example:
curl -X PATCH \
-H "Authorization: Bearer fb_your_key" \
-H "Content-Type: application/json" \
-d '{"archived": true}' \
"https://your-site.convex.site/feedback/api/items/BUG-2025-0001/archive"Response:
{ "success": true, "ticketNumber": "BUG-2025-0001", "isArchived": true }Error Responses
| Status | Response |
|--------|----------|
| 400 | { "error": "Bad request", "message": "..." } |
| 401 | { "error": "Unauthorized", "message": "Invalid or missing API key" } |
| 404 | { "error": "Not found", "message": "Bug report BUG-2025-0001 not found" } |
Integration Examples
Codebuff Integration
curl -s -H "Authorization: Bearer fb_your_key" \
"https://your-site.convex.site/feedback/api/prompt/BUG-2025-0001?template=codebuff" \
| jq -r '.prompt' \
| codebuffGitHub Actions
- name: Get open bugs count
run: |
curl -s -H "Authorization: Bearer ${{ secrets.FEEDBACK_API_KEY }}" \
"${{ secrets.CONVEX_SITE_URL }}/feedback/api/items?itemType=bug&status=open" \
| jq '.bugs | length'Convex API Reference
Bug Reports
bugReports.create
Create a new bug report.
const bugReportId = await ctx.runMutation(api.feedback.bugReports.create, {
title: "Button not working",
description: "The submit button doesn't respond to clicks",
severity: "high", // low | medium | high | critical
reporterType: "staff", // staff | customer
reporterId: "user_123",
reporterEmail: "[email protected]",
reporterName: "John Doe",
url: "https://app.example.com/form",
route: "/form",
browserInfo: JSON.stringify({ userAgent: "..." }),
consoleErrors: JSON.stringify([{ message: "Error..." }]),
screenshotStorageId: "storage_id_here", // optional
viewportWidth: 1920,
viewportHeight: 1080,
networkState: "online",
});bugReports.list
List bug reports with optional filters.
const reports = await ctx.runQuery(api.feedback.bugReports.list, {
status: "open", // optional: open | in-progress | resolved | closed
severity: "high", // optional: low | medium | high | critical
includeArchived: false, // optional, default: false
limit: 50, // optional, default: 50
});bugReports.get
Get a single bug report by ID.
const report = await ctx.runQuery(api.feedback.bugReports.get, {
reportId: "bug_report_id",
});bugReports.updateStatus
Update the status of a bug report.
await ctx.runMutation(api.feedback.bugReports.updateStatus, {
reportId: "bug_report_id",
status: "resolved",
});bugReports.archive / bugReports.unarchive
Archive or unarchive a bug report.
await ctx.runMutation(api.feedback.bugReports.archive, {
reportId: "bug_report_id",
});Feedback
feedback.create
Create new feedback.
const feedbackId = await ctx.runMutation(api.feedback.feedback.create, {
type: "feature_request", // feature_request | change_request | general
title: "Dark mode support",
description: "Please add dark mode to reduce eye strain",
priority: "important", // nice_to_have | important | critical
reporterType: "customer",
reporterId: "customer_123",
reporterEmail: "[email protected]",
reporterName: "Jane Doe",
url: "https://app.example.com/settings",
browserInfo: JSON.stringify({ userAgent: "..." }),
viewportWidth: 1920,
viewportHeight: 1080,
networkState: "online",
});feedback.list
List feedback with optional filters.
const items = await ctx.runQuery(api.feedback.feedback.list, {
status: "open", // optional
type: "feature_request", // optional
priority: "important", // optional
includeArchived: false, // optional
limit: 50, // optional
});Support Teams
supportTeams.create
Create a support team for notification routing.
const teamId = await ctx.runMutation(api.feedback.supportTeams.create, {
teamName: "Engineering Team",
memberEmails: ["[email protected]", "[email protected]"],
feedbackTypes: ["feature_request", "change_request"],
bugReportSeverities: ["high", "critical"],
});supportTeams.list
List all support teams.
const teams = await ctx.runQuery(api.feedback.supportTeams.list, {
activeOnly: true, // optional
});supportTeams.update
Update a support team.
await ctx.runMutation(api.feedback.supportTeams.update, {
teamId: "team_id",
teamName: "Updated Team Name",
memberEmails: ["[email protected]"],
isActive: true,
});React Components
BugReportProvider
Provider component that captures console errors for bug reports. Wrap your app with this provider.
import { BugReportProvider } from '@fatagnus/convex-feedback/react';
function App() {
return (
<BugReportProvider maxErrors={20}>
<YourApp />
</BugReportProvider>
);
}Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | - | Child components |
| maxErrors | number | 20 | Maximum console errors to capture |
BugReportButton
Floating action button for submitting bug reports and feedback.
import { BugReportButton } from '@fatagnus/convex-feedback/react';
<BugReportButton
reporterType="staff"
reporterId={user.id}
reporterEmail={user.email}
reporterName={user.name}
bugReportApi={{
create: api.feedback.bugReports.create,
generateUploadUrl: api.feedback.bugReports.generateUploadUrl,
}}
feedbackApi={{
create: api.feedback.feedback.create,
generateUploadUrl: api.feedback.feedback.generateUploadUrl,
}}
position={{ bottom: 20, right: 20 }}
buttonColor="red"
showFeedback={true}
onSuccess={(type) => console.log(`${type} submitted`)}
onError={(error, type) => console.error(`${type} failed:`, error)}
// AI Interview Mode (optional)
enableInterview={true}
defaultMode="interview"
interviewApi={{
startBugInterview: api.feedback.agents.feedbackInterviewAgent.startBugInterview,
startFeedbackInterview: api.feedback.agents.feedbackInterviewAgent.startFeedbackInterview,
continueInterview: api.feedback.agents.feedbackInterviewAgent.continueInterview,
getPendingForThread: api.feedback.inputRequests.getPendingForThread,
getSessionByThread: api.feedback.agents.feedbackInterviewAgent.getSessionByThread,
}}
interviewContext={{
appName: 'My App',
appDescription: 'A productivity tool for teams',
featureAreas: [
{ name: 'Dashboard', description: 'Main overview page' },
{ name: 'Reports', description: 'Analytics and reporting' },
],
}}
/>Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| reporterType | 'staff' \| 'customer' | - | Type of reporter |
| reporterId | string | - | Unique reporter identifier |
| reporterEmail | string | - | Reporter's email |
| reporterName | string | - | Reporter's display name |
| bugReportApi | object | - | Convex API references for bug reports |
| feedbackApi | object | - | Convex API references for feedback |
| position | object | { bottom: 20, right: 20 } | FAB position |
| buttonColor | string | 'red' | Button color |
| showFeedback | boolean | true | Show feedback tab |
| onSuccess | function | - | Success callback |
| onError | function | - | Error callback |
| enableInterview | boolean | false | Enable AI interview mode |
| defaultMode | 'form' \| 'interview' | 'form' | Default input mode when interview is enabled |
| interviewContext | InterviewContext | - | Context to help AI ask better questions |
| interviewApi | object | - | Convex API references for interview actions (required if enableInterview is true) |
ErrorBugReportButton
A self-contained bug report button designed for error pages. Pre-fills error details and allows users to submit bug reports directly from error boundaries or error pages.
import { ErrorBugReportButton } from '@fatagnus/convex-feedback/react';
import { api } from './convex/_generated/api';
function ErrorPage({ error }: { error: Error }) {
return (
<div>
<h1>Something went wrong</h1>
<p>{error.message}</p>
<ErrorBugReportButton
error={error}
reporterType="staff"
reporterId={user?.id}
reporterEmail={user?.email}
reporterName={user?.name}
bugReportApi={{
create: api.feedback.bugReports.create,
}}
variant="outline"
color="red"
onSuccess={() => console.log('Bug report submitted')}
onError={(err) => console.error('Failed to submit:', err)}
/>
</div>
);
}Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| error | Error | - | The error to report (required) |
| reporterType | 'staff' \| 'customer' | 'staff' | Type of reporter |
| reporterId | string | 'error-page' | Unique reporter identifier |
| reporterEmail | string | '[email protected]' | Reporter's email |
| reporterName | string | 'Error Page Reporter' | Reporter's display name |
| bugReportApi | object | - | Convex API reference with create mutation (required) |
| variant | 'filled' \| 'outline' \| 'light' \| 'subtle' | 'outline' | Button variant |
| color | string | 'red' | Button color |
| onSuccess | function | - | Success callback |
| onError | function | - | Error callback |
Note: Unlike BugReportButton, this component only requires the bugReportApi.create mutation (no generateUploadUrl needed since error pages typically don't include screenshots).
useBugReportContext
Hook to access the bug report context for error capture.
import { useBugReportContext } from '@fatagnus/convex-feedback/react';
function MyComponent() {
const { getConsoleErrors, clearConsoleErrors } = useBugReportContext();
const errors = getConsoleErrors();
// ...
}AI Interview Mode
The AI Interview Mode provides a conversational experience to help users articulate their bug reports and feedback more effectively. Instead of filling out a form, users chat with an AI that asks clarifying questions and generates a well-structured report.
Enabling Interview Mode
To enable AI interviews, you need:
OPENROUTER_API_KEYenvironment variable set in your Convex dashboard- The
@convex-dev/agentandzodpackages installed - The
enableInterviewandinterviewApiprops configured
import { BugReportButton } from '@fatagnus/convex-feedback/react';
import { api } from './convex/_generated/api';
<BugReportButton
reporterType="staff"
reporterId={user.id}
reporterEmail={user.email}
reporterName={user.name}
bugReportApi={{
create: api.feedback.bugReports.create,
generateUploadUrl: api.feedback.bugReports.generateUploadUrl,
}}
feedbackApi={{
create: api.feedback.feedback.create,
generateUploadUrl: api.feedback.feedback.generateUploadUrl,
}}
// Enable AI Interview Mode
enableInterview={true}
defaultMode="interview" // or "form" to default to manual form
interviewApi={{
startBugInterview: api.feedback.agents.feedbackInterviewAgent.startBugInterview,
startFeedbackInterview: api.feedback.agents.feedbackInterviewAgent.startFeedbackInterview,
continueInterview: api.feedback.agents.feedbackInterviewAgent.continueInterview,
getPendingForThread: api.feedback.inputRequests.getPendingForThread,
getSessionByThread: api.feedback.agents.feedbackInterviewAgent.getSessionByThread,
}}
/>Providing Context
You can provide application context to help the AI ask more relevant questions:
<BugReportButton
// ... other props
interviewContext={{
// Application information
appName: 'Logsign SIEM',
appDescription: 'A security information and event management platform',
// Feature areas (AI will offer these as choices)
featureAreas: [
{ name: 'Dashboard', description: 'Main overview and metrics' },
{ name: 'Alerts', description: 'Security alert management' },
{ name: 'Reports', description: 'Analytics and reporting' },
{ name: 'Settings', description: 'User and system configuration' },
],
// Known issues (AI will check for duplicates)
knownIssues: [
{ title: 'Slow dashboard loading', description: 'Dashboard takes 5+ seconds on first load' },
{ title: 'Export timeout', description: 'Large reports fail to export' },
],
// Custom questions to ask during interview
customQuestions: {
bug: [
'Are you using any browser extensions?',
'Did this work before a recent update?',
],
feedback: [
'How urgent is this for your workflow?',
'Would a workaround help in the meantime?',
],
},
// Additional instructions for the AI
additionalInstructions: 'Our users are security professionals, so technical terms are okay.',
}}
/>InterviewContext Type
interface InterviewContext {
/** Application/product name (e.g., "Logsign SIEM") */
appName?: string;
/** Brief description of what the app does */
appDescription?: string;
/** Feature areas/modules in the app */
featureAreas?: Array<{
name: string;
description?: string;
}>;
/** Known issues or FAQ items to help detect duplicates */
knownIssues?: Array<{
title: string;
description?: string;
}>;
/** Domain-specific questions to include in the interview */
customQuestions?: {
bug?: string[]; // Extra questions for bug reports
feedback?: string[]; // Extra questions for feedback
};
/** Free-form text for any other context the agent should know */
additionalInstructions?: string;
}How It Works
- User clicks the bug report button - The modal opens with an "AI Interview Mode" toggle
- Interview starts - The AI greets the user and asks what went wrong (for bugs) or what idea they have (for feedback)
- Conversational Q&A - The AI asks 3-5 clarifying questions to understand the issue
- Report generated - The AI generates a well-structured report with title, description, severity/priority, and reproduction steps
- Review and submit - The user can review and edit the generated report before submitting
Interview Input Types
The AI can ask for input in three formats:
- Text - Open-ended questions (single line or multiline)
- Choice - Select from predefined options (e.g., feature areas, frequency)
- Form - Multiple fields at once (rarely used)
Fallback Behavior
- If
OPENROUTER_API_KEYis not configured, interview mode will show an error and users can switch to the form - Users can always toggle between "AI Interview" and "Quick Form" modes
- Screenshots and browser diagnostics are still captured automatically in interview mode
AI Analysis
When OPENROUTER_API_KEY is configured, the component automatically analyzes submissions:
Bug Reports
- Summary - Concise description of the bug
- Root Cause Analysis - Potential underlying causes
- Reproduction Steps - Steps to reproduce the issue
- Suggested Fix - Recommended approach to resolve
- Estimated Effort - Low/Medium/High effort estimate
- Suggested Severity - AI-recommended severity level
Feedback
- Summary - Concise description of the feedback
- Impact Analysis - What areas/users are affected
- Action Items - Concrete next steps
- Estimated Effort - Low/Medium/High effort estimate
- Suggested Priority - AI-recommended priority level
Email Notifications
When RESEND_API_KEY is configured, the component sends email notifications:
To Reporters
- Thank you email with submission summary
- Professional HTML email template
To Support Teams
- Full submission details with AI analysis
- Routed based on feedback type or bug severity
- Rich HTML template with all diagnostics
Schema
The component creates these tables:
bugReports- Bug report submissionsfeedback- Feedback submissionssupportTeams- Team configuration for notification routingfeedbackInputRequests- Pending user input requests during AI interviewsinterviewSessions- Interview state and generated reportsticketCounters- Sequential counters for ticket numbersapiKeys- API keys for REST API authentication
TypeScript Types
import type {
BugReport,
Feedback,
BugSeverity,
BugStatus,
FeedbackType,
FeedbackPriority,
FeedbackStatus,
ApiKey,
ApiKeyCreated,
PromptTemplate,
RegisterFeedbackRoutesOptions,
InterviewContext,
} from '@fatagnus/convex-feedback';License
Apache-2.0
