mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(feedback): gate onboarding survey on first processing, add prompt lifecycle events (#615)
Defers the onboarding usage survey to the instance's first successful processing (the worker writes a one-time onboarding.firstProcessedAt marker and the overlay gates on it), so it reaches engaged users instead of first-landing visitors. Replaces the two questions telemetry already answers (modality preference from tool_used, install method from instance_started) with what it can't infer: prior tool, self-host motivation, and discovery source. Adds feedback_prompt_shown and feedback_prompt_dismissed on all five feedback surfaces (usage survey, per-job prompt, admin install card, global nav dialog, search-miss) so skip and completion rates are measurable, not just submissions. New survey strings translated into all 20 non-English locales.
This commit is contained in:
@@ -1,21 +1,29 @@
|
||||
import type {
|
||||
FeedbackDiscoverySource,
|
||||
FeedbackErrorCategory,
|
||||
FeedbackFrictionArea,
|
||||
FeedbackImportantArea,
|
||||
FeedbackInstallMethod,
|
||||
FeedbackPriorTool,
|
||||
FeedbackSelfHostMotivation,
|
||||
FeedbackSentiment,
|
||||
FeedbackSource,
|
||||
FeedbackSurveyId,
|
||||
FeedbackType,
|
||||
FeedbackUsageType,
|
||||
} from "@snapotter/shared";
|
||||
import { ANALYTICS_EVENTS, ONBOARDING_FIRST_PROCESSED_KEY } from "@snapotter/shared";
|
||||
import { track } from "@/lib/analytics";
|
||||
import { apiPost } from "@/lib/api";
|
||||
|
||||
export type {
|
||||
FeedbackDiscoverySource,
|
||||
FeedbackErrorCategory,
|
||||
FeedbackFrictionArea,
|
||||
FeedbackImportantArea,
|
||||
FeedbackInstallMethod,
|
||||
FeedbackPriorTool,
|
||||
FeedbackSelfHostMotivation,
|
||||
FeedbackSentiment,
|
||||
FeedbackSource,
|
||||
FeedbackSurveyId,
|
||||
@@ -50,6 +58,9 @@ export interface FeedbackPayload {
|
||||
usageType?: FeedbackUsageType;
|
||||
importantAreas?: FeedbackImportantArea[];
|
||||
frictionArea?: FeedbackFrictionArea;
|
||||
priorTool?: FeedbackPriorTool;
|
||||
selfHostMotivation?: FeedbackSelfHostMotivation;
|
||||
discoverySource?: FeedbackDiscoverySource;
|
||||
errorCategory?: FeedbackErrorCategory;
|
||||
}
|
||||
|
||||
@@ -100,6 +111,35 @@ export function promptVariantForSource(source: FeedbackSource): FeedbackPromptVa
|
||||
}
|
||||
}
|
||||
|
||||
/** How a feedback prompt was dismissed, for the feedback_prompt_dismissed event. */
|
||||
export type FeedbackDismissKind = "close" | "dont_ask_again" | "snooze";
|
||||
|
||||
/**
|
||||
* Fire when a feedback surface becomes visible. Paired with
|
||||
* trackFeedbackPromptDismissed and the server-side submit event, this gives skip
|
||||
* and completion rates a denominator instead of counting only submissions.
|
||||
*/
|
||||
export function trackFeedbackPromptShown(source: FeedbackSource): void {
|
||||
track(ANALYTICS_EVENTS.FEEDBACK_PROMPT_SHOWN, {
|
||||
source,
|
||||
survey_id: surveyIdForSource(source),
|
||||
prompt_variant: promptVariantForSource(source),
|
||||
});
|
||||
}
|
||||
|
||||
/** Fire when a feedback surface is dismissed without submitting. */
|
||||
export function trackFeedbackPromptDismissed(
|
||||
source: FeedbackSource,
|
||||
dismissKind: FeedbackDismissKind,
|
||||
): void {
|
||||
track(ANALYTICS_EVENTS.FEEDBACK_PROMPT_DISMISSED, {
|
||||
source,
|
||||
survey_id: surveyIdForSource(source),
|
||||
prompt_variant: promptVariantForSource(source),
|
||||
dismiss_kind: dismissKind,
|
||||
});
|
||||
}
|
||||
|
||||
export function classifyFeedbackError(message: string | null | undefined): FeedbackErrorCategory {
|
||||
const value = (message ?? "").toLowerCase();
|
||||
if (!value) return "unknown";
|
||||
@@ -146,6 +186,11 @@ export function shouldShowUsageSurvey({
|
||||
analyticsEnabled,
|
||||
}: UsageSurveyVisibilityOptions): boolean {
|
||||
if (!analyticsConfigLoaded || !analyticsEnabled || role !== "admin") return false;
|
||||
// Hold the survey until the instance has completed its first processing (the
|
||||
// worker writes this marker on the first successful job). Asking on an empty
|
||||
// first-landing app yields answers from users who haven't used the product;
|
||||
// waiting for one real result reaches an engaged admin instead.
|
||||
if (!settings[ONBOARDING_FIRST_PROCESSED_KEY]) return false;
|
||||
return (
|
||||
!settings["onboarding.usageSurvey.answeredAt"] &&
|
||||
!settings["onboarding.usageSurvey.dismissedAt"]
|
||||
|
||||
Reference in New Issue
Block a user