--- name: Customer Feedback Collection and Management description: Gathering, analyzing, and acting on user input through surveys, feedback widgets, rating systems, sentiment analysis, and integration with product roadmap to improve products and services. --- # Customer Feedback Collection and Management > **Current Level:** Intermediate > **Domain:** Customer Support / Product --- ## Overview Customer feedback collection enables businesses to gather, analyze, and act on user input to improve products and services. Effective feedback systems include multiple collection methods, sentiment analysis, categorization, routing, and integration with product development. --- ## Core Concepts ### Table of Contents 1. [Feedback Collection Methods](#feedback-collection-methods) 2. [Survey Design](#survey-design) 3. [Feedback Widgets](#feedback-widgets) 4. [Rating Systems](#rating-systems) 5. [Feedback Categorization](#feedback-categorization) 6. [Sentiment Analysis](#sentiment-analysis) 7. [Feedback Routing](#feedback-routing) 8. [Response Management](#response-management) 9. [Analytics and Reporting](#analytics-and-reporting) 10. [Integration with Product Roadmap](#integration-with-product-roadmap) 11. [Tools](#tools) 12. [Best Practices](#best-practices) --- ## Feedback Collection Methods ### In-App Surveys ```typescript interface InAppSurvey { id: string; name: string; type: 'nps' | 'csat' | 'ces' | 'custom'; trigger: SurveyTrigger; questions: SurveyQuestion[]; targeting: SurveyTargeting; status: 'draft' | 'active' | 'paused' | 'archived'; } interface SurveyTrigger { type: 'event' | 'time' | 'session_count' | 'feature_usage'; config: Record; } interface SurveyQuestion { id: string; type: 'rating' | 'text' | 'multiple_choice' | 'checkbox'; question: string; options?: string[]; required: boolean; min?: number; max?: number; } class InAppSurveyManager { constructor(private prisma: PrismaClient) {} /** * Create survey */ async createSurvey(survey: Omit): Promise { const created = await this.prisma.inAppSurvey.create({ data: { ...survey, status: 'draft', }, }); return created.id; } /** * Trigger survey */ async triggerSurvey( surveyId: string, userId: string, context?: Record ): Promise { const survey = await this.prisma.inAppSurvey.findUnique({ where: { id: surveyId }, include: { questions: true }, }); if (!survey || survey.status !== 'active') { return; } // Check targeting const eligible = await this.checkEligibility(survey, userId, context); if (!eligible) return; // Create survey instance const instance = await this.prisma.surveyInstance.create({ data: { surveyId, userId, context, status: 'pending', expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days }, }); // Send notification await this.sendSurveyNotification(userId, instance.id, survey); } /** * Check survey eligibility */ private async checkEligibility( survey: InAppSurvey, userId: string, context?: Record ): Promise { // Check user targeting if (survey.targeting.userSegments && survey.targeting.userSegments.length > 0) { const userSegments = await this.getUserSegments(userId); const hasRequiredSegment = survey.targeting.userSegments.some(segment => userSegments.includes(segment) ); if (!hasRequiredSegment) return false; } // Check trigger conditions if (survey.trigger.type === 'event') { const lastEvent = await this.getLastEvent(userId, survey.trigger.config.eventName); if (!lastEvent) return false; const eventAge = Date.now() - new Date(lastEvent.createdAt).getTime(); const minDelay = survey.trigger.config.minDelay || 0; if (eventAge < minDelay) return false; } // Check if already responded const existingResponse = await this.prisma.surveyResponse.findFirst({ where: { surveyId, userId, }, }); if (existingResponse) return false; return true; } /** * Submit survey response */ async submitResponse(params: { instanceId: string; userId: string; answers: Record; }): Promise { const instance = await this.prisma.surveyInstance.findUnique({ where: { id: params.instanceId }, include: { survey: true }, }); if (!instance || instance.status !== 'pending') { throw new Error('Survey instance not found or already completed'); } // Check expiry if (instance.expiresAt && instance.expiresAt < new Date()) { throw new Error('Survey has expired'); } // Save response await this.prisma.surveyResponse.create({ data: { instanceId: params.instanceId, userId: params.userId, answers: params.answers, submittedAt: new Date(), }, }); // Update instance await this.prisma.surveyInstance.update({ where: { id: params.instanceId }, data: { status: 'completed' }, }); // Trigger follow-up actions await this.handleSurveyResponse(instance.survey, params.answers); } /** * Handle survey response */ private async handleSurveyResponse( survey: any, answers: Record ): Promise { switch (survey.type) { case 'nps': await this.handleNPSResponse(survey, answers); break; case 'csat': await this.handleCSATResponse(survey, answers); break; case 'ces': await this.handleCESResponse(survey, answers); break; default: await this.handleCustomResponse(survey, answers); } } private async handleNPSResponse(survey: any, answers: Record): Promise { const score = answers['score']; const category = this.getNPSCategory(score); await this.prisma.npsScore.create({ data: { surveyId: survey.id, score, category, comment: answers['comment'], }, }); } private getNPSCategory(score: number): 'detractor' | 'passive' | 'promoter' { if (score >= 9) return 'promoter'; if (score >= 7) return 'passive'; return 'detractor'; } private async sendSurveyNotification( userId: string, instanceId: string, survey: any ): Promise { // Implement notification logic console.log(`Sending survey ${survey.id} to user ${userId}`); } private async getLastEvent(userId: string, eventName: string): Promise { // Implement event tracking return null; } private async getUserSegments(userId: string): Promise { // Implement user segmentation return []; } constructor(private prisma: PrismaClient) {} } ``` ### Email Surveys ```typescript class EmailSurveyManager { constructor(private prisma: PrismaClient) {} /** * Create email campaign */ async createCampaign(params: { name: string; subject: string; fromEmail: string; templateId: string; recipientIds: string[]; sendAt?: Date; }): Promise { const campaign = await this.prisma.emailSurveyCampaign.create({ data: { ...params, status: 'scheduled', }, }); // Schedule send if (params.sendAt) { await this.scheduleCampaignSend(campaign.id, params.sendAt); } else { await this.sendCampaign(campaign.id); } return campaign.id; } /** * Send campaign */ private async sendCampaign(campaignId: string): Promise { const campaign = await this.prisma.emailSurveyCampaign.findUnique({ where: { id: campaignId }, include: { recipients: true }, }); if (!campaign) { throw new Error('Campaign not found'); } // Get template const template = await this.prisma.surveyTemplate.findUnique({ where: { id: campaign.templateId }, }); // Send emails for (const recipient of campaign.recipients) { const surveyUrl = this.generateSurveyUrl(campaign.id, recipient.id); await emailService.send({ to: recipient.email, subject: campaign.subject, from: campaign.fromEmail, templateId: campaign.templateId, dynamicTemplateData: { surveyUrl, recipientName: recipient.name, }, }); } // Update campaign status await this.prisma.emailSurveyCampaign.update({ where: { id: campaignId }, data: { status: 'sent', sentAt: new Date(), }, }); } /** * Generate survey URL */ private generateSurveyUrl(campaignId: string, recipientId: string): string { const token = this.generateSurveyToken(campaignId, recipientId); return `${process.env.APP_URL}/survey/email/${campaignId}/${recipientId}?token=${token}`; } /** * Generate survey token */ private generateSurveyToken(campaignId: string, recipientId: string): string { const crypto = require('crypto'); return crypto .createHmac('sha256', process.env.SURVEY_SECRET!) .update(`${campaignId}:${recipientId}`) .digest('hex'); } /** * Schedule campaign send */ private async scheduleCampaignSend(campaignId: string, sendAt: Date): Promise { // Implement scheduling logic console.log(`Scheduling campaign ${campaignId} for ${sendAt}`); } } ``` --- ## Survey Design ### Survey Templates ```typescript interface SurveyTemplate { id: string; name: string; type: 'nps' | 'csat' | 'ces' | 'custom'; subject?: string; htmlContent: string; textContent?: string; variables: TemplateVariable[]; } interface TemplateVariable { name: string; description: string; type: 'text' | 'link' | 'rating'; required: boolean; } // NPS Template const npsTemplate: SurveyTemplate = { name: 'NPS Survey', type: 'nps', subject: 'How likely are you to recommend us?', htmlContent: `

