
Security News
Feross on TBPN: Socket's Series C and the State of Software Supply Chain Security
Feross Aboukhadijeh joins TBPN to discuss Socket's $60M Series C, 500%+ ARR growth, AI's impact on open source, and the rise in supply chain attacks.
@sjforge/feedback-widget
Advanced tools
In-app feedback widget SDK with screenshots, annotations, and session recording
In-app feedback widget SDK with screenshots, annotations, session recording, and offline support.
npm install @sjforge/feedback-widget
Or via CDN:
<script src="https://cdn.sjforge.dev/feedback-widget.min.js"></script>
import { FeedbackWidget } from '@sjforge/feedback-widget';
// Initialize the widget
FeedbackWidget.init({
projectId: 'my-project',
apiKey: 'fpk_xxxxx', // Get from the admin panel
});
// The floating button appears automatically
// Or submit programmatically:
await FeedbackWidget.submit({
type: 'bug',
priority: 'high',
title: 'Button not working',
description: 'The submit button does nothing when clicked',
});
<script src="https://cdn.sjforge.dev/feedback-widget.min.js"></script>
<script>
FeedbackWidget.init({
projectId: 'my-project',
apiKey: 'fpk_xxxxx',
});
</script>
import { FeedbackWidget, ElectronAdapter } from '@sjforge/feedback-widget';
FeedbackWidget.init({
projectId: 'my-app',
apiKey: 'fpk_xxxxx',
adapter: new ElectronAdapter(window.api),
});
See Electron Integration Guide for preload script setup.
Use the separate native package:
npm install @sjforge/feedback-widget-native
import { FeedbackWidget } from '@sjforge/feedback-widget-native';
FeedbackWidget.init({
projectId: 'my-app',
apiKey: 'fpk_xxxxx',
});
See @sjforge/feedback-widget-native for full documentation.
FeedbackWidget.init({
// Required
projectId: 'my-project',
apiKey: 'fpk_xxxxx',
// Optional API endpoint (defaults to https://feedback.sjforge.dev/api/widget)
apiUrl: 'https://your-portal.com/api/widget',
// Optional user info (attached to all submissions)
user: {
name: 'John Doe',
email: 'john@example.com',
},
// Custom context (sent with every submission)
customContext: {
subscription: 'premium',
version: '2.1.0',
},
// Privacy settings
privacy: {
maskSelectors: ['.sensitive-data', '.credit-card'],
blockSelectors: ['.do-not-record'],
autoMaskPasswords: true, // default: true
},
// UI customization
ui: {
position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
primaryColor: '#007bff',
showButton: true,
buttonText: 'Feedback',
},
// Feature flags
features: {
screenshots: true, // Enable screenshot capture
recording: false, // Enable session recording
captureConsoleErrors: true, // Capture console.error calls
captureNetworkErrors: true, // Capture failed fetch/XHR requests
},
// Recording options (when features.recording = true)
recording: {
maxDuration: 300000, // Max recording duration in ms (default: 5 min)
samplingRate: 'medium', // 'low' | 'medium' | 'high'
},
// Callbacks
onSubmitStart: () => console.log('Submitting...'),
onSubmitSuccess: (feedbackId) => console.log('Submitted:', feedbackId),
onSubmitError: (error) => console.error('Error:', error),
});
FeedbackWidget.init(config)Initialize the widget. Must be called before using other methods.
FeedbackWidget.submit(feedback)Submit feedback programmatically.
const result = await FeedbackWidget.submit({
type: 'bug', // 'bug' | 'feature' | 'design'
priority: 'high', // 'low' | 'medium' | 'high' | 'critical'
title: 'Short title',
description: 'Detailed description',
});
if (result.success) {
console.log('Feedback ID:', result.feedback_id);
} else {
console.error('Error:', result.error);
}
FeedbackWidget.open() / FeedbackWidget.close()Open or close the feedback form UI.
FeedbackWidget.setContext(context)Update custom context that gets sent with submissions.
FeedbackWidget.setContext({
userId: 'user-123',
page: 'checkout',
});
FeedbackWidget.getContext()Get the current context snapshot (for debugging).
FeedbackWidget.destroy()Destroy the widget instance and clean up resources.
FeedbackWidget.isInitialized()Returns true if the widget is initialized.
FeedbackWidget.getVersion()Get the SDK version string.
FeedbackWidget.captureScreenshot()Capture a screenshot of the current page.
const screenshot = await FeedbackWidget.captureScreenshot();
// Returns { dataUrl: string, width: number, height: number }
FeedbackWidget.submitWithScreenshot(feedback)Submit feedback with an automatically captured screenshot.
await FeedbackWidget.submitWithScreenshot({
type: 'bug',
priority: 'high',
title: 'UI Issue',
description: 'See attached screenshot',
});
FeedbackWidget.startRecording()Start a session recording.
FeedbackWidget.startRecording();
FeedbackWidget.stopRecording()Stop the current recording and return the data.
const recording = await FeedbackWidget.stopRecording();
// Returns { events: Event[], duration: number }
FeedbackWidget.isRecording()Check if a recording is in progress.
FeedbackWidget.submitWithRecording(feedback)Submit feedback with the current recording attached.
await FeedbackWidget.submitWithRecording({
type: 'bug',
priority: 'critical',
title: 'Workflow broken',
description: 'See attached recording',
});
FeedbackWidget.isOnline()Check if the widget has network connectivity.
FeedbackWidget.getPendingCount()Get the number of submissions waiting to sync.
const pending = await FeedbackWidget.getPendingCount();
console.log(`${pending} submissions waiting to sync`);
FeedbackWidget.syncOffline()Force sync offline submissions.
const { succeeded, failed } = await FeedbackWidget.syncOffline();
Screenshots are captured using html2canvas and can include annotations.
// Capture screenshot programmatically
const screenshot = await FeedbackWidget.captureScreenshot();
// Submit with screenshot
await FeedbackWidget.submitWithScreenshot({
type: 'bug',
priority: 'high',
title: 'UI Issue',
description: 'See attached screenshot',
});
The SDK includes a standalone annotation editor:
import { AnnotationEditor } from '@sjforge/feedback-widget';
const editor = new AnnotationEditor({
container: document.getElementById('editor'),
imageSrc: screenshotDataUrl,
onSave: (annotatedImageDataUrl) => {
console.log('Annotated image:', annotatedImageDataUrl);
},
onCancel: () => {
console.log('Cancelled');
},
});
// Get annotated image
const annotatedImage = editor.getAnnotatedImage();
// Clean up
editor.destroy();
Available annotation tools:
Session recording captures DOM events using rrweb for pixel-perfect replay in the admin portal.
FeedbackWidget.init({
projectId: 'my-project',
apiKey: 'fpk_xxxxx',
features: {
recording: true,
},
recording: {
maxDuration: 300000, // 5 minutes max
},
});
// Start recording
FeedbackWidget.startRecording();
// Check status
if (FeedbackWidget.isRecording()) {
console.log('Recording in progress...');
}
// Stop and submit
await FeedbackWidget.submitWithRecording({
type: 'bug',
priority: 'high',
title: 'See what happened',
description: 'Recording attached',
});
Recordings automatically respect privacy settings:
FeedbackWidget.init({
privacy: {
maskSelectors: ['.sensitive'], // Masked in recordings
blockSelectors: ['.private'], // Excluded from recordings
autoMaskPasswords: true, // Password inputs masked
},
});
<!-- Mask this element's content -->
<div data-feedback-mask>Sensitive content</div>
<!-- Completely exclude from capture -->
<div data-feedback-block>Private notes</div>
FeedbackWidget.init({
privacy: {
// CSS selectors to mask (shown as solid blocks)
maskSelectors: ['.credit-card', '.ssn-field'],
// CSS selectors to completely exclude
blockSelectors: ['.private-notes', '.admin-panel'],
// Auto-mask password fields (default: true)
autoMaskPasswords: true,
},
});
The widget automatically queues submissions when offline and syncs when connectivity returns.
// Check online status
if (!FeedbackWidget.isOnline()) {
console.log('Currently offline - submissions will queue');
}
// Check pending count
const pending = await FeedbackWidget.getPendingCount();
// Force sync attempt
const { succeeded, failed } = await FeedbackWidget.syncOffline();
console.log(`Synced: ${succeeded}, Failed: ${failed}`);
The widget automatically captures:
| Context | Description |
|---|---|
| User Agent | Browser and OS information |
| Viewport | Current window dimensions |
| Screen | Device screen size and pixel ratio |
| URL | Current page URL |
| Referrer | How user arrived at the page |
| Console Errors | Recent console.error calls |
| Network Errors | Failed fetch/XHR requests |
| Custom Context | Data you provide via setContext() |
npm install @sjforge/feedback-widget
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('feedbackAPI', {
// Screenshot capture
captureScreen: () => ipcRenderer.invoke('feedback:capture-screen'),
// Offline storage
storeOffline: (key: string, data: unknown) =>
ipcRenderer.invoke('feedback:store-offline', key, data),
getOffline: (key: string) =>
ipcRenderer.invoke('feedback:get-offline', key),
removeOffline: (key: string) =>
ipcRenderer.invoke('feedback:remove-offline', key),
// App info
getAppInfo: () => ipcRenderer.invoke('feedback:get-app-info'),
});
// main.ts
import { ipcMain, desktopCapturer, app } from 'electron';
import Store from 'electron-store';
const store = new Store({ name: 'feedback-widget' });
ipcMain.handle('feedback:capture-screen', async () => {
const sources = await desktopCapturer.getSources({
types: ['window'],
thumbnailSize: { width: 1920, height: 1080 },
});
const currentWindow = sources.find(s => s.name === 'Your App Name');
return currentWindow?.thumbnail.toDataURL();
});
ipcMain.handle('feedback:store-offline', (_, key, data) => {
store.set(key, data);
});
ipcMain.handle('feedback:get-offline', (_, key) => {
return store.get(key);
});
ipcMain.handle('feedback:remove-offline', (_, key) => {
store.delete(key);
});
ipcMain.handle('feedback:get-app-info', () => ({
name: app.getName(),
version: app.getVersion(),
}));
// renderer.ts
import { FeedbackWidget, ElectronAdapter } from '@sjforge/feedback-widget';
FeedbackWidget.init({
projectId: 'my-electron-app',
apiKey: 'fpk_xxxxx',
adapter: new ElectronAdapter(window.feedbackAPI),
});
For quick integration without a build step:
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<!-- Your app content -->
<script src="https://cdn.sjforge.dev/feedback-widget.min.js"></script>
<script>
FeedbackWidget.init({
projectId: 'my-project',
apiKey: 'fpk_xxxxx',
ui: {
position: 'bottom-right',
primaryColor: '#007bff',
},
});
</script>
</body>
</html>
Full TypeScript definitions are included:
import {
FeedbackWidget,
WidgetConfig,
FeedbackSubmission,
SubmissionResponse,
FeedbackType,
FeedbackPriority,
} from '@sjforge/feedback-widget';
const config: WidgetConfig = {
projectId: 'my-project',
apiKey: 'fpk_xxxxx',
};
FeedbackWidget.init(config);
| Import | Size (gzipped) |
|---|---|
| Core only | ~15 KB |
| + Screenshots | ~45 KB |
| + Recording | ~85 KB |
MIT
FAQs
In-app feedback widget SDK with screenshots, annotations, and session recording
The npm package @sjforge/feedback-widget receives a total of 1 weekly downloads. As such, @sjforge/feedback-widget popularity was classified as not popular.
We found that @sjforge/feedback-widget demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Feross Aboukhadijeh joins TBPN to discuss Socket's $60M Series C, 500%+ ARR growth, AI's impact on open source, and the rise in supply chain attacks.

Security News
OSV withdrew 157 OSV malware reports after automated false positives incorrectly flagged trusted npm and PyPI packages, sending bad records into tools that rely on OSV data.

Research
/Security News
TrapDoor crypto stealer hits 36 malicious packages across npm, PyPI, and Crates.io, targeting crypto, DeFi, AI, and security developers.