GitHub Integration
Automatically create GitHub Issues from feedback with rich context, labels, assignees, and embedded screenshots.
What's Included
- Automatic issue creation from feedback
- Screenshot uploads as issue attachments
- Dynamic labels based on feedback type
- Full DOM context in issue body
Prerequisites
- GitHub repository with Issues enabled
- Personal Access Token with 'repo' scope
- Environment variables configured
Environment Setup
.env.local
# GitHub Personal Access Token# Create at: https://github.com/settings/tokens# Required scopes: repo (or public_repo for public repos)GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
# Repository informationGITHUB_REPO=your-username/your-repository-nameBrowser-Side Setup
Use the HTTP adapter to send feedback to your API endpoint:
app/providers.tsx
'use client';
import { FeedbackProvider } from '@ewjdev/anyclick-react';import { createHttpAdapter } from '@ewjdev/anyclick-';
const adapter = createHttpAdapter({ endpoint: '/api/feedback',});
export function Providers({ children }: { children: React.ReactNode }) { return ( <FeedbackProvider adapter={adapter} // Enable screenshot capture for GitHub screenshotConfig={{ enabled: true, quality: 0.9, format: 'png', }} > {children} </FeedbackProvider> );}Server-Side API Route
Create an API route that receives feedback and creates GitHub Issues:
app/api/feedback/route.ts
import { createGitHubAdapter } from '@ewjdev/anyclick-github/server';import type { FeedbackPayload } from '@ewjdev/anyclick-core';
const repoName = process.env.GITHUB_REPO!;const [owner, repo] = repoName.split("/");
// Create the GitHub adapter with configurationconst github = createGitHubAdapter({ owner, repo, token: process.env.GITHUB_TOKEN!, // Dynamic labels based on feedback type getLabels: (payload) => { const labels = ['feedback', 'from-app']; switch (payload.type) { case 'issue': case 'bug': labels.push('bug'); break; case 'feature': labels.push('enhancement'); break; case 'like': case 'love': labels.push('positive-feedback'); break; } return labels; }, // Custom issue title getTitle: (payload) => { const emoji = { issue: '🐛', bug: '🐛', feature: '✨', like: '❤️', love: '❤️', }[payload.type] || '📝'; // Use comment as title if available, otherwise use selector const title = payload.comment ? payload.comment.slice(0, 80) : `Feedback on ${payload.element.tagName}`; return `${emoji} ${title}`; }, // Optional: assign to specific users getAssignees: (payload) => { // Assign bugs to the bug-triage team if (payload.type === 'issue' || payload.type === 'bug') { return ['bug-triage-user']; } return []; },});
export async function POST(request: Request) { try { const payload: FeedbackPayload = await request.json(); const result = await github.submit(payload); return Response.json(result); } catch (error) { console.error('Failed to create GitHub issue:', error); return Response.json( { success: false, error: 'Failed to create issue' }, { status: 500 } ); }}Generated Issue Format
The adapter generates well-formatted GitHub Issues with all captured context:
Issue Preview
🐛 Button not responding on click
The submit button doesn't trigger form submission when clicked.
📍 Element Context
- Selector: button.submit-btn[data-testid="submit"]
- Tag: button
- Text: Submit Form
🌐 Page Context
- URL: https://example.com/checkout
- Viewport: 1920 × 1080
- Timestamp: 2024-01-15T10:30:00Z
📸 Screenshots
Target
Container
Full Page
Custom Issue Formatting
Override the default markdown formatting:
import { createGitHubAdapter, formatFeedbackAsMarkdown } from '@ewjdev/anyclick-github/server';
const repoName = process.env.GITHUB_REPO!;const [owner, repo] = repoName.split("/");
const github = createGitHubAdapter({ owner, repo, token: process.env.GITHUB_TOKEN!, // Custom body formatting getBody: (payload) => { // Use built-in formatter as base const defaultBody = formatFeedbackAsMarkdown(payload); // Add custom sections return `${defaultBody}
---
## �� Additional Info
- **Environment**: ${payload.metadata?.environment || 'production'}- **User ID**: ${payload.metadata?.userId || 'anonymous'}- **Session**: ${payload.metadata?.sessionId || 'unknown'}
<details><summary>Raw HTML</summary>
\`\`\`html${payload.element.outerHTML}\`\`\`
</details> `; },});Screenshot Upload
Screenshots are automatically uploaded as issue attachments. The adapter handles base64 decoding and GitHub's asset upload API.
// Screenshots are included in the payloadconst payload: FeedbackPayload = { // ... other fields screenshots: { target: { dataUrl: 'data:image/png;base64,...', width: 200, height: 100, }, container: { dataUrl: 'data:image/png;base64,...', width: 800, height: 600, }, fullPage: { dataUrl: 'data:image/png;base64,...', width: 1920, height: 1080, }, },};
// The adapter uploads and embeds them automatically// // // Error Handling
app/api/feedback/route.ts
export async function POST(request: Request) { try { const payload: FeedbackPayload = await request.json(); // Validate required fields if (!payload.type || !payload.element) { return Response.json( { success: false, error: 'Invalid payload' }, { status: 400 } ); } const result = await github.submit(payload); if (!result.success) { console.error('GitHub API error:', result.error); return Response.json(result, { status: 500 }); } return Response.json(result); } catch (error) { console.error('Feedback submission error:', error); // Return user-friendly error return Response.json( { success: false, error: 'Unable to submit feedback. Please try again.', }, { status: 500 } ); }}Next: Local Development Workflow
Learn how to integrate with Cursor for instant AI-powered code fixes during development.
Cursor Local Example