Post Survey Processing - Done
Overview
PulseChecks are recurring surveys distributed on a scheduled basis to collect feedback. This document details the post-processing system that runs after each survey response is submitted, ensuring that data is properly calculated, stored, and ready for dashboard visualization.
Data Flow and Processing Levels
PulseCheck data is processed and stored at three distinct levels:
- Respondent Level - Individual survey responses and metrics
- Group Level - Aggregated data for defined groups (e.g., by company)
- Instance Level - Overall results for the entire survey period (e.g., January 2025)
Each survey period (instance) maintains its own set of metrics, calculations, and historical data.
Post-Processing Workflow
When a respondent completes a PulseCheck survey, the system performs a series of post-processing operations across all three data levels:
1. Respondent Level Processing
- Question Response Storage
- Store individual responses (positive, neutral, negative, N/A)
- Assign numerical scores (100, 50, 0, -1 respectively)
- Store comments and their sentiment analysis
- AI Sentiment Analysis
- Analyze comments using AI to determine sentiment (0-100 scale)
- Determine if the comment is positive or negative
- Store justification for the sentiment score
- Use sentiment scores to override default question scores when available
- Pulse Score Calculation
- Calculate overall Pulse score using weighted formula:
- 40% face ratings (positive/neutral/negative selections)
- 40% NPS rating (0-10 scale, converted to 0-100)
- 20% comment sentiment (when available)
- Record Previous Metrics
- Store previous Pulse score and NPS rating for trend calculations
2. Group Level Processing
If a PulseCheck is configured to aggregate by a specific group type (e.g., Company):
- Aggregate Response Data
- Calculate average scores for each question across group members
- Count positive, neutral, and negative responses by question
- Exclude N/A responses from all calculations
- Calculate Group Metrics
- Determine group Pulse score (average of member Pulse scores)
- Calculate group NPS score (% Promoters - % Detractors within the group)
- Store previous metrics for trend calculations
3. Instance Level Processing
- Overall Metrics
- Calculate instance Pulse score (average of all respondent Pulse scores)
- Determine instance NPS score (% Promoters - % Detractors)
- Calculate response rate percentage and trend indicator
- Question Statistics
- For each question, count positive, neutral, and negative responses
- Calculate average score per question
- Determine trend indicators (up, down, no change)
- Comment Metrics
- Count positive and negative comments
- Track unread comments
- Top 5 Statistical Categories
- Most Happy - Highest current Pulse scores
- Most Improved - Largest positive Pulse score change
- New Advocates - Shifted from ≤8 to ≥9 NPS rating
- Most At-Risk - Lowest current Pulse scores
- Most Declined - Largest negative Pulse score change
- Lost Advocates - Shifted from ≥9 to ≤8 NPS rating
Key Calculations
Pulse Score Calculation
The Pulse score is calculated as a weighted combination of three components:
- Face Rating Component (40% weight by default)
- NPS Component (40% weight by default)
- Comment Sentiment Component (20% weight by default)
If no comments are provided, the weights are redistributed proportionally.
NPS Calculation
Net Promoter Score is calculated as the percentage of promoters minus the percentage of detractors.
Trend Calculation
Trend indicators (up, down, or no change) are determined by comparing current values against previous values.
Group Aggregation Setup
- Administrators can define custom group types (e.g., Company, Department)
- Individual respondents are associated with specific groups
- Each PulseCheck can be configured to aggregate by one specific group type
- Aggregated scores use arithmetic means of individual scores
Special Handling
- N/A Responses: Stored but excluded from all calculations
- Empty Comments: Skipped during sentiment analysis
- First-Time Surveys: Trend calculations default to "no change"
Technical Details and Pseudocode
Tables Structure
PulseCheckInstance
Field | Type | Description |
|---|---|---|
id | primary key | |
name | string | e.g., "January 2025" |
pulseCheckId | foreign key | References PulseCheck configuration |
startDate | date | |
endDate | date | |
isActive | boolean | |
pulse_score | float (0-100) | |
previous_pulse_score | float (0-100) | |
nps_score | float (-100 to 100) | |
previous_nps_score | float (-100 to 100) | |
promoters_count | integer | |
passives_count | integer | |
detractors_count | integer | |
response_rate | float (percentage) | |
response_trend | enum: "up", "dn", "nc" | |
positive_comments_count | integer | |
negative_comments_count | integer | |
aggregation_group_type_id | foreign key, nullable | References group type for aggregation |
PulseCheckQuestionStats
Field | Type | Description |
|---|---|---|
id | primary key | |
pulseCheckInstanceId | foreign key | References PulseCheckInstance |
questionId | foreign key | References Question |
positive_count | integer | |
neutral_count | integer | |
negative_count | integer | |
average_score | float (0-100) | |
trend | enum: "up", "dn", "nc" | |
RespondentResult
Field | Type | Description |
|---|---|---|
id | primary key | |
pulseCheckInstanceId | foreign key | References PulseCheckInstance |
respondentId | foreign key | References Respondent |
completed | boolean | |
completedAt | timestamp | |
pulse_score | float (0-100) | |
previous_pulse_score | float (0-100) | |
nps_rating | integer (0-10) | |
previous_nps_rating | integer (0-10) | |
RespondentQuestionAnswer
Field | Type | Description |
|---|---|---|
id | primary key | |
respondentResultId | foreign key | References RespondentResult |
questionId | foreign key | References Question |
answer | enum: "positive", "neutral", | |
| "negative", "na", null | |
numerical_score | float (0-100, -1 for N/A, null) | |
comment | text, nullable | |
comment_sentiment | float (0-100), nullable | |
comment_is_positive | boolean, nullable | |
sentiment_justification | text, nullable | AI's explanation for sentiment score |
comment_read | boolean, default false | |
GroupResult
Field | Type | Description |
|---|---|---|
id | primary key | |
pulseCheckInstanceId | foreign key | References PulseCheckInstance |
groupId | foreign key | References Group |
groupTypeId | foreign key | References GroupType |
pulse_score | float (0-100) | |
previous_pulse_score | float (0-100) | |
nps_score | float (-100 to 100) | |
previous_nps_score | float (-100 to 100) | |
member_count | integer | |
GroupQuestionStats
Field | Type | Description |
|---|---|---|
id | primary key | |
groupResultId | foreign key | References GroupResult |
questionId | foreign key | References Question |
positive_count | integer | |
neutral_count | integer | |
negative_count | integer | |
average_score | float (0-100) | |
TopStatistics
Field | Type | Description |
|---|---|---|
id | primary key | |
pulseCheckInstanceId | foreign key | References PulseCheckInstance |
category | enum: "most_happy", "most_improved", | |
| "new_advocates", "most_at_risk", | |
| "most_declined", "lost_advocates" | |
entity_type | enum: "respondent", "group" | |
entity_id | foreign key | References respondent or group |
current_value | float | |
previous_value | float | |
difference | float | |
rank | integer (1-5) | |
Post-Processing Workflow
1. On Survey Completion
When a respondent completes a PulseCheck survey, execute the following steps:
// pseudocode
function processSurveyCompletion(respondentId, pulseCheckInstanceId):
// 1. Process Individual Responses
faceRatings = []
comments = []
foreach question in survey:
answer = getAnswerForQuestion(respondentId, question.id)
store answer in RespondentQuestionAnswer
if answer has comment:
// AI Sentiment Analysis takes the comment, question text, and face rating
// Returns three values:
// 1. sentiment score (0-100)
// 2. boolean indicating if comment is positive
// 3. justification text explaining the analysis
sentiment, isPositive, justification = performAISentimentAnalysis(
answer.comment,
getQuestionText(question.id),
answer === "positive" ? 100 : answer === "neutral" ? 50 : answer === "negative" ? 0 : -1
)
// Store all sentiment analysis results
store sentiment in RespondentQuestionAnswer.comment_sentiment
store isPositive in RespondentQuestionAnswer.comment_is_positive
store justification in RespondentQuestionAnswer.sentiment_justification
comments.append(answer.comment)
if answer is "positive":
numerical_score = 100
else if answer is "neutral":
numerical_score = 50
else if answer is "negative":
numerical_score = 0
else if answer is "na":
numerical_score = -1 // Store -1 to preserve N/A selection in records
if sentiment exists:
numerical_score = sentiment
update RespondentQuestionAnswer.numerical_score
// Only include non-N/A responses in pulse score calculation
if answer is not "na" and not null:
faceRatings.append(numerical_score)
// 2. Calculate Pulse Score
npsRating = getNPSRating(respondentId)
pulseScore = calculatePulseScore(faceRatings, npsRating, comments)
// 3. Update Respondent Result
previousPulseScore = getPreviousPulseScore(respondentId)
previousNPSRating = getPreviousNPSRating(respondentId)
update RespondentResult set:
completed = true
completedAt = current_timestamp
pulse_score = pulseScore
previous_pulse_score = previousPulseScore
nps_rating = npsRating
previous_nps_rating = previousNPSRating
// 4. Check for Group Aggregation
groupTypeId = getPulseCheckAggregationGroupType(pulseCheckInstanceId)
if groupTypeId is not null:
groupId = getRespondentGroupId(respondentId, groupTypeId)
if groupId is not null:
updateGroupResults(groupId, groupTypeId, pulseCheckInstanceId)
// 5. Update PulseCheck Instance Stats
updatePulseCheckInstanceStats(pulseCheckInstanceId)
// 6. Calculate Top Statistics
calculateTopStatistics(pulseCheckInstanceId)2. Group Results Processing
When updating group results:
// pseudocode
function updateGroupResults(groupId, groupTypeId, pulseCheckInstanceId):
// Get all completed respondent results for this group
respondentResults = getCompletedRespondentsInGroup(groupId, pulseCheckInstanceId)
if respondentResults.length == 0:
return
// Calculate aggregate metrics
sumPulseScore = 0
promoters = 0
passives = 0
detractors = 0
foreach respondent in respondentResults:
sumPulseScore += respondent.pulse_score
if respondent.nps_rating >= 9:
promoters++
else if respondent.nps_rating >= 7:
passives++
else:
detractors++
avgPulseScore = sumPulseScore / respondentResults.length
npsScore = (promoters * 100 / respondentResults.length) - (detractors * 100 / respondentResults.length)
// Get previous scores
previousGroupResult = getPreviousGroupResult(groupId, pulseCheckInstanceId)
previousPulseScore = previousGroupResult ? previousGroupResult.pulse_score : 0
previousNPSScore = previousGroupResult ? previousGroupResult.nps_score : 0
// Create or update GroupResult
upsert GroupResult set:
pulse_score = avgPulseScore
previous_pulse_score = previousPulseScore
nps_score = npsScore
previous_nps_score = previousNPSScore
member_count = respondentResults.length
// Process each question's stats for the group
foreach question in getQuestions(pulseCheckInstanceId):
positiveCount = 0
neutralCount = 0
negativeCount = 0
sumScore = 0
validAnswers = 0
foreach respondent in respondentResults:
answer = getRespondentQuestionAnswer(respondent.id, question.id)
// Explicitly omit N/A responses from all calculations
if answer is not null and answer is not "na":
if answer is "positive":
positiveCount++
else if answer is "neutral":
neutralCount++
else if answer is "negative":
negativeCount++
sumScore += answer.numerical_score
validAnswers++
avgScore = validAnswers > 0 ? sumScore / validAnswers : 0
upsert GroupQuestionStats set:
positive_count = positiveCount
neutral_count = neutralCount
negative_count = negativeCount
average_score = avgScore3. Instance Stats Processing
Update overall instance statistics:
// pseudocode
function updatePulseCheckInstanceStats(pulseCheckInstanceId):
// Get all completed respondent results
completedResults = getCompletedRespondentResults(pulseCheckInstanceId)
allInvitedRespondents = getAllInvitedRespondents(pulseCheckInstanceId)
// Note: Throughout this function, N/A responses (numerical_score = -1) are excluded from all calculations
// Calculate response rate
responseRate = (completedResults.length / allInvitedRespondents.length) * 100
// Get previous instance for trend comparison
previousInstance = getPreviousInstance(pulseCheckInstanceId)
previousResponseRate = previousInstance ? previousInstance.response_rate : 0
// Calculate response trend
if responseRate > previousResponseRate:
responseTrend = "up"
else if responseRate < previousResponseRate:
responseTrend = "dn"
else:
responseTrend = "nc"
// Calculate NPS breakdown
promoters = 0
passives = 0
detractors = 0
foreach result in completedResults:
if result.nps_rating >= 9:
promoters++
else if result.nps_rating >= 7:
passives++
else:
detractors++
// Calculate NPS
npsScore = promoters > 0 || detractors > 0 ?
((promoters * 100 / completedResults.length) - (detractors * 100 / completedResults.length)) : 0
// Calculate Pulse
sumPulse = 0
foreach result in completedResults:
sumPulse += result.pulse_score
avgPulse = completedResults.length > 0 ? sumPulse / completedResults.length : 0
// Comment counts
positiveComments = countPositiveComments(pulseCheckInstanceId)
negativeComments = countNegativeComments(pulseCheckInstanceId)
// Previous scores
previousPulse = previousInstance ? previousInstance.pulse_score : 0
previousNPS = previousInstance ? previousInstance.nps_score : 0
// Update instance stats
update PulseCheckInstance set:
pulse_score = avgPulse
previous_pulse_score = previousPulse
nps_score = npsScore
previous_nps_score = previousNPS
promoters_count = promoters
passives_count = passives
detractors_count = detractors
response_rate = responseRate
response_trend = responseTrend
positive_comments_count = positiveComments
negative_comments_count = negativeComments
// Process each question's stats
foreach question in getQuestions(pulseCheckInstanceId):
positiveCount = 0
neutralCount = 0
negativeCount = 0
sumScore = 0
validAnswers = 0
foreach result in completedResults:
answer = getRespondentQuestionAnswer(result.id, question.id)
if answer is not null and answer is not "na":
if answer is "positive":
positiveCount++
else if answer is "neutral":
neutralCount++
else if answer is "negative":
negativeCount++
sumScore += answer.numerical_score
validAnswers++
avgScore = validAnswers > 0 ? sumScore / validAnswers : 0
// Get previous stats for trend
previousStats = getPreviousQuestionStats(question.id, pulseCheckInstanceId)
previousAvg = previousStats ? previousStats.average_score : 0
// Calculate trend
if avgScore > previousAvg:
trend = "up"
else if avgScore < previousAvg:
trend = "dn"
else:
trend = "nc"
upsert PulseCheckQuestionStats set:
positive_count = positiveCount
neutral_count = neutralCount
negative_count = negativeCount
average_score = avgScore
trend = trend4. Top Statistics Calculation
Calculate the top 5 statistics for each category:
// pseudocode
function calculateTopStatistics(pulseCheckInstanceId):
// Clear previous top statistics for this instance
deleteTopStatistics(pulseCheckInstanceId)
// Get aggregation type
groupTypeId = getPulseCheckAggregationGroupType(pulseCheckInstanceId)
// Prepare data array that will hold respondents and/or groups
entities = []
// Get all completed respondent results
completedResults = getCompletedRespondentResults(pulseCheckInstanceId)
// Add respondents to entities array
foreach result in completedResults:
// If we're aggregating by group, only include respondents without a group
if groupTypeId is null or getRespondentGroupId(result.respondentId, groupTypeId) is null:
entities.push({
type: "respondent",
id: result.respondentId,
current_pulse: result.pulse_score,
previous_pulse: result.previous_pulse_score,
current_nps: result.nps_rating,
previous_nps: result.previous_nps_rating
})
// If aggregating by group, add groups to entities array
if groupTypeId is not null:
groupResults = getGroupResults(pulseCheckInstanceId)
foreach groupResult in groupResults:
entities.push({
type: "group",
id: groupResult.groupId,
current_pulse: groupResult.pulse_score,
previous_pulse: groupResult.previous_pulse_score,
current_nps: groupResult.nps_score,
previous_nps: groupResult.previous_nps_score
})
// Most Happy (highest current pulse score)
mostHappy = entities.sort((a, b) => b.current_pulse - a.current_pulse).slice(0, 5)
for i = 0; i < mostHappy.length; i++:
insert into TopStatistics:
category = "most_happy"
entity_type = mostHappy[i].type
entity_id = mostHappy[i].id
current_value = mostHappy[i].current_pulse
previous_value = mostHappy[i].previous_pulse
difference = mostHappy[i].current_pulse - mostHappy[i].previous_pulse
rank = i + 1
// Most Improved (largest positive pulse difference)
mostImproved = entities
.filter(e => e.previous_pulse > 0) // Ensure there's a previous score to compare with
.map(e => ({...e, diff: e.current_pulse - e.previous_pulse}))
.sort((a, b) => b.diff - a.diff)
.slice(0, 5)
for i = 0; i < mostImproved.length; i++:
insert into TopStatistics:
category = "most_improved"
entity_type = mostImproved[i].type
entity_id = mostImproved[i].id
current_value = mostImproved[i].current_pulse
previous_value = mostImproved[i].previous_pulse
difference = mostImproved[i].diff
rank = i + 1
// New Advocates (was ≤8 NPS, now ≥9, sorted by pulse)
newAdvocates = entities
.filter(e => (e.type == "respondent" && e.previous_nps <= 8 && e.current_nps >= 9) ||
(e.type == "group" && e.previous_nps <= 0 && e.current_nps > 0))
.sort((a, b) => b.current_pulse - a.current_pulse)
.slice(0, 5)
for i = 0; i < newAdvocates.length; i++:
insert into TopStatistics:
category = "new_advocates"
entity_type = newAdvocates[i].type
entity_id = newAdvocates[i].id
current_value = newAdvocates[i].current_pulse
previous_value = newAdvocates[i].previous_pulse
difference = newAdvocates[i].current_nps - newAdvocates[i].previous_nps
rank = i + 1
// Most At-Risk (lowest current pulse score)
mostAtRisk = entities.sort((a, b) => a.current_pulse - b.current_pulse).slice(0, 5)
for i = 0; i < mostAtRisk.length; i++:
insert into TopStatistics:
category = "most_at_risk"
entity_type = mostAtRisk[i].type
entity_id = mostAtRisk[i].id
current_value = mostAtRisk[i].current_pulse
previous_value = mostAtRisk[i].previous_pulse
difference = mostAtRisk[i].current_pulse - mostAtRisk[i].previous_pulse
rank = i + 1
// Most Declined (largest negative pulse difference)
mostDeclined = entities
.filter(e => e.previous_pulse > 0) // Ensure there's a previous score to compare with
.map(e => ({...e, diff: e.previous_pulse - e.current_pulse}))
.filter(e => e.diff > 0) // Only include those that actually declined
.sort((a, b) => b.diff - a.diff)
.slice(0, 5)
for i = 0; i < mostDeclined.length; i++:
insert into TopStatistics:
category = "most_declined"
entity_type = mostDeclined[i].type
entity_id = mostDeclined[i].id
current_value = mostDeclined[i].current_pulse
previous_value = mostDeclined[i].previous_pulse
difference = -mostDeclined[i].diff // Store as negative value
rank = i + 1
// Lost Advocates (was ≥9 NPS, now ≤8, sorted by lowest pulse)
lostAdvocates = entities
.filter(e => (e.type == "respondent" && e.previous_nps >= 9 && e.current_nps <= 8) ||
(e.type == "group" && e.previous_nps > 0 && e.current_nps <= 0))
.sort((a, b) => a.current_pulse - b.current_pulse)
.slice(0, 5)
for i = 0; i < lostAdvocates.length; i++:
insert into TopStatistics:
category = "lost_advocates"
entity_type = lostAdvocates[i].type
entity_id = lostAdvocates[i].id
current_value = lostAdvocates[i].current_pulse
previous_value = lostAdvocates[i].previous_pulse
difference = lostAdvocates[i].current_nps - lostAdvocates[i].previous_nps
rank = i + 1Key Calculations
Pulse Score Calculation
// pseudocode
function calculatePulseScore(faceRatings, npsScore, comments) {
// 1. Calculate Face Score
let faceSum = 0;
let faceCount = 0;
for (const rating of faceRatings) {
// Skip null and N/A (-1) ratings
if (rating !== null && rating !== -1) {
faceSum += rating;
faceCount++;
}
}
const faceScore = faceCount > 0 ? faceSum / faceCount : 0;
// 2. Calculate NPS Score
let npsScore100;
switch (npsScore) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6:
npsScore100 = 0;
break;
case 7: case 8:
npsScore100 = 50;
break;
case 9: case 10:
npsScore100 = 100;
break;
default:
npsScore100 = 0; // Handle invalid input
}
// 3. Calculate Comment Sentiment Score
let commentSentimentScore = 0;
let commentCount = 0;
if (comments.length > 0) {
for (const comment of comments) {
const sentiment = analyzeSentiment(comment); // External function
if (sentiment !== null) {
commentSentimentScore += sentiment;
commentCount++;
}
}
commentSentimentScore = commentCount > 0 ? commentSentimentScore / commentCount : 0;
}
// 4. Calculate Overall Pulse Score (with Re-weighting)
const wf = 0.4;
const wn = 0.4;
let wc = 0.2;
let newWf, newWn;
if (commentCount === 0) {
wc = 0;
newWf = wf / (wf + wn);
newWn = wn / (wf + wn);
} else {
newWf = wf;
newWn = wn;
}
const pulseScore = (newWf * faceScore) + (newWn * npsScore100) + (wc * commentSentimentScore);
return pulseScore;
}NPS Calculation
// pseudocode
function calculateNPS(promotersCount, passivesCount, detractorsCount) {
const totalRespondents = promotersCount + passivesCount + detractorsCount;
if (totalRespondents === 0) {
return 0;
}
const promotersPercentage = (promotersCount / totalRespondents) * 100;
const detractorsPercentage = (detractorsCount / totalRespondents) * 100;
return promotersPercentage - detractorsPercentage;
}Indexes and Optimization
To ensure optimal performance, create the following indexes:
- RespondentResult table:
- pulseCheckInstanceId, respondentId (composite)
- completed (for filtering completed responses)
- RespondentQuestionAnswer table:
- respondentResultId, questionId (composite)
- GroupResult table:
- pulseCheckInstanceId, groupId (composite)
- groupTypeId (for filtering by group type)
- PulseCheckQuestionStats table:
- pulseCheckInstanceId, questionId (composite)
- TopStatistics table:
- pulseCheckInstanceId, category (composite)
Transaction Management
The post-processing operations should be wrapped in database transactions to ensure data consistency:
- When processing an individual survey completion, wrap all database operations in a transaction
- When updating group results, use a transaction
- When updating instance statistics, use a transaction
- When calculating top statistics, use a transaction
This ensures that if any part of the process fails, the entire operation is rolled back, maintaining data integrity.
AI Sentiment Analysis Integration
The sentiment analysis function should:
- Accept a comment string, question text, and face rating as input
- Process using the prompt template defined below
- Return three distinct values:
- A sentiment score between 0-100 (or null if sentiment cannot be determined)
- A boolean flag indicating if the comment is positive (true) or negative (false)
- A justification paragraph explaining the analysis reasoning
- Include error handling for API failures or timeout situations
- Cache results to avoid redundant processing of identical comments
Sentiment Analysis Prompt
You are tasked with analyzing customer sentiment of a comment associated with a survey question. Your goal is to provide a sentiment score between 0 and 100, where 0 represents extremely negative sentiment, 50 represents neutral sentiment, and 100 represents extremely positive sentiment.
Here is the customer comment you need to analyze:
<survey-question>
<question>{{question_text}}</question>
<face-rating>{{face_rating}}</face-rating>
<comment>
{{comment}}
</comment>
</survey-question>
Carefully read and consider the comment above. Pay attention to the words used, the overall tone, and any specific positive or negative expressions.
To determine the sentiment score, follow these guidelines:
1. Identify key positive and negative words or phrases
2. Consider the overall context and tone of the comment
3. Evaluate any specific complaints or praises
4. Assess the intensity of the sentiment expressed
5. Take note of the face rating, but prioritize the actual content of the comment
Before providing the final score, explain your reasoning for the sentiment analysis. Consider the following questions in your justification:
- What specific words or phrases influenced your analysis?
- Is the overall tone primarily positive, negative, or neutral?
- Are there any mixed sentiments present?
- How intense is the sentiment expressed?
Provide your justification and final score in the following format:
<justification>
[Write your reasoning here, explaining how you arrived at the sentiment score]
</justification>
<score>
[Provide only the numeric score between 0 and 100, or null if sentiment cannot be determined]
</score>
<is_positive>
[Provide "true" if the sentiment is generally positive (score > 50), "false" if the sentiment is generally negative (score <= 50), or "null" if sentiment cannot be determined]
</is_positive>
Remember, if the sentiment cannot be determined from the given comment, return null as the score and is_positive value.Expected Return Format
{
score: 78, // Numerical score 0-100
isPositive: true, // Boolean indicating positive/negative sentiment
justification: "The comment uses positive language like 'excellent' and expresses satisfaction with the response time. There are no negative elements in the feedback."
}Sample Implementation
// pseudocode
async function performAISentimentAnalysis(comment, questionText, faceRating) {
if (!comment || comment.trim() === '') {
return { score: null, isPositive: null, justification: null };
}
try {
// Prepare the prompt with the actual values
const prompt = sentimentAnalysisPrompt
.replace('{{question_text}}', questionText || 'Not provided')
.replace('{{face_rating}}', getFaceRatingText(faceRating) || 'Not provided')
.replace('{{comment}}', comment);
// Call the AI service with the prepared prompt
const response = await aiService.analyze(prompt);
// Extract the relevant parts from the response
const justification = extractBetweenTags(response, 'justification');
const scoreText = extractBetweenTags(response, 'score');
const isPositiveText = extractBetweenTags(response, 'is_positive');
// Parse the score and isPositive values
let score = null;
if (scoreText && scoreText.toLowerCase() !== 'null') {
score = parseFloat(scoreText);
if (isNaN(score)) score = null;
}
let isPositive = null;
if (isPositiveText && isPositiveText.toLowerCase() !== 'null') {
isPositive = isPositiveText.toLowerCase() === 'true';
}
return { score, isPositive, justification };
} catch (error) {
console.error('Sentiment analysis failed:', error);
return { score: null, isPositive: null, justification: null };
}
}
// Helper function to extract content between XML tags
function extractBetweenTags(text, tagName) {
const openTag = `<${tagName}>`;
const closeTag = `</${tagName}>`;
const startIndex = text.indexOf(openTag) + openTag.length;
const endIndex = text.indexOf(closeTag);
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
return null;
}
return text.substring(startIndex, endIndex).trim();
}
// Helper function to convert numerical face rating to descriptive text
function getFaceRatingText(rating) {
if (rating === 100) return "Positive (Happy Face)";
if (rating === 50) return "Neutral (Meh Face)";
if (rating === 0) return "Negative (Unhappy Face)";
if (rating === -1) return "N/A";
return null;
}Notes for Implementation
- The system should handle respondents who don't complete all questions
- N/A Responses: When a respondent selects "N/A" for a question:
- The response must be stored with answer = "na" and numerical_score = -1
- This preserves the N/A response for displaying individual survey results
- However, N/A responses are completely excluded from all statistical calculations:
- They do not contribute to the face rating component of Pulse score
- They are excluded from question statistics (positive/neutral/negative counts)
- They are excluded from averages and group aggregations
- Dashboard and reporting views should indicate when questions have N/A responses
- For surveys without previous instances, trend calculations should default to "nc" (no change)
- Empty comments should be skipped during sentiment analysis
- When recalculating instance or group statistics, consider implementing a queue system for large datasets
- Add logging at key points to facilitate debugging and monitoring
- Implement validation to prevent duplicate processing of the same survey submission