feat: add PostHog customer feedback

This commit is contained in:
SnapOtter
2026-06-29 18:16:33 +08:00
parent 6f85b3d12a
commit 649e65b035
47 changed files with 3937 additions and 4 deletions
+142
View File
@@ -0,0 +1,142 @@
import { apiPost } from "@/lib/api";
export type FeedbackSource = "global" | "tool_result" | "failed_job" | "admin_installer";
export type FeedbackSurveyId =
| "global-feedback-v1"
| "tool-result-v1"
| "failed-job-v1"
| "admin-install-v1";
export type FeedbackPromptVariant =
| "nav-v1"
| "inline-v1"
| "failed-button-v1"
| "settings-card-v1";
export type FeedbackSentiment = "great" | "okay" | "issue" | "missing" | "bug" | "idea" | "other";
export type FeedbackType = "bug" | "feature_request" | "confusing_ux" | "performance" | "other";
export type FeedbackInstallMethod = "docker" | "docker_compose" | "source" | "cloud" | "other";
export type FeedbackUsageType =
| "personal"
| "team_internal"
| "business_workflow"
| "education"
| "evaluating";
export type FeedbackImportantArea =
| "images"
| "pdf_docs"
| "video_audio"
| "batch_workflows"
| "ai_tools";
export type FeedbackFrictionArea =
| "smooth"
| "docker"
| "environment_variables"
| "auth"
| "storage"
| "workers"
| "ai_tools"
| "docs"
| "performance"
| "other";
export type FeedbackErrorCategory =
| "validation_error"
| "upload_error"
| "processing_error"
| "timeout"
| "unsupported_format"
| "worker_unavailable"
| "unknown";
export interface FeedbackPayload {
source: FeedbackSource;
surveyId?: FeedbackSurveyId;
promptVariant?: FeedbackPromptVariant;
sentiment?: FeedbackSentiment;
feedbackType?: FeedbackType;
message?: string;
contactOk?: boolean;
contactEmail?: string;
contactName?: string;
company?: string;
toolId?: string;
jobStatus?: "completed" | "failed";
installMethod?: FeedbackInstallMethod;
usageType?: FeedbackUsageType;
importantAreas?: FeedbackImportantArea[];
frictionArea?: FeedbackFrictionArea;
errorCategory?: FeedbackErrorCategory;
}
export interface FeedbackResponse {
ok: boolean;
accepted: boolean;
}
interface InstallFeedbackVisibilityOptions {
settings: Record<string, string>;
role: string | null;
analyticsConfigLoaded: boolean;
analyticsEnabled: boolean;
now?: number;
}
export function surveyIdForSource(source: FeedbackSource): FeedbackSurveyId {
switch (source) {
case "tool_result":
return "tool-result-v1";
case "failed_job":
return "failed-job-v1";
case "admin_installer":
return "admin-install-v1";
case "global":
return "global-feedback-v1";
}
}
export function promptVariantForSource(source: FeedbackSource): FeedbackPromptVariant {
switch (source) {
case "tool_result":
return "inline-v1";
case "failed_job":
return "failed-button-v1";
case "admin_installer":
return "settings-card-v1";
case "global":
return "nav-v1";
}
}
export function classifyFeedbackError(message: string | null | undefined): FeedbackErrorCategory {
const value = (message ?? "").toLowerCase();
if (!value) return "unknown";
if (value.includes("timed out") || value.includes("timeout")) return "timeout";
if (value.includes("upload") || value.includes("interrupted")) return "upload_error";
if (value.includes("validation") || value.includes("invalid") || value.includes("required")) {
return "validation_error";
}
if (value.includes("unsupported")) return "unsupported_format";
if (value.includes("worker") || value.includes("queue")) return "worker_unavailable";
return "processing_error";
}
export function shouldShowInstallFeedbackCard({
settings,
role,
analyticsConfigLoaded,
analyticsEnabled,
now = Date.now(),
}: InstallFeedbackVisibilityOptions): boolean {
if (!analyticsConfigLoaded || !analyticsEnabled || role !== "admin") return false;
if (settings["feedback.install.submittedAt"] || settings["feedback.install.dismissedAt"]) {
return false;
}
const snoozedUntil = settings["feedback.install.snoozedUntil"];
if (!snoozedUntil) return true;
const parsedSnooze = Date.parse(snoozedUntil);
return !Number.isFinite(parsedSnooze) || parsedSnooze <= now;
}
export async function submitFeedback(payload: FeedbackPayload): Promise<FeedbackResponse> {
return apiPost<FeedbackResponse>("/v1/feedback", payload);
}