In Chapter 3: Survey Lifecycle State Machine, we built the logic that handles the survey once it is open (e.g., transitioning from Rating to Thank You).
But we are missing the most important question: When should the survey open?
If we asked the user for feedback after every single message, they would quickly uninstall our tool. We need a system that decides the perfect moment to interrupt.
Imagine a waiter at a restaurant. A polite waiter follows specific rules before approaching your table:
In our code, useFeedbackSurvey is that waiter. It checks clocks, counts messages, and rolls dice to decide if it's time to show the Main UI Controller.
First, we define the rules. These are stored in a configuration object. This allows us to tweak the "politeness" of the app without rewriting code.
Here are the key settings we care about:
type FeedbackSurveyConfig = {
// Wait 10 minutes (600,000ms) after app start before first survey
minTimeBeforeFeedbackMs: number;
// Wait 60 minutes between surveys in the same session
minTimeBetweenFeedbackMs: number;
// User must send at least 5 messages before we ask
minUserTurnsBeforeFeedback: number;
// Only show to 0.5% of eligible users (Probabilistic)
probability: number;
};
By changing these numbers, we can make the waiter very aggressive or very shy.
To enforce these rules, the system needs to remember what happened in the past. We use React state to track when the survey was last seen.
// Inside useFeedbackSurvey.tsx
const [feedbackSurvey, setFeedbackSurvey] = useState({
// Timestamp of the last time we bothered the user
timeLastShown: null,
// How many messages they had sent at that time
submitCountAtLastAppearance: null
});
We also track when the current session started:
// When did the user open the app?
const sessionStartTime = useRef(Date.now());
shouldOpen)
The heart of this chapter is a function called shouldOpen. It runs every time something changes in the app (like a new message arriving) and returns true or false.
It acts like a checklist. If any check fails, the survey stays hidden.
If the AI is currently typing (isLoading) or the user is looking at another prompt, we should stay quiet.
if (isLoading) {
return false;
}
// Don't overlay on top of other prompts
if (hasActivePrompt) {
return false;
}
We check the clock. If it hasn't been long enough since the app started, we abort.
const timeSinceStart = Date.now() - sessionStartTime.current;
// If we haven't hit the 10-minute mark yet...
if (timeSinceStart < config.minTimeBeforeFeedbackMs) {
return false;
}
We don't want to survey someone who just opened the app and hasn't done anything. We check the submitCount (number of messages sent).
// If user has sent fewer than 5 messages...
if (submitCount < config.minUserTurnsBeforeFeedback) {
return false;
}
Finally, even if the time is right and the user is active, we don't want to show the survey every time. We might only want to sample 0.5% of interactions.
// Generate a random number between 0 and 1
const randomRoll = Math.random();
// If roll is greater than 0.005, don't show
if (randomRoll > config.probability) {
return false;
}
return true; // All checks passed! Open the survey.
Let's visualize the decision process when a user sends a message.
Scenario: The user has been using the app for 15 minutes and just sent their 6th message. The config requires 10 minutes and 5 messages.
Once the logic decides to return true, the survey opens. Immediately after opening, we must update our records so we don't ask again 5 seconds later.
We use a callback onOpen to update the state we defined earlier.
const onOpen = useCallback(() => {
// Update the state with current time and count
updateLastShownTime(
Date.now(), // Current Time
submitCountRef.current // Current Message Count
);
// Log analytics that the survey appeared
logEvent('feedback_survey_appeared', { ... });
}, []);
Now, the next time shouldOpen runs, it will calculate time relative to this timestamp, effectively resetting the timer.
There is one advanced detail: Global Pacing.
If a user closes the app and opens it again immediately, sessionStartTime resets. If we only checked session time, we might annoy them again.
To solve this, we store a timestamp in a global configuration file on the user's computer.
// Inside useFeedbackSurvey.tsx
// Read from disk
const globalState = getGlobalConfig().feedbackSurveyState;
if (globalState?.lastShownTime) {
const timeSinceGlobal = Date.now() - globalState.lastShownTime;
// e.g. Don't show if they saw one in the last 24 hours
if (timeSinceGlobal < config.minTimeBetweenGlobalFeedbackMs) {
return false;
}
}
This ensures our "Polite Waiter" remembers you even if you leave the restaurant and come back the next day.
In this chapter, we implemented the General Pacing and Configuration.
This system runs automatically in the background. However, sometimes we want to break these rules. What if a specific event happensβlike the AI crashes or produces an errorβand we want to ask about that specific moment?
For that, we need Event-Driven Triggers.
Next Chapter: Event-Driven Survey Triggers
Generated by Code IQ