How likely are you to recommend [Company Name] to a friend or colleague?

`, variables: [ { name: 'surveyUrl', description: 'Survey submission URL', type: 'link', required: true }, ], }; // CSAT Template const csatTemplate: SurveyTemplate = { name: 'CSAT Survey', type: 'csat', subject: 'How was your experience?', htmlContent: `

How would you rate your recent experience with us?

`, variables: [ { name: 'surveyUrl', description: 'Survey submission URL', type: 'link', required: true }, ], }; // CES Template const cesTemplate: SurveyTemplate = { name: 'CES Survey', type: 'ces', subject: 'Was it easy to accomplish your task?', htmlContent: `

How easy was it to accomplish your task?

`, variables: [ { name: 'surveyUrl', description: 'Survey submission URL', type: 'link', required: true }, ], }; ``` --- ## Feedback Widgets ### React Feedback Widget ```tsx import React, { useState } from 'react'; interface FeedbackWidgetProps { type: 'rating' | 'emoji' | 'thumbs'; position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; showOnLoad?: boolean; delay?: number; // in seconds onFeedback: (feedback: any) => void; } const FeedbackWidget: React.FC = ({ type, position = 'bottom-right', showOnLoad = false, delay = 30, onFeedback, }) => { const [visible, setVisible] = useState(showOnLoad); React.useEffect(() => { if (!showOnLoad && delay > 0) { const timer = setTimeout(() => setVisible(true), delay * 1000); return () => clearTimeout(timer); } }, [showOnLoad, delay]); const handleSubmit = (feedback: any) => { onFeedback(feedback); setVisible(false); }; const positionClasses: Record = { 'bottom-right': 'bottom-4 right-4', 'bottom-left': 'bottom-4 left-4', 'top-right': 'top-4 right-4', 'top-left': 'top-4 left-4', }; return (
{type === 'rating' && (

Rate your experience:

{[1, 2, 3, 4, 5].map(star => ( ))}
)} {type === 'emoji' && (

How was your experience?

)} {type === 'thumbs' && (

Was this helpful?

)}