Examples/GitHub Integration

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 information
GITHUB_REPO=your-username/your-repository-name

Browser-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 configuration
const 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 payload
const 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
// ![Target Screenshot](https://github.com/.../assets/123)
// ![Container Screenshot](https://github.com/.../assets/456)
// ![Full Page Screenshot](https://github.com/.../assets/789)

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